Django – 使用Haystack+Whoosh配置全文搜索

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

Django Haystack 简介

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

安装必要依赖

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

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

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

配置 Haystack

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

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

1
2
3
4
5
6
INSTALLED_APPS = [
'django.contrib.admin',
# 其它 app...
'haystack',
'app',
]

然后加入如下配置项:

1
2
3
4
5
6
7
8
9
10
11
12
# 配置全文搜索
# 指定搜索引擎
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
},
}
# 指定如何对搜索结果分页,这里设置为每 10 项结果为一页,默认是 20 项为一页
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 使用那些数据建立索引以及如何存放索引。如果要对 app应用下的数据进行全文检索,做法是在 app应用下建立一个 search_indexes.py 文件,写上如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from .models import Test
from haystack import indexes


class TestIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)

def get_model(self):
return Test

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 为含有被检索数据的模型,如这里的 Test),并且继承 SearchIndexIndexable

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

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

并且,haystack 提供了use_template=True 在 text 字段中,这样就允许我们使用数据模板去建立搜索引擎索引的文件,说得通俗点就是索引里面需要存放一些什么东西,例如 Test的 title 字段,这样我们可以通过 title 内容来检索 Test数据了。举个例子,假如你搜索 Python ,那么就可以检索出 title 中含有 Python 的Test了,怎么样是不是很简单?数据模板的路径为 templates/search/indexes/youapp/\<model_name>_text.txt(例如 templates/search/indexes/app/test_text.txt),其内容为:

1
2
{{ object.title }}
{{ object.content}}

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

配置 URL

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

1
2
3
4
urlpatterns = [
# 其它...
url(r'^search/', include('haystack.urls')),
]

修改搜索表单

修改一下搜索表单,让它提交数据到 django haystack 搜索视图对应的 URL:

1
2
3
4
<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 属性改为自己写的视图路径。

创建搜索结果页面

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{% load highlight %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.keyword{
color: red;
}
</style>
</head>
<body>
<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>
<br>
{% if query %}
<h3>结果</h3>

{% for result in page.object_list %}
<p>
<a href="{{ result.object.get_absolute_url }}">{{ result.object.title }}</a><br/>
{% highlight result.object.content with query css_class "keyword" %}
</p>
{% empty %}
<p>没有结果发现.</p>
{% endfor %}

{% if page.has_previous or page.has_next %}
<div>
{% if page.has_previous %}<a href="?q={{ query }}&page={{ page.previous_page_number }}">{% endif %}« Previous{% if page.has_previous %}</a>{% endif %}
|
{% if page.has_next %}<a href="?q={{ query }}&page={{ page.next_page_number }}">{% endif %}Next »{% if page.has_next %}</a>{% endif %}
</div>
{% endif %}
{% else %}
{# Show some example queries to run, maybe query syntax, something else? #}
{% endif %}
</form>
</body>
</html>

高亮关键词

注意到百度的搜索结果页面,含有用户搜索的关键词的地方都是被标红的,在 django haystack 中实现这个效果也非常简单,只需要使用 highlight 模板标签即可,其用法如下:

1
2
3
4
5
6
7
8
# 使用默认值  
{% 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 %}

在搜索的结果页面也是用高亮语法:

1
2
# 就是说在 content 内容中的关键字使用名为 ‘keyword’ 的css样式包裹
{% highlight result.object.content with query css_class "keyword" %}

修改搜索引擎为中文分词

使用 Whoosh 作为搜索引擎,但在 django haystack 中为 Whoosh 指定的分词器是英文分词器,可能会使得搜索结果不理想,我们把这个分词器替换成 jieba 中文分词器。这里需要在项目环境下的Lib\site-packages\haystack\backends的目录中新建一个文件 ChineseAnalyzer.py,其中代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import jieba

from whoosh.analysis import RegexAnalyzer
from whoosh.analysis import Tokenizer, Token


class ChineseTokenizer(Tokenizer):
def __call__(self, value, positions=False, chars=False,
keeporiginal=False, removestops=True,
start_pos=0, start_char=0, mode='', **kwargs):
t = Token(positions, chars, removestops=removestops, mode=mode,
**kwargs)
seglist = jieba.cut(value, cut_all=True)
for w in seglist:
t.original = t.text = w
t.boost = 1.0
if positions:
t.pos = start_pos + value.find(w)
if chars:
t.startchar = start_char + value.find(w)
t.endchar = start_char + value.find(w) + len(w)
yield t


def ChineseAnalyzer():
return ChineseTokenizer()

再把 haystack/backends/whoosh_backends.py 文件复制一份到当前目录下,重命名为 whoosh_cn_backends.py(之前我们在 settings.py 中 的 HAYSTACK_CONNECTIONS 指定的就是这个文件),然后修改whoosh_cn_backends.py中的代码:

1
2
# 顶部引入刚才添加的中文分词
from .ChineseAnalyzer import ChineseAnalyzer

找到如下一行代码:

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

修改为:

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

建立索引文件

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

效果如下:

效果图
效果图