文章分类

热门文章

公众号:iMyShare 关注微信公众号

产品汪,爱好设计,业余Coding,想学运营,定期分享实用互联网小技巧!

Django使用haystack,Whoosh,Jieba进行高效的中文全文检索(全文搜索),并支持自定义搜索

MeetUp
2020-06-13
355
0

前言

在使用Django搭建博客的过程中,必不可少的需要博文搜索功能。读者输入关键此,然后根据关键词搜索出对应的博客内容。新手可能上来就直接使用数据库模糊查询了,然而事实上数据库的模糊查询效率低,博客文章多的时候就能很明显的感受到。而且模糊查询不支持将用户输入的内容进行分词检索,不支持关键词高亮等等。反正就是各种不好吧,哈哈哈🤦‍。所以就要用到搜索引擎,下面就介绍使用haystack,Whoosh,Jieba打造高效,可分词,并且支持中文分词,可自定义搜索,可多条件搜索,可关键词高亮的全文搜索功能。

Django Haystack Whoosh Jieba简介

django-haystack:是一个专门提供搜索功能的djang 第三方库,支持Solr、Elasticsearch、Whoosh、Xapian等多种搜索引擎。

whoosh:是一个纯Python编写的全文搜索引擎。虽然性能比不上sphinx、xapian、Elasticsearc等,但是无二进制包,程序不会莫名其妙的崩溃。而且比较小巧,安装后仅2.61M。同时配置简单方便,非常容易集成到django/python里面。对于小型的站点,whoosh已经足够使用。

jieba:是一款免费的中文分词包,用于替换Whoosh自带的英文分词(英文分词对中文支持不太友好)。

必备环境安装

在虚拟环境中依次安装:django-haystack,Whoosh,Jieba

pip install django-haystack
pip install whoosh
pip install jieba

安装如果报错:python setup.py egg_info Check,先安装setuptools_scm,再安装django-haystack

pip install setuptools_scm

配置Haystack

这里以博客中的blog应用配置Haystack为例,演示完整的配置过程。首先在settings.py中添加haystack应用。

INSTALLED_APPS = (
    ...
    'blog',
    'haystack',
)

对应的示例blog的models如下:

from __future__ import unicode_literals
from django.db import models
from django.contrib.auth.models import User


class Note(models.Model):
    user = models.ForeignKey(User)
    pub_date = models.DateTimeField()
    title = models.CharField(max_length=200)
    body = models.TextField()

    def __str__(self):
        return self.title

在blog应用目录下,添加一个索引文件blog/search_indexes.py

from haystack import indexes
from blog.models import Note

# 修改此处,类名为模型类的名称+Index,比如模型类为Note,则这里类名为NoteIndex
class NoteIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)

    def get_model(self):
		# 修改此处,为你自己的model
        return Note

    def index_queryset(self, using=None):
        """Used when the entire index for model is updated."""
        return self.get_model().objects.all()

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

并且django haystack规定。要相对某个app下的数据进行全文检索,就要在该app下创建一个search_indexes.py文件。然后创建一个XXIndex 类(XX 为含有被检索数据的模型,如这里的 Note),并且继承 SearchIndex 和 Indexable。

然后每个索引里面必须有且只能有一个字段为document=True,这代表django haystack和搜索引擎将使用此字段的内容作为索引进行检索(primary field)。注意,如果使用一个字段设置了document=True,则一般约定此字段名为text,这是在 SearchIndex 类里面一贯的命名,以防止后台混乱,当然名字你也可以随便改,不过不建议改。

并且,haystack 提供了use_template=True 在 text 字段中,这样就允许我们使用数据模板去建立搜索引擎索引的文件,说得通俗点就是索引里面需要存放一些什么东西,例如Note的title字段,这样我们可以通过title内容来检索Note数据了。这里以blog为例,数据模板的路径为 templates/search/indexes/blog/note_text.txt,并在其中添加要搜索的字段:

{{ object.title }}
{{ object.body }}

接下来就是配置URL,搜索的视图函数和URL模式django haystack都已经帮我们写好了,只需要项目的urls.py中包含它:

urlpatterns = [
    url(r'^search/', include('haystack.urls')),
]

haystack_search 视图函数会将搜索结果传递给模板 search/search.html,因此创建这个模板文件,对搜索结果进行渲染:

