Информации по Sphinx не так много, как хотелось бы. Лишняя статья не помешает.
Первые шаги в освоении Sphinx мне помогли сделать статьи Создание ознакомительного поискового движка на Sphinx + php и Пример Sphinx поиска на реальном проекте — магазин автозапчастей Tecdoc Советую начать с них.


Некоторое время на моем сайте работал поиск через LIKE по каждому слову запроса. Хотелось большего, и вот какие случаи теперь будут обрабатываться правильно:


  • Словоформы. Выдача по «винты» и «винтов» должна быть одинаковой.
  • Поиск по фрагменту слова.
  • Поиск нецелых чисел. Разделитель точка и запятая.
  • Буква Ё
  • Типичные ошибки. Например «Аммортизатор».
  • Синонимы. Регулятор и ESC.
  • Язык. mAh и мАч, В и V, AAA латиницей и кириллицей.
  • Слово из букв и цифр. 10х15х4, 6000mAh

Раздел source и дополнительная сортировка


Выдача сначала должна содержать позиции в наличии, потом временно отсутствующие, потом архивные. И все эти три группы должны быть отсортированы по релевантности. Для этого надо задать атрибуты. В моем случае это поля clearance и in_stock раздела source sphinx.conf


sql_query =     SELECT id, `art`, `name`, `clearance`, `in_stock`     FROM items_zip WHERE show_flag=1
sql_attr_bool = clearance
sql_attr_uint = in_stock

Эти поля будут использованы в формировании выдачи в PHP. Опишу ниже.


Раздел index в sphinx.conf


morphology = stem_enru
Морфология решает мою первою задачу. Поиск 'подшипники', 'подшипника', 'подшипников' приведет к единому результату.


Стэммы (stem_enru) быстрее, леммы (lemmatize_ru) точнее. Я пробовал только стэммы. Выбор повлияет на ваш словарь замен wordforms. Захотите поменять — придется переписывать.


min_word_len = 1
Индексируем слова любой длины.


html_strip = 1
Удаляем html тэги


min_infix_len = 1
Поиск будет по фрагменту слова. Проиндексируем фрагменты вплоть до 1 буквы. Так как база у меня менее 10000 наименований, то на индексе не экономлю.


expand_keywords = 1
Автоматически приводит запрос к виду "( running | running | =running )". min_infix_len и expand_keywords приведут, к тому что запрос RV 2205 выдаст RV2205. Кстати, тире – это разделитель эквивалентный пробелу. Так что RV-2205 то же выдаст RV2205.


charset_table = 0..9, A..Z->a..z, _, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F, U+401->U+0435, U+451->U+0435
Приводим латиницу и кириллицу в нижний регистр. Ё заменяем на е.


blend_chars = +, &, U+2C, U+2E
У меня много нецелых чисел. Их надо индексировать полностью. U+2C и U+2E это точка и запятая. Например, 1.25 будет индексирован как '1.25', '1' и '25'.


regexp_filter = (\d+)\,(\d+) => \1.\2
Десятичные знаки в числах могут быть разделены точками и запятыми: "1,75", "1.75". Приведем все к точке


Синонимы и опечатки


Единицы измерения можно писать по русски или английски: мм-mm, мАч-mAh, мВт-mW. Добавляем в словарь синонимов, путь к которому указан в wordforms: "мач > mah". Язык для индекса выбираю по собственным предпочтениям.


Знак ~ указывает применять замену после обработчика морфологии. Это позволяет не писать все словоформы и вместо правил для 'корка', 'корку', 'корки' написать "~корк > кузов"


Мой список полностью:


~регулятор > esc 
регуль > esc
мач > mah
~корк > кузов
~корпус  > кузов
~пищалк > buzz
~бузер > buzz
~буззер > buzz
~зуммер > buzz
~зумер > buzz
~бальс > бальз
~двигатель > мотор
~электродвигатель  > мотор
li-po > lipo
~аммортизатор > амортизатор 
~зарядк > зарядн
серво > сервопривод
серва > сервопривод
vtx > видеопередатчик
~антен > антенн
lollipop > lolipop
battery > аккумулятор
~пульт > аппаратур
~безколлекторн > бесколлекторн
~пиньен > пиньон
mkF > мкФ
бек > BEC
бэк > BEC
~термоусадк > термоусадочн
LED > светодиод
~светодиодн > светодиод 
driver > драйвер
~пакет > сумк
~пропеллер > лопаст
ААА > AAA
АА > AA
М > M
mm > мм
мВт > mW
В > V
А > A
deans > t-plug
tplug > t-plug  

Прилипание букв к цифрам


Иногда числа это часть названия (например LCD5208D), но чаще характеристика (100mAh, 10x15x4мм). Отделяем все числа от букв и индексируем.


Это решит несколько задач:


  • Кто-то будет искать 'подшипник 10x15x4', кто-то 'подшипник 15x10x4'. Проиндексированные числа приведут к правильной выдаче.
  • Единицы измерения могут быть или не быть отделены пробелом от числа: "1.75мм", "1.75 мм".
  • Для названий это тоже полезно. Правильная выдача будет по трем вариантам записи LCD-5208, LCD 5208 и LCD5208

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


Уберем икс, хэ и звезду в размерах типа 10х15х4 M3x10:


regexp_filter = (\d+)[x\x{0445}\*] => \1 x

Отбросим хвосты:


regexp_filter = (\d*\.?\d+)(\D+) => \1 \2

И головы:


regexp_filter = (\D+)(\d*\.?\d+) => \1 \2

