Главная | Контакты | Настройки СМЕНИТЬ ПАЛИТРУ:

Главная > Программы

Sphinx для НЕ полнотекстового поиска

Sphinx

Sphinx – один из самых популярных движков полнотекстового поиска, в данной статье я расскажу как можно его использовать не по назначению…
Для начала опишу задачу поставленную перед нами заказчиком – есть некие документы в БД, каждому документу может соответствовать пачка кейвордов, да еще есть связи к таблицам языки/страны/города/типы, и так далее, чтобы лучше себе это представить приведу следующую диаграмму:

Database Schema

Пример такой сущности:

  • Документ: Письмо дяди Васи мастеру Феди. (и его содержимое)
  • Кейворды: Вася, письмо, Федя
  • Языки: Русский, Украинский
  • Страны: Украина
  • Города: Харьков, Богодухов, Чугуев

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

Sphinx – установка и настройка

Установка – тут все достаточно просто – скачиваем архив с сорцами, затем собираем:

# ./configure
make
make install

Потом настраиваем, приведу пример своего sphinx.conf:

## источник данных - существующая база данных
source src1
{
	# база данных - PostgreSQL
	type			= pgsql

	# настройки соединения
	sql_host			= localhost
	sql_user			= root
	sql_pass			= god-sex-love-secret
	sql_db				= project
	sql_port			= 5432	# default is 3306

	# основной запрос по которому будем индекс строить
	sql_query = \
	SELECT id, title, adult, status, blacklist, visits, rank, votes, stars, date_part('epoch', dateupdate) AS updated \
	FROM documents \
	WHERE (status = 1 OR status = 7) 
						
	# определяем числовые атрибуты
	sql_attr_uint = status
	sql_attr_uint = visits
	sql_attr_uint = rank
	sql_attr_uint = votes

	# булеан
	sql_attr_bool = adult
	sql_attr_bool = blacklist

	# дата и время в UNIX timestamp
	sql_attr_timestamp = updated

	# с плавающей запятой
	sql_attr_float = stars

	# multi-valued attribute (MVA) - для обеспечения связи многое ко многим 
	sql_attr_multi = uint keyword from query; SELECT document_id, keyword_id FROM document_keyword
	sql_attr_multi = uint language from query; SELECT document_id, language_id FROM document_language
	sql_attr_multi = uint country from query; SELECT document_id, country_id FROM document_country
	sql_attr_multi = uint city from query; SELECT document_id, city_id FROM document_city
	sql_attr_multi = uint type from query; SELECT document_id, type_id FROM document_type

	# для отладки из консоли
	sql_query_info		= SELECT * FROM document WHERE document_id=$id
}

## определения индекса
index project
{
	# берем источник описанный выше
	source			= src1
	# путь к индексам
	path			= /usr/local/sphinx/var/data/project
	# тип хранилища
	docinfo			= extern
	# memory locking for cached data (.spa and .spi), to prevent swapping
	mlock			= 0
	# нам необходимо точное соответствие - морфологию игнорируем
	morphology		= none
	# индексируем слова даже из одной буквы
	min_word_len		= 1
	# кодировочка
	charset_type		= utf-8
	# и еще раз
	charset_table		= 0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F
}
## настройки индексатора
indexer
{
        mem_limit = 32M
}

## настройки демона
searchd
{
	listen			= 127.0.0.1
	listen			= 3312
	read_timeout	= 5
	client_timeout	= 300
	max_children	= 0
	pid_file		= /usr/local/sphinx/var/log/searchd.pid
	max_matches		= 1000
}

Стартуем индексацию для всех прописанных индексов:

# /usr/local/sphinx/bin/indexer --all
если демон уже запущен
# /usr/local/sphinx/bin/indexer --all --rotate

Поднимаем демона:

# /usr/local/sphinx/bin/searchd

Пробуем поиск из консоли:

# /usr/local/sphinx/bin/search keyword
#Sphinx 0.9.9-release (r2117)
#Copyright (c) 2001-2009, Andrew Aksyonoff

