Игорь Чакрыгин Игорь Чакрыгин

У любой задачи существует по крайней мере одно очевидное и невероятно простое для понимания неправильное решение

Sphinx - Источники данных в формате xml

В предыдущих статьях я рассказал о том, что в качестве источника данных для Sphinx можно использовать базы данных, в частности MS SQL Server. Во многих случаях этого наиболее удобный способ построения индекса, поскольку Sphinx может получить все данные одним запросом. Даже на очень больших объёмах данных этот запрос можно разбить на несколько ranged-запросов и получать данные из базы небольшими порциями.

Но данные в базе могут иметь сложную структуру со множеством связей, и получить их одним запросом может быть сложно. К тому же может потребоваться получать данные из источника, который Sphinx просто не поддерживает. Наиболее универсальным источником данных для Sphinx являются данные в формате xml. Именно об этом я и хочу рассказать в этой статье.

Пример 1: Индексация xml-файла

Для начала рассмотрим самый простой пример, когда индексируемые данные находятся в xml-файле, а схема индекса описывается в файле конфигурации.

Шаг 1. Создадим файл product.xml (я поместил его в папку c:\sphinx\data\source\) с документами, которые собираемся индексировать.

<?xml version="1.0" encoding="utf-8"?>
<sphinx:docset xmlns:sphinx="http://sphinxsearch.com/">

  <sphinx:document id="849">
    <name>Men's Sports Shorts</name>
    <price>24.7459</price>
  </sphinx:document>
  
  <sphinx:document id="852">
    <name>Women's Tights</name>
    <price>30.9334</price>
  </sphinx:document>
  
  <sphinx:document id="855">
    <name>Men's Bib-Shorts</name>
    <price>37.1209</price>
  </sphinx:document>
  
</sphinx:docset>

Как вы видите, корневым элементом xml-файла является элемент sphinx:docset, в котором при помощи элементов sphinx:document описаны индексируемые документы. Каждый документ имеет уникальный атрибут id и несколько вложенных элементов, названия которых соответствуют названиям полей и атрибутов в схеме индекса.

Шаг 2. Опишем источник данных и индекс в файле конфигурации.

searchd
{
    listen      = 9306:mysql41
    pid_file    = c:/sphinx/data/searchd.pid
    log         = c:/sphinx/data/log/log.txt
    query_log   = c:/sphinx/data/log/query_log.txt
    binlog_path = c:/sphinx/data/binlog/
}

source product
{
    type                 = xmlpipe2
    xmlpipe_command      = type c:\sphinx\data\source\product.xml
    xmlpipe_field_string = name
    xmlpipe_attr_float   = price
}

index product 
{
    source       = product 
    path         = c:/sphinx/data/index/product
    charset_type = utf-8
}

Блоки searchd и index product вряд ли нуждаются в пояснении. Они полностью аналогичны тому, что мы делали при индексации базы данных. Основной интерес представляет блок source product.

Опция type указывает тип источника данных. Sphinx поддерживает два типа источников данных в формате xml: xmlpipe и xmlpipe2. Однако тип xmlpipe считается уже устаревшим поэтому мы использует тип xmlpipe2.

Опция xmlpipe_command указывает команду для получения данных. Эта команда должна выводить xml-документ в стандартный выходной поток. Мы используем команду type, которая выводит содержимое файла, название которого передано ей в качестве аргумента. Убедиться в этом можно, если ввести эту команду в командную строку.

type c:\sphinx\data\source\product.xml

Опций xmlpipe_field_string и xmlpipe_attr_float определяют поле name и атрибуты name и price, которые будут содержаться в индексе. Полный список всех опций для определения полей и атрибутов можно найти в документации.

Шаг 3. Выполним индексацию.

c:\sphinx\bin\indexer product --config c:\sphinx\data\config.txt --rotate

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

select id, name, price from product;

Пример 2: Описание схемы в индексируемом xml-файле

Отличительной особенностью источников данных в формате xml является то, что схема индекса (набор полей и атрибутов) может быть описана не только в файле конфигурации, но и в самих данных. Это весьма полезная возможность, поскольку она позволяет изменять схему индекса без изменения файла конфигурации и последующего перезапуска Sphinx.

Шаг 1. Добавим схему индекса в файл product.xml

