集成Elasticsearch到django restful
文章目录
- 集成ES到django restful服务端项目
- 安装haystack
- 基本使用
- 安装配置
- 索引模型
- ORM模型中新增discount_json字段方法
- 全文索引字段模板
- 索引序列化器
- 全文搜索的索引视图
- 路由
- 手动构建es索引
集成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=李老师
若有错误与不足请指出,关注DPT一起进步吧!!!