<!DOCTYPE html>
<html>
<head>
    <title></title>
</head>
<body>
这是搜索结果:

{% if query %}

    <h3>搜索结果如下:</h3>

    {% for result in page.object_list %}

        <a href="{{ result.object.get_absolute_url }}">{{ result.object.title }}</a><br/>
    {% empty %}
        <p>啥也没找到</p>
    {% 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; 上一页{% if page.has_previous %}</a>{% endif %}
        |
            {% if page.has_next %}<a href="?q={{ query }}&amp;page={{ page.next_page_number }}">{% endif %}下一页 &raquo;{% if page.has_next %}</a>{% endif %}
        </div>
    {% endif %}
{% endif %}
<a href="/">重新搜索<a>
</body>
</html>

其中query:搜索的字符串,page:当前页的page对象,paginator:分页paginator对象。haystack自带分页,每页显示数量在settings.py可以设置:

# 当添加、修改、删除数据时,自动生成索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# 指定搜索结果每页显示多少条信息
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 13

注意:

  • 一定要配置HAYSTACK_SIGNAL_PROCESSOR这一项,否则我们新增/修改/删除文章时,不会自动生成索引,索引不更新,我们就搜不到对应的内容。
  • 在模板中使用时,需要使用xxx.object.xxx的方式来获取数据,和Django原生的可能有一点区别。

 

同步数据库,并第一次手动生成索引后,就可以正常搜索使用了。

python manager.py makemigrations
python migrate
# 手动生成索引
python manage.py rebuild_index

注意:如果django3.x 使用haystack 报错:ImportError: cannot import name 'six' from 'django.utils'。请按照下面步骤操作即可:

# 第一步
pip3 install six

# 第二步
cd #进入家目录
cd .local/lib/python3.6/site-packages
cp six.py django/utils #将six.py拷贝进django/utils/目录下

# 第三步
# 将site-packages/haystack/inputs.py 中
from django.utils.encoding import force_text, python_2_unicode_compatible
# 改为
from django.utils.encoding import force_text
from django.utils.six import python_2_unicode_compatible
# 问题解决

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

将 haystack/backends/whoosh_backends.py文件拷贝到blog目录下,重命名为whoosh_cn_backends.py,然后修改:whoosh_cn_backend.py

#在顶部添加
from jieba.analyse import ChineseAnalyzer
# 搜索`schema_fields[field_class.index_fieldname] = TEXT`,改成如下:
schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=ChineseAnalyzer(),field_boost=field_class.boost, sortable=True)

并在settings.py中添加如下配置:

# 全文检索框架配置
HAYSTACK_CONNECTIONS = {
    'default': {
		# 修改后的whoosh引擎,支持中文分词
        'ENGINE': 'blog.whoosh_cn_backend.WhooshEngine',
		# 索引文件路径
        'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
    }
}
# 当添加、修改、删除数据时,自动生成索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# 指定搜索结果每页显示多少条信息
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 6

然后同步数据库,重新生成索引即可。

python manager.py makemigrations
python migrate
# 手动生成索引
python manage.py rebuild_index

高亮搜索关键词

haystack为我们提供了{% highlight %} 和 {% more_like_this %}2个标签,可以让匹配的关键字显示高亮。

{% highlight <text_block> with <query> [css_class "class_name"] [html_tag "span"] [max_length 200] %}

此示例中的语法大概意思就是:我们为<text_block>中所有query的内容制定了一个span标签,标签的class设置为class_name,并限制<text_block>被高亮处理后的长度为200。

然后我们就可以通过css去控制class_name的高亮颜色等等样式了,还是很方便的。

重写搜索方法,实现自定义搜索

如果直接采用上面的配置进行搜索,可能有时候满足不了用户的复杂需求,比如:对搜索结果按照时间、按照文章分类等进行过滤。这里我看了haystack的源码后,发现搜搜其实是通过SearchQuerySet这个方法实现的。所以我们只需要自定义一个方法,对SearchQuerySet方法的搜索结果进行相应的操作(各种过滤都没问题),然后通过自定义的模板进行渲染即可。

from haystack.query import SearchQuerySet

def search(request):
	query = request.GET.get('q', '')
	# 这里可以随便过滤
	note = SearchQuerySet().filter(body=query, pub_date...)
	return render(request, 'search.html', {'query': query, 'note': note,)

 

									
交流评论