Elasticsearch 系列(七)- 在ASP.NET Core中使用高级客户端NEST来操作Elasticsearch

本章将和大家分享在ASP.NET Core中如何使用高级客户端NEST来操作我们的Elasticsearch。

NEST是一个高级别的Elasticsearch .NET客户端,它仍然非常接近原始Elasticsearch API的映射。所有的请求和响应都是通过类型来暴露的,这使得它非常适合快速上手和运行。

在底层,NEST使用Elasticsearch.Net低级客户端来发送请求和接收响应,使用并扩展了Elasticsearch.Net中的许多类型。这个低级客户端本身仍然可以通过高级客户端的 .LowLevel 属性来暴露。

高级客户端NEST官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/nest-getting-started.html

废话不多说,首选我们来看一下Demo的目录结构,如下所示:

本Demo的Web项目为ASP.NET Core Web 应用程序(目标框架为.NET 8.0) MVC项目。

ORM框架用的是SqlSugarCore,实体映射用的是AutoMapper,DI框架用的是Autofac。

高级客户端NEST版本用的 7.12.1 ,和Elasticsearch的版本保持一致。

一、连接Elasticsearch

1、单节点连接

var settings = new ConnectionSettings(new Uri("http://example.com:9200"))
    .DefaultIndex("people");

var client = new ElasticClient(settings);

2、多节点连接

var uris = new[]
{
    new Uri("http://localhost:9200"),
    new Uri("http://localhost:9201"),
    new Uri("http://localhost:9202"),
};

var connectionPool = new SniffingConnectionPool(uris);
var settings = new ConnectionSettings(connectionPool)
    .DefaultIndex("people");

var client = new ElasticClient(settings);

二、调试(Debugging)

官方文档:

https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/debug-information.html

https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/debug-mode.html

https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/logging-with-on-request-completed.html

在使用NEST开发Elasticsearch应用程序时,查看NEST生成并发送给Elasticsearch的请求以及Elasticsearch返回的响应信息是非常有价值的。

我们直接来看一个示例,核心代码如下:

using System.Text;
using Microsoft.Extensions.Configuration;
using Nest;
using Elasticsearch.Net;

namespace TianYaSharpCore.Elasticsearch
{
    /// <summary>
    /// ElasticClient提供者
    /// NEST官方文档:https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/nest-getting-started.html#nest-getting-started
    /// </summary>
    public class ElasticClientProvider : IElasticClientProvider
    {
        /// <summary>
        /// Linq查询的官方Client(高级客户端)
        /// </summary>
        public IElasticClient ElasticLinqClient { get; set; }

        /// <summary>
        /// Json查询的官方Client(低级客户端)
        /// </summary>
        public IElasticLowLevelClient ElasticJsonClient { get; set; }

        /// <summary>
        /// 构造函数
        /// </summary>
        public ElasticClientProvider(IConfiguration configuration)
        {
            /*
                var uris = new[]
                {
                    new Uri("http://localhost:9200"),
                    new Uri("http://localhost:9201"),
                    new Uri("http://localhost:9202"),
                }; 
            */
            var uris = configuration["ElasticsearchConfig:Uris"];
            var defaultIndex = configuration["ElasticsearchConfig:DefaultIndex"]; //默认索引库名称
            var uriList = uris?.Split(new char[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
                .ToList().ConvertAll(u => new Uri(u)) ?? new List<Uri>();
            if (uriList.Count <= 0)
            {
                uriList.Add(new Uri("http://localhost:9200"));
            }

            if (string.IsNullOrEmpty(defaultIndex))
            {
                defaultIndex = "defaultIndex";
            }

            var list = new List<string>();
            var connectionPool = new SniffingConnectionPool(uriList); //连接池
            var settings = new ConnectionSettings(connectionPool)
                //.BasicAuthentication("root", "123456")     //验证账号密码登录
                .RequestTimeout(TimeSpan.FromSeconds(30))  //请求超时 30s
                .DefaultFieldNameInferrer(fieldName => fieldName) //移除NEST将类型属性名称序列化为驼峰式命名的默认行为

                /* Debug调试开始 */
                // 请注意,启用详细的调试信息可能会对性能产生影响,并且可能会占用更多的内存来存储额外的信息。
                // 因此,在生产环境中应该禁用它,只在开发或故障排除时启用。
                // 在生产环境中排查问题时建议使用 RequestConfiguration() 以针对某个请求单独禁用直接流处理以捕获请求和响应的字节。
                // 官方文档:
                // https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/debug-mode.html
                // https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/debug-information.html
                // https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/logging-with-on-request-completed.html
                .EnableDebugMode() // 启用详细的调试信息(在生产环境中应该禁用它,只在开发或故障排除时启用)

                //.DisableDirectStreaming() // 禁用直接流处理以捕获请求和响应的字节
                //.PrettyJson() // 返回格式化的JSON响应

                // 这个回调会在每次请求完成(无论成功还是失败)时被调用
                .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}");
                    }
                })
                /* Debug调试结束 */

                .DefaultIndex(defaultIndex);

            ElasticLinqClient = new ElasticClient(settings);
            ElasticJsonClient = ElasticLinqClient.LowLevel; //高级客户端 NEST 可以通过访问客户端上的 .LowLevel 属性来获取 Elasticsearch.Net 低级客户端
        }
    }
}

其中 .EnableDebugMode() 表示启用详细的调试信息。请注意,启用详细的调试信息可能会对性能产生影响,并且可能会占用更多的内存来存储额外的信息。因此,在生产环境中应该禁用它,只在开发或故障排除时启用。在生产环境中排查问题时建议使用 RequestConfiguration() 以针对某个请求单独禁用直接流处理以捕获请求和响应的字节。

