在多对多相关模型上使用 SearchVectorFields
Using SearchVectorFields on many to many related models
我有两个模型 Author
和 Book
通过 m2m 关联(一个作者可以有很多书,一本书可以有很多作者)
当唯一标识符不可用时,我们经常需要使用文本字符串查询和匹配摄取记录,跨两种模型,即:“JRR Tolkien - Return of the King”。
我想测试将 SearchVectorField
与 GIN 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.title
和 Author.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),包括排序。
- 将
F Expression
添加到 SearchRank
的第一个参数(而不是使用引号 中的字段名称,这正是 the documentation)
- 向
SearchQuery
添加 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)一切都会很好.
我有两个模型 Author
和 Book
通过 m2m 关联(一个作者可以有很多书,一本书可以有很多作者)
当唯一标识符不可用时,我们经常需要使用文本字符串查询和匹配摄取记录,跨两种模型,即:“JRR Tolkien - Return of the King”。
我想测试将 SearchVectorField
与 GIN 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.title
和 Author.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),包括排序。
- 将
F Expression
添加到SearchRank
的第一个参数(而不是使用引号 中的字段名称,这正是 the documentation) - 向
SearchQuery
添加
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)一切都会很好.