django设置全文搜索引擎

背景

自己的网站一般都采用直接数据库搜索的方式,一直表现良好(数据量小)。直到某一天我将搜索词从“被掩埋的巨人”变成了“被掩埋 巨人”(中间有空格),数据库返回零。

使用的代码片段如下:

1
search_result = Article.objects.filter(Q(title__icontains=keywords))

很显然,这是由于我采用了`icontains造成的,无法自动分词。遂考虑换为全文搜索。

全文搜索的简单实现

参考官方教程,脚本之家(步骤详细)

按照上面两个教程的设置应该不会出现大问题。

教程中需要强调的地方

虽然上述两个教程已经非常详尽了,但是我在实现的过程中依旧碰到了一些麻烦。可见教程中还是忽略了一些自己并不知晓的东西,强调如下。

  1. 默认路径

简单起见,一般都是先按照教程中的设定做实现,这里就要考虑很多default设定。一般都和model有关。

在全文搜索(中文)教程中,共涉及到以下几个文件。

app路径下(我这里的app文件夹是viewer):

  • search_indexes.py
  • whoosh_cn_backend.py

这两个文件名不需要做变动。

1
2
3
4
5
6
7
8
9
10
├── viewer
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   ├── models.py
│   ├── search_indexes.py
│   ├── tests.py
│   ├── views.py
│   └── whoosh_cn_backend.py

templates路径下

  • search/indexes/viewer/item_text.txt
  • search/search.html

item_text.txt变更为你自己的模型名称,我的模型为item,所以是item_text.txt(未尝试名称不变更的后果)

1
2
3
4
5
6
7
8
9
├── templates
│   ├── article.html
│   ├── comments.html
│   ├── pagination.html
│   ├── search
│   │   ├── indexes
│   │   │   └── viewer
│   │   │   └── item_text.txt
│   │   └── search.html

  1. 默认名称

settings.py中有如下代码块:

1
2
3
4
5
6
7
import os
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}

其中,ENGINE字段需要根据自己实际情况做变动。如果是英文搜索,直接参考官方教程即可;如果是中文搜索,参考脚本之家的教程,改成whoosh_cn_backend.py所在的路径。

比如,我的whoosh_cn_backend.pyviewer路径下,就可以修改为:

1
2
3
4
5
6
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'viewer.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
},
}

增加搜索结果高亮

如果我们想要更优雅一些,比如让命中的文字高亮,该如何做呢?参考官方搜索结果高亮教程

总结来看,每次搜索向模板文件返回的结果包含两个要素,pagequery,page中包含分好页的搜索结果,query就是form.cleaned_data['q']语句的返回结果,而form则是ModelSearchForm的实例,它是使用了request.GET的参数来初始化的。

使用highlight标签配合query就可以将搜索结果高亮,主要的工作在template中完成。

一个典型的template文件示例如下:

不要忘了先 load highlight

1
2
3
4
5
6
7
8
{% load highlight %}
<style>
span.highlighted { color: red; }
</style>
<!--省略无关代码-->
{% highlight result.object.name with query %}
<!--省略无关代码-->

自定义view

在有些情况下,我们可能要自定义一个view来使用全文搜索的结果。比如说前端页面已经完成,不希望做太大更改;或者请求是post而不是get;或者说要实现聚合搜索,即本地数据库找到结果太少时,像其他主机请求数据。

使用默认的view显然无法满足需求。

还记得吗,在简单实现部分,两个教程都使用了url(r'^search/', include('haystack.urls')),路由,这也是很多文件必须使用默认路径的原因。

由于使用了默认的路由,所有的请求都由haystack处理,实际的处理函数是SearchView(),在库的安装路径可以找到,我的路径是~/.local/lib/python3.5/site-packages/haystack/views.py.

为方便阅读,SearchView的全部代码如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
class SearchView(object):
template = 'search/search.html'
extra_context = {}
query = ''
results = EmptySearchQuerySet()
request = None
form = None
results_per_page = RESULTS_PER_PAGE
def __init__(self, template=None, load_all=True, form_class=None, searchqueryset=None, results_per_page=None):
self.load_all = load_all
self.form_class = form_class
self.searchqueryset = searchqueryset
if form_class is None:
self.form_class = ModelSearchForm
if not results_per_page is None:
self.results_per_page = results_per_page
if template:
self.template = template
def __call__(self, request):
"""
Generates the actual response to the search.
Relies on internal, overridable methods to construct the response.
"""
self.request = request
self.form = self.build_form()
self.query = self.get_query()
self.results = self.get_results()
return self.create_response()
def build_form(self, form_kwargs=None):
"""
Instantiates the form the class should use to process the search query.
"""
data = None
kwargs = {
'load_all': self.load_all,
}
if form_kwargs:
kwargs.update(form_kwargs)
if len(self.request.GET):
data = self.request.GET
if self.searchqueryset is not None:
kwargs['searchqueryset'] = self.searchqueryset
return self.form_class(data, **kwargs)
def get_query(self):
"""
Returns the query provided by the user.
Returns an empty string if the query is invalid.
"""
if self.form.is_valid():
return self.form.cleaned_data['q']
return ''
def get_results(self):
"""
Fetches the results via the form.
Returns an empty list if there's no query to search with.
"""
return self.form.search()
def build_page(self):
"""
Paginates the results appropriately.
In case someone does not want to use Django's built-in pagination, it
should be a simple matter to override this method to do what they would
like.
"""
try:
page_no = int(self.request.GET.get('page', 1))
except (TypeError, ValueError):
raise Http404("Not a valid number for page.")
if page_no < 1:
raise Http404("Pages should be 1 or greater.")
start_offset = (page_no - 1) * self.results_per_page
self.results[start_offset:start_offset + self.results_per_page]
paginator = Paginator(self.results, self.results_per_page)
try:
page = paginator.page(page_no)
except InvalidPage:
raise Http404("No such page!")
return (paginator, page)
def extra_context(self):
"""
Allows the addition of more context variables as needed.
Must return a dictionary.
"""
return {}
def get_context(self):
(paginator, page) = self.build_page()
context = {
'query': self.query,
'form': self.form,
'page': page,
'paginator': paginator,
'suggestion': None,
}
if hasattr(self.results, 'query') and self.results.query.backend.include_spelling:
context['suggestion'] = self.form.get_suggestion()
context.update(self.extra_context())
return context
def create_response(self):
"""
Generates the actual HttpResponse to send back to the user.
"""
context = self.get_context()
return render(self.request, self.template, context)

可以看出,SearchView类被当做函数调用后,传入的参数是request,之后经过build_form(), get_query(),get_results()后获得搜索结果,返回函数create_response()的运行结果,而在create_response()中又调用了build_page()完成分页。

可以考虑继承SearchView类,接收keywords参数,并构造为一个request.GET对象由父类处理搜索,返回结果无需分页。

如此,我们需要重载build_form(),__call__()两个函数。

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
from haystack.views import SearchView
from django.http import QueryDict
class whoosh_search(SearchView):
def build_form(self,keywords,form_kwargs=None):
data = None
kwargs = {'load_all':self.load_all}
if form_kwargs:
kwargs.update(form_kwargs)
if len(keywords):
data = QueryDict('q='+keywords)
if self.searchqueryset is not None:
kwargs['searchqueryset'] = self.searchqueryset
return self.form_class(data,**kwargs)
def __call__(self,keywords):
self.form = self.build_form(keywords=keywords)
self.query = self.get_query()
self.results = self.get_results()
item_list = []
for item in self.results:
item_dict = {}
item_dict['name'] = item.object.name
item_dict['author'] = item.object.author
item_dict['id'] = item.object.id
item_list.append(item_dict)
return item_list,self.query

注意self.resultsSearchQuerySet对象,迭代之后需要使用.object来取数据对象。

这里使用了QueryDict对象,参考博客. 其实比较tricky,更优雅的方法是跳过form的构造过程,直接使用SearchQuery。 不希望再往深处挖了,只希望这个类能正常工作。

这样,在需要使用搜索引擎时,调用这个类就好了,比如:

1
post_list,query = whoosh_search()('hello')

其他:把类当函数使用

在实现自定义view时,碰到一个语法点觉得很有意思。

SearchView本来是一个类,将它作为url路由的处理函数时需要这样写,url('^search/',SearchView()), 这样在调用的时候就变成了SearchView()(request), 由类中的__call__()函数来具体处理。

0%