基于django+elasticsearch的全文检索

本文详细介绍如何在Django项目中集成Elasticsearch实现高效全文检索,包括配置haystack框架、索引创建、数据映射、批量写入、搜索接口设计及高级功能如高亮显示、排序、分页和搜索建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基于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建立的字段,都可以借助 HaystackElasticsearch 搜索引擎查询。
  • 其中 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&amp;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”,的属性具体有三,analyzednot_analyzedfalse/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",
                            }
                        }
                    }
                }
             }
         }
      }

至此,就结束了,有点突然的结束哈!就是写累了。😴希望大家给个赞呢!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值