一、Elasticsearch 简介

Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。

概述

Elasticsearch (ES)是一个基于 Lucene 的开源搜索引擎,它不但稳定、可靠、快速,而且也具有良好的水平扩展能力,是专门为分布式环境设计的。

特性

  • 安装方便:没有其他依赖,下载后安装非常方便;只用修改几个参数就可以搭建起来一个集群

  • JSON:输入/输出格式为 JSON,意味着不需要定义 Schema,快捷方便

  • RESTful:基本所有操作(索引、查询、甚至是配置)都可以通过 HTTP 接口进行

  • 分布式:节点对外表现对等(每个节点都可以用来做入口);加入节点自动均衡

  • 多租户:可根据不同的用途分索引;可以同时操作多个索引

    集群

    其中一个节点就是一个 ES 进程,多个节点组成一个集群。一般每个节点都运行在不同的操作系统上,配置好集群相关参数后 ES 会自动组成集群(节点发现方式也可以配置)。集群内部通过 ES 的选主算法选出主节点(目前版本 1.2 存在脑裂问题),而集群外部则是可以通过任何节点进行操作,无主从节点之分(对外表现对等/去中心化,有利于客户端编程,例如故障重连)。

    索引

    “索引”有两个意思:

    • 作为动词,它指的是把一个文档“保存”到 ES 中的过程,索引一个文档后,我们就可以使用 ES 搜索到这个文档

    • 作为名词,它是指保存文档的地方,相当于一个数据库概念中的“库”

    为了方便理解,我们可以将 ES 中的一些概念对应到我们熟悉的关系型数据库上:

    ES 索引 类型 文档
    DB

分片

ES 是一个分布式系统,我们一开始就应该以集群的方式来使用它。它保存索引时会选择适合的“分片”(Primary Shard),把索引保存到其中(我们可以把分片理解为一块物理存储区域)。分片的分法是固定的,而且是安装时候就必须要决定好的(默认是 5),后面就不能改变了。

既然有主分片,那肯定是有“从”分片的,在 ES 里称之为“副本分片”(Replica Shard)。副本分片主要有两个作用:

  • 高可用:某分片节点挂了的话可走其他副本分片节点,节点恢复后上面的分片数据可通过其他节点恢复

  • 负载均衡:ES 会自动根据负载情况控制搜索路由,副本分片可以将负载均摊

一个示例

来个示例总结一下上面的内容(结合下面的图一起看):

  • 3 个 ES 节点(es-58/59/60)组成一个集群

  • 搭建集群时使用默认的主分片数 5,shard0~shard4

  • 该集群内有加入两个索引 index1、index2

  • 这两个索引中分别“索引”(保存)了两个文档

  • index1 索引中这个文档被 ES 自动保存到了分片 2 中,主分片在 es-58 节点,副本分片在 es-59 节点

  • index2 索引中这个文档被 ES 自动保存到了分片 2 中,主分片在 es-59 节点,副本分片在 es-58 节点

    shards

(该图是使用 ES 的 RESTful 接口获取的,后面会介绍常用接口)

多租户

ES 的多租户简单的说就是通过多索引机制同时提供给多种业务使用,每种业务使用一个索引(关于多租户的详细定义与用途,可以参考这里)。前面我们提到过可以把索引理解为关系型数据库里的库,那多索引可以理解为一个数据库系统建立多个库给不同的业务使用。

在实际使用时,我们可以通过每个租户一个索引的方式将他们的数据进行隔离,并且每个索引是可以单独配置参数的(可对特定租户进行调优),这在典型的多租户场景下非常有用:例如我们的一个多租户应用需要提供搜索支持,这时可以通过 ES 根据租户建立索引,这样每个租户就可以在自己的索引下搜索相关内容了。

RESTful

这个特性非常方便,最关键的是 ES 的 HTTP 接口不只是可以进行业务操作(索引/搜索),还可以进行配置,甚至是关闭 ES 集群。下面我们介绍几个很常用的接口:

  • /_cat/nodes?v:查集群状态

  • /_cat/shards?v:查看分片状态

  • /${index}/${type}/_search:搜索

v 是 verbose 的意思,这样可以更可读(有表头,有对齐),cat 是监测相关的 APIs,/cat?help 来获取所有接口。${index} 和 ${type} 分别是具体的某一索引某一类型,是分层次的。我们也可以直接在所有索引所有类型上进行搜索:/_search。

官方术语表

最后,来份官方的术语表翻译,巩固一下理解:

analysis 分析

分析是将文本(text)转化为查询词(term)的过程。使用不同的分析器,这三种短语:FOO BAR,Foo-Bar,foo,bar 都有可能被分解成查询词 foo 与 bar。这些查询词实际上将被存储在索引中。一次对 FoO:bAR 的全文查询(不是查询词查询)可能会被分析为为查询词 foo,bar,可以匹配上保存在索引中的查询词。这就是分析处理过程(包含了索引与搜索),它使得 es 可以进行全文查询。

cluster 集群

一个或多个拥有同一个集群名称的节点组成了一个集群。每个集群都会自动选出一个主节点,如果该主节点故障,则集群会自动选出新的主节点来替换故障节点。

document 文档

一个文档就是一个保存在 es 中的 JSON 文本,可以把它理解为关系型数据库表中的一行。每个文档都是保存在索引中的,拥有一种类型和 id。一个文档是一个 JSON 对象(一些语言中的 hash / hashmap / associative array)包含了 0 或多个字段(键值对)。原始的 JSON 文本在索引后将被保存在 _source 字段里,搜索完成后返回值中默认是包含该字段的。

id

Id 是用于标识文档的,一个文档的索引/类型/id 必须是唯一的。文档 id 是自动生成的(如果不指定)。

field 字段

一个文档包含了若干字段,或称之为键值对。字段的值可以是简单(标量)值(例如字符串、整型、日期),也可以是嵌套结构,例如数组或对象。一个字段类似于关系型数据库表中的一列。每个字段的映射都有一个字段类型(不要和文档类型搞混了),它描述了这个字段可以保存的值类型,例如整型、字符串、对象。映射还可以让我们定义一个字段的值如何进行分析。

index 索引

一个索引类似关系型数据库中的一个数据库,它可以映射为多种类型。一个索引就是逻辑上的一个命名空间,对应到 1 或多个主分片上,可以拥有 0 个或多个副本分片。

mapping 映射

一个映射类似于关系型数据库中的模式定义。每个索引都存在一个映射,它定义了该索引中的每一种类型,以及索引相关的配置。映射可以显示定义,或者在文档被索引时自动创建。

node 节点

一个节点是集群中的一个 es 运行实例。测试时,多个节点可以同时启在同一个服务器上,生产环境一般是一个服务器上一个节点。节点启动时将使用单播(或者是组播)来发现和自己配置的集群名称相同的集群,并尝试加入到该集群中。

primary shard 主分片

每个文档都会被保存在一个主分片上。当我们索引一个文档时,它将在一个主分片上进行索引,然后才放到该主分片的各副本分片上。默认情况下,一个索引有 5 个主分片。我们可以指定更少或更多的主分片来伸缩索引可处理的文档数。需要注意的是,一旦索引创建,就不能修改主分片个数。

replica shard 副本分片

每个主分片可以拥有 0 个或多个副本分片。一个副本分片是主分片的一份拷贝,这样做有两个主要原因:

  1. 故障转移:当主分片失效时,一个副本分片会被提升为主分片

  2. 提高性能:获取与搜索请求可以被主分片或副本分片处理。默认情况下,每个主分片都有一个副本分片,副本分片的数量可以动态调整。在同一个节点上,副本分片和其主分片不会同时运行

routing 路由

当我们索引一个文档时,它将被保存在一个主分片上,分片的选择是通过路由值哈希得到的。默认情况下,路由值来自于文档 id,如果该文档指定来了父文档,则路由值来自于父文档 id(这是为了确保子文档和父文档被保存在相同的分片上)。该值可以在索引时指定,也可以通过映射路由字段来指定。

shard 分片

一个分片就是一个 Lucene 实例,它是 es 管理的底层“工作单元”。一个索引是逻辑上的一个命名空间,指向主分片和副本分片。索引的主分片和副本分片数量必须明确指定好,在应用代码使用时只需要处理和索引的交互,不会涉及到和分片的交互。Elasticsearch 会在集群中的所有节点上设置好分片,但节点失效或加入新节点时会自动将移动节点分片。

source field 源字段

默认情况下,在获取和搜索请求返回值中的 _source 字段保存了源 JSON 文本,这使得我们可以直接在返回结果中访问源数据,而不需要根据 id 再发一次检索请求。注意:索引的 JSON 字符串将完整返回,无论是否是一个合法的 JSON。该字段的内容也不会描述数据如何被索引。

term 查询词

一个查询词是一个被 es 索引的确切值。查询词 foo,Foo,FOO 是不同的。查询词可以使用查询词查询接口进行获取。

text 文本

文本(或称之为全文)是普通的、非结构化的文本,例如本段话。默认情况下,文本将被分析为查询词,查询词将被保存在索引中。为能够进行全文搜索,文本字段在索引时将被分析为查询词,查询关键字在搜索时也将被分析为查询词,通过对比查询词是否相同而完成全文搜索。

type 类型

一种类型类似于关系型数据库中的一张表。每种类型都有若干字段,可以用于指定给该类型文档。映射定义了该文档中的每个字段如何进行分析。

参考

二、ElasticSearch环境搭建与安装

1.安装(路径中不能有空格)

这里以windows环境为例,linux类似。 移步到ES官网,下载ElasticSearch和Kibana

ES: https://www.elastic.co/downloads/elasticsearch Kibana: https://www.elastic.co/downloads/kibana 这两个压缩包,解压之后即可用(ES自带jdk无需单独安装)。 ▼双击elasticsearch.bat启动ES

也可以执行elasticsearch-service.bat install将es安装为服务,随系统启动。

▼双击kibana.bat启动Kibana

都启动完成之后,浏览器输入localhost:9200查看ES,如果有输出内容,则证明ES运行正常。打开localhost:5601如果能看到页面则Kibana正常运行。

最新版本的ElasticSearch 8.x 直接访问http://localhost:9200 会访问失败,需要访问https://localhost:9200并输入用户名和密码

 

 

三、使用C#操作ElasticSearch

注意:ES的8.X以上的版本由新的包Elastic.Clients.Elasticsearch处理,8.0以下版本用NEST

NEST是一个高层的客户端,可以映射所有请求和响应对象,拥有一个强类型查询DSL(领域特定语言),并且可以使用.net的特性比如协变、Auto Mapping Of POCOs,NEST内部使用的依然是Elasticsearch.Net客户端。elasticsearch.net(NEST)客户端提供了强类型查询DSL,方便用户使用,源码下载

3.1、如何安装NEST

打开VS的工具菜单,通过NuGet包管理器控制台,输入以下命令安装NEST

Install-Package NEST

3.2、链接elasticsearch

你可以通过单个节点或者指定多个节点使用连接池链接到Elasticsearch集群,使用连接池要比单个节点链接到Elasticsearch更有优势,比如支持负载均衡、故障转移等。

通过单点链接:

1 var node = new Uri("http://myserver:9200");
2 var settings = new ConnectionSettings(node);
3 //settings.BasicAuthentication("elastic", "RHGeDLoqVsfy9ySNj2jB");
4 var client = new ElasticClient(settings);

 

通过连接池链接:

 1  var nodes = new Uri[]
 2  {
 3      new Uri("http://myserver1:9200"),
 4      new Uri("http://myserver2:9200"),
 5       new Uri("http://myserver3:9200")
 6  };
 7  
 8 var pool = new StaticConnectionPool(nodes);
 9 var settings = new ConnectionSettings(pool);
10 var client = new ElasticClient(settings);

 

 string indexName = "company";

 

插入数据

 1 //方法一:通过委托
 2 var res =  client.Index(company, i => i.Index(indexName));
 3 //方法二:通过 IndexRequest 对象
 4 client.Index(new IndexRequest<Company>(company, indexName));
 5 #region 批量插入
 6                 //如果需要批量插入需要用BulkDescriptor对象包裹,然后使用BulkAsync方法插入,或者不要包裹直接用IndexManyAsync方法插入
 7                 //var company = new Company()
 8                 //{
 9                 //    Name = "超级公司bulk",
10                 //    Description = "超级描述bulk"
11                 //};
12                 //BulkDescriptor bulkDescriptor = new BulkDescriptor();
13                 //bulkDescriptor.Index<Company>(op => op.Document(company).Index(indexName));
14                 //var res = client.Bulk(bulkDescriptor);
15                 //var list = new List<Company>();
16                 //list.Add(company);
17                 //var res = await client.IndexMany(list, indexName);
18 19 20                 // 如果实体有Id则会使用Id的值做为_id的索引文档唯一值,
21                 // 或者可以通过手动指定如await esClient.IndexAsync(company, g => g.Index(indexName).Id(company.Id)),
22                 // 如果id相同执行插入操作则为更新不会重复插入。在新增后是会返回id等信息可以加以利用。
23 #endregion

 

修改数据

 1 #region 修改
 2             {
 3                 DocumentPath<Company> deletePath = new DocumentPath<Company>("1231");
 4                 var res = client.Update(deletePath, (p) => p.Doc(company).Index(indexName));
 5                 //等价于
 6                 //IUpdateRequest<Company, Company> request = new UpdateRequest<Company, Company>(indexName, "1231")
 7                 //{
 8                 //    Doc = new Company()
 9                 //    {
10                 //        Id = "888",
11                 //        Description = "11111",
12                 //    }
13                 //};
14                 //var res =  client.Update(request);
15 16                 //如果有多个id更新多条数据可以用如下方法:
17                 var res2 = client.Bulk(b => b.UpdateMany(new List<Company>() { new Company(){ Id="1231",
18                      }}, (b, u) => b.Id(u.Id).Index(indexName).Doc(new Company { Name = "我无语了" })));
19 20                 #region 根据条件批量更新
21                 var req = new UpdateByQueryRequest<Company>(indexName)
22                 {
23                     MaximumDocuments = 10,//一次最多更新几条
24                     Query = new MatchQuery()
25                     {
26                         Field = "description",
27                         Query = "66",
28                     },
29                     Script = new ScriptDescriptor()
30                     .Source($"ctx._source.description = params.description;")
31                     .Params(new Dictionary<string, object> { { "description", "小时了123123123" } }),
32                     Refresh = true
33                 };
34                 var result = client.UpdateByQuery(req);
35                 #endregion
36             }
37             #endregion

 

删除数据

 1 #region 删除
 2             {
 3                 #region 删除单个数据
 4                 //删除指定单条数据需要知道数据的id
 5                 DocumentPath<Company> deletePath = new DocumentPath<Company>(Guid.Empty);
 6                 var delRes = client.Delete(deletePath, g => g.Index(indexName));
 7                 //或者
 8                 IDeleteRequest request = new DeleteRequest(indexName, "1231");
 9                 var delRes2 = client.Delete(request);
10                 #endregion
11 12                 #region 批量删除
13                 // 多条删除使用DeleteByQueryAsync方法进行匹配删除,下面两种方式等价,删除Description字段模糊查询有描述的数据(最多10条):
14                 var req = new DeleteByQueryRequest<Company>(indexName)
15                 {
16                     MaximumDocuments = 10,//一次最多删几条
17                     Query = new MatchQuery()
18                     {
19                         Field = "description",
20                         Query = "描述"
21                     }
22                 };
23                 var result = client.DeleteByQuery(req);
24                 //等价于
25                 var result2 = client.DeleteByQuery<Company>(dq => dq.MaximumDocuments(10)
26                                 .Query(q => q.Match(tr => tr.Field(fd => fd.Description).Query("描述"))).Index(indexName));
27                 #endregion
28             }
29             #endregion 删除

 

查询数据

 1 //简单查询
 2             var qr1 = client.Search<Company>(s => s
 3                 .Index(indexName)//"log.test_mix-2021.01.18"
 4                 .Query(q => q.MatchAll()));
 5             var list = qr1.Documents.ToList();
 6             
 7             var query = await Client.SearchAsync<Company>(x => x.Index(IndexName)
 8                                     .From((page - 1) * limit)
 9                                     .Size(limit)
10                                     .Sort(x => x.Descending(v => v.CreatedAt)));

 

四、使用Elastic.Clients.Elasticsearch

4.1 特征

  • 使用 REST API 进行一对一映射。

  • Elasticsearch API 的强类型请求和响应。

  • 用于构建请求的流畅 API。

  • 常见任务(如文档的批量索引)的帮助程序。

  • 基于 System.Text.Json 的请求和响应的可插入序列化。

  • 诊断、审核和 .NET 活动集成。

.NET Elasticsearch 客户端建立在 Elastic Transport 库之上,该库提供:

  • 跨所有可用节点的连接管理和负载平衡。

  • 请求重试和死连接处理。

4.2 安装

本页介绍如何为 Elasticsearch 安装 .NET 客户端。

