Пример Sphinx поиска на реальном проекте — магазин автозапчастей Tecdoc

    Вкратце: статья будет полезна тем программистам, кто уже заинтересовался релевантным поиском и прочитал статьи по стартовой установке сфинкс поиска, погонял на тестовых примерах и таких же синтетических задачах. Часто эти примеры не дают ответа на вопрос, а как же ощутить реальную пользу от поискового модуля Sphinx в сравнении с другими более простыми вариантами поиска. Примеры кода в статье — на php+smarty, Sphinx 2.0.1-beta, база данных — mysql, исходники и дамп структуры базы выложены отдельным архивом в подвале. В статье описан пример использования таких особенностей сфинкса, как:
    • Создание единого конфиг файла для 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

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

    Похожие публикации

    Комментарии 47
      +2
      >> Запрос можно было бы упростить представлением (view), но насколько я понял этого делать не рекомендуют и прямой запрос к данным с джойнами эффективнее по соображениям создаваемой нагрузки при индексировании.

      Разницы между view и запросом не будет так как для mysql это всего лишь сохраненный запрос.
      Оптимизатор не пальцем делан :)
        0
        В этом как раз и есть сомнения: при большом объеме данных этот оптимизитор, деланный не пальцем, может лажать. И в случае с прямым запросом — можно вручную исправить.

        Спорить не буду, вот здесь в коментах прочитал www.ibm.com/developerworks/ru/library/os-php-sphinxsearch/#icomments
          +2
          Две ошибки породили заблуждение.
          Что бы разобраться надо прочитать комментарий внимательней.
          В нем идет речь о том что документы индексируемые sphinx должны иметь уникальные id, в противном случае не гарантируется стабильная работа sphinx.
          После этого подвергается сомнению корректность использования view. Что не есть правильно так как в примере использование view не приведет к потере уникальности id.
          И уж тем более не приведет к потере производительности.
            0
            Перечитал еще раз комент, похоже, вы правы: в коменте речь идет об уникальности айди. К сожалению, исторически так сложилось, что представлениями не пользуемся и сказать, как себя они ведут в проекте с 20-100 млн позиций в таблицах я с уверенностью не могу.

            Возможно еще сказалось то, что в соседнем проекте полностью на вьюхах даже с минимальным количеством позиций ребята не справились и в слов лог постоянно сыпались запросы от этих вьюх. Возможно просто совпадение и вы умеете правильно готовить майскл вьюхи в высоконагрженных проектах.
              +1
              Тогда я вам рекомендую почитать доккументацию по mysql и больше не боятся: dev.mysql.com/doc/refman/5.5/en/view-algorithms.html

              Скорее всего у ребят были view в которых использовалось нечто из этого:

              Aggregate functions (SUM(), MIN(), MAX(), COUNT(), and so forth)
              DISTINCT
              GROUP BY
              HAVING
              LIMIT
              UNION or UNION ALL
              Subquery in the select list
              Refers only to literal values (in this case, there is no underlying table)


              Что заставляло mysql использовать временные таблицы и естественно работало не сильно быстро.

              Это поидее не смертельно и лечится если потюнить размер кеша для временных таблиц и ограничения на размеры самих временных таблиц.
              В любом случае чинить надо конкретный случай.
                0
                Спасибо за ссылку, почитаю по свободе. Согласен, личить нужно конкретный случай и возможно тупик и плавная смерть старого проекта не в представлениях, а в комплексе факторов.

                К сожалению, не все лечится тюнингом конфига майскл. В нашем частном случае все индексы базы данных просто физически не могу влезть в 4Г памяти сервера. И да: в более общем случае эти 4Г нашего типового сервера делятся 5-10 однотипными проектами, то есть фактически на проект 400Мб максимум — и работает. А раз работает — не трогаем и дальше юзаем прямые запросы, а не представления.
          +1
          С view в mysql я имел забавный прикол.

          Оптимизатор упорно выбирал не тот индекс, какой надо — ну ок, FORCE INDEX, все отлично.

          После чего вынес это дело во вьюху. Все скушало, force index в show create view видно. Но фактически force index напрочь игнорировался.

          Было это давно, не исключаю, что поправили, — но осадочек остался.
            0
            У меня почему-то аналогичный опыт отложился в памяти, что FORCE INDEX корректно работает для прямых запросов, а для представлений глючит.

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

            Может кто-то имеет более точную информацию по текущему положению дел?
          0
          С месяц назад поставил себе поиск на сайт объявлений очень доволен результатом.
            0
            Сортировать ещё можно по собственным параметрам, но не без релевантности (например когда одна марка товаров более приоритетная):

            $sphinx->select('@weight*priority as relevance')
            ->matchingMode(sphinx::matchExtended2)
            ->sortingMode(sphinx::sortExtended, 'relevance DESC');


            Где priority считается заранее.
              0
              А можно сделать двойную сортировку: релевантность, цена к примеру? То есть order by relevance desc, price asc…
                0
                Да можно, но, в основном, разбег параметров даст просто микс результатов.
                  0
                  В доке написано, в случае переполнения 32битного числа будут коллизии, так что я бы осторожно умножение использовал.
                    0
                    А зачем умножать?) Можно же делить! ) У меня priority варьировалось от 0.1 до 5, шанс получить переполнение почти нулевой (weight не будет таким большим).
              0
              > (масло | *масло*) & (Castrol | *Castrol*) & (5W40 | *5W40*)

              на sphinxsearch.com/docs/2.0.1/conf-expand-keywords.html сильно похоже
                0
                Похоже, но не тестировал: сразу решил, что буду пользоваться расширенным синтаксисом запросов, так как позволяет гибко настраивать нужный набор возвращаемых результатов. А вариант «из каробки» возможно работает также, не проверял.
                  0
                  Так одно с другим никак не связано. Запрос (масло castrol 5w40) в режиме с синтаксисом никак не отменяет работу expand_keywords. Просто она будет сделана автоматом и на стороне Сфинкса. Меньше кода в приложении
                    +1
                    Вся проблема в том, что куча скрытых не очевидных вещей — и чтобы о них узнать, нужно разобраться, проверить, погонять на тестах с живыми данными, а не семпловыми 2-3мя табличками с 3-5ю записями для сфинкса. Не уверен, но такие вещи называют «Best practices». То есть список типовых проблем — и такой же список наиболее коротких и правильных путей решения, чтобы каждый не ломал голову над своим велосипедом.

                    Я столкнулся с тем, что в сфинксе очень много функционала: он работает, бесспорно. Но не все так очевидно и явно, как хотелось бы. А почитать на русском толком кроме 2-3х статей для новичков -нечего. Основная масса информации черпалась по сути из комментариев к статьям на хабре, форуме на сайте и других сайтах таких же девелоперов. Я читаю и пишу на английском, но когда речь заходит о вещах которые не очевидны и не понятны — тяжело вникнуть в суть, даже если 80-90% слов в тексте понятны.

                    Для меня ступором было, что нету «service sphinx reload», «include *.conf», что ошибки при запуске не пишутся в текстовый лог — чтобы это понять я потратил кучу времени до того, как понял что именно нужно решать и как.

                    Я специально в статье перечислил все подобные свои грабли, как я их попытался решить, что у меня получилось и к каким выводам пришел. Надеюсь, следующему разработчику уже не нужно будет решать хотя бы эти грабли.
                      0
                      Про типовые проблемы мы ж ведь можем узнать только одним способом — когда люди много, но громко про них нам рассказывают.

                      Иначе мушка сбита!!!
                        0
                        Кстати, по поводу «нету«include *.conf»» помнится давно еще вопрос поднимался. И вроде даже обещали сделать так, что конфиг можно генерировать программно при запуске. Или я что-то путаю?
                          0
                          так уже можно и давно, #!/usr/bin/WHATEVER в начале файла и привет
                            0
                            опачки, вот так сюрприз!
                            Буду знать теперь…
                  0
                  А как вообще сам поиск и изложенный материал: не сильно рябит откровенными ляпами, характерными для новичка в поиске?
                    0
                    Мне немного не понятен п.4

                    Вы привели в пример метод задания весов для ручного управления релевантностью.
                    Правильно ли я тогда понимаю, что этот метод изменяет релевантность только в полученных результатах поиска, а не в самой базе Sphinx?
                      0
                      Да, веса настраиваются для запросов, а не зашиты статично в настройках сфинкса. То есть можно к одному индексу документов делать несколько разных запросов, в зависимости к примеру от настроек в профайле каждого пользователя или от пришедших гет параметров формы.

                      К примеру, кто то хочет по коду важному искать, а кому-то нужно именно описание запчасти.
                        0
                        «Релевантность» вообще в «базе» (индексе) никогда никем не хранится. Это функция от пары (документ, запрос), которая всегда и всюда считается на лету.
                        0
                        Не знаю, еще не читал.
                      0
                      Я так понял — каждый ваш запрос использует звездочки "*", т.е. поиск любого вхождения, но для этого не нужны звездочки, min_infix_len = 3 без enable_star = 1, так работает по дефолту. Попробуйте отключить enable_star = 1 и передавать запрос без преобразований «масла Castrol 5W40».
                        0
                        Ругается сфинкс indexer:
                        ERROR: index 'price_group': infixes and morphology are enabled, enable_star=0

                        не дает создать такой индекс
                        0
                        Важный момент для windows пользователей: при отказе запуска службы — ошибку нужно искать не в логах сфинкса, а в Панель управления — Администрирование — Просмотр событий.
                          0
                          Очень вовремя конечно задаю вопрос :) Но всё же, в версии 2.0.3 под win32 всё та же проблема. Система Windows 7 (без SP). При запуске сервиса вылежает ошибка, что файл не найден. До логов додумался сам, но там, собственно, ничего внятного не написано. Куда копать?

                          У знакомого сфинкс службой поднялся, но у него, емнип, SP 1.
                          0
                          >Так что в комментариях приветствуется более корректный пример запроса для получения того же, но средствами сфинкс группировки.

                          Группированный велосипед через мультизапросы исправил более корректной логикой сфинкса, пример использования ниже. Фактически используется функция из апи:
                          $oSphinxClient->SetGroupBy("id_price_group", SPH_GROUPBY_ATTR, "@count desc" );
                          

                          и нужный результат берется из $aResult[matches][attrs][@count]

                          Исходник на сервере обновил, вывод результата на первом шаге также добавил

                          Добавлен новый метод, который использует тот же шаблон, но вместо 100 запросов (кол-во груп запчастей) шлет сфинксу один.
                          public function AllPriceGrouped()
                          {
                          	if ($this->sQuery) {
                          
                          		require(SERVER_PATH.'/lib/sphinx/sphinxapi.php');
                          		$sSphinxKeyword=$this->GetSphinxKeyword($this->sQuery);
                          
                          		$oSphinxClient = new SphinxClient();
                          		$this->SetDefaultSetting($oSphinxClient);
                          
                          		$oSphinxClient->SetGroupBy("id_price_group", SPH_GROUPBY_ATTR, "@count desc" );
                          		$aResult = $oSphinxClient->Query($sSphinxKeyword, 'price_group');
                          		Base::$sText.=$sSphinxKeyword."
                          ";
                          
                          		if ( $aResult === false ) {
                          			Base::$sText.="Query failed: ".$oSphinxClient->GetLastError()."
                          ";
                          		}
                          		else {
                          			if ($oSphinxClient->GetLastWarning() ) {
                          				Base::$sText.="WARNING: ".$oSphinxClient->GetLastWarning()."
                          ";
                          			}
                          			if ($aResult['matches']) {
                          				$sPriceGroupAssoc=Db::GetAssoc('Assoc/PriceGroup',array(
                          				'visible'=>1,
                          				'multiple'=>1,
                          				));
                          				foreach ($aResult['matches'] as $aValue){
                          					$PriceGroupRow=$sPriceGroupAssoc[$aValue['attrs']['id_price_group']];
                          
                          					if ($PriceGroupRow) {
                          						$PriceGroupRow['total_found']=$aValue['attrs']['@count'];
                          						$PriceGroupRow['price_group']=$PriceGroupRow;
                          						$PriceGroupRow['price_group']['id']=$aValue['attrs']['id_price_group'];
                          						$aResultPriceGroup[]=$PriceGroupRow;
                          					}
                          				}
                          
                          				Base::$tpl->assign('aResultPriceGroup',$aResultPriceGroup);
                          				Base::$sText.=Base::$tpl->fetch($this->sPrefix.'/all_price_group.tpl');
                          			}
                          			Base::$sText.=Debug::PrintPre($aResult,false,true);
                          		}
                          	}
                          }
                          
                            0
                              0
                              Вы ничего не сломали, просто создали такой запрос, по которому ничего не найдено.

                              Аналогично по более простому «корола» autoklad.biz/?search%5Bquery%5D=%D0%BA%D0%BE%D1%80%D0%BE%D0%BB%D0%B0&action=search
                              ничего не найдено.

                              Я в статье писал, что не работают еще синонимы и запчасти привязаны только те, которые имеет смысл искать через поиск. Бампера к таким запчастям не очень подходят — для бамперов лучше через вин искать.
                                0
                                Вы, конечно, правы, я поторопился с выводами, простите меня.
                                Замечательная статья, большое вам спасибо.
                              0
                              а я вот такой поиск делал на сфинксе www.1aauto.com/search?words=toyota+headlights www.am-autoparts.com/search.html?words=toyota+headlights (обратите внимание на колонку с фильтрами). Тоже, кстати, автозапчасти.

                              Ооочень долго мучился с «AddQuery, RunQueries» из за следующих факторов:

                              1) Подзапросы к сфинксу для построения фасеточных фильтров нужно делать используя мультизапрос, т.е. AddQuery из соображений производительности.
                              2) Сфинксовское PHP апи представляет из себя один единственный класс… Принцип работы API следующий:
                              ....1. создаем объект сфинксовского API класса и подключаемся к серверу
                              ....2. вызываем методы типа setFilter() setGroupBy() setMatchMode() для задания критериев сортировки и фильтрации
                              ....3. вызываем метод addQuery() для добавления собранного запроса в очередь мультизапроса
                              ....4. Начинаем формировать следующий запрос для следующего addQuery() начиная с п. 2. нужное кол-во раз.
                              ....5. Вызываем runQueries() и получаем результаты по всем запросам.

                              Проблема в том, что в п. 4 нам нужно ОБНУЛИТЬ установленные для предыдущего запроса setFilter() setGroupBy() и т.п., потому что они при addQuery() не обнуляются. Пришлось придумывать какие-то обертки для этого, в общем времени потратил прилично.
                                0
                                bq. Проблема в том, что в п. 4 нам нужно ОБНУЛИТЬ установленные для предыдущего запроса setFilter() setGroupBy() и т.п., потому что они при addQuery() не обнуляются.

                                Есть же ResetFilters() и ResetGroupBy() для обнуления, а по-умолчанию они и не должны обнуляться — я написал в статье о них. Или может я что-то пропустил и все такие есть какие грабли с мультизапросами?

                                По вашему примеру сайта: найдено 767 results — честно говоря, я когда себе фару буду искать, врятли меня устроит такой широкий выбор :) Для фар, бамперов, и других 90% запчастей машины поиск сфинкс не подходит — и об этом тоже написано в статье. Или вы считаете что ваш поиск по фарам востребован пользователями?
                                  0
                                  Есть же ResetFilters() и ResetGroupBy() для обнуления, а по-умолчанию они и не должны обнуляться — я написал в статье о них. Или может я что-то пропустил и все такие есть какие грабли с мультизапросами?
                                  Я, честно говоря, не очень хорошо помню деталей проблемы, но точно помню что они были и были по этой причине.

                                  По вашему примеру сайта: найдено 767 results — честно говоря, я когда себе фару буду искать, врятли меня устроит такой широкий выбор :) Для фар, бамперов, и других 90% запчастей машины поиск сфинкс не подходит — и об этом тоже написано в статье. Или вы считаете что ваш поиск по фарам востребован пользователями?
                                  Во первых, я и запрос то привел чересчур общий. Вряд ли юзер прям будет искать любую фару для любой тойоты. Наверняка наберет типа «правая фара для тойота камри». Во вторых, фасеточные фильтры как раз служат для решения этой проблемы.
                                    0
                                    да, я уточнил свой вопрос чуть ниже.
                                  0
                                  ResetFilters, ResetGroupBy же есть
                                  0
                                  >обратите внимание на колонку с фильтрами
                                  Упустил я эту колонку из виду. Скажите, а как решаете проблему с возвратами по неправильному подбору. То есть если все параметры заданы правильно, а к примеру для запчасти не учтен к примеру параметр «Страна назначения авто» или «Праворукость» — как-то контролируется подбор и проверка запчасти по вин или все заказы идут как на мобильники?
                                    0
                                    Тут не смогу помочь, магазин не мой, я занимался только разработкой на аутсорсе. Сама контора находится в США. Но, насколько я знаю, 1aauto.com это типа «премиум-сервис» с завышенными ценами и запчасти там принимают назад без объяснения причин. Через am-autoparts.com продают те же запчасти, но дешевле и без возвратов, без колл-центра и поддержки. Как то так.
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                      0
                                      да, причем «масла Castrol 5W40» находит * Масла моторные (6)
                                      autoklad.biz/?action=search&search[query]=%D0%BC%D0%B0%D1%81%D0%BB%D0%B0%20Castrol%205W40&search[id_price_group]=35

                                      эта часть не должна была изменится. Можете привести урл или скриншот, где ничего не нашлось по этому азпросу?
                                      0
                                      > Также очень частым на форуме и сайте разработчиков вопросом является «Как поднять повыше точное вхождение фразы?», то есть чтобы вес документа «Искомое_слово» был выше «Искомое_слово а также еще кучу текста». Ответ — нужно использовать SPH_RANK_SPH04, специально созданный под эту типовую задачу, как я понял.

                                      Есть еще другой метод. В запросе индекса вставляется что-то типа «SELECT crc32(name) as name_hash»

                                      Потом при выборке делается SetSelect('name_hash='.crc32($name).' as name_equal') ну и SetSortMode идет сортировка по этому полю.

                                      К сожалению с postgresql там не совсем получается подружить такой метод, в постгресе нету функции crc32, но есть функция hashtext, и чтобы ее использовать, пришлось делать предварительно запрос в базу, для выяснения этого hashtext.

                                      Также на sphinx можно организовать замену Mysql фичи MATCH()… AGAINST(), которой нету в постгресе, то есть ранжирование записей между собой для поиска «Похожие товары». Результаты tsearch2 модуля (rank_cd функция и т.п.) оказались не совсем хорошими. Единственная там проблема оказалась которую не особо получилось решить, это если у товаров есть категории и желательно чтобы совпадение категорий тоже учитывалось при ранжировании, то приходится в индексном запросе id категорий преобразовывать в текстовое поле через запятую, а затем уже в запрос вставлять " @category 897 | @category 34" и т.п. Вот на это поле @category почему-то SetWeight особо как-то не влияет.
                                      В общем добиться идеального ранжирования не получилось, но приемлемого результата добиться можно.

                                      Если у кого есть опыт организации фичи «Похожие товары» через sphinx+postgresql или напрямую через postgresql — поделитесь.

                                      Выше речь шла о версии 0.9.9-release, может есть у кого опыт 2.0.1? И как вообще 2.01 в продакшн кто-то использует? Стабильная?
                                        0
                                        > может есть у кого опыт 2.0.1?

                                        децл

                                        > И как вообще 2.01 в продакшн кто-то использует? Стабильная?

                                        да
                                          0
                                          Используем на продакшене 2.0.1, проблем не возникает.

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое