Django 5 для начинающих

Прогресс по курсу:  9/1004

6.11 Добавление полнотекстового поиска в блог
4 из 4 шагов пройдено

Взвешивание запросов

Влияние конкретных векторов можно усиливать таким образом, чтобы им придавался бóльший вес при упорядочивании результатов по релевантности. Например, взвешивание можно использовать, чтобы придавать большую релевантность постам, которые сочетаются по заголовку, а не по содержимому.

Отредактируйте файл views.py приложения blog, видоизменив представление post_search, как показано ниже:

def post_search(request):
    form = SearchForm()
    query = None
    results = []

    if 'query' in request.GET:
        form = SearchForm(request.GET)
        if form.is_valid():
            query = form.cleaned_data['query']
            search_vector = SearchVector('title', weight='A') + \
                            SearchVector('body', weight='B')
            search_query = SearchQuery(query)
            results = Post.published.annotate(rank=SearchRank(search_vector, search_query)
            ).filter(rank__gte=0.3).order_by('-rank')

    return render(request,
                  'blog/post/search.html',
                  {'form': form,
                   'query': query,
                   'results': results})

В приведенном выше исходном коде к векторам поиска, сформированным с использованием полей title и body, применяются разные веса.

По умолчанию веса таковы: A, B, C, D и они относятся соответственно к числам 1.0, 0.4, 0.2, 0.1.

Вес 1.0 применяется к вектору поиска title(A), и вес 0.4 - к вектору body(B). Совпадения с заголовком будут преобладать над совпадениями с содержимым тела поста. Результаты фильтруются, чтобы отображать только те, у которых ранг выше 0.3.

 

Поиск по триграммному сходству

Еще одним подходом к поиску является триграммное сходство. Триграмма – это группа из трех следующих друг за другом символов. Сходство двух строковых литералов можно измерять, подсчитывая число общих для них триграмм. Во многих языках данный подход оказывается очень эффективным при измерении сходства слов.

Для того чтобы использовать триграммы в PostgreSQL, сначала необходимо установить расширение pg_trgm.

Запускаем SQL Shell чтобы подсоединиться к своей базе данных:

Затем исполните следующую ниже команду, чтобы установить расширение pg_trgm:

CREATE EXTENSION pg_trgm;

Вы получите такой результат:

CREATE EXTENSION

Давайте отредактируем представление и видоизменим его под триграммный поиск.

Отредактируйте файл views.py приложения blog, добавив следующую ниже инструкцию импорта:

from django.contrib.postgres.search import TrigramSimilarity

Затем видоизмените представление post_search, как показано ниже:

def post_search(request):
    form = SearchForm()
    query = None
    results = []

    if 'query' in request.GET:
        form = SearchForm(request.GET)

        if form.is_valid():
            query = form.cleaned_data['query']
            results = Post.published.annotate(
                similarity=TrigramSimilarity('title', query),
            ).filter(similarity__gt=0.1).order_by('-similarity')

    return render(request,
                  'blog/post/search.html',
                  {'form': form,
                   'query': query,
                   'results': results})

Пройдите по URL-адресу http://127.0.0.1:8000/blog/search/ в своем браузере и протестируйте различные варианты триграммного поиска.

В следующем ниже примере показана гипотетическая опечатка в термине django, показаны результаты поиска термина yango:

В приложение для ведения блога был добавлен мощный поисковый механизм.

Более подробная информация о полнотекстовом поиске находится на странице https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/search/


  • Комментариев
Будьте вежливы и соблюдайте наши принципы сообщества. Пожалуйста, не оставляйте решения и подсказки в комментариях, для этого есть отдельный форум.
Оставить комментарий

Тема про TrigramSimilarity одна из самых интересных оказалась! Спасибо за комменты в том числе!

Доброго времени суток!

Поиск по триграммному сходству в приведенном примере ищет текст только в заголовках(title), как сделать чтобы он мог еще в body искать сходство?

@Vladislav, посмотрите комментарий ниже https://stepik.org/lesson/973400/step/4?discussion=7923325&unit=980252, там ответ на такой-же вопрос.

@Дмитрий_Селезнев

Увидел, спасибо!

В чём может быть проблема?

@Дмитрий_Чекмасов, Для того чтобы использовать триграммы в PostgreSQL, сначала необходимо установить расширение pg_trgm. Этот момент сделали?

