Использование библиотеки для полнотекстового поиска Xapian в Python

Сегодня, в эпоху Web 2.0, когда контента на сайтах становится все больше и больше, перед разработчиками встает задача реализации полнотекстового поиска.

Вариантов немного:
  • использовать виджеты от разработчиков поисковых систем (Google, Яндекс, etc): легко внедрить, привычный для пользователя интерфейс, поддержка морфологии, исправление слов по словарю, возможно более быстрая индексация сайта поисковыми системами, но, как правило ограниченные возможности по настройке и неизбежное запаздывание индексации;
  • использовать встроенные в СУБД средства (например FULLTEXT-индекс для MySQL): достаточно легко внедрить, актуальный поисковый индекс, полный контроль над настройкой и внешним видом, но, чаще всего очень низкая производительность на больших объемах данных, отсутствие учета морфологии, либо, в худшем случае, полное отсутствие подобных средств в СУБД;
  • использовать отдельную библиотеку/систему полнотекствого поиска.

Третий вариант кажется самым лучшим, ведь он сочетает достоинства двух других вариантов. Правда и здесь не обошлось без недостатков — библиотека требует установки, иногда даже запуска демона (например Sphinx), что может быть неприемлемо.

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

Обзор


Эта открытая (GPL) кроссплатформенная библиотека написана на C++, имеются привязки к Python, PHP, Ruby, Perl, Java, Tcl, и C#.

Особенности библиотеки:
  • полная поддержка юникода;
  • булевый поиск, поиск с ранжированием, по маске, синонимам, есть поддержка сортировки результатов;
  • поддержка стемминга для 15 языков мира (включая русский);
  • поддержка коррекции запроса по словарю (например запрос xapain будет заменен на xapian)
  • поиск документов по подобию;
  • поддержка индексации документов в разных форматах из коробки (PDF, HTML, RTF, Microsoft Office, OpenDocument, даже пакеты RPM и Debian), несложно добавить фильтры для поддержки нового формата.

В некотором смысле основным недостатком Xapian являются привязки к языкам программирования отличным от C++. Для генерации кода привязки используется SWIG, поэтому API в ней полностью совпадает с таковым у версии для C++.

К счастью для Python есть простая и эффективная обертка Xappy которая берет на себя всю грязную работу.

Установка


Первым делом нужно установить сам Xapian, привязку к Python и Xappy. В репозиториях большинства дистрибутивов GNU/Linux уже есть все нужные пакеты, например в Ubuntu 10.10 нужно установить пакеты:
sudo apt-get install libxapian15 python-xapian python-xappy

Xappy также доступен через easy_install или pip:
sudo pip install xappy

Индексация


Попробуем что-нибудь проиндексировать:
import xappy

# открытие соединения для индексации с базой поискового индекса
# указывается полный или относительный путь к папке
connection = xappy.IndexerConnection('/path/to/base')

# свойства полей индекса
connection.add_field_action(
    'title', xappy.FieldActions.INDEX_FREETEXT, weight=5, language='ru')
connection.add_field_action(
    'description', xappy.FieldActions.INDEX_FREETEXT, language='ru')

При открытии соединения для индексации будет создана новая (или открыта уже существующая) база поискового индекса — папка с набором файлов. Формат базы независим от операционной системы.

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

Тип поля может быть:
  • INDEX_FREETEXT: в поле хранится текст, требуется создать лишь индекс без хранения самого текста. Для этого типа поля можно указать дополнительные атрибуты, в примере language='ru' для учета морфологии языка и weight=5 — «вес» поля при ранжировании;
  • INDEX_EXACT: в поле хранится точное значение слова (подойдет для поиска точных значений, например идентификаторов книг), текст хранится в индексе;
  • SORTABLE: по полю будет происходить сортировка. По умолчанию сортировка идет в лексикографическом формате независимо от того что в нем хранится. Такое поведение можно изменить через атрибут type='date' для сортировки дат (в формате YYYYMMDD, YYYY-MM-DD или YYYY/MM/DD) и type='float' для сортировки вещественных чисел (в любом поддерживаемом Python формате);
  • COLLAPSE: по полю будет происходить группировка (аналог GROUP BY в SQL, например найти по одному наиболее подходящему под запрос документу в каждой категории);
  • STORE_CONTENT: аналогично INDEX_FREETEXT только текст тоже хранится в индексе.

