Рубрики
Без рубрики

Полный текстовый поиск в Джангу с PostgreSQL

Статья основана на моем разговоре о полнотекственном поиске в Джангу с PostgreSQL. Теги с Fulltextsearch, Django, Postgres, Python.

Статья основана на моем разговоре о полнотекственном поиске в Джангу с PostgreSQL.

Я дал этот разговор в: Pycon IT 2017. , Europhton 2017. , Pgday. IT 2017. , Пирома 2017.

Цель

Чтобы показать, как я использовал Джанго Полнотекстовый поиск и PostgreSQL В оформлении реальный проект Отказ

Мотивация

Реализовать Полнотекстовый поиск Используя только Джанго и PostgreSQL , не прибегая к внешние продукты Отказ

Содержание

Это основные темы этой статьи:

  • Полнотекстовый поиск в целом
  • внешние продукты Для полнотекстового поиска
  • Полнотекстовый поиск поддержки в PostgreSQL
  • Джанго Поддержка полнотекстового поиска
  • ContockiRoma.com реальный проект
  • Предстоящие инновации в полнотекстовом поиске
  • Некоторые личные выводы
  • Полезно Ресурсы

Полнотекстовый поиск

Полный текстовый поиск вытекает из необходимости выполнять некоторые поиски в компьютерных документах. Например, чтобы найти все документы, которые содержат конкретные слова и их вариации. Если документ содержит «Дом» или «Дома», это будет одинаково для поиска.

«… полнотекстовый поиск * относится к методам для поиск Один компьютер, хранящийся на компьютере документ или Коллекция В оформлении Полнотекстовая база данных … “

Википедия

* FTS = F ull- T выпуклый S узел

Особенности FTS

Это список некоторых функций, которые мы можем найти в довольно продвинутом полнотекстовом поиске, который будет использоваться на веб-сайте реального мира.

  • Stemming.
  • Рейтинг
  • Удаление стоп-слов
  • Поддержка нескольких языков
  • Поддержка акцента
  • Индексация
  • Поиск фразы

Проверенные продукты

Elastic и Solr – два программных программа для полнотекстового поиска, которые являются популярными сегодня. Есть другие, но это единственные, которые я использовал в моих профессиональных проектах. Это Луси, основанные и написанные в Java.

  • Elasticsearch
  • Apache Solr.

Elasticsearch

Snap Market.

Snap Market был запуском, где я работал, который произвел приложение мобильного телефона для покупки и продажи использованных предметов, причем около полумиллиона мобильных пользователей.

Проблемы

Проблемы управления

В этом проекте мы использовали Elasticsearch который уже был создан в системе FreeBSD. У нас было трудно управлять и синхронизировать его.

Исправления плагинов Java

Нам пришлось применить несколько патчей на плагин Java, который мы использовали для «скомпонуты» слов на немецком языке.

Ява

@@ -52,7 +52,8 @@ public class DecompoundTokenFilter … {
-            posIncAtt.setPositionIncrement(0);
+            if (!subwordsonly)
+                posIncAtt.setPositionIncrement(0);
             return true;
         }

Apache Solr.

Головки

Goalscout.com – это веб-сайт, посвященный демонстрации спортивных видео, загруженных пользователей общедоступными, причем около 30 000 видео.

Проблемы

Проблемы синхронизации

Использование SOLR для полнотекстового поиска в этом проекте было выбором клиента. У нас всегда были некоторые проблемы, синхронизирующие данные между PostgreSQL и Solr.

В одну сторону

После этих проблем мы должны были начать делать все пишет к PostgreSQL И все Читает от Apache Solr. .

Существующие продукты

Плюс

  • Полнофункциональный продукты
  • Ресурсы (Документы, статьи, …)

Продукты, которые я пишу о полнофункциональных и продвижении, есть много онлайн-ресурсов в отношении IT: Документы, статьи и Часто ответы на вопросы.

Господин

  • Синхронизация
  • Обязательное использование Водитель ( HayStack, Bungiesearch, …)

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

Опс ориентирован

