Django全棧教程系列番外篇 - ElasticSearch CheatSheet

0x00 前言

本文為 Cheatsheet 類型文章,用於記錄我在日常編程中經常使用的 ElasticSearch 相關和命令。

最早使用 ElasticSearch 是兩年前了。最近準備用 Django 寫一個全棧式的應用,借用強大的 ES 來做搜索。

這是我在寫程序之餘寫這篇筆記的原因。

最近因為換工作的事情教程更新速度稍微慢一些就把這篇筆記放出來吧。

不定期更新。

官網介紹 ElasticSearch 不僅僅是全文搜索,也可以結構化搜索(這裡用結構化查詢會更準確一些),處理人類語言,地理位置,以及關係。

然而,我在項目使用過程中還是主要用到了全文搜索以及推薦。

不用其他的主要原因是因為 ES 尺有所短寸有所長:

  1. geo 處理方面 postgis 完全就是神一般的存在。為什麼還要用 ES 呢?
  2. 關係型資料庫的核心不就是處理關係?複雜的關係肯定還是放在關係資料庫裡面。
  • highlighted search
  • search-as-you-type
  • did-you-mean suggestions

我對 ElasticSearch 在後台組件里的作用在於搜索與推薦:

  1. 整站的搜索功能
  • 全文搜索
  1. 推薦
  • 依據某幾個維度的數據進行排序

知乎的文章居然不支持 toc, 實在是太蛋疼了。

文章目錄如下

▼ 0x00 前言 : section ▼ 0x01 安裝,配置,基本 shell 命令 : section 1. 安裝 : section 2. 配置 : section 3. 插件 : section 0x02 ElasticSearch 配套工具 : section ▼ 0x03 ElasticSearch 基礎概念 : section ▼ 3.1 Elasticsearch CRUDE 以及基本操作 : section CURDE : section 結構化搜索 : section 聚集搜索 : section ▼ 0x04 全文搜索的基本概念 : section 4.1 全文搜索遇到的挑戰 : section ▼ 4.2 全文搜索的索引時與查詢時 : section 1. 索引時 ES 做了什麼? : section 2. 查詢時 ES 做了什麼? : section 3. 全文搜索調優之中文分詞 : section 4. 全文搜索調優之停止詞 : section 5. 全文搜索調優之同義詞 : section 6. 全文搜索調優之拼寫錯誤 : section ▼ 7. 全文搜索調優之相關性 : section 索引時三因素 : section 查詢時 : section 計算公式 : section 0x05 搜索語法 : section 0x06 Python SDK : section 0x07 踩坑集 : section 0xEE 參考鏈接 : section

0x01 安裝,配置,基本 shell 命令

1. 安裝

具體在項目中的配置建議看一下我寫的配置文章 zhuanlan.zhihu.com/p/33 和並且參考現有代碼 github.com/twocucao/YaD

# 執行如下的命令curl http://localhost:9200/?pretty# 輸出結果{ "name" : "XOGvo8a", "cluster_name" : "docker-cluster", "cluster_uuid" : "fAwp341bQzalzBxRFyD1YA", "version" : { "number" : "6.2.1", "build_hash" : "7299dc3", "build_date" : "2018-02-07T19:34:26.990113Z", "build_snapshot" : false, "lucene_version" : "7.2.1", "minimum_wire_compatibility_version" : "5.6.0", "minimum_index_compatibility_version" : "5.0.0" }, "tagline" : "You Know, for Search"}

2. 配置

配置略

3. 插件

ES 的插件有很多,截止筆者寫這篇文章的時候,ES 最新的版本是 6.2.1 版本。

PS: 兩年前我用的還是 2.3.3 版本。新版本有很多插件配置起來已經有所不同了。比如說 head 現在已經被獨立出來作為一個單純的網頁,chrome 商店可以直接下載。

需要配 ik-analyser. 如果你在 YaDjangoBlog 中起了這個命令,則已經配置完畢。

0x02 ElasticSearch 配套工具

建議使用 Head 插件來進行簡單的查詢與調試。

0x03 ElasticSearch 基礎概念

3.1 Elasticsearch CRUDE 以及基本操作

CURDE

ES 使用的是 RESTFUL API 介面

這也就意味著:

  • PUT 創建記錄
  • GET 獲取記錄
  • POST 更新記錄
  • DELETE 刪除記錄
  • HEAD 是否存在

結構化搜索

ES 寫複雜查詢的時候,語法亂,這個過程需要多翻看 guide 和手冊。

elastic.co/guide/en/ela

全文搜索

全文搜索包含兩個重要方面:

  • 相關性:通過 TF/IDF , 距離 , 模糊相似度,以及其他演算法
  • 分析:大文本 token 化,用於形成倒排索引。這個過程見 4.2

