网站首页 文章专栏 django设置全文搜索引擎
自己的网站一般都采用直接数据库搜索的方式,一直表现良好(数据量小)。直到某一天我将搜索词从“被掩埋的巨人”变成了“被掩埋 巨人”(中间有空格),数据库返回零。
使用的代码片段如下:
search_result = Article.objects.filter(Q(title__icontains=keywords))
很显然,这是由于我采用了`icontains造成的,无法自动分词。遂考虑换为全文搜索。
按照上面两个教程的设置应该不会出现大问题。
虽然上述两个教程已经非常详尽了,但是我在实现的过程中依旧碰到了一些麻烦。可见教程中还是忽略了一些自己并不知晓的东西,强调如下。
简单起见,一般都是先按照教程中的设定做实现,这里就要考虑很多default设定。一般都和model
有关。
在全文搜索(中文)教程中,共涉及到以下几个文件。
app路径下(我这里的app文件夹是viewer): - search_indexes.py - whoosh_cn_backend.py
这两个文件名不需要做变动。
├── 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__()
函数来具体处理。