Фокус находится на системных интеграциях. Я больше #dev, чем #ops Поэтому я не люблю быть вынужденным интегрировать различные системы. Я предпочитаю развивать и решение проблем, написав код Python.

FTS в PostgreSQL

PostgreSQL поддерживает полнотекстовый поиск с 2008 года. Внутренне он использует тип данных «Tsvecor» и «TsQuery» для обработки данных для поиска. Он имеет некоторые индексы, которые могут быть использованы для ускорения поиска: Джин и Гист. PostgreSQL Добавлена поддержка фраза поиска в 2016 году. Поддержка JSON [B] полнотекстового поиска была добавлена в версии 10.

  • Поддержка FTS С Версия 8.3 (2008)
  • Tsvector представлять Текстовые данные
  • Цвательство представлять Поиск предикатов
  • Специальные индексы ( Джин , Gist )
  • Поиск фразы С Версия 9.6 (2016)
  • FTS для JSON [B] С Версия 10 (2017)

Что такое документы

«Документ» – это общая концепция, используемая в полнотекственном поиске, и где выполняется поиск. В базе данных документ может представлять собой поле на столе, комбинацию многих полей в таблице или в разных таблицах.

«… A Документ – это единица поиск в A полнотекстовый поиск система; например, журнал Статья или электронная почта Сообщение .. “

PostgreSQL Документация

Поддержка Джанго

Функции

Модуль Django.contrib.postgres содержит поддержку полнотекстового поиска в Django, поскольку версия 1.10. Индексы Brin и Gin были добавлены в версии 1.11. Индекс GIN очень полезен для ускорения полнотекстового поиска.

  • Модуль django.contrib.postgres
  • Поддержка FTS Поскольку версия 1.10 (2016)
  • Брин и Джин Индексы с версии 1.11 (2017)

Ориентирован с DEV

Фокус на программировании. Использование Postgres Full-Text Search в Django является более удобным для разработчиков.

Делать запросы

Мы можем посмотреть функции полнотекстового поиска в Django, начиная с моделей, присутствующих в официальной документации Django. У нас есть блог, а класс авторского класса, подключенный через класс входа, Django ORM создает таблицы и строить запросы.

Питон

from django.contrib.postgres.search import SearchVectorField
from django.db import models


class Blog(models.Model):
    name = models.CharField(max_length=100)
    tagline = models.TextField()
    lang = models.CharField(max_length=100, default='english')

    def __str__(self):
        return self.name


class Author(models.Model):
    name = models.CharField(max_length=200)
    email = models.EmailField()

    def __str__(self):
        return self.name


class Entry(models.Model):
    blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
    headline = models.CharField(max_length=255)
    body_text = models.TextField()
    pub_date = models.DateField(auto_now_add=True)
    mod_date = models.DateField(auto_now=True)
    authors = models.ManyToManyField(Author)
    n_comments = models.IntegerField(default=0)
    n_pingbacks = models.IntegerField(default=0)
    rating = models.IntegerField(default=5)
    search_vector = SearchVectorField(null=True)

    def __str__(self):
        return self.headline

Стандартные запросы

Это основные поиски, которые мы можем использовать на моделях в Django, используя «фильтр».

Питон

from blog.models import Author, Blog, Entry

Author.objects.filter(
  name__contains='Terry'
).values_list('name', flat=True)

SQL.

SELECT "blog_author"."name"
  FROM "blog_author"
 WHERE "blog_author"."name"::text LIKE '%Terry%'
['Terry Gilliam', 'Terry Jones']

Мы можем использовать «кондиционер для конкретного случая», чтобы получить больше результатов.

Питон

Author.objects.filter(
  name__icontains='ERRY'
).values_list('name', flat=True)

SQL.

SELECT "blog_author"."name"
  FROM "blog_author"
 WHERE UPPER("blog_author"."name"::text) LIKE UPPER('%ERRY%')
['Terry Gilliam', 'Terry Jones', 'Jerry Lewis']

Безумный запрос

Активируя unactcent PostgreSQL модуль, мы можем использовать расширение «Безватое».

Питон

from django.contrib.postgres.operations import UnaccentExtension


class Migration(migrations.Migration):
    ...

    operations = [
        UnaccentExtension(),
        ...
    ]