<?xml version="1.0" encoding="utf-8"?>
<sphinx:docset xmlns:sphinx="http://sphinxsearch.com/">

  <sphinx:schema>
    <sphinx:field name="name" attr="string" />
    <sphinx:attr name="price" type="float"/>
  </sphinx:schema>
  
  <sphinx:document id="849">
    <name>Men's Sports Shorts</name>
    <price>24.7459</price>
  </sphinx:document>
  
  <sphinx:document id="852">
    <name>Women's Tights</name>
    <price>30.9334</price>
  </sphinx:document>
  
  <sphinx:document id="855">
    <name>Men's Bib-Shorts</name>
    <price>37.1209</price>
  </sphinx:document>
  
</sphinx:docset>

Схема индекса описывается при помощи элемента sphinx:schema, который может содержать элементы sphinx:field и sphinx:attr.

Элемент sphinx:field определяет поле с названием, указанным в атрибуте name. Также в этом элементе может содержаться необязательный атрибут attr, который указывает, что вместе с полем нужно создать одноимённый атрибут. Значение атрибута attr при этом указывает тип создаваемого атрибута (обычно это строковой тип).

Элемент sphinx:attr определяет атрибут с названием, указанным в атрибуте name и типом, указанным в атрибуте type.

Более подробно про элементы sphinx:field и sphinx:attr можно прочитать в документации.

Шаг 2. Изменим файл конфигурации, убрав определение полей и атрибутов из блока source.

source product
{
    type            = xmlpipe2
    xmlpipe_command = type c:\sphinx\data\source\product.xml
}

Тут всё очень просто. Поскольку схема теперь описана в xml-файле, то никакой необходимости дублировать её в файле конфигурации.

Шаг 3. Снова выполним индексацию командой:

c:\sphinx\bin\indexer product --config c:\sphinx\data\config.txt --rotate

Как мы видим, индексация снова прошла успешно.

Пример 3: Индексация потоковых данных

В предыдущих примерах мы индексировали готовые xml-файлы, используя для этого команду type. Однако создавать этот файл не обязательно. Вместо команды type можно указать любую программу, которая будет выводить данные в стандартный выходной поток.

Шаг 1. Воспользуемся Visual Studio и создадим консольное приложение SphinxSourceApp на языке C#, которое будет выводить данные из предыдущего примера при помощи класса XmlTextWriter.

public static class Program
{
    private const string sphinxns = "http://sphinxsearch.com";

    public static void Main(string[] args)
    {
        var products = new[]
        {
            new { ID = 849, Name = "Men's Sports Shorts", Price = 24.7459M },
            new { ID = 852, Name = "Women's Tights", Price = 30.9334M },
            new { ID = 855, Name = "Men's Bib-Shorts", Price = 37.1209M },
        };

        Console.OutputEncoding = Encoding.UTF8;
        using (var writer = new XmlTextWriter(Console.Out))
        {
            writer.WriteStartDocument();

            writer.WriteStartElement("sphinx", "docset", sphinxns);

            // <sphinx:schema>
            writer.WriteStartElement("sphinx", "schema", sphinxns);

            // <sphinx:field />
            writer.WriteStartElement("sphinx", "field", sphinxns);
            writer.WriteAttributeString("name", "name");
            writer.WriteAttributeString("attr", "string");
            writer.WriteEndElement();

            // <sphinx:attr />
            writer.WriteStartElement("sphinx", "attr", sphinxns);
            writer.WriteAttributeString("name", "price");
            writer.WriteAttributeString("type", "float");
            writer.WriteEndElement();

            writer.WriteEndElement();
            // </sphinx:schema>

            foreach (var product in products)
            {
                // <sphinx:document>
                writer.WriteStartElement("sphinx", "document", sphinxns);
                writer.WriteAttributeString("id", product.ID.ToString());
                writer.WriteElementString("name", product.Name);
                writer.WriteElementString("price", product.Price.ToString());
                writer.WriteEndElement();
                // </sphinx:document>
            }

            writer.WriteEndElement();
        }
    }
}

Скомпилируем это приложение и скопируем его в папку c:\sphinx\data\source\.

Шаг 2. Отредактируем файл конфигурации, изменив опцию xmlpipe_command.

source product
{
    type            = xmlpipe2
    xmlpipe_command = c:\sphinx\data\source\SphinxSourceApp.exe
}

Шаг 3. Ещё раз выполним индексацию командой:

c:\sphinx\bin\indexer product --config c:\sphinx\data\config.txt --rotate

Индексация снова прошла успешно.

Заключение

Источники данных в формате xml во многом являются более гибкими, чем источники, связанные с базами данных. Они позволяют индексировать как относительно небольшие xml-файлы, так и большие объёмы данных, генерируемые «на лету». При этом они не ограничивают вас жёстко заданной схемой, позволяя вместо этого передавать её вместе с данными.

