Files
test-search-engine/README.md
T

263 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Offline Product Matcher
Небольшой FastAPI-сервис для поиска товаров из `catalog_excel.csv` по сообщениям
покупателей. Работает локально, без внешних API и ML-моделей.
## Запуск
Нужен Python 3.12.
```bash
python -m pip install -r requirements.txt
python -m app
```
По умолчанию каталог читается из `catalog_excel.csv` в корне проекта. Другой
путь можно передать через `CATALOG_PATH`:
```powershell
$env:CATALOG_PATH="C:\path\to\catalog.csv"
python -m app
```
Сервис запускается на `http://127.0.0.1:8000`.
## API
В обычном режиме доступен один маршрут:
```text
POST /match
```
Пример запроса:
```bash
curl -X POST http://127.0.0.1:8000/match \
-H "Content-Type: application/json" \
-d '{"messages":["Арс-12","удар","ушм на 150"]}'
```
Ответ:
```json
{
"results": [
{
"message": "Арс-12",
"status": "matched",
"candidates": [
{"sku": "INS-0017", "confidence": 0.9108}
]
},
{
"message": "удар",
"status": "ambiguous",
"candidates": [
{"sku": "INS-0008", "confidence": 0.6884},
{"sku": "INS-0009", "confidence": 0.6764},
{"sku": "INS-0022", "confidence": 0.6635}
]
},
{
"message": "ушм на 150",
"status": "not_found",
"candidates": []
}
]
}
```
Статусы:
| Статус | Значение |
| --- | --- |
| `matched` | После фильтрации остался один товар |
| `ambiguous` | Подходит несколько товаров; возвращаются первые три |
| `not_found` | Тип товара не распознан или характеристики не совпали |
`confidence` нужен для сортировки результатов. Это не вероятность и не
калиброванная оценка качества.
## Поиск
Обработка запроса выглядит так:
```text
нормализация текста
↓
определение типа товара
↓
разбор бренда, модели и характеристик
↓
отбор совместимых позиций
↓
лексическая сортировка
↓
matched / ambiguous / not_found
```
### Нормализация
Текст приводится к нижнему регистру и Unicode NFKC, `ё` заменяется на `е`,
десятичная запятая — на точку. Записи размеров `3,5х25`, `3.5x25` и
`3.5×25` получают одинаковое представление.
### Тип товара
Сначала проверяются известные названия и разговорные варианты: `ушм`,
`болгарка`, `шурик`, `гкл`, `круг зачистной` и другие.
Опечатки обрабатываются символьными триграммами и
Damerau–Levenshtein. Если явного названия нет, тип можно определить по словам
в каталоге. Например, `удар` приводит к ударным дрелям. Для такого перехода
три лучших каталожных совпадения должны относиться к одному типу и иметь общее
совпавшее слово. Одного числа недостаточно: `аквариум 125 мм` не превращается
в запрос на УШМ.
### Характеристики
Из текста извлекаются:
- размеры и длины;
- резьба;
- профиль бит;
- зернистость;
- напряжение и мощность;
- количество зубьев;
- фасовка и единица продажи;
- бренд и модель;
- тип SDS;
- разновидность товара: по дереву, по металлу, ГКЛ, отрезной, пильный и т. п.
Явное несовпадение исключает позицию. `T50` не совпадает с `T30`,
`150 мм` — со `125 мм`, а `4х2.5` — с `3х2.5`.
Неполные характеристики допустимы. Для `труба 20х20` подойдут разные толщины
стенки, поэтому ответ будет `ambiguous`.
В модельном коде можно пропустить конечную букву: `АРС-12` совпадает с
`АРС-12Л`. Числовая часть сравнивается целиком, поэтому `АРС-12` не совпадёт
с `АРС-125`.
Бренды `Makita` и `Макита` нормализуются в один бренд. В запросе
`как у макиты, только дешевле` бренд считается ориентиром, а не обязательным
фильтром. Слово `дешевле` включает сортировку по возрастанию цены.
### Примеры
| Запрос | Результат |
| --- | --- |
| `Арс-12` | `INS-0017` |
| `210 мм 24 зуба` | `DSK-0035` |
| `24 зуба` | три пильных диска с 24 зубьями |
| `ToolKraft 125 мм` | `INS-0027` |
| `удар мощно 900 хромированый анус долбить` | ударные дрели на 900 Вт |
| `бита T50` | `not_found`, если T50 нет в каталоге |
| `аквариум 125 мм` | `not_found` |
## Локальная страница для проверки
`search.html` отправляет запросы в `/match` и подгружает названия и цены
товаров. Файл исключён из Git и нужен только для ручной проверки.
Чтобы страница могла получать данные по SKU, запустите приложение с тестовым
маршрутом:
```powershell
$env:ENABLE_TEST_INFO_ROUTE="1"
python -m app
```
После этого откройте `search.html` в браузере. Дополнительный маршрут имеет вид
`GET /get_info_by/{sku}`. Без `ENABLE_TEST_INFO_ROUTE` он не регистрируется,
и приложение предоставляет только `POST /match`.
## Тесты
```bash
python -m pytest -q
```
`tests/test_message_corpus.py` читает каждую непустую строку из
`messages.txt` и `more_messages.txt`. Для каждого запроса явно указан
ожидаемый статус и порядок SKU. Добавленная в файл строка без ожидаемого
результата ломает тест.
Дополнительно проверяются:
- полные названия всех товаров каталога;
- канонические типы, бренды и модельные коды;
- размеры, резьба, профили, зернистость, мощность и другие атрибуты;
- опечатки и короткие запросы;
- ложные совпадения на числах и мусорном тексте;
- сортировка по цене;
- формат API и количество маршрутов.
## Структура проекта
```text
app/
├── aliases.py # типы товаров и их варианты
├── attributes.py # разбор и сравнение характеристик
├── catalog.py # загрузка каталога
├── dev_routes.py # тестовый маршрут с данными по SKU
├── intents.py # нетоварные обращения
├── main.py # FastAPI-приложение
├── matcher.py # основной поиск
├── models.py # модели API
├── normalization.py
├── search_index.py # BM25 и триграммы
└── typo.py # расстояния и похожесть строк
tests/
├── test_api.py
├── test_catalog_self_retrieval.py
├── test_matcher.py
├── test_message_corpus.py
└── test_normalization.py
```
## Если каталог станет большим
Текущая версия загружает каталог в память и рассчитана на несколько сотен
позиций. Для миллионов SKU поиск нужно перенести в базу:
- PostgreSQL с отдельными колонками для часто используемых характеристик;
- `JSONB` для редких атрибутов;
- B-tree индексы по магазину, типу товара, бренду и модели;
- `pg_trgm` или FTS для получения короткого списка кандидатов;
- проверка характеристик и финальная сортировка в приложении.
Пример последовательности:
```text
разобрать запрос
↓
определить магазин и тип товара
↓
применить точные фильтры в PostgreSQL
↓
получить 20–100 кандидатов через pg_trgm/FTS
↓
проверить совместимость и отсортировать
```
Для нескольких магазинов нужен `store_id`. Формат тела `/match` менять
нельзя, поэтому идентификатор можно брать из авторизации, hostname или HTTP
заголовка. Импорт каталога должен быть версионным: загрузка во временную
таблицу, проверка и атомарное переключение активной версии.
Elasticsearch имеет смысл добавлять только после замеров, если PostgreSQL не
справляется с полнотекстовым поиском или нагрузкой.
## Ограничения
- правила разбора привязаны к текущим товарным группам;
- `confidence` не калиброван на реальных обращениях;
- неизвестные цвета, диапазоны и сложные сравнительные запросы могут
потребовать новых правил;
- сервисные обращения и неизвестный текст возвращают одинаковый
`not_found`;
- остатки товаров в каталоге отсутствуют, поэтому наличие не проверяется.