Django3.0+Python3.8+MySQL8.0 個人博客搭建十七|Haystack 全文搜索

一、前言

簡單的博客搜索、查詢功能查找到符合關鍵字的對象就行了。不過爲了提升逼格,至少應該能夠根據用戶的搜索關鍵詞對搜索結果進行排序以及高亮關鍵字。django-haystack 全文搜索包可以帶你輕鬆裝逼

django-haystack
是一個專門提供搜索功能的 Django 第三方應用,它支持 Solr、Elasticsearch、Whoosh、Xapian 等多種搜索引擎,配合著名的中文自然語言處理庫 jieba 分詞,就可以爲我們的博客提供一個效果不錯的博客文章搜索功能

二、安裝依賴包

啓動虛擬環境 fswy

$ source fswy/bin/activate
(fswy) blog xiatian$ pip3 install whoosh
(fswy) blog xiatian$ pip3 install django-haystack
(fswy) blog xiatian$ pip3 install jieba

Whoosh

是一個由純 Python 實現的全文搜索引擎,沒有二進制文件等,比較小巧,配置簡單方便

jieba

由於 Whoosh 自帶的是英文分詞,對中文的分詞支持不是太好,所以使用 jieba 替換Whoosh 的分詞組件

三、Whoosh搜索引擎添加結巴分詞

我們使用 Whoosh 作爲搜索引擎,但在 Django Haystack 中爲 Whoosh 指定的分詞器是英文分詞器,搜索結果可能不理想,我們把這個分詞器替換成 jieba 中文分詞器。

進入 fswy/Lib/site-packages/haystack/backends 拷貝 whoosh_backend.pyblog -> fswy 修改文件名爲 whoosh_cn_backend.py
【提示】——fswy是本項目使用的虛擬環境文件夾

blog -> fswy -> whoosh_cn_backend.py

#在全局引入的最後一行加入jieba分詞器
from jieba.analyse import ChineseAnalyzer


elif field_class.field_type == 'edge_ngram':
    schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', stored=field_class.stored, field_boost=field_class.boost)
else:
    # 修改
    schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)

原始

analyzer=StemmingAnalyzer()

修改後

analyzer=ChineseAnalyzer()

四、配置 Haystack

添加'django.contrib.humanize'和haystack到設置中

blog -> blog -> settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 網站地圖應用
    'django.contrib.sitemaps',
    'django.contrib.sites', # 添加評論app註冊
    'django_comments',
    'django.contrib.humanize',  # 添加人性化過濾器
    'haystack',  # 全文搜索應用 這個要放在其他應用之前
    'imagekit', # 使用imagekit
    'apps.fswy', # 添加用戶應用
    'apps.user',
    'apps.comment', #添加評論應用
]
# 統一分頁設置
BASE_PAGE_BY = 4
BASE_ORPHANS = 5

# 全文搜索應用配置
HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'fswy.whoosh_cn_backend.WhooshEngine',  # 選擇語言解析器爲自己更換的結巴分詞
        'PATH': os.path.join(BASE_DIR, 'whoosh_index'),  # 保存索引文件的地址,選擇主目錄下,這個會自動生成
    }
}
# 自動更新索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

haystack設置參數

ENGINE

指定 django haystack 使用的搜索引擎,這裏我們使用 fswy.whoosh_cn_backend.WhooshEngine,雖然目前這個引擎還不存在,但我們接下來會創建它

PATH

指定索引文件需要存放的位置,我們設置爲項目根目錄BASE_DIR 下的 whoosh_index 文件夾(在建立索引時會自動創建)

BASE_PAGE_BY

指定如何對搜索結果分頁,這裏設置爲每 10 項結果爲一頁。

HAYSTACK_SIGNAL_PROCESSOR

指定什麼時候更新索引,這裏定義爲每當有文章更新時就更新索引。由於博客文章更新不會太頻繁,因此實時更新沒有問題。

重建索引:

