Offline Product Matcher
Небольшой FastAPI-сервис для поиска товаров из catalog_excel.csv по сообщениям
покупателей. Работает локально, без внешних API и ML-моделей.
Запуск
Нужен Python 3.12.
python -m pip install -r requirements.txt
python -m app
По умолчанию каталог читается из catalog_excel.csv в корне проекта. Другой
путь можно передать через CATALOG_PATH:
$env:CATALOG_PATH="C:\path\to\catalog.csv"
python -m app
Сервис запускается на http://127.0.0.1:8000.
API
В обычном режиме доступен один маршрут:
POST /match
Пример запроса:
curl -X POST http://127.0.0.1:8000/match \
-H "Content-Type: application/json" \
-d '{"messages":["Арс-12","удар","ушм на 150"]}'
Ответ:
{
"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 нужен для сортировки результатов. Это не вероятность и не
калиброванная оценка качества.
Поиск
Обработка запроса выглядит так:
нормализация текста
↓
определение типа товара
↓
разбор бренда, модели и характеристик
↓
отбор совместимых позиций
↓
лексическая сортировка
↓
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, запустите приложение с тестовым маршрутом:
$env:ENABLE_TEST_INFO_ROUTE="1"
python -m app
После этого откройте search.html в браузере. Дополнительный маршрут имеет вид
GET /get_info_by/{sku}. Без ENABLE_TEST_INFO_ROUTE он не регистрируется,
и приложение предоставляет только POST /match.
Тесты
python -m pytest -q
tests/test_message_corpus.py читает каждую непустую строку из
messages.txt и more_messages.txt. Для каждого запроса явно указан
ожидаемый статус и порядок SKU. Добавленная в файл строка без ожидаемого
результата ломает тест.
Дополнительно проверяются:
- полные названия всех товаров каталога;
- канонические типы, бренды и модельные коды;
- размеры, резьба, профили, зернистость, мощность и другие атрибуты;
- опечатки и короткие запросы;
- ложные совпадения на числах и мусорном тексте;
- сортировка по цене;
- формат API и количество маршрутов.
Структура проекта
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 для получения короткого списка кандидатов;- проверка характеристик и финальная сортировка в приложении.
Пример последовательности:
разобрать запрос
↓
определить магазин и тип товара
↓
применить точные фильтры в PostgreSQL
↓
получить 20–100 кандидатов через pg_trgm/FTS
↓
проверить совместимость и отсортировать
Для нескольких магазинов нужен store_id. Формат тела /match менять
нельзя, поэтому идентификатор можно брать из авторизации, hostname или HTTP
заголовка. Импорт каталога должен быть версионным: загрузка во временную
таблицу, проверка и атомарное переключение активной версии.
Elasticsearch имеет смысл добавлять только после замеров, если PostgreSQL не справляется с полнотекстовым поиском или нагрузкой.
Ограничения
- правила разбора привязаны к текущим товарным группам;
confidenceне калиброван на реальных обращениях;- неизвестные цвета, диапазоны и сложные сравнительные запросы могут потребовать новых правил;
- сервисные обращения и неизвестный текст возвращают одинаковый
not_found; - остатки товаров в каталоге отсутствуют, поэтому наличие не проверяется.