.NET Core接入ElasticSearch 7.5
寫在前面
最近一段時(shí)間,團(tuán)隊(duì)在升級(jí)ElasticSearch(以下簡稱ES),從ES 2.2升級(jí)到ES 7.5。也是這段時(shí)間,我從零開始,逐步的了解了ES,中間也踩了不少坑,所以特地梳理和總結(jié)一下相關(guān)的技術(shù)點(diǎn)。
?ES小趣聞:
多年前,一個(gè)叫做Shay Banon的剛結(jié)婚不久的開發(fā)者,由于妻子要去倫敦學(xué)習(xí)廚師,他便跟著也去了。在他找工作的過程中,為了給妻子構(gòu)建一個(gè)食譜的搜索引擎,他開始使用Lucene進(jìn)行嘗試。直接基于Lucene工作會(huì)比較困難,所以Shay開始抽象Lucene代碼以便可以在應(yīng)用中添加搜索功能。他發(fā)布了他的第一個(gè)開源項(xiàng)目,叫做“Compass”。后來Shay找到一份工作,這份工作處在高性能和內(nèi)存數(shù)據(jù)網(wǎng)格的分布式環(huán)境中,因此高性能的、實(shí)時(shí)的、分布式的搜索引擎也是理所當(dāng)然需要的。然后他決定重寫Compass庫使其成為一個(gè)獨(dú)立的服務(wù)叫做Elasticsearch。Shay的妻子依舊等待著她的食譜搜索……
?由此看見,一個(gè)成功的男人背后總是站著一個(gè)女人,所以程序員們要早點(diǎn)找到對(duì)象,可程序員找到女朋友又談何容易,程序猿注定悲傷-_-||。
ElasticSearch前期準(zhǔn)備
EElasticsearch是一個(gè)開源的分布式、RESTful 風(fēng)格的搜索和數(shù)據(jù)分析引擎,ES底層基于開源庫Apache Lucene,不過Lucene使用門檻太高,ES隱藏了Lucene使用時(shí)的復(fù)雜性,使得分布式實(shí)時(shí)文檔搜索、實(shí)時(shí)分析引擎、高擴(kuò)展性變得更加容易。
安裝
安裝ES,首先要配置Java SDK,然后配置一下環(huán)境變量即可。然后再從官網(wǎng)下載ES安裝包,可以選用默認(rèn)配置,點(diǎn)擊下一步—>安裝。
在瀏覽器上輸入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"
}
?部分基本概念
節(jié)點(diǎn) & 集群
集群由多個(gè)節(jié)點(diǎn)組成,其中一個(gè)節(jié)點(diǎn)為主節(jié)點(diǎn),主節(jié)點(diǎn)由內(nèi)部選舉算法選舉產(chǎn)生。當(dāng)然主節(jié)點(diǎn)是相對(duì)的,是相對(duì)于內(nèi)部而言的。ES去中心化,這是相對(duì)于外部而言的,從邏輯上說,與任何一個(gè)節(jié)點(diǎn)的的通信和與集群通信是沒有區(qū)別的。如下圖所示。
索引
索引保存相關(guān)數(shù)據(jù)的地方,是指向一個(gè)或者多個(gè)物理分片的邏輯命名空間 。另外,每個(gè)Index的名字必須是小寫。
文檔
Document的核心元數(shù)據(jù)有三個(gè):_index、_type(7.X已經(jīng)弱化了,8.0開始就會(huì)移除)、_id。Document 使用 JSON 格式表示。
分片
一個(gè)分片是一個(gè)底層的工作單元,它僅保存了全部數(shù)據(jù)中的一部分。我們的文檔被存儲(chǔ)和索引到分片內(nèi),但是應(yīng)用程序是直接與索引而不是與分片進(jìn)行交互。
Elasticsearch 是利用分片將數(shù)據(jù)分發(fā)到集群內(nèi)各處的。分片是數(shù)據(jù)的容器,文檔保存在分片內(nèi),分片又被分配到集群內(nèi)的各個(gè)節(jié)點(diǎn)里。當(dāng)你的集群規(guī)模擴(kuò)大或者縮小時(shí), Elasticsearch 會(huì)自動(dòng)的在各節(jié)點(diǎn)中遷移分片,使得數(shù)據(jù)仍然均勻分布在集群里。
一個(gè)分片可以是主分片或者副本分片。索引內(nèi)任意一個(gè)文檔都?xì)w屬于一個(gè)主分片,所以主分片的數(shù)目決定著索引能夠保存的最大數(shù)據(jù)量。
一個(gè)副本分片只是一個(gè)主分片的拷貝。副本分片作為硬件故障時(shí)保護(hù)數(shù)據(jù)不丟失的冗余備份,并為搜索和返回文檔等讀操作提供服務(wù)。
在索引建立的時(shí)候就已經(jīng)確定了主分片數(shù),但是副本分片數(shù)可以隨時(shí)修改。
理論上一個(gè)主分片最大能夠存儲(chǔ)Integer.MAX_VALUE^128 個(gè)文檔。
寫操作探討
文檔會(huì)被保存到主分片,那么在多個(gè)分片的情況下是如何寫入和精確搜索的。實(shí)際上這是通過以下公式確定的:
shard = hash(routing) % number_of_primary_shards
以上的routing的值是一個(gè)任意的字符串,它默認(rèn)被設(shè)置成文檔的_id字段,但是也可以被設(shè)置成其他指定的值。這個(gè)routing字符串會(huì)被傳入到一個(gè)哈希函數(shù)(Hash Function)來得到一個(gè)數(shù)字,然后該數(shù)字會(huì)和索引中的主要分片數(shù)進(jìn)行模運(yùn)算來得到余數(shù)。這個(gè)余數(shù)的范圍應(yīng)該總是在0和number_of_primary_shards - 1之間,它就是一份文檔被存儲(chǔ)到的分片的號(hào)碼。
這就解釋了為什么索引中的主要分片數(shù)量只能在索引創(chuàng)建時(shí)被指定,并且將來都不能在被更改:如果主要分片數(shù)量在索引創(chuàng)建后改變了,那么之前的所有路由結(jié)果都會(huì)變地不正確,從而導(dǎo)致文檔不能被正確地獲取。那么如何水平擴(kuò)展呢,可以移步Designing for scale。
所有的文檔API(get, index, delete, bulk, update)都接受一個(gè)routing參數(shù),它用來定制從文檔到分片的映射。一個(gè)特定的routing值能夠確保所有相關(guān)文檔 - 比如屬于相同用戶的所有文檔 - 都會(huì)被存儲(chǔ)在相同的分片上。
寫操作原理圖:寫入的請(qǐng)求流程如圖所示(此圖源自《Elasticsearch權(quán)威指南》):寫入到磁盤流程如下圖所示:由此可見ES的實(shí)時(shí)并非是完全的實(shí)時(shí),而是一種準(zhǔn)實(shí)時(shí)(Near-Real-Time)。
讀操作探討
讀操作分為兩個(gè)階段,查詢階段(Query Phrase)以及聚合提取階段(Fetch Phrase)。
查詢階段
協(xié)調(diào)節(jié)點(diǎn)接受到讀請(qǐng)求,并將請(qǐng)求分配到相應(yīng)的分片上(有可能是主分片或是副本分片,這個(gè)機(jī)制后續(xù)會(huì)提及),默認(rèn)情況下,每個(gè)分片創(chuàng)建10個(gè)結(jié)果(僅包含 document_id 和 Scores)的優(yōu)先級(jí)隊(duì)列,并以相關(guān)性排序,返回給協(xié)調(diào)節(jié)點(diǎn)。
?查詢階段如果不特殊指定,落入的分片有可能是 primary 也有可能是 replicas,這個(gè)根據(jù)協(xié)調(diào)節(jié)點(diǎn)的負(fù)載均衡算法來確定。
?聚合提取階段
假設(shè)查詢落入的分片數(shù)為 N,那么聚合階段就是對(duì) N*10 個(gè)結(jié)果集進(jìn)行排序,然后再通過已經(jīng)拿到的 document_id 查到對(duì)應(yīng)的 document 并組裝到隊(duì)列里,組裝完畢后將有序的數(shù)據(jù)返回給客戶端。
客戶端發(fā)送請(qǐng)求到任意一個(gè)Node,成為Coordinating node
Coordinating node對(duì)Document進(jìn)行路由,將請(qǐng)求轉(zhuǎn)發(fā)到對(duì)應(yīng)的Node上,此時(shí)會(huì)使用Round-Robin隨機(jī)輪詢算法,在Primary Shard以及其所有Replica中隨機(jī)選擇一個(gè),讓讀請(qǐng)求負(fù)載均衡
接收請(qǐng)求的node返回Document給Coordinating node
Coordinating node返回Document給客戶端
ElasticSearch實(shí)戰(zhàn)
ES在.NET平臺(tái)上的官方客戶端是NEST,以下操作都是基于該package的。
常用操作
以下操作均基于ES-Head,該工具是一個(gè)Chrome插件,非常簡單實(shí)用,而且可以在GitHub上搜到源碼,方便個(gè)性化開發(fā)。
寫入數(shù)據(jù)
返回的數(shù)據(jù)中,可以看到Id是一段字符串,這是因?yàn)樵趯懭氲倪^程中并沒有指定,所以會(huì)由ES默認(rèn)生成。當(dāng)然可以指定:
更新數(shù)據(jù)
_version值會(huì)隨著操作次數(shù),逐漸迭代。
刪除數(shù)據(jù)
cluster查詢操作:
cluster項(xiàng)目升級(jí)過程中遇到的問題
分頁查詢過慢
初次的查詢使用了深度分頁(from-size)查詢,當(dāng)數(shù)據(jù)達(dá)到百萬千萬級(jí)別時(shí),已經(jīng)慢的讓人忍無可忍。所謂深度查詢就是涉及到大量 shard 的查詢時(shí),直接跳頁到幾千甚至上萬頁的數(shù)據(jù),協(xié)調(diào)節(jié)點(diǎn)就有宕機(jī)的風(fēng)險(xiǎn),畢竟協(xié)調(diào)節(jié)點(diǎn)需要將大量數(shù)據(jù)匯總起來進(jìn)行排序,耗費(fèi)大量的內(nèi)存和 CPU 資源。所以慎用!盡可能用 Scroll API ,即只允許拿到下一頁的信息,不允許跳頁的情況出現(xiàn),會(huì)避免這種情況的發(fā)生。
后來改用了快照分頁(scroll),整個(gè)查詢過程非常穩(wěn)定,方差幾乎可以忽略。該查詢會(huì)自動(dòng)返回一個(gè)_scroll_id,通過這個(gè)id(經(jīng)過base64編碼)可以繼續(xù)查詢。查詢語句如下:http://localhost:9200/_search/scroll?scroll=1m&scroll_id=c2MkjsjskMkkssllasKKKOzM0NDg1ODpksksks5566HHsaskLLLqi692215。這個(gè)語句雖然很快,但是無法做到跳頁查詢,只能一頁一頁的查詢。
快照分頁參考代碼如下:
?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);//跳出循環(huán)的條件}
?模糊查詢
該場景涉及到多個(gè)字段的模糊查詢,當(dāng)然,這種查詢是十分消耗效率的,使用的時(shí)候要慎重,同時(shí)還要控制模糊關(guān)鍵字的數(shù)量,以盡可能在滿足業(yè)務(wù)的情況下,提升查詢效率,參考代碼如下:
?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();}
?關(guān)于排序
在本次的ES優(yōu)化升級(jí)過程中,關(guān)于排序的操作可以說是很糾結(jié)的。按照業(yè)務(wù)要求,要根據(jù)兩個(gè)時(shí)間類型的字段進(jìn)行排序,如果某個(gè)為空,就按照不為空的排序,使得其排序結(jié)果達(dá)到穿插的效果,而不是像SQL語句那樣order field1, field2的排序結(jié)果那樣。
找出解決方案的過程很痛苦,因?yàn)楣俜降膁emo無法運(yùn)行,經(jīng)過多方嘗試,終于在查看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
總結(jié)
以上是生活随笔為你收集整理的.NET Core接入ElasticSearch 7.5的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 干货分享:如何使用Kubernetes的
- 下一篇: ASP.NET Core on K8s学