第一次需要受手動創建索引

$ cd ~/blog
$ python manage.py rebuild_index 

或者

Pycharm 中 Tools -> run manage.py task 下執行命令:

rebuild_index 

注意

這裏其實發生過很多因爲python和django版本而導致的錯誤,詳情可以查看 Django3.0+Python3.8+MySQL8.0 個人博客搭建十七|Haystack 全文搜索的坑

建立成功

(fswy) blog xiatian$ python3 manage.py rebuild_index 
WARNING: This will irreparably remove EVERYTHING from your search index in connection 'default'.
Your choices after this are to restore from backups or rebuild via the `rebuild_index` command.
Are you sure you wish to continue? [y/N] y
Removing all documents from your index because you said so.
All documents removed.
Indexing 2 文章
Building prefix dict from the default dictionary ...
Loading model from cache /var/folders/7d/s7t1kh7n4x59zqltw7gs26640000gn/T/jieba.cache
Loading model cost 0.745 seconds.
Prefix dict has been built successfully.

五、創建檢索模型

blog -> fswy 創建search_indexes.py

blog -> fswy -> search_indexes.py

# -*- coding: utf-8 -*-

from haystack import indexes
from .models import Article


class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
    '''
    document:指定了將模型類中的哪些字段建立索引
    use_template:在模板文件夾中創建文件夾指明具體的字段建立索引
    '''
    text = indexes.CharField(document=True, use_template=True)
    views = indexes.IntegerField(model_attr='views')

    def get_model(self):
        # 爲那個模型表建立索引
        return Article

    def index_queryset(self, using=None):
        return self.get_model().objects.all()

注意

文件名稱必須是 search_indexes.py

Django Haystack: 要想對某個 app下的數據進行全文檢索,就要在該 app 下創建一 search_indexes.py 文件,然後創建一個 XXIndex 類(XX 爲含有被檢索數據的模型,如這裏的 Article),並且繼承 SearchIndexIndexable

爲什麼要創建索引?

索引就是一本書的目錄,可以爲讀者提供更快速的導航與查找。在這裏也是同樣的道理,當數據量非常大的時候,若要從這些數據裏找出所有的滿足搜索條件的幾乎是不太可能的,將會給服務器帶來極大的負擔。所以我們需要爲指定的數據添加一個索引(目錄),在這裏是爲 Article 創建一個索引,索引的實現細節是我們不需要關心的,我們只關心爲哪些字段創建索引,如何指定。

每個索引裏面必須有且只能有一個字段爲document=True,這代表 django haystack搜索引擎將使用此字段的內容作爲索引進行檢索(primary field)。

注意

如果使用一個字段設置了document=True,則一般約定此字段名爲 text,這是在 SearchIndex類裏面一貫的命名,以防止後臺混亂,不建議改。

haystack 提供了 use_template=Truetext字段中,這樣就允許我們使用數據模板去建立搜索引擎索引的文件,就是索引裏面需要存放一些什麼東西,例如Articletitle 字段,這樣我們可以通過title內容來檢索 Article 數據。舉個例子,假如你搜索 Python ,那麼就可以檢索出 title中含有 PythonArticle

六、編寫視圖

MySearchView:重寫搜索視圖,可以增加一些額外的參數,且可以重新定義名稱

blog -> fswy -> views.py

# 重寫搜索視圖,可以增加一些額外的參數,且可以重新定義名稱
class MySearchView(SearchView):
    # 返回搜索結果集
    context_object_name = 'search_list'
    # 設置分頁
    paginate_by = getattr(settings, 'BASE_PAGE_BY', None)
    paginate_orphans = getattr(settings, 'BASE_ORPHANS', 0)
    # 搜索結果以瀏覽量排序
    queryset = SearchQuerySet().order_by('-views')

七、配置路由

blog -> fswy -> urls.py

from .views import MySearchView

# 全文搜索
    path(r'search/', MySearchView.as_view(), name='search'),

