弹性搜索嵌套 Insert/Update

ElasticSearch Nest Insert/Update

我使用以下查询在 elastic 中创建了一个索引:

PUT public_site
{
  "mappings": {
    "page": {
      "properties": {
        "url": {
          "type": "string"
        },
        "title":{
          "type": "string"
        },
        "body":{
          "type": "string"
        },
        "meta_description":{
          "type": "string"
        },
        "keywords":{
          "type": "string"
        },
        "category":{
          "type": "string"
        },
        "last_updated_date":{
          "type": "date"
        },
        "source_id":{
        "type":"string"
        }
      }
    }
  }
}

我想使用 .net NEST 库将文档插入到该索引中。我的问题是 .net 更新方法的签名对我来说没有任何意义。

client.Update<TDocument>(IUpdateRequest<TDocument,TPartialDocument>)

Java 库对我来说更有意义:

UpdateRequest updateRequest = new UpdateRequest();
updateRequest.index("index");
updateRequest.type("type");
updateRequest.id("1");
updateRequest.doc(jsonBuilder()
        .startObject()
            .field("gender", "male")
        .endObject());
client.update(updateRequest).get();

在 NEST 中,TDocumentTPartialDocument 类 来自哪里? 我制作的这些 C# 类 是否代表我的索引?

TDocumentTPartialDocument

的 POCO 类型的通用类型参数
  • 表示 Elasticsearch 中的文档 (TDocument) 和
  • 执行部分更新时,Elasticsearch (TPartialDocument) 中部分文档的表示。

在完全更新的情况下,TDocumentTPartialDocument 可能指的是相同的具体 POCO 类型。让我们看一些例子来证明。

让我们用您在上面定义的映射创建一个索引。首先,我们可以使用 POCO 类型

表示文档
public class Page
{
    public string Url { get; set; }

    public string Title { get; set; }

    public string Body { get; set; }

    [String(Name="meta_description")]
    public string MetaDescription { get; set; }

    public IList<string> Keywords { get; set; }

    public string Category { get; set; }

    [Date(Name="last_updated_date")]
    public DateTimeOffset LastUpdatedDate { get; set; }

    [String(Name="source_id")]
    public string SourceId { get; set; }
}

默认情况下,当 NEST 序列化 POCO 属性时,它使用驼峰命名约定。因为您的索引具有某些属性的蛇形套管,例如"last_updated_date",我们可以覆盖 NEST 将这些序列化为使用属性的名称。

接下来,让我们创建要使用的客户端

var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var pagesIndex = "pages";
var connectionSettings = new ConnectionSettings(pool)
        .DefaultIndex(pagesIndex)
        .PrettyJson()
        .DisableDirectStreaming()
        .OnRequestCompleted(response =>
            {
                // log out the request
                if (response.RequestBodyInBytes != null)
                {
                    Console.WriteLine(
                        $"{response.HttpMethod} {response.Uri} \n" +
                        $"{Encoding.UTF8.GetString(response.RequestBodyInBytes)}");
                }
                else
                {
                    Console.WriteLine($"{response.HttpMethod} {response.Uri}");
                }

                Console.WriteLine();

                // log out the response
                if (response.ResponseBodyInBytes != null)
                {
                    Console.WriteLine($"Status: {response.HttpStatusCode}\n" +
                             $"{Encoding.UTF8.GetString(response.ResponseBodyInBytes)}\n" +
                             $"{new string('-', 30)}\n");
                }
                else
                {
                    Console.WriteLine($"Status: {response.HttpStatusCode}\n" +
                             $"{new string('-', 30)}\n");
                }
            });

var client = new ElasticClient(connectionSettings);

连接设置的配置方式有助于开发;

  1. DefaultIndex() - 默认索引已配置为 "pages"。如果没有在请求中传递明确的索引名称并且无法为 POCO 推断出索引名称,则将使用默认索引。
  2. PrettyJson() - 美化(即缩进)json 请求和响应。这将有助于查看 Elasticsearch 发送和接收的内容。
  3. DisableDirectStreaming() - NEST 默认将 POCO 序列化为请求流,并从响应流中反序列化响应类型。禁用这种直接流式传输将在内存流中缓冲请求和响应字节,允许我们在 OnRequestCompleted()
  4. 中注销它们
  5. OnRequestCompleted() - 在收到响应后调用。这允许我们在开发时注销请求和响应。

2、3 和 4 在开发期间很有用,但会带来一些性能开销,因此您可能决定不在生产中使用它们。

现在,让我们使用页面映射创建索引

// delete the index if it exists. Useful for demo purposes so that
// we can re-run this example.
if (client.IndexExists(pagesIndex).Exists)
    client.DeleteIndex(pagesIndex);

// create the index, adding the mapping for the Page type to the index
// at the same time. Automap() will infer the mapping from the POCO
var createIndexResponse = client.CreateIndex(pagesIndex, c => c
    .Mappings(m => m
        .Map<Page>(p => p
            .AutoMap()
        )
    )
);