Для добавления документа подойдет такой код:
# создание нового документа
doc = xappy.UnprocessedDocument()

# заполнение полей
doc.fields.append(xappy.Field('title', 'Какой хороший день'))
doc.fields.append(xappy.Field('description', 'Моя струится лень'))

# добавление в индекс
connection.add(doc)

У каждого документа должен быть уникальный идентификатор, в примере выше он будет добавлен автоматически, однако можно указать свой:
# например индексируем посты в блоге

for posts_item in posts:
    doc = xappy.UnprocessedDocument()

    # не забывайте что идентификатор должен быть 
    # уникальным для поискового индекса!
    doc.id = posts_item.id
    doc.fields.append(xappy.Field('title', posts_item.title))
    doc.fields.append(xappy.Field('description', posts_item.description)

    connection.add(doc)

После добавления документов нужно обязательно записать все изменения на диск и закрыть соединение:
connection.flush()
connection.close()

Все, индекс создан!

Поиск


Для поиска по имеющемуся индексу требуется открыть соединение для поиска по базе поискового индекса:
import xappy
 
# открытие соединения для поиска с базой поискового индекса
# указывается полный или относительный путь к папке
connection = xappy.SearchConnection('/path/to/base')

Возможна ситуация когда новые документы были проиндексированы уже после открытия поискового соединения. В этом случае нужно переоткрыть соединение для получения доступа к актуальной базе:
connection.reopen()

Есть несколько методов для выполнения поискового запроса (класс SearchConnection), самым простым является query_parse:
# обычный поисковый запрос
query = connection.query_parse('день')

# нужны лишь первые 10 результатов
# для следующей десятки нужно указать 10, 20 и т.д.
results = connection.search(query, 0, 10)

# что-то нашлось
if results.matches_estimated > 0:
    for results_item in results:
        print(results_item.rank, results_item.id)
else:
    print('Ничего не найдено')

Для полей с типом STORE_CONTENT или INDEX_EXACT можно вывести их содержимое, что позволяет, например, не выбирать из основной базы данных выбранные записи по ID, а обойтись лишь поисковым индексом:
for results_item in results:
    print(results_item.data['title'])


Ссылки по теме


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

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 19

    +1
    А есть ли какие-либо замеры производительности на разных размерах базы (100, 100 тыщ, 100 тыщ мильёнов документов)?
      0
      К сожалению не нашел, постараюсь провести собственное исследование
        0
        В сравнении производительности Xapian, Sphinx и Lucence написано про реальное использование Xapian на объемах 1.5Тб, и миллионах документов.
          +1
          Я когда-то бенчил Xapian. Цифр нет, помню ряд занятных фактов. Disclaimer: это было минима год назад, ситуация могла измениться.

          1. Первые 10 документов в порядке тн. релевантности (vanilla BM25) выдавались дико быстро. Видимо, для каждого слова хранится топ-N документов в порядке убывания частоты. Значение N выяснять не стал.

          2. Число матчей при этом втупую врало, сильно. Вместо точного числа оно при таком поиске делает оценку, оценка получается слишком грубая.

          3. Поиск фразы из двух слов резко исправляет ситуацию, все очень медленно.

          4. Допсортировка по атрибутам либо отсутствует, либо опять все очень медленно.

          По результатам этих простейших тестов плотнее мерить и глубже рыть не стал, тк. порядок скорости был сразу не тот. За исключением ровно одного юзкейса «дай топ-10 документов по bm25, соври в разы про количество совпадений».
        +3
        Хм, уже про него в черновках пишу, выложу чуть позже как прикручивать к C#. C тестами производительности на 700.000 индексированных документах.
          0
          К сожалению, как правило, поиск Google «site:example.com keyword» дает заметно более релевантные результаты, чем любой встроенный поисковый движок.

          Но одно из важных применений «доморощенных» поисковиков — это параметрический поиск («хочу нечто белое, гладкое, не больше 55 см в ширину, искать только в данной категории»).
          Очень интересно, годится ли Xapian для этого?
            +1
            Не соглашусь. Проиндексировать сайт с закрытым контентом не получится. Да и в случае «доморощенного» поисковика всё управление настройками релевантности в ваших руках.

            У Xapian есть поддержка фасеточного поиска, который, я думаю, можно адаптировать под подобные запросы
              0
              Fulltext search используется далеко не только в поиске по сайту. Например, чтобы заменить 50-60 строчные sql-запросы сложные для переваривания sql-сервером.
                0
                Также не забывайте, что область применения не ограничивается лишь веб-приложениями. Например Xapian используется в aptitude — оболочке для системы управления пакетами в debian-based дистрибутивах GNU/Linux
                0
                Я бы с вами согласился раньше. Но у меня возникли обстоятельства в которых поиск через google по сайту давал ужасные результаты. Дело в том, что на большинстве страниц моего сайта есть блоки с ссылками на другие статьи на нем же. При поиске через google больше всего попадались те страницы на которых на момент индексации поисковиком стоит ссылка на нужную страницу, а не сама страница, которую ищет посетитель. Положение спас sphinx, который индексировал только текст материала, без служебных и интерфейсных блоков.
                0
                Это случайно не та либа, которая использует лайт для базы и не может рабоать в тредах?
                0
                Я вот одного не понял. Вы предлагаете использовать Xapian потому что «библиотека требует установки, иногда даже запуска демона (например Sphinx), что может быть неприемлемо». Но при этом сами ставите библиотеку, даже несколько разных. Если уж есть sudo, то тут никаких проблем хоть со сфинксом, хоть с чем.
                Как встраиваемое решение по-моему гораздо интереснее что-то типа Zend_Search_Lucene (для python тоже вроде был аналог). Использовал его на практике, не очень шустро, но для тех сайтов что держат на shared-хостинге достаточно. Единственное, в ZF нет в комплекте русского стеммера, но это не проблема.
                  0
                  Читайте внимательнее, я писал что это как раз является недостатком большинства подобных решений.

                  Насколько я помню, у Zend_Search_Lucene нет стемминга. Для Python есть нечто подобное — Whoosh. Не спорю, для некоторых конфигураций это может быть лучший вариант
                    +1
                    Извините, значит я вас не так понял. Вот эта фраза «Решений существует масса, у каждого есть свои достоинства и недостатки. Я бы хотел подробнее остановиться на относительно малоизвестной библиотеке Xapian.» в моем понимании предполагает что вы решаете перечисленные выше проблемы.

                    Стемминга стандартно в Zend_Search_Lucene нет, но приделывается он туда очень быстро. если надо могу дать адаптированный под стиль зенд стеммер портера (snowball).
                      0
                      Конечно, скиньте ссылку в комментах, наверняка кому-то пригодится. Спасибо!
                        +2
                        Выложил сам код стеммера на pastebin pastebin.com/53pw9Hy0
                        А чтобы подключить нужно сделать примерно так:
                            $analyzer = new Zend_Search_Lucene_Analysis_Analyzer_Common_Utf8_CaseInsensitive();
                            $analyzer->addFilter(new Application_Search_Stemmer_Russian());
                            Zend_Search_Lucene_Analysis_Analyzer::setDefault($analyzer);


                        * This source code was highlighted with Source Code Highlighter.
                  0
                  Благодарю за подробный разбор. Попробую присобачить к проекту на flask.

                  Only users with full accounts can post comments. Log in, please.