210 lines
12 KiB
Markdown
210 lines
12 KiB
Markdown
|
---
|
|||
|
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
|
|||
|
```
|