Django Haystack 全文檢索與關鍵詞高亮

做者:HelloGitHub-追夢人物css

文中所涉及的示例代碼,已同步更新到 HelloGitHub-Team 倉庫html

博客提供 RSS 訂閱應該是標配,這樣讀者就能夠經過一些聚合閱讀工具訂閱你的博客,時時查看是否有文章更新,而沒必要每次都跳轉到博客上來查看。如今咱們就來爲博客添加 RSS 訂閱功能。python

在此以前咱們使用了 Django 內置的一些方法實現了一個簡單的搜索功能。但這個搜索功能實在過於簡單,沒有多大的實用性。對於一個搜索引擎來講,至少應該可以根據用戶的搜索關鍵詞對搜索結果進行排序以及高亮關鍵字。如今咱們就來使用 django-haystack 實現這些特性。ios

Django Haystack 簡介

django-haystack 是一個專門提供搜索功能的 django 第三方應用,它支持 Solr、Elasticsearch、Whoosh、Xapian 等多種搜索引擎,上一版本的教程中咱們使用 Whoosh 加 jieba 中文分詞的方案,緣由是爲了簡單,無需安裝外部服務。但如今有了 docker,安裝一個外部服務就是垂手可得的事情,因此此次咱們採用更爲強大的 elasticsearch 做爲咱們博客的搜索引擎,同時使用 elasticsearch 的中文分詞插件 ik,來提高中文搜索的效果。nginx

安裝必要依賴

安裝 django-haystack

django-haystack 安裝很是簡單,只須要執行 pipenv install django-haystack 便可。須要注意的是,目前 elasticsearch 有 2 系列和 5 系列兩大版本,原本新項目的原則是儘量採用新版本,但目前 django-haystack 在 pypi 上發佈的穩定版只支持 elasticsearch2,master 分支下支持 elasticsearch5,所以處於穩定性考慮,咱們暫時使用 elasticsearch2,後續若是 django-haystack 發佈了支持 elasticsearch5 的pypi版本,咱們會升級到 elasticsearch5,有了 docker,升級就是垂手可得的事情。git

因爲使用 elasticsearch 服務,haystack 鏈接 elasticsearch 須要 python 版本的 SDK 支持,所以還須要安裝 elasticsearch python SDK,這裏咱們不要直接使用 pipenv 安裝,而是手動編輯 Pipfile 文件,指定 SDK 的版本,不然 pipenv 默認會安裝最新版。打開 Pipfile 文件,將依賴手動添加到 packages 板塊下:github

[packages]
django = "~=2.2"
elasticsearch = ">=2,<3"
複製代碼

安裝 elasticsearch 2

接下來就是構建一個新的容器來運行 elasticsearch 服務,所以首先須要來編排容器鏡像,回顧一下容器鏡像的目錄結構:docker

compose\
	local\
	production\
		django\
		nginx\
複製代碼

因爲 elasticsearch 在線上環境和本地測試都要使用,咱們把鏡像編排在 production 目錄下,新建一個 elasticsearch 目錄,用來存放和 elasticsearch 相關的內容。Dockfile 內容以下:django

FROM elasticsearch:2.4.6-alpine

COPY ./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.zip 
USER root
COPY ./compose/production/elasticsearch/elasticsearch.yml /usr/share/elasticsearch/config/ RUN chown elasticsearch:elasticsearch /usr/share/elasticsearch/config/elasticsearch.yml 
USER elasticsearch
複製代碼

這個鏡像從 elasticsearch 的官方基礎鏡像 2.4.6 版本進行構建,接着咱們把 ik 分詞插件複製到 elasticsearch 安裝插件的目錄下,而後解壓啓用。bootstrap

接着咱們又把 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_local

 elasticsearch_local:
 build:
 context: .
 dockerfile: ./compose/production/elasticsearch/Dockerfile
 image: elasticsearch_local
 container_name: elasticsearch_local
 volumes:
 - esdata_local:/usr/share/elasticsearch/data
 ports:
 - "9200:9200"
 environment:
 - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
 ulimits:
 memlock:
 soft: -1
 hard: -1
 nproc: 65536
 nofile:
 soft: 65536
 hard: 65536
複製代碼

主要是加入了 elasticsearch 服務,其中 environmentulimits 的參數與 elasticksearch 服務調優有關,對於簡單的博客搜索來講,調優的意義不是很大,所以這裏不作過多介紹,感興趣的能夠參考 elasticksearch 的官方文檔

配置 Haystack

安裝好 django haystack 後須要在項目的 settings.py 作一些簡單的配置。

首先是把 django haystack 加入到 INSTALLED_APPS 設置裏:

blogproject/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
	# 其它 app...
    'haystack',
    'blog',
    'comments',
]
複製代碼

而後加入以下配置項:

blogproject/common.py

# 搜索設置
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_CONNECTIONSENGINE 指定了 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 設置文件指定之:

HAYSTACK_CONNECTIONS['default']['URL'] = 'http://elasticsearch_local:9200/'
複製代碼

處理數據

接下來就要告訴 django haystack 使用哪些數據創建索引以及如何存放索引。若是要對 blog 應用下的數據進行全文檢索,作法是在 blog 應用下創建一個 search_indexes.py 文件,寫上以下代碼:

blog/search_indexes.py

from haystack import indexes
from .models import Post


class PostIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)

    def get_model(self):
        return Post

    def index_queryset(self, using=None):
        return self.get_model().objects.all()
複製代碼

這是 django haystack 的規定。要相對某個 app 下的數據進行全文檢索,就要在該 app 下建立一個 search_indexes.py 文件,而後建立一個 XXIndex 類(XX 爲含有被檢索數據的模型,如這裏的 Post),而且繼承 SearchIndexIndexable

