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

django full text search Для простого поиска в Django обычно хватает стандартного Q-объекта с индексом по поисковым полям. Но иногда нужны более серьезные возможности поиска, например, ранжирование, морфологический поиск, n-gram'ы, фасетная классификация, проверка синтаксиса и т.д., многое из перечисленного поддерживают в той или иной степени Sphinx, Xapian, Solr, Whoosh.

Каждый из перечисленных вариантов хорош в своем случаи, где какой применять зависит от навыков и фантазии разработчика. Сравнительный обзор некоторых решений есть тут. На страницах документации Haystack есть таблица с списком возможностей разных поисковых бэкендов и реализованными возможностями в haystack (прослойке между django и поисковыми бэкендами).

На stackoverflow.com есть тестирование скорости индексирования и поиска в sphinx и xapian.

High Performance FullText Search - презентация с сравнением некоторых полнотекстовых поисковых движков.

Встроенные средства Django

Q objects

Для начала коротко про Q-объекты, которые не относятся к полнотекстовому поиску, но не все знают как их готовить.

Поиск с объединением через И (AND)

from django.db.models import Q
Products.objects.filter(Q(title__icontains=title) & Q(category=cat))

Поиск с объединением через ИЛИ (OR)

Products.objects.filter(Q(title__icontains=title) | Q(category=cat))

Поиск с объединением через И (AND) и исключением по одному из полей

Products.objects.filter(Q(title__icontains=title) | ~Q(category=cat))

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

import operator
criterions = [Q(title__icontains=title),]
criterions.append(Q(category=cat))
Products.objects.filter(reduce(operator.or_, criterions))

Также можно использовать operator.and_ для операции AND.

Еще один вариант объединения критериев

from django.db.models import Q
criterions = Q(title__icontains=title)
criterions.add(Q(category=cat), Q.OR)
Products.objects.filter(criterions)

Надстройка над MySQL

Если у поля есть полнотекстовый индекс, то можно искать с указанием что включить в поиск, а что нет

products = Products.objects.filter(title__search='+майка -красная')

Для создания полнотекстового индекса нужно выполнить такую mysql-команду:

CREATE FULLTEXT INDEX title_idx ON products_product (title);

Этот вариант поиска работает только с таблицами типа MyISAM с полями типа CHAR, VARCHAR, и TEXT.

Sphinx

Sphinx (Sql PHrase INdeX) - система полнотекстового поиска, распространяющийся под лицензией GPL2 с поддержкой ранжирования и стемминга для русского и английского языков. Начиная с версии 1.10-beta поддерживаются два типа индексов: disk-индексы и realtime. В качестве источника данных для индексации может быть MySQL, PostgreSQL и любая другая БД которая поддерживает ODBC, также источником может быть XML.

Возможности sphinx весьма широки, с полным списком можно ознакомится тут.

Для установки под *nix можно воспользоваться уже собранными пакетами с официальной страницы скачивания.

Установка под Ubuntu 10.04 LTS

wget http://sphinxsearch.com/files/sphinxsearch_2.0.6-release-0ubuntu11~lucid_i386.deb
sudo dpkg -i sphinxsearch_2.0.6-release-0ubuntu11~lucid_i386.deb

Установка под Arch

yaourt -S sphinx

Пример сборки из исходников с поддержкой PostgreSQL, но без MySQL

wget http://sphinxsearch.com/files/sphinx-2.0.6-release.tar.gz
tar xzf sphinx-2.0.6-release.tar.gz
cd sphinx-2.0.6-release

./configure --without-mysql --with-pgsql
make
make install

Две основные составляющие Sphinx с которыми придется взаимодействовать:

  • indexer - индексатор, используется для индексирования и переиндексирования источника данных, который указывается явно при вызове индексатора либо используется указанный в конфигурационном файле /etc/sphinxsearch/sphinx.conf;
  • searchd - поисковый демон, который отвечает за поиск в индексе и возврат результатов поиска;

Конфигурационный файл (может находится в /etc/sphinxsearch/ или /etc/sphinx/) состоит из следующих секций:

  • source - описывает параметры доступа к источнику данных для последующего индексирования, в этой секции описывается sql-запрос для выборки данных из источника (параметр sql_query) и вспомогательные атрибуты объекта (параметры sql_attr_*);
  • index - описывает параметры индекса, в которых указывается: тип индекса, место хранения индекса, использование стемминга и т.д.;
  • indexer - опции для программы-индексатора;
  • searchd - опции для демона отвечающего за поиск по индексу и возвращения результатов;

Ниже приведен пример конфигурационного файла. Описанные выше секции source и index поддерживают наследование параметров, виде source derived : base {}. Первоначальный конфиг можно сгенерить с помощью командной утилиты входящей в пакет django-sphinx (расмотрен ниже) а дальше подправить в любимом редакторе.

# файл /etc/sphinx/blog.conf

source base {
    type            = mysql
    sql_host        = localhost
    sql_user        = root
    sql_pass        = qwerty
    sql_db          = blog
    sql_port        = 3306
    sql_query_pre  = SET NAMES utf8
}

source articles : base {
    sql_query = SELECT id, title, body, UNIX_TIMESTAMP(pub_date) AS pdate, status FROM articles WHERE status = 1

    sql_attr_uint = status
    sql_attr_timestamp = pdate
    sql_attr_multi = uint rubric_id from query; SELECT article_id, rubric_id FROM rubrics

    sql_query_info = SELECT id, title FROM articles WHERE id=$id
}

index articles {
    source = articles
    docinfo = extern
    path = /var/lib/sphinx/data/blog/articles

    min_word_len = 2
    charset_type = utf-8

    min_infix_len      = 2
    morphology      = stem_en, stem_ru, soundex, metaphone
    enable_star = 1
    index_exact_words = 1
}