适用于 .NET 的 v8 客户端没有完全的功能奇偶校验 V7 客户端。它可能不适用于所有应用,直到 支持其他终结点和功能。因此,我们建议您在尝试将现有应用程序迁移到库之前仔细阅读我们的发行说明。在新客户端支持应用程序所需的所有终结点和功能之前, 您可以继续使用客户端的 7.17.x 版本与 v8 通信 使用兼容模式的 Elasticsearch 服务器。有关配置 8.7.x 客户端的指导,请参阅使用 v17.7.x 客户端连接到 Elasticsearch v17.x 文档NEST``Elastic.Clients.Elasticsearch

安装 .NET 客户端

对于 SDK 风格的项目,您可以通过运行以下命令来安装 Elasticsearch 客户端 终端中的 .NET CLI 命令:

dotnet add package Elastic.Clients.Elasticsearch

 

此命令将包引用添加到项目 (csproj) 文件 客户端的最新稳定版本。

如果您愿意,也可以在项目中手动添加包引用 文件:

<PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.0.0" />

 

注意:版本号应反映 NuGet.org 最新发布的版本。要安装其他版本,请根据需要修改版本。

对于Visual Studio用户,也可以从包中安装.NET客户端 Visual Studio 中的管理器控制台,使用以下命令:

Install-Package Elastic.Clients.Elasticsearch

 

或者,在 NuGet 包中搜索 管理器用户界面。Elastic.Clients.Elasticsearch

要了解如何连接 Elasticsearch 客户端,请参阅连接部分。

兼容性

Elasticsearch 客户端与当前维护的 .NET 运行时版本兼容。 不保证或支持与生命周期结束 (EOL) .NET 运行时的兼容性。

语言客户端向前兼容;意味着客户支持 与更大或相等的 Elasticsearch 次要版本进行通信。弹性搜索语言 客户端仅向后兼容默认发行版,没有 做出的保证。

4.3 连接

此页包含创建 .NET 实例所需的信息 连接到 Elasticsearch 集群的 Elasticsearch 客户端。

可以通过单个节点连接到您的 Elasticsearch 集群,也可以通过以下方式连接到 使用节点池指定多个节点。使用节点池有几个 与单个节点相比的优势,例如负载平衡和群集故障转移 支持。客户端提供方便的配置选项来连接到 弹性云部署。

客户端应用程序应创建单个实例,该实例在整个应用程序中使用,以贯穿其整个应用程序 辈子。在内部,客户端管理和维护与节点的 HTTP 连接, 重用它们以优化性能。如果使用依赖关系注入 应用程序的容器,客户端实例应注册到 单例生存期。ElasticsearchClient

连接到云部署

Elastic Cloud 是开始使用 Elasticsearch 的最简单方法。连接到弹性云时 使用 .NET Elasticsearch 客户端时,您应该始终使用 Cloud ID。你可以找到这个 创建群集后“管理部署”页中的值 (如果您在 Kibana 中,请查看左上角)。

