Django Haystack 全文检索与关键词高亮

30563 177 2017年6月16日

在此之前我们使用了 Django 内置的一些方法实现了一个简单的搜索功能。但这个搜索功能实在过于简单,没有多大的实用性。对于一个搜索引擎来说,至少应该能够根据用户的搜索关键词对搜索结果进行排序以及高亮关键字。现在我们就来使用 django-haystack 实现这些特性。

Django Haystack 简介

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

安装必要依赖

要使用 django haystack,首先必须安装它,并且安装一些必要的依赖,具体需要安装的依赖有:

  • Whoosh。Whoosh 是一个由纯 Python 实现的全文搜索引擎,没有二进制文件等,比较小巧,配置简单方便。
  • jieba 中文分词。由于 Whoosh 自带的是英文分词,对中文的分词支持不是太好,所以使用 jieba 替换Whoosh 的分词组件。

直接使用 pip 安装这些包即可(安装到你使用的虚拟环境下):pip install whoosh django-haystack jieba

配置 Haystack

安装好 django haystack 后需要在项目的 settings.py 做一些简单的配置。

首先是把 django haystack 加入到 INSTALLED_APPS 选项里:

blogproject/settings.py

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

然后加入如下配置项:

blogproject/settings.py

HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'blog.whoosh_cn_backend.WhooshEngine',
        'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
    },
}
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

HAYSTACK_CONNECTIONSENGINE 指定了 django haystack 使用的搜索引擎,这里我们使用了 blog.whoosh_cn_backend.WhooshEngine,虽然目前这个引擎还不存在,但我们接下来会创建它。PATH 指定了索引文件需要存放的位置,我们设置为项目根目录 BASE_DIR 下的 whoosh_index 文件夹(在建立索引是会自动创建)。

HAYSTACK_SEARCH_RESULTS_PER_PAGE 指定如何对搜索结果分页,这里设置为每 10 项结果为一页。

HAYSTACK_SIGNAL_PROCESSOR 指定什么时候更新索引,这里我们使用 haystack.signals.RealtimeSignalProcessor,作用是每当有文章更新时就更新索引。由于博客文章更新不会太频繁,因此实时更新没有问题。

处理数据

接下来就要告诉 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 = [
    # 其它...
    url(r'^search/', include('haystack.urls')),
]

另外在此之前我们也为自己写的搜索视图配置了 URL,把那个 URL 删掉,以免冲突:

blog/urls.py

# url(r'^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>
                {% if page.has_previous %}
                    <a href="?q={{ query }}&amp;page={{ page.previous_page_number }}">{% endif %}&laquo; Previous
                {% if page.has_previous %}</a>{% endif %}
                |
                {% 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>

修改搜索引擎为中文分词

我们使用 Whoosh 作为搜索引擎,但在 django haystack 中为 Whoosh 指定的分词器是英文分词器,可能会使得搜索结果不理想,我们把这个分词器替换成 jieba 中文分词器。从你安装的 haystack 中把 haystack/backends/whoosh_backends.py 文件拷贝到 blog/ 下,重命名为 whoosh_cn_backends.py(之前我们在 settings.py 中 的 HAYSTACK_CONNECTIONS 指定的就是这个文件),然后找到如下一行代码:

schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=StemmingAnalyzer(), field_boost=field_class.boost, sortable=True)

将其中的 analyzer 改为 ChineseAnalyzer,当然为了使用它,你需要在文件顶部引入:from jieba.analyse import ChineseAnalyzer。

from jieba.analyse import ChineseAnalyzer

...
#注意先找到这个再修改,而不是直接添加  
schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=ChineseAnalyzer(),field_boost=field_class.boost, sortable=True)  

建立索引文件

最后一步就是建立索引文件了,运行命令 python manage.py rebuild_index 就可以建立索引文件了。

来看看搜搜效果吧,效果还不错。

django haystack搜索结果

总结

本章节的代码位于:Step26: full text search using django haystack

如果遇到问题,请通过下面的方式寻求帮助。

后记

结束了么?是的,Django 博客开发教程系列在这里全部结束了,感谢大家的阅读和反馈!

But...这只是个开始,技术开发学无止境,Django 还有很多更加高级的特性等我们去学习和探索。所以,请关注我的 个人博客 和加入 Pythonzhcn 社区,让我们在 Python 学习的路上共同进步。

资源整理与汇总

-- EOF --

最后更新:2018-11-11 12:21:12

177 条评论 / 91 人参与
LeeHeng7

博主,我需要做一个支持多个模型搜索,通过一个视图序列化输出想要的字段,并且支持中文分词和英文分词的比如,abc输入a、ab、abc都可以查找的到的功能,一直找不到解决办法,能给一些指导吗?