@Илья_Перминов, делал, у меня такой результат был  

Изменен Дмитрий Чекмасов

@Дмитрий_Чекмасов, внимательно посмотрите на скрин в лекции, делать нужно было не под админом postgres, а у бд, к которой джанго прикручен.

@Илья_Перминов, а блин не заметил, спасибо ) всё заработало

Могут ли значение весов быть больше 1?

К примеру:

По 1 посту совпадение по title и body = 1.4

По 2 посту совпадение только по title = 1

Означает ли что 1 пост будет выше в списке выдачи? Или первому посту будет назначен rank=1 и оба поста будут равнозначны в выдаче?

@Антон_Глухенко, в нашем случае значение не может быть больше 1. Пост с полным совпадением по title и body будет выше, чем пост с совпадением по title. Можете посмотреть скриншот ниже:

А можно ли веса задавать значениями с плавающей точкой вместо буквенных обозначений?

Минимальное значение, которые мы использовали "В" равно 0.4, то есть у нас не может быть случая при котором, в результате поиска будет значение которое не будет показано, верно?

@Виктор_Русинович, Вес должен быть одной из следующих букв: D, C, B, A. По умолчанию эти веса относятся к числам 0.1, 0.2, 0.4 и 1.0. Если вы хотите придать им другой вес, передайте список из четырех чисел c плавающей точкой в SearchRank, как вес в том же порядке:

rank = SearchRank(search_vector, search_query, weights=[0.2, 0.4, 0.6, 0.8])
Post.objects.annotate(rank=rank).filter(rank__gte=0.3).order_by('-rank')

Или можете сделать как в комментарии ниже я написал, по такому принципу тоже можно менять вес. 

Изменен Илья Перминов

А можно ли комбинировать TrigramSimilarity так, чтобы поиск совпадений был по двум полям title и body, как это было с SearchVector?

@Павел_Лепешинский, TrigramSimilarity  не умеет работать с несколькими полями, можно комбинировать только так:

            results = Post.published.annotate(
                similarity=TrigramSimilarity('title', query),
            ).filter(similarity__gt=0.1).order_by('-similarity')
            if not results:
                results = Post.published.annotate(
                    search=SearchVector('body', 'title'),
                    rank=SearchRank(SearchVector('body', 'title'), SearchQuery(query))
                ).filter(search=SearchQuery(query)).order_by('-rank')

@Павел_Лепешинский, Вот так можно реализовать через триграммное сходство используя вес.

from django.contrib.postgres.search import TrigramSimilarity, TrigramWordSimilarity

def post_search(request):
    form = SearchForm()
    query = None
    results = []

    if 'query' in request.GET:
        form = SearchForm(request.GET)
        if form.is_valid():
            query = form.cleaned_data['query']
            A = 1.0
            B = 0.4
            results = Post.published.annotate(
                similarity=(A / (A + B) * TrigramSimilarity('title', query)
                            + B / (A + B) * TrigramWordSimilarity(query, 'body'))
            ).filter(similarity__gte=0.1).order_by('-similarity')

    return render(request,
                  'blog/post/search.html',
                  {'form': form,
                   'query': query,
                   'results': results})

@Илья_Перминов, интересный вариант, спасибо)

@Илья_Перминов, Без знания и понимания  математики тут точно не обойтись! Решение точно не для Джуна)

@Евгений_Куликов, на самом деле, в данном коде, нет ничего сложного. Просто чужой код всегда сложнее читать.

В первом пункте "Взвешивание запросов", видимо, зря удалили аргумент, поэтому поиск выдает всегда пустой результат:

И, что интересно, фильтрация по rank >= 0.3 там так же работает некорректно. Я вывел веса рядом с заголовками, получилось так:

На тексты внимания не обращайте, нагенерил в Яндекс.Рефератах

Т.е. несмотря на заданные веса (A и B), 1 не удается получить даже при нахождении искомого слова и в заголовке, и в теле. 

Попробовал задать веса другим способом, описанным в документации:

            search_vector = SearchVector('title', 'body')
            search_query = SearchQuery(query)
            results = Post.published_only.annotate(
                search=search_vector,
                rank=SearchRank(search_vector, search_query, weights=[1.0, 0.4, 0.2, 0.1])
                                                   ).filter(rank__gte=0.3).order_by('-rank')

