django haystack深入研究

django haystack深入研究

前言

evalshell.com 风炫安全 是我自己开发的一个搜集全网安全领域知识库的一个垂直领域的网站,对于一个垂直搜索网站,我深知搜索准确度和速度的重要,所以经过一番调研之后我选择了haystack这个全文搜索扩展库。

但是我有着自定义的需求,所以对haystack进行了源码分析定制。

搜索1

软件简介

Haystack 是 Django 框架的搜索扩展模块。Haystack 提供统一的 API 允许你使用不同的搜索后端,包括 SolrElasticsearchWhooshXapian 等等。

使用解析

安装

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

搜索1

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值