elasticsearch技術實戰——第一篇(使用篇)
為了提高搜尋命中率和準確率,改善現有羸弱的搜尋功能,公司決定搭建全文搜尋服務。由於之前缺乏全文搜尋使用經驗,經過一番折騰,終於不負期望按期上線。總結了一些使用心得體會,希望對大家有所幫助。計劃分三篇:
- 第一篇(使用篇),主要講解基本概念、分詞、資料同步、搜尋API。
- 第二篇(配置及引數調優篇),主要圍繞JVM引數調優、異常排查、安全性等方面講解。
- 第三篇(倒排索引原理篇),知其然知其所以然。
一、技術選型
說到全文搜尋大家肯定會想到solr和elasticsearch(以下簡稱es),兩者都是基於lucence,到底有什麼區別呢?主要列出四個方面:
對比項 | solr | elasticsearch |
分散式 | 利用zookeeper進行分散式協調 | 自帶分散式協調能力 |
資料格式 | 支援更多的資料格式(XML、JSON、CSV等) | 僅支援JSON |
查詢效能 | 更適合偏傳統的搜尋應用,單純對已有資料進行搜尋效能更高,但實時建立索引時查詢效能較差。 | 在實時搜尋應用中表現更好,資料匯入效能更好 |
資料量對查詢效能影響 | 明顯下降 | 影響不大 |
最終選擇es,主要原因:
- 作為後起之秀,吸收了solr的優秀設計,在實時搜尋上效能更佳,大有超越solr之勢。
- 社群非常活躍,文件齊全,越來越多的應用從solr遷移至es。典型案例較多:GitHub使用es來檢索超過1300億行程式碼、Wikipedia 使用es提供帶有高亮片段的全文搜尋。
二、基本概念
- 叢集(cluster)和節點(node):一個叢集裡包含多個節點,其中一個主節點通過選舉產生,叢集中任一節點的通訊與整個es叢集通訊是等價的。
- 索引(index):es包含一個或多個索引,相當於關係型資料庫(以下簡稱RDS)裡的資料庫,可以向索引裡寫入或讀取資料。
- 型別(type):一個索引包含一個或多個type,相當於RDS裡的表。
- 文件(document):相當於RDS裡的資料行,文件沒有固定的格式(schemaless),與mongodb很類似。
- 分片(shards):可以把一個大索引拆分成多個分片,分佈到不同的節點上,提高檢索效率。分片數在建立索引時確定,無法更改。
- 副本(replicas):副本有兩個作用,一是增加容錯,當某個分片損壞或丟失時可以由其他副本恢復;二是增加系統負載,當搜尋流量增加可以通過動態增加副本來滿足要求。
- 倒排索引(inverted index):由文件中所有不重複詞的列表構成,對於其中每個詞,有一個包含它的文件列表。倒排索引時lucence核心資料儲存結構。
三、中文分詞
3.1、分詞器選型
預設分詞器對英文支援較好,但對中文不友好,會把中文拆分成一個個漢字,這顯然不滿足需求。
市面上中文分詞器不少,該如何選擇,主要考慮以下幾點:
- 自帶預設詞庫,支援自定義詞庫擴充套件。
- 詞庫支援熱更新(不重啟es服務,自動生效)。
- 社群活躍,使用較廣,分詞效果好。
基於以上幾點,很容易想到IK分詞器,IK提供了兩種分詞模式:
分詞模式 | 描述 |
ik_max_word |
會將文字做最細粒度的拆分,比如會將“中華人民共和國國歌” 拆分為“中華人民共和國,中華人民,中華,華人,人民共和國,人民,人,民,共和國,共和,和,國國,國歌”, 會窮盡各種可能的組合 |
ik_smart | 會做最粗粒度的拆分,比如會將“中華人民共和國國歌”拆分為“中華人民共和國,國歌” |
3.2、詞庫更新
分詞是否合理直接影響搜尋結果的精確度,因此詞庫的更新尤為重要,由於es服務剛剛搭建完成,存在以下幾個問題:
- 詞庫更新不便捷、不及時。詞庫雖然支援熱更新,但是需要DBA操作,產品和運營人員無法自行更新。
- 自定義詞庫相對單一。目前只有疾病庫。
- 線上由於分詞不當影響搜尋結果的比例不低。舉個例子:使用者搜尋“浙二醫院”,顯然是想搜“浙大醫學院附屬第二醫院”,但是現有詞庫利用ik_smart模式拆分成“浙”、“二醫院”兩個詞,顯然不符合需求。
- 重建索引不方便。由於詞庫更新後需要重建索引才能使已有資料按照新的詞庫分詞,目前也是需要DBA手動操作,增加了風險。
針對以上問題,提出了幾個解決方案,後續逐步優化解決:
- 某些專有名稱(醫生姓名、醫院科室名稱等)自動實時更新。
- 定期人為擴充詞庫,例如醫院別名、科室別名、疾病症狀等。
- 定期分析使用者搜尋記錄,發現新詞。
- 運營後臺增加詞庫更新和重建索引功能,支援產品和運營人員自行維護詞庫。
丟擲一個問題:由於詞庫更新後需要重建索引才能使已有資料按照新的詞庫分詞,在資料量較小的情況下沒有問題,一旦資料達到一定量級,重建索引的成本較高。百度這種量級的資料是如何應對詞庫更新的呢?可在評論區留言一起探討。
四、資料同步
4.1、資料同步方式選擇
這裡的資料同步是指將資料從mysql同步到es。主要有幾種方式:
- 呼叫es提供的api同步。這種方式最靈活、最實時,但是有一定的編碼成本,主要適用於對索引資料實時性要求較高的場景。
- 同步工具。開源的同步工具也不少,主要有兩種模式:
模式描述 | 代表 | 優點 | 缺點 |
服務定期掃表,通過時間戳欄位實現同步 | logstash | 支援全量和增量同步,索引重建更方便 | 存在一定資料延遲,最少一分鐘同步一次,且無法感知sql的delete操作 |
將自身偽裝成mysql從庫,監控binlog日誌實現同步 | go-mysql-elasticsearch | 實時性較高 | 全量同步較困難,增加mysql伺服器的同步成本 |
結合實際情況,會有定期重建索引需求,線上資料只允許邏輯刪除,且對資料實時性要求並不高,公司的日誌平臺是通過logstash實現的日誌收集,故選擇logstash。
4.2、現有同步方式
公司正在做微服務拆分,且索引往往涉及多條業務線的資料。拿商品舉例,主要包含基本資訊(實時性要求較高)、統計資料(商品購買量、評論量、瀏覽量等,實時性要求不高)。所以最終決定藉助大資料平臺,實時資料10分鐘做一次增量同步,統計資料一天一次同步,資料整理成寬表吐到mysql庫,然後利用logstash將資料同步到es。
五、搜尋API
搜尋是全文索引的核心,下面列出了一些常用的搜尋模式,為了便於理解,下面將各搜尋語句類比成sql。
5.1、基本搜尋(搜尋骨架)
- Query。使用Query DSL(Domain Specific Language領域特定語言)定義一條搜尋語句。
- From/Size。分頁搜尋,類似sql的limit子句。
- Sort。排序,支援一個或多個欄位,類似sql的order by子句。
- Sourcing Filter。欄位過濾,支援萬用字元,類似sql的select欄位。
- Script Fields。使用指令碼基於現有欄位虛構出欄位。例如索引裡包含first name和second name兩個欄位,使用Script Fields可以虛構出一個full name是first name和second name的組合。
- Doc Value Fields。欄位格式化,例如Date格式化成字串,支援自定義格式化型別。
- Highlighting。高亮。
- Rescoring。再評分,僅對原始結果的Top N(預設10)進行二次評分。
- Explain。執行計劃,主要列出文件評分的過程。類似mysql的explain檢視執行計劃。
- Min Score。指定搜尋文件的最小分值,實現過濾。
- Count。返回符合條件的文件數量。
- ...
5.2、核心搜尋(Query DSL)
如果說上面的基本搜尋類比成整條sql語句的骨架,那麼Query DSL就是where條件,主要有以下幾種型別語句:
型別 | 描述 |
Match Query | 全文模糊匹配 |
Match Phrase Query | 短語匹配,和Match Query類似,但要求索引詞的先後順序與輸入搜尋詞的順序一致。完全一致條件似乎比較嚴苛,可通過slop引數控制短語相隔多久也能匹配。 |
Match Phrase Prefix Query | 與短語匹配一致,支援在輸入文字的最後一個詞項上的字首匹配,常用於根據使用者輸入的即時查詢,例如淘寶搜尋框輸入關鍵字後的下拉展示。 |
Multi Match Query |
多欄位搜尋。包含以下幾種模式:1、best_fields:為每個欄位分別生成Match Query搜尋語句,然後取評分最高的欄位作為文件最終得分。2、most_fields:為每個欄位分別生成Match Query搜尋語句,然後將各分值相加然後除以命中語句數,得到文件最終得分。3、phrase and phrase_prefix:行為跟best_fields一致,但使用 Match Phrase Query 代替 Match Query。4、cross_fields:將多欄位合併成一個大欄位搜尋。 |
... |
型別 | 描述 |
Term Query | 術語精確搜尋,將關鍵字當成一個詞來處理。1、如果欄位為keyword型別,即是欄位的精確匹配。2、如果欄位為text型別,則僅當搜尋詞按ik_smart模式分詞後只得到一個詞的情況下才有可能搜尋到文件。 |
Terms Query | 同上,允許入參多個詞。 |
Range Query | 範圍搜尋,常用語數值和時間格式。類似sql的between子句。 |
Exists Query | 搜尋包含指定欄位的文件。 |
Prefix Query | 字首搜尋,常用於實現下拉框輸入的即時搜尋。 |
Wildcard Query | 萬用字元搜尋。通過萬用字元匹配詞條。 |
Regexp Query | 正則表示式搜尋。通過正則表示式匹配詞條。 |
... |
模式 | 描述 | 引數介紹 |
Bool Query | 布林搜尋,由一個或多個型別化的Bool子句構成 |
must:用於搜尋命中文件,條件組合是“and”關係,並且影響評分。filter:用於過濾文件,不同於must,不會對評分有任何影響。should:如果Bool Query包含must或filter子句,則該子句主要用於評分;否則用於搜尋命中文件。可通過minimum_should_match(至少匹配幾個條件)引數控制該行為。must not:與must作用相反,且不會影響評分。 |
Function Score Query | 自定義函式評分搜尋 |
score_mode:自定義函式分值計算模式,包含 Multiply(相乘)、Sum(求和)、Avg(平均)、First(第一個)、Max(最大)、Min(最小)。 boost_mode:搜尋結果分值與自定義函式分值結合得到最終分值的模式,包含 Multiply(相乘)、Replace(僅使用函式分值)、Sum(求和)、Avg(平均)、Max(最大)、Min(最小)。 field_value_factor:欄位值因素,例如文章閱讀量、評論量影響分值。 其他:Weight(權重)、Decay functions(衰變函式)、Random score(隨機評分) |
總結:以上對各種搜尋模式做了簡單介紹,每種模式裡都包含一些搜尋引數,沒有具體展開。開發過程中往往需要結合實際情況,利用各種模式,設定搜尋引數,配置欄位權重,調優自定義函式分值,最終得到比較理想的搜尋結果。
5.3、示例實戰
Talk is cheap, show me the code。
1 GET doctor_index/doctor_info/_search 2 { 3 "query1": {
4 "function_score": { 5 "query": { 6 "bool5": { 7 "must6": [ 8 { 9 "multi_match7": { 10 "query": "張內科", 11 "fields": [ 12 "doctor_name^2", 13 "department_name^1.2", 14 "doctor_skill^0.8", 15 "institution_name^1.4" 16 ], 17 "type8": "cross_fields", 18 "operator9":"and", 19 "analyzer10": "ik_smart" 20 } 21 } 22 ], 23 "must_not11": [ 24 { 25 "term12": { 26 "doctor_is_del": { 27 "value": "1" 28 } 29 } 30 } ] 39 } 40 }, 41 "functions13": [ 42 43 { 44 "script_score14": { 45 46 "script": { 47 "source15": "return Math.log(_score)/Math.log(2);" 48 } 49 } 50 }, { 65 "script_score": { 66 "script": { 67 "source": "String doctorProfessional = doc['doctor_professional'].value; if (doctorProfessional == '主任醫師') { return 1; } else if (doctorProfessional == '副主任醫師') { return 0.8; } else if (doctorProfessional == '主治醫師') { return 0.6; } else if (doctorProfessional == '住院醫師') { return 0.4; } return 0;" 68 } 69 } 70 } ], 86 "boost_mode16": "replace", 87 "score_mode17": "sum" 90 }, 91 "min_score2":3, 92 "sort3": [ 93 { 94 "_score": { 95 "order": "desc" 96 } 97 }, 98 { 99 "doctor_name": { 100 "order": "desc" 101 } 102 } 103 ], 104 "explain4": true 105 }
分析如下:
- 1、定義一個Function Score Query子句。
- 2、指定篩選文件的最低分值為3。
- 3、文件優先按分值降序排,分值相同的情況下按doctor_name降序排。
- 4、展示評分過程的執行計劃。
- 5、定義Bool Query的組合搜尋模式。
- 6、定義Bool Query的must子句。
- 7、定義多欄位搜尋,搜尋關鍵字“張內科”,搜尋欄位:doctor_name權重2、department_name權重1.2、doctor_skill權重0.8、institution_name權重1.4。
- 8、定義多欄位搜尋型別為cross_fields,將以上四個欄位合併成一個大欄位處理。
- 9、定義關鍵字and搜尋,即只有分詞後多欄位同時出現才滿足命中條件。
- 10、定義使用ik_smart分詞模式拆分搜尋詞。
- 11、定義Bool Query的must_not子句。
- 12、過濾掉doctor_is_del=1的文件。
- 13、定義具體的自定義函式陣列。
- 14、定義一條評分規則。
- 15、定義評分函式邏輯,將Query計算後的分值做對數運算。
- 16、指定使用自定義函式分值作為文件的最終分值。
- 17、指定多個自定義函式使用相加的方式計算分值。
一句話解釋:使用自定義函式搜尋模式,定義Bool組合搜尋條件,將doctor_name等四個欄位按照不同的權重組合成一個大欄位,搜尋同時滿足“張內科”關鍵字按照ik_smart分詞後的結果,將關鍵字搜尋得到的分值取對數後加上醫生職稱的分值作為最終分值,然後過濾掉doctor_is_del=1和分值小於3分的文件,最後按照最終分值和doctor_name兩個欄位降序排列,預設取10條記錄,並且展示分值計算過程。
是不是覺得很酸爽,這是提條相對複雜的語句,細細體會。
5.4、評分機制
評分計算主要跟以下三個因素相關:
- 詞頻。詞在文件中出現的次數越多,分值越高。
- 逆向文件頻率。詞在所有文件裡出現的頻率越高,分值越低。
- 欄位長度歸一值。欄位長度越短,分值越高。
5.5、其他API
es還提供了其他強大的API功能,在此就不一一贅述了,例如:
- 文件管理API
- 索引管理API
- 聚合搜尋API
- 叢集資訊API