我们建议尽可能使用 Cloud ID,因为您的客户将 自动配置,以优化与弹性云(包括 HTTPS 和 HTTP 压缩。

连接到 Elasticsearch Service 部署是通过提供 配置实例时部署的唯一云 ID。您还需要合适的凭据,用户名和密码或 应用程序用于向部署进行身份验证的 API 密钥。ElasticsearchClient

作为安全最佳实践,建议为每个 应用程序,权限仅限于任何 API 调用所需的权限 申请是授权的。

以下代码片段演示如何创建连接到 云中的 Elasticsearch 部署。

using Elastic.Clients.Elasticsearch;
using Elastic.Transport;

var client = new ElasticsearchClient("<CLOUD_ID>", new ApiKey("<API_KEY>")); 

 

 

 将上面的占位符字符串值替换为您的云 ID 和 API 密钥 为应用程序配置以访问部署。
   

连接到单个节点

单节点配置最适合连接到多节点群集 在负载均衡器或反向代理后面运行,通过单个 网址。在本地应用程序期间使用单个节点也可能很方便 发展。如果 URL 代表单个 Elasticsearch 节点,请注意,这提供了 如果服务器无法访问或无响应,则无法复原。

默认情况下,身份验证和TLS等安全功能在Elasticsearch上启用 集群。首次启动 Elasticsearch 时,会配置 TLS 自动用于 HTTP 层。CA 证书生成并存储在 磁盘,用于对 Elasticsearch 的 HTTP 层的证书进行签名 簇。

为了使客户端通过 HTTPS 与集群建立连接, 客户端应用程序必须信任 CA 证书。最简单的 选择是使用 CA 证书的十六进制编码的 SHA-256 指纹。这 CA 指纹会在您第一次启动 Elasticsearch 时输出到终端。 你会在 Elasticsearch 的输出中看到一个明显的块,如下所示(你可以 如果已经有一段时间了,必须向上滚动):

----------------------------------------------------------------
-> Elasticsearch security features have been automatically configured!
-> Authentication is enabled and cluster connections are encrypted.

-> Password for the elastic user (reset with `bin/elasticsearch-reset-password -u elastic`):
lhQpLELkjkrawaBoaz0Q

-> HTTP CA certificate SHA-256 fingerprint:
a52dd93511e8c6045e21f16654b77c9ee0f34aea26d9f40320b531c474676228
...
----------------------------------------------------------------

 

记下用户密码和 HTTP CA 指纹以供下次使用 部分。elastic

还可以使用 以下命令:

openssl x509 -fingerprint -sha256 -in config/certs/http_ca.crt

 

该命令返回安全证书,包括指纹。应该是 .issuer``Elasticsearch security auto-configuration HTTP CA

issuer= /CN=Elasticsearch security auto-configuration HTTP CA
SHA256 Fingerprint=<FINGERPRINT>

 

有关更多信息,请访问在自动启用安全性的情况下启动 Elastic Stack 文档。

以下代码片段演示如何创建连接到 您的 Elasticsearch 集群通过单个节点,使用 CA 指纹:

using Elastic.Clients.Elasticsearch;
using Elastic.Transport;

var settings = new ElasticsearchClientSettings(new Uri("https://localhost:9200"))
    .CertificateFingerprint("<FINGERPRINT>")
    .Authentication(new BasicAuthentication("<USERNAME>", "<PASSWORD>"));

var client = new ElasticsearchClient(settings);

 

前面的代码片段演示了将客户端配置为通过以下方式进行身份验证 提供具有基本身份验证的用户名和密码。如果愿意,您可以 还可以使用身份验证,如云连接示例中所示。ApiKey

使用节点池连接到多个节点

若要提供复原能力,应为群集配置多个节点,以便 客户端尝试通信的内容。默认情况下,客户端循环通过 以轮循机制方式为每个请求的节点。客户端还跟踪 不正常的节点,并避免向它们发送请求,直到它们变得正常。

此配置最适合连接到已知的小型群集, 不需要嗅探来检测群集拓扑的地方。

以下代码片段演示如何使用 静态节点池:

using Elastic.Clients.Elasticsearch;
using Elastic.Transport;

var nodes = new Uri[]
{
    new Uri("https://myserver1:9200"),
    new Uri("https://myserver2:9200"),
    new Uri("https://myserver3:9200")
};

var pool = new StaticNodePool(nodes);

var settings = new ElasticsearchClientSettings(pool)
    .CertificateFingerprint("<FINGERPRINT>")
    .Authentication(new ApiKey("<API_KEY>"));

var client = new ElasticsearchClient(settings);

 

4.4 配置

ElasticsearchClientSettings 上的选项

以下是 上可用的连接配置选项列表:ElasticsearchClientSettings

Authentication
描述要使用的 http 标头的实现 以对产品进行身份验证。IAuthenticationHeader

`BasicAuthentication` for basic authentication
`ApiKey` for simple secret token
`Base64ApiKey` for Elastic Cloud style encoded api keys
ClientCertificate
使用以下证书对所有 HTTP 请求进行身份验证。你也可以 使用 根据单个请求设置它们。ClientCertificates
ClientCertificates
使用以下证书对所有 HTTP 请求进行身份验证。你也可以 使用 根据单个请求设置它们。ClientCertificates
ConnectionLimit
限制可以打开到终结点的并发连接数。 缺省值为 80(请参阅)。DefaultConnectionLimit

对于桌面 CLR,此设置适用于属性 创建对象时在对象上, 影响默认实现。DefaultConnectionLimitServicePointManagerServicePointIConnection

对于核心 CLR,此设置适用于 上的属性 默认实现内部使用的实例。MaxConnectionsPerServerHttpClientHandlerHttpClientIConnection

DeadTimeout
将死节点从旋转中移出的时间(这将乘以 他们死了的次数)。
DefaultDisableIdInference
禁用给定 CLR 类型的自动 Id 推理。

默认情况下,客户端将使用在 CLR 类型上命名的属性的值 作为发送到 Elasticsearch。添加类型将禁用此行为 CLR 类型。如果应对所有 CLR 类型禁用推理,请使用 。Id_idIdDefaultDisableIdInference

DefaultFieldNameInferrer
指定如何从 CLR 属性名称推断字段名称。

默认情况下,客户端驼峰大小写属性名称。例如,CLR 属性将被推断为“emailAddress”Elasticsearch 文档字段名称。EmailAddress

DefaultIndex
在没有显式索引时用于请求的默认索引 指定,但未为指定的给定 CLR 类型指定默认索引 对于请求。
DefaultMappingFor
指定如何推断给定 CLR 类型的映射。映射可以推断 给定 CLR 类型的索引、ID 和关系名称,以及控件 CLR 属性的序列化行为。
DisableAutomaticProxyDetection
禁用了 Web 请求上的代理检测,在某些情况下,这可能会加快 您的应用程序域建立的第一个连接,在其他情况下,它实际上会增加 首次连接的时间。没有灵丹妙药!小心使用!
DisableDirectStreaming
设置为 true 时,将禁用(反)序列化直接到请求,并且 响应流并返回原始请求和响应的 byte[] 副本。 默认为 false。
DisablePing
这表示我们不想将初始ping发送到未知/以前 死节点,只需立即发送呼叫。
DnsRefreshTimeout
连接的 DnsRefreshTimeout。默认为 5 分钟。
EnableDebugMode
打开有助于调试的设置,以便可以检查原始请求和响应 JSON。 它还始终要求服务器在错误时进行完整堆栈跟踪。DisableDirectStreaming()PrettyJson()
EnableHttpCompression
启用 gzip 压缩请求和响应。
EnableHttpPipelining
是否启用了 HTTP 流水线。缺省值为 。true
EnableTcpKeepAlive
设置 TCP 连接上的保持活动状态选项。

对于桌面 CLR,设置 ..ServicePointManagerSetTcpKeepAlive

EnableTcpStats
启用有关在发出请求时要收集的 TCP 连接的统计信息。
GlobalHeaders
尝试为每个请求发送这些标头。
GlobalQueryStringParameters
将这些查询字符串参数自动附加到每个请求。
MaxDeadTimeout
允许节点标记为死的最长时间。
MaximumRetries
当发生可重试的异常或返回状态代码时,这将控制 我们应该重试调用 Elasticsearch 的最大次数。
MaxRetryTimeout
限制总运行时间,包括与 分开的重试次数。 如果未指定默认值,则其自身默认为 60 秒。RequestTimeoutRequestTimeout
MemoryStreamFactory
提供内存流工厂。
NodePredicate
注册谓词以选择要执行 API 调用的节点 上。请注意,嗅探请求省略此谓词,并始终在所有 节点。使用支持重新设定种子的实现时 节点,这将默认为从常规 API 调用中省略仅主节点。 使用静态或单节点连接池时,假定列表 您用来实例化客户端的节点应逐字获取。IConnectionPool
OnRequestCompleted
允许您在每次返回 API 调用时注册回调。
OnRequestDataCreated
创建 for 请求时要运行的操作。RequestData
PingTimeout
用于 ping 请求的超时(以毫秒为单位),这些请求发出给 确定节点是否处于活动状态。
PrettyJson
为序列化程序和产品提供提示,以生成漂亮的非缩小 json。

注意:这并不能保证您总是会得到美化的 json。

Proxy
如果连接必须通过代理,请使用此方法指定 代理网址。
RequestTimeout
向 Elasticsearch 发出的每个请求的超时时间(以毫秒为单位)。
ServerCertificateValidationCallback
为每个请求注册一个。ServerCertificateValidationCallback
SkipDeserializationForStatusCodes
将客户端配置为跳过某些状态代码的反序列化,对于 例如,您在返回意外 JSON 格式的代理后面运行 Elasticsearch。
SniffLifeSpan
当群集状态信息较旧时,强制对群集进行新的嗅探 超过指定的时间跨度。
SniffOnConnectionFault
每次连接中断时,强制对群集状态进行新的嗅探。
SniffOnStartup
在启动时立即嗅探群集状态。
ThrowExceptions
而不是遵循 c/go 之类的错误检查响应。 做扔 当 调用导致客户端或 Elasticsearch 服务器上出现异常。IsValidSuccessOrKnownError

此类异常的原因可能是搜索解析器错误、索引缺失 例外,依此类推。

TransferEncodingChunked
是否应使用分块传输编码发送请求。
UserAgent
要随请求一起发送的用户代理字符串。可用于调试目的 了解向 Elasticsearch 发起请求的客户端和框架版本。

 

下面是演示如何使用客户端设置配置选项的示例。

var settings= new ElasticsearchClientSettings()
    .DefaultMappingFor<Project>(i => i
        .IndexName("my-projects")
        .IdProperty(p => p.Name)
    )
    .EnableDebugMode()
    .PrettyJson()
    .RequestTimeout(TimeSpan.FromMinutes(2));

var client = new ElasticsearchClient(settings);

 

4.5 客户端概念

序列化

默认情况下,Elasticsearch 的 .NET 客户端使用 Microsoft System.Text.Json 库进行序列化。客户端了解如何序列化和 正确反序列化请求和响应类型。它还处理用户 POCO 类型的(反)序列化,这些类型表示读取或写入 Elasticsearch 的文档。

客户端有两个不同的序列化职责 - 库拥有的类型的序列化和源文档的序列化,在应用程序代码中建模。第一个责任完全是内部的;第二个是可配置的。Elastic.Clients.Elasticsearch

源序列化

源序列化是指将消费者应用程序中的 POCO 类型(反)序列化为从 Elasticsearch 索引和检索的源文档的过程。源序列化程序实现处理序列化,默认实现使用该库。因此,可以使用属性和转换器来控制序列化行为。System.Text.Json``System.Text.Json

使用类型对文档进行建模

Elasticsearch 为发送的文档和索引提供搜索和聚合功能。这些文件作为 HTTP 请求的请求正文中的 JSON 对象。在 Elasticsearch .NET 客户端中使用 POCO(普通旧 CLR 对象对文档进行建模是很自然的。

本节概述了如何使用类型和类型层次结构对文档进行建模。

默认行为

默认行为是将类型属性名称序列化为驼峰大小 JSON 对象成员。

我们可以使用常规类 (POCO) 对文档进行建模。

public class MyDocument
{
    public string StringProperty { get; set; }
}

 

然后,我们可以将文档的一个实例索引到 Elasticsearch 中。

using System.Threading.Tasks;
using Elastic.Clients.Elasticsearch;

var document = new MyDocument
{
    StringProperty = "value"
};

var indexResponse = await Client
    .IndexAsync(document, "my-index-name");

 

索引请求被序列化,源序列化程序处理类型,序列化名为 的 JSON 对象成员的 POCO 属性。MyDocument``StringProperty``stringProperty

{
  "stringProperty": "value"
}

 

自定义源序列化

内置的源序列化程序可以正确处理大多数 POCO 文档模型。有时,您可能需要进一步控制类型的序列化方式。

内置的源序列化程序在内部使用 Microsoft System.Text.Json。您可以应用属性和转换器来控制文档类型的序列化。System.Text.Json

使用属性System.Text.Json编辑

System.Text.Json包括可应用于类型和属性以控制其序列化的属性。这些可以应用于您的 POCO 文档类型以执行操作,例如控制属性的名称或完全忽略属性。有关更多示例,请访问微软文档

我们可以对文档进行建模,以使用常规类 (POCO) 表示有关人员的数据,并根据需要应用属性。System.Text.Json

using System.Text.Json.Serialization;

public class Person
{
    [JsonPropertyName("forename")] 
    public string FirstName { get; set; }

    [JsonIgnore] 
    public int Age { get; set; }
}

 

 该属性可确保属性在序列化时使用 JSON 名称。JsonPropertyName``FirstName``forename
  该属性可防止该属性出现在序列化的 JSON 中。JsonIgnore``Age

然后,我们可以将文档的一个实例索引到 Elasticsearch 中。

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Elastic.Transport;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Serialization;

var person = new Person { FirstName = "Steve", Age = 35 };
var indexResponse = await Client.IndexAsync(person, "my-index-name");

 

索引请求被序列化,源序列化程序处理类型,序列化名为 的 JSON 对象成员的 POCO 属性。该属性将被忽略,并且不会显示在 JSON 中。Person``FirstName``forename``Age

{
  "forename": "Steve"
}

 

配置自定义JsonSerializerOptions

默认源序列化程序在序列化源文档类型时应用一组标准。在某些情况下,您可能需要覆盖我们的某些默认值。这可以通过创建 的实例并传递一个 来实现,该实例在设置默认值后应用。此机制允许您应用其他设置或更改默认值的值。JsonSerializerOptions``DefaultSourceSerializer``Action<JsonSerializerOptions>

包括一个接受当前电流的构造函数和一个 .DefaultSourceSerializer``IElasticsearchClientSettings``configureOptions``Action

public DefaultSourceSerializer(IElasticsearchClientSettings settings, Action<JsonSerializerOptions> configureOptions);

我们的应用程序定义了以下类,该类模拟了我们将索引到 Elasticsearch 的文档。Person

public class Person
{
public string FirstName { get; set; }
}

我们希望使用 Pascal 大小写对 JSON 属性序列化源文档。由于在 中应用的选项 设置为 ,我们必须覆盖此设置。配置 后,我们将文档索引到 Elasticsearch。DefaultSouceSerializer``PropertyNamingPolicy``JsonNamingPolicy.CamelCase``ElasticsearchClientSettings

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Elastic.Transport;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Serialization;

static void ConfigureOptions(JsonSerializerOptions o) => 
    o.PropertyNamingPolicy = null;

var nodePool = new SingleNodePool(new Uri("http://localhost:9200"));
var settings = new ElasticsearchClientSettings(
    nodePool,
    sourceSerializer: (defaultSerializer, settings) =>
        new DefaultSourceSerializer(settings, ConfigureOptions)); 
var client = new ElasticsearchClient(settings);

var person = new Person { FirstName = "Steve" };
var indexResponse = await client.IndexAsync(person, "my-index-name");

 

 可以定义一个接受参数的本地函数。在这里,我们设置为 .这将返回到 的默认行为,该行为使用 Pascal Case。JsonSerializerOptions``PropertyNamingPolicy``null``System.Text.Json
  在创建 时,我们使用 lambda 提供 。工厂函数创建一个 的新实例,传入 和我们的本地函数。现在,我们已使用源序列化程序的自定义实例配置了设置。ElasticsearchClientSettings``SourceSerializerFactory``DefaultSourceSerializer``settings``ConfigureOptions

实例已序列化,源序列化程序序列化使用 Pascal Case 命名的 POCO 属性。Person``FirstName

{
"FirstName": "Steve"
}

作为使用局部函数的替代方法,我们可以将 a 存储到一个变量中,该变量可以传递给构造函数。Action<JsonSerializerOptions>``DefaultSouceSerializer

Action<JsonSerializerOptions> configureOptions = o => o.PropertyNamingPolicy = null;
注册自定义转换器System.Text.Json

在某些更高级的情况下,在序列化期间可能需要进一步自定义的类型,而不是使用属性属性。在这些情况下,微软的建议是利用自定义.使用 序列化的源文档类型可以利用自定义转换器的强大功能。System.Text.Json``JsonConverter``DefaultSourceSerializer

对于此示例,我们的应用程序有一个文档类,该类应使用旧版 JSON 结构继续使用现有索引文档进行操作。有几个选项可用,但在这种情况下,我们将应用自定义转换器。

定义了我们的类,并将属性应用于类类型,指定自定义转换器的类型。JsonConverter

using System.Text.Json.Serialization;

[JsonConverter(typeof(CustomerConverter))] 
public class Customer
{
    public string CustomerName { get; set; }
    public CustomerType CustomerType { get; set; }
}

public enum CustomerType
{
    Standard,
    Enhanced
}

 

 该属性发出信号,表明在序列化此类的实例时,它应使用类型的转换器。JsonConverter``System.Text.Json``CustomerConverter
   

序列化此类时,我们必须发送一个名为 .此要求可以通过自定义 JsonConverter 实现来实现。CustomerType``isStandard

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

public class CustomerConverter : JsonConverter<Customer>
{
    public override Customer Read(ref Utf8JsonReader reader,
        Type typeToConvert, JsonSerializerOptions options)
    {
        var customer = new Customer();

        while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
        {
            if (reader.TokenType == JsonTokenType.PropertyName)
            {
                if (reader.ValueTextEquals("customerName"))
                {
                    reader.Read();
                    customer.CustomerName = reader.GetString();
                    continue;
                }

                if (reader.ValueTextEquals("isStandard")) 
                {
                    reader.Read();
                    var isStandard = reader.GetBoolean();

                    if (isStandard)
                    {
                        customer.CustomerType = CustomerType.Standard;
                    }
                    else
                    {
                        customer.CustomerType = CustomerType.Enhanced;
                    }

                    continue;
                }
            }
        }

        return customer;
    }

    public override void Write(Utf8JsonWriter writer,
        Customer value, JsonSerializerOptions options)
    {
        if (value is null)
        {
            writer.WriteNullValue();
            return;
        }

        writer.WriteStartObject();

        if (!string.IsNullOrEmpty(value.CustomerName))
        {
            writer.WritePropertyName("customerName");
            writer.WriteStringValue(value.CustomerName);
        }

        writer.WritePropertyName("isStandard");

        if (value.CustomerType == CustomerType.Standard) 
        {
            writer.WriteBooleanValue(true);
        }
        else
        {
            writer.WriteBooleanValue(false);
        }

        writer.WriteEndObject();
    }
}

 

 读取时,此转换器读取布尔值并将其转换为正确的枚举值。isStandard``CustomerType
  写入时,此转换器将枚举值转换为布尔属性。CustomerType``isStandard

然后,我们可以将客户文档索引到 Elasticsearch 中。

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Elastic.Transport;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Serialization;

var customer = new Customer
{
    CustomerName = "Customer Ltd",
    CustomerType = CustomerType.Enhanced
};
var indexResponse = await Client.IndexAsync(customer, "my-index-name");

 

实例使用自定义转换器进行序列化,并创建以下 JSON 文档。Customer

{
  "customerName": "Customer Ltd",
  "isStandard": false
}

 

创建自定义SystemTextJsonSerializer

内置功能包括注册在源序列化期间应用的实例。在大多数情况下,它们为序列化源文档(包括在其属性上使用类型的文档)提供了正确的行为。DefaultSourceSerializer``JsonConverter``Elastic.Clients.Elasticsearch

可能需要对转换器注册顺序进行更多控制的情况的一个示例是序列化类型。寄存器 ,因此枚举值使用其字符串表示形式进行序列化。通常,对于用于将文档索引到 Elasticsearch 的类型,这是首选选项。enum``DefaultSourceSerializer``System.Text.Json.Serialization.JsonStringEnumConverter

在某些情况下,可能需要控制为枚举值发送的字符串值。这在 中不直接支持,但可以通过为要自定义的类型创建自定义来实现。在这种情况下,仅使用 on 类型来注册转换器是不够的。 将首选添加到集合的转换器应用于类型的属性。因此,有必要从集合中删除 或在 之前为您的类型注册专用转换器。System.Text.Json``JsonConverter``enum``JsonConverterAttribute``enum``System.Text.Json``Converters``JsonSerializerOptions``enum``JsonStringEnumConverter``Converters``enum``JsonStringEnumConverter

后者可以通过几种技术实现。当使用 Elasticsearch .NET 库时,我们可以通过从抽象类派生来实现这一点。SystemTextJsonSerializer

这里我们有一个 POCO,它使用枚举作为属性的类型。CustomerType

using System.Text.Json.Serialization;

public class Customer
{
    public string CustomerName { get; set; }
    public CustomerType CustomerType { get; set; }
}

public enum CustomerType
{
    Standard,
    Enhanced
}

 

为了自定义序列化期间使用的字符串,我们定义了一个特定于我们的类型的自定义。CustomerType``JsonConverter``enum

using System.Text.Json.Serialization;

public class CustomerTypeConverter : JsonConverter<CustomerType>
{
    public override CustomerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return reader.GetString() switch 
        {
            "basic" => CustomerType.Standard,
            "premium" => CustomerType.Enhanced,
            _ => throw new JsonException(
                $"Unknown value read when deserializing {nameof(CustomerType)}."),
        };
    }

    public override void Write(Utf8JsonWriter writer, CustomerType value, JsonSerializerOptions options)
    {
        switch (value) 
        {
            case CustomerType.Standard:
                writer.WriteStringValue("basic");
                return;
            case CustomerType.Enhanced:
                writer.WriteStringValue("premium");
                return;
        }

        writer.WriteNullValue();
    }
}

 

 读取时,此转换器会将 JSON 中使用的字符串转换为匹配的枚举值。
  写入时,此转换器将枚举值转换为写入 JSON 的自定义字符串值。CustomerType

我们创建了一个派生自的序列化程序,以便我们完全控制转换器注册顺序。SystemTextJsonSerializer

using System.Text.Json;
using Elastic.Clients.Elasticsearch.Serialization;

public class MyCustomSerializer : SystemTextJsonSerializer 
{
    private readonly JsonSerializerOptions _options;

    public MyCustomSerializer(IElasticsearchClientSettings settings) : base(settings)
    {
        var options = DefaultSourceSerializer.CreateDefaultJsonSerializerOptions(false); 

        options.Converters.Add(new CustomerTypeConverter()); 

        _options = DefaultSourceSerializer.AddDefaultConverters(options); 
    }

    protected override JsonSerializerOptions CreateJsonSerializerOptions() => _options; 
}

 

 继承自 。SystemTextJsonSerializer
  在构造函数中,使用 factory 方法创建序列化的默认选项。在此阶段没有注册默认转换器,因为我们作为参数传递。DefaultSourceSerializer.CreateDefaultJsonSerializerOptions``false
  注册我们作为第一个转换器。CustomerTypeConverter
  若要应用任何默认转换器,请调用帮助程序方法,传递要修改的选项。DefaultSourceSerializer.AddDefaultConverters
  实现返回存储 .CreateJsonSerializerOptions``JsonSerializerOptions

因为我们在默认转换器(包括 )之前注册了我们的转换器,所以在序列化源文档上的实例时,我们的转换器优先。CustomerTypeConverter``JsonStringEnumConverter``CustomerType

基类处理绑定的实现细节,这是确保内置转换器可以访问所需的位置所必需的。SystemTextJsonSerializer``IElasticsearchClientSettings

然后,我们可以将客户文档索引到 Elasticsearch 中。

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Elastic.Transport;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Serialization;

var customer = new Customer
{
    CustomerName = "Customer Ltd",
    CustomerType = CustomerType.Enhanced
};

var indexResponse = await client.IndexAsync(customer, "my-index-name");

 

实例使用自定义转换器进行序列化,并创建以下 JSON 文档。Customer``enum

{
  "customerName": "Customer Ltd",
  "customerType": "premium" 
}

 

 序列化期间应用的字符串值由我们的自定义转换器提供。
   
创建自定义Serializer

假设你更喜欢对源类型使用备用 JSON 序列化库。在这种情况下,可以注入一个独立的序列化程序,以便调用该序列化程序以进行序列化,或期望写入和返回用户提供的值的任何位置。_source``_fields

从技术上讲,实现足以创建自定义源序列化程序。Elastic.Transport.Serializer

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Elastic.Transport;

public class VanillaSerializer : Serializer
{
    public override object Deserialize(Type type, Stream stream) =>
        throw new NotImplementedException();

    public override T Deserialize<T>(Stream stream) =>
        throw new NotImplementedException();

    public override ValueTask<object> DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default) =>
        throw new NotImplementedException();

    public override ValueTask<T> DeserializeAsync<T>(Stream stream, CancellationToken cancellationToken = default) =>
        throw new NotImplementedException();

    public override void Serialize<T>(T data, Stream stream, SerializationFormatting formatting = SerializationFormatting.None) =>
        throw new NotImplementedException();

    public override Task SerializeAsync<T>(T data, Stream stream,
        SerializationFormatting formatting = SerializationFormatting.None, CancellationToken cancellationToken = default) =>
            throw new NotImplementedException();
}

 