elastic.co/guide/en/ela

聚集搜索

elastic.co/guide/en/ela

0x04 全文搜索的基本概念

4.1 全文搜索遇到的挑戰

在最初開源搜索引擎技術還不是很成熟的時候,我們一般都會使用 RDBMS 進行簡單搜索。

簡單搜索,也就是我們常常使用的 like 查詢(當然,有的資料庫可以使用正則表達式)

這種方式是簡單暴力的查詢方式,優點是實現起來簡單暴力。缺點是在這個場景下性能和準確度很差。

舉例:

  • 假如站點裡文章數量比較大,並且文章內容比較長,則進行一次全表查詢,效率可想而知。當然,做好分庫分表讀寫分離也是能用的。
  • 如果我要對搜索到的詞語進行高亮,則實現方式就只能是把查詢到的文章放在應用層裡面進行批量替換。
  • RDBMS 似乎完全不懂語言與語言之間的區別。比如說:
    • 『停止詞 / 常用詞』有的字我是不需要的,比如南京的狗,其實我想搜的是南京狗,這裡的『的』就不是我需要的。
    • 『同義詞』有的字我需要的是他的同義詞,比如日本黃狗,其實我想搜的是柴犬。
    • 『附加符號』假如說我們搜索一個聲調 [nǐ], 總不能讓用戶打出 [nǐ] 進行搜索吧?總歸要轉為 ni 才能方便搜索
    • 『詞根形式』對於一個單詞,假如是動詞可能有時態上的區分,如果是名詞,可能有單複數的區分。假如我搜 mice, 其實同樣的 mouse 也應該被搜索出來。但有事用這種方式也會矯枉過正,比如 organizations 的 原型其實並不是 organization 而是,organ. (當然,overstemming 和 understemming 也是兩個不可忽視的問題)
      • Number: fox, foxes
      • Tense: pay, paid, paying
      • Gender: waiter, waitress
      • Person: hear, hears
      • Case: I, me, my
      • Aspect: ate, eaten
      • Mood: so be it, were it so
      • PS: 萬幸的是,中文處理中木詞根這個概念。我也就不深入這塊了。
    • 『拼寫問題』 周傑棍與周杰倫
    • 『分詞 / 識別詞』中文不像英文,詞和詞之間是完全沒有空格的,也就是說,中文天然要比英文多一個關於分詞的步驟。

是的,我們需要一種新的姿勢,來進行搜索。也就是本文所說的全文搜索。

4.2 全文搜索的索引時與查詢時

本小節先搞清楚兩個點,

  1. 索引時 ES 做了什麼?
  2. 查詢時 ES 做了什麼?
  • 索引時,指的是 ElasticSearch 在存儲文檔的階段。
  • 查詢時,指的是 ElasticSearch 在查詢文檔的階段。

1. 索引時 ES 做了什麼?

這裡我們略過定義 index,type,document 僅僅指某個 field 被賦值 document 被保存的時候針對這個被賦值的 text 類型 field 的處理。

  • 第一步:文本經過 analyzer 處理
  • 第二步:形成倒排索引

先看第一步:

通常在定義 field 的時候顯式指定 analyzer(分析器).

這個 analyzer 一般的作用如下:

  • STEP 1: 令牌化文本為獨立的詞
  • STEP 2: 詞語轉小寫
  • STEP 3: 去除常見的停止詞
  • STEP 4: 獲取詞的詞根的原型

不同的 analyzer 作用大同小異,拿我們常用的 github.com/medcl/elasti 的話,則也是類似的步驟(下面步驟是我猜測的,沒看源碼)

  1. 令牌化文本為獨立的詞語 - 分詞,並且令牌化文本為獨立的辭彙
  2. 除去常見的停止詞
  3. 匹配同義詞
  4. ....

可以定義欄位的時候可以指定 analyzer(索引時) 與 search_analyzer(查詢時)

先看經過第一步之後,就可以進入第二步形成倒排索引了,此時,倒排索引之於 ElasticSearch 可以類比於 btree 之於 MySQL 或者 Gist 之於 PostgreSQL.

那麼,倒排索引包含哪些東西呢?

  • Terms dictionary
    • 已排序完畢的 terms, 以及包含這些 terms 的 documents 的數量。
  • Postings List
    • 哪些 document 包含這些詞
  • Terms frequency
    • 每個 term 在這些文章的頻率
  • Position
    • 每個 term 在每個 document 的位置,這是為了便於 phrase query 和 proximity query
    • 高頻詞的 phrase query 可能導致 上 G 的數據被讀取。雖然有 cache, 但是遠遠不夠。
  • Offsets
    • 每個 term 在每個 document 的開始和結束,便於高亮
  • Norms
    • 用於給短 field 更多權重的因素.(TODO: 啥玩意)

