為了提高搜索命中率和准確率,改善現有羸弱的搜索功能,公司決定搭建全文搜索服務。由於之前缺乏全文搜索使用經驗,經過一番折騰,終於不負期望按期上線。總結了一些使用心得體會,希望對大家有所幫助。計划分三篇:
- 第一篇(使用篇),主要講解基本概念、分詞、數據同步、搜索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 | 會做最粗粒度的拆分,比如會將“中華人民共和國國歌”拆分為“中華人民共和國,國歌” |
IK分詞器項目地址:https://github.com/medcl/elasticsearch-analysis-ik
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條件,主要有以下幾種類型語句:
- 全文搜索(Full Text Query)。文檔地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html
類型 | 描述 |
Match Query | 全文模糊匹配 |
Match Phrase Query | 短語匹配,和Match Query類似,但要求索引詞的先后順序與輸入搜索詞的順序一致。完全一致條件似乎比較嚴苛,可通過slop參數控制短語相隔多久也能匹配。 |
Match Phrase Prefix Query | 與短語匹配一致,支持在輸入文本的最后一個詞項上的前綴匹配,常用於根據用戶輸入的即時查詢,例如淘寶搜索框輸入關鍵字后的下拉展示。 |
Multi Match Query | 多字段搜索。包含以下幾種模式: |
... |
- 詞條搜索(Term Level Query)。文檔地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/term-level-queries.html
類型 | 描述 |
Term Query | 術語精確搜索,將關鍵字當成一個詞來處理。 1、如果字段為keyword類型,即是字段的精確匹配。 2、如果字段為text類型,則僅當搜索詞按ik_smart模式分詞后只得到一個詞的情況下才有可能搜索到文檔。 |
Terms Query | 同上,允許入參多個詞。 |
Range Query | 范圍搜索,常用語數值和時間格式。類似sql的between子句。 |
Exists Query | 搜索包含指定字段的文檔。 |
Prefix Query | 前綴搜索,常用於實現下拉框輸入的即時搜索。 |
Wildcard Query | 通配符搜索。通過通配符匹配詞條。 |
Regexp Query | 正則表達式搜索。通過正則表達式匹配詞條。 |
... |
- 組合搜索(Compound Query)。主要是對以上搜索語句的各種組合,主要介紹Bool Query和Function Score Query,文檔地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/compound-queries.html
模式 | 描述 | 參數介紹 |
Bool Query | 布爾搜索,由一個或多個類型化的Bool子句構成 | must:用於搜索命中文檔,條件組合是“and”關系,並且影響評分。 |
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
六、開發流程
建議使用官方推薦的RestHighLevelClient SDK按照以下流程開發。