注册序列化程序在构造函数中执行。ConnectionSettings

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Elastic.Transport;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.Serialization;

var nodePool = new SingleNodePool(new Uri("http://localhost:9200"));
var settings = new ElasticsearchClientSettings(
    nodePool,
    sourceSerializer: (defaultSerializer, settings) =>
        new VanillaSerializer()); 
var client = new ElasticsearchClient(settings);

 

 如果实现就足够了,为什么我们必须提供一个包装在工厂中的实例?Serializer``Func
   

在各种情况下,您可能有一个 POCO 类型,该类型包含类型作为其属性之一。委托提供对默认内置序列化程序的访问,以便您可以在必要时访问它。例如,考虑是否要使用渗透;您需要将 Elasticsearch 查询存储为文档的一部分,这意味着您需要有一个看起来像这样的 POCO。Elastic.Clients.Elasticsearch``SourceSerializerFactory``_source

using Elastic.Clients.Elasticsearch.QueryDsl;

public class MyPercolationDocument
{
    public Query Query { get; set; }
    public string Category { get; set; }
}

 

自定义序列化程序不知道如何序列化或可能显示为 文档的。因此,您的自定义需要存储对我们内置序列化程序的引用,并将 Elastic 类型的序列化委托回该引用。Query``Elastic.Clients.Elasticsearch``_source``Serializer

 