Отбросим "мм", так как они часто не указаны в названии товара.
Сделаем файл stop.txt и пропишем его в stopwords.
Содержимое:


мм
mm

Теперь немного про PHP


Sphinxapi рано или поздно будет depricated. Будем использовать Sphinxql. Для этого надо подключиться к БД. В моем случае Sphinx подключаемого через хостинг это выглядит так:


$opt  = array(
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => TRUE,
);
$dsn = 'mysql:host=127.0.0.1;port=9306;';
$this->pdo = new PDO($dsn, DB_USER, DB_PASS, $opt);  

А все общение со Spinxql это один SELECT передающий отфильтрованный текст запроса


$stmt = $this->pdo->prepare("SELECT `id`, WEIGHT() as `w`, in_stock>0 AS stock  FROM `items` WHERE MATCH ('".$search."') ORDER BY clearance ASC, stock DESC, w DESC LIMIT ".$limit." OPTION field_weights=(name=10, art=3, cat_names=3, model_names=3)");

SphinxQL не понимает выражения в разделе сортировки ORDER BY, поэтому WEIGHT() и in_stock>0 пришлось поместить в поля. Кстати, LIMIT по умолчанию всего 20.


Сортировка сначала выдаст позиции в наличии, потом временно отсутствующие, потом архивные. И все эти три группы будут отсортированы по релевантности (весу).


Через field_weights задаем какие поля будут обладать большим весом.


Выполнив запрос, мы получим отсортированный массив id. Но, к сожалению, отбор данных через WHERE id IN() эту сортировку нарушит. Придется формировать свой запрос для каждого id.


На этапе отладки сильно помогает запрос "SHOW META" сразу после запроса SELECT. Особенно для проверки словаря wordforms и регулярных выражений фильтров. Можно увидеть перечень ключевых слов, на которые разложился запрос.


Усложняем sql_query


Мы продаем запчасти. Я решил добавить в индекс название категории товара и название модели, для которой предназначается запчасть. Но каждый товар может быть привязан сразу к нескольким категориям и подходить для нескольких моделей. И я открыл для себя функцию GROUP_CONCAT Она позволяет получить данные по группировке в строку. Например поле categories.name будет содержать все категории отобранного items_zip.id через пробел.


SELECT items_zip.id, `art`, items_zip.`name`, `clearance`, `in_stock`,
   GROUP_CONCAT(DISTINCT categories.name SEPARATOR ' ') AS cat_names,
   GROUP_CONCAT(DISTINCT items.family SEPARATOR ' ') AS model_names
FROM items_zip LEFT JOIN items_cat ON items_cat.item_id=items_zip.id
    LEFT JOIN categories ON categories.id=items_cat.cat_id
    LEFT JOIN zip_comp ON zip_comp.zip_id=items_zip.id
    LEFT JOIN items ON zip_comp.model_id=items.id
WHERE items_zip.show_flag=1 GROUP BY items_zip.id

Комментарии (8)


  1. coh
    05.02.2019 17:40

    В теме запчастей задача поинтереснее — обрабатывать запросы вида «колодки кашкай» (если в наименованиии товара нет указания применяемости). Ее тоже можно решить текдоком и sphinx.


    1. kolu4iy
      05.02.2019 17:47

      И тут прихожу я, с вопросом «колодки веста». Текдок говорит «ой», хотя реально — это колодки логана и прочих ренобазных машин. Всё немножечко сложнее, как показывает практика.


      1. coh
        05.02.2019 17:52

        Кто же спорит задача комплексная: каталоги, применяемость, кроссы, наименования, алиасы


  1. pumi
    05.02.2019 19:24

    +Если ничего не нашлось, можно поискать еще с учетом опечаток (при помощи расстояния Левенштейна).


    1. ilih
      05.02.2019 23:01

      В сфинксе уже есть выражения call qsuggest/suggest для поиска вариантов исправления опечаток
      www.sphinxsearch.com/docs/devel.html#sphinxql-call-qsuggest


  1. ilih
    05.02.2019 22:40

    > Но, к сожалению, отбор данных через WHERE id IN() эту сортировку нарушит. Придется формировать свой запрос для каждого id.

    для сохранения порядка записей в MySQL есть функция field().
    Использование:
    ORDER BY FIELD(id,3,2,1,4)
    записи будут отсортированы в порядке указанном в функции


    1. klirichek
      06.02.2019 11:17

      Тут от mysql всё же только протокол. Ну и возможность подключиться консольным клиентом, чтоб быстро "пощупать", как там дела.
      Всё прочее до определённого момента пытается поддерживаться совместимым, но это всё же SphinxQL, НЕ MySQL. И если во втором есть разные FIELD — это в общем случае бесполезная информация (надо смотреть доку сфинкса, есть ли ТАМ эта функция)


      1. ilih
        06.02.2019 13:50

        процитирую автора статьи еще раз
        «Выполнив запрос, мы получим отсортированный массив id. Но, к сожалению, отбор данных через WHERE id IN() эту сортировку нарушит. Придется формировать свой запрос для каждого id.»

        Пояснение:
        — выполнив запрос к сфинксу получили id документов, отсортированные по релевантности
        — автор предлагает для сохранения релевантности получать документы из БД (не из сфинкса) отдельным запросом для каждого id
        — на деле можно получить все документы одним запросом сохранив релевантность запросом вида «select… from… where id in (?) order by field(id,?)», на место? подставляется перечисление id документов

        повторю: речь не про запросы к сфинксу, а про запросы к оригинальной БД