其中 .OnRequestCompleted() 这个回调会在每次请求完成(无论成功还是失败)时被调用。如果您有复杂的日志记录需求,这是一个很好的地方来实现它们,因为您可以访问到请求和响应的详细信息。根据您的具体需求,您可能需要在回调中执行更复杂的逻辑,比如记录详细的日志、发送警报或执行其他业务逻辑。

需要注意的是,此处的 .EnableDebugMode() 配置是针对所有的请求都生效的。在生产环境中,您可能不希望为所有请求都禁用直接流传输,因为这样做会由于在内存中缓存请求和响应字节而产生性能开销。

为此,可以针对每个请求单独启用 DisableDirectStreaming 功能,如下所示:

/// <summary>
/// 调试
/// </summary>
public async Task DebugInformationAsync()
{
    // 其中HotelDoc类为自定义酒店数据对应的ES文档
    var searchResponse = await _elasticClientProvider.ElasticLinqClient.SearchAsync<HotelDoc>(s => s
        .RequestConfiguration(r => r
            // 官方文档:https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/logging-with-on-request-completed.html
            // 在生产环境中运行应用程序时,您可能不希望为所有请求都禁用直接流传输,因为这样做会由于在内存中缓存请求和响应字节而产生性能开销。
            // 然而,在临时需要时捕获请求和响应可能是有用的,比如为了排查生产环境中的问题。
            // 为此,可以针对每个请求单独启用 DisableDirectStreaming 功能。
            // 利用此功能,可以在 OnRequestCompleted 中配置一个通用的日志记录机制,并仅在必要时记录请求和响应信息。
            .DisableDirectStreaming() // 仅针对此请求禁用直接流
        )
        .From(0)
        .Size(2)
        .Query(q => q
                .Match(m => m
                .Field(f => f.city)
                .Query("上海")
                )
        )
    );

    // 每个响应都包含一个DebugInformation属性
    // 访问DebugInformation属性来获取调试信息
    string debugInfo = searchResponse.DebugInformation;
}

当你使用 Elasticsearch.Net 和 NEST 客户端库与 Elasticsearch 服务器进行交互时,每个响应对象都包含一个 DebugInformation 属性,该属性提供了有关请求和响应的详细信息,以帮助你进行调试和故障排除。通过配置 ConnectionSettings 和 RequestConfiguration 上的属性,你可以控制哪些额外的信息被包含在调试信息中。这些控制可以针对所有请求统一设置,或者针对每个请求单独设置。

具体来说:

  • ConnectionSettings:允许你在客户端初始化时全局设置调试信息的详细程度。例如,你可以决定是否包含请求正文、响应正文或是时间戳等信息。
  • RequestConfiguration:提供了更细粒度的控制,使得你能够为特定的请求覆盖全局设置,添加或移除某些调试信息项目。这意味着对于某个特别关注的请求,你可以增加更多的调试细节,而不影响其他请求的输出。

最后我们来看一下 searchResponse.DebugInformation 输出的具体内容,如下所示:

Valid NEST response built from a successful (200) low level call on POST: /hotel/_search?pretty=true&error_trace=true&typed_keys=true
# Audit trail of this API call:
 - [1] SniffOnStartup: Took: 00:00:00.2151884
 - [2] SniffSuccess: Node: http://localhost:9200/ Took: 00:00:00.2027398
 - [3] PingSuccess: Node: http://127.0.0.1:9200/ Took: 00:00:00.0043129
 - [4] HealthyResponse: Node: http://127.0.0.1:9200/ Took: 00:00:00.0943867
# Request:
{"from":0,"query":{"match":{"city":{"query":"上海"}}},"size":2}
# Response:
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 83,
      "relation" : "eq"
    },
    "max_score" : 0.88342106,
    "hits" : [
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "36934",
        "_score" : 0.88342106,
        "_source" : {
          "id" : 36934,
          "name" : "7天连锁酒店(上海宝山路地铁站店)",
          "address" : "静安交通路40号",
          "price" : 336,
          "score" : 37,
          "brand" : "7天酒店",
          "city" : "上海",
          "starName" : "二钻",
          "business" : "四川北路商业区",
          "location" : "31.251433, 121.47522",
          "pic" : "https://m.tuniucdn.com/fb2/t1/G1/M00/3E/40/Cii9EVkyLrKIXo1vAAHgrxo_pUcAALcKQLD688AAeDH564_w200_h200_c1_t0.jpg",
          "suggestion" : [
            "7天酒店",
            "四川北路商业区"
          ]
        }
      },
      {
        "_index" : "hotel",
        "_type" : "_doc",
        "_id" : "38609",
        "_score" : 0.88342106,
        "_source" : {
          "id" : 38609,
          "name" : "速8酒店(上海赤峰路店)",
          "address" : "广灵二路126号",
          "price" : 249,
          "score" : 35,
          "brand" : "速8",
          "city" : "上海",
          "starName" : "二钻",
          "business" : "四川北路商业区",
          "location" : "31.282444, 121.479385",
          "pic" : "https://m.tuniucdn.com/fb2/t1/G2/M00/DF/96/Cii-TFkx0ImIQZeiAAITil0LM7cAALCYwKXHQ4AAhOi377_w200_h200_c1_t0.jpg",
          "suggestion" : [
            "速8",
            "四川北路商业区"
          ]
        }
      }
    ]
  }
}

# TCP states:
  Established: 162
  TimeWait: 14
  SynSent: 4
  CloseWait: 13
  LastAck: 1
  FinWait1: 1

# ThreadPool statistics:
  Worker: 
    Busy: 1
    Free: 32766
    Min: 12
    Max: 32767
  IOCP: 
    Busy: 0
    Free: 1000
    Min: 1
    Max: 1000

三、索引库操作