4.6 使用.NET客户端

 

重复使用同一客户端实例

在使用 Elasticsearch .NET 客户端时,我们建议消费者重用单个 的实例,用于应用程序的整个生存期。 重用同一实例时:ElasticsearchClient

  • 初始化开销仅限于第一次使用。

  • TCP 连接等资源可以池化并重用以改进 效率。

  • 减少了序列化开销,从而提高了性能。

该类型是线程安全的,可以共享和重用 跨使用应用程序中的多个线程。可实现客户端重用 通过创建单一实例静态实例或将类型注册到 使用依赖关系注入容器时的单一实例生存期。ElasticsearchClient

首选异步方法

Elasticsearch .NET 客户端在 .我们建议始终首选异步方法, 具有后缀。使用 Elasticsearch .NET 客户端需要发送 HTTP 请求 Elasticsearch 服务器。访问 Elasticsearch 有时会很慢或延迟,有些 复杂查询可能需要几秒钟才能返回。如果此类操作是 通过调用同步方法阻止,线程必须等到 HTTP 请求已完成。在高负载情况下,这可能会导致大量线程 使用情况,可能会影响消耗的吞吐量和性能 应用。通过首选异步方法,应用程序线程可以 继续执行不依赖于 Web 资源的其他工作,直到 可能阻止任务已完成。ElasticsearchClient``Async

 

CRUD 使用示例

本页帮助您了解如何执行各种基本的 Elasticsearch CRUD 使用 .NET 客户端(创建、读取、更新、删除)操作。它演示了 如何通过将对象索引到 Elasticsearch 中创建文档,读回文档, 按 ID 检索它或执行搜索,更新 记录并删除特定文档。

这些示例假设您有一个可通过名为的局部变量和几个 using 指令访问的实例 您的 C# 文件。ElasticsearchClient``client

using System;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;
var client = new ElasticsearchClient(); 

 

 默认构造函数,假设一个不安全的 Elasticsearch 服务器正在运行,并且 暴露在 http://localhost:9200 上。有关示例,请参阅连接 连接到安全服务器和 Elastic Cloud 部署。
   

这些示例对表示推文的数据进行操作。推文在 使用名为 Tweet 的 C# 类的客户端应用程序,其中包含多个属性 映射到存储在 Elasticsearch 中的文档结构。

public class Tweet
{
    public int Id { get; set; } 
    public string User { get; set; }
    public DateTime PostDate { get; set; }
    public string Message { get; set; }
}

 

 默认情况下,.NET 客户端将尝试查找在 .class。当存在这样的属性时,它会将文档索引到 Elasticsearch 中 使用此属性的值指定的 ID。Id
   

为文档编制索引

可以通过创建表示推文和 通过客户端将其索引。在这些示例中,我们将使用名为 my-tweet-index 的索引。

var tweet = new Tweet 
{
    Id = 1,
    User = "stevejgordon",
    PostDate = new DateTime(2009, 11, 15),
    Message = "Trying out the client, so far so good?"
};

var response = await client.IndexAsync(tweet, "my-tweet-index"); 

if (response.IsValidResponse) 
{
    Console.WriteLine($"Index document with ID {response.Id} succeeded."); 
}

 

 创建设置了相关属性的类的实例。Tweet
  首选需要等待响应的异步 API。
  检查响应上的属性以确认请求和 操作成功。IsValid
  如有必要,访问属性,例如 ID。IndexResponse

获取文档

var response = await client.GetAsync<Tweet>(1, idx => idx.Index("my-tweet-index")); 

if (response.IsValidResponse)
{
    var tweet = response.Source; 
}

 

 与 Elasticsearch JSON 响应进行一对一映射。GetResponse
  原始文档作为 Tweet 类的实例反序列化, 可通过酒店回复。Source

搜索文档

客户端公开了一个流畅的接口和一个强大的查询DSL,用于搜索。