DocterWhom

另外作为1名小白,想问下,博客简历起来以后,这过程中创建的文章等数据,如何进行管理和备份。

例如我在博客上创建了10篇文章,这时候我在本地维护了代码和新增了模型,但是上传时,本地的数据会不会对云服务器上的数据造成影响呢。


追梦人物 [博主] DocterWhom

不会,源码更新不会影响数据库,git管理的是源码,数据没有纳入版本管理。


DocterWhom 追梦人物 [博主]

只是我的版本库。https://github.com/Linpean/blogproject

因为我不想覆盖云服务器上的代码,使用如下GIT命令

(   git fetch --all && git reset --hardorigin/master && git pull 会自动逐条执行)

执行之后会有2个意想不到的结果

1.版本的代码确实拉取到了,但是会导致云服务器上的文章数据丢失,我怀疑是我数据库文件没有删除干净,可否请博主查看下(因为文章提到不要上传数据文件到版本库,我手动对比了博主的版本库,对我自己的版本库删除了所有pycache文件夹,但是我不确定是否全部删除了)

2.会导致Django访问在云服务器上的工程目录权限丢失,会报错打算写入readonly文件,不然访问文章或者登陆后台都会网页500错误,有可能是阿里云自己对权限设置的问题,所以我每次在版本库更新后后,都不得不使用如下命令来授权。

进入到数据库文件(db.sqlite3)和数据库文件所在文件夹提升权限

chmod 777 db.sqlite3 cd .. chmod 777  *


追梦人物 [博主] DocterWhom

你把 .db 数据库放到了 git,应该 ignore 掉。


DocterWhom

非常感谢博主的教程,作为一名小白,捣鼓完了,就等通管局备案了。

http://47.106.219.55/

踩了不少坑,VIM编辑器和GIT的使用都是从头学。

最后有个问题,之后维护,可以直接在云服务器上移交GIT版本库么。


追梦人物 [博主] DocterWhom

嗯 以后的维护就是本地更新源码,推送到远程仓库,云服务器拉取源码。


SamK6517433923

完结!!


SamK6517433923 SamK6517433923

测试环境 :

django 2.1.5

Markdown 3.0.1

PyMySQL 0.9.3

Whoosh 2.7.4

django-haystack 2.8.1

jieba 0.39

wheel 0.32.3


socker111

那请问如何对这个项目进行安全方面的处理呢?  看了一下这个项目,做了csrf跨站请求的防护,还能不能加入其它的安全防护呢


追梦人物 [博主] socker111

django自带了 csrf、sql注入、点击劫持等一系列安全防护。


socker111

请问楼主,这个项目能不能对文章进行加密处理呢?


追梦人物 [博主] socker111

不能的


真实的-smile

完结撒花


startlearner

ImportError: No module named 'haystack.singals'

有没有小伙伴遇到这个错误的?

自己搜了下没有将这个问题的啊?

版本从1.10直接升级到了目前的最新版Django2.1版本后按照底下评论的朋友把Django换成1.10.6,并把

django-haystack换成了2.6.0,怎么还会带入错误啊?


追梦人物 [博主] startlearner

去 haystack 下看看 signals.py 是否存在?极可能是版本问题。


startlearner 追梦人物 [博主]

我看了下haystack是有signals这个文件的,然后我尝试重装了下django-haystack==2.6.0版本的,发现Requirement already satisfied: Django<1.11,按照道理我django安装的1.10.6符合要求应该兼容的啊


wengmei

请问我的HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'为什么删除数据,索引没有删除,但是更新和修改是可以同步的


个人开发者

#修改搜索引擎为中文分词

从你安装的 haystack 中把 haystack/backends/whoosh_backends.py 文件拷贝到 blog/ 下,重命名为 whoosh_cn_backends.py

这里是个坑,博主文件命名是whoosh_cn_backends.py,但在:

blogproject/settings.py 

HAYSTACK_CONNECTIONS = { 'default': { 'ENGINE': 'blog.whoosh_cn_backend.WhooshEngine', 'PATH': os.path.join(BASE_DIR, 'whoosh_index'), },}

中引用的是没有whoosh_cn_backend,名字引用不对,导致掉坑到晚上1点多都没解决,但在博主的源代码中是用的whoosh_cn_backend命名,可能是写博客的时候写错了,希望博主改一下。


whutlhc 个人开发者

其实没有关系,是写的有点问题,但是你直接去安装的底下找了拷贝的haystack/backends/whoosh_backend.py 后面是没有s的,所以直接拷贝过去也是对的呀。


肉肉的小斌斌 个人开发者

我也是同样的错误,,刚刚找到错误一看评论居然还有志同道合者呀