[Из песочницы] Настройка поиска Sphinx для интернет-магазина

habr.png

Информации по 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, 10×15x4 мм). Отделяем все числа от букв и индексируем.

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


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

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

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

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

© Habrahabr.ru