.NET Core接入ElasticSearch 7.5
寫在前面
最近一段時間,團隊在升級ElasticSearch(以下簡稱ES),從ES 2.2升級到ES 7.5。也是這段時間,我從零開始,逐步的了解了ES,中間也踩了不少坑,所以特地梳理和總結一下相關的技術點。
?ES小趣聞:
多年前,一個叫做Shay Banon的剛結婚不久的開發者,由于妻子要去倫敦學習廚師,他便跟著也去了。在他找工作的過程中,為了給妻子構建一個食譜的搜索引擎,他開始使用Lucene進行嘗試。直接基于Lucene工作會比較困難,所以Shay開始抽象Lucene代碼以便可以在應用中添加搜索功能。他發布了他的第一個開源項目,叫做“Compass”。后來Shay找到一份工作,這份工作處在高性能和內存數據網格的分布式環境中,因此高性能的、實時的、分布式的搜索引擎也是理所當然需要的。然后他決定重寫Compass庫使其成為一個獨立的服務叫做Elasticsearch。Shay的妻子依舊等待著她的食譜搜索……
?由此看見,一個成功的男人背后總是站著一個女人,所以程序員們要早點找到對象,可程序員找到女朋友又談何容易,程序猿注定悲傷-_-||。
ElasticSearch前期準備
EElasticsearch是一個開源的分布式、RESTful 風格的搜索和數據分析引擎,ES底層基于開源庫Apache Lucene,不過Lucene使用門檻太高,ES隱藏了Lucene使用時的復雜性,使得分布式實時文檔搜索、實時分析引擎、高擴展性變得更加容易。
安裝
安裝ES,首先要配置Java SDK,然后配置一下環境變量即可。然后再從官網下載ES安裝包,可以選用默認配置,點擊下一步—>安裝。
在瀏覽器上輸入http://localhost:9200/,顯示如下文本,就意味著安裝成功了。
?{
?"name" : "XXXXXXXXXX",
?"cluster_name" : "elasticsearch",
?"cluster_uuid" : "mB04ov3OTvSz7OSe0GtZ_A",
?"version" : {
??"number" : "7.5.2",
??"build_flavor" : "unknown",
??"build_type" : "unknown",
??"build_hash" : "8bec50e1e0ad29dad5653712cf3bb580cd1afcdf",
??"build_date" : "2020-01-15T12:11:52.313576Z",
??"build_snapshot" : false,
??"lucene_version" : "8.3.0",
??"minimum_wire_compatibility_version" : "6.8.0",
??"minimum_index_compatibility_version" : "6.0.0-beta1"
?},
? "tagline" : "You Know, for Search"
}
?部分基本概念
節點 & 集群
集群由多個節點組成,其中一個節點為主節點,主節點由內部選舉算法選舉產生。當然主節點是相對的,是相對于內部而言的。ES去中心化,這是相對于外部而言的,從邏輯上說,與任何一個節點的的通信和與集群通信是沒有區別的。如下圖所示。
索引
索引保存相關數據的地方,是指向一個或者多個物理分片的邏輯命名空間 。另外,每個Index的名字必須是小寫。
文檔
Document的核心元數據有三個:_index、_type(7.X已經弱化了,8.0開始就會移除)、_id。Document 使用 JSON 格式表示。
分片
一個分片是一個底層的工作單元,它僅保存了全部數據中的一部分。我們的文檔被存儲和索引到分片內,但是應用程序是直接與索引而不是與分片進行交互。
Elasticsearch 是利用分片將數據分發到集群內各處的。分片是數據的容器,文檔保存在分片內,分片又被分配到集群內的各個節點里。當你的集群規模擴大或者縮小時, Elasticsearch 會自動的在各節點中遷移分片,使得數據仍然均勻分布在集群里。
一個分片可以是主分片或者副本分片。索引內任意一個文檔都歸屬于一個主分片,所以主分片的數目決定著索引能夠保存的最大數據量。
一個副本分片只是一個主分片的拷貝。副本分片作為硬件故障時保護數據不丟失的冗余備份,并為搜索和返回文檔等讀操作提供服務。
在索引建立的時候就已經確定了主分片數,但是副本分片數可以隨時修改。
理論上一個主分片最大能夠存儲Integer.MAX_VALUE^128 個文檔。
寫操作探討
文檔會被保存到主分片,那么在多個分片的情況下是如何寫入和精確搜索的。實際上這是通過以下公式確定的:
shard = hash(routing) % number_of_primary_shards
以上的routing的值是一個任意的字符串,它默認被設置成文檔的_id字段,但是也可以被設置成其他指定的值。這個routing字符串會被傳入到一個哈希函數(Hash Function)來得到一個數字,然后該數字會和索引中的主要分片數進行模運算來得到余數。這個余數的范圍應該總是在0和number_of_primary_shards - 1之間,它就是一份文檔被存儲到的分片的號碼。
這就解釋了為什么索引中的主要分片數量只能在索引創建時被指定,并且將來都不能在被更改:如果主要分片數量在索引創建后改變了,那么之前的所有路由結果都會變地不正確,從而導致文檔不能被正確地獲取。那么如何水平擴展呢,可以移步Designing for scale。
所有的文檔API(get, index, delete, bulk, update)都接受一個routing參數,它用來定制從文檔到分片的映射。一個特定的routing值能夠確保所有相關文檔 - 比如屬于相同用戶的所有文檔 - 都會被存儲在相同的分片上。
寫操作原理圖:寫入的請求流程如圖所示(此圖源自《Elasticsearch權威指南》):寫入到磁盤流程如下圖所示:由此可見ES的實時并非是完全的實時,而是一種準實時(Near-Real-Time)。
讀操作探討
讀操作分為兩個階段,查詢階段(Query Phrase)以及聚合提取階段(Fetch Phrase)。
查詢階段
協調節點接受到讀請求,并將請求分配到相應的分片上(有可能是主分片或是副本分片,這個機制后續會提及),默認情況下,每個分片創建10個結果(僅包含 document_id 和 Scores)的優先級隊列,并以相關性排序,返回給協調節點。
?查詢階段如果不特殊指定,落入的分片有可能是 primary 也有可能是 replicas,這個根據協調節點的負載均衡算法來確定。
?聚合提取階段
假設查詢落入的分片數為 N,那么聚合階段就是對 N*10 個結果集進行排序,然后再通過已經拿到的 document_id 查到對應的 document 并組裝到隊列里,組裝完畢后將有序的數據返回給客戶端。
客戶端發送請求到任意一個Node,成為Coordinating node
Coordinating node對Document進行路由,將請求轉發到對應的Node上,此時會使用Round-Robin隨機輪詢算法,在Primary Shard以及其所有Replica中隨機選擇一個,讓讀請求負載均衡
接收請求的node返回Document給Coordinating node
Coordinating node返回Document給客戶端
ElasticSearch實戰
ES在.NET平臺上的官方客戶端是NEST,以下操作都是基于該package的。
常用操作
以下操作均基于ES-Head,該工具是一個Chrome插件,非常簡單實用,而且可以在GitHub上搜到源碼,方便個性化開發。
寫入數據
返回的數據中,可以看到Id是一段字符串,這是因為在寫入的過程中并沒有指定,所以會由ES默認生成。當然可以指定:
更新數據
_version值會隨著操作次數,逐漸迭代。
刪除數據
cluster查詢操作:
cluster項目升級過程中遇到的問題
分頁查詢過慢
初次的查詢使用了深度分頁(from-size)查詢,當數據達到百萬千萬級別時,已經慢的讓人忍無可忍。所謂深度查詢就是涉及到大量 shard 的查詢時,直接跳頁到幾千甚至上萬頁的數據,協調節點就有宕機的風險,畢竟協調節點需要將大量數據匯總起來進行排序,耗費大量的內存和 CPU 資源。所以慎用!盡可能用 Scroll API ,即只允許拿到下一頁的信息,不允許跳頁的情況出現,會避免這種情況的發生。
后來改用了快照分頁(scroll),整個查詢過程非常穩定,方差幾乎可以忽略。該查詢會自動返回一個_scroll_id,通過這個id(經過base64編碼)可以繼續查詢。查詢語句如下:http://localhost:9200/_search/scroll?scroll=1m&scroll_id=c2MkjsjskMkkssllasKKKOzM0NDg1ODpksksks5566HHsaskLLLqi692215。這個語句雖然很快,但是無法做到跳頁查詢,只能一頁一頁的查詢。
快照分頁參考代碼如下:
?var searchResponse = client.Search(p =>
p.Query(t =>t.Bool(l => l.Filter(f => f.DateRange(m => m.GreaterThanOrEquals(startTime).Field(d => d.PostDate))))).From(0).Size(Configurations.SyncSize).Index("archive").Sort(s => s.Ascending(a => a.PostDate)).Scroll("60s"));while(某條件)
{
searchResponse = client.Scroll<ElasticsearchTransaction>("60s", searchResponse.ScrollId);//跳出循環的條件}
?模糊查詢
該場景涉及到多個字段的模糊查詢,當然,這種查詢是十分消耗效率的,使用的時候要慎重,同時還要控制模糊關鍵字的數量,以盡可能在滿足業務的情況下,提升查詢效率,參考代碼如下:
?public static List<IHit> GetDataByFuzzy(ElasticClient client920>0) {
string[] fieldList = {"filed1","filed2","filed3","filed4","filed5","filed6","filed7","filed8","filed9" };string term = string.Concat("*", string.Join("* *", "i u a n".Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)), "*"); var result = client9200.Search<TModel>(p => p.Query(q => q.Bool(b=>b.Must(t=>t.QueryString(c => c.Fields(fieldList).Query(term).Boost(1.1).Fuzziness(Fuzziness.Auto).MinimumShouldMatch(2).FuzzyRewrite(MultiTermQueryRewrite.ConstantScoreBoolean).TieBreaker(1).Lenient())).Filter(f=>f.Term(t=>t.Field(d=>d.AccountKey).Value("123456789"))))).ScriptFields(sf => sf.ScriptField("datetime1",sc => sc.Source("doc['datetime1'].value == null?doc['datetime2'].value: doc['datetime1'].value"))).Source(true).Index("archive").From(0).Size(10000).Sort(s => s.Descending(a => a.CreateDate)));return result.Hits.Select(p=>p.Source).ToList();}
?關于排序
在本次的ES優化升級過程中,關于排序的操作可以說是很糾結的。按照業務要求,要根據兩個時間類型的字段進行排序,如果某個為空,就按照不為空的排序,使得其排序結果達到穿插的效果,而不是像SQL語句那樣order field1, field2的排序結果那樣。
找出解決方案的過程很痛苦,因為官方的demo無法運行,經過多方嘗試,終于在查看ElasticSearch源代碼的情況下,找到了解決方案。
Github地址:https://github.com/elastic/elasticsearch/blob/master/server/src/main/java/org/elasticsearch/script/JodaCompatibleZonedDateTime.java,第411行
查詢語句如下:
?{
"from": 0, "query": {"bool": {"filter": [{"term": {"UserId": {"value": "123456789"}}}]} }, "size": 10, "sort": [{"_script": {"script": {"source": "doc.DateTime1.empty?doc.DateTime2.value.toInstant().toEpochMilli():doc.DateTime1.value.toInstant().toEpochMilli()"},"type": "number","order": "desc"}} ]}
?C#參考代碼如下:
?var searchResponse = _elasticsearchClient.Search(s => s
.Query(q => q.Bool(b => b.Filter(m => m.Term(t => t.Field(f => f.UserId).Value(userId)),m => m.QueryString(qs => qs.Fields(fieldList).Query(term.PreProcessQueryString()))))).Index(indexName).ScriptFields(sf => sf.Source(true).Sort(s=>s.Script(sr=>sr.Script(doc => doc.Source("doc.DateTime1.empty ? doc.DateTime2.value.toInstant().toEpochMilli() : doc.DateTime1.value.toInstant().toEpochMilli()")))).From(startIndex).Size(pageSize)); ?參考鏈接:
https://www.dazhuanlan.com/2020/02/13/5e44f118b75cb/ https://www.toutiao.com/i6824365055832752653
總結
以上是生活随笔為你收集整理的.NET Core接入ElasticSearch 7.5的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 干货分享:如何使用Kubernetes的
- 下一篇: ASP.NET Core on K8s学