本章將和大家分享在ASP.NET Core中如何使用高級客戶端NEST來操作我們的Elasticsearch。 NEST是一個高級別的Elasticsearch .NET客戶端,它仍然非常接近原始Elasticsearch API的映射。所有的請求和響應都是通過類型來暴露的,這使得它非常適合快速上手 ...
本章將和大家分享在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 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 = de