来自 ManyToManyField 的 Django 最大相似度(TrigramSimilarity)
Django max similarity (TrigramSimilarity) from ManyToManyField
我必须实现一个可以容错的搜索功能。
目前,我有以下情况:
型号:
class Tag(models.Model):
name = models.CharField(max_length=255)
class Illustration(models.Model):
name = models.CharField(max_length=255)
tags = models.ManyToManyField(Tag)
查询:
queryset.annotate(similarity=TrigramSimilarity('name', fulltext) + TrigramSimilarity('tags__name', fulltext))
示例数据:
插图:
ID | Name | Tags |
---|--------|-------------------|
1 | "Dog" | "Animal", "Brown" |
2 | "Cat" | "Animals" |
插图有标签:
ID_Illustration | ID_Tag |
----------------|--------|
1 | 1 |
1 | 2 |
2 | 3 |
标签:
ID_Tag | Name |
-------|----------|
1 | Animal |
2 | Brown |
3 | Animals |
当我运行查询"Animal"
时,"Dog"
的相似度应该高于"Cat"
,因为它是完美匹配。
不幸的是,这两个标签以某种方式被考虑在一起。
目前,它看起来像是将标签连接成一个字符串,然后检查相似性:
TrigramSimilarity("Animal Brown", "Animal") => X
但我想调整它,使 Illustration
实例名称与其标签之间的相似度最高:
Max([
TrigramSimilarity('Name', "Animal"),
TrigramSimilarity("Tag_1", "Animal"),
TrigramSimilarity("Tag_2", "Animal"),
]) => X
Edit1: 我正在尝试查询所有插图,其中标题或其中一个标签的相似度大于 X。
Edit2:附加示例:
fulltext = 'Animal'
TrigramSimilarity('Animal Brown', fulltext) => x
TrigramSimilarity('Animals', fulltext) => y
Where x < y
But what I want is actually
TrigramSimilarity(Max(['Animal', 'Brown]), fulltext) => x (Similarity
to Animal) TrigramSimilarity('Animals', fulltext) => y
Where x > y
你不能拆开tags__name
(至少我不知道办法)。
根据您的示例,我可以假设 2 种可能的解决方案(第一种解决方案并非严格使用 Django):
并不是所有的东西都需要严格通过Django
我们有 Python 权力,所以让我们使用它们:
让我们先编写查询:
from difflib import SequenceMatcher
from django.db.models import Q
def create_query(fulltext):
illustration_names = Illustration.objects.values_list('name', flat=True)
tag_names = Tag.objects.values_list('name', flat=True)
query = []
for name in illustration_names:
score = SequenceMatcher(None, name, fulltext).ratio()
if score == 1:
# Perfect Match for name
return [Q(name=name)]
if score >= THRESHOLD:
query.append(Q(name=name))
for name in tag_names:
score = SequenceMatcher(None, name, fulltext).ratio()
if score == 1:
# Perfect Match for name
return [Q(tags__name=name)]
if score >= THRESHOLD:
query.append(Q(tags__name=name))
return query
然后创建您的查询集:
from functools import reduce # Needed only in python 3
from operator import or_
queryset = Illustration.objects.filter(reduce(or_, create_query(fulltext)))
解码以上内容:
我们正在根据我们的 fulltext
检查每个 Illustration
和 Tag
名称,并且我们正在用每个名称组成一个查询,它的相似性通过了 THRESHOLD
.
SequenceMatcher
method compares sequences and returns a ratio 0 < ratio < 1
where 0 indicates No-Match and 1 indicates Perfect-Match. Check this answer for another usage example: Find the similarity percent between two strings(注意:还有其他字符串比较模块,找到适合自己的)
Q()
Django 对象,允许创建复杂的查询(有关链接文档的更多信息)。
- 使用
operator
and reduce
我们将 Q()
对象列表转换为 OR 分隔的查询参数:
Q(name=name_1) | Q(name=name_2) | ... | Q(tag_name=tag_name_1) | ...
注:
您需要定义一个可接受的 THRESHOLD
.
正如您所想象的那样,这会有点慢,但是当您需要进行 "fuzzy" 搜索时,这是可以预料的。
(The Django Way:)
使用具有高相似度阈值的查询并按此相似率对查询集进行排序:
queryset.annotate(
similarity=Greatest(
TrigramSimilarity('name', fulltext),
TrigramSimilarity('tags__name', fulltext)
)).filter(similarity__gte=threshold).order_by('-similarity')
解码以上内容:
Greatest()
接受表达式或模型字段的聚合(不要与 Django 方法 aggregate
混淆)和 returns 最大项。
TrigramSimilarity(word, search)
returns 介于 0 和 1 之间的比率。比率越接近 1,word
与 search
越相似。
.filter(similarity__gte=threshold)
, 将过滤相似度低于 threshold
.
0 < threshold < 1
。您可以将阈值设置为相当高的 0.6
(考虑到默认值为 0.3
)。 您可以使用它来调整您的性能。
- 最后,将查询集按
similarity
率降序排列。
我只用了TrigramSimilarity, Max and Greatest.
就解决了
我按照你的问题填充了一些数据:
from illustrations.models import Illustration, Tag
Tag.objects.bulk_create([Tag(name=t) for t in ['Animal', 'Brown', 'Animals']])
Illustration.objects.bulk_create([Illustration(name=t) for t in ['Dog', 'Cat']])
dog=Illustration.objects.get(name='Dog')
cat=Illustration.objects.get(name='Cat')
animal=Tag.objects.get(name='Animal')
brown=Tag.objects.get(name='Brown')
animals=Tag.objects.get(name='Animals')
dog.tags.add(animal, brown)
cat.tags.add(animals)
我导入了所有必要的函数并初始化了fulltext
:
from illustrations.models import Illustration
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models.functions import Greatest
from django.db.models import Max
fulltext = 'Animal'
然后我执行查询:
Illustration.objects.annotate(
max_similarity=Greatest(
Max(TrigramSimilarity('tags__name', fulltext)),
TrigramSimilarity('name', fulltext)
)
).values('name', 'max_similarity')
结果如下:
<QuerySet [{'name': 'Dog', 'max_similarity': 1.0}, {'name': 'Cat', 'max_similarity': 0.666667}]>
这是从 PostgreSQL 执行的 SQL 查询:
SELECT "illustrations_illustration"."name", GREATEST(MAX(SIMILARITY("illustrations_tag"."name", 'Animal')), SIMILARITY("illustrations_illustration"."name", 'Animal')) AS "max_similarity"
FROM "illustrations_illustration"
LEFT OUTER JOIN "illustrations_illustration_tags" ON ("illustrations_illustration"."id" = "illustrations_illustration_tags"."illustration_id")
LEFT OUTER JOIN "illustrations_tag" ON ("illustrations_illustration_tags"."tag_id" = "illustrations_tag"."id")
GROUP BY "illustrations_illustration"."id", SIMILARITY("illustrations_illustration"."name", 'Animal')
您可以使用 max_similarity
注释来过滤或排序您的结果。
我必须实现一个可以容错的搜索功能。
目前,我有以下情况:
型号:
class Tag(models.Model):
name = models.CharField(max_length=255)
class Illustration(models.Model):
name = models.CharField(max_length=255)
tags = models.ManyToManyField(Tag)
查询:
queryset.annotate(similarity=TrigramSimilarity('name', fulltext) + TrigramSimilarity('tags__name', fulltext))
示例数据:
插图:
ID | Name | Tags |
---|--------|-------------------|
1 | "Dog" | "Animal", "Brown" |
2 | "Cat" | "Animals" |
插图有标签:
ID_Illustration | ID_Tag |
----------------|--------|
1 | 1 |
1 | 2 |
2 | 3 |
标签:
ID_Tag | Name |
-------|----------|
1 | Animal |
2 | Brown |
3 | Animals |
当我运行查询"Animal"
时,"Dog"
的相似度应该高于"Cat"
,因为它是完美匹配。
不幸的是,这两个标签以某种方式被考虑在一起。
目前,它看起来像是将标签连接成一个字符串,然后检查相似性:
TrigramSimilarity("Animal Brown", "Animal") => X
但我想调整它,使 Illustration
实例名称与其标签之间的相似度最高:
Max([
TrigramSimilarity('Name', "Animal"),
TrigramSimilarity("Tag_1", "Animal"),
TrigramSimilarity("Tag_2", "Animal"),
]) => X
Edit1: 我正在尝试查询所有插图,其中标题或其中一个标签的相似度大于 X。
Edit2:附加示例:
fulltext = 'Animal'
TrigramSimilarity('Animal Brown', fulltext) => x TrigramSimilarity('Animals', fulltext) => y
Where x < y
But what I want is actually
TrigramSimilarity(Max(['Animal', 'Brown]), fulltext) => x (Similarity to Animal) TrigramSimilarity('Animals', fulltext) => y
Where x > y
你不能拆开tags__name
(至少我不知道办法)。
根据您的示例,我可以假设 2 种可能的解决方案(第一种解决方案并非严格使用 Django):
并不是所有的东西都需要严格通过Django
我们有 Python 权力,所以让我们使用它们:让我们先编写查询:
from difflib import SequenceMatcher from django.db.models import Q def create_query(fulltext): illustration_names = Illustration.objects.values_list('name', flat=True) tag_names = Tag.objects.values_list('name', flat=True) query = [] for name in illustration_names: score = SequenceMatcher(None, name, fulltext).ratio() if score == 1: # Perfect Match for name return [Q(name=name)] if score >= THRESHOLD: query.append(Q(name=name)) for name in tag_names: score = SequenceMatcher(None, name, fulltext).ratio() if score == 1: # Perfect Match for name return [Q(tags__name=name)] if score >= THRESHOLD: query.append(Q(tags__name=name)) return query
然后创建您的查询集:
from functools import reduce # Needed only in python 3 from operator import or_ queryset = Illustration.objects.filter(reduce(or_, create_query(fulltext)))
解码以上内容:
我们正在根据我们的
fulltext
检查每个Illustration
和Tag
名称,并且我们正在用每个名称组成一个查询,它的相似性通过了THRESHOLD
.SequenceMatcher
method compares sequences and returns a ratio0 < ratio < 1
where 0 indicates No-Match and 1 indicates Perfect-Match. Check this answer for another usage example: Find the similarity percent between two strings(注意:还有其他字符串比较模块,找到适合自己的)Q()
Django 对象,允许创建复杂的查询(有关链接文档的更多信息)。- 使用
operator
andreduce
我们将Q()
对象列表转换为 OR 分隔的查询参数:Q(name=name_1) | Q(name=name_2) | ... | Q(tag_name=tag_name_1) | ...
注: 您需要定义一个可接受的
THRESHOLD
.
正如您所想象的那样,这会有点慢,但是当您需要进行 "fuzzy" 搜索时,这是可以预料的。
(The Django Way:)
使用具有高相似度阈值的查询并按此相似率对查询集进行排序:queryset.annotate( similarity=Greatest( TrigramSimilarity('name', fulltext), TrigramSimilarity('tags__name', fulltext) )).filter(similarity__gte=threshold).order_by('-similarity')
解码以上内容:
Greatest()
接受表达式或模型字段的聚合(不要与 Django 方法aggregate
混淆)和 returns 最大项。TrigramSimilarity(word, search)
returns 介于 0 和 1 之间的比率。比率越接近 1,word
与search
越相似。.filter(similarity__gte=threshold)
, 将过滤相似度低于threshold
.0 < threshold < 1
。您可以将阈值设置为相当高的0.6
(考虑到默认值为0.3
)。 您可以使用它来调整您的性能。- 最后,将查询集按
similarity
率降序排列。
我只用了TrigramSimilarity, Max and Greatest.
就解决了我按照你的问题填充了一些数据:
from illustrations.models import Illustration, Tag
Tag.objects.bulk_create([Tag(name=t) for t in ['Animal', 'Brown', 'Animals']])
Illustration.objects.bulk_create([Illustration(name=t) for t in ['Dog', 'Cat']])
dog=Illustration.objects.get(name='Dog')
cat=Illustration.objects.get(name='Cat')
animal=Tag.objects.get(name='Animal')
brown=Tag.objects.get(name='Brown')
animals=Tag.objects.get(name='Animals')
dog.tags.add(animal, brown)
cat.tags.add(animals)
我导入了所有必要的函数并初始化了fulltext
:
from illustrations.models import Illustration
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models.functions import Greatest
from django.db.models import Max
fulltext = 'Animal'
然后我执行查询:
Illustration.objects.annotate(
max_similarity=Greatest(
Max(TrigramSimilarity('tags__name', fulltext)),
TrigramSimilarity('name', fulltext)
)
).values('name', 'max_similarity')
结果如下:
<QuerySet [{'name': 'Dog', 'max_similarity': 1.0}, {'name': 'Cat', 'max_similarity': 0.666667}]>
这是从 PostgreSQL 执行的 SQL 查询:
SELECT "illustrations_illustration"."name", GREATEST(MAX(SIMILARITY("illustrations_tag"."name", 'Animal')), SIMILARITY("illustrations_illustration"."name", 'Animal')) AS "max_similarity"
FROM "illustrations_illustration"
LEFT OUTER JOIN "illustrations_illustration_tags" ON ("illustrations_illustration"."id" = "illustrations_illustration_tags"."illustration_id")
LEFT OUTER JOIN "illustrations_tag" ON ("illustrations_illustration_tags"."tag_id" = "illustrations_tag"."id")
GROUP BY "illustrations_illustration"."id", SIMILARITY("illustrations_illustration"."name", 'Animal')
您可以使用 max_similarity
注释来过滤或排序您的结果。