八、編寫自定義模板標籤

blog -> fswy -> templatetags -> blog_tags.py

@register.simple_tag
def my_highlight(text, q):
    """自定義標題搜索詞高亮函數,忽略大小寫"""
    if len(q) > 1:
        try:
            text = re.sub(q, lambda a: '<span class="highlighted">{}</span>'.format(a.group()),
                          text, flags=re.IGNORECASE)
            text = mark_safe(text)
        except:
            pass
    return text

九、製作搜索結果頁面

blog -> templates 創建 search文件

|-- search              
|   |-- indexes
|   |   |-- storm
|   |   |   |-- article_text.txt
|   |-- search.html   

配置全文搜索字段

blog -> templates -> search -> indexes -> fswy -> article_text.txt

# 文章標題
{{ object.title }}
# 文章內容
{{ object.body_to_markdown }}

這個數據模板的作用是對 Article.titleArticle.body_to_markdown 這兩個字段建立索引,當檢索的時候會對這兩個字段做全文檢索匹配,然後將匹配的結果排序後作爲搜索結果返回。

在模板中使用循環來遍歷 search_list 變量,變量的類型: SearchResult

SearchResult 參數

  • app_label - The application the model is attached to.
    -model_name- The model’s name.
  • pk - The primary key of the model.
  • score- The score provided by the search engine.
  • object - The actual model instance (lazy loaded).
  • model- The model class.
  • verbose_name- A prettier version of the model’s class name for display.
  • verbose_name_plural- A prettier version of the model’s plural class name for display.
  • searchindex - Returns the SearchIndex class associated with this result.
    -distance - On geo-spatial queries, this returns a Distance object representing the distance the result was from the focused point.

修改搜索表單

blog -> templates -> base.html

<!--搜索框-->
<li style="float:right;">
    <div class="toggle-search"><i class="fa fa-search"></i></div>
    <div class="search-expand" style="display: none;">
        <div class="search-expand-inner">
            <form class="nav-item navbar-form mr-2 py-md-2" role="search" method="get" id="searchform" action="{% url 'blog:search' %}">
                <div class="input-group">
                    <input type="search" name="q" class="form-control rounded-0 f-15" placeholder="搜索" required=True>
                    <div class="input-group-btn">
                        <button class="btn btn-info rounded-0" type="submit"><i class="fa fa-search"></i></button>
                    </div>
                </div><!-- /input-group -->
            </form>
        </div>
    </div>
</li>
<!--搜索框結束-->

搜索結果展示頁面

直接拷貝:blog -> templates -> content.html內容至 blog -> templates -> search -> search.html

稍加修改即可作爲搜索結果頁面

blog -> templates -> search -> search.html

{% extends 'base_right.html' %}
{% load blog_tags oauth_tags comment_tags static %}
{% load humanize %}
{% load highlight %}

{% block head_title %}文章搜索:{{ query }}{% endblock %}
{% block title %}甫式人生 | 文章搜索:{{ query }}{% endblock title %}
{% block metas %}
<meta name="description" content="文章搜索:{{ query }},網站全文搜索功能,按照文章標題和內容建立索引,實現整站搜索,django-haystack全文搜索庫的使用">
<meta name="keywords" content="{{ query }},全文搜索,django-haystack">
{% endblock %}


{% block description %}
<meta name="description" content="文章搜索:{{ query }},網站全文搜索功能,按照文章標題和內容建立索引,實現整站搜索,django-haystack全文搜索庫的使用"/>
{% endblock description %}

{% block keywords %}
<meta name="keywords" content="fswy,{{ query }},全文搜索,django-haystack"/>
{% endblock keywords %}