Причем пришлось перечислить веса в обратном порядке. Но 1 все равно получить не удалось, хотя все веса поменялись. Короче, в этом еще разбираться и разбираться...

Изменен ilya kutaev

@ilya_kutaev, не зря удалили, он всё-равно не используется в запросе.

Код взвешивания запросов тут точно такой-же, как в официально документации: https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/search/#weighting-queries

>>> from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
>>> vector = SearchVector("body_text", weight="A") + SearchVector(
...     "blog__tagline", weight="B"
... )
>>> query = SearchQuery("cheese")
>>> Entry.objects.annotate(rank=SearchRank(vector, query)).filter(rank__gte=0.3).order_by(
...     "rank"
... )

У нас:

            search_vector = SearchVector('title', weight='A') + \
                            SearchVector('body', weight='B')
            search_query = SearchQuery(query)
            results = Post.published.annotate(rank=SearchRank(search_vector, search_query)
            ).filter(rank__gte=0.3).order_by('-rank')

Попробовал задать веса другим способом, описанным в документации:

search_vector = SearchVector('title', 'body')

Только в документации задаётся вес для выбранных полей:

vector = SearchVector("body_text", weight="A") + SearchVector("blog__tagline", weight="B")

Причем пришлось перечислить веса в обратном порядке.

Это делать не нужно, порядок должен быть такой: D, C, B, A.

For both these functions, the optional weights argument offers the ability to weigh word instances more or less heavily depending on how they are labeled. The weight arrays specify how heavily to weigh each category of word, in the order:

{D-weight, C-weight, B-weight, A-weight}
If no weights are provided, then these defaults are used:
{0.1, 0.2, 0.4, 1.0}

https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-RANKING

@Дмитрий_Селезнев, Понятно, спасибо. Но пока не вернул аргумент search=, поиск ничего не выдавал (

Поэкспериментирую еще

он не работает 

Поиск по триграммному сходству


поиск на сайте не находит даже те посты которые есть в БД

в моей БД кодировка UTF8 и все равно не работает 

Изменен No Name

@No_Name, удалил базу данных и установил как рекомендовали все гуд
 

CREATE DATABASE blog OWNER blog TEMPLATE=template0 ENCODING 'UTF-8' LC_COLLATE 'ru_RU.UTF-8' LC_CTYPE 'ru_RU.UTF-8';


не забываем после бекапа данных выполнить команду иначе будет ошибка

CREATE EXTENSION pg_trgm;

Изменен No Name

@No_Name, да, у меня и простой поиск не работал, пока так же не удалил БД и таким способом не установил новую

посты на русском не находит при триграммном поиске
А при search_vector я так и не смог сделать поиск регистронезависимым(

Изменен Никита Ильин

@Никита_Ильин, Там проблема с кодировкой по дефолту. Нужно создать базу данных вот так:

CREATE DATABASE blog OWNER blog TEMPLATE=template0 ENCODING 'UTF-8' LC_COLLATE 'ru_RU.UTF-8' LC_CTYPE 'ru_RU.UTF-8';

И не забыть выполнить:

CREATE EXTENSION pg_trgm;

И все будет работать. Сейчас проверил.

@Илья_Перминов, как оказалось поменять эти параметры у созданной базы данных не получается. Чат жпт предложил создать новую базу и перенести данные. Начал, но оказалось, что у меня клиент версии 14.1, а сам постгрес 15.5. После 3х часовых танцев с чатом жпт по итогу снес все и создал все заново. Устал

Не очень понял:

results = Post.published.annotate(search=search_vector, rank=SearchRank(search_vector, search_query) ).filter(rank__gte=0.3).order_by('-rank') а где фильтрация по полю search?

@Кирилл_Семенихин, А зачем нам она? Если мы выводим только те результаты где rank больше 0.3, то есть дополнительная фильтрация нам не к чему.

@Илья_Перминов, Хм, я просто думал, что надо в любом случае сначала отфильтровать только те посты, у которых search=search_query, а ранжирование уже на них. Тогда, получается, аннотирование search=search_vector - излишне?

@Кирилл_Семенихин, Да, код будет правильнее без нее. Я просто изначально не правильно вас понял.

Post.published.annotate(rank=SearchRank(search_vector, search_query)).filter(rank__gte=0.3).order_by('-rank')

Поправили в лекции.

Изменен Илья Перминов