对于索引库操作本人更倾向于使用DSL语句去执行。高级客户端 NEST 可以通过访问客户端上的 .LowLevel 属性来获取 Elasticsearch.Net 低级客户端。

在某些场景下,低级客户端非常有用,比如你已经有了代表你想发送请求的JSON,此时并不想将其转换成Fluent API或对象初始化语法,又或者客户端中存在一个可以通过发送字符串请求或匿名类型来规避的bug。

通过 .LowLevel 属性使用低级客户端意味着你可以兼得两者之长:

  • 利用高级客户端
  • 在合适的情况下使用低级客户端,同时充分利用NEST中的所有强类型及其序列化器进行反序列化。

示例:

using Nest;
using Elasticsearch.Net;
using Elasticsearch.Net.Specification.IndicesApi;

namespace TianYaSharpCore.Elasticsearch
{
    /// <summary>
    /// ES帮助类
    /// </summary>
    public class ElasticsearchHelper : IElasticsearchHelper
    {
        /*
            1、高级客户端 NEST 可以通过访问客户端上的 .LowLevel 属性来获取 Elasticsearch.Net 低级客户端。

            2、在某些场景下,低级客户端非常有用,比如你已经有了代表你想发送请求的JSON,此时并不想将其转换成Fluent API或对象初始化语法,
               又或者客户端中存在一个可以通过发送字符串请求或匿名类型来规避的bug。

            3、通过 .LowLevel 属性使用低级客户端意味着你可以兼得两者之长:
                *利用高级客户端
                *在合适的情况下使用低级客户端,同时充分利用NEST中的所有强类型及其序列化器进行反序列化。
        */

        private readonly IElasticClientProvider _elasticClientProvider;
        public ElasticsearchHelper(IElasticClientProvider elasticClientProvider)
        {
            _elasticClientProvider = elasticClientProvider;
        }

        #region 索引库操作

        /// <summary>
        /// 判断某个索引库是否存在
        /// </summary>
        /// <param name="indexName">索引库名称</param>
        /// <returns>返回true表示已存在</returns>
        public async Task<bool> IsIndexExistsAsync(string indexName)
        {
            ExistsResponse existsResponse = await _elasticClientProvider.ElasticLinqClient.Indices
                .ExistsAsync(indexName);
            return existsResponse.IsValid && existsResponse.Exists;
        }

        /// <summary>
        /// 创建索引库
        /// </summary>
        /// <param name="indexName">索引库名称</param>
        /// <param name="dsl">用于创建索引库的DSL语句</param>
        /// <returns>返回true表示创建索引库成功</returns>
        public async Task<bool> CreateIndexAsync(string indexName, string dsl)
        {
            // 发送PUT请求到 Elasticsearch 创建索引  
            CreateIndexResponse createIndexResponse = await _elasticClientProvider.ElasticJsonClient.Indices
                .CreateAsync<CreateIndexResponse>(indexName, PostData.String(dsl));
            return createIndexResponse.IsValid && createIndexResponse.Acknowledged;
        }

        /// <summary>
        /// 创建索引库
        /// </summary>
        /// <param name="indexName">索引库名称</param>
        /// <param name="body">请求数据</param>
        /// <returns>返回true表示创建索引库成功</returns>
        public async Task<bool> CreateIndexAsync(string indexName, PostData body,
            CreateIndexRequestParameters requestParameters = null, CancellationToken ctx = default(CancellationToken))
        {
            // 发送PUT请求到 Elasticsearch 创建索引  
            CreateIndexResponse createIndexResponse = await _elasticClientProvider.ElasticJsonClient.Indices
                .CreateAsync<CreateIndexResponse>(indexName, body, requestParameters, ctx);
            return createIndexResponse.IsValid && createIndexResponse.Acknowledged;
        }

        /// <summary>
        /// 修改索引库(注意:索引库和mapping一旦创建无法修改,但是可以添加新的字段。)
        /// </summary>
        /// <param name="indexName">索引库名称</param>
        /// <param name="dsl">用于修改索引库的DSL语句</param>
        /// <returns>返回true表示修改索引库成功</returns>
        public async Task<bool> PutMappingAsync(string indexName, string dsl)
        {
            PutMappingResponse putMappingResponse = await _elasticClientProvider.ElasticJsonClient.Indices
                .PutMappingAsync<PutMappingResponse>(indexName, PostData.String(dsl));
            return putMappingResponse.IsValid && putMappingResponse.Acknowledged;
        }

        /// <summary>
        /// 修改索引库(注意:索引库和mapping一旦创建无法修改,但是可以添加新的字段。)
        /// </summary>
        /// <param name="indexName">索引库名称</param>
        /// <param name="body">请求数据</param>
        /// <returns>返回true表示修改索引库成功</returns>
        public async Task<bool> PutMappingAsync(string indexName, PostData body,
            PutMappingRequestParameters requestParameters = null, CancellationToken ctx = default(CancellationToken))
        {
            PutMappingResponse putMappingResponse = await _elasticClientProvider.ElasticJsonClient.Indices
                .PutMappingAsync<PutMappingResponse>(indexName, body, requestParameters, ctx);
            return putMappingResponse.IsValid && putMappingResponse.Acknowledged;
        }

        /// <summary>
        /// 删除索引库
        /// </summary>
        /// <param name="indexName">索引库名称</param>
        /// <returns>返回true表示删除索引库成功</returns>
        public async Task<bool> DeleteIndexAsync(string indexName)
        {
            DeleteIndexResponse deleteIndexResponse = await _elasticClientProvider.ElasticLinqClient.Indices
                .DeleteAsync(indexName);
            return deleteIndexResponse.IsValid && deleteIndexResponse.Acknowledged;
        }

        #endregion

        #region 文档操作

