RAG · Retrieval-augmented generation · урок 3 · 13 мин
Normalization: цифры, письменности, Unicode
Normalization: numbers, scripts, Unicode
Зачем
После урока ты объяснишь, почему правильный документ бывает невидим для поиска из-за цифр, разделителей, форм Unicode и смешения письменностей, и напишешь функцию normalization на 10 строк. В вакансиях это иногда называют «data regularization», и уточняющий вопрос о смысле этого слова сам по себе показывает зрелость.
Что такое normalization и чем она не является
Normalization (нормализация) — приведение одного и того же к одному виду, чтобы сравнение, поиск и сопоставление работали. Её путают с sanitization (санитарной очисткой): sanitization убирает опасное (персональные данные, секреты, скрытые инструкции), normalization ничего не убирает по смыслу, а выравнивает запись.
Слово «data regularization» в описании вакансии — не стандартный термин. В конвейере данных оно почти наверняка означает normalization. В машинном обучении regularization значит другое: приёмы, которые мешают модели заучить обучающие данные (штрафы L1 и L2 на веса, dropout, early stopping). Сильный кандидат спрашивает: «Which meaning do you have in mind?» — и отвечает на оба.
Цифры и разделители
Одно число пишут по-разному в разных письменностях. Арабско-индийские цифры ٠١٢٣٤٥٦٧٨٩ (U+0660–U+0669), их персидский вариант ۰۱۲۳۴۵۶۷۸۹ (U+06F0–U+06F9), деванагари ०१२३… — это разные символы Unicode, хотя значат то же, что 0–9. Документ с «١٥٠٠» и запрос «1500» для поиска по словам — разные строки. Это тихий промах: ошибки нет, документа в выдаче тоже нет.
Интересная деталь Python: int('١٥٠٠') вернёт 1500 — функция понимает любые десятичные цифры Unicode. Но строковое сравнение, BM25 и регулярное выражение \d в чужом движке этого не делают, поэтому цифры в тексте приводят к 0–9 явно.
Разделители тысяч ещё хитрее. «1,500», «1 500» (с обычным, неразрывным или узким неразрывным пробелом U+202F), «1.500», «١٬٥٠٠» (арабский разделитель U+066C). Хуже всего точка и запятая: «1.500» по-английски — полтора, по-немецки — тысяча пятьсот; «1,5» по-русски — полтора. Угадывать нельзя. Правило: локаль документа (locale — правила записи чисел и дат для языка и страны) — поле metadata (служебных сведений о документе), и разбор чисел с точкой и запятой идёт по ней. Однозначные разделители (U+066C, узкие пробелы между группами из трёх цифр) можно убирать всегда.
То же с датами и суммами. «03/04/2026» — 3 апреля или 4 марта в зависимости от страны; бывают даты по другому календарю (например, хиджры) и разные часовые пояса. «AED 1,500», «1500 dirhams» и «1 500 ₸» для поиска удобнее хранить ещё и в разобранном виде: сумма числом и код валюты отдельными полями metadata. Тогда фильтр «комиссии больше 1 000» делает код, а не текстовое совпадение.
Unicode: NFC, NFD, NFKC
Один и тот же видимый символ можно записать одной кодовой точкой или двумя. «é» — это либо U+00E9, либо «e» плюс комбинируемое ударение U+0301. На экране одинаково, а == вернёт False, и длины строк будут 4 и 5 для «Café». Unicode определяет формы нормализации (стандарт UAX #15):
| Форма | Что делает | Когда |
|---|---|---|
| NFC | собирает символ в одну точку, где можно | по умолчанию для хранения и сравнения |
| NFD | раскладывает на базовую букву и знаки | так приходят имена файлов с некоторых систем |
| NFKC | NFC плюс замена «совместимых» символов | для ключа поиска, с осторожностью |
NFKC превращает лигатуру «fi» в «fi», полноширинные «1500» в «1500», римскую цифру «Ⅰ» в «I». Но она же делает из «x²» строку «x2», а из «½» — «1⁄2». Сам стандарт предупреждает: формы KC и KD нельзя слепо применять к произвольному тексту, они стирают различия, важные для смысла. Важно и обратное: NFKC не трогает арабско-индийские цифры — их переводят отдельной таблицей.
Ещё два шага того же рода: casefold() (регистр, агрессивнее, чем lower()) и свёртка вариантов букв, своя для каждого языка: «ё» → «е» в русском, варианты алифа и диакритика в арабском. Свёртка помогает поиску, но тоже стирает различия, поэтому её проверяют замером.
Смешанные письменности
Кириллическая «А» и латинская «A» выглядят одинаково, но это разные символы. Код продукта «АF-2026», набранный с русской раскладкой, не совпадёт с «AF-2026» ни в BM25 (поиске по словам), ни в базе. Имена в латинской транслитерации пишут по-разному («Mohammed», «Muhammad»), а запросы бывают на двух языках в одной фразе.
Стандарт UTS #39 описывает mixed-script detection (обнаружение строки из нескольких письменностей) и confusables (похожие символы) с функцией skeleton (скелет строки: каждый символ заменён типовым двойником). Важная оговорка стандарта: skeleton — не normalization, а внутренний инструмент проверки, и хранить его как ключ навсегда нельзя: таблицы меняются с версиями Unicode. Практический вывод: для кодов и идентификаторов проверяй, что строка в одной письменности, и отклоняй или исправляй смесь при вводе.
Правило двух сторон и функция на 10 строк
Главное правило: одна и та же функция normalize() на ingestion (приёме документов в систему) и на запросе. Нормализовал документы, но не запрос — поиск промахивается молча. Версию функции пишут в metadata индекса: изменили normalization — переиндексируй, иначе стороны разойдутся.
Второе правило: нормализованная копия — только для поиска. Оригинальный текст хранится рядом и идёт в ответ и в citation (ссылку на источник), а проверка чисел в ответе сравнивает с оригиналом.
import re, unicodedata
DIGITS = str.maketrans('٠١٢٣٤٥٦٧٨٩۰۱۲۳۴۵۶۷۸۹', '0123456789' * 2)
GROUP = re.compile(r'(?<=\d)[٬ ](?=\d{3}\b)')
def normalize(s: str) -> str:
s = s.translate(DIGITS) # арабско-индийские и персидские цифры
s = GROUP.sub('', s) # ٬ и пробелы между группами цифр
s = unicodedata.normalize('NFKC', s)
return s.casefold()
assert normalize('١٬٥٠٠') == normalize('1 500') == '1500'
Порядок шагов важен. Если сначала вызвать NFKC, узкий неразрывный пробел превратится в обычный, и правило разделителей уже не отличит его от пробела между словами.
Когда normalization вредит
Та же функция склеит «раздел 4 500 AED» в «раздел 4500 AED» — номер раздела слился с суммой. NFKC превратит «м²» в «м2», свёртка диакритики сольёт разные слова. Поэтому normalization проверяют замером: 10–20 вопросов на каждом языке с цифрами в обеих формах, recall (доля найденных верных документов) до и после, отдельно по языку.
Схема
Термины · Key terms
- normalization
- приведение одного и того же к одному виду для поиска и сравнения
- sanitization
- удаление опасного: персональных данных, секретов, скрытых инструкций
- regularization (ML)
- приёмы против заучивания данных моделью: L1, L2, dropout
- code point
- кодовая точка, номер символа в Unicode
- NFC / NFD
- собранная и разложенная форма одного символа
- NFKC
- NFC плюс замена совместимых символов; теряет часть различий
- casefold
- приведение регистра для сравнения
- locale
- локаль: правила записи чисел и дат для языка и страны
- mixed-script
- строка из символов нескольких письменностей
- confusables
- разные символы, которые выглядят одинаково
- citation
- ссылка на источник утверждения в ответе
Сделай сам
Скопируй функцию из урока и допиши три проверки через assert: «١٥٠٠» равно «1500»; «Café», записанное через комбинируемое ударение, равно «Café» одной точкой; «АF-2026» с кириллической «А» не равно «AF-2026». Добавь функцию, которая для строки печатает имена письменностей её букв через unicodedata.name(), и проверь её на «АF-2026».
Обсуди с тьютором: покажи код и вывод и спроси, стоит ли исправлять кириллическую «А» в кодах автоматически или лучше отклонять такой ввод — и где в pipeline (конвейере обработки) это делать.
Проверь себя · ответ под вопросом
1Что сломается, если нормализовать документы, но не запрос?
2Какие два значения у слова regularization, и что ты спросишь собеседующего?
3Приведи случай, когда нормализация вредит: стирает различие, важное для смысла.
Interview questions · US / UK
1The job description says "data regularization". What would you assume it means, and what would you build?
2Users search "1500" and miss documents that contain the same amount. How do you debug it?
3Scenario: someone proposes applying NFKC and stripping all diacritics on every document. Do you agree?
Ключевые фразы · Say it aloud
- Normalize the same way at ingestion and at query timeодинаковая нормализация на входе и на запросе
- A silent missтихий промах: ошибки нет, документа нет
- The normalized copy is the search key; the original is what we show and citeкопия для поиска, оригинал для ответа
- Compatibility forms must not be applied blindlyNFKC нельзя применять вслепую
- Which meaning of regularization do you have in mind?какой смысл regularization вы имеете в виду?
- Version the normalizer and re-index when it changesверсия нормализатора и переиндексация
Материалы
- Unicode UAX #15 — Unicode Normalization Forms — разделы 1.1–1.2: canonical и compatibility, предупреждение про KC и KD (10 мин)
- Python — unicodedata —
normalize,decimal, пример с «Ç» (5 мин) - Unicode UTS #39 — Unicode Security Mechanisms — разделы 4 и 5.1: confusables и mixed-script detection (10 мин)