10

我必须实现一个容错的搜索功能。
目前,我有以下情况:

楷模:

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", for 的相似性"Dog"应该高于 for "Cat",因为它是完美匹配的。
不幸的是,这两个标签以某种方式被考虑在一起。
目前,它看起来像是将标签连接在一个字符串中,然后检查相似性:

TrigramSimilarity("Animal Brown", "Animal") => X

但我想调整它,使Illustration实例名称与其标签之间的相似度最高:

Max([
    TrigramSimilarity('Name', "Animal"), 
    TrigramSimilarity("Tag_1", "Animal"), 
    TrigramSimilarity("Tag_2", "Animal"),
]) => X

Edit1:我正在尝试查询所有插图,其中标题或其中一个标签的相似度大于 X。

编辑2:附加示例:

全文 = '动物'

TrigramSimilarity('Animal Brown', fulltext) => x TrigramSimilarity('Animals', fulltext) => y

其中 x < y

但我想要的其实是

TrigramSimilarity(Max(['Animal', 'Brown]), fulltext) => x (Similarity to Animal) TrigramSimilarity('Animals', fulltext) => y

其中 x > y

4

2 回答 2

10

你不能分手tags__name(至少我不知道办法)。
从您的示例中,我可以假设 2 个可能的解决方案(第一个解决方案不是严格使用 Django):


  1. 并非所有东西都需要严格通过 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)))
    

    解码以上内容:

    我们正在对照我们的名称检查每个名称,Illustration并且我们正在编写一个查询,其中每个名称的相似性都通过.TagfulltextTHRESHOLD

    • SequenceMatcher方法比较序列并返回一个比率0 < ratio < 1,其中 0 表示No-Match, 1 表示Perfect-Match。检查此答案以获取另一个用法示例:查找两个字符串之间的相似度百分比注意:还有其他字符串比较模块,找到适合您的)
    • Q()Django 对象,允许创建复杂的查询(更多关于链接的文档)。
    • 使用operatorandreduce我们将Q()对象列表转换为 OR 分隔的查询参数:
      Q(name=name_1) | Q(name=name_2) | ... | Q(tag_name=tag_name_1) | ...

    注意: 您需要定义一个可接受的THRESHOLD.
    正如您可以想象的那样,这会有点慢,但是当您需要进行“模糊”搜索时,这是可以预料的。


  1. (Django 方式:)
    使用具有高相似度阈值的查询并按此相似率对查询集进行排序:

    queryset.annotate(
        similarity=Greatest(
            TrigramSimilarity('name', fulltext), 
            TrigramSimilarity('tags__name', fulltext)
        )).filter(similarity__gte=threshold).order_by('-similarity')
    

    解码以上内容:

    • Greatest()接受表达式或模型字段的聚合(不要与 Django 方法混淆aggregate)并返回最大项。
    • TrigramSimilarity(word, search)返回介于 0 和 1 之间的比率。比率越接近 1,越word类似于search.
    • .filter(similarity__gte=threshold), 将过滤低于 的相似度threshold
    • 0 < threshold < 1. 您可以将阈值设置0.6为相当高(考虑默认值为0.3)。你可以用它来调整你的表现。
    • 最后,按similarity降序对查询集进行排序。
于 2018-02-08T14:03:21.873 回答
4

我只使用TrigramSimilarityMaxGreatest解决了它。

我在您的问题中填充了一些数据:

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注释对结果进行过滤或排序。

于 2018-02-09T15:10:47.823 回答