        /// <summary>
        /// 获取文档
        /// </summary>
        /// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
        /// <param name="documentId">文档Id</param>
        /// <returns></returns>
        public async Task<GetResponse<TDocument>> GetAsync<TDocument>(DocumentPath<TDocument> documentId,
            Func<GetDescriptor<TDocument>, IGetRequest> selector = null, CancellationToken ct = default(CancellationToken))
            where TDocument : class
        {
            return await _elasticClientProvider.ElasticLinqClient.GetAsync(documentId, selector, ct);
        }

        /// <summary>
        /// 新增文档或全量修改文档
        /// </summary>
        /// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
        /// <param name="document">文档</param>
        /// <returns>返回true表示操作成功</returns>
        public async Task<bool> IndexDocumentAsync<TDocument>(TDocument document, CancellationToken ct = default(CancellationToken))
            where TDocument : class
        {
            IndexResponse indexResponse = await _elasticClientProvider.ElasticLinqClient.IndexDocumentAsync(document, ct);
            return indexResponse != null && indexResponse.IsValid;
        }

        /// <summary>
        /// 新增文档或全量修改文档(需要设置额外的参数时使用该方法,例如:需要指定索引库)
        /// </summary>
        /// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
        /// <param name="document">文档</param>
        /// <returns>返回true表示操作成功</returns>
        public async Task<bool> IndexAsync<TDocument>(TDocument document, Func<IndexDescriptor<TDocument>, IIndexRequest<TDocument>> selector,
            CancellationToken ct = default(CancellationToken)) where TDocument : class
        {
            IndexResponse indexResponse = await _elasticClientProvider.ElasticLinqClient.IndexAsync(document, selector, ct);
            return indexResponse != null && indexResponse.IsValid;
        }

        /// <summary>
        /// 局部修改(增量修改)文档字段,修改指定字段值
        /// </summary>
        /// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
        /// <param name="indexName">索引库名称</param>
        /// <param name="documentId">文档Id</param>
        /// <param name="dsl">修改指定字段值的DSL语句</param>
        /// <returns>返回true表示修改成功</returns>
        public async Task<bool> UpdateAsync<TDocument>(string indexName, string documentId, string dsl)
            where TDocument : class
        {
            UpdateResponse<TDocument> updateResponse = await _elasticClientProvider.ElasticJsonClient
                .UpdateAsync<UpdateResponse<TDocument>>(indexName, documentId, PostData.String(dsl));
            return updateResponse != null && updateResponse.IsValid;
        }

        /// <summary>
        /// 局部修改(增量修改)文档字段,修改指定字段值
        /// </summary>
        /// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
        /// <param name="indexName">索引库名称</param>
        /// <param name="documentId">文档Id</param>
        /// <param name="body">请求数据</param>
        /// <returns>返回true表示修改成功</returns>
        public async Task<bool> UpdateAsync<TDocument>(string indexName, string documentId, PostData body,
            UpdateRequestParameters requestParameters = null, CancellationToken ctx = default(CancellationToken))
            where TDocument : class
        {
            UpdateResponse<TDocument> updateResponse = await _elasticClientProvider.ElasticJsonClient
                .UpdateAsync<UpdateResponse<TDocument>>(indexName, documentId, body, requestParameters, ctx);
            return updateResponse != null && updateResponse.IsValid;
        }

        /// <summary>
        /// 删除文档
        /// </summary>
        /// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
        /// <param name="documentId">文档Id</param>
        /// <returns>返回true表示删除文档成功</returns>
        public async Task<bool> DeleteAsync<TDocument>(DocumentPath<TDocument> documentId,
            Func<DeleteDescriptor<TDocument>, IDeleteRequest> selector = null, CancellationToken ct = default(CancellationToken))
            where TDocument : class
        {
            DeleteResponse deleteResponse = await _elasticClientProvider.ElasticLinqClient.DeleteAsync(documentId, selector, ct);
            return deleteResponse != null && deleteResponse.IsValid;
        }

        /// <summary>
        /// 批量新增文档
        /// </summary>
        /// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
        /// <param name="objects">文档集合</param>
        /// <returns>返回true表示操作成功</returns>
        public async Task<bool> IndexManyAsync<TDocument>(IEnumerable<TDocument> objects, IndexName index = null,
            CancellationToken cancellationToken = default(CancellationToken)) where TDocument : class
        {
            BulkResponse bulkResponse = await _elasticClientProvider.ElasticLinqClient.IndexManyAsync(objects, index, cancellationToken);
            return bulkResponse != null && bulkResponse.IsValid;
        }

        /// <summary>
        /// 执行批量数据操作,包括新增、更新或删除多个文档,而只需发起一次HTTP请求
        /// </summary>
        /// <returns>返回true表示操作成功</returns>
        public async Task<bool> BulkAsync(Func<BulkDescriptor, IBulkRequest> selector, CancellationToken ct = default(CancellationToken))
        {
            BulkResponse bulkResponse = await _elasticClientProvider.ElasticLinqClient.BulkAsync(selector, ct);
            return bulkResponse != null && bulkResponse.IsValid;
        }

        #endregion

        #region 根据DSL语句查询

        /// <summary>
        /// 根据DSL语句查询
        /// </summary>
        /// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
        /// <param name="queryDsl">DSL查询语句</param>
        /// <returns></returns>
        public async Task<ISearchResponse<TDocument>> SearchAsync<TDocument>(string queryDsl)
            where TDocument : class
        {
            return await SearchAsync<TDocument>(PostData.String(queryDsl));
        }

        /// <summary>
        /// 根据DSL语句查询
        /// </summary>
        /// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
        /// <param name="indexName">索引库名称</param>
        /// <param name="queryDsl">DSL查询语句</param>
        /// <returns></returns>
        public async Task<ISearchResponse<TDocument>> SearchAsync<TDocument>(string indexName, string queryDsl)
           where TDocument : class
        {
            return await SearchAsync<TDocument>(indexName, PostData.String(queryDsl));
        }

