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

news/2024/10/3 19:16:10

本章将和大家分享在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框架用的是SqlSugarScope,实体映射用的是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 requestif (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 responseif (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: 162TimeWait: 14SynSent: 4CloseWait: 13LastAck: 1FinWait1: 1# ThreadPool statistics:Worker: Busy: 1Free: 32766Min: 12Max: 32767IOCP: Busy: 0Free: 1000Min: 1Max: 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)] //数据库是主键需要加上IsPrimaryKeypublic 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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/44515.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

Perfetto分析进阶

一、Perfetto介绍 Perfetto是Android Q中引入的全新下一代平台级跟踪工具,为Android、Linux和Chrome平台提供了一种通用的性能检测和跟踪分析工具集。其核心是引入了一种全新的用户空间到用户空间的跟踪协议,该协议基于protobuf序列化机制将抓取的数据填充到共享内存缓冲区,…

Kali中MSF利用永恒之蓝(复现)

Kali中MSF利用永恒之蓝(复现) 1、进入MSF框架:2、搜索MS17_010漏洞:这里找到了四个模块:0、1、4是漏洞利用模块;2、3是辅助模块,主要探测主机是否存在MS17_010漏洞。 3、利用Auxiliary辅助探测模块对漏洞进行探测(查看下所需要的参数):4、设置要探测的远程目标(两种…

B - Ticket Counter

B - Ticket Counter https://atcoder.jp/contests/abc358/tasks/abc358_b思路 第i个完成的时刻,done[i] 跟第i-1完成时间done[i-1]有关系, 第i个的开始时刻t[i] 大于 done[i-1], done[i] = t[i]+a第i个的开始时刻t[i] 不大于 done[i-1], done[i] = done[i-1]+aCode https…

使用Kimi+Markmap总结文件内容生成思维导图原创

一份文件内容太长,完整阅读下来太费时间,但如果使用AI进行内容提炼,再总结成思维导图,方便快速看到这份文件的核心内容和主题结构,就会极大地节约时间,目前就可以使用Kimi+Markmap这两个工具,帮我们把ppt、word、pdf等文件内容快速总结成思维导图。 一、工具准备 Kimi,…

vue状态共享--VUEX

一,VUEX是什么 Vue中跨组件状态管理模式+库 包含以下几个部分: 状态:State,驱动应用的数据源; 视图:Vue组件,以声明方式将状态映射到视图; 操作:Action,响应在视图上的用户输入导致的状态变化。

2024/4/1

今天完成了结对作业,完成了web端和手机端的主要功能, 其中数据库分为两个表,第一个表简单的记录地铁每条线的id以及地铁线的名字,第二个表是主用表,同时存储许多数据,存储线路上节点的id 上一站点的id以及下一站点的id 还有本站点的名字,以及本站点在本线路的顺序,是否…

Codeforces Round 953 Div.2 F 题解

经典蹭热度连通块计数的一种常见思路是钦定代表元,但发现这题的连边方式并不好指定一个代表元,那么只能尝试优化建图。 我们尝试观察一下连边的情况,通过手玩样例获得一些几何直观的感受: 3 4 5 5 3 4 4 5 3这个样例也许比较小,不过你真的把边画出来就会发现:连边形如 \(…