全文搜索抽象化
正如我在本章的介绍中所说的,我希望能够轻松地从 Elasticsearch 切换到其他搜索引擎,并且我也不希望将此功能专门用于搜索用户动态,我更愿意设计一个可复用的解决方案,如果需要,我可以轻松扩展到其他模型。 出于所有这些原因,我决定将搜索功能 抽象化 。 我的想法是以通用条件来设计特性,所以不会假设 Post
模型是唯一需要编制索引的模型,也不会假设 Elasticsearch 是唯一选择的搜索引擎。 但是如果我不能对任何事情做出任何假设,我是不可能完成这项工作的!
我需要的做的第一件事,是找到一种通用的方式来指定哪个模型以及其中的某个或某些字段将被索引。 我设定任何需要索引的模型都需要定义一个 __searchable__
属性,它列出了需要包含在索引中的字段。 对于 Post 模型来说,变化如下:
app/models.py : 为 Post 模型添加一个__searchable__属性。
class Post(db.Model):
__searchable__ = ['body']
# ...
需要说明的是,这个模型需要有 body
字段才能被索引。 不过,为了清楚地确保这一点,我添加的这个 __searchable__
属性只是一个变量,它没有任何关联的行为。 它只会帮助我以通用的方式编写索引函数。
我将在 app/search.py 模块中编写与 Elasticsearch 索引交互的所有代码。 这么做是为了将所有 Elasticsearch 代码限制在这个模块中。 应用的其余部分将使用这个新模块中的函数来访问索引,而不会直接访问 Elasticsearch。 这很重要,因为如果有一天我不再喜欢 Elasticsearch 并想切换到其他引擎,我所需要做的就是重写这个模块中的函数,而应用将继续像以前一样工作。
对于本应用,我需要三个与文本索引相关的支持功能:我需要将条目添加到全文索引中,我需要从索引中删除条目(假设有一天我会支持删除用户动态),还有就是我需要执行搜索查询。 下面是 app/search.py 模块,它使用我在 Python 控制台中向你展示的功能实现 Elasticsearch 的这三个函数:
app/search.py : Search functions.
from flask import current_app
def add_to_index(index, model):
if not current_app.elasticsearch:
return
payload = {}
for field in model.__searchable__:
payload[field] = getattr(model, field)
current_app.elasticsearch.index(index=index, doc_type=index, id=model.id,
body=payload)
def remove_from_index(index, model):
if not current_app.elasticsearch:
return
current_app.elasticsearch.delete(index=index, doc_type=index, id=model.id)
def query_index(index, query, page, per_page):
if not current_app.elasticsearch:
return [], 0
search = current_app.elasticsearch.search(
index=index, doc_type=index,
body={'query': {'multi_match': {'query': query, 'fields': ['*']}},
'from': (page - 1) * per_page, 'size': per_page})
ids = [int(hit['_id']) for hit in search['hits']['hits']]
return ids, search['hits']['total']
这些函数都是通过检查 app.elasticsearch
是否为 None
开始的,如果是 None
,则不做任何事情就返回。 当 Elasticsearch 服务器未配置时,应用会在没有搜索功能的状态下继续运行,不会出现任何错误。 这都是为了方便开发或运行单元测试。
这些函数接受索引名称作为参数。 在传递给 Elasticsearch 的所有调用中,我不仅将这个名称用作索引名称,还将其用作文档类型,一如我在 Python 控制台示例中所做的那样。
添加和删除索引条目的函数将 SQLAlchemy 模型作为第二个参数。 add_to_index()
函数使用我添加到模型中的 __searchable__
变量来构建插入到索引中的文档。 回顾一下,Elasticsearch 文档还需要一个唯一的标识符。 为此,我使用 SQLAlchemy 模型的 id
字段,该字段正好是唯一的。 在 SQLAlchemy 和 Elasticsearch 使用相同的 id
值在运行搜索时非常有用,因为它允许我链接两个数据库中的条目。 我之前没有提到的一点是,如果你尝试添加一个带有现有 id 的条目,那么 Elasticsearch 会用新的条目替换旧条目,所以 add_to_index()
可以用于新建和修改对象。
在 remove_from_index()
中的 es.delete()
函数,我之前没有展示过。 这个函数删除存储在给定 id
下的文档。 下面是使用相同 id
链接两个数据库中条目的便利性的一个很好的例子。
query_index()
函数使用索引名称和文本进行搜索,通过分页控件,还可以像 Flask-SQLAlchemy 结果那样对搜索结果进行分页。 你已经从 Python 控制台中看到了 es.search()
函数的示例用法。 我在这里发布的调用非常相似,但不是使用 match
查询类型,而是使用 multi_match
,它可以跨多个字段进行搜索。 通过传递 *
的字段名称,我告诉 Elasticsearch 查看所有字段,所以基本上我就是搜索了整个索引。 这对于使该函数具有通用性很有用,因为不同的模型在索引中可以具有不同的字段名称。
es.search()
查询的 body
参数还包含分页参数。 from
和 size
参数控制整个结果集的哪些子集需要被返回。 Elasticsearch 没有像 Flask-SQLAlchemy 那样提供一个很好的 Pagination 对象,所以我必须使用分页数学逻辑来计算 from
值。
query_index()
函数中的 return
语句有点复杂。 它返回两个值:第一个是搜索结果的 id
元素列表,第二个是结果总数。 两者都从 es.search()
函数返回的 Python 字典中获得。 用于获取 ID 列表的表达式,被称为 列表推导式 ,是 Python 语言的一个奇妙功能,它允许你将列表从一种格式转换为另一种格式。 在本例,我使用列表推导式从 Elasticsearch 提供的更大的结果列表中提取 id
值。
这样看起来是否太混乱? 也许从 Python 控制台演示这些函数可以帮助你更好地理解它们。 在接下来的会话中,我手动将数据库中的所有用户动态添加到 Elasticsearch 索引。 在我的测试数据库中,我有几条用户动态中包含数字“one”,“two”, “three”, “four” 和“five”,因此我将其用作搜索查询。 你可能需要调整你的查询以匹配数据库的内容:
>>> from app.search import add_to_index, remove_from_index, query_index
>>> for post in Post.query.all():
... add_to_index('posts', post)
>>> query_index('posts', 'one two three four five', 1, 100)
([15, 13, 12, 4, 11, 8, 14], 7)
>>> query_index('posts', 'one two three four five', 1, 3)
([15, 13, 12], 7)
>>> query_index('posts', 'one two three four five', 2, 3)
([4, 11, 8], 7)
>>> query_index('posts', 'one two three four five', 3, 3)
([14], 7)
我发出的查询返回了七个结果。 当我以每页 100 项查询第 1 页时,我得到了全部的七项,但接下来的三个例子显示了我如何以与 Flask-SQLAlchemy 类似的方式对结果进行分页,当然,结果是 ID 列表而不是 SQLAlchemy 对象。
如果你想保持数据的清洁,可以在做实验之后删除 posts
索引:
>>> app.elasticsearch.indices.delete('posts')
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论