{% block body %}
    <div class="content-wrap">
	    <div class="content">
            <header class="archive-header">
                <h1><i class="fa fa-folder-open"></i>  &nbsp;分類:{{ query }}
                    <a title="訂閱福利專區" target="_blank" href="{% url 'blog:category' resources '' %}"><i class="rss fa fa-rss"></i></a>
                </h1>
            </header>
            {% for article in search_list %}
            <article class="excerpt">
                <header>
                    <a class="label label-important" href="{{ article.object.category.get_absolute_url }}">{{ article.object.category.name }}<i class="label-arrow"></i></a>

                    <!--高亮標題-->
                    <h2 class="mt-0 font-weight-bold text-info f-17">
                            <a href="{{ article.object.get_absolute_url }}" target="_blank">{% my_highlight article.object.title query %}</a>
                    </h2>

                </header>
                <div class="focus"><a target="_blank" href="{{ article.object.get_absolute_url }}">
                    <img class="thumb" width="200" height="123" src="{{ article.object.img_link }}" alt="{{ article.object.title }}" /></a>
                </div>
                <!--摘要處顯示部分文章內容-->
                {% with article.object.body_to_markdown|safe as this_body %}
                <p class="d-none d-sm-block mb-2 f-15">{% highlight this_body with query max_length 130 %}</p>
                <p class="d-block d-sm-none mb-2 f-15">{% highlight this_body with query max_length 64 %}</p>
                {% endwith %}
                <!--摘要處顯示部分文章內容結束-->

                <p class="auth-span">
                <span class="muted"><i class="fa fa-user"></i> <a href="/author/{{ article.object.author }}">{{ article.object.author }}</a></span>
                <span class="muted"><i class="fa fa-clock-o"></i> {{ article.object.create_date|date:'Y-m-d'}}</span>
                <span class="muted"><i class="fa fa-eye"></i> {{ article.object.views }}瀏覽</span>
                <span class="muted"><i class="fa fa-comments-o"></i>
                    <a target="_blank" href="/article/{{ article.object.slug }}#comments">{% get_comment_count article.object.id  article.object.id%}評論</a>
                </span>
                <span class="muted"><a href="javascript:;" data-action="ding" data-id="455" id="Addlike" class="action">
                    <i class="fa fa-heart-o"></i>
                <span class="count">{{ article.object.love }}</span>喜歡</a></span></p>
            </article>
            {% empty %}
                    <div class="no-post">未搜索到相關內容!</div>
            {% endfor %}
            {% if is_paginated %}
                <div class="text-center mt-2 mt-sm-1 mt-md-0 mb-3 f-16">
                    {% if page_obj.has_previous %}
                    <a class="text-success" href="?q={{ query }}&amp;page={{ page_obj.previous_page_number }}">上一頁</a>
                    {% else %}
                    <span class="text-secondary" title="當前頁已經是首頁">上一頁</span>
                    {% endif %}
                    <span class="mx-2">&nbsp;{{ page_obj.number }}&nbsp;/&nbsp;{{ paginator.num_pages }}&nbsp;</span>
                    {% if page_obj.has_next %}
                    <a class="text-success" href="?q={{ query }}&amp;page={{ page_obj.next_page_number }}">下一頁</a>
                    {% else %}
                    <span class="text-secondary" title="當前頁已經是末頁">下一頁</span>
                    {% endif %}
                </div>
            {% endif %}
        </div>
    </div>
{% endblock body %}

content.html做的主要改動就是,添加了搜索詞 query信息,返回的查詢集 search_list,標題和摘要高亮關鍵詞

query:用戶搜索的關鍵詞

search_list:即爲 MySearchView 視圖傳給模板對搜索結果集 search_list,數據類型:SearchResult

is_paginated:haystack 對搜索結果做了分頁,is_paginated判斷是否有分頁

關鍵詞高亮

文章標題關鍵詞高亮

<!--高亮標題-->
<h2 class="mt-0 font-weight-bold text-info f-17">
        <a href="{{ article.object.get_absolute_url }}" target="_blank">{% my_highlight article.object.title query %}</a>
</h2>

注意

這裏使用 自定義my_highlight標題高亮方法,如果使用

 {% highlight article.object.title with query %}  

