tech-tips/Программное обеспечение/SphinxSearch/Настройка поиска Sphinx для интернет-магазина.md

210 lines
12 KiB
Markdown
Raw Normal View History

---
source: https://habr.com/ru/post/439018/
---
Информации по #Sphinx не так много, как хотелось бы. Лишняя статья не помешает.
Первые шаги в освоении Sphinx мне помогли сделать статьи
[Создание ознакомительного поискового движка на Sphinx + php](https://habr.com/ru/post/104690/) и [Пример Sphinx поиска на реальном проекте — магазин автозапчастей Tecdoc](https://habr.com/ru/post/132118/). Советую начать с них.
Некоторое время на моем сайте работал поиск через 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 подключаемого через хостинг это выглядит так:
```php
$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` передающий отфильтрованный текст запроса
```php
$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](http://blog.nagaychenko.com/2010/06/15/%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0-%D1%81-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B5%D0%B9-group_concat/) Она позволяет получить данные по группировке в строку. Например поле categories.name будет содержать все категории отобранного `items_zip.id` через пробел.
```sql
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
```