SQL.

CREATE EXTENSION unaccent;

Мы можем искать, не беспокоясь о акцентированных персонажах, полезных на разных языках.

Питон

Author.objects.filter(
  name__unaccent='Helene Joy'
).values_list('name', flat=True)

SQL.

SELECT "blog_author"."name"
  FROM "blog_author"
 WHERE UNACCENT("blog_author"."name") = (UNACCENT('Helene Joy'))
['Hélène Joy']

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

Триграмма сходства

Активая модуль Trigram PostgreSQL, мы можем использовать расширение «Trigram».

Питон

from django.contrib.postgres.operations import TrigramExtension


class Migration(migrations.Migration):
    ...

    operations = [
        TrigramExtension(),
        ...
    ]

SQL.

CREATE EXTENSION pg_trgm;

«Trigram» – это группа трех последовательных персонажей, взятых из строки. Мы можем оценить сходство двух строк по количеству «триграмм», которые они делятся.

Питон

Author.objects.filter(
  name__trigram_similar='helena'
).values_list('name', flat=True)

SQL.

SELECT "blog_author"."name"
  FROM "blog_author"
 WHERE "blog_author"."name" % 'helena'
['Helen Mirren', 'Helena Bonham Carter']

Поиск поиска

Это базовый поиск поиска Django и с этим мы можем выполнить реальный полнотекстовый поиск на поле.

Питон

Entry.objects.filter(
  body_text__search='Cheese'
).values_list('headline', flat=True)

SQL.

SELECT "blog_entry"."headline"
  FROM "blog_entry"
 WHERE to_tsvector(COALESCE("blog_entry"."body_text", ''))
       @@ (plainto_tsquery('Cheese')) = true
['Cheese on Toast recipes', 'Pizza Recipes']

Для выполнения более сложного запроса мы должны использовать три новых объекта Django: SearchVector , Поисковый запрос , SearchRank. .

SearchVector

Мы можем использовать «SearchVector», чтобы создать наш документ на более полях того же объекта или подключенных объектов. Затем мы можем фильтровать на документе со строкой.

Питон

from django.contrib.postgres.search import SearchVector

search_vector = SearchVector('body_text', 'blog__name')

Entry.objects.annotate(
  search=search_vector
).filter(
  search='Cheese'
).values_list('headline', flat=True)

SQL.

SELECT "blog_entry"."headline"
  FROM "blog_entry"
 INNER JOIN "blog_blog"
    ON ("blog_entry"."blog_id" = "blog_blog"."id")
 WHERE to_tsvector(
         COALESCE("blog_entry"."body_text", '') || ' ' ||
         COALESCE("blog_blog"."name", '')
       ) @@ (plainto_tsquery('Cheese')) = true
['Cheese on Toast recipes', 'Pizza Recipes']

Поисковый запрос

Когда мы вставляем текст в полнотекстовый поиск, используя «поисковый запрос», мы можем применить «stemming» и «удаление стоп-слов» даже для пользовательских текстов. И к этому мы можем применять основные логические операции.

НЕТ

Питон

from django.contrib.postgres.search import SearchQuery

search_query = ~SearchQuery('toast')
search_vector = SearchVector('body_text')

Entry.objects.annotate(
  search=search_vector
).filter(
  search=search_query
).values_list('headline', flat=True)

SQL.

SELECT "blog_entry"."headline"
  FROM "blog_entry"
 WHERE to_tsvector(COALESCE("blog_entry"."body_text", ''))
       @@ (!!(plainto_tsquery('toast'))) = true
['Pizza Recipes', 'Pain perdu']

ИЛИ

Питон

search_query = SearchQuery('cheese') | SearchQuery('toast')
search_vector = SearchVector('body_text')

Entry.objects.annotate(
  search=search_vector
).filter(
  search=search_query
).values_list('headline', flat=True)

SQL.

SELECT "blog_entry"."headline"
  FROM "blog_entry"
 WHERE to_tsvector(COALESCE("blog_entry"."body_text", ''))
       @@ ((
         plainto_tsquery('cheese') ||
         plainto_tsquery('toast')
       )) = true
