第 11 篇:基于 drf-haystack 的文章搜索插口
优采云 发布时间: 2020-08-26 00:20第 11 篇:基于 drf-haystack 的文章搜索插口
作者:HelloGitHub-追梦人物
在 django 博客教程中,我们使用了 django-haystack 和 Elasticsearch 进行文章内容的搜索。django-haystack 默认返回的搜索结果是一个类似于 django QuerySet 的对象,需要配合模板系统使用,因为未被序列化,所以未能直接用于 django-rest-framework 的插口。当然解决方案也很简单,编写相应的序列化器将返回结果序列化就可以了。
但是,通过之前的功能我们看见,使用 django-rest-framework 是一个近乎标准化但又沉闷无聊的过程:首先是编撰序列化器用于序列化资源,然后是编撰视图集,提供对资源各种操作的插口。既然是标准化的东西,肯定早已有人写好了相关的功能以供复用。此时就要发挥开源社区的力量,去 GitHub 使用关键词 rest haystack 搜索,果然搜到一个 drf-haystack 开源项目,专门用于解决 django-rest-framework 和 haystack 结合使用的问题。因此我们就不再重复造轮子,直接使用开源第三方库来实现我们的需求。
既然要使用第三方库,第一步其实是安装它,进入项目根目录,运行:
$ pipenv install drf-haystack
由于须要使用到搜索功能,因此须要启动 Elasticsearch 服务,最简单的方法就是使用项目中编排的 Elasticsearch 镜像启动容器。
项目根目录下运行如下命令启动全部项目所需的容器服务:
$ docker-compose -f local.yml up --build
启动完成后运行 docker ps 命令可以检测到如下 2 个运行的容器,说明启动成功:
hellodjango_rest_framework_tutorial_local
hellodjango_rest_framework_tutorial_elasticsearch_local
接着创建一些文章,以便用于搜索测试,可以自己在 admin 后台添加,当然最简单的方式是运行项目中的 fake.py 脚本,批量生成测试数据:
$ docker-compose -f local.yml run --rm
hellodjango.rest.framework.tutorial.local python -m scripts.fake
测试文章生成后,还要运行下边的命令给文章的内容创建索引,这样搜索引擎能够按照索引搜索到相应的内容:
注意
如果生成索引时见到如下错误:
elasticsearch.exceptions.ConnectionError: ConnectionError(: Failed to establish a new connection: [Errno -2] Name does not resolve) caused by: NewConnectionError(: Failed to establish a new connection: [Errno -2] Name does not resolve)
这是因为项目配置中 Elasticsearch 服务的 URL 配置出错造成,解决方式是步入 settings/local.py 配置文件中,将搜索设置改为下边的内容:
HAYSTACK_CONNECTIONS['default']['URL'] = ':9200/'
因为这个 URL 地址需和容器编排文件 local.yml 中指定的容器服务名一致 Docker 才能正确解析。
现在万事具备了,数据库中早已有了文章,搜索服务早已有了文章的索引,只须要等待客户端来进行查询,然后返回结果。所以接下来就步入到 django-rest-framework 标准开发流程:定义序列化器 -> 编写视图 -> 配置路由,这样一个标准的搜索插口就开发下来了。
先来定义序列化器,粗略过一遍 drf-haystack 官方文档[3],依葫芦画瓢创建文章(Post) 的 Serializer
根据官方文档的介绍,为了复用早已定义好用于序列化文章列表的序列化器,我们直接承继了 PostListSerializer,同时我们还混进了 HaystackSerializerMixin,这是 drf-haystack 的混入类,提供搜索结果序列化相关的功能。
另外内部类 Meta 同样承继 PostListSerializer.Meta,这样就无需重复定义序列化数组列表 fields。关键的地方在这个 search_fields,这个列表申明用于搜索的数组(通常都定义为索引数组),我们在上一部教程设置 django-haystack 时,文章的索引数组设置的名子叫 text,如果对这一块有疑问,可以简单回顾一下 Django Haystack 全文检索与关键词高亮[4] 中的内容。
然后编撰视图集,需承继 HaystackViewSet:
这个视图集特别简单,只须要通过类属性 index_models 声明须要搜索的模型,以及搜索结果的序列化器就行了,剩余的功能均由 HaystackViewSet 内部替我们实现了。
最后是在路由器中注册视图集,自动生成 URL 模式:
搞定了!一套标准化的 django-restful-framework 开发流程,不过大量工作已由 drf-haystack 在背后替我们完成,我们只写了极其少量的代码即实现了一套搜索插口。
来瞧瞧搜索疗效。我们启动 Docker 容器,在浏览器输入如下格式的 URL:
$ docker-compose -f local.yml run --rm hellodjango.rest.framework.tutorial.local python manage.py rebuild_index# 输出如下Your choices after this are to restore from backups or rebuild via the `rebuild_index` command.Are you sure you wish to continue? [y/N] yRemoving all documents from your index because you said so.All documents removed.Indexing 201 文章GET /hellodjango_blog_tutorial/_mapping [status:404 request:0.005s]
将 key-word 替换为须要搜索的关键字,例如将其替换为 markdown,测试集数据中得到的搜索结果如下:
搜索结果符合预期,但略微有一点不太好的地方,就是没有高亮的标题和摘要,我们希望将来显示的结果应当是下边这样的,因此返回的数据必须支持这样的显示:
关键词高亮的实现原理似乎十分简单,通过解析整段文本,将搜索关键词替换为由 HTML 标签包裹的富文本,并给这个包裹标签设置 CSS 样式,让其显示不同的字体颜色就可以了。
了解其原理后其实就是实现其功能,不过 django-haystack 已经为我们造好了轮子,而且在上一部教程的 Django Haystack 全文检索与关键词高亮[5],我们还对默认的高亮辅助类进行了整修,优化了文章标题被从关键字位置截断的问题,因此我们使用改建后的辅助类来对须要高亮的结果进行处理。
需要高亮的虽然是 2 个数组,一个是 title、一个是 body。而 body 我们不需要完整的内容,只须要摘出其中一部分作为搜索结果的摘要即可。这两个功能,辅助类均早已为我们提供了,我们只须要调用所需的方式就行。
注意到这儿我们须要对 title、body 两个数组进行高亮处理,其基本逻辑虽然就是接收 title、body 的值作为输入,高亮处理后再输出。回顾一下序列化器的序列化数组,其实也是接收某个数组的值作为输入,对其进行处理,将其转化为可序列化的结果后输出,和我们须要的逻辑太象。但是,django-rest-framework 并没有提供这种比较个性化需求的序列化数组,因此接下来我们接触 drf 的一点中级用法——自定义序列化数组。
自定义序列化数组虽然十分的简单,基本流程分两步走:
从 drf 官方提供的序列化数组中找一个数据类型最为接近的作为父类。重写 to_representation 方法,加入自己的序列化逻辑。
以我们的需求为例。因为 title、body 均为字符型,因此选择父类序列化数组为 CharField,定义一个 HighlightedCharField 字段如下:
django-rest-framework 通过调用序列化数组的 to_representation 方法对输入的值进行序列化,这个方式接收的第一个参数就是须要序列化的值。在我们自定义的逻辑中,首先调用父类 CharField 的 to_representation 方法,父类序列化的逻辑是将任何输入的值都转为字符串;接着我们从 context 属性中取得 request 对象,这个对象就是视图中的 HTTP 请求对象,但是由于 django 中 request 对象难以象 flask 那样从全局获取,因此 drf 在视图中将其保存在了序列化器和序列化数组的 context 属性中便于在视图外访问;获取 request 对象的目的是希望获取查询的关键字,query_params 属性是一个类字典对象,用于记录来自 URL 的查询参数,例如我们之前测试查询功能时调用的 URL 为 /api/search/?text=markdown,所以 query_params 保存了 URL 中的查询参数,将其封装为一个类数组对象 {"text": "markdown"},这里 text 的值就是查询的关键字,我们将它传给 Highlighter 辅助类,然后调用 highlight 方法将须要序列化的值进行进一步的高亮处理。
序列化数组定义好后,我们就可以在序列化器中用它了:
title 字段原先使用默认的 CharField 进行序列化,这里我们重新指定为自定义的 HighlightedCharField,这样序列化后的值就是高亮的格式。
summary 是我们新增的数组,注意我们序列化的对象是文章 Post,但这个对象是没有 summary 这个属性的,但是 summary 其实是对属性 body 序列化后的结果,因此我们通过指定序列化化数组的 source 参数,指定值的来源。
最后别忘了在 fields 中声明全部序列化的数组,主要是把新增的 summary 加进去。
来瞧瞧改进后的搜索疗效:
注意观察返回的 title 和 summary,我们搜索的关键词是 markdown,可以看见所有 markdown 关键字都被包裹了一个 span 标签,并且设置了 class 属性为 highlighted,只要设置好 css 样式,页面所有的 markdown 关键词都会显示不同的颜色,从而实现搜索关键词高亮的疗效了。
当然,我们如今并没有实际用到这个特点,下一部教程我们将使用 Vue 来开发博客,到时候调用搜索插口领到搜索结果后才会实际用到了。