基于django+elasticsearch的全文检索
关于django和elasticsearch,就不做过多的介绍了(内容太多啦,哈哈哈哈)。直接进入正题。
一、django+haystack+elasticsearch
了解过django+elasticsearch全文检索的,可能还听说过python另外一个搜索框架:haystack,这是python里面自带的一个搜索引擎,通常情况下可以满足一些基本的需求,相对来说功能也是比较强大的。当然haystack还有一个作用就是接入第三方的搜索引擎elasticsearch,solr,whoosh等。具体可能就是需要对settings里面进行安装和配置:
一、安装
$ pip install django-haystack # 官方支持弹性1.x、2.x和5.x,所以es版本不能太高
$ pip install elasticsearch==x.x.x #这里安装的版本需要注意了,一定是比服务器版本的es低的,根据自己部署的es版本来下载客户端
二、应用注册和配置
#应用注册
INSTALLED_APPS = [
# 全文检索
'haystack',
]
# Haystack接入Elasticsearch
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
'URL': 'http://xxx.xxx.xxx.xxx:9200/', # Elasticsearch服务器ip地址,端口号固定为9200
'INDEX_NAME': 'XXX', # Elasticsearch建立的索引库的名称
},
}
#这俩个配置就是属于后期配置了
# 当添加、修改、删除数据时,自动生成索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# 用于决定每页显示数据条数:
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 5
三、路由配置
# Haystack 注册
url(r'^search/', include('haystack.urls')),
四、数据库配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql', # 数据库引擎
'HOST': '127.0.0.1', # 数据库主机
'PORT': 3306, # 数据库端口
'USER': 'xxx', # 数据库用户名
'PASSWORD': 'xxx', # 数据库用户密码
'NAME': 'xxx' # 数据库名字
},
}
五、 Haystack 建立数据索引
1.创建索引类
- 通过创建索引类,来指明让搜索引擎对哪些字段建立索引,也就是可以通过哪些字段的关键字来检索数据。
- 在项目中对 需要进行信息全文检索的表进行建立索引类,所以在 对应的应用中新建
search_indexes.py
文件,用于存放索引类。这个文件名是固定的哦!
from haystack import indexes
from es.models import EveryoneDemo
class File_SearchIndex(indexes.SearchIndex, indexes.Indexable):
"""索引数据模型类"""
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
"""返回建立索引的模型类"""
return EveryoneDemo
# 针对哪些数据进行查询
def index_queryset(self, using=None):
"""返回要建立索引的数据查询集"""
#return self.get_model().objects.filter()
return self.get_model().objects.all()
索引类 SKUIndex 说明:
- 在 EveryoneDemo建立的字段,都可以借助
Haystack
由Elasticsearch
搜索引擎查询。 - 其中
text
字段我们声明为document=True
,表名该字段是主要进行关键字查询的字段。 text
字段的索引值可以由多个数据库模型类字段组成,具体由哪些模型类字段组成,我们用use_template=True
表示后续通过模板来指明。
创建 text 字段索引值模板文件
- 在
templates
目录中创建text 字段
使用的模板文件 - 具体在
templates/search/indexes/es/everyonedemo_text.txt
文件中定义
{{ object.字段名 }}
{{ object.字段名 }}
{{ object.字段名 }}
....
模板文件说明:当将关键词通过text参数名传递时
- 此模板指明 EveryoneDemo 的某三个字段作为
text
字段的索引值来进行关键字索引查询。 - es是我的应用名,everyonedemo_text.txt,是模型类名小写_text.txt这是固定的,不要乱改
#生成模型文件和进行数据迁移
python manage.py makemigrations
python manage.py migrate
#手动生成索引
python manage.py rebuild_index
前端部分就是直接根据路"/search/"去提交搜索咯,这里就不说了。
六、前端部分的补充
上面有介绍分页,在这就补充一下代码(有点乱啊哈哈哈哈)
准备搜索页分页器
<div class="search_list">
<ul class="search_result_list">
{% if query %}
{% for result in page %}
<div class="search_res">
{# <a href="file:///{{ result.object.full_path }}" id="a1">#}
<li class="li_style">
文件名:{% highlight result.object.file_name with query %}<br>
{# 标签名:{% highlight result.object.F_Key with query %}<br>#}
提交时间:{{ result.object.ctime }}<br>
文件详情:{% highlight result.object.text_info with query %}<br>
<a href="{{ result.object.full_path }}">连接地址</a><br>
</li>
</div>
{% empty %}
<p>没有找到你要信息</p>
{% endfor %}
{% else %}
请输入搜索关键词,例如 10月5日文件,可用空格进行分词搜索!
{% endif %}
</ul>
<div class="pagenation">
<div id="pagination" class="page"></div>
</div>
</div>
<script type="text/javascript" src="{% static 'js/jquery.pagination.min.js' %}"></script>
<script>
$(function () {
$('#pagination').pagination({
currentPage: {{ page.number }},
totalPage: {{ paginator.num_pages }},
callback: function (current) {
{#window.location.href = '/search/?q=iphone&page=1';#}
window.location.href = '/search/?q={{ query }}&page=' + current;
}
})
});
</script>
然后就没了!!!!
二、django+elasticsearch
呵呵。。好像跟上一段没啥区别哦,所以上述不是重点,就是废话,哈哈哈哈。简单使用ES的话,上述就够了,有点ES基础得可以继续学习咯!因为不用haystack这个框架,很多繁杂得事情需要我们自己去处理了。个人经验告诉我,不用这个框架写起来更舒服,哈哈哈哈。
还是一个django项目,并进行配置,数据库配置同上,然后就是应用的注册。这一次es就不用配置了。
然后就是直接进行创建索引映射,上代码:
一、索引的创建与映射
def es2(request):
es = Elasticsearch(['http://xxx.xxx.xxx.xxx:9200/'])
# "analyzer": "ik_smart"
body = {
"mappings": {
"doc": {
"properties": {
"file_name": {
"type": "text",
"analyzer": "ik_smart"
},
"full_path": {
"type": "text",
"index":"false"
},
"ctime": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
},
"text_info": {
"type": "text",
"analyzer": "ik_smart"
},
}
}
},
"settings": {
"number_of_shards": 2, #分片数
"number_of_replicas": 0 #副本数
},
}
es.indices.create(index='索引名', body=body)
return HttpResponse('ok')
-
分片数和副本数设计的作用就是提高我们的查询速度,如果查询的数据很大的话分片数就增加,默认是5个分片,当然我用不了那么多,大概了解了一下如果你有100G的数据,大概就给5个分片左右,以此类推。副本数,就等于是一个备份数据一样,当然我用不到,其次就是,单个服务器就是0哦,es集群可以考虑副本。
-
如果你的es中配置了ik中文分词器呢,在建立映射的时候可以指定指定进行ik分词:“analyzer”: “ik_smart”,不指定的话默认是官方的简单分词器。也可以指定keyword类型,是不分词的,具体场景下,某些词,确实不分词的好,“index”,的属性具体有三,analyzed、not_analyzed、false/no, analyzed:表示该字段被分析,编入索引,产生的token能被搜索到;not_analyzed:表示该字段不会被分析,使用原始值编入索引,在索引中作为单个词;no:不编入索引,无法搜索该字段;
-
时间类型的字段,这里用的是:“format”: "strict_date_optional_time||epoch_millis"如:“2020-01-01T1:10:30Z” 还可以是yyyy-MM-dd,yyyy-MM-dd mm:ss等等格式 。当然还有很多复杂的数据类型,大家可以自行了解。
-
这个创建索引映射的函数我是写在view视图中的,这里按照大家的喜欢来写吧,es的东西太多了,可以单独去学习一下。
-
如果想看到自己创建的索引,可以去下载安装elasticsearch-head-master,需要的环境很复杂,可参考: https://www.cnblogs.com/hualess/p/11540477.html 博主大哥写的特别详细。
-
关于es的用法可见:https://elasticsearch-py.readthedocs.io/en/master/api.html 。dsl的可见:https://elasticsearch-dsl.readthedocs.io/en/latest/,这里针对的是python,es关于python的博客太少了。
二、helpers.bulk写入数据
def es2(request):
.....
query_obj = models.模型类名.objects.all()
#
action = [
{
"_index": "上面创建的索引名",
"_type": "doc",
"_source": {
"file_name": i.file_name,
"full_path": i.full_path,
"ctime": i.ctime,
"text_info": i.text_info
}
} for i in query_obj]
# 批量写入数据
helpers.bulk(es, action, request_timeout=1000)
return HttpResponse('ok')
- 批量模型类里面指定好的数据库表,我这里用的是MySQL,SQL server也是可以的哦。这里也可以直接通过上传es,不通过python。具体做法,网上很丰富的呢!
三、搜索接口设计
懒得讲解了就简单粗暴一些吧(哈哈哈哈),有不懂可以私聊咯:
1、前端js部分:
function show(page) {
var searchMsg = $('#searchMsg').val();
$.ajax({
url: "/everyone/",
type: "POST",
data: {"search_msg": searchMsg},
success: function (data) {
if (data) {
//显示结果条数
$('#totalNum').html("<b style='color:red'>约" + data.hits.total + "条</b>");
//结果展示
var html = '';
$.each(data.data_msg, function (index, item) {
html +=
'<div class = "res_show">\n' +
'<h3>' + item._source.file_name + '</h3>\n' +
'<p>' + item._source.ctime + '</p>' +
'<p>' + item._source.text_info + '</p>\n' +
'</div>';
});
$('#showData').html(html);
}
},
})
}
- ajax发送请求给后端,并传递搜索的关键字参数searchMsg,后端进行es的搜索。
- 返回的结果进行HTML页面字符串拼接。在放入我们指定的showData的div中。
2、html部分
<div class="search_test">
<input type="text" class="input_text" name="q" placeholder="搜索文件" id="searchMsg">
<input type="submit" class="input_btn1" name="" value="搜索" οnclick="show(page)">
<span id="totalNum"></span>
</div>
3、路由部分
urlpatterns = [
path('everyone/', views.everyone, name="everyone"),
]
4、视图函数部分
def everyone(request):
if request.method == "POST":
search_msg = request.POST.get("search_msg")
res = filter_msg(search_msg, "everyone_all")
return JsonResponse(res)
else:
return render(request, "everyone.html")
- 通过前端的ajax请求拿到search_msg,并通过filter_msg函数进行搜索。
5、es的搜索部分
def filter_msg(search_msg, search_data):
es = Elasticsearch(['http://xxx.xxx.xxx.xxx:9200/'])
body = {
"size": 200,
"query": {
"bool": {
"should": [
{
"match": {
"file_name": search_msg,
}
},
{
"match": {
"text_info": search_msg,
}
},
]
}
},
}
res = es.search(index=search_data, body=body)
return res
- es搜索方式很多哦,这里也是最常用的bool综合类型的查询,其关键方法有should,must,not must具体用法很多。也比较普遍。其他查询方式贼多。
四、关键字高亮显示
关键字高亮是一个搜索引擎很常见也很必要的功能,我的简单理解就是将查询后的结果进行渲染,拿我们的HTML标签将关键字包裹起来,在进行颜色的渲染,所以就是将搜索部分的函数filter_msg进行添加一个高亮的参数设置。
"highlight": {
"pre_tags": ["<font style='color:red;font-size:20px'>"],
"post_tags": ["</font>"],
"fields": {
"file_name": {"fragment_size": 20, "no_match_size": 10},
"text_info": {"fragment_size": 20, "no_match_size": 10},
}
}
- 前端部分就是将原先的_source,替换为highlight。
- ES的内置高亮分为三种:plain、postings、fast-vector-highlighter,考虑到各位的任性,ES也支持自定义标签高亮。良心啊!但是这三种高亮也是有特别之处的,例如fast-vector-highlighter,就是专门针对长文本出的高亮,总体来说,ES的搜索速度有一部分原因耽误在高亮渲染的过程上,也就是我们处理高亮的速度,fast-vector-highlighter:为解决 highlighter 高亮器质大文本字段上高亮速度跟不上的问题,lucene高亮模块提供了基于向量的高亮方式 fast-vector-highlighter(也称为fvh)。fvh高亮器利用建索引时候保存好的词向量来直接计算高亮段落,在高亮过程中比plain高亮方式少了实时分析过程,取而代之的是直接从磁盘中将分词结果直接读取到内存中进行计算。故要使用fvh的前置条件就是在建索引时候,需要配置存储词向量,词向量需要包含词位置信息、词偏移量信息。上述不是我说的!哈哈哈哈!这么专业的话。我不会!
五、排序搜索
同上啊,一样的思路。es语法中,排序就是指定一个sort的字段,并指明排序方式。这里我是按照时间进行降序。排序的接口肯定是单独写的,类比于点击搜索一样的思路。
"sort": [
{
"ctime": {"order": "desc",
}
}
]
根据业务场景来记录,多级排序得实现:_score就是es得评分机制,先按照文档分数排序,再根据时间排序,这个方式还是比较好的。
"sort": [
{"_score": {"order": "desc"}},
{"ctime": {"order": "desc", }},
]
六、搜索分页
这里因为没用haystack,所以自己得写一个分页器。底下是分页器得函数,当然这不是固定的,网上很多种分页器函数,可以自己参考一下。
class Pagination():
def __init__(self, current_page, all_count, per_num=8, max_show=10):
try:
# self.current_page = int(request.GET.get('page', 1))
self.current_page = int(current_page)
if self.current_page <= 0:
self.current_page = 1
except Exception as e:
self.current_page = 1
# 最多显示的页码数
self.max_show = max_show
half_show = max_show // 2
# 每页显示的数据条数
self.per_num = per_num
# 总数据量
self.all_count = all_count
# 总页码数
self.total_num, more = divmod(all_count, per_num)
if more:
self.total_num += 1
# 总页码数小于最大显示数:显示总页码数
if self.total_num <= max_show:
self.page_start = 1
self.page_end = self.total_num
else:
# 总页码数大于最大显示数,最多显示5个
if self.current_page <= half_show:
self.page_start = 1
self.page_end = max_show
elif self.current_page + half_show >= self.total_num:
self.page_end = self.total_num
self.page_start = self.total_num - max_show + 1
else:
self.page_start = self.current_page - half_show
self.page_end = self.current_page + half_show
@property
def start(self):
return (self.current_page - 1) * self.per_num
@property
def end(self):
return self.current_page * self.per_num
@property
def show_li(self):
# 存放li标签的列表
html_list = []
# first_li = '<li><a href="{}?page=1">首页</a></li>'.format(self.base_url)
# html_list.append(first_li)
if self.current_page == 1:
prev_li = '<li class="disabled"><a>上一页</a></li>'
else:
prev_li = '<li ><a href="#" page={}>上一页</a></li>'.format(self.current_page - 1)
html_list.append(prev_li)
for num in range(self.page_start, self.page_end + 1):
if self.current_page == num:
li_html = '<li class = "active"><a href="javascript:;" page={0}> {0}</a></li>'.format(num)
else:
li_html = '<li><a href="javascript:;" page={0}>{0}</a></li>'.format(num)
html_list.append(li_html)
if self.current_page == self.total_num:
next_li = '<li class="disabled"><a>下一页</a></li>'
else:
next_li = '<li><a href="javascript:;" page={}>下一页</a></li>'.format(self.current_page + 1)
html_list.append(next_li)
return ''.join(html_list)
- 这里的分页是与前文的搜索部分一样的接口,我这里是这么理解的。当我们传入,search_msg与current_page时,可以考虑当当前页默认为1时,肯定代表的就是搜索,当当前大于1时,就是所谓的翻页,在show_li中已经将这样的一个结果进行前端页面的字符串拼接,所以前端拿到之后可以直接渲染展示,作为一个后端程序员,即便是拼接,咱也得在后端进行拼接。哈哈哈哈!
七、搜索建议器
- 关于建议器,可分为三类,词条建议器:term suggest、词组建议器:phrase suggester、完成建议器:completion suggester,上下文建议器:context suggester,具体实现都差不多,前俩个类似于是一种纠错功能,第三种就是提供自动完成得功能。第四个就是对于第三种得完善,作为一个合格搜索引擎,了解用户想搜什么与纠正用户的错误还是很有必要的。(版本较低的es可能不适用,建议升级下版本5.X以上吧)
这里举例一个完成建议器:
def suggest(search_msg, search_data):
body = {
"suggest": {
"my_sugget": {
"text": search_msg,
"completion": {
"field": "subject.suggest",
"size": 5
}
}
}
}
try:
res = es.search(index=search_data, body=body)
res = [i['text'] for i in res['suggest']['my_sugget'][0]['options']]
except Exception as e:
pass
return res
如果想要做completion建议器,建立映射的时候,也要给这个字段加上completion 类型,才能使用哦!term ,phrase 很简单就可以实现,想要了解的可以自行百度。哈哈哈哈!
body = {
"mappings": {
"doc": {
"properties": {
"subject": {
"type": "text",
"fields": {
"suggest": {
"type": "completion",
# "analyzer": "standard",
}
}
}
}
}
}
}
至此,就结束了,有点突然的结束哈!就是写累了。😴希望大家给个赞呢!