本文由腾讯云+社区自动同步,原文地址 http://blogtest.stackoverflow.club/125/
自己的网站一般都采用直接数据库搜索的方式,一直表现良好(数据量小)。直到某一天我将搜索词从“被掩埋的巨人”变成了“被掩埋 巨人”(中间有空格),数据库返回零。
使用的代码片段如下:
search_result = Article.objects.filter(Q(title__icontains=keywords))
很显然,这是由于我采用了`icontains造成的,无法自动分词。遂考虑换为全文搜索。
按照上面两个教程的设置应该不会出现大问题。
虽然上述两个教程已经非常详尽了,但是我在实现的过程中依旧碰到了一些麻烦。可见教程中还是忽略了一些自己并不知晓的东西,强调如下。
简单起见,一般都是先按照教程中的设定做实现,这里就要考虑很多default设定。一般都和model
有关。
在全文搜索(中文)教程中,共涉及到以下几个文件。
app路径下(我这里的app文件夹是viewer):
这两个文件名不需要做变动。
├── viewer
│ ├── admin.py
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ ├── models.py
│ ├── search_indexes.py
│ ├── tests.py
│ ├── views.py
│ └── whoosh_cn_backend.py
templates路径下
item_text.txt变更为你自己的模型名称,我的模型为item,所以是item_text.txt(未尝试名称不变更的后果)
├── templates
│ ├── article.html
│ ├── comments.html
│ ├── pagination.html
│ ├── search
│ │ ├── indexes
│ │ │ └── viewer
│ │ │ └── item_text.txt
│ │ └── search.html
在settings.py
中有如下代码块:
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.py
在viewer
路径下,就可以修改为:
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'viewer.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
},
}
如果我们想要更优雅一些,比如让命中的文字高亮,该如何做呢?参考官方搜索结果高亮教程
总结来看,每次搜索向模板文件返回的结果包含两个要素,page
和query
,page
中包含分好页的搜索结果,query
就是form.cleaned_data['q']
语句的返回结果,而form
则是ModelSearchForm
的实例,它是使用了request.GET
的参数来初始化的。
使用highlight
标签配合query
就可以将搜索结果高亮,主要的工作在template
中完成。
一个典型的template
文件示例如下:
不要忘了先 load highlight
{% load highlight %}
<style>
span.highlighted { color: red; }
</style>
<!--省略无关代码-->
{% highlight result.object.name with query %}
<!--省略无关代码-->
在有些情况下,我们可能要自定义一个view
来使用全文搜索的结果。比如说前端页面已经完成,不希望做太大更改;或者请求是post
而不是get
;或者说要实现聚合搜索,即本地数据库找到结果太少时,像其他主机请求数据。
使用默认的view显然无法满足需求。
还记得吗,在简单实现部分,两个教程都使用了url(r'^search/', include('haystack.urls')),
路由,这也是很多文件必须使用默认路径的原因。
由于使用了默认的路由,所有的请求都由haystack
处理,实际的处理函数是SearchView()
,在库的安装路径可以找到,我的路径是~/.local/lib/python3.5/site-packages/haystack/views.py
.
为方便阅读,SearchView
的全部代码如下:
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__()
两个函数。
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.results
是SearchQuerySet
对象,迭代之后需要使用.object
来取数据对象。
这里使用了QueryDict
对象,参考博客. 其实比较tricky,更优雅的方法是跳过form的构造过程,直接使用SearchQuery
。 不希望再往深处挖了,只希望这个类能正常工作。
这样,在需要使用搜索引擎时,调用这个类就好了,比如:
post_list,query = whoosh_search()('hello')
在实现自定义view
时,碰到一个语法点觉得很有意思。
SearchView
本来是一个类,将它作为url
路由的处理函数时需要这样写,url('^search/',SearchView())
, 这样在调用的时候就变成了SearchView()(request)
, 由类中的__call__()
函数来具体处理。