indexer {
    mem_limit = 128M
}

searchd {
    listen = 9312
    listen = 9306:mysql41
    log = /var/log/sphinx/searchd.log
    query_log = /var/log/sphinx/query.log
    read_timeout = 5
    client_timeout = 300
    max_children = 30
    pid_file = /var/run/sphinxsearch/searchd.pid
    max_matches = 1000
    seamless_rotate = 1
    preopen_indexes = 1
    unlink_old = 1
    mva_updates_pool = 1M
    max_packet_size = 8M
    max_filters = 256
    max_filter_values = 4096
    max_batch_queries = 32
    workers = threads
}

Проиндексируем новый источник данных

indexer --config /etc/sphinx/blog.conf --all

Пример поиска из командной строки

# поиск по всем рубрикам 
search --config /etc/sphinx/blog.conf django

# поиск по рубрике с id = 3 (python)
search --config /etc/sphinx/blog.conf -f rubric_id 3 django

Запустим searchd для работы с индексом сторонними клиентами через Sphinx API. API существует для многих популярных языков программирования, в данной заметке рассматриваются примеры на python.

sudo searchd -c /etc/sphinx/blog.conf

searchd поддерживает mysql-интерфейс, пример поиска с помощью консольной утилиты mysql

mysql -h 0 -P 9306
mysql> SELECT * FROM articles WHERE MATCH('django');

Для работы с sphinx на python нам понадобится файл sphinxapi.py, который можно взять из tarball на странице скачивания. Результат запроса через API - список id найденных документов, используя которые мы можем извлечь объекты из БД. Кроме id также возвращается список атрибутов, описанных в секции source и вес каждого документа, чем выше вес - тем более релевантнее документ по отношению к запросу.

Пример поиска по django-модели Article. Сначала находим id статей, которые храняться в индексе sphinx'a. Затем ищем реальные стати по id с помощью стандартного django orm.

import sphinxapi
from articles.models import Article

weights = {
    'title': 100,
    'body': 80
}

# подключение 
c = sphinxapi.SphinxClient()
c.SetServer('localhost', 9312)
c.SetConnectTimeout(2.0)

# режим совпадения слов из запроса с существующими статьями
c.SetMatchMode(sphinxapi.SPH_MATCH_ANY)

# режим сортировки
c.SetSortMode(sphinxapi.SPH_SORT_RELEVANCE)

# веса для полей модели
c.SetFieldWeights(weights)

# установить фильтр по рубрике с id = 3
c.SetFilter('rubric_id', (3,))

# ограничить результат 30 совпадениями
c.SetLimits(0, 30)

# поиск слова django по индексу articles
result = c.Query('django', 'articles')

# выборка объектов
ids = [obj['id'] for obj in result['matches']]
articles = Article.objects.filter(id__in=ids)

Режимы совпадений описаны в Matching modes.

Режимы сортировки описаны в Sorting modes.

Для упрощения интеграции sphinx и django можно воспользоваться django-sphinx от FactorAG или django-sphinx от Fak3. Приведенная версия поддерживает последние версии sphinx из линейки 2.*.

Для начало нужно добавить в settings.py указатель на используемою версию Sphinx'а

# Sphinx 2.0.6
SPHINX_API_VERSION = 0x119

Пример описания модели для django.

from djangosphinx import SphinxSearch

class Article(models.Model):
    title = models.CharField('Title', max_length=200)
    body = models.TextField('Body')
    pub_date = models.DateTimeField('Publication date')
    status = models.PositiveIntegerField('Status')
    rubric = models.ForeignKey(Rubric, blank=False, null=False)

    search = SphinxSearch(
           index = 'articles', 
           weights = { 
               'title': 100,
               'body': 80,
           }
       )

Сгенерим конфиг с источником данных

./manage.py generate_sphinx_config [app_name] >> blog.conf

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

sudo cp ~/projects/blog/blog.conf /etc/sphinx/
sudo indexer --config /etc/sphinx/blog.conf --all

Простой пример поиска, без указания дополнительных опций

from articles.models import Article
results = Article.search.query('django')

Морфология

Примеры charset_table для украинского и русских языков:

Пример построения словоформ для русского языка из словарей myspell, ispell, pspell, aspell.

Кузявые ли бутявки, т.е. пишем морфологический анализатор на Python

Словари украинского языка от проекта phpMorphy, библиотеки морфологического анализа.

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

index articles {
    source              = articles
    path                = /var/lib/sphinx/data/blog/articles
    docinfo             = extern
    min_word_len        = 2
    min_infix_len      = 2
    charset_type        = utf-8
}

Т.е. будут индексироваться все части слов начиная с 2-х символов.

Сортировка по текстовому полю

Для сортировки по текстовому полю нужно создать числовое представление этого поля, нужно добавить в секцию source.

sql_query = SELECT id, title as title_ordinal, title as title FROM articles;
sql_attr_str2ordinal = title_ordinal
sql_field_string = title

Получаем следующие: сортировка по полю title_ordinal, а поиск по title.

Подсветка найденного текста

Пример поиска по Articles и подсветка найденного совпадения

Articles.search.query(query).filter(**criterions).set_options(passages=True, passages_opts={
    'before_match': "<span class='highlight'>",
    'after_match': '</span>',
    'chunk_separator': ' ... ',
    'around': 10
     })

Получить снипет в python-коде можно с помощью такого вызова

thearticle.sphinx.get('passages')

или в шаблоне

{{thearticle.sphinx.passages.body|safe}} 

Индексы

Дополнительное чтиво

Solr

Xapian

Whoosh

Дополнительное чтиво

Цитата
Сказать "Я тебя люблю" займет несколько секунд, показать как - всю жизнь...
Категории
Архив