在Django中優化 Postgres 全文搜索
Postgres提供了開箱即用的強大搜索功能。這對於大多數Django應用程序來說,不需要運行和維護一個ElasticSearch集群,除非你需要ElasticSearch提供的高級特性。Django通過內置的Postgres模塊很好地集成了Postgres搜索。
(此處已添加圈子卡片,請到今日頭條客戶端查看)對於小型數據集,默認配置執行得很好,但是當數據增長時,默認的搜索配置就會變得非常慢,我們需要啟用某些優化來保持查詢的速度。
本文將引導你設置Django和Postgres、索引示例數據以及執行和優化全文搜索。
這些示例是經過Django + Postgres設置測試過的,但是這些優化建議通常適用於任何編程語言或框架,只要它使用Postgres。
如果你已經是一個Django的老手,那麼你可以跳過第一步,直接跳到優化部分。
項目設置
創建目錄並配置好Django項目。
現在我們需要安裝3個依賴:
- psycopg2:用於Python的Postgres客戶端庫
- wikipedia:檢索Wikipedia文章的客戶端庫
- django-extensions:用來簡化SQL查詢的調試
我們還需要在本地運行Postgres。我將在這裡使用Postgres的docker化版本,因為它更容易設置,但是如果你願意,你可以自行安裝一個Postgres二進位文件。
打開 full_text_search/docker-compose.yml
項目結構現在應該如下面的輸出所示。我們將忽略venv目錄,因為它包含了很多文件,現在還用不到。
我們將修改默認資料庫設置,使用Postgres而不是SQLite。在settings.py中更改 DATABASES屬性:
我們還將修改我們的INSTALLED_APPS來引入幾個應用程序:
- django.contrib.postgres Django的Postgres模塊,全文搜索要用到
- django_extensions 用於在Python中執行查詢時列印SQL日誌
- 我們的web應用程序
打開full_text_search/settings.py並修改:
啟動 Postgres 和 Django.
如果我們打開瀏覽器並輸入http://localhost:8000,應該會看到安裝成功提示。
創建模型並索引示例數據
假設我們有一個表示Wikipedia頁面的模型。為了簡單起見,我們只使用兩個欄位:標題和內容。
打開 full_text_search/web/models.py
現在運行遷移去創建這個模型。
我們使用一個腳本來索引隨機的Wikipedia文章,並將內容保存到Postgres中。
編輯 web/index_wikipedia.py
現在我們運行腳本來索引Wikipedia。運行這個腳本時可能會出現錯誤,但只要我們設法存儲了幾百篇文章,就不必擔心這些錯誤。這個腳本運行將需要一段時間,所以請喝杯咖啡,幾分鐘後再回來吧。
優化搜索
現在假設我們希望允許用戶對內容執行全文搜索。我們將互動式地查詢數據集來測試全文搜索。打開一個Django shell會話:
Django會執行兩個預備查詢,最後執行我們的搜索查詢。查看最後一個查詢,我們第一眼就可以看到,僅查詢執行和序列化的執行時間為~315ms。當我們想要將頁面載入速度保持在以毫秒為單位的兩位數時,這就太慢了。
讓我們仔細看看為什麼這個查詢執行得如此慢。打開第二個終端,我們將在其中使用出色的Postgres查詢分析器。複製上面的查詢並運行 EXPLAIN ANALYZE:
我們可以看到,雖然計劃時間非常快(~3ms),但是執行時間卻非常慢,在~220ms。
我們可以注意到,查詢對整個表執行順序掃描,以便找到匹配的記錄。我們可以通過使用索引來優化這個查詢。
此外,這個查詢使用to_tsvector將content列從文本規範化為tsvector,以便執行全文本搜索。
tsvector類型是文本的標記化版本,它使搜索列規範化(更多關於標記化的內容請查看這裡)。Postgres需要對每一行執行這種標準化,而每一行包含一個完整的Wikipedia頁面。這是一個CPU密集型且比較慢的操作。
專門的搜索列和gin索引
為了避免將文本實時轉換為tsvector,我們將創建一個專門的列,它只用於搜索。這個列應該在插入或更新時被填充。在執行查詢時,我們將避免轉換類型帶來的性能損失。
由於我們現在可以擁有一個tsvector類型,所以我們還可以添加一個gin索引來加快查詢速度。gin索引會確保使用索引掃描而不是對所有記錄進行順序掃描來執行搜索。
打開我們的web/models.py文件,修改Page模型。
運行這個遷移。
Postgres觸發器
理論上我們的問題已經解決了。我們有一個Gin索引列,當我們對它進行搜索時,它應該會執行得很好,但是這樣做我們又引入了另一個問題:優化後的content_search列需要手動保持同步,並在內容列更新時進行更新。
幸運的是,Postgres為我們提供了一個額外的特性來解決這個問題,即觸發器。觸發器是Postgres函數,當對一行執行特定操作時觸發。我們將創建一個觸發器,它會在一個content行被創建或更新時自動填充content_search。這樣一來,Postgres將保持這兩列的同步,而無需我們編寫任何Python代碼。
為了添加觸發器,我們需要手工編寫Django遷移。這將添加觸發器函數並更新我們所有的Page行,以確保在遷移時為我們的現有記錄觸發觸發器並填充content_search列。如果你有一個非常大的數據集,你可能不希望在生產中這樣做。
在web/migrations/0003_create_text_search_trigger.py中添加一個新的遷移。確保在dependencies中修改前面的遷移,因為前面自動生成的遷移可能對你的來說是不同的。
運行這個遷移。
測量性能改進情況
最後就到了有趣的部分,我們來驗證一下這個查詢的執行速度是否比以前更快。再次打開一個Django shell,但是在過濾行時使用索引化的content_search列,而不是普通的content列。
查詢執行時間從0.220秒下降到0.001秒!
讓我們再次分析這個查詢,看看Postgres是如何執行它的。複製上面的查詢並通過EXPLAIN ANALYZE運行它。
這裡是有趣的地方:
Postgres 在content_searchcolumn列上使用索引代替順序掃描。
我們也不再為每一行執行昂貴的to_tsquery操作,而是按原樣使用content_search列。
缺點
不幸的是,在使用這種優化技術時存在權衡。
- 因為我們維護文本的另一個列的唯一目的是加快搜索速度,所以表的大小佔用了更多的空間。此外,content_search列上的gin索引也需要佔用空間。
- 由於搜索列在每次UPDATE或INSERT時都會更新,因此也會減慢對資料庫的寫入操作。
如果你受到內存和磁碟的限制,或者需要快速寫入,這種技術可能不適合你的使用情況。然而,我懷疑大多數CRUD應用程序都可以犧牲磁碟和寫入速度來實現閃電般的快速搜索。
結論
Postgres提供了出色的全文搜索功能,但速度有點慢。為了加快文本搜索,我們添加了一個tsvector類型的二級列,這是我們文本的一個搜索優化版本。
我們在搜索列上添加一個Gin索引,以確保Postgres執行索引掃描,而不是順序掃描。這將減少一個數量級的查詢執行時間。
為了保持文本列和搜索列同步,我們使用了一個Postgres觸發器,它會在我們對文本列進行任何修改時自動填充搜索列。
完整的代碼示例可以在Github(https://github.com/danihodovic/django-postgres-fulltext-search )上找到。
英文原文:https://dev.to/danihodovic/optimizing-postgres-full-text-search-with-django-42hg
譯者:野生大熊貓
※Python中Scikit-Learn庫的分類方法總覽
※Python標準庫可能準備大清洗了!
TAG:Python部落 |