會存在標題不能全部顯示的問題,此問題主要是因爲

fswy->Lib->site-packages->haystack->utils->highlighting.py

if start_offset > 0:
    highlighted_chunk = '...%s' % highlighted_chunk

if end_offset < len(self.text_block):
    highlighted_chunk = '%s...' % highlighted_chunk
return highlighted_chunk

start_offsetend_offset 分別代表高亮代碼的開始位置與結束位置,如果高亮部分在中間的話,前面的部分就直接顯示 …

文章摘要關鍵詞高亮

<!--摘要處顯示部分文章內容-->
{% with article.object.body_to_markdown|safe as this_body %}
<p class="d-none d-sm-block mb-2 f-15">{% highlight this_body with query max_length 130 %}</p>
<p class="d-block d-sm-none mb-2 f-15">{% highlight this_body with query max_length 64 %}</p>
{% endwith %}
<!--摘要處顯示部分文章內容結束-->

max_length
限制最終內容被高亮處理後的長度,也即摘要內容長度

【提示】——這裏是我自己添加的全文搜索功能 崔慶才 個人博客樣式需要添加一點內容

blog -> static -> css -> style.css

在文件尾部添加

.highlighted {
    color: #ea6f5a;
}

【提示】——如果存在標題不能全部顯示可以修改 haystack 高亮顯示源碼

fswy->Lib->site-packages->haystack->utils->highlighting.py

if start_offset > 0:
    highlighted_chunk = '...%s' % highlighted_chunk

if end_offset < len(self.text_block):
    highlighted_chunk = '%s...' % highlighted_chunk
return highlighted_chunk

start_offsetend_offset分別代表高亮代碼的開始位置與結束位置,如果高亮部分在中間的話,前面的部分就直接顯示…。我們可以在這之前再加一句判斷,如果字符串長度小於 max_length的值的話,我們就直接將其返回

if len(self.text_block) < self.max_length:  
    return self.text_block[:start_offset] + highlighted_chunk

if start_offset > 0:
    highlighted_chunk = '...%s' % highlighted_chunk

if end_offset < len(self.text_block):
    highlighted_chunk = '%s...' % highlighted_chunk
return highlighted_chunk

color:高亮關鍵詞顏色

十、效果展示

在這裏插入圖片描述

教程目錄

Django3.0+Python3.8+MySQL8.0 個人博客搭建一|前言
Django3.0+Python3.8+MySQL8.0 個人博客搭建二|創建虛擬環境
Django3.0+Python3.8+MySQL8.0 個人博客搭建三|創建博客項目
Django3.0+Python3.8+MySQL8.0 個人博客搭建四|創建第一個APP
Django3.0+Python3.8+MySQL8.0 個人博客搭建五|makemigrations連接MySQL數據庫的坑
Django3.0+Python3.8+MySQL8.0 個人博客搭建六|數據庫結構設計
Django3.0+Python3.8+MySQL8.0 個人博客搭建七|makemigrations創建數據庫的坑(第二彈)
Django3.0+Python3.8+MySQL8.0 個人博客搭建八|通過admin管理後臺
Django3.0+Python3.8+MySQL8.0 個人博客搭建九|博客首頁開發(一)
Django3.0+Python3.8+MySQL8.0 個人博客搭建十|整理項目結構
Django3.0+Python3.8+MySQL8.0 個人博客搭建十一|博客首頁開發(二)
Django3.0+Python3.8+MySQL8.0 個人博客搭建十二|博客首頁開發(三)
Django3.0+Python3.8+MySQL8.0 個人博客搭建十三|博客詳情頁面
Django3.0+Python3.8+MySQL8.0 個人博客搭建十四|註冊登錄
Django3.0+Python3.8+MySQL8.0 個人博客搭建十五|評論區
Django3.0+Python3.8+MySQL8.0 個人博客搭建十六|網站地圖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章