        /// <summary>
        /// 根据DSL语句查询
        /// </summary>
        /// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
        public async Task<ISearchResponse<TDocument>> SearchAsync<TDocument>(PostData body,
            SearchRequestParameters requestParameters = null, CancellationToken ctx = default(CancellationToken))
           where TDocument : class
        {
            return await _elasticClientProvider.ElasticJsonClient.SearchAsync<SearchResponse<TDocument>>(body, requestParameters, ctx);
        }

        /// <summary>
        /// 根据DSL语句查询
        /// </summary>
        /// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
        /// <param name="indexName">索引库名称</param>
        public async Task<ISearchResponse<TDocument>> SearchAsync<TDocument>(string indexName, PostData body,
            SearchRequestParameters requestParameters = null, CancellationToken ctx = default(CancellationToken))
           where TDocument : class
        {
            return await _elasticClientProvider.ElasticJsonClient.SearchAsync<SearchResponse<TDocument>>(indexName, body, requestParameters, ctx);
        }

        #endregion
    }
}

在此处,查询我使用的是DSL语句,但是响应的主体是高级客户端 NEST 返回的相同响应类型的具体实现

NEST中提供的所有方法都提供了同步和异步两个版本,其中异步版本的方法名在末尾使用了标准的 *Async 后缀。

案例:根据提供的酒店数据创建索引库,索引库名称为hotel,mapping属性根据数据库表结构来定义。

其中 MySQL 数据库中 tb_hotel 酒店表的表结构如下所示:

CREATE TABLE `tb_hotel` (
  `id` bigint(20) NOT NULL COMMENT '酒店id',
  `name` varchar(255) NOT NULL COMMENT '酒店名称;例:7天酒店',
  `address` varchar(255) NOT NULL COMMENT '酒店地址;例:航头路',
  `price` int(10) NOT NULL COMMENT '酒店价格;例:329',
  `score` int(2) NOT NULL COMMENT '酒店评分;例:45,就是4.5分',
  `brand` varchar(32) NOT NULL COMMENT '酒店品牌;例:如家',
  `city` varchar(32) NOT NULL COMMENT '所在城市;例:上海',
  `star_name` varchar(16) DEFAULT NULL COMMENT '酒店星级,从低到高分别是:1星到5星,1钻到5钻',
  `business` varchar(255) DEFAULT NULL COMMENT '商圈;例:虹桥',
  `latitude` varchar(32) NOT NULL COMMENT '纬度;例:31.2497',
  `longitude` varchar(32) NOT NULL COMMENT '经度;例:120.3925',
  `pic` varchar(255) DEFAULT NULL COMMENT '酒店图片;例:/img/1.jpg',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

分析其数据结构,创建相应的酒店数据索引库,对应的DSL语句如下所示:

# 创建酒店数据索引库
PUT /hotel
{
  "settings": {
    "analysis": {
      "analyzer": {
        "text_anlyzer": {
          "tokenizer": "ik_max_word",
          "filter": "py"
        },
        "completion_analyzer": {
          "tokenizer": "keyword",
          "filter": "py"
        }
      },
      "filter": {
        "py": {
          "type": "pinyin",
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name": {
        "type": "text",
        "analyzer": "text_anlyzer",
        "search_analyzer": "ik_smart",
        "copy_to": "all"
      },
      "address": {
        "type": "keyword",
        "index": false
      },
      "price": {
        "type": "integer"
      },
      "score": {
        "type": "integer"
      },
      "brand": {
        "type": "keyword",
        "copy_to": "all"
      },
      "city": {
        "type": "keyword"
      },
      "starName": {
        "type": "keyword"
      },
      "business": {
        "type": "keyword",
        "copy_to": "all"
      },
      "location": {
        "type": "geo_point"
      },
      "pic": {
        "type": "keyword",
        "index": false
      },
      "all": {
        "type": "text",
        "analyzer": "text_anlyzer",
        "search_analyzer": "ik_smart"
      },
      "suggestion": {
        "type": "completion",
        "analyzer": "completion_analyzer"
      }
    }
  }
}

当然,你既可以使用 NEST 高级客户端去创建索引库,也可以直接使用 kibana 等工具创建索引库。

其中需要特别注意的是:

1)此处的 id 并不是定义成 long 类型,这是因为在 ES 中 id 字段比较特殊,我们一般都会把它定义成固定的 keyword 类型。

2)此处的 index 属性表示该字段是否需要参与搜索,默认值为 true 表示需要参与搜索,如果该字段不需要参与搜索则可以手动把它设置成 false 。

3)数据库中的 longitude(经度)字段 和 latitude(纬度)字段 合并成了 ES 中的 location(地理坐标) 字段,字段类型为 geo_point ,它的值其实就是字符串类型,由经度和纬度的字段值使用英文逗号拼接而成。

4)字段拷贝,可以使用 copy_to 属性将当前字段拷贝到指定字段上(字段名称可以随意)。我们知道根据一个字段去搜索的效率要明显高于根据多个字段去搜索的效率,但是有些需求就是需要根据多个字段去搜索,这时候就可以使用 copy_to 属性了,可以将多个需要同时搜索的字段拷贝到某个指定的字段上,将来再根据这个指定的字段来搜索,这样子就可以提升查询效率了。copy_to 能够实现在一个字段里面搜索多个字段的内容,而且这种拷贝它还做了优化,它并不是真的把文档拷贝进去了,而只是基于它创建倒排索引,所以将来你去查的时候其实你是看不到这个字段的,虽然看不到但是搜索却能根据它搜,这就很完美了。

四、文档操作

