![](https://habrastorage.org/storage1/a852324d/0bcaab81/7dd9f3c3/8be6c329.jpg)
- Создание единого конфиг файла для windows development и linux production
- SetMatchMode(SPH_MATCH_EXTENDED2) и почему SPH_MATCH_ANY и другие не подходят для реального поиска
- SetSortMode(SPH_SORT_RELEVANCE), SetFieldWeights — сортировка по релевантности и установка весов для полей индекса
- SetLimits(0,20) — ограничение вывода результатов
- AddQuery, RunQueries — построение мультизапросов
- SetFilter, ResetFilters — добавление фильтрации в мулльтизапросе для ограничения получаемых данных
- Wordforms — использование синонимов и преодоление ограничений для нестандартных словоформ, как «C#»
Также хочется внести свой вклад в развитие проекта и откровенно недостаточной русской документации при том, что проект создан и поддерживается русскоязычным программистом. Поэтому решено: непрекращающийся поток блокер задач идет лесом, вместо него в качестве благодарности разработчикам сфинкса в общем и пользователю Андрей Аксёнов ака shodan я пишу эту статью.
1. Вступление
Если сфинкс еще не установлен и хотите начать, ссылка на статью для новичка: Создание ознакомительного поискового движка на Sphinx + php. Потестировать и посмотреть как работает этот поиск можно по адресу autoklad.biz/?action=search.
Сразу скажу, что наш модуль поиска находится в версии бета тестирования: есть много еще шероховатостей, потенциальных дыр и других менее очевидных багов, в связи с чем не рекомендуется сломя голову ставить текущий пример на свой продакшин, так как нужен еще этап тестирования с реальными нагрузками и реальными запросами от многочисленных пользователей наших сайтов.
Авто запчастями, а точнее продажей интернет магазинов по запчастям, наша компания занимается давно, плодотворно и довольно успешно. Но в силу ряда причин релевантный поиск понадобился только сейчас. Основная причина скорее всего в том, что большинству запчастей полнотекстовый поиск не подходит. Но есть 5-10% товаров, для которых он катастрофически нужен и без него уж никак. А наш стандартный поиск с прямыми по своей сути кросс связями и указанием четкой модели и марки авто из tecdoc, для этой группы товаров не работает. Пример таких «неправильных» товаров: масла, шины, аккумуляторы и другие подобные.
Средний прайс по запчастям небольшой рядовой компании — 2-10 млн позиций, соответственно 10% от этой базы и будут занимать нужные нам данные. То есть индекс в приведенных ниже примерах строится по базе около 300 тысяч документов.
2. Создание единого конфиг файла для windows development и linux production ОС
Решаемая проблема — конфигурационные файлы машины разработчика и продакшин сервера отличаются, а при разработке нужно оперативно обновлять неустоявшуюся структуру и постоянно менять эти сфинкс конфиги. Усугубилось в нашем случае тем, что эти конфиги на сервере нужно делить с отдельным проектом другой команды разработки, а такой секции как «include *.conf» в сфинксе пока не предусмотрено.
На локальной windows машине конфиг лежит в «D:\Sphinx\sphinx.conf», на сервере в "/etc/sphinx/sphinx.conf", причем на линукс машине создана символическая ссылка на обновляемый скриптом Search->CreateConfigFile() файл в /var/www/autoklad.com.ua/imgbank/sphinx/sphinx.conf. Локальный файл обновляется прямо в папку, так как он не мешает соседям.
Исходный код методов:
public function CreateConfigFile()
{
$sConfigFilePath=Db::GetConstant('sphinx:config_file_path',SERVER_PATH.'/imgbank/sphinx/');
$sConfigFileName='sphinx.conf';
$sConfigTemplate=Db::GetConstant('sphinx:config_template','production');
if (!file_exists($sConfigFilePath)) mkdir($sConfigFilePath);
$sTopSection.=$this->GetPriceGroupConfig();
Base::$tpl->assign('sTopSection',$sTopSection);
$sFileContent=Base::$tpl->fetch($this->sPrefix.'/config_sphinx_'.$sConfigTemplate.'.tpl');
file_put_contents($sConfigFilePath.$sConfigFileName,$sFileContent);
}
private function GetPriceGroupConfig()
{
Base::$tpl->assign('sDataFilePath',Base::GetConstant('sphinx:data_file_path','/var/data/'));
return Base::$tpl->fetch($this->sPrefix.'/config_price_group.tpl');
}
Шаблон config_price_group.tpl, остальные — в архиве, чтобы не растягивать статью
source price_group
{ldelim}
type = mysql
sql_host = {$aDbConf.Host}
sql_user = {$aDbConf.User}
sql_pass = {$aDbConf.Password}
sql_db = {$aDbConf.Database}
sql_query_pre = SET NAMES utf8
sql_query_pre = SET CHARACTER SET utf8
sql_query = \
select p.id \
, p.code as code \
, c.title as brand \
, if(ifnull(cp.name_rus,'')<>'', cp.name_rus, ifnull(p.part_rus,'')) as part_name \
, pgr.name as price_group_name \
, p.id_price_group as id_price_group \
from price as p \
left join cat_part as cp on cp.item_code=p.item_code \
inner join cat as c on p.pref=c.pref \
inner join provider_virtual as pv on p.id_provider=pv.id_provider \
inner join user_provider as up on pv.id_provider_virtual=up.id_user \
inner join provider_group as pg on up.id_provider_group=pg.id \
inner join user as u on up.id_user=u.id and u.visible=1 \
inner join currency as cu on up.id_currency=cu.id \
inner join price_group as pgr on pgr.id=p.id_price_group \
where 1=1
sql_attr_uint = id_price_group
sql_query_info = SELECT * FROM price WHERE id=$id
{rdelim}
index price_group
{ldelim}
source = price_group
path = {$sDataFilePath}price_group/index
morphology = stem_ru
min_word_len = 3
charset_type = utf-8
min_infix_len = 3
#min_prefix_len = 3
enable_star = 1
{rdelim}
Запрос можно было бы упростить представлением (view), но насколько я понял этого делать не рекомендуют и прямой запрос к данным с джойнами эффективнее по соображениям создаваемой нагрузки при индексировании.
Значение констант, которые берутся из бд для локального сайта
sphinx:data_file_path D:/Sphinx/data/
sphinx:config_template local
sphinx:config_file_path D:/Sphinx/
Значение констант, которые берутся из бд для продакшин сайта
sphinx:data_file_path /var/data/
sphinx:config_template production
sphinx:config_file_path /var/www/autoklad.com.ua/imgbank/sphinx/
В результате работы для локального сфинкса мы имеем вот такой конфиг файл:
http://www.mstarproject.com/temp/3/sphinx/sphinx.conf
3. SetMatchMode(SPH_MATCH_EXTENDED2) и почему SPH_MATCH_ANY и другие не подходят для реального поиска
Для того, чтобы работала морфология и в запросе «масла Castrol 5W40» нашлись документы с текстом «Масло» и «15W40» — нужно одновременно использовать символ "*" и поиск по словоформе «масл», а для этого нужен построитель запросов, который работает именно в режиме «SPH_MATCH_EXTENDED2». Есть также SPH_MATCH_EXTENDED, но как я понял это старая версия и рекомендуют использовать новую версию режима.
В режиме SPH_MATCH_ANY нельзя добиться того, чтобы при увеличении слов в запросе — количество результатов уменьшалось. В режиме SPH_MATCH_ALL нельзя добиться одновременной работы режима по частичному вхождению и словоформе. Другие режимы вскользь просмотрел, пока не пригодились, поэтому ничего сказать по ним не могу.
Сам запрос к сфинксу по фразе «масла Castrol 5W40» будет выглядеть так:
(масло | *масло*) & (Castrol | *Castrol*) & (5W40 | *5W40*)
Важно: в конфиге используемого индекса должно быть 2 строки:
min_infix_len = 3
enable_star = 1
Первая позволяет искать по частичному вхождению слово справа и слева, то есть с конца и с начала слова. Вторая строка позволяет использовать в запросе "*". Можно использовать min_prefix_len, если нужно к примеру только вхождения слева (с начала) слова.
Функция, которая обрабатывает входящую строку и формирует правильный запрос:
private function GetSphinxKeyword($sQuery)
{
$aRequestString=preg_split('/[\s,-]+/', $sQuery, 5);
if ($aRequestString) {
foreach ($aRequestString as $sValue)
{
if (strlen($sValue)>3)
{
$aKeyword[] .= "(".$sValue." | *".$sValue."*)";
}
}
$sSphinxKeyword = implode(" & ", $aKeyword);
}
return $sSphinxKeyword;
}
Результат запроса можно протестировать по адресу: http://autoklad.biz/?action=search&search[query]=%D0%BC%D0%B0%D1%81%D0%BB%D0%BE%20Castrol%205W40&search[id_price_group]=35
Ниже результатов поиска выведен результирующий массив, который возвращает сфинкс для обработки — обратить внимание на секцию [words], в котором указано, по каким словам какое количество документов найдено. Другие секции не менее важны, но о них пока речь не идет.
Также очень частым на форуме и сайте разработчиков вопросом является «Как поднять повыше точное вхождение фразы?», то есть чтобы вес документа «Искомое_слово» был выше «Искомое_слово а также еще кучу текста». Ответ — нужно использовать SPH_RANK_SPH04, специально созданный под эту типовую задачу, как я понял.
4. SetSortMode(SPH_SORT_RELEVANCE), SetFieldWeights — сортировка по релевантности и установка весов для полей индекса
Данный метод определяет, какие результаты будут выше в отсортированном массиве данных, возвращаемых сфинксом. В случае с SPH_SORT_RELEVANCE — результат будет отсортирован по т.н. «релевантности». Релевантность, как бы нам того не хотелось, работает по чисто арифметическим правилам, а не так как у гугл или яндекс поиска. То есть никакой магии: перемножение и сложение веса индекса, веса поля, количества вхождения искомого слова в документ и частота этого слова в других документах.
Мы на вход задаем в самом простом случае веса для полей индекса:
$oSphinxClient->SetFieldWeights(array (
'code' => 50,
'brand' => 40,
'part_name' => 10,
'price_group_name' => 5,
));
а на выходе получаем отсортированный по «релевантности»=«суммарному весу» массив, где вес — это целочисленное значение. Этими числами можно управлять, настраивая релевантность под себя, то есть более важному полю нужно присваивать больший вес. В нашем примере самое важное поле — это код запчасти «code».
5. SetLimits(0,20) — ограничение вывода результатов
Данный метод самый простой, работает аналогично майскл-евскому limit 0,20 и нужен соответственно для того же: для получения порционных данных для построения степперов. В нашем проекте нужны просто первые 20 (константа) результатов, так как дальше по шагам, если их будет 3 и более смысла идти нету.
6. AddQuery, RunQueries — построение мультизапросов
Мультизапросы — очень удобное решение пакетных запросов, когда нужно послать не один запрос сфинксу, а несколько. В нашем примере это отправка всем группам запчастей одного и того же запроса для получения списка групп и количества записей в каждой группе. То есть посылается около 100 запросов, а возвращается один результат в одном соединении к сфинксу. Также «решено» ограничение в 32 максимально допустимых одновременных запросов в одном пакете запросов.
Пример кода:
$aPriceGroup=Db::GetAll(Base::GetSql("Price/Group",array(
'visible'=>1,
"where"=>" and pg.code_name is not null",
)));
if ($aPriceGroup) {
$aResultAll=array();
$i=0;
foreach ($aPriceGroup as $aValue) {
$oSphinxClient->SetFilter('id_price_group', array($aValue['id']));
$iQuery = $oSphinxClient->AddQuery($sSphinxKeyword, 'price_group');
$oSphinxClient->ResetFilters();
$bAddedUnrunQuery=true;
$aPriceGroupAssoc[$iQuery+(32*$i)]=$aValue;
if ($iQuery && !($iQuery % 31) ) {
$aResultQuery=$oSphinxClient->RunQueries();
$aResultAll=array_merge($aResultAll,$aResultQuery);
$sLastError=$oSphinxClient->GetLastError();
$i++;
$bAddedUnrunQuery=false;
}
}
if ($bAddedUnrunQuery) {
$aResultQuery=$oSphinxClient->RunQueries();
$aResultAll=array_merge($aResultAll,$aResultQuery);
}
}
По причине того, что у выполняемого задания были конечные сроки, — вникнуть во все тонкости запросов на старте, задача не ставилась. Поэтому я наверняка написал велосипед, который решает задачу «группированного» запроса, аналогичного group by в mysql. С другой стороны, если бы я разобрался с группировкой в сфинкс — не было бы примера, где можно использовать мультизапросы.
Так что в комментариях приветствуется более корректный пример запроса для получения того же, но средствами сфинкс группировки.
7. SetFilter, ResetFilters — добавление фильтрации в мультизапросе для ограничения получаемых данных
Для того, чтобы использовать фильтры — нужно сначала прописать в конфиг индекса поля, по которым будет использоваться фильтрация. В нашем примере это поле id_price_group:
sql_attr_uint = id_price_group
Соответственно в коде используется вот так:
foreach ($aPriceGroup as $aValue) {
$oSphinxClient->SetFilter('id_price_group', array($aValue['id']));
$iQuery = $oSphinxClient->AddQuery($sSphinxKeyword, 'price_group');
$oSphinxClient->ResetFilters();
//...
}
То есть в цикле foreach для каждого запроса в мультизапросе сначала устанавливается фильтр, а после добавления — сбрасывается, чтобы для других запросов он не работал. По-моему все логично, очевидно и трудностей возникнуть не должно.
8. Wordforms — использование синонимов и преодоление ограничений для нестандартных словоформ, как «C#»
Чтобы работали синонимы и нестандартные (свои) словоформы — нужно в конфиг индекса включить файл с такими словоформами:
wordforms = D:\Sphinx\data\wordforms.txt
Сам файл может содержать к примеру такой набор данных в UTF-8 кодировке:
bosh > bosch
бошш > bosch
CASTROL > CASTROLL
кастрол > CASTROLL
кастролл > CASTROLL
То есть с левой стороны все возможные синонимы — с правой значения этих слов. Причем слева не должно быть к примеру «bosch», если он уже есть справа. По крайней мере если это сделать — поиск ведет себя не так, как я ожидал.
В нашем примере можно использовать запрос «масла кастрол 5W40» и он найдет то же, что и «масла Castrol 5W40». В примере с «C#» нужно включать такие нестандартные словоформы, чтобы они не обрабатывались по стандартной схеме индекса и работали вручную именно так, как вы их настроите. Только вы знаете, какой именно смысл в вашем проекте несет фраза, к примеру «C#» = «ДО ДИЕЗ для музыкантов»
Данного функционала нету в конфиг файле и примере на сервере, приведен только пример, но еще не внедрен в существующей структуре синонимов проекта.
Исходные коды, архивы, ссылки на полезные сайты
* Неофициальная вики документация, в том числе и на не английском языках http://sphinxsearch.com/wiki/doku.php
* Архив урезанных исходников примера автозапчастей http://www.mstarproject.com/temp/3/sphinx/sphinxsearch_soruce.zip
* Архив структуры урезанной бд примера http://www.mstarproject.com/temp/3/sphinx/sphinxsearch_db_structure.zip
* Архив урезанной бд примера (43 MB) с данными http://www.mstarproject.com/temp/3/sphinx/sphinxsearch_db_data.zip
* Оплаченная спонсором рекламная ссылка: разработка интернет магазина tecdoc+sphinxsearch
Буду рад конструктивной критике и постараюсь ответить на возникшие вопросы. На конференцию в Санкт-Петербурге скорее всего не поеду: очень неудобный перелет, да и зима как никак. Решил, что пользы будет больше от статьи, а с автором сфинкса можно встретиться в Украине, нужно только подождать.