using config file '/usr/local/sphinx/etc/sphinx.conf'...
index 'project': query 'keyword ': returned 1000 matches of 2277 total in 0.014 sec

displaying matches:
1. document=5761, weight=2, adult=0, status=1, blacklist=0, visits=0, rank=0, votes=0, stars=0.000000, updated=Sun Sep 20 20:22:07 2009, keyword=(318,323,611), language=(1), country=(), city=(), aim=(), type_first=()
2. document=351943, weight=2, adult=0, status=1, blacklist=0, visits=0, rank=0, votes=0, stars=0.000000, updated=Sun Sep 20 20:22:07 2009, keyword=(10480,10490), language=(1), country=(), city=(), aim=(), type_first=()
3. document=351956, weight=2, adult=0, status=1, blacklist=0, visits=0, rank=0, votes=0, stars=0.000000, updated=Sun Sep 20 20:22:07 2009, keyword=(10480,10490), language=(1), country=(), city=(), aim=(), type_first=()
...
words:
1. 'keyword': 2277 documents, 2614 hits

Связка PHP+Sphinx

А теперь, непосредственно поиск с использованием sphinxapi.php (читайте комментарии к коду):

// подключаем сфинкс
require ( "library/sphinxapi.php" );

// опущу получение данных из запроса
$sortby = "relev";
// $keywords - это массив id - т.е. до этого у нас должен быть запрос к нашей БД, который вытащит их по поисковой строке
// т.е. было "Вася, письмо, Федя" стало array(152, 345, 6342)
$keywords = array();
// тоже массивы id'шников
$languages = array();
$countries = array();
$cities = array();
$type = array();

// создаем инстанц клиента
$cl = new SphinxClient ();

// настройки
$cl->SetServer("localhost", 3312);
$cl->SetConnectTimeout(1);
$cl->SetLimits($offset, $limit); // постраничную навигацию организовываем тут
$cl->SetArrayResult (true);
$cl->SetRankingMode(SPH_RANK_PROXIMITY_BM25);
$cl->SetMatchMode(SPH_MATCH_EXTENDED);

// пишем select
// sphinx может сказать есть ли совпадение между двумя множествами, но не может сказать сколько их - приходится извращаться
// результирующий select будет иметь следующий вид:
// *, (IN(keyword, "152") + IN(keyword, "345") + IN(keyword, "6342") + ... ) AS relev
$select = "*, (IN(keyword,'.join(') + IN(keyword,',$keywords).')) AS relev";

$cl->SetSelect($select);

// накладываем фильтры
if (!empty($languages))
    $cl->SetFilter('language', $languages );
    
if (!empty($countries))
    $cl->SetFilter('country', $countries );
    
if (!empty($cities))
    $cl->SetFilter('city', $cities );
    
if (!empty($type))
    $cl->SetFilter('type_first', $type );

// применяем сортировку $sortby
switch ($sortby) {
    case 'relev':
        $cl->SetSortMode(SPH_SORT_EXTENDED, 'relev DESC');
        break;
    case 'visits':
        $cl->SetSortMode(SPH_SORT_EXTENDED, 'visits DESC, relev DESC');
        break;
    case 'stars':
        $cl->SetSortMode(SPH_SORT_EXTENDED, 'stars DESC, relev DESC, updated DESC');
        break;
    default:
        break;
}

// накладываем фильтр на поле обновления - нас интересуют записи за последний год
$cl->setFilterRange('updated', (time() - 60*60*24*365), time());

// теперь запрос к демону
$res = $cl->Query("", "project");

