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

文章目录

  • 一、前言
  • 二、安装依赖包
    • Whoosh
    • jieba
  • 三、Whoosh搜索引擎添加结巴分词
  • 四、配置 Haystack
    • haystack设置参数
      • ENGINE
      • PATH
      • BASE_PAGE_BY
      • HAYSTACK_SIGNAL_PROCESSOR
    • 重建索引:
      • 注意
      • 建立成功
  • 五、创建检索模型
    • 注意
    • 为什么要创建索引?
      • 注意
  • 六、编写视图
  • 七、配置路由
  • 八、编写自定义模板标签
  • 九、制作搜索结果页面
    • SearchResult 参数
    • 修改搜索表单
    • 搜索结果展示页面
    • 关键词高亮
      • 注意
    • 文章摘要关键词高亮
  • 十、效果展示
  • 教程目录

一、前言

简单的博客搜索、查询功能查找到符合关键字的对象就行了。不过为了提升逼格,至少应该能够根据用户的搜索关键词对搜索结果进行排序以及高亮关键字。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: '{}'.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>
            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>   分类:{{ 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 }}&page={{ page_obj.previous_page_number }}">上一页a>
                    {% else %}
                    <span class="text-secondary" title="当前页已经是首页">上一页span>
                    {% endif %}
                    <span class="mx-2"> {{ page_obj.number }} / {{ paginator.num_pages }} span>
                    {% if page_obj.has_next %}
                    <a class="text-success" href="?q={{ query }}&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 个人博客搭建十七|Haystack 全文搜索_第1张图片

教程目录

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 个人博客搭建十六|网站地图

你可能感兴趣的:(#)