其它综合

关注公众号 jb51net

关闭
首页 > 网络编程 > 其它综合 > Elasticsearch集成django restful

如何集成Elasticsearch到django restful

作者:叫我DPT

在Django项目中集成Elasticsearch可通过Haystack实现,Haystack作为Django插件提供搜索接口,Elasticsearch作为后端搜索引擎存储检索数据,Haystack支持多种搜索引擎,易于切换且不需改动代码,本文给大家介绍如何集成Elasticsearch到django restful,感兴趣的朋友一起看看吧

集成ES到django restful服务端项目

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

安装haystack

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

  • Haystack 作为 Django 的一个插件,提供了一个 Django 应用接口来实现搜索功能。
  • Elasticsearch 作为 Haystack 支持的搜索引擎之一,可以被 Haystack 用来作为后端搜索引擎来存储和检索数据。
  • 当你在 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

# 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

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。

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

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,代码:

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。代码:

{{ object.name }}
{{ object.description }}
{{ object.teacher.name }}
{{ object.category.name }}
{{ object.diretion.name }}

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

索引序列化器

courses.serializers,代码:

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)

全文搜索的索引视图

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

路由

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中是没有创建索引。所以需要先把之前的数据同步生成全文索引。在终端下执行以下命令

# 重建索引
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=李老师

到此这篇关于集成Elasticsearch到django restful的文章就介绍到这了,更多相关Elasticsearch集成django restful内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文