Максимальное сходство Django (TrigramSimilarity) от 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: Дополнительный пример:

полный текст = 'Животное'

TrigramSimilarity('Животное коричневое', полный текст) => x TrigramSimilarity('Животные', полный текст) => y

Где х ‹ у

Но то, что я хочу, на самом деле

TrigramSimilarity(Max(['Животное', 'Коричневый]), полный текст) => x (Сходство с животным) TrigramSimilarity('Животные', полный текст) => y

Где х > у


person Lukas    schedule 03.02.2018    source источник
comment
Можете ли вы уточнить, из какой модели вы выполняете набор запросов? Кроме того, что это за примерные данные? имя+идентификатор иллюстрации и теги?   -  person Thom    schedule 06.02.2018
comment
Вопрос скорректирован   -  person Lukas    schedule 06.02.2018
comment
Добавлено редактирование 2 (подробнее).   -  person Lukas    schedule 06.02.2018
comment
является TrigramSimilarity функцией Python или она переводится в SQL? Если это python, я не думаю, что вы можете использовать его в вызове annotate, как вы пытаетесь сделать.   -  person Laurent S    schedule 07.02.2018
comment
TrigramSimilarity является частью Django Framework ( docs.djangoproject.com/en /2.0/ref/contrib/postgres/search), но для этого требуется база данных PostgreSQL (не будет работать с SQLite) с активированным расширением pg_trgm.   -  person Lukas    schedule 08.02.2018
comment
@ Лукас, могу я спросить, пробовали ли вы мое решение?   -  person Paolo Melchiorre    schedule 12.02.2018
comment
@Paolo: У меня пока не было времени, но я попробую вечером и дам вам отзыв. Извините, но я уже начал применять подход Джона, прежде чем вы опубликовали свое решение.   -  person Lukas    schedule 12.02.2018
comment
@Lukas в следующий раз, когда вы выберете ответ на свой вопрос, примите его немедленно, вы избежите того, чтобы люди тратили время на написание решения, которое вы не примете во внимание. В любом случае, я жду ваших отзывов о моем решении.   -  person Paolo Melchiorre    schedule 12.02.2018


Ответы (2)


Вы не можете разбить tags__name (по крайней мере, я не знаю, как это сделать).
Из ваших примеров я могу предположить 2 возможных решения (1-е решение строго не использует 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 и Tag с нашим fulltext и составляем запрос с каждым именем, сходство которого проходит 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 (Note: There are other strings comparing modules as well, find one that suits you)
    • Q() объекты Django позволяют создание сложных запросов (подробнее в связанных документах).
    • С помощью operator и reduce мы преобразуем список Q() объектов в аргумент запроса, разделенный ИЛИ:
      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() accepts an aggregation (not to be confused with the Django method aggregate) of expressions or of model fields and returns the max item.
    • TrigramSimilarity(word, search) возвращает скорость от 0 до 1. Чем ближе скорость к 1, тем больше похоже word на search.
    • .filter(similarity__gte=threshold) будет отфильтровывать сходства меньше, чем threshold.
    • 0 < threshold < 1. Вы можете установить пороговое значение 0.6, что довольно много (учитывайте, что значение по умолчанию равно 0.3). Вы можете поэкспериментировать с этим, чтобы улучшить свою производительность.
    • Наконец, упорядочите набор запросов по частоте similarity в порядке убывания.
person John Moutafis    schedule 08.02.2018
comment
К сожалению, это не работает так, поскольку TrigramSimilarity работает только так: TrigramSimilarity(columnName, searchString). Поэтому он будет искать столбец с пометкой «Животное», «Коричневый» или «Животные». - person Lukas; 09.02.2018
comment
Большое спасибо, я попробую. Но это выглядит иначе, чем то, что было у меня раньше, так как позволяет мне только фильтровать, но не сортировать в зависимости от результата или я ошибаюсь? - person Lukas; 09.02.2018
comment
@Lukas, вы не можете сортировать как есть, но с небольшой модификацией вы можете это сделать (sort список имен перед составом запроса). - person John Moutafis; 09.02.2018
comment
Мне удалось решить проблему, как вы описали в варианте 1. Большое спасибо за вашу помощь! - person Lukas; 12.02.2018
comment
@Лукас Приятно знать :) - person John Moutafis; 12.02.2018

Я решил это, используя только TrigramSimilarity, Макс и Лучший.

Я заполнил некоторые данные, как в вашем вопросе:

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}]>

Это SQL-запрос, выполненный из PostgreSQL:

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 для фильтрации или упорядочивания результатов.

person Paolo Melchiorre    schedule 09.02.2018