Для простого поиска в 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 с которыми придется взаимодействовать:
Конфигурационный файл (может находится в /etc/sphinxsearch/ или /etc/sphinx/) состоит из следующих секций:
Ниже приведен пример конфигурационного файла. Описанные выше секции 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
Дополнительное чтиво