集成Elasticsearch到django restful

文章目录

集成ES到django restful服务端项目

如果直接在Django项目直接编写代码作为ElasticSearch的客户端,比较复杂,所以借助第三方包Haystack来对接ELasticSearch的客户端。而且使用了Haystack后,以后你换其他的全文搜索服务器时,也不用修改Django项目已经写好的代码。

安装haystack

Haystack ,Django ,Elasticsearch 三者之间的关系是:

  1. Haystack 作为 Django 的一个插件,提供了一个 Django 应用接口来实现搜索功能。
  2. Elasticsearch 作为 Haystack 支持的搜索引擎之一,可以被 Haystack 用来作为后端搜索引擎来存储和检索数据。
  3. 当你在 Django 项目中使用 Haystack 并选择 Elasticsearch 作为搜索引擎时,Haystack 会作为中间层,让你能够通过 Django 的视图和模板来操作 Elasticsearch,实现全文搜索的功能。

简单来说,Haystack 为 Django 提供了搜索功能的抽象层,而 Elasticsearch 是这个抽象层背后的具体实现之一。通过 Haystack,你可以在 Django 项目中轻松地实现强大的搜索功能。

haystack是django的开源搜索框架,能够结合目前市面上大部分的搜索引擎用于实现自定义搜索功能,特别是全文搜索。

haystack支持多种搜索引擎,不仅仅是 jieba ,whoosh,使用solr、elasticsearch等搜索,也可通过haystack,而且直接切换引擎即可,甚至无需修改搜索代码。中文分词最好的就是jieba和elasticsearch+ik。

github: https://github.com/rhblind/drf-haystack

bash 复制代码
# python操作elasticsearch的模块,注意对应版本,类似pymysql
pip install -U elasticsearch==7.13.4
# django开发的haystack的模块,务必先安装drf`-haystack,接着才安装django-haystack。因为drf-haystack不支持es7
pip install -U drf-haystack
pip install -U django-haystack

基本使用

安装配置

文档:https://drf-haystack.readthedocs.io/en/latest/01_intro.html#examples

python 复制代码
INSTALLED_APPS = [
	# 必须在自己创建的子应用前面
	'haystack',

	# 自己创建的子应用
]

# haystack连接elasticsearch的配置信息
HAYSTACK_CONNECTIONS = {
    'default': {
        # haystack操作es的核心模块
        'ENGINE': 'haystack.backends.elasticsearch7_backend.Elasticsearch7SearchEngine',
        # es服务端地址
        'URL': 'http://127.0.0.1:9200/',
        # es索引仓库
        'INDEX_NAME': 'haystack',
    },
}

# 当mysqlORM操作数据库改变时,自动更新es的索引,否则es的索引会找不到新增的数据
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
索引模型

在courses子应用下创建search_indexes.py,用于设置es的索引模型。注意,索引模型的文件名必须是search_indexes。

  1. 类名必须为需要检索的Model_name+Index
  2. 每个索引里面必须有且只能有一个字段为 document=True,这代表haystack 和搜索引擎将使用此字段的内容作为索引进行检索(primary field)。其他的字段只是附属的属性,方便调用,并不作为检索数据。
  3. 如果使用一个字段设置了document=True,则一般约定此字段名为text,这是在SearchIndex类里面一贯的命名,以防止后台混乱,当然名字你也可以随便改,不过不建议改。
  4. haystack提供了use_template=True在text字段,这样就允许我们使用数据模板去建立搜索引擎索引的文件,说得通俗点就是索引里面需要存放一些什么东西
  5. text字段用于构造索引,只不过具体构造索引的值写在另一个文件内。
  6. id、title、digest、content、image_url等字段用于以索引查询到的返回内容。
  7. get_model方法用于指明建立索引的对应模型。
  8. index_queryset方法用于返回建立索引的数据查询集。
python 复制代码
from haystack import indexes
from .models import Course

class CourseIndex(indexes.SearchIndex, indexes.Indexable):
    # 全文索引[可以根据配置,可以包括多个字段索引]
    # document=True 表示当前字段为全文索引
    # use_template=True 表示接下来haystack需要加载一个固定路径的html模板文件,让text与其他索引字段绑定映射关系
    text = indexes.CharField(document=True, use_template=True)
    # 普通索引[单字段,只能提供单个字段值的搜索,所以此处的声明更主要是为了提供给上面的text全文索引使用的]
    # es索引名 = indexes.索引数据类型(model_attr="ORM中的字段名")
    id = indexes.IntegerField(model_attr="id")
    name = indexes.CharField(model_attr="name")
    description = indexes.CharField(model_attr="description")
    teacher = indexes.CharField(model_attr="teacher__name")
    course_cover = indexes.CharField(model_attr="course_cover")
    get_level_display=indexes.CharField(model_attr="get_level_display")
    students=indexes.IntegerField(model_attr="students")
    get_status_display=indexes.CharField(model_attr="get_status_display")
    lessons=indexes.IntegerField(model_attr="lessons")
    pub_lessons=indexes.IntegerField(model_attr="pub_lessons")
    price=indexes.DecimalField(model_attr="price")
    discount=indexes.CharField(model_attr="discount_json")
    orders=indexes.IntegerField(model_attr="orders")

    # 指定与当前es索引模型对接的mysql的ORM模型
    def get_model(self):
        return Course

    # 当用户搜索es索引时,对应的提供的mysql数据集有哪些?
    def index_queryset(self, using=None):
        return self.get_model().objects.filter(is_deleted=False,is_show=True)
ORM模型中新增discount_json字段方法

courses.models,代码:

python 复制代码
import json



class Course(BaseModel):
    course_type = (
        (0, '付费购买'),
        (1, '会员专享'),
        (2, '学位课程'),
    )
    level_choices = (
        (0, '初级'),
        (1, '中级'),
        (2, '高级'),
    )
    status_choices = (
        (0, '上线'),
        (1, '下线'),
        (2, '预上线'),
    )
    # course_cover = models.ImageField(upload_to="course/cover", max_length=255, verbose_name="封面图片", blank=True, null=True)
    course_cover = StdImageField(variations={
        'thumb_1080x608': (1080, 608),   # 高清图
        'thumb_540x304': (540, 304),    # 中等比例,
        'thumb_108x61': (108, 61, True),  # 小图(第三个参数表示保持图片质量),
    }, max_length=255, delete_orphans=True, upload_to="course/cover", null=True, verbose_name="封面图片",blank=True)

    course_video = models.FileField(upload_to="course/video", max_length=255, verbose_name="封面视频", blank=True, null=True)
    course_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型")
    level = models.SmallIntegerField(choices=level_choices, default=1, verbose_name="难度等级")
    description = RichTextUploadingField(null=True, blank=True, verbose_name="详情介绍")
    pub_date = models.DateField(auto_now_add=True, verbose_name="发布日期")
    period = models.IntegerField(default=7, verbose_name="建议学习周期(day)")
    attachment_path = models.FileField(max_length=1000, blank=True, null=True, verbose_name="课件路径")
    attachment_link = models.CharField(max_length=1000, blank=True, null=True, verbose_name="课件链接")
    status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")
    students = models.IntegerField(default=0, verbose_name="学习人数")
    lessons = models.IntegerField(default=0, verbose_name="总课时数量")
    pub_lessons = models.IntegerField(default=0, verbose_name="已更新课时数量")
    price = models.DecimalField(max_digits=10,decimal_places=2, verbose_name="课程原价",default=0)
    recomment_home_hot = models.BooleanField(default=False, verbose_name="是否推荐到首页新课栏目")
    recomment_home_top = models.BooleanField(default=False, verbose_name="是否推荐到首页必学栏目")
    direction = models.ForeignKey("CourseDirection", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="学习方向")
    category = models.ForeignKey("CourseCategory", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="课程分类")
    teacher = models.ForeignKey("Teacher", related_name="course_list", on_delete=models.DO_NOTHING, null=True, blank=True, db_constraint=False, verbose_name="授课老师")

    class Meta:
        db_table = "fg_course_info"
        verbose_name = "课程信息"
        verbose_name_plural = verbose_name

    def __str__(self):
        return "%s" % self.name

    def course_cover_small(self):
        if self.course_cover:
            return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_108x61.url}">')
        return ""

    course_cover_small.short_description = "封面图片(108x61)"
    course_cover_small.allow_tags = True
    course_cover_small.admin_order_field = "course_cover"

    def course_cover_medium(self):
        if self.course_cover:
            return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_540x304.url}">')
        return ""

    course_cover_medium.short_description = "封面图片(540x304)"
    course_cover_medium.allow_tags = True
    course_cover_medium.admin_order_field = "course_cover"

    def course_cover_large(self):
        if self.course_cover:
            return mark_safe(f'<img style="border-radius: 0%;" src="{self.course_cover.thumb_1080x608.url}">')
        return ""

    course_cover_large.short_description = "封面图片(1080x608)"
    course_cover_large.allow_tags = True
    course_cover_large.admin_order_field = "course_cover"

    @property
    def discount(self):
        # todo 将来通过计算获取当前课程的折扣优惠相关的信息
        import random
        return {
            "type": ["限时优惠","限时减免"].pop(random.randint(0,1)), # 优惠类型
            "expire": random.randint(100000, 1200000),  #  优惠倒计时
            "price": float(self.price - random.randint(1,10) * 10),  # 优惠价格
        }

    def discount_json(self):
        # 必须转成字符串才能保存到es中。所以该方法提供给es使用的。
        return json.dumps(self.discount)
