12 KiB
Информации по #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 = [
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