Как мы сопоставили 41 157 строк составов косметики со справочником на 5184 ингредиента

Задача звучала просто: человек вставляет состав с этикетки, мы показываем, что означает каждый компонент. На практике около 37% совпадений не находятся сравнением строк, потому что производители пишут одно и то же вещество десятком способов, а часть составов приезжает из распознавания фотографии этикетки.

Я делаю Beauty Helper, сервис для разбора составов косметики. Ниже разбор матчера: какие тиры сопоставления мы построили, почему каждый следующий тир опаснее предыдущего и где мы осознанно отказались от матча.

Что приходит на вход

Строка состава с реальной упаковки может выглядеть так: «Aqua, Sodium Laureth Sulfate, Cocamidopropyl Betaine, 1,2-Hexanediol, Parfum, Limonene, Linalool, C12,15 Alkyl Benzoate». Первая ловушка живёт прямо здесь: наивное разбиение по запятой превращает «1,2-Hexanediol» в два ингредиента, «1» и «2-Hexanediol». У нас в базе до сих пор лежат следы этой ошибки, например позиция «bis-ethoxydiglycol cyclohexane 1» с отрезанным хвостом «,4-dicarboxylate». Обратная крайность тоже была: один из ранних парсеров резал строго по «запятая плюс пробел», и состав вида «AQUA ,PROPYLENE GLYCOL ,PEG-40» оставался одной строкой целиком.

Правило, к которому мы пришли: резать по запятой, точке с запятой, маркеру списка и переводу строки, кроме одного случая. Запятая между двумя цифрами без пробелов вокруг сначала прячется под служебным символом и возвращается на место уже после разбиения строки. Подмена на служебный символ вместо регулярного выражения с lookbehind выбрана намеренно: lookbehind не поддерживается в Safari до версии 16.4, а это заметная доля старых iPhone у российской аудитории, и неподдерживаемый синтаксис в литерале регулярки роняет весь модуль на этапе разбора, то есть выдаёт белый экран вместо деградации одной функции.

Тир 1. Строгий ключ

Точное сравнение строк закрывает мало. Производители пишут «Sodium Benzoate», «SODIUM BENZOATE», «Sodium benzoate (preservative)», «Sodium Benzoate 0.5%». Нормализация приводит строку к нижнему регистру, чинит гомоглифы, убирает скобочные уточнения и хвост-концентрацию, схлопывает лишние пробелы.

Отдельная история это гомоглифы. В составах, набранных в русской раскладке, регулярно попадаются кириллические «а», «е», «о», «с», «р» внутри латинских слов. Визуально строка идентична, побайтово нет. Мы приводим 12 кириллических гомоглифов к латинским аналогам до любого сравнения.

Тот же ключ считается и в SQL при чистке данных, формулы совпадают строка в строку. Если ключ на клиенте и ключ в миграции разойдутся, дубли схлопнутся не там, где ожидалось.

Тир 2. Агрессивный ключ для распознанных фото

Распознавание фотографии этикетки ломает пробелы и дефисы: «Butyl-eneGlycol», «Butylene Glycol», «ButyleneGlycol». Строгий ключ такое не ловит, поэтому есть второй, который оставляет в строке только буквы, цифры и слэш.

Опасность очевидна: чем агрессивнее нормализация, тем выше шанс склеить разные вещества. Два ограничения снимают почти весь риск. Ключ короче четырёх символов не используется, и матч засчитывается только при единственном кандидате: при коллизии тир молча пропускается, а не гадает.

Тир 3. Опечатки через расстояние Левенштейна

Этот тир появился из живого бага. В составе одного блеска для губ почти ничего не сопоставилось, хотя вещества в базе были: «Polysobutene» вместо «Polyisobutene», «Alcyl» вместо «Alkyl», «Hydroganated» вместо «Hydrogenated». Ни один ключ такое не ловит: тут буквальная разница в буквах, форматирование ни при чём.