首先我们需要创建与酒店数据相对应的实体类型,如下所示:

Hotel类(酒店数据,与MySQL数据库表字段相对应):

using SqlSugar;

namespace Demo.Domain.Entities
{
    /// <summary>
    /// 酒店数据
    /// </summary>
    [SugarTable("tb_hotel")] //指定数据库表名
    public class Hotel
    {
        /// <summary>
        /// 酒店id
        /// </summary>
        [SugarColumn(IsPrimaryKey = true)] //数据库是主键需要加上IsPrimaryKey
        public long id { get; set; }

        /// <summary>
        /// 酒店名称
        /// </summary>
        public string name { get; set; }

        /// <summary>
        /// 酒店地址
        /// </summary>
        public string address { get; set; }

        /// <summary>
        /// 酒店价格
        /// </summary>
        public int price { get; set; }

        /// <summary>
        /// 酒店评分
        /// </summary>
        public int score { get; set; }

        /// <summary>
        /// 酒店品牌
        /// </summary>
        public string brand { get; set; }

        /// <summary>
        /// 所在城市
        /// </summary>
        public string city { get; set; }

        /// <summary>
        /// 酒店星级
        /// </summary>
        [SugarColumn(ColumnName = "star_name")] //指定数据库表字段 
        public string starName { get; set; }

        /// <summary>
        /// 商圈
        /// </summary>
        public string business { get; set; }

        /// <summary>
        /// 纬度
        /// </summary>
        public string latitude { get; set; }

        /// <summary>
        /// 经度
        /// </summary>
        public string longitude { get; set; }

        /// <summary>
        /// 酒店图片
        /// </summary>
        public string pic { get; set; }
    }
}
public class Hotel

HotelDoc类(酒店数据对应的ES文档):

using System;

namespace Demo.Domain.Docs
{
    /// <summary>
    /// 酒店数据对应的ES文档
    /// </summary>
    public class HotelDoc
    {
        /// <summary>
        /// 酒店id
        /// </summary>
        public long id { get; set; }

        /// <summary>
        /// 酒店名称
        /// </summary>
        public string name { get; set; }

        /// <summary>
        /// 酒店地址
        /// </summary>
        public string address { get; set; }

        /// <summary>
        /// 酒店价格
        /// </summary>
        public int price { get; set; }

        /// <summary>
        /// 酒店评分
        /// </summary>
        public int score { get; set; }

        /// <summary>
        /// 酒店品牌
        /// </summary>
        public string brand { get; set; }

        /// <summary>
        /// 所在城市
        /// </summary>
        public string city { get; set; }

        /// <summary>
        /// 酒店星级
        /// </summary>
        public string starName { get; set; }

        /// <summary>
        /// 商圈
        /// </summary>
        public string business { get; set; }

        /// <summary>
        /// 纬度
        /// </summary>
        //public string latitude { get; set; }

        /// <summary>
        /// 经度
        /// </summary>
        //public string longitude { get; set; }

        /// <summary>
        /// 地理坐标字段(将经度和纬度字段合并成一个地理坐标字段)
        /// 将经度和纬度的字段值用英文逗号拼在一起,例如:"40.048969, 116.619566"
        /// </summary>
        public string location { get; set; }

        /// <summary>
        /// 酒店图片
        /// </summary>
        public string pic { get; set; }

        /// <summary>
        /// 自动补全搜索字段
        /// </summary>
        public List<string> suggestion { get; set; }
    }
}
public class HotelDoc

Hotel类 和 HotelDoc类 二者的映射关系:

using AutoMapper;
using Demo.Domain.Docs;
using Demo.Domain.Entities;

namespace Demo.Domain.AutoMapperConfigs
{
    public class MyProfile : Profile
    {
        public MyProfile()
        {
            // 配置 mapping 规则
            CreateMap<Hotel, HotelDoc>()
                .AfterMap((tbl, doc) =>
                {
                    #region 地理坐标字段处理

                    if (!string.IsNullOrEmpty(tbl.latitude) && !string.IsNullOrEmpty(tbl.longitude))
                    {
                        //将经度和纬度的字段值用英文逗号拼在一起,例如:"40.048969, 116.619566"
                        doc.location = string.Format(@"{0}, {1}", tbl.latitude, tbl.longitude);
                    }

                    #endregion

                    #region 自动补全搜索字段处理

                    var suggestionList = new List<string>();
                    if (!string.IsNullOrEmpty(tbl.brand))
                    {
                        //品牌
                        suggestionList.Add(tbl.brand);
                    }

                    if (!string.IsNullOrEmpty(tbl.business))
                    {
                        //商圈
                        if (tbl.business.Contains("/"))
                        {
                            suggestionList.AddRange(tbl.business.Split('/'));
                        }
                        else
                        {
                            suggestionList.Add(tbl.business);
                        }
                    }

                    doc.suggestion = suggestionList;

                    #endregion
                });
        }
    }
}
public class MyProfile

实体类型创建好后,接下来我们就可以去操作ES的文档了。

1、新增文档或全量修改文档

/// <summary>
/// 新增文档或全量修改文档
/// </summary>
/// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
/// <param name="document">文档</param>
/// <returns>返回true表示操作成功</returns>
public async Task<bool> IndexDocumentAsync<TDocument>(TDocument document, CancellationToken ct = default(CancellationToken))
    where TDocument : class
{
    IndexResponse indexResponse = await _elasticClientProvider.ElasticLinqClient.IndexDocumentAsync(document, ct);
    return indexResponse != null && indexResponse.IsValid;
}