全文索引字段模板

全文索引模板必须先配置django项目中的TEMPLATES模板引擎路径,而且全文索引模板的路径必须是模板目录下的search/indexes/子应用目录名/模型类名_text.txt。否则报错。settings.dev,代码:

python 复制代码
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            BASE_DIR / "templates",  # BASE_DIR 是apps的父级目录,是主应用目录,templates需要手动创建
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

创建全文索引字段的html模板,在HTML模板中采用django的模板语法,绑定text与其他es单字段索引的映射关系。

注意:course_text.txt 中course就是ORM模型类名小写,text就是es索引模型类中的全文索引字段名。

templates/search/indexes/courses/course_text.txt。代码:

python 复制代码
{{ object.name }}
{{ object.description }}
{{ object.teacher.name }}
{{ object.category.name }}
{{ object.diretion.name }}

object表示当前orm的模型对应。

索引序列化器

courses.serializers,代码:

python 复制代码
from drf_haystack.serializers import HaystackSerializer
from .search_indexes import CourseIndex
from django.conf import settings

class  CourseIndexHaystackSerializer(HaystackSerializer):
    """课程搜索的序列化器"""
    class Meta:
        index_classes = [CourseIndex]
        fields = ["text", "id", "name", "course_cover", "get_level_display", "students", "get_status_display", "pub_lessons", "price", "discount", "orders"]

    def to_representation(self, instance):
        """用于指定返回数据的字段的"""
        # 课程的图片,在这里通过elasticsearch提供的,所以不会提供图片地址左边的域名的。因此在这里手动拼接
        instance.course_cover = f'//{settings.OSS_BUCKET_NAME}.{settings.OSS_ENDPOINT}/uploads/{instance.course_cover}'
        return super().to_representation(instance)
全文搜索的索引视图
python 复制代码
from drf_haystack.viewsets import HaystackViewSet
from drf_haystack.filters import HaystackFilter
from .serializers import CourseIndexHaystackSerializer
from .models import Course

class CourseSearchViewSet(HaystackViewSet):
    """课程信息全文搜索视图类"""
    # 指定本次搜索的最终真实数据的保存模型
    index_models = [Course]
    serializer_class = CourseIndexHaystackSerializer
    filter_backends = [OrderingFilter, HaystackFilter]
    ordering_fields = ('id', 'students', 'orders')
    pagination_class = CourseListPageNumberPagination
路由
python 复制代码
from django.urls import path,re_path
from . import views

from rest_framework import routers
router = routers.DefaultRouter()
# 注册全文搜索到视图集中生成url路由信息
router.register("search", views.CourseSearchViewSet, basename="course-search")

urlpatterns = [
    path("directions/", views.CourseDirectionListAPIView.as_view()),
    re_path("^categories/(?P<direction>\d+)/$", views.CourseCategoryListAPIView.as_view()),
    re_path("^(?P<direction>\d+)/(?P<category>\d+)/$", views.CourseListAPIView.as_view()),
] + router.urls
手动构建es索引

因为此前mysql中已经有了部分的数据,而这部分数据在es中是没有创建索引。所以需要先把之前的数据同步生成全文索引。在终端下执行以下命令

bash 复制代码
# 重建索引
python manage.py rebuild_index

# 更新索引
# python manage.py update_index --age=<num_hours>

# 删除索引
# python manage.py clear_index

访问

http://api.fuguang.cn:8000/courses/search/?text=入门

http://api.fuguang.cn:8000/courses/search/?text=李老师

若有错误与不足请指出,关注DPT一起进步吧!!!

相关推荐
Mitch3111 小时前
【漏洞复现】CVE-2015-3337 Arbitrary File Reading
elasticsearch·网络安全·docker·漏洞复现
Mitch3111 小时前
【漏洞复现】CVE-2015-5531 Arbitrary File Reading
web安全·elasticsearch·网络安全·docker·漏洞复现
凡人的AI工具箱2 小时前
每天40分玩转Django:Django国际化
数据库·人工智能·后端·python·django·sqlite
喝醉酒的小白3 小时前
Elasticsearch(ES)监控、巡检及异常指标处理指南
大数据·elasticsearch·搜索引擎
孤水寒月5 小时前
Git忽略文件.gitignore
git·elasticsearch
LKAI.12 小时前
搭建Elastic search群集
linux·运维·elasticsearch·搜索引擎
isSamle19 小时前
使用Vue+Django开发的旅游路书应用
前端·vue.js·django
it噩梦1 天前
elasticsearch中使用fuzzy查询
elasticsearch
╰つ゛木槿1 天前
Spring Boot与Django对比:哪个更适合做为Web服务器框架?
前端·spring boot·django
Null箘1 天前
从零创建一个 Django 项目
后端·python·django