天天看点

Django 基于 Postgres 的全文搜索 Django 基于 Postgres 的全文搜索 用 SearchVectorField 来储存向量 更新文档 结论

<b>本文讲的是Django 基于 Postgres 的全文搜索,</b>

<b></b>

不言自明,这次主要说的是 Django 和 Postgres 后端技术栈。在 SQLite 或者 MYSQL 中是不会有效的。我也认为你已经熟悉 Django 并且对 Postgres 有基本的了解。

我们将使用这些模型作为例子。这是一个类似博客的应用程序的简单数据,其中包括直接包含和通过关系引用数据的 Posts 。但是最重要的是,我们有了想要通过多对一关系( author ) 和 多对多关系( tag ) 查询的数据。

我们将会使用以下数据:

首先是为我们的 posts 创建文档。每一份文档在逻辑上都将代表一个 post ,包括

title

content

Author's name

All tag names

这里是 Django 查询的一个例子:

这包括了我们每篇文章实例的所有数据,字段数据间通过空格来分割。

每个文档都被统一到一组常用的词根。其中包括所有字母都切换到小写,去除通用的前缀和后缀(比如像英语中的 's' 和 'es'),并且移除掉像 'a','an' 和 'the' 这样的通用词汇。这个数据前面的数字表示词根在文档中的位置,后面的字母表示这个词根的比重。如果我们想要覆盖 Postgres 处理这些词汇的配置,比如说使用不同的语言,我们需要向查询向量传递一个额外的参数 config。如果没有声明这个配置, Postgres 将会使用数据库默认的配置,这样很可能基于其配置的 locale。

我们现在已经有了我们的文档,就可以执行一次查询啦。实现查询最简单的方式就是在我们的文档中筛选。

如果我们在 SearchVector() 中使用了自定义的 <code>config</code>,那么我们就应该在 SearchQuery() 中使用同样的 <code>config</code>。

考虑到我们为文档的每个部分分配了不同的权重,如果可以对返回的结果进行排序将会更有意义。Django 为此提供了 SearchRank 类。

Django 为我们提供了一个叫做 <code>SearchVectorField</code> 的字段来储存预先计算好的向量。我们将会把这个字段加入到我们的 Post 模型。

之后我们会执行 migrate 操作来添加这个字段。

让我们现在手工更新这个字段。

注意: 这将为表中的每一行触发一次UPDATE,如果我们的表有很多行,这过程将会持续很久很久。如果我们仅需要在文档中包含来自单个模型的字段,那么这么做会更有效率:

Django 并不允许我们使用带有 update 子句的集合函数,但是 Postgres 允许,所以如果我们真的想那么做的话,我们可以执行一次像这样的查询来一次性更新所有文档。

现在我们已经储存了我们的文档,我们就可以很简单的对它们进行查询

现在我们的文档是储存在一个字段中的,我们可以创建一个 GIN 索引来加快查询速度。在 Django 1.11 中,这简单到只需要为我们的模型添加一个 <code>indexes</code> Meta 选项,然后创建并执行 migrate 操作。

在 Django 1.10 中我们需要创建一个空的迁移并且添加上 <code>RunSQL</code> 操作。

目前为止是非常好的,但是一旦其中的任何数据发生改变,这个文档也就过期了,搜寻结果也将变得不正确。我们能够解决这个问题的第一个方法是使用一个 cron 或计划任务来定期更新整张表(如上所述)。这对于需要处理大量更新或者大批量更新的应用是个很好的选择。这样,我们就不需要为每一次更新增加额外的开销,而且可以更有效的一次性更新全部行。

对于其它有着缓慢更新流程的应用,每次数据改变就更新数据表是更加合适的。这样做的优点是查询的数据将是实时的。这样做的缺点是每次更新都会计算 search_vector 从而增加了额外的开销。

一种妥协的方式是把 search_vector 作为异步的进程放到队列里,这样它的更新可以非常快,而且更新仍然可以批量处理。这不在本文的范围之内,但根据应用的架构,这样做应该不会很难。

最好的方式将取决于具体的应用。这里有一些简单的方法可以在每次数据更新时保存文档。

更新文档的其中一个方式是重写 Post 的 save() 方法。在这个方法中,每次查询依赖的数据更新了,search_vector 也会随之更新。所以查询的结果可以立即反映数据的改变。然而这会对数据库的每次更新操作增加额外的开销。

首先我们将会创建一个自定义管理器,当我们调用它时将会向查询集添加文档,这样我们可以保持代码 DRY (译者注:Don't repeat yourself),而且把我们的搜索向量只定义在了一个地方。

现在更新我们的 Post 模型,添加自定义管理器和自定义 save 方法。这里的想法时将数据保存到数据库,然后执行一个 SELECT 查询来将所有的数据连接到一起,之后再创建新的 search_vector。这样每次保存都会导致一次 UPATE,SELECT 以及另一个 UPDATE 的操作。

另外,更新 authors 和 tags 并不会触发这个 <code>save()</code>,所以我们也为它们添加信号来强制执行 Post 模型的 <code>save()</code> 来更新 search_vector。

现在所有对 Post,Author 或添加、删除、移除 tags 的操作都会触发查询数据的更新。如果一个 tag 被重命名了,那么我们不会在没有创建另一个信号处理程序的情况下接收它。

也可以为数据库安装一些当数据改变时会自动更新 search_vector 的触发器。我不会描述太多的细节,但它们看起来会像下面这样。我们可以将它们添加到一次迁移中,使用 RunSQL 命令将它们安装到数据库。这个想法与上述完全一样,但是由于数据库可以在本地执行所有操作,并且不必将数据来回发送到Django,它将执行得更好。

<b>原文发布时间为:2017年6月09日</b>

<b>本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。</b>