['Cheese on Toast recipes', 'Pizza Recipes']

И

Питон

search_query = SearchQuery('cheese') & SearchQuery('toast')
search_vector = SearchVector('body_text')

Entry.objects.annotate(
  search=search_vector
).filter(
  search=search_query
).values_list('headline', flat=True)

SQL.

SELECT "blog_entry"."headline"
  FROM "blog_entry"
 WHERE to_tsvector(COALESCE("blog_entry"."body_text", ''))
       @@ ((
         plainto_tsquery('cheese') &&
         plainto_tsquery('toast')
       )) = true
['Cheese on Toast recipes']

SearchRank.

Мы можем использовать PostgreSQL «Rank», чтобы рассчитать счет документа по отношению к поисковому тексту, и мы можем использовать его для фильтрации и отсортировать его.

Питон

from django.contrib.postgres.search import SearchRank

search_vector = SearchVector('body_text')
search_query = SearchQuery('cheese')
search_rank = SearchRank(search_vector, search_query)

Entry.objects.annotate(
  rank=search_rank
).order_by(
  '-rank'
).values_list('headline', 'rank'))

SQL.

SELECT "blog_entry"."headline",
       ts_rank(
         to_tsvector(COALESCE("blog_entry"."body_text", '')),
         plainto_tsquery('cheese')
       ) AS "rank"
  FROM "blog_entry"
 ORDER BY "rank" DESC
[
  ('Cheese on Toast recipes', 0.0889769),
  ('Pizza Recipes', 0.0607927),
  ('Pain perdu', 0.0)
]

Конфигурация поиска

Мы можем настроить «вектор поиска» или «поисковый запрос» для выполнения «stemming» или «удаление слов« Стоп »для определенного языка.

Питон

language = 'french'

search_vector = SearchVector('body_text', config=language)
search_query = SearchQuery('œuf', config=language)

Entry.objects.annotate(
  search=search_vector
).filter(
  search=search_query
).values_list('headline', flat=True)

SQL.

SELECT "blog_entry"."headline"
  FROM "blog_entry"
 WHERE to_tsvector(
         'french'::regconfig,
         COALESCE("blog_entry"."body_text", ''))
       @@ (
         plainto_tsquery('french'::regconfig, 'œuf')
       ) = true
['Pain perdu']

Мы можем получить язык из класса.

Питон

from django.db.models import F

language = F('blog__lang')

search_vector = SearchVector('body_text', config=language)
search_query = SearchQuery('œuf', config=language)

Entry.objects.annotate(
  search=search_vector
).filter(
  search=search_query
).values_list('headline', flat=True)

SQL.

SELECT "blog_entry"."headline"
  FROM "blog_entry"
 INNER JOIN "blog_blog"
    ON ("blog_entry"."blog_id" = "blog_blog"."id")
 WHERE to_tsvector(
         "blog_blog"."lang"::regconfig,
         COALESCE("blog_entry"."body_text", '')
       )
       @@ (
         plainto_tsquery("blog_blog"."lang"::regconfig, 'œuf')
       ) = true
['Pain perdu']

Запрашивает взвешивание

Можно настроить поиск, чтобы дать другой вес в различные поля, а затем использовать эти значения в поисках.

Питон

search_vector = SearchVector('body_text', weight='A') +
                SearchVector('headline', weight='B')
search_query = SearchQuery('cheese')
search_rank = SearchRank(search_vector, search_query)

Entry.objects.annotate(
  rank=search_rank
).order_by(
  '-rank'
).values_list('headline', 'rank')

SQL.

SELECT "blog_entry"."headline",
       ts_rank((
         setweight(
           to_tsvector(COALESCE("blog_entry"."body_text", '')),
           'A'
         )
         ||
         setweight(
           to_tsvector(COALESCE("blog_entry"."headline", '')),
           'B'
         )
       ), plainto_tsquery('cheese')) AS "rank"
  FROM "blog_entry"
 ORDER BY "rank" DESC
[
  ('Cheese on Toast recipes', 0.896524),
  ('Pizza Recipes', 0.607927),
  ('Pain perdu', 0.0)
]