Скачать материалы к этой статье (скрипты, файлы конфигурации, xml-файлы и приложение SphinxSourceApp)

30 комментариев

  1. Игорь, спасибо большое за материал. Очень нужная информация.

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

    ОтветитьУдалить
  3. Приятно читать такие отзывы =)
    Думаю, что 2-3 статьи про Sphinx ещё точно будет.

    ОтветитьУдалить
  4. Только в таком формате. Иначе Sphinx не определит, где документы с их id и какие теги являются полями и атрибутами.

    ОтветитьУдалить
  5. Через mysql прогонять вряд ли стоит.
    Можно прогнать через программу которая документ преобразует "на лету" (как в третьем примере). Или вообще использовать какую-нибудь утилиту, которая выполнит xslt преобразование.

    ОтветитьУдалить
  6. Очень неплохо!
    А ещё можно комбинировать в одном индексе несколько источников. Главное, чтобы схемы совпадали!
    Например, основную базу проиндексировать из sql-based источника, а kill-list - из xmlpipe.
    В описании индекса в этом случае будет несколько строчек source. И обрабатываться они будут сверху вниз, по порядку.

    ОтветитьУдалить
  7. ну да, xslt неплохой вариант.
    Но в случае большой базы будет нещадно тормозить (ну, просто подавляющее большинство XSLT-процессоров сперва "всасывают" весь док, строят DOM и лишь потом делают процессинг. Совсем не уверен, умеет ли кто из них работать с потоком, и насколько корректно. А большую коллекцию, пожалуй, только потоком и обработаешь).
    Можно написать элементарный потоковый парсер (на питоне, например). А после отладки можно его же переписать на голом C (если критична производительность)

    ОтветитьУдалить
  8. мда. вот про это: xmlns:sphinx="http://sphinxsearch.com/" в доке ничего нет :(
    при чем под линуксом работает без этого, а я под виндой мучался пока сам не понял как это прописать. непонятно правда почему. может там какой-то другой парсер используется?

    ОтветитьУдалить
  9. Честно говоря, я добавлял схему только из-за того, что Visual Studio выдаёт предупреждения при редактировании xml-файлов, если там используется необъявленная схема. Ну и по хорошему, схему всё-таки нужно объявлять.



    Тем не менее, без неё у меня под Windows 7 всё правильно индексировалось и работало.

    ОтветитьУдалить
  10. у меня не проходила индексация.. никак. выдавало сообщениеоб ошибке примерно такого содержания: "Namespace prefix sphinx on docset is not defined in Entity". но потом, когда я добавил Namespace - проиндексировалось. самое интересное - я сейчас убрал его - и о чудо, все равно индексируется...

    ОтветитьУдалить
  11. и еще, чтобы два раза не вставать...
    поток формирую из php.
    файл выводится в поток с помощью echo $content;
    где $content - содержимое xml- файла
    так вот, в консоле при выводе перед содержимым выводится 'Content-type:
    и соотетсвенно сфинкс выдает ошибку:
    ERROR: index 'ad': xmlpipe: expected '', got 'Content-type:

    раньше выводилась инфа еще и о генераторе потока: X-Powered-By
    это я нашел где отключить в php.ini
    там же обнулил дефолтный Content-type
    теперь если раньше выводилось 'Content-type: text/html, то теперь просто 'Content-type:
    если перед выводом в поток принудительно указать 'Content-type: через header(...) то он и выводится.
    уже и не знаю в какую сторону копать и где плясать с бубном.
    третий день мучаюсь, не могу запустить проект.. :(

    ОтветитьУдалить
  12. 1. Не понял при чём тут Content-Type, в xml файле нет никаких заголовков, на индексацию нужно передавать только контент (без заголовков), но в формате utf8

    2. тег document - это похоже на формат xmlpipe. Он сейчас уже устаревший и не рекомендуется для использования. Лучше попробовать индексировать в xmlpipe2 (http://sphinxsearch.com/docs/2.1.1/xmlpipe2.html)

    ОтветитьУдалить
  13. 1. я знаю, я так и отправляю. вот начало документа:








    ... другие атрибуты







    1
    1
    какой-то тайтл
    9000
    какой-то техт
    809
    810
    2146
    2179
    2216
    2227
    2243
    2297
    2299

    ... еще документы


    дело в том. что в поток выводится из php командой echo
    и вот сам php или апач вставляет перед началом файла
    X-Powered-By и Content-type
    как отключить добавление X-Powered-By - я нашел в php.ini а вот с Content-type - не могу побороть никак :(
    2. см.п.1 - по структуре видно что это и есть xmlpipe2

    ОтветитьУдалить
  14. xmlpipe_command = z:\home\bixti.loc\www/localyiic sphinx XML

    вызывается localyiic.bat содержащим:
    @echo off

    rem -------------------------------------------------------------
    rem Yii command line script for Windows.
    rem This is the bootstrap script for running yiic on Windows.
    rem -------------------------------------------------------------

    @setlocal

    set BIN_PATH=%~dp0

    if "%PHP_COMMAND%" == "" set PHP_COMMAND=z:\usr\bin\php.exe

    "%PHP_COMMAND%" "%BIN_PATH%localyiic.php" %*

    @endlocal

    ОтветитьУдалить
  15. Блин, даже не знаю, никогда так не извращался. По идее php.exe всегда будет отдавать заголовки, а не только контент. (Ну первая строка всегда будет с методом и урлом).


    Можно попробовать дополнительно прогонять вывод через curl для винды (вроде есть)

    ОтветитьУдалить
  16. хорошо, а есть пример вывода данных в поток на php?
    при чем данные формируются текстовой строкой а не с помощью dom или xmlwriter

    ОтветитьУдалить
  17. Я не знаком с php.

    Подозреваю, что нужно использовать поток stdout как-то так:
    xmlpipe_command = c:\your_path_to_php\bin\php.exe your_php_script.php

    Лучше в интернете поискать, т.к. про php мало что посоветую (.NET forever =)

    ОтветитьУдалить
  18. ну он почти так и используется :)
    батник запускает z:\usr\bin\php.exe с параметром localyiic.php который принимает параметры sphinx XML, который в свою очередь вызывает файл с классом SphinxCommand и его метод XML(), и вот он уже формирует xml и выводит в поток стандартной командой echo.
    а вот в поток выводит впереди эту гадость либо сам интерпретатор, либо апач. но я так и не нашел.
    проблему не решил, но для разработки временно поставил костыль: сформировал и сохранил xml в файл, а в сфинксе указал этот файл источником. в общем для разработки пока пойдет.

    что касается .NET я теорию знаю, сделал кучу учебных проектов, однако без опыта в .NET никто нормальных денег платить не хочет :)
    вот и приходится заниматься php так там опыта больше :)
    а так бы я с удовольствием работал бы с .NET :)

    ОтветитьУдалить
  19. можно вопрос по sphinx?

    как получить список атрибутов, у которых есть значение?

    уточню:
    есть схема, описанная в xml, допустим там описано 100 атрибутов.
    но реально в записанных и проиндексированных документах используются только 30 реквизитов, остальные не использовались.
    вопрос: как получить список использованных атрибутов?

    ОтветитьУдалить
  20. Странный вопрос.
    1. Не выгружать неиспользуемые атрибуты в индекс.
    2. Выбирать нужные атрибуты в select-е
    3. Использовать where

    Зависит от того, что значит "список использованных атрибутов"

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

    ОтветитьУдалить
  22. мне тоже так кажется, но вот мой senior утверждает, что если нужные данные находятся в сформированном XML файле, который подается сфинксу для индексирования, то и искать надо в нем с помощью сфинкса.
    я просмотрел api сфинкса, но ничего подобного там не нашел, кроме возможности подать строку запроса в стиле sql, но не уверен что он будет искать так как надо по xml файлу. если таки он не прав, а я прав - как ему это доказать?

    ОтветитьУдалить
  23. спасибо за отклик.

    ОтветитьУдалить
  24. Игорь, спасибо за статью. Только вот подскажите такую вещь - сделал конфиги, сделал xml, все индексируется и ищет нормально. Но сфинкс при поиске мне возвращает только ID документов. Может ли он возвращать другие поля? Например у меня есть поле content, вот его хотелось бы достать из индекса.

    ОтветитьУдалить
  25. Может, но для этого поле нужно ещё объявить как атрибут с тем же названием через attr="string", тогда по нему можно не только искать, но и возвращать в запросах:

    ОтветитьУдалить
  26. Алексей Мышкин26 ноября 2014 г. в 13:47

    Потоковых парсеров полно готовых.
    В Perl - XML::SAX (который по-сути является фабрикой)
    XML::LibXML::SAX

    ОтветитьУдалить

© Игорь Чакрыгин. Все права защищены при помощи чёрной магии. Технологии Blogger.