/// <summary>
/// 新增文档或全量修改文档(需要设置额外的参数时使用该方法,例如:需要指定索引库)
/// </summary>
/// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
/// <param name="document">文档</param>
/// <returns>返回true表示操作成功</returns>
public async Task<bool> IndexAsync<TDocument>(TDocument document, Func<IndexDescriptor<TDocument>, IIndexRequest<TDocument>> selector,
    CancellationToken ct = default(CancellationToken)) where TDocument : class
{
    IndexResponse indexResponse = await _elasticClientProvider.ElasticLinqClient.IndexAsync(document, selector, ct);
    return indexResponse != null && indexResponse.IsValid;
}

示例:

/// <summary>
/// 新增文档或全量修改文档
/// </summary>
/// <returns></returns>
public async Task IndexAsync()
{
    var hotelDoc = new HotelDoc
    {
        id = 1,
        name = "7天连锁酒店",
        address = "永泰县",
        price = 100,
        score = 68,
        brand = "7天酒店",
        city = "上海",
        starName = "二钻",
        business = "龙岗中心区/大运新城",
        location = "40.048969, 116.619566",
        pic = "TF3PFkiIb27dAAEqdDcKl3YAAEViQGVWY0AASqM960_w200_h200_c1_t0.jpg",
        suggestion = new List<string> { "7天酒店", "龙岗中心区", "大运新城" }
    };

    // 新增文档或全量修改文档
    bool flag = await _elasticsearchHelper.IndexAsync(hotelDoc, i => i.Index("hotel"));
}

2、局部修改(增量修改)文档

/// <summary>
/// 局部修改(增量修改)文档字段,修改指定字段值
/// </summary>
/// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
/// <param name="indexName">索引库名称</param>
/// <param name="documentId">文档Id</param>
/// <param name="dsl">修改指定字段值的DSL语句</param>
/// <returns>返回true表示修改成功</returns>
public async Task<bool> UpdateAsync<TDocument>(string indexName, string documentId, string dsl)
    where TDocument : class
{
    UpdateResponse<TDocument> updateResponse = await _elasticClientProvider.ElasticJsonClient
        .UpdateAsync<UpdateResponse<TDocument>>(indexName, documentId, PostData.String(dsl));
    return updateResponse != null && updateResponse.IsValid;
}

/// <summary>
/// 局部修改(增量修改)文档字段,修改指定字段值
/// </summary>
/// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
/// <param name="indexName">索引库名称</param>
/// <param name="documentId">文档Id</param>
/// <param name="body">请求数据</param>
/// <returns>返回true表示修改成功</returns>
public async Task<bool> UpdateAsync<TDocument>(string indexName, string documentId, PostData body,
    UpdateRequestParameters requestParameters = null, CancellationToken ctx = default(CancellationToken))
    where TDocument : class
{
    UpdateResponse<TDocument> updateResponse = await _elasticClientProvider.ElasticJsonClient
        .UpdateAsync<UpdateResponse<TDocument>>(indexName, documentId, body, requestParameters, ctx);
    return updateResponse != null && updateResponse.IsValid;
}

示例:

/// <summary>
/// 局部修改(增量修改)文档字段
/// </summary>
/// <returns></returns>
public async Task UpdateAsync()
{
    // 修改指定字段值
    var updateFields = new
    {
        doc = new
        {
            price = 360,
            score = 59
        }
    };

    bool flag = await _elasticsearchHelper.UpdateAsync<HotelDoc>(indexName: "hotel",
        documentId: "1", body: PostData.Serializable(updateFields));
}

3、获取文档

/// <summary>
/// 获取文档
/// </summary>
/// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
/// <param name="documentId">文档Id</param>
/// <returns></returns>
public async Task<GetResponse<TDocument>> GetAsync<TDocument>(DocumentPath<TDocument> documentId,
    Func<GetDescriptor<TDocument>, IGetRequest> selector = null, CancellationToken ct = default(CancellationToken))
    where TDocument : class
{
    return await _elasticClientProvider.ElasticLinqClient.GetAsync(documentId, selector, ct);
}

示例:

/// <summary>
/// 获取文档
/// </summary>
/// <returns></returns>
public async Task GetAsync()
{
    GetResponse<HotelDoc> getResponse = await _elasticsearchHelper.GetAsync<HotelDoc>(documentId: 1, g => g.Index("hotel"));
    if (getResponse != null && getResponse.Found)
    {
        // 获取文档成功
        var hotelDoc = getResponse.Source;
    }
    else
    {
        // 未获取到文档
    }
}

4、删除文档

/// <summary>
/// 删除文档
/// </summary>
/// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
/// <param name="documentId">文档Id</param>
/// <returns>返回true表示删除文档成功</returns>
public async Task<bool> DeleteAsync<TDocument>(DocumentPath<TDocument> documentId, Func<DeleteDescriptor<TDocument>, IDeleteRequest> selector = null,
    CancellationToken ct = default(CancellationToken)) where TDocument : class
{
    DeleteResponse deleteResponse = await _elasticClientProvider.ElasticLinqClient.DeleteAsync(documentId, selector, ct);
    return deleteResponse != null && deleteResponse.IsValid;
}

示例:

/// <summary>
/// 删除文档
/// </summary>
/// <returns></returns>
public async Task DeleteAsync()
{
    bool flag = await _elasticsearchHelper.DeleteAsync<HotelDoc>(documentId: "1", d => d.Index("hotel"));
}

5、批量新增文档 

/// <summary>
/// 批量新增文档
/// </summary>
/// <typeparam name="TDocument">索引库对应的文档类型</typeparam>
/// <param name="objects">文档集合</param>
/// <returns>返回true表示操作成功</returns>
public async Task<bool> IndexManyAsync<TDocument>(IEnumerable<TDocument> objects, IndexName index = null,
    CancellationToken cancellationToken = default(CancellationToken)) where TDocument : class
{
    BulkResponse bulkResponse = await _elasticClientProvider.ElasticLinqClient.IndexManyAsync(objects, index, cancellationToken);
    return bulkResponse != null && bulkResponse.IsValid;
}

