网站首页 文章专栏 django设置全文搜索引擎
django设置全文搜索引擎
创建于:2018-11-19 00:00:00 更新于:2024-12-03 16:49:42 羽瀚尘 1098
django django,全文搜索,whoosh,haystack,后端

背景

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

使用的代码片段如下:

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

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

全文搜索的简单实现

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

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

教程中需要强调的地方

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

  1. 默认路径

简单起见,一般都是先按照教程中的设定做实现,这里就要考虑很多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路径下

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

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

├── templates
│   ├── article.html
│   ├── comments.html
│   ├── pagination.html
│   ├── search
│   │   ├── indexes
│   │   │   └── viewer
│   │   │       └── item_text.txt
│   │   └── search.html
  1. 默认名称

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.pyviewer路径下,就可以修改为:

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

{% 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的全部代码如下:

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.resultsSearchQuerySet对象,迭代之后需要使用.object来取数据对象。

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

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

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

其他:把类当函数使用

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

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