Searchvectorfield.

Если мы хотим ускорить и упростить выполнение запроса, мы можем добавить «поле вектор поиска» к модели, а затем выполнить поиск на этом определенном поле.

Питон

Entry.objects.filter(
  search_vector='cheese'
).values_list('headline', flat=True)

SQL.

SELECT "blog_entry"."headline"
  FROM "blog_entry"
 WHERE "blog_entry"."search_vector"
       @@ (plainto_tsquery('cheese')) = true
['Cheese on Toast recipes', 'Pizza Recipes']

Мы должны обновить это поле вручную, например, периодически выполнять команду, используя сигналы Django или с помощью триггеров PostgreSQL.

Питон

search_vector = SearchVector('body_text')

Entry.objects.update(search_vector=search_vector)

SQL.

UPDATE "blog_entry"
   SET "search_vector" = to_tsvector(
         COALESCE("blog_entry"."body_text", ''))

www.concertiaroma.com

“… Сегодняшние шоу в столице “

www.concertiaroma.com Это веб-сайт, используемый для поиска шоу, фестивалей, групп и мест в городе Рима и был в сети с 2014 года.

Числа проекта:

  • Около 1000 промежуток времени
  • около 15 000 группы
  • Более 16 000 выставка
  • Около 200 фестивали
  • около 30 000 Пользователь/месяц

Версия 2.0

Старая версия сайта была разработана несколько лет назад с Django 1.7 и работает на Python 2.7. Данные управлялись PostgreSQL версии 9.1, и поиск был выполнен с помощью SQL КАК синтаксис.

  • Python 2.7
  • Джанго 1.7
  • PostgreSQL 9.1
  • SQL КАК

Версия 3.0

Новая версия, недавно выпущенная, была разработана с Django 1.11 и работает на Python 3.6. Данные управляются PostgreSQL 9.6 и поиск использует его полнотекстовую поисковую систему.

  • Python 3.6
  • Джанго 1.11
  • PostgreSQL 9.6
  • PG Ребята

Модели группы

Мы можем посмотреть на функции полнотекстового поиска в www.concertiaroma.com Начиная с тех же моделей, присутствующих в проекте. У нас есть ** жанр * Класс подключен к Группа класс. *

Питон

from django.db import models
from .managers import BandManager


class Genre(models.Model):
    name = models.CharField(max_length=255)


class Band(models.Model):
    nickname = models.CharField(max_length=255)
    description = models.TextField()
    genres = models.ManyToManyField(Genre)

    objects = BandManager()

Менеджер группы

Это пример «менеджера» для класса группы, который определяет метод поиска, который содержит все полнотекстовые поисковые логики.

Питон

from django.contrib.postgres.aggregates import StringAgg
from django.contrib.postgres.search import (
    SearchQuery, SearchRank, SearchVector, TrigramSimilarity,
)
from django.db import models

search_vectors = (
    SearchVector('nickname', weight='A', config='english') +
    SearchVector(
        StringAgg('genres__name', delimiter=' '),
        weight='B', config='english') +
    SearchVector('description', weight='D', config='english')
)


class BandManager(models.Manager):

    def search(self, text):
        search_query = SearchQuery(text, config='english')
        search_rank = SearchRank(search_vectors, search_query)
        trigram_similarity = TrigramSimilarity('nickname', text)
        return self.get_queryset().annotate(
            search=search_vectors
        ).filter(
            search=search_query
        ).annotate(
            rank=search_rank + trigram_similarity
        ).order_by('-rank')

Настройка тестов группы

Чтобы лучше понять механизм, мы можем учитывать пример упрощенного теста.

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

Питон

from collections import OrderedDict
from django.test import TestCase
from .models import Band, Genre


