上文 使用PostgreSQL進行中文全文檢索 中我使用 PostgreSQL 搭建完成了一套中文全文檢索系統,對數據庫配置和分詞都進行了優化,基本的查詢徹底能夠支持,可是在使用過程當中仍是發現了一些很惱人的問題,包括查詢效果和查詢效率,萬幸都一一解決掉了。css
其中過程自認爲仍是頗有借鑑意義的,今天來總結分享一下。html
博客歡迎轉載,請帶上來源:http://www.cnblogs.com/zhenbianshu/p/8253131.html web
一開始是分詞效果的問題:sql
乒乓球拍賣啦、南京市長江大橋
這種歧義句的分詞,尚未一個分詞插件可以達到 100% 的準確率,固然包括咱們正在使用的 scws
分詞庫;scws 支持更爲靈活的分詞等級,爲了能分出較多的詞來儘可能包含目標結果,咱們將 scws 的分詞等級調爲了 7
(不瞭解的能夠看上文),但同時也引入了更奇葩的問題: 搜索天安門
查不到 天安門廣場
。。。數據庫
緣由也很另人無語:數組
天安門廣場
的分詞結果向量 tsv 是 '天安':2 '天安門廣場':1 '廣場':4 '門廣':3
;to_tsquery('parser', '天安門')
tsq 的結果是 '天安門' & '天安' & '安門'
;SELECT * FROM table WHERE tsv @@ tsq
, 因爲 tsv 裏沒有 tsq 裏的 安門
向量,匹配失敗。一個常識:你們想搜一個地點時大多會先輸入其名稱前面的部分,基於此考慮,我向表內引入 B樹索引支持前綴查詢,配合原來分詞的 GIN 索引,解決了此問題。緩存
如Mysql同樣,PostgreSQL 也支持經過 like '關鍵詞%'
語句來使用 B樹索引。在 name 列上添加了 B樹索引,再修改查詢語句變爲 SELECT * FROM table WHERE tsv @@ tsq OR name LIKE 'keyword%'
,這樣結果就徹底 OK 啦。工具
緊接着又發現了新的問題:post
PostgreSQL 的 GIN 索引(Generalized Inverted Index 通用倒排索引)存儲的是 (key, posting list)對
, 這裏的 posting list 是一組出現鍵的行ID。如 數據:性能
行ID | 分詞向量 |
---|---|
1 | 測試 分詞 |
2 | 分詞 結果 |
則索引的內容就是 測試=>1 分詞=>1,2 結果=>2
,在咱們要查詢分詞向量內包含 分詞
的數據時就能夠快速查找到第1,2列。
但這種設計也帶來了另外一個問題,當某一個 key 對應的 posting list 過大時,數據操做會很慢,如咱們的數據中地點名帶有 飯店
的數據就不少,有幾十萬,而咱們的需求有一項就是要對查詢結果按照 評分
一列倒序排序,這麼幾十萬數據,數據庫響應超時會達到 3000 ms。
咱們指望的響應時間是 90% 50ms 之內,雖然統計結果顯示,確實 90% 的請求已經符合要求,但另外的 10% 徹底不能用也是不可能接受的。
接下來的優化就是針對這些 bad case。
對於這種響應超時的問題,你們確定會想到萬能的緩存:把響應超時的查詢結果放到緩存,查詢時先檢查緩存。
但是超時的畢竟只有不多一部分,緩存的命中率堪憂
。雖然這一小部分查詢可用了,可是全部查詢語句都會多出一次取緩存的操做。
爲了能提升緩存命中率,我還特地統計了關鍵字各長度的搜索數量佔比和超時率佔比,發現如下狀況:
這種狀況打消了我只針對某些長度的關鍵詞設置緩存的想法。
不只是命中率問題,緩存過時時間和緩存更新等更是大坑,基於以上考慮,緩存方案完全被放棄。
一個方法不行,那就換一個方向,既然某些關鍵詞的結果集太大,那麼咱們就將它變小一些,咱們一開始採用的策略是分表。
因爲 Poi 地點都有區域屬性,咱們以區域 ID 將這些數據分紅了多個數據表,原來最大的關鍵詞結果集有幾十萬,拆分到多個表後,每一個表中最大的關鍵詞結果集也就幾萬,此時的排序性能提升了,基本在 100~200ms
之間。
查詢時咱們先經過位置將用戶定位到區域,根據區域 ID 肯定要查詢的表,再從對應表內查詢結果。
這個方案的缺點也很是多:
終於靈活考慮了業務需求,引入子查詢提出了一種頗爲完美的方案:
用戶在搜索框鍵入了 飯店、賓館
等無心義關鍵詞,不一樣於搜索 海底撈
,此時用戶也不知道他本身須要什麼,對搜索結果是沒有明確期待的。
這時候,咱們也並不須要很愣地把全國名字中帶有飯店、賓館的地點都拿出來排序,這樣的排序結果用戶也不必定滿意。 咱們能夠只取一部分 Poi 地點給用戶,若是結果用戶不滿意,會再完善關鍵詞,而關鍵詞稍有完善,結果集就會極大地減少。
子查詢用來實現結果集過濾很是有效,如咱們能夠在極大頁碼查詢分頁時使用子查詢先過濾掉一大批無用數據。
本例中,咱們在子查詢語句中使用 limit 語句限制取的結果集條數,從而大大減少排序壓力,查詢語句相似 SELECT id FROM (SELECT * FROM table WHERE tsv @@ tsq OR name LIKE 'keyword%' LIMIT 10000) AS tmp ORDER BY score DESC
。
這樣優化事後,查詢語句的最差性能也能夠穩定在 170ms 如下了。
本覺得優化到此爲止了呢,但是有次在試着查詢 中關村
和 東
兩個關鍵詞時,我明確感受到了響應時間的差別, 100ms 左右的時間差仍是很明顯的。
子查詢語句纔是這條 SQL 語句的效率關鍵,因而我開始分析 東
這個關鍵詞的 子查詢SQL
語句,首先我試着調整語句中 limit 的限制值,發現即便只取 1000條,響應時間也在 100ms 以上。
接着我又嘗試改變 SQL 語句的 WHERE 條件,去除 OR name LIKE 'keyword%'
後, 總條數並無太大的變更,結果集由 13w 減少到了 11w, 但 添加 limit 後的效率卻急劇提高:
SQL | 結果條數 | 響應時間 | 添加 limit 後 | SQL | 響應時間 |
---|---|---|---|---|---|
WHERE tsv @@ tsq OR name LIKE 'keyword%' |
13W | 2400ms | WHERE tsv @@ tsq OR name LIKE 'keyword%' LIMIT 10000 |
170ms | |
WHERE tsv @@ tsq |
11W | 1900ms | WHERE tsv @@ tsq limit 10000 |
25ms |
這樣對比起來就很明顯了, 分詞查詢的 GIN 索引和前綴詞查詢的 B樹索引之間配合並不完美。
想一想也是,若是在一個索引上取 1w 條數據,直接取就好了,而若是在兩個索引上取 1w 數據,那麼還得考慮每一個索引上各取多少,取完後還要排重。
問題分析完,那麼就得根據問題尋找解決方案了,怎麼能把兩個索引併到同一索引上呢?把分詞 GIN 索引併到 B樹索引顯然是不可能的,只能試着使用分詞來替代 B樹索引。
當時有三種方案:
text[]
)存儲分詞結果,後續往此字段內靈活添加前綴詞。但填充數組字段須要調用 SELECT to_tsvector('parser', 'nane')
查詢後使用腳本處理結果後再寫入數組,比較麻煩。最好的方案固然是最後一種,改動最小,因而我就查詢了一下 PostgreSQL 向量拼接,仍是找到了向量拼接的方法,使用 ::tsvector
將字符串強轉成向量,再使用 ||
拼接到原來的分詞向量上,SQL 語句相似 SELECT to_tsvector('parser', 'keyword') || 'prefix'::tsvector
。
在查詢時,就能夠直接使用 WHERE tsv @@ to_tsquery('parser', 'keyword')
查詢前綴了。這樣,子查詢語句的響應時間就能夠大大下降了,在 50ms 左右,並且還能夠經過減少 LIMIT 值來加快響應。
此後,B樹索引就能夠退休啦~
以上就是我對 PostgreSQL 關鍵詞查詢從效果到效率優化的全過程了,效果和效率已經徹底達標了。固然,還能夠對用戶體驗進行再優化,好比添加錯別字識別、拼音首字母智能識別等,打磨好一款產品固然是很是不容易的,還須要繼續努力。
順便吐槽幾句周邊同事對 PostgreSQL 的態度,理由居然是認爲它是一個開源產品,可能會有各類埋得深的坑,因此不信任。
比較想不到比較前沿的互聯網公司也會有人對開源抱如此見解,不能否認不少開源產品或工具都有各類各樣的坑,但爲此因噎廢食大可沒必要,咱們一直在用的 Linux/Git 仍是開源產品呢,可有多少人離不開它們?並且閉源產品就不會出現問題麼?也不能否認 PostgreSQL 小衆,但它也有本身的特點,並且近年來它的佔有率一率攀升,將來什麼樣,還未可知。
關於本文有什麼問題能夠在下面留言交流,若是您以爲本文對您有幫助,能夠點擊下面的 推薦
支持一下我,博客一直在更新,歡迎 關注
。