var response = await client.SearchAsync<Tweet>(s => s 
    .Index("my-tweet-index") 
    .From(0)
    .Size(10)
    .Query(q => q
        .Term(t => t.User, "stevejgordon") 
    )
);

if (response.IsValidResponse)
{
    var tweet = response.Documents.FirstOrDefault(); 
}

 

 泛型类型参数指定类,在以下情况下使用该类 从响应中反序列化命中。Tweet
  如果在 上配置了 ,或者在映射时配置了特定索引,则可以省略索引 这种类型。`DefaultIndexElasticsearchClientSettings
  针对字段执行术语查询,搜索创作的推文 由用户史蒂夫戈登user
  可通过集合访问与查询匹配的文档 属性上的 .Documents``SearchResponse

您可能更喜欢对请求使用对象初始值设定项语法,如果 lambda 不是你的事。

var request = new SearchRequest("my-tweet-index") 
{
    From = 0,
    Size = 10,
    Query = new TermQuery("user") { Value = "stevejgordon" }
};

var response = await client.SearchAsync<Tweet>(request); 

if (response.IsValidResponse)
{
    var tweet = response.Documents.FirstOrDefault();
}

 

 创建 的实例,设置属性以控制 搜索操作。SearchRequest
  将请求传递给客户端上的方法。SearchAsync

更新文档

可以通过多种方式更新文档,包括提供完整的 替换现有文档 ID。

tweet.Message = "This is a new message"; 

var response = await client.UpdateAsync<Tweet, Tweet>("my-tweet-index", 1, u => u
    .Doc(tweet)); 

if (response.IsValidResponse)
{
    Console.WriteLine("Update document succeeded.");
}

 

 更新现有推文实例上的属性。
  在更新请求中发送更新的推文对象。

删除文档

可以通过提供要删除的文档的 ID 来删除文档。

var response = await client.DeleteAsync("my-tweet-index", 1);

if (response.IsValidResponse)
{
    Console.WriteLine("Delete document succeeded.");
}

 

4.7 日志记录-故障排除

使用 OnRequestDone 进行日志记录

构造要传递给客户端的连接设置时,可以将类型的回调传递给每次 收到响应(好或坏)。Action<IApiCallDetails>``OnRequestCompleted

如果您有复杂的日志记录需求,这是一个添加它的好地方 因为您可以访问请求和响应详细信息。

在此示例中,我们将使用连接设置来每次递增一个计数器 它被称为。OnRequestCompleted

var counter = 0;
var client = new ElasticClient(new AlwaysInMemoryConnectionSettings().OnRequestCompleted(r => counter++)); 

client.RootNodeInfo(); 
counter.Should().Be(1);

await client.RootNodeInfoAsync(); 
counter.Should().Be(2);

 

 构造客户端
  进行同步调用并断言计数器递增
  进行异步调用并断言计数器递增

OnRequestCompleted即使抛出异常,也会调用,因此即使客户端 配置为引发异常

var counter = 0;
var client = FixedResponseClient.Create( 
    new { },
    500,
    connectionSettings => connectionSettings
        .ThrowExceptions() 
        .OnRequestCompleted(r => counter++)
);

Assert.Throws<TransportException>(() => client.RootNodeInfo()); 
counter.Should().Be(1);

await Assert.ThrowsAsync<TransportException>(async () => await client.RootNodeInfoAsync());
counter.Should().Be(2);

 

 使用始终返回 HTTP 500 响应的连接配置客户端
  当调用导致异常时,始终引发异常
  断言引发异常并且计数器递增

下面是一个用于更复杂的日志记录的示例OnRequestCompleted()

默认情况下,客户端直接写入请求流,并直接从 响应流。

如果您还想捕获请求和/或响应字节, 您还需要设置为 。.DisableDirectStreaming()``true

var list = new List<string>();
var connectionPool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));

var settings = new ConnectionSettings(connectionPool, new InMemoryConnection()) 
    .DefaultIndex("default-index")
    .DisableDirectStreaming() 
    .OnRequestCompleted(apiCallDetails => 
    {
        // log out the request and the request body, if one exists for the type of request
        if (apiCallDetails.RequestBodyInBytes != null)
        {
            list.Add(
                $"{apiCallDetails.HttpMethod} {apiCallDetails.Uri} " +
                $"{Encoding.UTF8.GetString(apiCallDetails.RequestBodyInBytes)}");
        }
        else
        {
            list.Add($"{apiCallDetails.HttpMethod} {apiCallDetails.Uri}");
        }

        // log out the response and the response body, if one exists for the type of response
        if (apiCallDetails.ResponseBodyInBytes != null)
        {
            list.Add($"Status: {apiCallDetails.HttpStatusCode}" +
                     $"{Encoding.UTF8.GetString(apiCallDetails.ResponseBodyInBytes)}");
        }
        else
        {
            list.Add($"Status: {apiCallDetails.HttpStatusCode}");
        }
    });

var client = new ElasticClient(settings);

var syncResponse = client.Search<object>(s => s 
    .AllIndices()
    .Scroll("2m")
    .Sort(ss => ss
        .Ascending(SortSpecialField.DocumentIndexOrder)
    )
);
list.Count.Should().Be(2);

var asyncResponse = await client.SearchAsync<object>(s => s
.AllIndices()
.Scroll("10m")
.Sort(ss => ss
.Ascending(SortSpecialField.DocumentIndexOrder)
)
);

list.Count.Should().Be(4);
list.Should().BeEquivalentTo(new[]
{
@"POST http://localhost:9200/_all/_search?typed_keys=true&scroll=2m {""sort"":[{""_doc"":{""order"":""asc""}}]}",
@"Status: 200",
@"POST http://localhost:9200/_all/_search?typed_keys=true&scroll=10m {""sort"":[{""_doc"":{""order"":""asc""}}]}",
@"Status: 200"
});
 


 在这里我们使用,但在实际应用程序中,您将使用实际发送请求的InMemoryConnection``IConnection``HttpConnection
  禁用直接流式传输,以便我们可以捕获请求和响应字节
  在请求完成时执行某些操作。在这里,我们只是添加到列表中,但在您的应用程序中,您可能正在记录到一个文件。
  进行同步调用
  进行异步调用
  断言列表包含传递给的委托中写入的内容OnRequestCompleted

在生产环境中运行应用程序时,您可能不希望禁用所有请求的直接流式处理,因为这样做会产生性能开销,因为缓冲请求和 内存中的响应字节。但是,以临时方式捕获请求和响应可能很有用, 也许是为了解决生产中的问题。

DisableDirectStreaming`为此,可以基于*每个请求*启用。在使用此功能时, 可以配置常规日志记录机制 仅在必要时请求和响应`OnRequestCompleted
var list = new List<string>();
var connectionPool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));

var settings = new ConnectionSettings(connectionPool, new InMemoryConnection())
    .DefaultIndex("default-index")
    .OnRequestCompleted(apiCallDetails =>
    {
        // log out the request and the request body, if one exists for the type of request
        if (apiCallDetails.RequestBodyInBytes != null)
        {
            list.Add(
                $"{apiCallDetails.HttpMethod} {apiCallDetails.Uri} " +
                $"{Encoding.UTF8.GetString(apiCallDetails.RequestBodyInBytes)}");
        }
        else
        {
            list.Add($"{apiCallDetails.HttpMethod} {apiCallDetails.Uri}");
        }

        // log out the response and the response body, if one exists for the type of response
        if (apiCallDetails.ResponseBodyInBytes != null)
        {
            list.Add($"Status: {apiCallDetails.HttpStatusCode}" +
                     $"{Encoding.UTF8.GetString(apiCallDetails.ResponseBodyInBytes)}");
        }
        else
        {
            list.Add($"Status: {apiCallDetails.HttpStatusCode}");
        }
    });

var client = new ElasticClient(settings);

var syncResponse = client.Search<object>(s => s 
    .AllIndices()
    .Scroll("2m")
    .Sort(ss => ss
        .Ascending(SortSpecialField.DocumentIndexOrder)
    )
);

list.Count.Should().Be(2);

var asyncResponse = await client.SearchAsync<object>(s => s 
    .RequestConfiguration(r => r
        .DisableDirectStreaming()
    )
    .AllIndices()
    .Scroll("10m")
    .Sort(ss => ss
        .Ascending(SortSpecialField.DocumentIndexOrder)
    )
);

list.Count.Should().Be(4);
list.Should().BeEquivalentTo(new[]
{
    @"POST http://localhost:9200/_all/_search?typed_keys=true&scroll=2m", 
    @"Status: 200",
    @"POST http://localhost:9200/_all/_search?typed_keys=true&scroll=10m {""sort"":[{""_doc"":{""order"":""asc""}}]}", 
    @"Status: 200"
});

 

 进行同步调用,其中请求和响应字节不会被缓冲
  在启用的位置进行异步调用DisableDirectStreaming()
  仅捕获第一个请求的方法和 url
  捕获第二个请求的正文

 

4.8 调试

审计跟踪

Elasticsearch.Net 和 NEST 为请求管道中的事件提供审核跟踪,这些事件 在发出请求时发生。此审核跟踪在响应中可用,如 以下示例。

我们将在这里使用嗅探连接池,因为它在启动时嗅探并在之前执行 ping 操作 第一次使用,因此我们可以获得包含一些事件的审计跟踪

var pool = new SniffingConnectionPool(new []{ TestConnectionSettings.CreateUri() });
var connectionSettings = new ConnectionSettings(pool)
    .DefaultMappingFor<Project>(i => i
        .IndexName("project")
    );

var client = new ElasticClient(connectionSettings);

 

发出以下请求后

var response = client.Search<Project>(s => s
    .MatchAll()
);

 

审核跟踪在人工调试信息中提供 可读时尚,类似于

Valid NEST response built from a successful low level call on POST: /project/doc/_search
# Audit trail of this API call:
 - [1] SniffOnStartup: Took: 00:00:00.0360264
 - [2] SniffSuccess: Node: http://localhost:9200/ Took: 00:00:00.0310228
 - [3] PingSuccess: Node: http://127.0.0.1:9200/ Took: 00:00:00.0115074
 - [4] HealthyResponse: Node: http://127.0.0.1:9200/ Took: 00:00:00.1477640
# Request:
<Request stream not captured or already read to completion by serializer. Set DisableDirectStreaming() on ConnectionSettings to force it to be set on the response.>
# Response:
<Response stream not captured or already read to completion by serializer. Set DisableDirectStreaming() on ConnectionSettings to force it to be set on the response.>

 

帮助排除故障

var debug = response.DebugInformation;

 

但也可以手动访问:

response.ApiCall.AuditTrail.Count.Should().Be(4, "{0}", debug);
response.ApiCall.AuditTrail[0].Event.Should().Be(SniffOnStartup, "{0}", debug);
response.ApiCall.AuditTrail[1].Event.Should().Be(SniffSuccess, "{0}", debug);
response.ApiCall.AuditTrail[2].Event.Should().Be(PingSuccess, "{0}", debug);
response.ApiCall.AuditTrail[3].Event.Should().Be(HealthyResponse, "{0}", debug);

 

每个审核都有一个开始和结束,这将提供 对需要多长时间有所了解DateTime

response.ApiCall.AuditTrail
    .Should().OnlyContain(a => a.Ended - a.Started >= TimeSpan.Zero);

 

审计跟踪

Elasticsearch.Net 和 NEST 为请求管道中的事件提供审核跟踪,这些事件 在发出请求时发生。此审核跟踪在响应中可用,如 以下示例。

我们将在这里使用嗅探连接池,因为它在启动时嗅探并在之前执行 ping 操作 第一次使用,因此我们可以获得包含一些事件的审计跟踪

var pool = new SniffingConnectionPool(new []{ TestConnectionSettings.CreateUri() });
var connectionSettings = new ConnectionSettings(pool)
    .DefaultMappingFor<Project>(i => i
        .IndexName("project")
    );

var client = new ElasticClient(connectionSettings);

 

发出以下请求后

var response = client.Search<Project>(s => s
    .MatchAll()
);

 

审核跟踪在人工调试信息中提供 可读时尚,类似于

Valid NEST response built from a successful low level call on POST: /project/doc/_search
# Audit trail of this API call:
 - [1] SniffOnStartup: Took: 00:00:00.0360264
 - [2] SniffSuccess: Node: http://localhost:9200/ Took: 00:00:00.0310228
 - [3] PingSuccess: Node: http://127.0.0.1:9200/ Took: 00:00:00.0115074
 - [4] HealthyResponse: Node: http://127.0.0.1:9200/ Took: 00:00:00.1477640
# Request:
<Request stream not captured or already read to completion by serializer. Set DisableDirectStreaming() on ConnectionSettings to force it to be set on the response.>
# Response:
<Response stream not captured or already read to completion by serializer. Set DisableDirectStreaming() on ConnectionSettings to force it to be set on the response.>

 

帮助排除故障

var debug = response.DebugInformation;

 

但也可以手动访问:

response.ApiCall.AuditTrail.Count.Should().Be(4, "{0}", debug);
response.ApiCall.AuditTrail[0].Event.Should().Be(SniffOnStartup, "{0}", debug);
response.ApiCall.AuditTrail[1].Event.Should().Be(SniffSuccess, "{0}", debug);
response.ApiCall.AuditTrail[2].Event.Should().Be(PingSuccess, "{0}", debug);
response.ApiCall.AuditTrail[3].Event.Should().Be(HealthyResponse, "{0}", debug);

 

每个审核都有一个开始和结束,这将提供 对需要多长时间有所了解DateTime

response.ApiCall.AuditTrail
    .Should().OnlyContain(a => a.Ended - a.Started >= TimeSpan.Zero);

 

 

审计跟踪

Elasticsearch.Net 和 NEST 为请求管道中的事件提供审核跟踪,这些事件 在发出请求时发生。此审核跟踪在响应中可用,如 以下示例。

我们将在这里使用嗅探连接池,因为它在启动时嗅探并在之前执行 ping 操作 第一次使用,因此我们可以获得包含一些事件的审计跟踪

var pool = new SniffingConnectionPool(new []{ TestConnectionSettings.CreateUri() });
var connectionSettings = new ConnectionSettings(pool)
    .DefaultMappingFor<Project>(i => i
        .IndexName("project")
    );

var client = new ElasticClient(connectionSettings);

 

发出以下请求后

var response = client.Search<Project>(s => s
    .MatchAll()
);

 

审核跟踪在人工调试信息中提供 可读时尚,类似于

Valid NEST response built from a successful low level call on POST: /project/doc/_search
# Audit trail of this API call:
 - [1] SniffOnStartup: Took: 00:00:00.0360264
 - [2] SniffSuccess: Node: http://localhost:9200/ Took: 00:00:00.0310228
 - [3] PingSuccess: Node: http://127.0.0.1:9200/ Took: 00:00:00.0115074
 - [4] HealthyResponse: Node: http://127.0.0.1:9200/ Took: 00:00:00.1477640
# Request:
<Request stream not captured or already read to completion by serializer. Set DisableDirectStreaming() on ConnectionSettings to force it to be set on the response.>
# Response:
<Response stream not captured or already read to completion by serializer. Set DisableDirectStreaming() on ConnectionSettings to force it to be set on the response.>

 

帮助排除故障

var debug = response.DebugInformation;

 

但也可以手动访问:

response.ApiCall.AuditTrail.Count.Should().Be(4, "{0}", debug);
response.ApiCall.AuditTrail[0].Event.Should().Be(SniffOnStartup, "{0}", debug);
response.ApiCall.AuditTrail[1].Event.Should().Be(SniffSuccess, "{0}", debug);
response.ApiCall.AuditTrail[2].Event.Should().Be(PingSuccess, "{0}", debug);
response.ApiCall.AuditTrail[3].Event.Should().Be(HealthyResponse, "{0}", debug);

 

每个审核都有一个开始和结束,这将提供 对需要多长时间有所了解DateTime

response.ApiCall.AuditTrail
    .Should().OnlyContain(a => a.Ended - a.Started >= TimeSpan.Zero);

 

 当然也可以使用其它的分词搜索工具:https://docs.meilisearch.com/

 

参考文档:

https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/release-notes-8.0.0.html (官方文档)

https://elasticsearch.bookhub.tech/rest_apis/autoscaling_apis/ (api文档)

https://www.cnblogs.com/yswenli/p/6266569.html (如何使用)

https://88250.b3log.org/full-text-search-elasticsearch#b3_solo_h3_0

https://blog.csdn.net/catshitone/article/details/95939413 (安装步骤 推荐看)

https://www.jianshu.com/p/c3d15fbfec36

https://docs.meowv.com/stack/dotnetcore/elasticsearch-in-dotnet.html (在最新.NET7版本有问题)

https://www.cnblogs.com/coderxz/p/13268417.html

https://docs.kilvn.com/elasticsearch/docs/1.html (中文文档)

https://zhuanlan.zhihu.com/p/54384152

https://juejin.cn/post/7009593798011387917

posted on 2023-04-12 14:25  青春似雨后霓虹  阅读(1004)  评论(1编辑  收藏  举报