減少停止詞僅僅可以減少少部分 terms dictionary 和 postings list , 但是 positions 和 offsets data 對 index 的影響則是非常大的。

2. 查詢時 ES 做了什麼?

  • 第一步:文本經過 analyzer 處理
  • 第二步:查詢倒排索引

其實搜索的就是這個玩意。

  • Terms dictionary
  • Postings List
  • Terms frequency
  • Position
  • Offsets
  • Norms

於是,我們就必須關注如何更好的查詢文檔了。下面幾個小節,你就知道全文搜索是比較難調優的了。好,一個一個來。

3. 全文搜索調優之中文分詞

中文分詞以前是個難點,現在基本有成熟的解決方案,在沒有更加牛逼的分詞技術解決方案之前,現在分詞效果主要是拼詞典。

TODO: 這個話題可能比較大,先挖坑,以後填

4. 全文搜索調優之停止詞

使用停止詞是減少索引大小的一種方式(減小索引效果不明顯),那麼,哪些詞語可以唄當做停止詞呢?

  • 低頻詞語:低頻詞語具備高權重
  • 高頻詞語:高頻詞語具備低權重

當然,是否是高頻詞語依據個人經驗主要依據兩點來判斷:

  • 具體場景:比如在英文中,and/the 之類的會比較多,但是中文會比較少。同樣的,中文裡面其他語言的東西會少一些。正文八經的文章出現不正經的辭彙的概率會低。在技術問裡面,『資料庫』屬於高頻辭彙,但是在比如簡書之類的,可能夢想 / 雞湯 / 超級 / 震驚會多一些。掘金的『前端』兩個字絕壁是高頻詞。
  • 抽樣跑新詞發現的程序。社區里多的是新詞發現的腳本。對文章內容或者從搜索框記錄下來的搜索詞跑一下新詞發現的程序,然後人工篩選,應該可以發現更多的高頻和低頻的辭彙。

是不是用上停止詞就好了呢?並不是。

比如:

  • 假如停止詞裡面包含了 not , 那麼 happy 和 not happy 搜索出來的結果則一致。
  • 假如停止詞裡面包含了或,那麼,如果有個樂隊名字叫做『或或』, 則搜索不出來。
  • 假如停止詞裡面包含了 to / be / not / or , 則莎士比亞的名言 『To be, or not to be』 則搜索不出來。

5. 全文搜索調優之同義詞

同義詞也有很多種:

  1. 平級關係:插、戳、刺、扎
  2. 包含關係:成人包含男人和女人
  3. 不容易分清楚關係:
  • 炒,煎,貼,烹,炸,溜 - 汆,涮,煮,燉,煨,焐 - 蒸,鮓 - 鹵,醬,熏,烤,熗,腌,拌,焗

隨著場景的不同,上面有些同義詞也是不能輕而易舉同義的。

-索引時查詢時索引大小耗時變多,同義詞被索引,大小更大耗時幾乎不變相關性準確度下降,所有同義詞相同 IDF, 則在所有文檔的索引記錄中,常用詞和冷門詞權重相同準確度提升,每個同義詞的 IDF 將被校正性能性能下降,查詢需要漲到性能下降,查詢被重寫,用於查找同義詞靈活性變差,同義詞法則不改變已存在記錄,需重新索引不變,同義詞法則可被更新,無需重新索引

由此可見,大部分場景下的索引時如果沒有特別的需求,謹慎使用同義詞

同義詞使用自定義 filter , 並且在新建 analyzer 並指定 filter 即可。

6. 全文搜索調優之拼寫錯誤

有的時候,用戶也會輸入錯誤:

  • 手誤,把『周杰倫』拼成『周傑棍』

這個時候,搜索引擎應該提示一下,您搜索的是不是『周杰倫』呢?

這裡面就遇到了一個問題,我們顯然知道周傑棍和周杰倫是是相似的,為什麼呢?或者說,直觀上感知的詳細,能用數學方式表達出來嗎?

有人說,正則匹配 / 通配符匹配唄。這是一個思路。

Vladimir Levenshtein 和 frederic damerau 給出了一種相似度演算法 en.wikipedia.org/wiki/D

一個片語通過轉換到另一個詞的步數就是其距離:

  • 替換:『周杰倫』到『周傑棍』
  • 插入:『周傑』到『周傑棍』
  • 刪除:『周杰倫』到『周傑』
  • 相鄰字元轉換:『周倫傑』和『周杰倫』 , 但是『周傑棍的雙節倫』到『周杰倫的雙節棍』 並不是相鄰字元轉換

用法:

指定 "fuzziness": step 即可