Take a look at the automapping documentation for more details around how you can control mapping for POCO types

索引一个新的页面类型就像

一样简单
// create a sample Page
var page = new Page
{
    Title = "Sample Page",
    Body = "Sample Body",
    Category = "sample",
    Keywords = new List<string>
    {
        "sample",
        "example", 
        "demo"
    },
    LastUpdatedDate = DateTime.UtcNow,
    MetaDescription = "Sample meta description",
    SourceId = "1",
    Url = "/pages/sample-page"
};

// index the sample Page into Elasticsearch.
// NEST will infer the document type (_type) from the POCO type,
// by default it will camel case the POCO type name
var indexResponse = client.Index(page);

如果文档不存在,索引文档将创建该文档,如果存在,则覆盖现有文档。 Elasticsearch has optimistic concurrency control 可用于控制其在不同条件下的行为方式。

我们可以使用 Update 方法更新文档,但首先要了解一些背景知识。

我们可以通过指定索引、类型和id从Elasticsearch获取文档。 NEST 使这稍微容易一些,因为我们可以从 POCO 中推断出所有这些。当我们创建映射时,我们没有在 POCO 上指定 Id 属性;如果 NEST 看到一个名为 Id 的 属性,它会将其用作文档的 ID,但因为我们没有,所以这不是问题,因为 Elasticsearch 将为文档并将其放入文档元数据中。然而,由于文档元数据与源文档是分开的,这会使将文档建模为 POCO 类型有点棘手(但并非不可能);对于给定的响应,我们将可以通过元数据访问文档的 ID,并通过 _source 字段访问源。我们可以在应用程序中将 id 与我们的来源结合起来。

解决这个问题的更简单方法是在 POCO 上有一个 ID。我们可以在 POCO 上指定一个 Id 属性 ,这将用作文档的 id,但我们不必调用 属性 Id 如果我们不想,如果我们不想,我们需要告诉 NEST 哪个 属性 代表 id。这可以通过属性来完成。假设 SourceIdPage 实例的唯一 ID,请使用 ElasticsearchTypeAttribute IdProperty 属性 来指定它。也许我们不应该也分析这个字符串而是逐字索引它,我们也可以通过 属性

属性的 Index 属性 来控制它
[ElasticsearchType(IdProperty = nameof(SourceId))]
public class Page
{
    public string Url { get; set; }

    public string Title { get; set; }

    public string Body { get; set; }

    [String(Name="meta_description")]
    public string MetaDescription { get; set; }

    public IList<string> Keywords { get; set; }

    public string Category { get; set; }

    [Date(Name="last_updated_date")]
    public DateTimeOffset LastUpdatedDate { get; set; }

    [String(Name="source_id", Index=FieldIndexOption.NotAnalyzed)]
    public string SourceId { get; set; }
}

有了这些,我们需要像以前一样重新创建索引,以便这些更改反映在映射中,NEST 可以在索引 Page 实例时使用此配置。

现在,回到更新:) 我们可以从 Elasticsearch 获取文档,在应用程序中更新它,然后重新索引它

var getResponse = client.Get<Page>("1");

var page = getResponse.Source;

// update the last updated date 
page.LastUpdatedDate = DateTime.UtcNow;

var updateResponse = client.Update<Page>(page, u => u.Doc(page));

第一个参数是我们想要获取的文档的 ID,NEST 可以从 Page 实例中推断出它。由于我们将 整个 文档传回此处,我们可以只使用 .Index() 而不是 Update(),因为我们正在更新所有字段

var indexResponse = client.Index(page);

但是,由于我们只想更新 LastUpdatedDate,必须从 Elasticsearch 获取文档,在应用程序中更新它,然后将文档发送回 Elasticsearch 是一项繁重的工作。我们可以只将更新后的 LastUpdatedDate 发送到 Elasticsearch,而不是使用 partial 文档。 C# 匿名类型在这里真的很有用

// model our partial document with an anonymous type. 
// Note that we need to use the snake casing name
// (NEST will still camel case the property names but this
//  doesn't help us here)
var lastUpdatedDate = new
{
    last_updated_date = DateTime.UtcNow
};

// do the partial update. 
// Page is TDocument, object is TPartialDocument
var partialUpdateResponse = client.Update<Page, object>("1", u => u
    .Doc(lastUpdatedDate)
);

这里如果需要使用RetryOnConflict(int)

可以使用乐观并发控制
var partialUpdateResponse = client.Update<Page, object>("1", u => u
    .Doc(lastUpdatedDate)
    .RetryOnConflict(1)
);

对于部分更新,Elasticsearch 将获取文档,应用部分更新,然后索引更新后的文档;如果文档在获取和更新之间发生变化,Elasticsearch 将根据 RetryOnConflict(1).

再次重试

希望对您有所帮助:)