返回介绍

集成 SQLAlchemy 到搜索

发布于 2025-01-02 21:53:57 字数 5095 浏览 0 评论 0 收藏 0

我在前面的章节中给出的解决方案是可行的,但它仍然存在一些问题。 最明显的问题是结果是以数字 ID 列表的形式出现的。 这非常不方便,我需要 SQLAlchemy 模型,以便我可以将它们传递给模板进行渲染,并且我需要用数据库中相应模型替换数字列表的方法。 第二个问题是,这个解决方案需要应用在添加或删除用户动态时明确地发出对应的索引调用,这并非不可行,但并不理想,因为在 SQLAlchemy 侧进行更改时错过索引调用的情况是不容易被检测到的,每当发生这种情况时,两个数据库就会越来越不同步,并且你可能在一段时间内都不会注意到。 更好的解决方案是在 SQLAlchemy 数据库进行更改时自动触发这些调用。

用对象替换 ID 的问题可以通过创建一个从数据库读取这些对象的 SQLAlchemy 查询来解决。 这在实践中听起来很容易,但是使用单个查询来高效地实现它实际上有点棘手。

对于自动触发索引更改的问题,我决定用 SQLAlchemy 事件 驱动 Elasticsearch 索引的更新。 SQLAlchemy 提供了大量的 事件 ,可以通知应用程序。 例如,每次提交会话时,我都可以定义一个由 SQLAlchemy 调用的函数,并且在该函数中,我可以将 SQLAlchemy 会话中的更新应用于 Elasticsearch 索引。

为了实现这两个问题的解决方案,我将编写 mixin 类。 记得 mixin 类吗? 在 第五章 中,我将 Flask-Login 中的 UserMixin 类添加到了 User 模型,为它提供 Flask-Login 所需的一些功能。 对于搜索支持,我将定义我自己的 SearchableMixin 类,当它被添加到模型时,可以自动管理与 SQLAlchemy 模型关联的全文索引。 mixin 类将充当 SQLAlchemy 和 Elasticsearch 世界之间的“粘合”层,为我上面提到的两个问题提供解决方案。

让我先告诉你实现,然后再来回顾一些有趣的细节。 请注意,这使用了多种先进技术,因此你需要仔细研究此代码以充分理解它。

app/models.py :SearchableMixin 类。

from app.search import add_to_index, remove_from_index, query_index

class SearchableMixin(object):
    @classmethod
    def search(cls, expression, page, per_page):
        ids, total = query_index(cls.__tablename__, expression, page, per_page)
        if total == 0:
            return cls.query.filter_by(id=0), 0
        when = []
        for i in range(len(ids)):
            when.append((ids[i], i))
        return cls.query.filter(cls.id.in_(ids)).order_by(
            db.case(when, value=cls.id)), total

    @classmethod
    def before_commit(cls, session):
        session._changes = {
            'add': [obj for obj in session.new if isinstance(obj, cls)],
            'update': [obj for obj in session.dirty if isinstance(obj, cls)],
            'delete': [obj for obj in session.deleted if isinstance(obj, cls)]
        }

    @classmethod
    def after_commit(cls, session):
        for obj in session._changes['add']:
            add_to_index(cls.__tablename__, obj)
        for obj in session._changes['update']:
            add_to_index(cls.__tablename__, obj)
        for obj in session._changes['delete']:
            remove_from_index(cls.__tablename__, obj)
        session._changes = None

    @classmethod
    def reindex(cls):
        for obj in cls.query:
            add_to_index(cls.__tablename__, obj)

这个 mixin 类有四个函数,都是类方法。复习一下,类方法是与类相关联的特殊方法,而不是实例的。 请注意,我将常规实例方法中使用的 self 参数重命名为 cls ,以明确此方法接收的是类而不是实例作为其第一个参数。 例如,一旦连接到 Post 模型,上面的 search() 方法将被调用为 Post.search() ,而不必将其实例化。

search() 类方法封装来自 app/search.py 的 query_index() 函数以将对象 ID 列表替换成实例对象。你可以看到这个函数做的第一件事就是调用 query_index() ,并传递 cls .__tablename__ 作为索引名称。这将是一个约定,所有索引都将用 Flask-SQLAlchemy 模型关联的表名。该函数返回结果 ID 列表和结果总数。通过它们的 ID 检索对象列表的 SQLAlchemy 查询基于 SQL 语言的 CASE 语句,该语句需要用于确保数据库中的结果与给定 ID 的顺序相同。这很重要,因为 Elasticsearch 查询返回的结果不是有序的。如果你想了解更多关于这个查询的工作方式,你可以参考这个 StackOverflow 问题 的接受答案。 search() 函数返回替换 ID 列表的查询结果集,以及搜索结果的总数。

before_commit()after_commit() 方法分别对应来自 SQLAlchemy 的两个事件,这两个事件分别在提交发生之前和之后触发。 前置处理功能很有用,因为会话还没有提交,所以我可以查看并找出将要添加,修改和删除的对象,如 session.newsession.dirtysession.deleted 。 这些对象在会话提交后不再可用,所以我需要在提交之前保存它们。 我使用 session._changes 字典将这些对象写入会话提交后仍然存在的地方,因为一旦会话被提交,我将使用它们来更新 Elasticsearch 索引。

当调用 after_commit() 处理程序时,会话已成功提交,因此这是在 Elasticsearch 端进行更新的适当时间。 session 对象具有 before_commit() 中添加的_changes 变量,所以现在我可以迭代需要被添加,修改和删除的对象,并对 app/search.py 中的索引函数进行相应的调用。

reindex() 类方法是一个简单的帮助方法,你可以使用它来刷新所有数据的索引。 你看到我在上面做的将所有用户动态初始加载到测试索引中,这个操作与 Python shell 会话中的类似。 有了这个方法,我可以调用 Post.reindex() 将数据库中的所有用户动态添加到搜索索引中。

为了将 SearchableMixin 类整合到 Post 模型中,我必须将它作为 Post 的基类,并且还需要监听提交之前和之后的事件:

app/models.py :添加 SearchableMixin 类到 Post 模型。

class Post(SearchableMixin, db.Model):
    # ...

db.event.listen(db.session, 'before_commit', Post.before_commit)
db.event.listen(db.session, 'after_commit', Post.after_commit)

请注意, db.event.listen() 调用不在类内部,而是在其后面。 这两行代码设置了每次提交之前和之后调用的事件处理程序。 现在 Post 模型会自动为用户动态维护一个全文搜索索引。 我可以使用 reindex() 方法来初始化当前在数据库中的所有用户动态的索引:

>>> Post.reindex()

我可以通过运行 Post.search() 来搜索使用 SQLAlchemy 模型的用户动态。 在下面的例子中,我要求查询第一页的五个元素:

>>> query, total = Post.search('one two three four five', 1, 5)
>>> total
7
>>> query.all()
[<Post five>, <Post two>, <Post one>, <Post one more>, <Post one>]

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文