當 step >=2 的時候,ES 進行查詢的時候,每次查詢都會遍歷 terms 字典,所以,如果 fuzziness 大於 2 的時候遍歷 terms 的數量則非常驚人了。

  • 方法一:設置 prefix_length, 單詞的前面一定長度不進行 fuzzy 匹配。一般設置為 3 (估計這是屬於英文的匹配,中文環境做不了參考);
  • 方法二:設置 max_expansins, 類似於 RDBMS 的 limit, 查詢到一定記錄之後停止查詢。

fuzzy match query 也是支持的,比如說,假如你指定 "fuzziness" 為 1, 搜索周傑棍,則將周杰倫,周傑全搜索出來了。似乎搜索的很全面呀,但是問題來了:

依據 TF/IDF 的高頻低權重,低頻高權重的計算方式,周傑棍由於出現次數極少,反而獲得了極高的權重。

跑個題,這種因為『出現次數少,查詢的時候反而顯得權重較高』的情況。並不僅僅出現在 TF/IDF 演算法上。

這方面搜索引擎和人是一樣一樣的

  • 小孩子聽膩了家長們所說的『帶著腦子去學習』, 反而覺得以前沒出現的新詞叫『刻意學習』牛逼到爆。
  • 美女聽膩了直男癌說的『漂亮』, 反而覺得誇她『品質 / 品味』的話語詞詞入心。

回到正題

所以,一般情況下還是建議拼寫錯誤主要還是用於:

  • Search as you type : completion suggester
  • Did you mean : phrase suggester

7. 全文搜索調優之相關性

我們在接觸 RDBMS 的時候系統是沒有相關性的說法的,比如說,2017 年 12 月份 xxx 用戶的訂單,就是直接 select 出來這些訂單。因為 where 語句後面包含了界限明確的條件,而全文搜索則不然。

這個時候一個人拍著桌子站起來,說:不對呀,我要搜索包含周杰倫的所有文章。這咋沒有條件邊界。

嗯,稍等,『選出所有包含周杰倫的文章』條件很清晰。但問題是,排序怎麼做?按照日期排?按照點擊率排?這篇文章上周已經在在搜索靠前了,已經『長江後浪推前浪了』上了,這周是不是該差不多『前浪死在沙灘上』了?

Elasticsearch 中使用的計算 score 的公式叫做 practical scoring function, 這個公式借鑒於 TF/TDF 以及 矢量空間模型,但有更多的特徵比如,條件因素,欄位長度正態化,term / query clause boosting

全文搜索不僅僅找到匹配的 documents, 並且按照相關性進行排序(其實就是打分 score)。

為什麼需要打分呢?從相親角度來說,上海內環有房肯定是個超級大加分項。同樣是錄入信息,在上海內環有房的權重值可是設置的高一些。

嗯,其實相關性的調優是最難的部分。

索引時三因素

先看前兩個因素 TF/IDF

  • tf(t in d) = sqrt(frequency)
  • idf(t) = 1 + log (numDocs / (docFreq + 1))

再看後一個因素 Field-Length norm

標題越短,這個詞對這個 field 的代表性越強

  • norm(d) = 1 / sqrt(numTerms)

查詢時

幾個詞 -> 幾維度 -> 尋求最佳匹配以及近似匹配

  • 最佳匹配應該是通過計算長度(應該是,但不確定)
  • 近似匹配,計算距離最近的 cos 值。

計算公式

這個公式調優的時候需要用到

score(q,d) = queryNorm(q) ·coord(q,d) ·∑(tf(t in d)·idf(t)2·t.getBoost()·norm(t,d)) (t in q)

0x05 搜索語法

Single document APIs

  • Index API
  • Get API
  • Delete API
  • Update API
  • Multi-document APIs

Multi Get API

  • Bulk API
  • Delete By Query API
  • Update By Query API
  • Reindex API

0x06 Python SDK

官方提供了兩個 SDK 方便我們進行日常的開發:

  • elasticsearch
  • elasticsearch_dsl

我更喜歡 elasticsearch , 而不是 elasticsearch_dsl, 因為寫起來更容易結合 elasticsearch-head 進行 profile

前者偏底層一些,後者偏高層一些,高底層關係的有點類似於 sql 和 sqlalchemy core 之間的關係。

0x07 踩坑集

0xEE 參考鏈接

  • zhihu.com/question/1964

ChangeLog:

  • 2018-02-15 重修文字
  • 2018-03-31 編輯器居然不支持表格?

推薦閱讀:

Elasticsearch Logstash Kibana(ELK)代碼和知識點總結(三)
elastic search集群配置
Elasticsearch與Solr的選擇?
ElasticSearch優化系列二:機器設置(內存)
elasticsearch-數據自動刪除

TAG:Python | Elasticsearch | 中文分詞 |