Elasticsearch (ES)是一個分散式搜尋和分析引擎,它能為我們提供全文搜尋等各種豐富的功能,You know, for search (and analysis)。此前關於 Elasticsearch 大多都是調優分享、分散式相關,關於基礎的文件基本是簡單介紹,本文是從文件搜尋實踐出發介紹如何搭建一個全文搜尋平臺。本文不做 ES 的介紹,因此看文章需要了解 ES 相關基礎知識。本文作者:allencao,騰訊應用開發工程師。
前言
最開始接到過一個需求,將部門內的研究報告與文件管理起來,利於他們查詢與閱讀。我毫不費勁的直接使用了 mysql 來管理列表,`where title like “%query%” `來實現了搜尋。在使用之初大概只有幾百份文件,並且這些文件標題管理規範,報告也沒有被打上標籤,更沒有摘要之類的資訊,因此這個 “like” 工作的比網盤好用多了。
但是網站慢慢迭代,文件也增長到幾千份,並且運營同學都打上了標籤寫上了摘要,這時 mysql 的文字匹配就完全不能滿足需求了,使用 ES 也是自然而然的事情。ES 全文搜尋解決方案已經非常成熟,應用起來也比較方便,但也有很多細節需要關注,這樣搜尋功能才會更完善。下面將介紹一下用研雲使用 ES 搭建全文搜尋的實踐經驗。
一、搭建步驟
整體搭建步驟如下:
二、準備資料
首先我們需要準備以下三個內容:
被搜尋資料中文分詞停止詞自定義分詞詞庫被搜尋資料被搜尋資料一定要認真處理,資料質量越高,搜尋結果就越準確,被搜尋欄位越多,搜尋結果越豐富。這裡的高質量可以指,被搜尋欄位的文字長短控制在一定範圍,較少的符號,中文儘量使用標準的表達,英文單詞使用空格隔開。尤其是像標題這種權重較高的欄位,可以人工運營的方式進行修改。
報告被搜尋的欄位主要是標題、摘要、標籤、報告內容。其中標題與標籤完全是人工運營方式修改的,質量非常高,在搜尋中也是高權重欄位。摘要有部分人工撰寫,部分自動生成(質量很低)。報告內容使用 OCR 解析出高質量的文字,然後進行清洗,將無效字元、符號、數字等非中文過濾掉,這裡需要注意,其實報告內容文字在業務邏輯中是無法被使用者檢視的,這裡只能被搜尋。所以在 ES 裡報告內容資料可以是無符號,不完整句子的純文字,這樣搜尋時結果會更加豐富,但是因為文字質量低內容混雜,所以權重很低。
分詞停止詞中文分詞停止詞在搜尋中是一個雙刃劍,比較齊全的停止詞,有利於減少文件有效內容的噪音,減少索引容量,但不利於搜尋的精確命中和高亮。停止詞主要是出現頻率高,但實際意義不大的詞,例如介詞“在”,“的”等。
分詞時停止詞的應用可以靈活一些,在標題和標籤的分詞中不使用停止詞,這樣不至於搜“我的世界”時結果只高亮了“我”和“世界”,文件內容文字的分詞上應用停止詞,使用 ES 的 IK 外掛這裡比較難實現,可以改用其他分詞方式。
分詞詞庫自定義分詞詞庫是很有必要的,如果是專業領域分詞的準確率會大大下降,這裡推薦去找一些專業領域詞庫,也可以使用專案中積累的專家詞庫,例如“00後”這個詞就在我們的自定義詞庫中。
三、資料結構
眾所周知 ES 儲存的是文件型資料,mysql儲存的是關係型資料,我們要把關係型資料放進ES裡搜尋需要改變一下資料結構,這裡有三點:
反正規化設計不按照關係型資料庫的設計正規化來設計資料結構,也就是我們通常說的拉平資料,把一條主表記錄與其關聯記錄取出,作為一個文件。舉個例子:
關聯關係欄位的設計在關係型資料中,主記錄的所有關聯關係可以被我們篩選,例如標籤篩選器。
如果在搜尋時也需要支援篩選過濾,這裡設計時需要把被篩選欄位的id也放進 ES 中,例如標籤欄位,標籤title的欄位型別為 text(需要搜尋,會被分詞),但是 id 作為數字被 ES 儲存,數字的篩選效率比 text 高很多。
欄位的取捨資料欄位可以被分為幾種,ID,搜尋欄位,內容欄位,功能欄位。
ID欄位建議 mysql 與 ES 的一樣,這樣節省一個欄位的索引還更加方便管理。不過增加記錄時沒有 ES auto id 快,因為自定義 ID 需要做一次重複檢測。
搜尋欄位指的是需要被全文搜尋的欄位,例如標題,摘要,內容,標籤名等。
內容欄位會根據結果的處理方式有所不同,搜尋結果處理有兩種方式,如下圖:
左圖的方式在查詢到結果後,透過 id 在 mysql 中重新獲取資料,返回到前端,右圖是直接將搜尋結果返回到前端。如果我們還需要查詢一次 mysql 才返回到前端,我們應該捨去純內容欄位來精簡 ES 索引,因為這些內容可以從 mysql 獲取。如果結果是直接返回到前端,那應該按照實際需求來放前端需要的完整內容。採用哪個方式還是要具體需求,透過評估索引大小與資料庫的壓力來選擇。
功能欄位包括剛才提到的被篩選欄位,許可權過濾欄位,還有搜尋最佳化要用到的,報告時間欄位,熱度評分欄位,運營評分欄位。採用第一種方式,具體的欄位如下圖(圖中程式碼僅為示例):
四、ES索引
ES 索引的 Mappings 配置時只有兩點需要注意
使用 text 資料型別需要被搜尋的欄位,欄位型別要設定為 text,這樣欄位才會被分析器處理。但是應用了 text 將不能被 term 過濾器篩選,如果需要過濾可以使用 string 資料型別,他將會自動把欄位處理成 text + keyword。
Analyzer 要靈活設定分析器分為兩種,一個是寫入資料時的使用 `analyzer` 關鍵字配置,還有一個是搜尋時用來分析搜尋關鍵詞的使用 `search_analyzer` 來配置。這裡我們可以直接使用 ik 外掛中的分析器 ik_smart 和 ik_max_word。
索引時,為了提供索引的覆蓋範圍,通常會採用 ik_max_word 分析器,會以最細粒度分詞索引,搜尋時為了提高索引的準確度,會採用ik_smart分析器,會以粗粒度分詞,示例如下:
有個技巧,當某些欄位是高質量並且嚴謹的詞語或者短語時,比如標籤欄位,可以兩個都使用ik_smart分析器,例如有如下文件:
搜尋“微信”、“小程式”、“微信小程式”等,肯定被搜到,因為標題命中了搜尋“小”,這個時候應該被搜到,因為標題有這個字,這樣跟關鍵字匹配是一致的搜尋“艾”,雖然可以匹配到“艾瑞諮詢”,但這個時候還應該被搜到嗎?“艾瑞諮詢”這個標籤是一個專業術語,而且標籤的意義就是要完整表達含義,這個時候只需要分為“艾瑞”和“諮詢”,甚至不分直接使用“艾瑞諮詢”。(艾瑞諮詢這個case我們使用了自定義詞典,因此只做為一整個詞能被搜到)因此將 tags 的 analyzer 都設定為 ik_smart:
五、資料同步
Mysql 與 ES 資料同步有很多種方案,大多數基於 binlog 同步或者中介軟體同步的方案,無法靈活的轉換資料結構。不過資料同步並不是一件難事,只要在業務程式碼中捕捉到 ORM 模型中某些欄位的修改事件,事件發生時,再透過佇列往 ES 中推送資料即可
這裡有幾個地方需要注意:
並不是所有欄位更新都要推送新的資料,只有直接影響搜尋行為的欄位才更新。更新關聯模型的資訊時,可能要批次更新資料,這裡無法避免。要注意佇列的壓力以及ES更新的壓力,搜尋功能較重的平臺,ES 查詢壓力較大,應減小寫入壓力。如果關聯模型更新頻繁,需要從業務邏輯上避免此類情況。某些更新頻繁,但是影響較小的資料可以定時推送,例如熱度評分,評分實時在更新,可以用一個限速的迴圈 worker 來更新此類資料,雖然會有不小的延遲,但基本不影響搜尋體驗。這種偽實時的更新可以滿足資料修改時的需求,但是無法滿足修改索引 mappings 或者全量建立(更新)效率的要求,因此還需要一個全量建立索引的方案。使用場景主要有兩個,a.修改索引 mappings、b.初始化資料時。這裡有幾個要注意的點:
傳輸資料應該使用 bulk 介面,這個介面的效率會非常高請使用 alias 來關聯實際的索引名稱,並在呼叫 ES 介面時使用 alias在儲存量充足的情況下,可以全量建立一個新的索引,然後切換 alias 來達到平滑重建索引六、搜尋 DSL
本節內容我直接放在了《搜尋排名最佳化篇》裡。
Elasticsearch Query DSL 比較複雜並且有一些學習成本,針對不同場景也沒有通用設定,經常一開始用的時候毫無頭緒,就算搜尋可以跑起來了,可結果跟自己想的完全不一樣,所以需要大量的時間來最佳化。
七、豐富搜尋功能
報告搜尋上線以後,同事們確實能很快的找到想要的報告,不過有同事提出了更近一步的搜尋,在搜尋之後直接訪問相關的報告頁面,如果能夠根據報告 ID 聚合在一起就更好了。
這裡給我提了個難題,如果把每一頁的內容當做一個文件,需要先搜尋出所有的內容再根據報告ID聚合在一起,首先無法做分頁,並且資源也不是無限制,這種方案明顯不可取。
這時我瞭解到了 ES 的 nested 和 join(也稱 Parent join) 資料型別,透過 join query 便可以實現以上功能,先來介紹一下 nested 和 join。
文件型資料庫設計一般是沒有考慮關聯關係的,因為其儲存方式不同,需要把資料扁平化。但是 ES 提供了這樣的功能,不過根據使用場景的不同,實現了兩種方案 nested object 和 parent join。
Nested object 把關聯物件和父物件放在同一份文件中,這樣查詢速度快,但子物件更新必須更新整個文件。
Paraent join 把關聯物件和父物件放在不同文件中儲存,這個就很像關係型資料庫中的結構,這樣維護方便,但是涉及要父子關係查詢時無法一個請求完成,查詢速度也慢了一個數量級。
我們可以透過一個表格來檢視區別:
因為這些區別,所以 nested 適應查詢頻繁,更新較少的場景,join 適合更新頻繁,查詢較少的場景。
看完文件以後,發現 nested 是非常適合我們的,因此給報告文件的索引中加入 nested object:
查詢時的 DSL 片段:
因為 OCR 解析出的報告頁文字質量太差,搜尋的效果並不是很好,之後又透過簡單的模式識別的方式從比較規範的報告文件中解析出標題和關鍵詞等欄位用來搜尋。
總結
總體來說 Elasticsearch 的使用沒有想象中複雜,搭建一個搜尋功能的步驟非常明確,但是想要做出一個比較完善的搜尋還是有一定的難度,按照本文介紹的實踐經驗基本可以搭建出一個滿足需求的文件搜尋平臺。