Postgres提供了开箱即用的强大搜索功能。这对于大多数Django应用程序来说,不需要运行和维护一个ElasticSearch集群,除非你需要ElasticSearch提供的高级特性。Django通过内置的Postgres模块很好地集成了Postgres搜索。
对于小型数据集,默认配置执行得很好,但是当数据增长时,默认的搜索配置就会变得非常慢,我们需要启用某些优化来保持查询的速度。
本文将引导你设置Django和Postgres、索引示例数据以及执行和优化全文搜索。
这些示例是经过Django + Postgres设置测试过的,但是这些优化建议通常适用于任何编程语言或框架,只要它使用Postgres。
如果你已经是一个Django的老手,那么你可以跳过第一步,直接跳到优化部分。
项目设置
创建目录并配置好Django项目。
现在我们需要安装3个依赖:
psycopg2:用于Python的Postgres客户端库
wikipedia:检索Wikipedia文章的客户端库
django-extensions:用来简化SQL查询的调试
我们还需要在本地运行Postgres。我将在这里使用Postgres的docker化版本,因为它更容易设置,但是如果你愿意,你可以自行安装一个Postgres二进制文件。
打开 full_text_search/docker-compose.yml
项目结构现在应该如下面的输出所示。我们将忽略venv目录,因为它包含了很多文件,现在还用不到。
我们将修改默认数据库设置,使用Postgres而不是SQLite。在settings.py中更改 DATABASES属性:
我们还将修改我们的INSTALLED_APPS来引入几个应用程序:
django.contrib.postgres Django的Postgres模块,全文搜索要用到
django_extensions 用于在Python中执行查询时打印SQL日志
我们的web应用程序
打开full_text_search/settings.py并修改:
启动 Postgres 和 Django.
如果我们打开浏览器并输入http://localhost:8000,应该会看到安装成功提示。
创建模型并索引示例数据
假设我们有一个表示Wikipedia页面的模型。为了简单起见,我们只使用两个字段:标题和内容。
打开 full_text_search/web/models.py
现在运行迁移去创建这个模型。
我们使用一个脚本来索引随机的Wikipedia文章,并将内容保存到Postgres中。
编辑 web/index_wikipedia.py
现在我们运行脚本来索引Wikipedia。运行这个脚本时可能会出现错误,但只要我们设法存储了几百篇文章,就不必担心这些错误。这个脚本运行将需要一段时间,所以请喝杯咖啡,几分钟后再回来吧。
优化搜索
现在假设我们希望允许用户对内容执行全文搜索。我们将交互式地查询数据集来测试全文搜索。打开一个Django shell会话:
Django会执行两个预备查询,最后执行我们的搜索查询。查看最后一个查询,我们第一眼就可以看到,仅查询执行和序列化的执行时间为~315ms。当我们想要将页面加载速度保持在以毫秒为单位的两位数时,这就太慢了。
让我们仔细看看为什么这个查询执行得如此慢。打开第二个终端,我们将在其中使用出色的Postgres查询分析器。复制上面的查询并运行 EXPLAIN ANALYZE:
我们可以看到,虽然计划时间非常快(~3ms),但是执行时间却非常慢,在~220ms。
我们可以注意到,查询对整个表执行顺序扫描,以便找到匹配的记录。我们可以通过使用索引来优化这个查询。
此外,这个查询使用to_tsvector将content列从文本规范化为tsvector,以便执行全文本搜索。
tsvector类型是文本的标记化版本,它使搜索列规范化(更多关于标记化的内容请查看这里)。Postgres需要对每一行执行这种标准化,而每一行包含一个完整的Wikipedia页面。这是一个CPU密集型且比较慢的操作。
专门的搜索列和gin索引
为了避免将文本实时转换为tsvector,我们将创建一个专门的列,它只用于搜索。这个列应该在插入或更新时被填充。在执行查询时,我们将避免转换类型带来的性能损失。
由于我们现在可以拥有一个tsvector类型,所以我们还可以添加一个gin索引来加快查询速度。gin索引会确保使用索引扫描而不是对所有记录进行顺序扫描来执行搜索。
打开我们的web/models.py文件,修改Page模型。
运行这个迁移。
Postgres触发器
理论上我们的问题已经解决了。我们有一个Gin索引列,当我们对它进行搜索时,它应该会执行得很好,但是这样做我们又引入了另一个问题:优化后的content_search列需要手动保持同步,并在内容列更新时进行更新。
幸运的是,Postgres为我们提供了一个额外的特性来解决这个问题,即触发器。触发器是Postgres函数,当对一行执行特定操作时触发。我们将创建一个触发器,它会在一个content行被创建或更新时自动填充content_search。这样一来,Postgres将保持这两列的同步,而无需我们编写任何Python代码。
为了添加触发器,我们需要手工编写Django迁移。这将添加触发器函数并更新我们所有的Page行,以确保在迁移时为我们的现有记录触发触发器并填充content_search列。如果你有一个非常大的数据集,你可能不希望在生产中这样做。
在web/migrations/0003_create_text_search_trigger.py中添加一个新的迁移。确保在dependencies中修改前面的迁移,因为前面自动生成的迁移可能对你的来说是不同的。
运行这个迁移。
测量性能改进情况
最后就到了有趣的部分,我们来验证一下这个查询的执行速度是否比以前更快。再次打开一个Django shell,但是在过滤行时使用索引化的content_search列,而不是普通的content列。
查询执行时间从0.220秒下降到0.001秒!
让我们再次分析这个查询,看看Postgres是如何执行它的。复制上面的查询并通过EXPLAIN ANALYZE运行它。
这里是有趣的地方:
Postgres 在content_searchcolumn列上使用索引代替顺序扫描。
我们也不再为每一行执行昂贵的to_tsquery操作,而是按原样使用content_search列。
缺点
不幸的是,在使用这种优化技术时存在权衡。
因为我们维护文本的另一个列的唯一目的是加快搜索速度,所以表的大小占用了更多的空间。此外,content_search列上的gin索引也需要占用空间。
由于搜索列在每次UPDATE或INSERT时都会更新,因此也会减慢对数据库的写入操作。
如果你受到内存和磁盘的限制,或者需要快速写入,这种技术可能不适合你的使用情况。然而,我怀疑大多数CRUD应用程序都可以牺牲磁盘和写入速度来实现闪电般的快速搜索。
结论
Postgres提供了出色的全文搜索功能,但速度有点慢。为了加快文本搜索,我们添加了一个tsvector类型的二级列,这是我们文本的一个搜索优化版本。
我们在搜索列上添加一个Gin索引,以确保Postgres执行索引扫描,而不是顺序扫描。这将减少一个数量级的查询执行时间。
为了保持文本列和搜索列同步,我们使用了一个Postgres触发器,它会在我们对文本列进行任何修改时自动填充搜索列。
完整的代码示例可以在Github(https://github.com/danihodovic/django-postgres-fulltext-search )上找到。
英文原文:https://dev.to/danihodovic/optimizing-postgres-full-text-search-with-django-42hg
网友评论