Ключи короче шести символов не матчатся вовсе. Допустимая дистанция растёт с длиной ключа: один символ для ключей короче десяти, два для более длинных. Полный перебор 5184 ключей на каждую несопоставленную строку не нужен: ключи разложены по корзинам по длине, просматриваются только корзины в пределах допустимой дистанции, а само расстояние считается с ранним выходом, как только минимум текущего ряда превысил лимит.

Правило единственного кандидата действует и здесь: если два разных ингредиента дают одинаковое расстояние, матча нет. Пользователю лучше увидеть нераспознанную строку, чем чужое вещество.

Тир 4. Русские названия

Некоторые производители печатают состав только по-русски. У нас русское название заполнено у 4804 записей из 5184, то есть данные для обратного поиска были давно, ими просто не пользовались.

Русский тир получился самым осторожным из всех, и вот почему. В русском полно коротких слов, отличающихся одной буквой при совершенно разном смысле: вода и сода, мёд и лёд. Поэтому точное совпадение работает с четырёх символов, а поиск опечатки включается только с восьми.

Отдельный подтир решает перестановку катиона и аниона: производители пишут то «Бензоат натрия», то «Натрия бензоат». Ключ сортирует слова, но применяется строго к двум словам и без всякого нечёткого поиска сверху. Две ослабленные проверки разом, порядок плюс правописание, подняли бы риск ложного матча выше пользы.

Коллизии в справочнике

Одно русское название нередко указывает на несколько записей: «Глицерин» это и Glycerin, и Glycerine, и Glicerin, то есть дубли, которые ещё не слиты. Отказ от матча при коллизии стоил бы нам пользы, поэтому есть детерминированный отбор: сначала более заполненная запись (описание, группа, алиасы), потом более короткое латинское название, последним критерием меньший идентификатор записи.

Короткое имя выигрывает по практической причине: на «бензоат натрия» справочник отвечает и Sodium Benzoate, и Sodium Benzoate Potassium Sorbate, а вторая запись описывает сразу два вещества и правильным ответом быть не может.

Что получилось в цифрах

На 20 августа 2026 года: 1477 средств, 41 157 позиций в составах, сопоставлено 40 308, то есть 97,9%. Несопоставленные хранятся текстом без привязки к справочнику, они видны в интерфейсе как есть и попадают в очередь на пополнение справочника.

Источник матча пишется в отдельном поле. Оно появилось не сразу, поэтому заполнено у 10 808 позиций, остальные приехали массовым импортом раньше. По этим 10 808 распределение такое:

ТирПозицийДоля
Точное совпадение строки683463,2%
Нормализация и алиасы348632,3%
Опечатки (Левенштейн)2152,0%
Агрессивный ключ (распознанные фото)1831,7%
Русские названия900,8%

Главный вывод для тех, кто строит похожее: точное сравнение строк закрывает примерно две трети задачи. Следующая треть достаётся нормализацией, и это самый дешёвый по трудозатратам тир. Хвостовые тиры дают около 4,5% и отнимают львиную долю времени на отладку, зато именно они превращают «сервис не понял мой состав» в «сервис понял».

Где это работает

Матчер живёт в вебе, всё сопоставление идёт на клиенте: справочник загружается один раз и кэшируется, поэтому разбор состава не требует круга к серверу. Парсер и матчер покрыты юнит-тестами на реальных составах, которые когда-то ломались.

Посмотреть, как это выглядит — Вставляете состав с банки, получаете разбор по каждому компоненту.

Симона, разбираю составы косметики и собираю схемы домашнего ухода за волосами и лицом. Автор сервиса Beauty Helper.

Больше обо мне →

Что дальше

У меня есть раздел бесплатных материалов: чек-лист чтения этикетки, разборы частых ошибок в уходе за волосами и лицом, таблица сочетаемости активов. Он открывается после короткого опроса и регистрации, платить за материалы не нужно.

А когда понадобится разобрать конкретную банку с полки, это умеет сервис Beauty Helper: вставьте состав с упаковки и получите разбор по каждому компоненту с проверкой сочетаемости по всей Вашей полке.