--- 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 ```