简单的博客搜索、查询功能查找到符合关键字的对象就行了。不过为了提升逼格,至少应该能够根据用户的搜索关键词对搜索结果进行排序以及高亮关键字。django-haystack 全文搜索包可以带你轻松装逼
django-haystack
是一个专门提供搜索功能的 Django 第三方应用,它支持 Solr、Elasticsearch、Whoosh、Xapian 等多种搜索引擎,配合著名的中文自然语言处理库 jieba 分词,就可以为我们的博客提供一个效果不错的博客文章搜索功能
启动虚拟环境 fswy
$ source fswy/bin/activate
(fswy) blog xiatian$ pip3 install whoosh
(fswy) blog xiatian$ pip3 install django-haystack
(fswy) blog xiatian$ pip3 install jieba
是一个由纯 Python 实现的全文搜索引擎,没有二进制文件等,比较小巧,配置简单方便
由于 Whoosh 自带的是英文分词,对中文的分词支持不是太好,所以使用 jieba 替换Whoosh 的分词组件
我们使用 Whoosh 作为搜索引擎,但在 Django Haystack 中为 Whoosh 指定的分词器是英文分词器,搜索结果可能不理想,我们把这个分词器替换成 jieba 中文分词器。
进入 fswy/Lib/site-packages/haystack/backends
拷贝 whoosh_backend.py
至 blog -> fswy
修改文件名为 whoosh_cn_backend.py
【提示】——fswy是本项目使用的虚拟环境文件夹
blog -> fswy -> whoosh_cn_backend.py
#在全局引入的最后一行加入jieba分词器
from jieba.analyse import ChineseAnalyzer
elif field_class.field_type == 'edge_ngram':
schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', stored=field_class.stored, field_boost=field_class.boost)
else:
# 修改
schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True)
原始
analyzer=StemmingAnalyzer()
修改后
analyzer=ChineseAnalyzer()
添加'django.contrib.humanize'和haystack
到设置中
blog -> blog -> settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 网站地图应用
'django.contrib.sitemaps',
'django.contrib.sites', # 添加评论app注册
'django_comments',
'django.contrib.humanize', # 添加人性化过滤器
'haystack', # 全文搜索应用 这个要放在其他应用之前
'imagekit', # 使用imagekit
'apps.fswy', # 添加用户应用
'apps.user',
'apps.comment', #添加评论应用
]
# 统一分页设置
BASE_PAGE_BY = 4
BASE_ORPHANS = 5
# 全文搜索应用配置
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'fswy.whoosh_cn_backend.WhooshEngine', # 选择语言解析器为自己更换的结巴分词
'PATH': os.path.join(BASE_DIR, 'whoosh_index'), # 保存索引文件的地址,选择主目录下,这个会自动生成
}
}
# 自动更新索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
指定 django haystack
使用的搜索引擎,这里我们使用 fswy.whoosh_cn_backend.WhooshEngine
,虽然目前这个引擎还不存在,但我们接下来会创建它
指定索引文件需要存放的位置,我们设置为项目根目录BASE_DIR
下的 whoosh_index
文件夹(在建立索引时会自动创建)
指定如何对搜索结果分页,这里设置为每 10 项结果为一页。
指定什么时候更新索引,这里定义为每当有文章更新时就更新索引。由于博客文章更新不会太频繁,因此实时更新没有问题。
第一次需要受手动创建索引
$ cd ~/blog
$ python manage.py rebuild_index
或者
Pycharm 中 Tools -> run manage.py task
下执行命令:
rebuild_index
这里其实发生过很多因为python和django版本而导致的错误,详情可以查看 Django3.0+Python3.8+MySQL8.0 个人博客搭建十七|Haystack 全文搜索的坑
(fswy) blog xiatian$ python3 manage.py rebuild_index
WARNING: This will irreparably remove EVERYTHING from your search index in connection 'default'.
Your choices after this are to restore from backups or rebuild via the `rebuild_index` command.
Are you sure you wish to continue? [y/N] y
Removing all documents from your index because you said so.
All documents removed.
Indexing 2 文章
Building prefix dict from the default dictionary ...
Loading model from cache /var/folders/7d/s7t1kh7n4x59zqltw7gs26640000gn/T/jieba.cache
Loading model cost 0.745 seconds.
Prefix dict has been built successfully.
blog -> fswy
创建search_indexes.py
blog -> fswy -> search_indexes.py
# -*- coding: utf-8 -*-
from haystack import indexes
from .models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
'''
document:指定了将模型类中的哪些字段建立索引
use_template:在模板文件夹中创建文件夹指明具体的字段建立索引
'''
text = indexes.CharField(document=True, use_template=True)
views = indexes.IntegerField(model_attr='views')
def get_model(self):
# 为那个模型表建立索引
return Article
def index_queryset(self, using=None):
return self.get_model().objects.all()
文件名称必须是 search_indexes.py
Django Haystack: 要想对某个 app
下的数据进行全文检索,就要在该 app
下创建一 search_indexes.py
文件,然后创建一个 XXIndex
类(XX 为含有被检索数据的模型,如这里的 Article
),并且继承 SearchIndex
和Indexable
索引就是一本书的目录,可以为读者提供更快速的导航与查找。在这里也是同样的道理,当数据量非常大的时候,若要从这些数据里找出所有的满足搜索条件的几乎是不太可能的,将会给服务器带来极大的负担。所以我们需要为指定的数据添加一个索引(目录),在这里是为 Article 创建一个索引,索引的实现细节是我们不需要关心的,我们只关心为哪些字段创建索引,如何指定。
每个索引里面必须有且只能有一个字段为document=True
,这代表 django haystack
和搜索引擎
将使用此字段的内容作为索引进行检索(primary field
)。
如果使用一个字段设置了document=True
,则一般约定此字段名为 text
,这是在 SearchIndex
类里面一贯的命名,以防止后台混乱,不建议改。
haystack
提供了 use_template=True
在 text
字段中,这样就允许我们使用数据模板
去建立搜索引擎索引
的文件,就是索引里面需要存放一些什么东西,例如Article
的title
字段,这样我们可以通过title
内容来检索 Article
数据。举个例子,假如你搜索 Python
,那么就可以检索出 title
中含有 Python
的 Article
。
MySearchView
:重写搜索视图,可以增加一些额外的参数,且可以重新定义名称
blog -> fswy -> views.py
# 重写搜索视图,可以增加一些额外的参数,且可以重新定义名称
class MySearchView(SearchView):
# 返回搜索结果集
context_object_name = 'search_list'
# 设置分页
paginate_by = getattr(settings, 'BASE_PAGE_BY', None)
paginate_orphans = getattr(settings, 'BASE_ORPHANS', 0)
# 搜索结果以浏览量排序
queryset = SearchQuerySet().order_by('-views')
blog -> fswy -> urls.py
from .views import MySearchView
# 全文搜索
path(r'search/', MySearchView.as_view(), name='search'),
blog -> fswy -> templatetags -> blog_tags.py
@register.simple_tag
def my_highlight(text, q):
"""自定义标题搜索词高亮函数,忽略大小写"""
if len(q) > 1:
try:
text = re.sub(q, lambda a: '{}'.format(a.group()),
text, flags=re.IGNORECASE)
text = mark_safe(text)
except:
pass
return text
blog -> templates
创建 search
文件
|-- search
| |-- indexes
| | |-- storm
| | | |-- article_text.txt
| |-- search.html
配置全文搜索字段
blog -> templates -> search -> indexes -> fswy -> article_text.txt
# 文章标题
{{ object.title }}
# 文章内容
{{ object.body_to_markdown }}
这个数据模板的作用是对 Article.title
、Article.body_to_markdown
这两个字段建立索引,当检索的时候会对这两个字段做全文检索匹配,然后将匹配的结果排序后作为搜索结果返回。
在模板中使用循环来遍历 search_list
变量,变量的类型: SearchResult
app_label
- The application the model is attached to.
model_name
- The model’s name.
pk
- The primary key of the model.
score
- The score provided by the search engine.
object
- The actual model instance (lazy loaded).
model
- The model class.
verbose_name
- A prettier version of the model’s class name for display.
verbose_name_plural
- A prettier version of the model’s plural class name for display.
searchindex
- Returns the SearchIndex class associated with this result.
distance
- On geo-spatial queries, this returns a Distance object representing the distance the result was from the focused point.
blog -> templates -> base.html
<li style="float:right;">
<div class="toggle-search"><i class="fa fa-search">i>div>
<div class="search-expand" style="display: none;">
<div class="search-expand-inner">
<form class="nav-item navbar-form mr-2 py-md-2" role="search" method="get" id="searchform" action="{% url 'blog:search' %}">
<div class="input-group">
<input type="search" name="q" class="form-control rounded-0 f-15" placeholder="搜索" required=True>
<div class="input-group-btn">
<button class="btn btn-info rounded-0" type="submit"><i class="fa fa-search">i>button>
div>
div>
form>
div>
div>
li>
直接拷贝:blog -> templates -> content.html
内容至 blog -> templates -> search -> search.html
稍加修改即可作为搜索结果页面
blog -> templates -> search -> search.html
{% extends 'base_right.html' %}
{% load blog_tags oauth_tags comment_tags static %}
{% load humanize %}
{% load highlight %}
{% block head_title %}文章搜索:{{ query }}{% endblock %}
{% block title %}甫式人生 | 文章搜索:{{ query }}{% endblock title %}
{% block metas %}
<meta name="description" content="文章搜索:{{ query }},网站全文搜索功能,按照文章标题和内容建立索引,实现整站搜索,django-haystack全文搜索库的使用">
<meta name="keywords" content="{{ query }},全文搜索,django-haystack">
{% endblock %}
{% block description %}
<meta name="description" content="文章搜索:{{ query }},网站全文搜索功能,按照文章标题和内容建立索引,实现整站搜索,django-haystack全文搜索库的使用"/>
{% endblock description %}
{% block keywords %}
<meta name="keywords" content="fswy,{{ query }},全文搜索,django-haystack"/>
{% endblock keywords %}
{% block body %}
<div class="content-wrap">
<div class="content">
<header class="archive-header">
<h1><i class="fa fa-folder-open">i> 分类:{{ query }}
<a title="订阅福利专区" target="_blank" href="{% url 'blog:category' resources '' %}"><i class="rss fa fa-rss">i>a>
h1>
header>
{% for article in search_list %}
<article class="excerpt">
<header>
<a class="label label-important" href="{{ article.object.category.get_absolute_url }}">{{ article.object.category.name }}<i class="label-arrow">i>a>
<h2 class="mt-0 font-weight-bold text-info f-17">
<a href="{{ article.object.get_absolute_url }}" target="_blank">{% my_highlight article.object.title query %}a>
h2>
header>
<div class="focus"><a target="_blank" href="{{ article.object.get_absolute_url }}">
<img class="thumb" width="200" height="123" src="{{ article.object.img_link }}" alt="{{ article.object.title }}" />a>
div>
{% with article.object.body_to_markdown|safe as this_body %}
<p class="d-none d-sm-block mb-2 f-15">{% highlight this_body with query max_length 130 %}p>
<p class="d-block d-sm-none mb-2 f-15">{% highlight this_body with query max_length 64 %}p>
{% endwith %}
<p class="auth-span">
<span class="muted"><i class="fa fa-user">i> <a href="/author/{{ article.object.author }}">{{ article.object.author }}a>span>
<span class="muted"><i class="fa fa-clock-o">i> {{ article.object.create_date|date:'Y-m-d'}}span>
<span class="muted"><i class="fa fa-eye">i> {{ article.object.views }}浏览span>
<span class="muted"><i class="fa fa-comments-o">i>
<a target="_blank" href="/article/{{ article.object.slug }}#comments">{% get_comment_count article.object.id article.object.id%}评论a>
span>
<span class="muted"><a href="javascript:;" data-action="ding" data-id="455" id="Addlike" class="action">
<i class="fa fa-heart-o">i>
<span class="count">{{ article.object.love }}span>喜欢a>span>p>
article>
{% empty %}
<div class="no-post">未搜索到相关内容!div>
{% endfor %}
{% if is_paginated %}
<div class="text-center mt-2 mt-sm-1 mt-md-0 mb-3 f-16">
{% if page_obj.has_previous %}
<a class="text-success" href="?q={{ query }}&page={{ page_obj.previous_page_number }}">上一页a>
{% else %}
<span class="text-secondary" title="当前页已经是首页">上一页span>
{% endif %}
<span class="mx-2">第 {{ page_obj.number }} / {{ paginator.num_pages }} 页span>
{% if page_obj.has_next %}
<a class="text-success" href="?q={{ query }}&page={{ page_obj.next_page_number }}">下一页a>
{% else %}
<span class="text-secondary" title="当前页已经是末页">下一页span>
{% endif %}
div>
{% endif %}
div>
div>
{% endblock body %}
对 content.html
做的主要改动就是,添加了搜索词 query
信息,返回的查询集 search_list
,标题和摘要高亮关键词
query
:用户搜索的关键词
search_list
:即为 MySearchView
视图传给模板对搜索结果集 search_list
,数据类型:SearchResult
is_paginated
:haystack 对搜索结果做了分页,is_paginated
判断是否有分页
文章标题关键词高亮
<h2 class="mt-0 font-weight-bold text-info f-17">
<a href="{{ article.object.get_absolute_url }}" target="_blank">{% my_highlight article.object.title query %}a>
h2>
这里使用 自定义my_highlight
标题高亮方法,如果使用
{% highlight article.object.title with query %}
会存在标题不能全部显示的问题,此问题主要是因为
fswy->Lib->site-packages->haystack->utils->highlighting.py
if start_offset > 0:
highlighted_chunk = '...%s' % highlighted_chunk
if end_offset < len(self.text_block):
highlighted_chunk = '%s...' % highlighted_chunk
return highlighted_chunk
start_offset
与 end_offset
分别代表高亮代码的开始位置与结束位置,如果高亮部分在中间的话,前面的部分就直接显示 …
{% with article.object.body_to_markdown|safe as this_body %}
<p class="d-none d-sm-block mb-2 f-15">{% highlight this_body with query max_length 130 %}p>
<p class="d-block d-sm-none mb-2 f-15">{% highlight this_body with query max_length 64 %}p>
{% endwith %}
max_length
限制最终内容被高亮处理后的长度,也即摘要内容长度
【提示】——这里是我自己添加的全文搜索功能 崔庆才 个人博客样式需要添加一点内容
blog -> static -> css -> style.css
在文件尾部添加
.highlighted {
color: #ea6f5a;
}
【提示】——如果存在标题不能全部显示可以修改 haystack 高亮显示源码
fswy->Lib->site-packages->haystack->utils->highlighting.py
if start_offset > 0:
highlighted_chunk = '...%s' % highlighted_chunk
if end_offset < len(self.text_block):
highlighted_chunk = '%s...' % highlighted_chunk
return highlighted_chunk
start_offset
与 end_offset
分别代表高亮代码的开始位置与结束位置,如果高亮部分在中间的话,前面的部分就直接显示…。我们可以在这之前再加一句判断,如果字符串长度小于 max_length
的值的话,我们就直接将其返回
if len(self.text_block) < self.max_length:
return self.text_block[:start_offset] + highlighted_chunk
if start_offset > 0:
highlighted_chunk = '...%s' % highlighted_chunk
if end_offset < len(self.text_block):
highlighted_chunk = '%s...' % highlighted_chunk
return highlighted_chunk
color:高亮关键词颜色
Django3.0+Python3.8+MySQL8.0 个人博客搭建一|前言
Django3.0+Python3.8+MySQL8.0 个人博客搭建二|创建虚拟环境
Django3.0+Python3.8+MySQL8.0 个人博客搭建三|创建博客项目
Django3.0+Python3.8+MySQL8.0 个人博客搭建四|创建第一个APP
Django3.0+Python3.8+MySQL8.0 个人博客搭建五|makemigrations连接MySQL数据库的坑
Django3.0+Python3.8+MySQL8.0 个人博客搭建六|数据库结构设计
Django3.0+Python3.8+MySQL8.0 个人博客搭建七|makemigrations创建数据库的坑(第二弹)
Django3.0+Python3.8+MySQL8.0 个人博客搭建八|通过admin管理后台
Django3.0+Python3.8+MySQL8.0 个人博客搭建九|博客首页开发(一)
Django3.0+Python3.8+MySQL8.0 个人博客搭建十|整理项目结构
Django3.0+Python3.8+MySQL8.0 个人博客搭建十一|博客首页开发(二)
Django3.0+Python3.8+MySQL8.0 个人博客搭建十二|博客首页开发(三)
Django3.0+Python3.8+MySQL8.0 个人博客搭建十三|博客详情页面
Django3.0+Python3.8+MySQL8.0 个人博客搭建十四|注册登录
Django3.0+Python3.8+MySQL8.0 个人博客搭建十五|评论区
Django3.0+Python3.8+MySQL8.0 个人博客搭建十六|网站地图