Для простого поиска в 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
Дополнительное чтиво