Django+Elasticsearch完成搜索功能(含高亮)
文章目录
前言
之前一篇文章,讲了如何使用Django+Elasticsearch+Haystack来构建一个搜索器:传送门。但是在ES6之后就陆续将type退出历史舞台,导致Haystack无法使用,因此这篇文章是使用Django(3.1)+Elasticsearch(7.10.1)完成的一个搜素功能。在完成项目的路上,非常感谢这篇博客基于django+elasticsearch的全文检索。
下面这篇文章,是在已经完成数据库配置和django配置的前提下进行的。
一、Elasticsearch简介
Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。它能很方便的使大量数据具有搜索、分析和探索的能力。充分利用Elasticsearch的水平伸缩性,能使数据在生产环境变得更有价值。Elasticsearch 的实现原理主要分为以下几个步骤,首先用户将数据提交到Elasticsearch 数据库中,再通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据,当用户搜索数据时候,再根据权重将结果排名,打分,再将返回结果呈现给用户。
二、ES索引的创建与映射
首先,要创建索引,就必须连接ES,方法如下:
from elasticsearch import Elasticsearch
es = Elasticsearch(["http://127.0.0.1:9200"])
接下来,就是根据我们的模型映射来创建映射。
下面是我们的News模型:
class News(ModelBase):
title = models.CharField(max_length=150, verbose_name="标题", help_text="标题")
digest = models.CharField(max_length=200, verbose_name="摘要", help_text="摘要")
content = models.TextField(verbose_name="内容", help_text="内容")
clicks = models.IntegerField(default=0, verbose_name="点击量", help_text="点击量")
image_url = models.URLField(default="", verbose_name="图片url", help_text="图片url")
tag = models.ForeignKey('Tag', on_delete=models.SET_NULL, null=True)
author = models.ForeignKey('user.Users', on_delete=models.SET_NULL, null=True)
为了方便,我们可以在ES中减少tag和auther。那么,接下来步骤就很简单了,来回顾一下py添加es索引的代码:
# 直接创建索引
es.indices.create(index="blog_django")
# 在一般情况下,如果我们想让我们的索引映射按照自己的预期走,那么我们可以添加一个body
body = {
"mappings": {
"properties": {
"id": {"type": "long", "index": "false"},
"title": {"type": "text", "analyzer": "ik_smart"},
"image_url": {"type": "keyword"}
}
},
"settings": {
"number_of_shards": 2, # 分片数
"number_of_replicas": 0 # 副本数
}
}
es.indices.create(index="blog_django", body=body)
- 分片数和副本数设计的作用就是提高我们的查询速度,如果查询的数据很大的话分片数就增加,默认是5个分片,当然我用不了那么多,大概了解了一下如果你有100G的数据,大概就给5个分片左右,以此类推。副本数,就等于是一个备份数据一样,当然我用不到,其次就是,单个服务器就是0哦,es集群可以考虑副本。官方文档
- 如果你的es中配置了ik中文分词器,在建立映射的时候可以指定指定进行ik分词:“analyzer”: “ik_smart”,不指定的话默认是官方的简单分词器。
- keyword类型,指定该字段不进行分词,例如我们的url地址,如果一般不会让他进行分词。更多字段类型知识点
- “index”,的属性具体有三,analyzed、not_analyzed、false/no, analyzed:表示该字段被分析,编入索引,产生的token能被搜索到;not_analyzed:表示该字段不会被分析,使用原始值编入索引,在索引中作为单个词;no:不编入索引,无法搜索该字段;
- 查看官方关于创建索引的文档:传送门
索引的映射创建
下面这个文件是放在view函数中
# news/views.py
# 该函数为创建索引
def es2(request):
# 创建连接
es = Elasticsearch(["http://127.0.0.1:9200"])
# 设置映射
body = {
"mappings": {
"properties": {
"id": {"type": "long", "index": "false"},
"title": {"type": "text", "analyzer": "ik_smart"},
"digest": {"type": "text", "analyzer": "ik_smart"},
"content": {"type": "text", "analyzer": "ik_smart"},
"image_url": {"type": "keyword"}
}
},
"settings": {
"number_of_shards": 2, # 分片数
"number_of_replicas": 0 # 副本数
}
}
# 创建索引
es.indices.create(index="blog_django", body=body)
当创建好索引之后,我们可以使用kabana查看是否创建成功。
批量导入数据
def es2(request):
... # 这里是上面的创建部分
# 批量添加数据
# 从模型中获取全部数据
query_obj = models.News.objects.all()
# 将数据进行格式化
action = [
{
"_index": "blog_django",
"_source": {
"id": i.id,
"title": i.title,
"digest": i.digest,
"content": i.content,
"image_url": i.image_url
}
} for i in query_obj]
# 批量写入数据
helpers.bulk(es, action, request_timeout=1000)
print("导入成功")
return HttpResponse("ok")
完整的es2函数
# 该函数为创建索引
def es2(request):
# 创建连接
es = Elasticsearch(["http://127.0.0.1:9200"])
# 设置映射
body = {
"mappings": {
"properties": {
"id": {"type": "long", "index": "false"},
"title": {"type": "text", "analyzer": "ik_smart"},
"digest": {"type": "text", "analyzer": "ik_smart"},
"content": {"type": "text", "analyzer": "ik_smart"},
"image_url": {"type": "keyword"}
}
},
"settings": {
"number_of_shards": 2, # 分片数
"number_of_replicas": 0 # 副本数
}
}
# 创建索引
es.indices.create(index="blog_django", body=body)
print("索引创建成功")
# 批量添加数据
# 从模型中获取全部数据
query_obj = models.News.objects.all()
# 将数据进行格式化
action = [
{
"_index": "blog_django",
"_source": {
"id": i.id,
"title": i.title,
"digest": i.digest,
"content": i.content,
"image_url": i.image_url
}
} for i in query_obj]
# 批量写入数据
helpers.bulk(es, action, request_timeout=1000)
print("导入成功")
return HttpResponse("ok")
批量导入数据时,我们使用bulk,当完成了视图后,那就必须有对应的路由。以下就是路由文件
ES索引路由
# news.urls.py
urlpatterns = [
path("indeses/", views.es2, name="indeses"),
]
接下来就是在浏览器输入,如果出现ok就说明全部成功。
127.0.0.1:8000/indeses
三、设计后端
后端视图思路:
- 获取前端传入的q值
- 如果存在q,则使用搜索函数进行搜索
- 如果不存在,则直接返回热门新闻3条数据
- 搜索函数输入q,然后去查找title,digest和content,将所有的值进行返回。
1. 最基本的搜索视图
# 搜索函数
class SearchView(View):
def get(self, request):
# 获取前端传递的值
kw = request.GET.get("q", "")
# 如果值存在
if kw:
show = False
# 使用搜索函数,然后接收返回值
page = self.filter_msg(kw, "blog_django")["hits"]["hits"]
# 序列化值,不然前端无法进行提取
new_page = []
for news in page:
p = dict()
new = news["_source"]
p["digest"] = new["digest"][0]
p["title"] = new["title"]
p["id"] = new["id"]
p["image_url"] = new["image_url"]
p["content"] = new["content"]
new_page.append(p)
# 使用django默认的分页器进行分页
paginator = Paginator(new_page, 5)
# 如果不存在,则直接将热门新闻返回
else:
show = True
host_news = models.HotNews.objects.select_related('news').only('news_id', 'news__title',
'news__image_url').filter(
is_delete=False).order_by('priority')
paginator = Paginator(host_news, 5)
# 获取参数中的page
try:
page = paginator.page(int(self.request.GET.get("page", 1)))
# 如果传的不是整数
except PageNotAnInteger:
# 默认返回第一页的数据
page = paginator.page(1)
except EmptyPage:
page = paginator.page(paginator.num_pages)
return render(request, "news/search.html", locals())
2. ES搜索函数
class SearchView(View):
def filter_msg(self, search_msg, search_index):
es = Elasticsearch(["http://127.0.0.1:9200"])
body = {
"query": {
"bool": {
"should": [
{
"match": {
"title": search_msg
}
},{
"match": {
"content": search_msg
}
},{
"match": {
"digest": search_msg
}
}
]
}
},
"size": 200, # 设置最大的返回值
res = es.search(index=search_index, body=body)
return res
上面就是最基本的一个搜索后台,我们现在来添加一些值:
- 我们如果想让值根据得分进行排列,我们可以添加一个sort
- 如果我们想让数据进行高亮显示,则我们可以使用highlight,
3. 小升级(排序和高亮)
核心代码:
"sort": [{"_score": {"order": "desc"}}],
"highlight": {
"pre_tags": ["<font style='color:red;font-size:20px'>"],
"post_tags": ["</font>"],
"fields": {
"title": {"type": "plain"},
"digest": {"type": "plain"},
}
使用高亮之后,前面的也得进行一些修改
new_page = []
for news in page:
p = dict()
new = news["highlight"]
print(new)
p["digest"] = new["digest"][0]
new = news["_source"]
p["title"] = new["title"]
p["id"] = new["id"]
p["image_url"] = new["image_url"]
p["content"] = new["content"]
new_page.append(p)
下面就直接展示总的代码
# 搜索函数
class SearchView(View):
def get(self, request):
# 获取前端传递的值
kw = request.GET.get("q", "")
# 如果值存在
if kw:
show = False
# 使用搜索函数,然后接收返回值
page = self.filter_msg(kw, "blog_django")["hits"]["hits"]
# 序列化值,不然前端无法进行提取
new_page = []
for news in page:
p = dict()
new = news["highlight"]
print(new)
p["digest"] = new["digest"][0]
new = news["_source"]
p["title"] = new["title"]
p["id"] = new["id"]
p["image_url"] = new["image_url"]
p["content"] = new["content"]
new_page.append(p)
# 使用django默认的分页器进行分页
paginator = Paginator(new_page, 5)
# 如果不存在,则直接将热门新闻返回
else:
show = True
host_news = models.HotNews.objects.select_related('news').only('news_id', 'news__title',
'news__image_url').filter(
is_delete=False).order_by('priority')
paginator = Paginator(host_news, 5)
# 获取参数中的page
try:
page = paginator.page(int(self.request.GET.get("page", 1)))
# 如果传的不是整数
except PageNotAnInteger:
# 默认返回第一页的数据
page = paginator.page(1)
except EmptyPage:
page = paginator.page(paginator.num_pages)
return render(request, "news/search.html", locals())
def filter_msg(self, search_msg, search_index):
es = Elasticsearch(["http://127.0.0.1:9200"])
body = {
"query": {
"bool": {
"should": [
{
"match": {
"title": search_msg
}
},{
"match": {
"content": search_msg
}
},{
"match": {
"digest": search_msg
}
}
]
}
},
"size": 200,
"sort": [{"_score": {"order": "desc"}}],
"highlight": {
"pre_tags": ["<font style='color:red;font-size:20px'>"],
"post_tags": ["</font>"],
"fields": {
"title": {"type": "plain"},
"digest": {"type": "plain"},
}
}
}
res = es.search(index=search_index, body=body)
return res
四、 自定义分页器
虽然后面部分已经进行了分页,但是我们的前端并没有任何分页器,我们可以自制一个分页器:
在news下创建一个templatestags
文件夹,然后创建一个py文件写入即可
# news/templatestags/news_template
# -*- coding: utf-8 -*-
# @Auther:Summer
from django import template
register = template.Library()
@register.filter()
def page_bar(page):
page_list = []
# 左边
if page.number != 1:
page_list.append(1)
if page.number - 3 > 1:
page_list.append('...')
if page.number - 2 > 1:
page_list.append(page.number - 2)
if page.number - 1 > 1:
page_list.append(page.number - 1)
page_list.append(page.number)
# 右边
if page.paginator.num_pages > page.number + 1:
page_list.append(page.number + 1)
if page.paginator.num_pages > page.number + 2:
page_list.append(page.number + 2)
if page.paginator.num_pages > page.number + 3:
page_list.append('...')
if page.paginator.num_pages != page.number:
page_list.append(page.paginator.num_pages)
return page_list
五、设计前端视图
我们前端直接使用表单提交,而不使用js来辅助我们,因为这里需要一个分页器,当然也可以根据页面移动量来使用ajax自动访问后台。这里就直接使用post提交后台,然后从后台拿取数据。
{% extends 'base/base.html' %}
{% block title %}搜索{% endblock %}
{% load news_template %}
{% block link %}
<link rel="stylesheet" href="../../static/css/news/search.css">
{% endblock %}
{% block main_contain %}
<div class="main-contain ">
<!-- search-box start -->
<div class="search-box">
<form action="" style="display: inline-flex;">
<input type="search" placeholder="请输入要搜索的内容" name="q" class="search-control">
<input type="submit" value="搜索" class="search-btn">
</form>
<!-- 可以用浮动 垂直对齐 以及 flex -->
</div>
<!-- search-box end -->
<!-- content start -->
<div class="content">
{% if not show %}
<!-- search-list start -->
<div class="search-result-list">
<h2 class="search-result-title">
搜索结果 <span style="font-weight: 700;color: #ff6620;">{{ paginator.num_pages }}</span>页
</h2>
<ul class="news-list">
{% for one_news in page %}
<li class="news-item clearfix">
<a href="{% url 'news:news_detail' one_news.id %}" class="news-thumbnail" target="_blank">
<img src="{{ one_news.image_url }}">
</a>
<div class="news-content">
<h4 class="news-title">
<a href="{% url 'news:news_detail' one_news.id %}">
{{ one_news.title }}
</a>
</h4>
<p class="news-details">{{ one_news.digest|safe }}</p>
{# <div class="news-other">#}
{# <span class="news-type">{{ one_news.object.tag.name }}</span>#}
{# <span class="news-time">{{ one_news.object.update_time }}</span>#}
{# </div>#}
</div>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<div class="news-contain">
<div class="hot-recommend-list">
<h2 class="hot-recommend-title">热门推荐</h2>
<ul class="news-list">
{% for one_hotnews in page.object_list %}
<li class="news-item clearfix">
<a href="#" class="news-thumbnail">
<img src="{{ one_hotnews.news.image_url }}">
</a>
<div class="news-content">
<h4 class="news-title">
<a href="{% url 'news:news_detail' one_hotnews.news.id %}">{{ one_hotnews.news.title }}</a>
</h4>
<p class="news-details">{{ one_hotnews.news.digest }}</p>
<div class="news-other">
<span class="news-type">{{ one_hotnews.news.tag.name }}</span>
<span class="news-time">{{ one_hotnews.update_time }}</span>
<span class="news-author">{{ one_hotnews.news.author.username }}</span>
</div>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<!-- search-list end -->
<!-- news-contain start -->
{# 分页导航 #}
<div class="page-box" id="pages">
<div class="pagebar" id="pageBar">
<a class="a1">{{ paginator.num_pages | default:0 }}条</a>
{# 上一页的URL地址#}
{% if page.has_previous %}
{% if kw %}
<a href="{% url 'news:search' %}?q={{ kw }}&page={{ page.previous_page_number }}&q={{ kw }}"
class="prev">上一页</a>
{% else %}
<a href="{% url 'news:search' %}?page={{ page.previous_page_number }}" class="prev">上一页</a>
{% endif %}
{% endif %}
{# 列出所有的URL地址 页码#}
{% if page.has_previous or page.has_next %}
{% for n in page|page_bar %}
{% if kw %}
{% if n == '...' %}
<span class="point">{{ n }}</span>
{% else %}
{% if n == page.number %}
<span class="sel">{{ n }}</span>
{% else %}
<a href="{% url 'news:search' %}?page={{ n }}&q={{ kw }}">{{ n }}</a>
{% endif %}
{% endif %}
{% else %}
{% if n == '...' %}
<span class="point">{{ n }}</span>
{% else %}
{% if n == page.number %}
<span class="sel">{{ n }}</span>
{% else %}
<a href="{% url 'news:search' %}?page={{ n }}">{{ n }}</a>
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{# next_page 下一页的URL地址#}
{% if page.has_next %}
{% if kw %}
<a href="{% url 'news:search' %}?q={{ kw }}&page={{ page.next_page_number }}&q={{ kw }}"
class="next">下一页</a>
{% else %}
<a href="{% url 'news:search' %}?page={{ page.next_page_number }}" class="next">下一页</a>
{% endif %}
{% endif %}
</div>
</div>
<!-- news-contain end -->
</div>
<!-- content end -->
</div>
{% endblock %}
六、路由
# -*- coding: utf-8 -*-
# @Author : summer
from django.urls import path
from . import views
app_name = "news"
urlpatterns = [
path("search/", views.SearchView.as_view(), name="search")
]