// вывод результатов
if ( $res === false ) {
  echo "Query failed: " . $cl->GetLastError() . ".\n";
} else {
  if ( $cl->GetLastWarning() ) {
      echo "WARNING: " . $cl->GetLastWarning() . "\n";
  }
  echo  "total found: <strong>{$res['total_found']}</strong> at <strong>{$res['time']} sec</strong><hr/>";
  
  if ( ! empty($res["matches"]) ) {

      // это вывод того, что нашел Sphinx
      // такая подробная информация будет возвращаться только, если установлен параметр $cl->SetArrayResult (true);
      foreach ( $res["matches"] as $doc => $docinfo ) {
            if (!isset($docinfo['attrs']['relev'])) $docinfo['attrs']['relev'] = 0;
            echo "id: {$docinfo['id']}<br/>";
            echo "weight: {$docinfo['weight']}<br/>";
            echo "relevance: {$docinfo['attrs']['relev']}<br/>";
            echo "votes: {$docinfo['attrs']['votes']}<br/>";
            echo "stars: {$docinfo['attrs']['stars']}<br/>";
            echo "rank: {$docinfo['attrs']['rank']}<br/>";
            echo "visits: {$docinfo['attrs']['visits']}<br/>";
            echo "keywords: ".join(',', $docinfo['attrs']['keyword'] )."<br/>";
            echo "languages: ".join(',', $docinfo['attrs']['language'] )."<br/>";
            echo "countries: ".join(',', $docinfo['attrs']['country'] )."<br/>";
            echo "cities: ".join(',', $docinfo['attrs']['city'] )."<br/>";
            echo "types: ".join(',', $docinfo['attrs']['type'] )."<br/>";
            echo "updated: ".date("Y-m-d H:i",$docinfo['attrs']['updated'])."<br/>";
            echo "<hr/>";
      }

      // это был вывод того, что нам вернул Sphinx, для вывода необходимой информации надо постучаться к нашей БД
      // нам же надо взять ID всех документов
      // для правильной работы следующего кода отключите параметр $cl->SetArrayResult (true);
      $ids = array_keys($result['matches']);

      // и выведем в порядке, которые нам нашептал Sphinx
      // данный пример подходит для MySQL в PostgreSQL для эмуляции конструкции ORDER BY FIELD используют ORDER BY CASE
      $id_list = implode(',', $ids);
      $sql = sprintf('SELECT * FROM `documents` WHERE `id` IN (%s) ORDER BY FIELD(`id`, %s)', $id_list, $id_list);
  }
}

Таки полнотекстовый

Дальше немного о грустном, когда количество документов выросло до 4-х млн, поиск стал занимать недопустимые 4 и более секунды, пришлось пойти на небольшую хитрость – для вычисление совпадений таки использовать полнотекстовый поиск, и не заморачиваться с вычислением точных совпадений…

Изменения в конфиге:

source src1
{
	# добавлено поле keywords - сие есть заранее подготовленное поле, обновляемое по тригеру
        # содержит (array_to_string(array_agg(k.value), ', ') AS keywords
	sql_query = \
	SELECT id, title, keywords, adult, status, blacklist, visits, rank, votes, stars, date_part('epoch', dateupdate) AS updated \
	FROM documents \
	WHERE (status = 1 OR status = 7) 
}

Изменения в PHP части:

// дабы много не переписывать из предыдущего примера, был добавлен алиас
$select = "*, @weight AS relev";

// запрос претерпел изменения
// теперь он имеет вид @keywords ("Вася"|"письмо"|"Федя")
$query = '("'.join('"|"',$keywords).'")';
$query = '@keywords '.$query;

// теперь запрос к демону
$res = $cl->Query($query, "project");
Материал взят с сайта: https://anton.shevchuk.name/php/sphinx-for-not-full-text-search/

Sphinx. Установка и первичная настройка

Итак, начинаю серию топиков по прикручиванию поисковой машины Sphinx к нашему любимому движку.

Что есть Sphinx и с чем его едят

Сфинкс является системой полнотекствого поиска, распространяемой под лицензией GPL второй версии.
Сфинкс позволяет добавить функции быстрого полнотекстового поика в сторонние приложения и был специально спроектирован для взаимодействия с реляционными базами данных и скриптовыми языками программирования. На данный момент движок позволяет индексировать данные, хрянящиеся в базах данных MySQL, PostgreSQL или в любом другом формате через XML-шлюз.

Установка