6、执行批量数据操作

/// <summary>
/// 执行批量数据操作,包括新增、更新或删除多个文档,而只需发起一次HTTP请求
/// </summary>
/// <returns>返回true表示操作成功</returns>
public async Task<bool> BulkAsync(Func<BulkDescriptor, IBulkRequest> selector, CancellationToken ct = default(CancellationToken))
{
    BulkResponse bulkResponse = await _elasticClientProvider.ElasticLinqClient.BulkAsync(selector, ct);
    return bulkResponse != null && bulkResponse.IsValid;
}

示例:

/// <summary>
/// 执行批量数据操作,包括新增、更新或删除多个文档,而只需发起一次HTTP请求
/// </summary>
/// <returns></returns>
public async Task BulkAsync()
{
    var hotelDocList = new List<HotelDoc> {
        new HotelDoc
        {
            id = 1,
            name = "7天连锁酒店(上海宝山路地铁站店)",
            address = "静安交通路40号",
            price = 100,
            score = 38,
            brand = "7天酒店",
            city = "上海",
            starName = "二钻",
            business = "龙岗中心区/大运新城",
            location = "40.048969, 116.619566",
            pic = "TF3PFkiIb27dAAEqdDcKl3YAAEViQGVWY0AASqM960_w200_h200_c1_t0.jpg",
            suggestion = new List<string> { "7天酒店", "龙岗中心区", "大运新城" }
        },
        new HotelDoc
        {
            id = 2,
            name = "维也纳酒店(北京花园路店)",
            address = "海淀北太平庄花园路甲17号",
            price = 381,
            score = 85,
            brand = "维也纳",
            city = "北京",
            starName = "三钻",
            business = "马甸、安贞地区",
            location = "39.970837, 116.365244",
            pic = "https://m.tuniucdn.com/filebroker/cdn/res/17/00/1700926908bae6ba3e5ef96de7b7d4cc_w200_h200_c1_t0.jpg",
            suggestion = new List<string> { "维也纳", "马甸、安贞地区" }
        }
    };

    bool flag = await _elasticsearchHelper.BulkAsync(bulk => bulk
        .Index("hotel") // 指定索引库
        .IndexMany(hotelDocList) // 批量新增文档
    );
}

五、文档查询

官方文档:https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/writing-queries.html

示例1:

/// <summary>
/// 文档查询
/// </summary>
/// <returns></returns>
public async Task SearchAsync()
{
    var searchResponse = await _elasticClientProvider.ElasticLinqClient
        .SearchAsync<HotelDoc>(s => s
        .Source(sf => sf
            //指定返回的字段
            .Includes(i => i
                .Fields(
                    f => f.id,
                    f => f.name,
                    f => f.score
                )
            )
            //指定不返回的字段
            .Excludes(e => e
                .Fields("pic*") //可以通过通配符模式来包含或排除字段
            )
        )
        .From(0)
        .Size(10)
        .Query(q => q
            .Match(m => m
                .Field(f => f.name)
                .Query("如家酒店")
            )
        )
    );

    if (searchResponse != null && searchResponse.IsValid)
    {
        var hotelDocs = searchResponse.Documents;
    }
}

其生成的DSL语句如下所示:

GET /hotel/_search
{
  "from": 0,
  "size": 10,
  "query": {
    "match": {
      "name": {
        "query": "如家酒店"
      }
    }
  },
  "_source": {
    "excludes": [
      "pic*"
    ],
    "includes": [
      "id",
      "name",
      "score"
    ]
  }
}

示例2:

/// <summary>
/// 布尔查询
/// </summary>
/// <returns></returns>
public async Task BooleanQueryAsync()
{
    //布尔查询
    var searchResponse = await _elasticClientProvider.ElasticLinqClient
        .SearchAsync<HotelDoc>(s => s
        .From(0)
        .Size(10)
        .Query(q => q
            .Bool(b => b
                .Must(
                    mu => mu.Term(h => h.city, "上海"),
                    mu => mu.Bool(bl => bl
                        .Should(
                            bs => bs.Term(h => h.brand, "皇冠假日"),
                            bs => bs.Term(h => h.brand, "华美达")
                        )
                    )
                )
            )
        )
    );

    //布尔查询简写
    searchResponse = await _elasticClientProvider.ElasticLinqClient.SearchAsync<HotelDoc>(s => s
        .From(0)
        .Size(10)
        .Query(q =>
            q.Term(h => h.city, "上海") && (q.Term(h => h.brand, "皇冠假日") || q.Term(h => h.brand, "华美达"))
        )
    );

    if (searchResponse != null && searchResponse.IsValid)
    {
        var hotelDocs = searchResponse.Documents;
    }
}

其生成的DSL语句如下所示:

GET /hotel/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "city": {
              "value": "上海"
            }
          }
        },
        {
          "bool": {
            "should": [
              {
                "term": {
                  "brand": {
                    "value": "皇冠假日"
                  }
                }
              },
              {
                "term": {
                  "brand": {
                    "value": "华美达"
                  }
                }
              }
            ]
          }
        }
      ]
    }
  },
  "from": 0,
  "size": 10
}

更多查询写法可直接参考官网文档,此处就不做过多的介绍了。 

至此本文就全部介绍完了,如果觉得对您有所启发请记得点个赞哦!!!

 

Demo源码:

链接:https://pan.baidu.com/s/1tv6VQ7nxqv-f7sEGUgYTPQ 
提取码:l30a

此文由博主精心撰写转载请保留此原文链接:https://www.cnblogs.com/xyh9039/p/18200453

版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!

posted @ 2024-06-16 23:12  谢友海  阅读(147)  评论(0编辑  收藏  举报