在多对多相关模型上使用 SearchVectorFields

Using SearchVectorFields on many to many related models

我有两个模型 AuthorBook 通过 m2m 关联(一个作者可以有很多书,一本书可以有很多作者)

当唯一标识符不可用时,我们经常需要使用文本字符串查询和匹配摄取记录,跨两种模型,即:“JRR Tolkien - Return of the King”。

我想测试将 SearchVectorFieldGIN indexes 结合使用是否可以缩短全文搜索响应时间 - 但由于搜索查询将是 SearchVector(author__name, book__title) 看来这两个模型都需要添加了 SearchVectorField。

当每个 table 都需要更新时,这会变得更加复杂,因为看起来 Postgres Triggers 需要在两个 table 上进行设置,这可能会使任何更新完全无法维持。

问题

当涉及 m2m 相关模型时,Django 中采用矢量化全文搜索方法的现代最佳实践是什么? SearchVectorField 应该通过 table 放置吗?还是在每个模型中?应如何应用触发器?

我一直在专门寻找这方面的指南 - 但在谈论 SearchVectorFields 时似乎没有人提到 m2ms。我确实找到了

另外,如果 Postgres 真的不是现代 Django 的前进方向,我也很乐意指导更好的东西 suited/supported/documented。在我们的例子中,我们使用的是 Postgres 11.6。

复制

from django.db import models
from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex

class Author(models.Model):
    name = models.CharField(max_length=100, unique=True)
    main_titles = models.ManyToManyField(
        "Book",
        through="BookMainAuthor",
        related_name="main_authors",
    )
    search = SearchVectorField(null=True)

class BookMainAuthor(models.Model):
    """The m2m through table for book and author (main)"""

    book = models.ForeignKey("Book", on_delete=models.CASCADE)
    artist = models.ForeignKey("Author", on_delete=models.CASCADE)

    class Meta:
        unique_together = ["book", "author"]

class Book(models.Model):
    title = models.CharField(max_length=100, unique=True)
    search = SearchVectorField(null=True)

通过 table

探索 M2M 索引

探索下面 Yevgeniy-kosmak 的 answer,这是索引 table 到 Book.titleAuthor.name[=32= 的字符串排列的简单方法]

使用 SearchVectorField 执行搜索速度快,而且对于一些有多位作者的图书更有效。

然而,当尝试使用 SearchRank - 速度会急剧下降:

BookMainAuthor.objects.annotate(rank=SearchRank("search", SearchQuery("JRR Tolkien - Return of the King")).order_by("-rank:).explain(analyze=True)

"Gather Merge  (cost=394088.44..489923.26 rows=821384 width=227) (actual time=8569.729..8812.096 rows=989307 loops=1)
Workers Planned: 2
Workers Launched: 2
  ->  Sort  (cost=393088.41..394115.14 rows=410692 width=227) (actual time=8559.074..8605.681 rows=329769 loops=3)
        Sort Key: (ts_rank(to_tsvector(COALESCE((search_vector)::text, ''::text)), plainto_tsquery('JRR Tolkien - Return of the King'::text), 6)) DESC
        Sort Method: external merge  Disk: 77144kB
 – 

Worker 0:  Sort Method: external merge  Disk: 76920kB
        Worker 1:  Sort Method: external merge  Disk: 76720kB
        ->  Parallel Seq Scan on bookstore_bookmainauthor  (cost=0.00..264951.11 rows=410692 width=227) (actual time=0.589..8378.569 rows=329769 loops=3)
Planning Time: 0.369 ms
Execution Time: 8840.139 ms"

没有排序,只节省 500ms:

BookMainAuthor.objects.annotate(rank=SearchRank("search", SearchQuery("JRR Tolkien - Return of the King")).explain(analyze=True)

'Gather  (cost=1000.00..364517.21 rows=985661 width=227) (actual time=0.605..8282.976 rows=989307 loops=1)
  Workers Planned: 2
  Workers Launched: 2
  ->  Parallel Seq Scan on bookstore_bookmainauthor (cost=0.00..264951.11 rows=410692 width=227) (actual time=0.356..8187.242 rows=329769 loops=3)
Planning Time: 0.039 ms
Execution Time: 8306.799 ms'

但是我注意到,如果您执行以下操作,它会显着缩短查询执行时间 (~17x),包括排序。

  1. F Expression 添加到 SearchRank 的第一个参数(而不是使用引号 中的字段名称,这正是 the documentation)
  2. SearchQuery
  3. 添加 config kwarg
BookMainAuthor.objects.annotate(rank=SearchRank(F("search"), SearchQuery("JRR Tolkien - Return of the King", config='english')).order_by("-rank").explain(analyze=True)

Gather Merge  (cost=304240.66..403077.76 rows=847116 width=223) (actual time=336.654..559.367 rows=989307 loops=1)
  Workers Planned: 2
  Workers Launched: 2
  ->  Sort  (cost=303240.63..304299.53 rows=423558 width=223) (actual time=334.970..373.282 rows=329769 loops=3)
        Sort Key: (ts_rank(search_vector, '''jrr'' & ''tolkien'' & ''return'' & ''king'''::tsquery)) DESC
        Sort Method: external merge  Disk: 75192kB
        Worker 0:  Sort Method: external merge  Disk: 76672kB
        Worker 1:  Sort Method: external merge  Disk: 76976kB
        ->  Parallel Seq Scan on bookstore_bookmainauthor  (cost=0.00..173893.48 rows=423558 width=223) (actual time=0.014..211.007 rows=329769 loops=3)
Planning Time: 0.059 ms
Execution Time: 584.402 ms

终于明白了。我想您需要通过同时包含作者和书名的查询进行搜索。而且您将无法将它们分开以查看查询的“书”部分的 Book table 以及 Author.

的相同内容

是的,使用 PostgreSQL 不可能从单独的 table 中创建字段索引。我不认为这是 PostgreSQL 的弱点,当您 确实 需要这样的索引时,这只是一个非常不寻常的情况。在大多数情况下,还有其他解决方案,但效率并不差。当然,如果出于某种原因您确定有必要,您可以随时查看 ElasticSearch

我会建议您采用这种方法。您可以使用以下结构制作 BookMainAuthor

class BookMainAuthor(models.Model):
    """The m2m through table for book and author (main)"""

    book = models.ForeignKey("Book", on_delete=models.CASCADE)
    artist = models.ForeignKey("Author", on_delete=models.CASCADE)
    book_full_name = models.CharField(max_length=200)
    search = SearchVectorField(null=True)

    class Meta:
        unique_together = ["book", "author"]

在我看来,维护 book_full_name 字段应该不会造成任何麻烦,该字段将包含作者和书名以及适当的分隔符。其他都是 textbook 案例。

根据我的经验,如果 table BookMainAuthor 包含的条目不超过 1000 万条,在平均单个服务器上(例如来自 here 的 AX161)一切都会很好.