文章目錄
- 1. Django Haystack 簡介
- 2. 安裝 django-haystack和elasticsearch 2
- 3. 構建容器來運行 elasticsearch 服務
- 4. 配置 Haystack
- 5. 處理數據
- 6. 配置 URL
- 7. 修改搜索表單
- 8. 創建搜索結果頁面
- 9. 高亮關鍵詞
- 10. 建立索引文件
- 11. 修改搜索引擎為中文分詞
- 12. 防止標題被截斷
- 13. 線上發布
1. Django Haystack 簡介
在此之前我們使用了 Django 內置的一些方法實現了一個簡單的搜索功能。但這個搜索功能實在過于簡單,沒有多大的實用性。對于一個搜索引擎來說,至少應該能夠根據用戶的搜索關鍵詞對搜索結果進行排序以及高亮關鍵字。現在我們就來使用 django-haystack 實現這些特性。
jango-haystack 是一個專門提供搜索功能的 django 第三方應用,它支持 Solr、Elasticsearch、Whoosh、Xapian 等多種搜索引擎,上一版本的教程中我們使用 Whoosh 加 jieba 中文分詞的方案,原因是為了簡單,無需安裝外部服務。但現在有了 docker,安裝一個外部服務就是輕而易舉的事情,所以這次我們采用更為強大的 elasticsearch 作為我們博客的搜索引擎,同時使用 elasticsearch 的中文分詞插件 ik,來提升中文搜索的效果。
2. 安裝 django-haystack和elasticsearch 2
django-haystack 安裝非常簡單,只需要執行 pipenv install django-haystack 即可。需要注意的是,目前 elasticsearch 有 2 系列和 5 系列兩大版本,本來新項目的原則是盡可能采用新版本,但目前 django-haystack 在 pypi 上發布的穩定版只支持 elasticsearch2,master 分支下支持 elasticsearch5,因此處于穩定性考慮,我們暫時使用 elasticsearch2,后續如果 django-haystack 發布了支持 elasticsearch5 的pypi版本,我們會升級到 elasticsearch5,有了 docker,升級就是輕而易舉的事情。
由于使用 elasticsearch 服務,這里我們不要直接使用 pipenv 安裝,而是手動編輯 Pipfile 文件,指定 SDK 的版本,否則 pipenv 默認會安裝最新版。打開 Pipfile 文件,將依賴手動添加到 packages 板塊下:
[packages]
django = "~=2.2"
django-haystack = "*"
elasticsearch = ">=2,<3"
3. 構建容器來運行 elasticsearch 服務
接下來就是構建一個新的容器來運行 elasticsearch 服務,因此首先需要來編排容器鏡像,回顧一下容器鏡像的目錄結構:
由于 elasticsearch 在線上環境和本地測試都要使用,我們把鏡像編排在 production 目錄下,新建一個 elasticsearch 目錄,用來存放和 elasticsearch 相關的內容。Dockfile 內容如下:
FROM elasticsearch:2.4.6-alpineCOPY ./compose/production/elasticsearch/elasticsearch-analysis-ik-1.10.6.zip /usr/share/elasticsearch/plugins/
RUN cd /usr/share/elasticsearch/plugins/ && mkdir ik && unzip elasticsearch-analysis-ik-1.10.6.zip -d ik/
RUN rm /usr/share/elasticsearch/plugins/elasticsearch-analysis-ik-1.10.6.zipUSER root
COPY ./compose/production/elasticsearch/elasticsearch.yml /usr/share/elasticsearch/config/
RUN chown elasticsearch:elasticsearch /usr/share/elasticsearch/config/elasticsearch.ymlUSER elasticsearch
這個鏡像從 elasticsearch 的官方基礎鏡像 2.4.6 版本進行構建,接著我們把 ik 分詞插件復制到 elasticsearch 安裝插件的目錄下,然后解壓啟用。
接著我們又把 elasticsearch.yml 配置文件復制到容器內,然后切換用戶為 elasticsearch,因為我們將以 elasticsearch 用戶和組運行 elasticsearch 服務。
elasticsearch.yml 配置文件內容很簡單:
bootstrap.memory_lock: true
network.host: 0.0.0.0
其中 bootstrap.memory_lock 這個參數是為了提高 elasticsearch 的效率(涉及到 JVM 相關的優化,不做過多介紹)。network.host 指定服務啟動的地址。
接著修改 docker compose 文件,我們先在本地啟動,因此修改 local.yml 文件,加入 elasticsearch 服務:
version: '3'volumes:database_local:esdata_local:services:hellodjango_blog_tutorial_local:# 其它配置不變...depends_on:- elasticsearch_localelasticsearch_local:build:context: .dockerfile: ./compose/production/elasticsearch/Dockerfileimage: elasticsearch_localcontainer_name: elasticsearch_localvolumes:- esdata_local:/usr/share/elasticsearch/dataports:- "9200:9200"environment:- "ES_JAVA_OPTS=-Xms512m -Xmx512m"ulimits:memlock:soft: -1hard: -1nproc: 65536nofile:soft: 65536hard: 65536
主要是加入了 elasticsearch 服務,其中 environment 和 ulimits 的參數與 elasticksearch 服務調優有關,對于簡單的博客搜索來說,調優的意義不是很大,因此這里不做過多介紹,感興趣的可以參考 elasticksearch 的官方文檔。
4. 配置 Haystack
安裝好 django haystack 后需要在項目的 settings.py 做一些簡單的配置。
首先是把 django haystack 加入到 INSTALLED_APPS 設置里:
文件位置:blogproject\settings\common.py
INSTALLED_APPS = ['django.contrib.admin',# 其它 app...'haystack','blog','comments',
]
然后加入如下配置項
# 搜索設置
HAYSTACK_CONNECTIONS = {'default': {'ENGINE': 'haystack.elasticsearch2_backend.Elasticsearch2SearchEngine','URL': '','INDEX_NAME': 'hellodjango_blog_tutorial',},
}
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
HAYSTACK_CONNECTIONS 的 ENGINE 指定了 django haystack 使用的搜索引擎,這里我們使用了 haystack 默認的 Elasticsearch2 搜索引擎。PATH 指定了索引文件需要存放的位置,我們設置為項目根目錄 BASE_DIR 下的 whoosh_index 文件夾(在建立索引是會自動創建)。
HAYSTACK_SEARCH_RESULTS_PER_PAGE 指定如何對搜索結果分頁,這里設置為每 10 項結果為一頁。
HAYSTACK_SIGNAL_PROCESSOR 指定什么時候更新索引,這里我們使用 haystack.signals.RealtimeSignalProcessor,作用是每當有文章更新時就更新索引。由于博客文章更新不會太頻繁,因此實時更新沒有問題。
由于開發環境和線上環境,elasticsearch 服務的 url 地址是不同的,所以我們在 common 的配置中沒有指定 url,在 local.py 設置文件指定之:
文件位置:blogproject/settings/local.py
HAYSTACK_CONNECTIONS['default']['URL'] = 'http://elasticsearch_local:9200/'
5. 處理數據
接下來就要告訴 django haystack 使用哪些數據建立索引以及如何存放索引。如果要對 blog 應用下的數據進行全文檢索,做法是在 blog 應用下建立一個 search_indexes.py 文件,寫上如下代碼:
文件位置:blog/search_indexes.py
from haystack import indexes
from .models import Postclass PostIndex(indexes.SearchIndex, indexes.Indexable):text = indexes.CharField(document=True, use_template=True)def get_model(self):return Postdef index_queryset(self, using=None):return self.get_model().objects.all()
這是 django haystack 的規定。要相對某個 app 下的數據進行全文檢索,就要在該 app 下創建一個 search_indexes.py 文件,然后創建一個 XXIndex 類(XX 為含有被檢索數據的模型,如這里的 Post),并且繼承 SearchIndex 和 Indexable。
為什么要創建索引?索引就像是一本書的目錄,可以為讀者提供更快速的導航與查找。在這里也是同樣的道理,當數據量非常大的時候,若要從這些數據里找出所有的滿足搜索條件的幾乎是不太可能的,將會給服務器帶來極大的負擔。所以我們需要為指定的數據添加一個索引(目錄),在這里是為 Post 創建一個索引,索引的實現細節是我們不需要關心的,我們只關心為哪些字段創建索引,如何指定。
每個索引里面必須有且只能有一個字段為 document=True,這代表 django haystack 和搜索引擎將使用此字段的內容作為索引進行檢索(primary field)。注意,如果使用一個字段設置了document=True,則一般約定此字段名為text,這是在 SearchIndex 類里面一貫的命名,以防止后臺混亂,當然名字你也可以隨便改,不過不建議改。
并且,haystack 提供了 use_template=True 在 text 字段中,這樣就允許我們使用數據模板去建立搜索引擎索引的文件,說得通俗點就是索引里面需要存放一些什么東西,例如 Post 的 title 字段,這樣我們可以通過 title 內容來檢索 Post 數據了。舉個例子,假如你搜索 Python ,那么就可以檢索出 title 中含有 Python 的Post了,怎么樣是不是很簡單?數據模板的路徑為 templates/search/indexes/youapp/<model_name>_text.txt(例如 templates/search/indexes/blog/post_text.txt),其內容為:
文件位置:templates/search/indexes/blog/post_text.txt
{{ object.title }}
{{ object.body }}
這個數據模板的作用是對 Post.title、Post.body 這兩個字段建立索引,當檢索的時候會對這兩個字段做全文檢索匹配,然后將匹配的結果排序后作為搜索結果返回
6. 配置 URL
接下來就是配置 URL,搜索的視圖函數和 URL 模式 django haystack 都已經幫我們寫好了,只需要項目的 urls.py 中包含它:
文件位置:blogproject/urls.py
urlpatterns = [# 其它...path('search/', include('haystack.urls')),
]
另外在此之前我們也為自己寫的搜索視圖配置了 URL,把那個 URL 刪掉,以免沖突:
文件位置:blog/urls.py
# path('search/', views.search, name='search'),
7. 修改搜索表單
修改一下搜索表單,讓它提交數據到 django haystack 搜索視圖對應的 URL:
文件位置:templates/base.html
<form role="search" method="get" id="searchform" action="{% url 'haystack_search' %}"><input type="search" name="q" placeholder="搜索" required><button type="submit"><span class="ion-ios-search-strong"></span></button>
</form>
主要是把表單的 action 屬性改為 {% url ‘haystack_search’ %}
8. 創建搜索結果頁面
haystack_search 視圖函數會將搜索結果傳遞給模板 search/search.html,因此創建這個模板文件,對搜索結果進行渲染:
文件位置:templates/search/search.html
{% extends 'base.html' %}
{% load highlight %}{% block main %}{% if query %}{% for result in page.object_list %}<article class="post post-{{ result.object.pk }}"><header class="entry-header"><h1 class="entry-title"><a href="{{ result.object.get_absolute_url }}">{% highlight result.object.title with query %}</a></h1><div class="entry-meta"><span class="post-category"><a href="{% url 'blog:category' result.object.category.pk %}">{{ result.object.category.name }}</a></span><span class="post-date"><a href="#"><time class="entry-date" datetime="{{ result.object.created_time }}">{{ result.object.created_time }}</time></a></span><span class="post-author"><a href="#">{{ result.object.author }}</a></span><span class="comments-link"><a href="{{ result.object.get_absolute_url }}#comment-area">{{ result.object.comment_set.count }} 評論</a></span><span class="views-count"><ahref="{{ result.object.get_absolute_url }}">{{ result.object.views }} 閱讀</a></span></div></header><div class="entry-content clearfix"><p>{% highlight result.object.body with query %}</p><div class="read-more cl-effect-14"><a href="{{ result.object.get_absolute_url }}" class="more-link">繼續閱讀 <spanclass="meta-nav">→</span></a></div></div></article>{% empty %}<div class="no-post">沒有搜索到你想要的結果!</div>{% endfor %}{% if page.has_previous or page.has_next %}<div class="text-center" style="margin-top: 30px">{% if page.has_previous %}<a href="?q={{ query }}&page={{ page.previous_page_number }}">{% endif %}« Previous{% if page.has_previous %}</a>{% endif %}<span style="margin: 0 10px">|</span>{% if page.has_next %}<a href="?q={{ query }}&page={{ page.next_page_number }}">{% endif %}Next»{% if page.has_next %}</a>{% endif %}</div>{% endif %}{% else %}請輸入搜索關鍵詞,例如 django{% endif %}
{% endblock main %}
這個模板基本和 blog/index.html 一樣,只是由于 haystack 對搜索結果做了分頁,傳給模板的變量是一個 page 對象,所以我們從 page 中取出這一頁對應的搜索結果,然后對其循環顯示,即 {% for result in page.object_list %}。另外要取得 Post(文章)以顯示文章的數據如標題、正文,需要從 result 的 object 屬性中獲取。query 變量的值即為用戶搜索的關鍵詞。
9. 高亮關鍵詞
注意到百度的搜索結果頁面,含有用戶搜索的關鍵詞的地方都是被標紅的,在 django haystack 中實現這個效果也非常簡單,只需要使用 {% highlight %} 模板標簽即可,其用法如下:
# 1)使用默認值
{% highlight result.summary with query %} # 2)這里我們為 {{ result.summary }} 里所有的 {{ query }} 指定了一個<div></div>標簽,并且將class設置為highlight_me_please,這樣就可以自己通過CSS為{{ query }}添加高亮效果了,怎么樣,是不是很科學呢
{% highlight result.summary with query html_tag "div" css_class "highlight_me_please" %} # 3)可以 max_length 限制最終{{ result.summary }} 被高亮處理后的長度
{% highlight result.summary with query max_length 40 %}
在博客文章搜索頁templates/search/search.html中,我們對 title 和 body 做了高亮處理:{% highlight result.object.title with query %},{% highlight result.object.body with query %}。高亮處理的原理其實就是給文本中的關鍵字包上一個 span 標簽并且為其添加 highlighted 樣式(當然你也可以修改這個默認行為,具體參見上邊給出的用法)。因此我們還要給 highlighted 類指定樣式,在 base.html 中添加即可:
文件位置:base.html
<head><title>Black & White</title>...<style>/* 搜索關鍵詞高亮 */span.highlighted {color: red;}</style>...
</head>
10. 建立索引文件
最后一步就是建立索引文件了,運行命令 :
$ docker exec -it hellodjango_blog_tutorial_local python manage.py rebuild_index
就可以建立索引文件了。一切就緒后,就可以嘗試搜索了。但是體驗下來會發現搜索的結果并不是很友好,很多關鍵詞文章中命名存在但搜索結果中卻沒有顯示,原因是 haystack 專門為英文搜索設計,如果使用其默認的搜索引擎分詞器,中文搜索的結果就不是很理想,接下來我們來將它默認的分詞器設置為中文分詞器。
11. 修改搜索引擎為中文分詞
還記得文章開頭編排 elasticsearch 的 Docker 鏡像時,我們將一個 elasticsearch 的中文分詞插件復制到了 elasticsearch 的插件目錄,接下來要做的,就是讓 haystack 在創建索引時,使用指定的插件來對進行分詞并創建索引,具體做法是,首先在 blog 應用下創建一個 elasticsearch2_ik_backend.py,代碼如下:
from haystack.backends.elasticsearch2_backend import Elasticsearch2SearchBackend, Elasticsearch2SearchEngineDEFAULT_FIELD_MAPPING = {'type': 'string', "analyzer": "ik_max_word", "search_analyzer": "ik_smart"}class Elasticsearch2IkSearchBackend(Elasticsearch2SearchBackend):def __init__(self, *args, **kwargs):self.DEFAULT_SETTINGS['settings']['analysis']['analyzer']['ik_analyzer'] = {"type": "custom","tokenizer": "ik_max_word",}super(Elasticsearch2IkSearchBackend, self).__init__(*args, **kwargs)class Elasticsearch2IkSearchEngine(Elasticsearch2SearchEngine):backend = Elasticsearch2IkSearchBackend
這些代碼的作用是,繼承 haystack 默認的 Elasticsearch2SearchBackend 和 Elasticsearch2SearchEngine,覆蓋掉它的一些默認行為,這里主要就是讓 haystack 在創建索引時,使用指定的 ik 分詞器。
由于自定義了搜索引擎,因此在配置文件中將原來指定的 Elasticsearch2SearchEngine 替換為自定義的 Engine:
# 搜索設置
HAYSTACK_CONNECTIONS = {'default': {'ENGINE': 'blog.elasticsearch2_ik_backend.Elasticsearch2IkSearchEngine','URL': '','INDEX_NAME': 'hellodjango_blog_tutorial',},
}
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
由于修改了索引創建方式,因此需要重建一下索引:python manage.py rebuild_index。然后就可以查看搜索結果了,中文搜索體驗是不是好了很多?
12. 防止標題被截斷
haystack 在展示搜索結果時,默認行為是將第一個出現的關鍵詞前的內容截斷,被截掉的部分用省略號代替。對于正文來說,因為內容較多,截斷是合理的,但是對于標題這種較短的內容來說,截斷就沒有必要了。同樣的,我們通過繼承的方式,替換掉 haystack 的默認行為。我們在 blog/utils.py 中繼承 HaystackHighlighter 這個用于高亮搜索關鍵詞的輔助類。
from django.utils.html import strip_tags
from haystack.utils import Highlighter as HaystackHighlighterclass Highlighter(HaystackHighlighter):"""自定義關鍵詞高亮器,不截斷過短的文本(例如文章標題)"""def highlight(self, text_block):self.text_block = strip_tags(text_block)highlight_locations = self.find_highlightable_words()start_offset, end_offset = self.find_window(highlight_locations)if len(text_block) < self.max_length:start_offset = 0return self.render_html(highlight_locations, start_offset, end_offset)
關鍵代碼是:if len(text_block) < self.max_length:,start_offset 是 haystack 根據關鍵詞算出來第一個關鍵詞在文本中出現的位置。max_length 指定了展示結果的最大長度。我們在代碼中做一個判斷,如果文本內容 text_block 沒有超過允許的最大長度,就將 start_offset 設為 0,這樣就從文本的第一個字符開始展示,標題這種短文本就不會被截斷了。
然后設置,讓 haystack 在高亮文本時,使用我們自定義的輔助類:
HAYSTACK_CUSTOM_HIGHLIGHTER = 'blog.utils.Highlighter'
13. 線上發布
以上步驟都是在本地運行調試的,elasticsearch 服務也是在本地的 Docker 容器中運行,接下來在 production.yml 中加入 elasticsearch 服務,就可以發布線上了,配置內容和 local.yml 是一樣的,只是簡單修改一下服務名和容器名等命名:
文件位置:HelloDjango-blog-tutorial\production.yml
elasticsearch:build:context: .dockerfile: ./compose/production/elasticsearch/Dockerfileimage: hellodjango_blog_tutorial_elasticsearchcontainer_name: hellodjango_blog_tutorial_elasticsearchvolumes:- esdata:/usr/share/elasticsearch/dataports:- "9200:9200"environment:- "ES_JAVA_OPTS=-Xms512m -Xmx512m"ulimits:memlock:soft: -1hard: -1nproc: 65536nofile:soft: 65536hard: 65536
別忘了修改 settings/production.py,修改線上環境 elasticsearch 服務的連接地址:
HAYSTACK_CONNECTIONS['default']['URL'] = 'http://hellodjango_blog_tutorial_elasticsearch:9200/'
這樣就可以直接發布線上了!