RAG не находит слово из PDF: лигатуры и мягкий перенос в извлечённом тексте
В PDF есть строка office policy. Человек видит обычное слово office, но извлечённый текст содержит office: два символа fi склеены в лигатуру fi. В другом абзаце слово «договор» разбито невидимым мягким переносом и переводом строки. Поиск по словам пропускает оба места, хотя визуально документ выглядит исправным.
Такие ошибки легко принять за проблему модели эмбеддингов. Начните с более дешёвой проверки: распечатайте repr извлечённого фрагмента и сравните символы с запросом. Если различие уже появилось между PDF и текстом, изменение top-k не вернёт отсутствующее точное совпадение. Нужен отдельный шаг подготовки поискового текста.
Посмотрите на символы, а не на изображение страницы
Сохраните небольшой проблемный абзац до любых очисток. Для каждого подозрительного символа можно вывести код U+XXXX и название через unicodedata.name. Так различаются обычный дефис, мягкий перенос U+00AD и неразрывный дефис. У них разные задачи: артикул AB-007 нельзя превратить в AB007 только потому, что где-то дефис переносил слово.
Проверьте несколько страниц одного файла. Если extractor систематически отдаёт fi вместо fi, это воспроизводимое правило преобразования. Если буквы перепутаны после распознавания скана, потребуется разбирать качество OCR. Нормализатор символов исправляет известное представление текста; он не угадывает слова, которых в извлечении уже нет.
Держите оригинал и поисковую копию отдельно
Исходный текст нужен для просмотра и цитирования. В поисковой копии можно раскрыть заранее согласованные лигатуры и убрать явный мягкий перенос внутри слова. Пример ниже заменяет только небольшой список символов и соединяет буквы вокруг U+00AD. Обычный дефис и перенос строки без специального знака остаются как были.
Не применяйте глобальное «убрать все дефисы перед переводом строки». В технической документации строка может закончиться частью артикула, диапазоном или составным словом. Для неоднозначных случаев нужны данные о структуре страницы либо отдельная проверка извлечения. Узкое правило проще проверить на рабочих документах и объяснить владельцу базы.
import re
import unicodedata
LIGATURES = str.maketrans({
"\ufb00": "ff", "\ufb01": "fi", "\ufb02": "fl",
"\ufb03": "ffi", "\ufb04": "ffl",
})
def search_text(original):
text = original.translate(LIGATURES)
# Remove only an explicit soft hyphen between word characters.
return re.sub(r"(?<=\w)\u00ad(?:\r?\n)?(?=\w)", "", text)
original = "of\ufb01ce policy\nДо\u00ad\nговор AB-007\nплан-\nграфик"
prepared = search_text(original)
queries = ["office", "Договор", "AB-007"]
hits_before = {q: q.lower() in original.lower() for q in queries}
hits_after = {q: q.lower() in prepared.lower() for q in queries}
assert hits_before == {"office": False, "Договор": False, "AB-007": True}
assert all(hits_after.values())
assert "план-\nграфик" in prepared
assert "AB-007" in prepared
assert original != prepared
print({"before": hits_before, "after": hits_after})
print(unicodedata.name("\ufb01"), repr(original))До подготовки находятся только артикул и его неизменённые символы; после — все три контрольных запроса. «План-» в конце строки остаётся разорванным, потому что пример не знает, относится ли дефис к слову. Это полезный результат диагностики: неизвестный случай не спрятан массовым удалением знаков.
Подключите подготовку до разбиения на фрагменты
Порядок обработки важен: извлеките страницу, сохраните её оригинал, подготовьте поисковую копию, затем разбивайте её на фрагменты и стройте индекс. Если сначала разрезать текст на части, мягкий перенос может оказаться на границе и правило уже не увидит соседние буквы. Версию нормализатора сохраните рядом с версией загрузчика.
Каждый поисковый фрагмент должен ссылаться на ID документа, страницу и исходный абзац. Не переносите числовые смещения из нормализованной строки в оригинал: раскрытие одной лигатуры меняет длину. Для показа цитаты проще сначала вернуть исходный абзац целиком. Если интерфейсу нужна точная подсветка слова, потребуется явное соответствие позиций между копиями.
Для генератора задайте вопрос «Что описывает раздел office policy?» и передайте найденный фрагмент вместе с документом и страницей. Инструкция: «Ответь по содержимому источника, укажи документ и страницу; не достраивай недостающие правила». Нормализация помогает источнику попасть в поиск. Содержательный ответ по-прежнему зависит от того, что действительно написано в разделе.
Проверьте пользу на маленьком наборе
Соберите реальные примеры лигатур, мягких переносов, артикулов и слов с обычным дефисом. Для каждого сохраните исходный абзац, ожидаемую поисковую форму и допустимые изменения. После обновления сравните не только число найденных запросов, но и ложные совпадения: если подготовка склеила разные коды, рост найденных строк нельзя считать улучшением.
Наконец, повторите поиск на реальном индексе до и после обработки одних и тех же документов. Локальный скрипт показывает изменение строк и точных совпадений; результат вашего гибридного или векторного поиска нужно измерять отдельно. Так решение о полной переиндексации опирается на конкретные вопросы, которые раньше не находили нужный абзац.
Проверьте, что уже делает используемый поисковый движок. Например, некоторые способы приведения регистра сами раскрывают лигатуры. В локальном сравнении применяется lower, чтобы изменение было видно отдельно от такого преобразования. Если ваш анализатор уже находит office в исходной строке, эта часть подготовки может оказаться лишней; мягкие переносы и переносы строк всё равно проверьте собственными запросами. Сохраните конфигурацию анализатора вместе с результатом эксперимента, иначе одинаковая Python-строка в двух индексах даст разные совпадения.
Доведите подготовку PDF до проверяемого RAG
После очистки поисковой копии нужно проверить выдачу и ответы по всему набору документов. В платном курсе LangChain вы создадите RAG по PDF с цитатами, гибридным поиском и реранжированием, измерите качество и задержку. Затем подключите приложение через API и наблюдаемость, чтобы видеть, на каком этапе потерялся нужный источник. Откройте программу платного курса «LangChain 1.0.x: c нуля до продакшн LLM-приложений».
Продолжить на практике
- LangChain 1.0.x: c нуля до продакшн LLM-приложений ↗
Практический курс по LangChain. Соберёте продакшн LLM-бота: RAG по PDF с цитатами, веб-поиск и трекинг заказов, многошаговый агент для анализа CSV и финальное веб-приложение (UI + API). Плюс профессиональные блоки: PromptOps и версионирование промптов, метрики качества RAG, гибридный поиск с реранком, observability/стоимость, надёжность (ретраи, таймауты), guardrails и отдельный модуль по LangGraph для устойчивых workflow. P.S. Полностью обновили материалы на актуальную версию LangChain
