登录        注册
本站基于Django开发,源码 Github 欢迎 Fork、Star。由于站点升级导致评论区留言信息丢失,欢迎前来发表新的评论

Django个人博客开发十六 | Haystack 全文搜索

Django stormsha 6104浏览 123喜欢 0评论
本渣渣不专注技术,只专注使用技术,不是一个资深的coder,是一个不折不扣的copier

1、前言

简单的博客搜索、查询功能查找到符合关键字的对象就行了。不过为了提升逼格,至少应该能够根据用户的搜索关键词对搜索结果进行排序以及高亮关键字。django-haystack 全文搜索包可以带你轻松装逼

django-haystack 是一个专门提供搜索功能的 Django 第三方应用,它支持 Solr、Elasticsearch、Whoosh、Xapian 等多种搜索引擎,配合著名的中文自然语言处理库 jieba 分词,就可以为我们的博客提供一个效果不错的博客文章搜索功能

2、安装依赖包

启动虚拟环境 stormsha

$ workon stormsha
$ pip install whoosh
$ pip install django-haystack
$ pip install jieba

Whoosh:是一个由纯 Python 实现的全文搜索引擎,没有二进制文件等,比较小巧,配置简单方便

jieba: 由于 Whoosh 自带的是英文分词,对中文的分词支持不是太好,所以使用 jieba 替换Whoosh 的分词组件

3、Whoosh搜索引擎添加结巴分词

我们使用 Whoosh 作为搜索引擎,但在 Django Haystack 中为 Whoosh 指定的分词器是英文分词器,搜索结果可能不理想,我们把这个分词器替换成 jieba 中文分词器。

进入 stormsha/Lib/site-packages/haystack/backends 拷贝 whoosh_backend.py  

blog -> storm 修改文件名为  whoosh_cn_backend.py

【提示】——stormsha是本项目使用的虚拟环境文件夹

blog -> storm -> 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()

4、配置 Haystack

blog -> blog -> settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    # 其它 应用...
    'haystack',
]


HAYSTACK_CONNECTIONS = {
    'default': {
        # 选择语言解析器为自己更换的结巴分词
        'ENGINE': 'blog.whoosh_cn_backend.WhooshEngine',
        # 保存索引文件的地址,选择主目录下,这个会自动生成
        'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
    },
}


# 统一分页设置
BASE_PAGE_BY = 10
BASE_ORPHANS = 5


HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

ENGINE: 指定 django haystack 使用的搜索引擎,这里我们使用 blog.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 

5、创建检索模型

blog -> storm 创建 search_indexes.py

blog -> storm -> search_indexes.py

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

from haystack import indexes
from .models import Article


class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
    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),并且继承 SearchIndex 和 Indexable

为什么要创建索引?索引就是一本书的目录,可以为读者提供更快速的导航与查找。在这里也是同样的道理,当数据量非常大的时候,若要从这些数据里找出所有的满足搜索条件的几乎是不太可能的,将会给服务器带来极大的负担。所以我们需要为指定的数据添加一个索引(目录),在这里是为 Article 创建一个索引,索引的实现细节是我们不需要关心的,我们只关心为哪些字段创建索引,如何指定

每个索引里面必须有且只能有一个字段为 document=True,这代表 django haystack 和搜索引擎将使用此字段的内容作为索引进行检索(primary field)。

【注意】——如果使用一个字段设置了 document=True,则一般约定此字段名为 text,这是在 SearchIndex 类里面一贯的命名,以防止后台混乱,不建议改

haystack 提供了 use_template=True 在 text 字段中,这样就允许我们使用数据模板去建立搜索引擎索引的文件,就是索引里面需要存放一些什么东西,例如 Article 的 title 字段,这样我们可以通过 title 内容来检索 Article 数据。举个例子,假如你搜索 Python ,那么就可以检索出 title 中含有 Python 的 Article

6、配置路由

blog -> storm -> urls.py

from .views import MySearchView

# 全文搜索
url(r'^search/$', MySearchView.as_view(), name='search_view'),

7、编写视图

MySearchView:重写搜索视图,可以增加一些额外的参数,且可以重新定义名称

blog -> storm -> 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')

8、编写自定义模板标签

blog -> storm -> 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

9、制作搜索结果页面

blog -> templates 创建 search 文件

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

配置全文搜索字段

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

    # 文章标题
    {{ object.title }}
    # 文章内容
    {{ object.body_to_markdown }}

这个数据模板的作用是对 Article.title、Article.body_to_markdown 这两个字段建立索引,当检索的时候会对这两个字段做全文检索匹配,然后将匹配的结果排序后作为搜索结果返回

在模板中使用循环来遍历 search_list 变量,变量的类型: SearchResult

SearchResult 参数:

    app_label - The application the model is attached to.
    model_name - The models 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 models class name for display.
    verbose_name_plural - A prettier version of the models 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="StormSha,{{ 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 %}  

会存在标题不能全部显示的问题,此问题主要是因为

stormsha->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_offset 与 end_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 高亮显示源码

stormsha->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_offset 与 end_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:高亮关键词颜色

10、效果展示

20391

【友情提示】——如果发现有表达错误,或者知识点错误,或者搞不懂的地方,请及时留言,可以在评论区互相帮助,让后来者少走弯路是我的初衷。我也是一步步摸着石头走过来的,深知网络上只言片语的图文教程,给初学者带来的深深困扰。

【建议】——在对项目结构不太熟悉时,参照完整源码少走弯路

转载请注明: StormSha » Django个人博客开发十六 | Haystack 全文搜索

发表我的评论

表情
(0)个小伙伴在吐槽|0条评论
暂时没有评论,欢迎来尬聊!