爲何要建立索引?索引就像是一本書的目錄,能夠爲讀者提供更快速的導航與查找。在這裏也是一樣的道理,當數據量很是大的時候,若要從這些數據裏找出全部的知足搜索條件的幾乎是不太可能的,將會給服務器帶來極大的負擔。因此咱們須要爲指定的數據添加一個索引(目錄),在這裏是爲 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 這兩個字段創建索引,當檢索的時候會對這兩個字段作全文檢索匹配,而後將匹配的結果排序後做爲搜索結果返回。

配置 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'),
複製代碼

修改搜索表單

修改一下搜索表單,讓它提交數據到 django haystack 搜索視圖對應的 URL:

<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' %}

建立搜索結果頁面

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"><a href="{{ 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">繼續閱讀 <span class="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 }}&amp;page={{ page.previous_page_number }}">{% endif %}&laquo; Previous
        {% if page.has_previous %}</a>{% endif %}
        <span style="margin: 0 10px">|</span>
        {% if page.has_next %}<a href="?q={{ query }}&amp;page={{ page.next_page_number }}">{% endif %}Next
        &raquo;{% 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 變量的值即爲用戶搜索的關鍵詞。

高亮關鍵詞

注意到百度的搜索結果頁面,含有用戶搜索的關鍵詞的地方都是被標紅的,在 django haystack 中實現這個效果也很是簡單,只須要使用 {% highlight %} 模板標籤便可,其用法以下:

# 使用默認值  
{% highlight result.summary with query %}  
  
# 這裏咱們爲 {{ result.summary }} 裏全部的 {{ query }} 指定了一個<div></div>標籤,而且將class設置爲highlight_me_please,這樣就能夠本身經過CSS爲{{ query }}添加高亮效果了,怎麼樣,是否是很科學呢  
{% highlight result.summary with query html_tag "div" css_class "highlight_me_please" %}  
  
# 能夠 max_length 限制最終{{ result.summary }} 被高亮處理後的長度
{% highlight result.summary with query max_length 40 %}  
複製代碼

在博客文章搜索頁中咱們對 title 和 body 作了高亮處理:{% highlight result.object.title with query %},{% highlight result.object.body with query %}。高亮處理的原理其實就是給文本中的關鍵字包上一個 span 標籤而且爲其添加 highlighted 樣式(固然你也能夠修改這個默認行爲,具體參見上邊給出的用法)。所以咱們還要給 highlighted 類指定樣式,在 base.html 中添加便可:

base.html

<head>
    <title>Black &amp; White</title>
  	...
    <style> /* 搜索關鍵詞高亮 */ span.highlighted { color: red; } </style>
    ...
</head>
複製代碼

創建索引文件

最後一步就是創建索引文件了,運行命令 :

$ docker exec -it hellodjango_blog_tutorial_local python manage.py rebuild_index
複製代碼

就能夠創建索引文件了。一切就緒後,就能夠嘗試搜索了。可是體驗下來會發現搜索的結果並非很友好,不少關鍵詞文章中命名存在但搜索結果中卻沒有顯示,緣由是 haystack 專門爲英文搜索設計,若是使用其默認的搜索引擎分詞器,中文搜索的結果就不是很理想,接下來咱們來將它默認的分詞器設置爲中文分詞器。

修改搜索引擎爲中文分詞

還記得文章開頭編排 elasticsearch 的 Docker 鏡像時,咱們將一個 elasticsearch 的中文分詞插件複製到了 elasticsearch 的插件目錄,接下來要作的,就是讓 haystack 在建立索引時,使用指定的插件來對進行分詞並建立索引,具體作法是,首先在 blog 應用下建立一個 elasticsearch2_ik_backend.py,代碼以下:

from haystack.backends.elasticsearch2_backend import Elasticsearch2SearchBackend, Elasticsearch2SearchEngine

DEFAULT_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。而後就能夠查看搜索結果了,中文搜索體驗是否是好了不少?

防止標題被截斷

haystack 在展現搜索結果時,默認行爲是將第一個出現的關鍵詞前的內容截斷,被截掉的部分用省略號代替。對於正文來講,由於內容較多,截斷是合理的,可是對於標題這種較短的內容來講,截斷就沒有必要了。一樣的,咱們經過繼承的方式,替換掉 haystack 的默認行爲。咱們在 blog/utils.py 中繼承 HaystackHighlighter 這個用於高亮搜索關鍵詞的輔助類。

from django.utils.html import strip_tags
from haystack.utils import Highlighter as HaystackHighlighter


class 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 = 0
        return 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'
複製代碼

在來看一下搜索效果吧!

線上發佈

以上步驟都是在本地運行調試的,elasticsearch 服務也是在本地的 Docker 容器中運行,接下來在 production.yml 中加入 elasticsearch 服務,就能夠發佈線上了,配置內容和 local.yml 是同樣的,只是簡單修改一下服務名和容器名等命名:

 elasticsearch:
 build:
 context: .
 dockerfile: ./compose/production/elasticsearch/Dockerfile
 image: hellodjango_blog_tutorial_elasticsearch
 container_name: hellodjango_blog_tutorial_elasticsearch
 volumes:
 - esdata:/usr/share/elasticsearch/data
 ports:
 - "9200:9200"
 environment:
 - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
 ulimits:
 memlock:
 soft: -1
 hard: -1
 nproc: 65536
 nofile:
 soft: 65536
 hard: 65536
複製代碼

別忘了修改 settings/production.py,修改線上環境 elasticsearch 服務的鏈接地址:

HAYSTACK_CONNECTIONS['default']['URL'] = 'http://hellodjango_blog_tutorial_elasticsearch:9200/'
複製代碼

這樣就能夠直接發佈線上了!


關注公衆號加入交流羣

相關文章
相關標籤/搜索