django haystack深入研究
前言
evalshell.com 风炫安全 是我自己开发的一个搜集全网安全领域知识库的一个垂直领域的网站,对于一个垂直搜索网站,我深知搜索准确度和速度的重要,所以经过一番调研之后我选择了haystack这个全文搜索扩展库。
但是我有着自定义的需求,所以对haystack进行了源码分析定制。
软件简介
Haystack 是 Django 框架的搜索扩展模块。Haystack 提供统一的 API 允许你使用不同的搜索后端,包括 Solr, Elasticsearch, Whoosh, Xapian 等等。
使用解析
安装
pip install django-haystack
pip install whoosh
pip install jieba
添加到APP
把Haystack添加到settings.py中的INSTALLED_APPS中:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
# 添加
'haystack',
# 你的app
'App',
]
配置后端搜索引擎
然后在settings.py中添加一个haystack所要使用的后端搜索引擎。我这里选择最简单的whoosh引擎,对我这里的小站来说已经够了
import os
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}
# 自动更新索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
编写模型
我自己的文章模型
class Article(BaseModel):
"""文章"""
STATIC_CHOICE = (
('a', '审核中'),
('p', '发表'),
)
TYPE = (
(1, '原创'),
(2, '外链'),
)
SOURCE = (
(1, '原创'),
(2, '外链'),
)
ARTICLE_TYPE = (
('paper', '文章'),
('poc', '漏洞'),
('tool', '工具'),
)
RANK_CHOICE = (
(1, '低危'),
(2, '中危'),
(3, '高危'),
)
author = models.TextField('作者', blank=False, null=False)
title = models.CharField('标题', max_length=200, unique=True)
body = MDTextField('正文')
pub_time = models.DateTimeField(
'发布时间',blank=False,null=False,default=now
)
status = models.CharField(
'发布状态',
max_length=1,
choices=STATIC_CHOICE,
default='p'
)
source = models.IntegerField('来源', choices=SOURCE, default=2)
article_type = models.CharField('文章类型', choices=ARTICLE_TYPE, default='paper', max_length=50)
views = models.PositiveIntegerField('浏览量', default=0)
article_order = models.IntegerField('排序,数字越大越靠前', blank=True, null=True, default=0)
rank = models.IntegerField('危险级别', choices=RANK_CHOICE, default=1)
category = models.ForeignKey(
'Category',
verbose_name='分类',
blank=True,
null=True,
on_delete=models.CASCADE
)
tags = models.ManyToManyField('Tag', verbose_name='标签集合', blank=True, null=True)
def body_to_string(self):
return self.title
class Meta:
ordering = ['-rank', '-article_order', '-pub_time']
verbose_name = '文章'
verbose_name_plural = verbose_name
get_latest_by = 'id'
db_table = 'article'
编写views层
在Views中编写控制器,这里需要继承HaystackViewSet
,从from drf_haystack.viewsets import HaystackViewSet
导入。
from blog.models import Article
class ArticleSearchAPIView(HaystackViewSet):
index_models = [Article]
serializer_class = ArticleIndexSerializer
pagination_class = ArticlePagination
编写序列化类
我自己的序列化类:
class ArticleIndexSerializer(hyserializers.HaystackSerializerMixin, ArticleSerializer):
class Meta(ArticleSerializer.Meta):
index_classes = [ArticleIndex]
search_fields = ['text']
配置urls路由
router = routers.DefaultRouter()
router.register(r"search", views.ArticleSearchAPIView, basename='rsearch')
urlpatterns += router.urls
编写索引文件
from haystack import indexes
from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
#类名必须为需要检索的Model_name+Index,这里需要检索Article,所以创建ArticleIndex
text = indexes.CharField(document=True, use_template=True)#创建一个text字段
def get_model(self):#重载get_model方法,必须要有!
return Article.objects.filter(status='p')
def index_queryset(self, using=None):
return self.get_model().objects.all()
编写模板字段
templates文件夹中建立一个新的模板,search/indexes/项目名/模型名_text.txt,并且将以下的内容放入txt文件中
#在目录“templates/search/indexes/应用名称/”下创建“模型类名称_text.txt”文件
{{ object.title }}
{{ object.desc }}
{{ object.content }}
建立索引
运行 python manage.py rebuild_index
建立索引
深度需求
haystack官方提供的是基于drf 的API接口,但是我自己走的是后端渲染模板的方式,我不想直接调用接口,这样反爬虫会难做一些。
所以看了下haystack的官方文档, https://django-haystack.readthedocs.io/en/v2.8.1/searchquery_api.html 看到haystack是返回了一种类似于drf的QuerySet对象,命名为SearchQuerySet
可以像Django自带的ORM一样对数据进行查询
from haystack.query import SearchQuerySet
results = SearchQuerySet().exclude(content='hello').filter(content='world').order_by('-pub_date').boost('title', 0.5)[10:20]
还提供了更人性化的Input type https://django-haystack.readthedocs.io/en/v2.8.1/inputtypes.html#ref-inputtypes , 比如说可以用来查询两个相近的字符串Exact
sqs = SearchQuerySet().filter(author=Exact('n-gram support'))
就类似我们在百度上搜索 "n-gram support" 。
AutoQuery
,默认就是用的AutoQuery
查询url上的q参数的内容。
最后找到了我们想要的内容 https://django-haystack.readthedocs.io/en/v2.8.1/views_and_forms.html ,于是编写代码
forms.py
class ArticleSearchForm(SearchForm):
q = forms.CharField(required=True, max_length=254, error_messages={
'required': '关键字不能为空',
'max_length': '关键字不能超过20位'
})
article_type = forms.CharField(required=True, initial='paper', max_length=20, error_messages={
'required': '文章类型不能为空',
'max_length': '文章类型不能超过20位'
})
def search(self):
# First, store the SearchQuerySet received from other processing.
sqs = super(ArticleSearchForm, self).search()
if not self.is_valid():
return self.no_query_found()
if not self.cleaned_data.get("q"):
return self.no_query_found()
if self.cleaned_data['article_type']:
sqs = sqs.filter(article_type=self.cleaned_data['article_type'])
return sqs
Views.py中
class ArticleSearchView(SearchView):
template_name = 'blog/search.html'
form_class = ArticleSearchForm
paginate_by = settings.PAGINATE_BY
def get_context_data(self, **kwargs):
context = super(ArticleSearchView, self).get_context_data(**kwargs)
context['category_list'] = getallCategory()
context['count'] = self.get_queryset().count()
return context
写到这里的时候,我真正的需求时 我想如果url上q的参数内容为空,我不返回任何文章给用户.于是查找haystack的源代码,根据SearchQuerySet
在 lib/python3.7/site-packages/haystack/query.py 找到了 EmptySearchQuerySet
,这里返回了一个空的queryset对象。
但是发现一个问题,就是如果form验证失败的话,根本就不会走search方法,所以找到父类SearchView
, lib/python3.7/site-packages/haystack/generic_views.py
class SearchView(SearchMixin, FormView):
"""A view class for searching a Haystack managed search index"""
def get(self, request, *args, **kwargs):
"""
Handles GET requests and instantiates a blank version of the form.
"""
form_class = self.get_form_class()
form = self.get_form(form_class)
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
看到如果表单不合法的话,就会走return self.form_invalid(form)
, 而form_invalid
的代码为:
def form_invalid(self, form):
context = self.get_context_data(
**{self.form_name: form, "object_list": self.get_queryset()}
)
return self.render_to_response(context)
可以看到是调用了self.get_queryset()
而如果搜索中的q为空的话,这里的queryset会因为AutoQuery
的原因搜索所有文章中有空格的文章,就会导致所有文章被搜索出来,这是不正确的!
所以最后我在views中重写了form_invalid,最后views的代码为
class ArticleSearchView(SearchView):
template_name = 'blog/search.html'
form_class = ArticleSearchForm
paginate_by = settings.PAGINATE_BY
def form_invalid(self, form):
context = self.get_context_data(
**{self.form_name: form, "object_list": EmptySearchQuerySet()} #这里不走self.get_queryset(),而是返回空的SearchQuerySet
)
return self.render_to_response(context)
def get_context_data(self, **kwargs):
context = super(ArticleSearchView, self).get_context_data(**kwargs)
context['category_list'] = getallCategory()
context['count'] = self.get_queryset().count()
return context