Django+Elasticsearch完成搜索功能(含高亮)

本文详细介绍了如何使用Django和Elasticsearch创建一个实时搜索功能,包括索引创建、数据映射、高亮显示和排序。通过实例展示了如何在后端实现基本搜索视图、自定义搜索函数和前端展示,适合初学者理解和实践。
摘要由CSDN通过智能技术生成

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”,的属性具体有三,analyzednot_analyzedfalse/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 }}&amp;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 }}&amp;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")
]

七、效果展示

在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值