Топаем по ссылке и скачиваем себе файл с исходниками последней версии Sphinx-а. Актуальной, на данный момент, является версия 3.3.1, взять которую можно здесь.
Распаковываем архив стандартной командой (tar zxf /path/to/SphinxSourceFile.tar.gz), переходим в новый каталог sphinx и выполняем «волшебную последовательность» (./configure; make; make install).
Для успешной сборки Сфинкса в системе должны быть установлены и доступны текущему пользователю:
* компилятор С++ (вполне подойдет стандартный из коллекции компиляторов GNU)
* программа make, например, из набора GNU

Если есть необходимость указать какие-нибудь опции компиляции, например, путь до библиотеки MySQL или пути для установки, то передаем их конфигурационному скрипту. Префикс «по-умолчанию» зависит от вашей системе (в *BSD — /usr/local..., а в разных Linux-ах по разному, но велика вероятность, что конфиги лягут в /etc/sphinx, а бинарники в /usr/local/bin). Для простоты дальнейшего повествования, предположим, что конфиги у вас будут лежать в каталоге /usr/local/sphinx/etc, а бинарники в /usr/local/sphinx/bin.
На этом этап установки завершен.

Настройка

Для начала стоит сказать, что конфигурация поисковой машины Сфинкс оперирует двумя фундаментальными параметрами:
 * Источник (source) — описание объектов индексируемых данных, параметры доступа к ним и указание поисковой машине аттрибутов данных, их типов и правил их анализа.
 * Индекс (index) — хранилище индекса данных, описанных в источнике (да, тавтология :) ). В качестве данных, хранимых в индексе, указывается ранее описанный источник.

Исходя из написанного выше, для настройки Сфинкса на индексацию базы данных Живой Улицы, нам необходимо описать источники и индексы. В данном топике будет приведен пример конфигурационного файла с индексами комментариев и топиков. Расширенную настройку каждый сможет провести самостоятельно, в зависимости от требований проекта.
Со сфинксом поставляется конфигурационный файл по-умолчанию, однако он нам не пригодиться. При первичной настройке Вы можете воспользоваться моим, который приведен ниже. Однако учтите, что в нем необходимо прописать параметры доступа к базе данных и пути до каталогов, в которых будут храниться индексы.

Настройка автозапуска поискового демона при старте системы

Для осуществления того, что написано в заголовке раздела, необходимо в один из стартовых скриптов системы, например, /etc/rc.local для ОС Linux вставить код:

# Запускаем демона Сфинкс
/usr/local/sphinx/bin/searchd --config /usr/local/sphinx/etc/sphinx.conf

Настройка периодической индексации базы данных

Самый простой способ запустить индексацию — это выполнить или добавить в планировщик команду:

/usr/local/sphinx/bin/indexer --all
, однако данный способ является сильно неоптимальным, поскольку топики обновляются (точнее появляются новые) значительно реже, нежели комментарии, поэтому частоту обновления индекса комментариев должны быть больше.

Для различной частоты индексации объектов разного типа, в планировщик необходимо добавить следующие инстукции:

12 */ 3 * * * /usr/local/sphinx/bin/indexer --rotate topicsIndex > /dev/null 2>&1
*/50 * * * * /usr/local/sphinx/bin/indexer --rotate commentsIndex > /dev/null 2>&1

(означает индексацию топиков каждые 3 часа с запуском процесса на 12-ой минуте часа и индексацию комментариев каждые 50 минут)

Пример конфигурационного файла

## Конфигурационный файл Sphinx-а для индексации Живой улицы
#######################
#
# Описываем индексы
#
#######################
# Источник-родитель для всех остальных источников. Здесь указываются параметры доступа 
# к базе данных сайта
source lsParentSource
{
        type            = mysql
        sql_host        = your_database_host
        sql_user        = your_database_login
        sql_pass        = your_database_password
        sql_db          = your_database_name
        sql_port        = 3306
        # Для ускорения работы прописываем путь до MySQL-го UNIX-сокета (чтобы 
        # операции с БД происходили не через TCP/IP стек сервера)
        sql_sock        = /var/run/mysqld/mysqld.sock

        mysql_connect_flags     = 32 # 32- включение сжатие при обмене данными с БД

        # Включам нужную кодировку соединения и выключаем кеш запросов
        sql_query_pre                   = SET NAMES utf8
        sql_query_pre                   = SET SESSION query_cache_type =OFF    
}

