Дубликаты документов в векторной базе: как убрать лишнее
Дубликаты документов в векторной базе портят поиск и ответы LLM. Разберем checksum, смысловое сходство и приоритет источника без лишней теории.

Почему дубликаты ломают поиск и ответы
В начале дубликаты в векторной базе почти не пугают. Индекс растет, поиск что-то находит, ответы приходят. Но потом появляется неприятный эффект: данных стало больше, а ответы не стали лучше. Иногда они становятся хуже.
Причина простая. Один и тот же факт попадает в выдачу несколько раз. В базе лежат копии регламента, старая выгрузка из wiki и тот же текст в PDF. Поиск возвращает их вместе. Формально это несколько документов, по смыслу - один и тот же материал. Пользователь не получает новой информации, а модель видит повтор.
Дальше страдает top-k. У поиска мало мест в выдаче, и каждое место важно. Если три из пяти слотов заняли копии одного документа, в контекст не попадут соседние источники: свежая инструкция, исключение из правила, комментарий поддержки. В итоге RAG хуже покрывает вопрос и чаще упускает детали.
Проблема бьет и по самой LLM. Когда она читает несколько похожих фрагментов подряд, она чаще повторяет одну и ту же мысль и сильнее цепляется за устаревшую формулировку. Если старая версия документа размножилась по базе, модель может принять ее за более надежную просто потому, что видит ее чаще. Частота начинает выглядеть как достоверность.
Есть и более приземленный эффект: индекс растет без пользы. Вы храните лишние чанки, дольше считаете эмбеддинги, тратите больше памяти и времени на поиск. Качество ответа при этом не растет. Иногда оно падает, потому что система выбирает из большего объема шума.
На практике дубликаты портят не только точность, но и доверие к системе. Человек задает вопрос и получает ответ, где одна мысль повторяется три раза, а важное исключение пропадает. После нескольких таких случаев пользователи начинают винить весь поиск. Обычно не зря.
Какие дубликаты встречаются чаще всего
Повторы обычно появляются не из-за одной большой ошибки, а из-за обычной жизни данных. Файл пересохранили под новым именем, статью выгрузили в другой формат, старую версию забыли убрать, один и тот же текст приехал из нескольких систем. Так дубликаты копятся тихо, а поиск начинает возвращать почти одинаковые фрагменты вместо разных источников.
Самый простой случай - полная копия файла, когда содержимое совпадает байт в байт. Обычно это повторная загрузка того же PDF или архив с уже известными документами. Чуть сложнее случай, когда один и тот же текст лежит в PDF, HTML и DOCX. Форматы разные, служебные символы разные, но для пользователя это один документ.
Рядом со старой версией часто живет новая. Название похоже, смысл почти тот же, но меняются даты, суммы, условия или несколько абзацев. Во многих базах повторяются и одинаковые служебные блоки: футеры, шаблонные дисклеймеры, подписи отделов, стандартные инструкции в конце документа. Иногда именно они забивают индекс сильнее, чем полезный текст.
Еще один частый источник шума - копии из wiki, сайта, CRM, Service Desk и внутреннего хранилища. Команда видит один документ, а индекс получает три или четыре почти одинаковые версии.
Хуже всего работают не полные копии, а почти одинаковые документы с мелкими отличиями. Они выглядят безобидно, но именно они ломают ранжирование, мешают обновлениям и дают модели лишние совпадающие куски контекста.
Когда хватает checksum
Checksum хорошо работает там, где у вас появляются точные копии. Это самый дешевый и быстрый способ убрать повторы еще до индексации. Если сотрудник загрузил тот же PDF второй раз под другим именем, хеш поймает это сразу.
Считать checksum лучше до чанкинга. Сначала проверьте целый файл, и только потом режьте его на части. Иначе вы потратите время на OCR, очистку, эмбеддинги и запись в индекс для документа, который уже есть в базе.
Обычно checksum закрывает четыре типовых случая: повторную выгрузку одного и того же отчета без изменений, точную копию файла в другой папке, документ с новым именем, но тем же содержимым, и повторную загрузку после сбоя пайплайна.
На практике полезно хранить не один, а два хеша. Первый считают от исходного файла. Он ловит побайтные копии. Второй считают от нормализованного текста, где уже убрали лишние пробелы, колонтитулы, мусор из конвертации и привели текст к одному виду.
Это помогает в реальных загрузках. Один и тот же регламент могут прислать как PDF, DOCX и текст после конвертера. Побайтно это разные файлы, поэтому file checksum не совпадет. Но после нормализации текст часто окажется одинаковым, и второй хеш снимет дубль без сложных правил.
После OCR и очистки мусора хеш тоже стоит пересчитать. Скан одного и того же договора может отличаться на уровне файла из-за метаданных, сжатия или настроек сканера. Зато текст после OCR уже может совпасть почти полностью. Если не считать хеш заново, вы пропустите много точных повторов.
У checksum есть жесткое ограничение: он ищет только одинаковое. Если два документа передают один смысл разными словами, хеш этого не увидит. Не заметит он и случаи, где автор поправил пару абзацев, поменял порядок разделов или обновил дату в шапке.
Для пайплайна RAG checksum - хороший первый фильтр. Он быстро срезает точные копии и повторные выгрузки. Но на нем одном останавливаться не стоит. Тексты с одним смыслом придется ловить другими правилами.
Когда смотреть на смысловое сходство
Checksum хорошо ловит точные копии. Но в реальной базе чаще мешают почти одинаковые тексты: один и тот же регламент в PDF и в wiki, FAQ после новой выгрузки, шаблон письма с чуть другой версткой. В таких случаях помогает смысловое сходство эмбеддингов.
Эмбеддинги лучше считать на двух уровнях. На уровне документа можно найти почти полные дубликаты, даже если у них разный формат и другой порядок блоков. На уровне чанков видны повторы внутри больших файлов, когда 80% текста совпадает, а меняется только один раздел.
Порог сходства не стоит делать одинаковым для всей базы. Короткие тексты легко обманывают: два абзаца про разные вещи могут получить высокий score только потому, что у них общий словарь. Для коротких чанков порог обычно держат выше. Для длинных документов его можно немного снизить, потому что смысл распределен по всему тексту.
Хорошая стартовая точка такая: для коротких чанков проверяйте пары примерно от 0.92 и выше, для длинных документов смотрите диапазон около 0.82-0.90, а все пограничные случаи отправляйте на ручную проверку.
Ручная проверка нужна всегда. Иначе система начнет выбрасывать то, что похоже по форме, но отличается по сути. Это часто видно во внутренних базах знаний: две инструкции почти совпадают, но в одной срок 30 дней, а в другой 45. Для поиска это уже не дубликат, а другая норма.
Особенно осторожно стоит работать с шаблонными фразами. Юридические дисклеймеры, стандартные вступления, футеры, подписи и повторы навигации сильно завышают сходство. Их лучше убирать до расчета эмбеддингов или хотя бы снижать их вес. Тогда система сравнивает полезное содержание, а не служебный шум.
Есть простой тест, который быстро отрезвляет. Если в двух близких текстах различаются даты, суммы, лимиты, роли согласования или условия применения, удалять один из них рано. Сначала пометьте пару как near-duplicate, а потом решите, какой источник важнее и какая версия новее.
Во внутреннем RAG это видно сразу. Если загрузить старый тариф и новый тариф как один документ, ответ получится уверенным, но неверным. Лучше оставить оба текста, связать их как похожие и дать более свежей версии больший вес при поиске.
Как расставить приоритет источников
Если один и тот же факт есть в регламенте, wiki, письме и на сайте, проблема уже не в поиске, а в правилах доверия. Индекс должен заранее знать, какой источник сильнее. Иначе RAG вытащит случайный фрагмент и ответит не по той версии.
Во многих случаях дубликаты перестают сильно мешать, когда команда задает порядок источников еще до загрузки. Часто логика такая: утвержденный регламент или политика выше, чем внутренняя wiki; wiki выше, чем письмо с разъяснением; письмо выше, чем публичный сайт. Сам порядок можно менять под свой процесс, но не стоит менять его от случая к случаю. Иначе поиск начнет вести себя неровно.
Официальность без свежести тоже не спасает. Если у вас есть две версии регламента, новая должна побеждать до индексации. Старую лучше пометить как superseded или убрать из рабочего индекса. Иначе ассистент будет цитировать пункт, который уже отменили.
Черновики и утвержденные документы не стоит держать в одном рабочем слое без явной пометки. Черновик полезен редактору, но мешает продакшен-поиску. Проще держать его в отдельной коллекции или ставить approval_status=draft и исключать из выдачи.
Какие метаданные сохранить
Причину выбора лучше записывать рядом с документом, а не держать в голове команды. Обычно хватает нескольких полей: source_type, source_rank, version_date, approval_status, supersedes и chosen_reason.
Тогда вы сможете объяснить, почему система взяла один текст и отбросила другой. Это особенно полезно там, где нужно показать, на каком основании ассистент ответил именно так.
Простой пример: в компании есть письмо о новом порядке обработки персональных данных и есть утвержденный регламент, который вышел через неделю. В индекс лучше отправить регламент как основной источник, а письму дать низкий приоритет или оставить его только для аудита. Сначала команда решает конфликт версий, потом режет документ на чанки и строит эмбеддинги. Этот порядок почти всегда чище, чем попытка разобраться уже после поиска.
Как собрать рабочее правило
Дедупликацию лучше строить как конвейер, а не как один фильтр. Если смешать все проверки сразу, часть дублей исчезнет, часть останется, а часть случайно победит более свежую версию.
Сначала приведите текст и метаданные к одному виду. Уберите повторяющиеся шапки, подписи, лишние пробелы, номера страниц, HTML-мусор и другие служебные блоки, которые не меняют смысл. Затем выровняйте метаданные: один формат даты, один набор статусов, единые названия источников. Иначе два одинаковых документа будут выглядеть разными только потому, что в одном дата записана как 01.02.2025, а в другом как 2025-02-01.
После нормализации убирайте точные копии через checksum. Лучше считать хеш не только от сырого файла, но и от уже очищенного текста. Так вы поймаете один и тот же регламент, который пришел в PDF и DOCX, если содержимое у них совпадает. Этот шаг дешевый и быстрый, поэтому его почти всегда ставят первым.
Дальше ищите почти одинаковые версии через смысловое сходство эмбеддингов. Но не сравнивайте все со всем. Сначала сузьте кандидатов по типу документа, разделу базы или языку, а потом считайте сходство. Порог тоже не стоит делать общим для всей базы: для инструкций, договоров и FAQ он часто разный. Все спорные пары лучше складывать в очередь на ручную проверку.
В конце включается правило приоритета источника и версии. Оно отвечает на вопрос, что считать основной записью, если тексты очень близки, но живут в разных системах или относятся к разным редакциям. Это уже не задача эмбеддингов. Это правило доверия.
Простой пример для внутренней базы знаний
У команды есть обычная внутренняя база: действующий регламент в PDF, страница в wiki с тем же правилом простыми словами и старая рассылка, где это правило когда-то объявили сотрудникам. На первый взгляд набор полезный. На практике в индекс попадает почти один и тот же смысл в трех видах, и поиск начинает тянуть лишнее.
Сначала помогает checksum. Если один и тот же PDF выгрузили повторно после очередного импорта, хеш сразу покажет точное совпадение. Это быстрый шаг и почти без ложных срабатываний, но он видит только точный повтор. Если документ сохранили в другом формате или слегка поменяли верстку, checksum уже не поможет.
Дальше подключают смысловое сходство эмбеддингов. Оно связывает страницу wiki и HTML-копию той же инструкции, даже если у них разная разметка, другой заголовок и немного отличается порядок абзацев. Для RAG это полезнее, чем кажется: система понимает, что перед ней не два независимых источника, а одна и та же норма в разной упаковке.
После этого нужен приоритет источника. Иначе письмо из рассылки окажется рядом с регламентом и добавит шум. В рабочем правиле действующий регламент остается основным документом, wiki получает роль удобного пересказа, а письмо уходит в вспомогательные или архивные материалы.
Тогда результат меняется заметно. Top-k чаще приносит один сильный документ вместо трех копий. Ответ модели тоже становится спокойнее: она опирается на действующий регламент, а не смешивает норму, пересказ и старое письмо в один неуверенный вывод.
Где дедупликация ломается
Частая ошибка начинается слишком рано: команда сначала режет документы на чанки, а потом пытается искать повторы. В этот момент связь между версиями уже слабеет. Один абзац из новой редакции может выглядеть как дубль старой, хотя весь документ давно изменился.
Из-за этого в индексе остаются куски старой версии, куски новой и несколько почти одинаковых фрагментов без понятного родителя. Поиск потом тащит их вместе, а RAG отвечает смесью из разных редакций.
Еще одна типичная ошибка - один порог сходства для всех документов. Для новостей, инструкций, договоров и прайс-листов это плохая идея. Короткая карточка товара и длинный регламент могут получить одинаковый score, но риск ошибки у них разный.
Особенно опасно удалять близкие тексты без проверки чисел, дат и исключений. Два фрагмента могут отличаться одной строкой: лимитом в 500 000 рублей, сроком действия до 31 декабря или фразой про исключение для отдельного отдела. Эмбеддинги часто считают такие тексты почти одинаковыми, но для бизнеса это уже другой документ.
Много проблем дают таблицы, вложения и OCR. Таблица после конвертации в текст теряет структуру, и строки меняются местами. Вложение к письму может содержать ту же политику, но с подписью, печатью или приложением. OCR добавляет шум: путает 0 и O, 1 и I, дробит слова, съедает переносы. В итоге система то склеивает разные документы, то считает дублями то, что ими не было.
Отдельно часто забывают про связь между удаленной копией и оригиналом. Это мешает разбору ошибок, аудиту и повторной индексации. Если через месяц окажется, что правило было слишком агрессивным, восстановить цепочку уже трудно.
Обычно хватает простого набора полей: canonical_document_id для оригинала, duplicate_of для удаленной копии, source_system, source_priority, version_date или revision и причина, по которой документ признали дублем. С таким минимумом проще откатить решение и понять, почему система оставила один текст, а второй убрала.
Быстрая проверка перед загрузкой
Перед импортом новая пачка документов должна пройти короткий фильтр. Иначе дубликаты тихо раздуют индекс, собьют ранжирование и начнут вытеснять более свежие версии.
Сначала сравните два checksum. Первый считайте с исходного файла, второй - с очищенного текста после OCR, удаления колонтитулов и нормализации пробелов. Совпадение первого ловит точные копии файла. Совпадение второго находит случаи, когда файл другой, а текст внутри тот же.
Потом проверьте метаданные. У каждого документа должны быть источник, дата, статус и владелец. Без этого система не поймет, что делать с двумя почти одинаковыми инструкциями: одна лежит в архиве, другая действует сейчас, третью вообще загрузил тестовый сервис.
Дальше задайте порог для смыслового сходства, но не берите его с потолка. На 20-30 реальных парах быстро видно, где модель начинает путать редакции одного документа с разными материалами на одну тему. Для политики безопасности и для FAQ порог часто нужен разный. Это нормально.
Еще один полезный шаг - не только удалять дубликаты, но и уметь помечать их. Во многих случаях лучше оставить запись в индексе, связать ее с основной версией и понизить ее вес при поиске. Тогда команда сможет разобрать спорный случай, откатить ошибку и понять, почему система выбрала один документ, а не другой.
После пробной загрузки проверьте три вещи: долю дублей, размер индекса и качество поиска на контрольных запросах. Если индекс вырос на 40%, а ответы лучше не стали, загрузку лучше остановить и пересчитать правила.
Что делать дальше
Не пытайтесь сразу очистить всю базу. Лучше взять один домен, где ошибки уже заметны: регламенты поддержки, продуктовые FAQ или внутренние инструкции. Затем соберите 20-30 спорных пар документов и вручную подпишите их: это точная копия, почти копия, новая версия или просто похожий текст с другим смыслом.
Смотрите не только на метрики индекса, но и на ответы на живых вопросах. Возьмите набор запросов, которые сотрудники или пользователи задают каждый день, и сравните результат до и после очистки. Обычно разница видна быстро: ответ короче, источник чище, в выдаче меньше почти одинаковых кусков.
Рабочее правило обычно выглядит просто. Сначала убирают полные копии по checksum. Потом ищут почти одинаковые тексты по эмбеддингам. В конце решают конфликт по приоритету источника и версии документа. Спорные случаи отправляют на ручную проверку, а не пытаются чинить бесконечной настройкой порогов.
Не стоит менять пороги каждую неделю. Если команда постоянно двигает границы сходства с 0.88 на 0.91, а потом обратно, база начинает жить по настроению, а не по правилам. Намного полезнее один раз зафиксировать простую политику, прогнать ее на тестовом наборе и обновлять только после новых наблюдений.
Для RAG в России есть еще один слой проверки. Если в документах, логах запросов или пользовательских вопросах встречаются персональные данные, сразу проверьте, где это хранится, кто видит журналы, как устроен аудит и что попадает в резервные копии. Это уже не мелкая настройка качества, а часть нормальной эксплуатации под требования 152-ФЗ.
Если вы строите пайплайн на RU LLM, клиентский код ради дедупликации менять не нужно. Можно оставить OpenAI-совместимый вызов через api.rullm.com, а правила очистки, приоритет источника и отбор версий держать отдельно в своем конвейере загрузки и индексации.
Финальный тест простой: задайте десять частых вопросов и посмотрите, стал ли ответ понятнее с первого чтения. Если да, правило уже работает.
Часто задаваемые вопросы
Почему дубликаты вообще портят RAG-поиск?
Они занимают места в top-k и вытесняют соседние источники. В итоге модель видит повтор одного и того же текста вместо свежей версии, исключения или комментария.
Из-за этого ответ часто повторяет одну мысль и пропускает детали. Пользователь видит шум и меньше доверяет поиску.
В каких случаях хватает checksum?
Когда в базе лежит точная копия файла. Это лучший первый фильтр для повторной загрузки PDF, файла с новым именем или дубля после сбоя импорта.
Считайте хеш до чанкинга. Так вы не потратите время на OCR, очистку и эмбеддинги для документа, который уже есть.
Может ли checksum сам убрать все повторы?
Нет, не решает. Хеш ловит только одинаковое содержимое или почти то же самое после нормализации текста.
Если документ пересохранили в другом формате, поменяли пару абзацев, дату или порядок блоков, checksum такой случай часто пропустит. Тут уже нужно сравнивать смысл.
Когда лучше проверять смысловое сходство, а не хеш?
Смотрите на эмбеддинги, когда тексты почти совпадают по смыслу, но отличаются форматом, версткой или мелкими правками. Это частый случай для PDF, wiki, HTML и старых выгрузок из разных систем.
Полезно проверять сходство и на уровне документа, и на уровне чанков. Так проще найти и почти полные копии, и повторяющиеся куски внутри больших файлов.
Как выбрать порог сходства без долгой настройки?
Начните с простого ориентира: для коротких чанков смотрите пары от 0.92, для длинных документов — примерно 0.82–0.90. Потом проверьте эти границы на своих 20–30 реальных парах.
Не ставьте один порог на всю базу. FAQ, договоры и инструкции ведут себя по-разному, поэтому один общий score часто дает лишние ошибки.
Что проверять вручную перед удалением похожих документов?
Сначала проверьте числа, даты, лимиты, роли и исключения. Если они отличаются, перед вами часто не дубль, а другая версия правила.
В спорных случаях не удаляйте документ сразу. Пометьте пару как near-duplicate и дайте команде решить, какой источник оставить основным.
Как выбрать главный источник, если один текст есть в регламенте, wiki и письме?
Задайте порядок доверия заранее. Обычно утвержденный регламент ставят выше wiki, wiki — выше письма, а письмо — выше публичной страницы.
Потом добавьте свежесть. Если есть две версии одного регламента, новая должна выигрывать даже тогда, когда старая выглядит более знакомой для поиска.
Какие метаданные хранить для дедупликации?
Минимум такой: source_type, source_rank, version_date, approval_status, supersedes и chosen_reason. Эти поля помогают понять, почему система оставила один документ и понизила другой.
Еще полезно хранить canonical_document_id и duplicate_of. Тогда вы сможете откатить спорное решение и быстро восстановить связь между копией и оригиналом.
Нужно ли искать дубликаты уже после чанкинга?
Нет, так делать не стоит. Сначала нормализуйте документ, уберите точные копии и разберите версии, а потом режьте текст на части.
Если начать с чанков, поиск смешает куски старой и новой редакции. После этого модель легко соберет ответ из разных версий одного правила.
С чего начать, если база уже раздулась от дублей?
Не пытайтесь чистить всю базу сразу. Возьмите один домен, соберите 20–30 спорных пар и вручную подпишите их: точная копия, почти копия, новая версия или просто похожий текст.
После этого прогоните контрольные вопросы до и после очистки. Если выдача стала короче, а ответы — понятнее с первого чтения, правило уже приносит пользу.