class BandTest(TestCase):

    def setUp(self):
        # Genres
        blues, _ = Genre.objects.get_or_create(name='Blues')
        jazz, _ = Genre.objects.get_or_create(name='Jazz')
        swing, _ = Genre.objects.get_or_create(name='Swing')
        # Bands
        ella_fitzgerald, _ = Band.objects.get_or_create(
            nickname='Ella Fitzgerald',
            description=(
                'Ella Jane Fitzgerald (25 Apr 1917-15 Jun 1996)'
                ' was an American jazz singer often referred to'
                ' as the First Lady of Song, Queen of Jazz and '
                'Lady Ella. She was noted for her purity of '
                'tone, impeccable diction, phrasing and '
                'intonation, and a horn-like improvisational '
                'ability, particularly in her scat singing.'))
        django_reinhardt, _ = Band.objects.get_or_create(
            nickname='Django Reinhardt',
            description=(
                'Jean Django Reinhardt (23 Jan 1910-16 May 1953)'
                ' was a Belgian-born, Romani French jazz '
                'guitarist and composer, regarded as one of the '
                'greatest musicians of the twentieth century. He'
                ' was the first jazz talent to emerge from '
                'Europe and remains the most significant.'))
        louis_armstrong, _ = Band.objects.get_or_create(
            nickname='Louis Armstrong',
            description=(
                'Louis Armstrong (4 Aug 1901-6 Jul 1971), '
                'nicknamed Satchmo, Satch and Pops, was an '
                'American trumpeter, composer, singer and '
                'occasional actor who was one of the most '
                'influential figures in jazz. His career spanned'
                ' five decades, from the 1920s to the 1960s, '
                'and different eras in the history of jazz.'))
        # Bands and Genres
        ella_fitzgerald.genres.add(blues)
        django_reinhardt.genres.add(jazz)
        louis_armstrong.genres.add(blues, swing)

    def test_band_search(self):
        # ...

Содержание от “Wikipedia, бесплатная энциклопедия” :

Метод теста на полосе

В поисковом тесте на полосах мы просто вызваны метод поиска, давая текст поиска, и мы вернем список значений для поля «псевдоним» и «скорость». «Nickname» хранится на таблице диапазона, а «скорость» рассчитывается нашим методом поиска во время выполнения.

Питон

from collections import OrderedDict
from django.test import TestCase
from .models import Band, Genre


class BandTest(TestCase):

    def setUp(self):
        # ...

    def test_band_search(self):
        band_queryset = Band.objects.search(
            'jazz').values_list('nickname', 'rank')
        band_objects = list(
            OrderedDict(band_queryset).items())
        band_list = [
            ('Django Reinhardt', 0.265124),
            ('Ella Fitzgerald', 0.0759909),
            ('Louis Armstrong', 0.0759909)]
        self.assertSequenceEqual(band_objects, band_list)

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

Что дальше

Мы видели упрощенное использование текущих функций Django и PostgreSQL Full-Text Search.

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

  • Продвинутый MISPPELLING служба поддержки
  • Несколько Конфигурация языка
  • Поиск предложения
  • Searchvectorfield с триггеры
  • JSON/JSONB Полнотекстовый поиск
  • Ром индексация

Выводы

В заключение, следующее являются условиями для оценки реализации полнотекстового поиска с PostgreSQL в Джангу:

  • не иметь никаких дополнительных Зависимости
  • не делает очень сложный
  • Управление все компоненты легко
  • избегать данных Синхронизация между различными системами
  • Наличие PostgreSQL уже в стеке
  • работает в Только Python окружающая обстановка

Благодарность

20Tab.

Для всех Поддержка ( www.0tab.com )

Марк Тамлин

Для django.contrib.postgres ( github.com/mjtamlyn )

Ресурсы

Это ресурсы, которые я использовал для подготовки этой статьи и разработать функцию поиска, которую я показал вам.

Спасибо

CC BY-SA

Эта статья и соответствующая презентация выпущены с помощью Creative Commons Attribution Sharealike лицензией. creativecommons.org/licenses/by-sa

Исходный код

Я опубликовал исходный код, используемый в этой статье на GitHub. github.com/pauloxnet/django_queries

Слайды

Вы можете скачать презентацию с моей учетной записи SpeakerDeck. SpeakerDeck.com/pauloxnet.

Первоначально опубликовано на www.paulox.net/2017/12/22/full-text-search-in-django-with-postgresql.

Оригинал: “https://dev.to/pauloxnet/full-text-search-in-django-with-postgresql-ddp”