# Источник топиков
source topicsSource : lsParentSource
{

# запрос на получения данных топиков
        sql_query               = \
                SELECT t_fast.topic_id, t_fast.topic_title, UNIX_TIMESTAMP(t_fast.topic_date_add) as topic_date_add, \
                tc.topic_text, t_fast.topic_publish \
                FROM prefix_topic as t_fast, prefix_topic_content AS tc \
                WHERE t_fast.topic_id=tc.topic_id AND t_fast.topic_id>=$start AND t_fast.topic_id<=$end

        # запрос для дробления получения топиков на неколько итераций
        sql_query_range         = SELECT MIN(topic_id),MAX(topic_id) FROM prefix_topic

        # сколько получать объектов за итерацию
        sql_range_step          = 1000

        # Указываем булевый атрибут критерия "топик опубликован". Для возможности указания этого критерия при поиске
        sql_attr_bool           = topic_publish

        # Атрибут даты добавления, типа "время"
        sql_attr_timestamp      = topic_date_add
        # мульти-аттрибут "теги топика"
        sql_attr_multi  = uint tag from query; SELECT topic_id, topic_tag_id FROM prefix_topic_tag
        sql_ranged_throttle     = 0
}

# Источник комментариев
source commentsSource : lsParentSource
{
        sql_query               = \
                        SELECT comment_id, comment_text, UNIX_TIMESTAMP(comment_date) as comment_date, comment_delete \
                        FROM prefix_topic_comment \
                        WHERE comment_id>=$start AND comment_id<=$end

        sql_query_range         = SELECT MIN(comment_id),MAX(comment_id) FROM prefix_topic_comment
        sql_range_step          = 5000

        sql_attr_bool           = comment_delete
        sql_attr_timestamp      = comment_date
}

#######################
#
# Описываем индексы
#
#######################

index topicsIndex
{
        # Источник, который будет хранить данный индекса
        source                  = topicsSource
        path                    = ПУТЬ/ДО/КАТАЛОГА/ИНДЕСА

        # Тип хранения аттрибутов
        docinfo                 = extern

        mlock                   = 0

        # Используемые морфологические движки
        morphology              = stem_enru, soundex, metaphone

        # Кодировака данных из источника
        charset_type            = utf-8

        # Из данных источника HTML-код нужно вырезать
        html_strip                              = 1
}

# Индекс комментариев
index commentsIndex
{
        source                  = commentsSource
        path                    = ПУТЬ/ДО/КАТАЛОГА/ИНДЕСА
        docinfo                 = extern
        mlock                   = 0
        morphology              = stem_enru, soundex, metaphone
        charset_type            = utf-8
}

#######################
#
# Настройки индексатора
#
#######################


indexer
{
        
# Лимит памяти, который может использавать демон-индексатор
        mem_limit                       = 32M
}

#######################
#
# Настройка демона-поисковика
#
#######################

searchd
{
        # Адрес, на котором будет прослушиваться порт
        address                         = 127.0.0.1
        # Ну и собственно номер порта демона searchd
        port                            = 3312
        # Лог-файл демона
        log                                     = /var/log/sphinx/searchd.log
        # Лог поисковых запросов. Если закомментировать,то логировать поисковые строки не будет
        query_log                       = /var/log/sphinx/query.log
        # Время в секундах, которое ждет демон при обмене данными с клиентом. По исчерпании происходит разрыв коннекта
        read_timeout            = 5
        # Максимальное количество одновременно-обрабатываемых запросов. 0 означает дофига, а точнее без ограничения
        max_children            = 30
        # Файл, в который сохраняется PID-процесса при запуске
        pid_file                         = /var/log/sphinx/searchd.pid
}

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

Материал взят с сайта: http://livestreet.ru/blog/dev_documentation/366.html

Главная > Программы