Почему «удалили файл» недостаточно
В рабочем RAG один документ быстро превращается в несколько производных объектов. Исходник лежит в файловом хранилище, разобранный текст — в промежуточной базе, фрагменты — в очереди индексации, embeddings с метаданными — в векторном индексе, готовые ответы — в кэше. К этому добавляются журналы, резервные копии и неудачные задания, ожидающие повтора.
Если удалить только исходный PDF, векторный поиск продолжит находить его фрагменты. Если очистить индекс, повторное задание может загрузить их обратно. Поэтому удаление — не кнопка в одном интерфейсе, а распределённый бизнес-процесс с идентификатором, состояниями и проверкой результата.
Для малого бизнеса это важно не только при запросе субъекта персональных данных. Причины бывают прозаичнее: расторгнут договор, ошибочная версия инструкции, секретный прайс, отозванное коммерческое предложение или документ, загруженный не в тот проект.
Сначала нужен стабильный document_id
Удаление невозможно доказать, если части документа связаны только именем файла. Имя меняется, совпадает у разных клиентов и теряется после OCR. При приёмке каждому источнику нужен стабильный `document_id`, а каждому фрагменту — производный `chunk_id`, например хэш от `document_id`, версии и позиции.
Эти идентификаторы должны проходить через весь конвейер:
- запись источника и объектное хранилище;
- результат OCR или парсинга;
- очередь и таблицу заданий;
- payload каждого вектора;
- поисковый индекс;
- кэш ответов и ссылки на использованные документы;
- аудит операций.
Без этой связи команда вынуждена искать текстовые совпадения. Такой поиск пропускает переформатированные фрагменты и может удалить чужие документы с похожим содержанием.
Удаление как сага из пяти шагов
Практичная архитектура начинается с реестра удаления. API принимает `document_id`, причину, область и идемпотентный ключ. Запрос переводит документ в состояние `blocked`: новые чтения и индексация запрещены немедленно, даже если физическая очистка ещё выполняется.
Дальше оркестратор проходит пять шагов:
1. Помечает источник удалённым и запрещает повторное создание задания.
2. Отменяет ожидающие и повторяющиеся задания по этому `document_id`.
3. Удаляет фрагменты из полнотекстового и векторного индексов по точному фильтру.
4. Инвалидирует ответы и семантический кэш, где документ указан в provenance.
5. Запускает контрольный поиск и сохраняет квитанцию о завершении.
Каждый шаг имеет статус, число найденных объектов и краткую ошибку. Повтор запроса с тем же ключом продолжает незавершённую сагу, а не создаёт параллельное удаление. Это тот же принцип идемпотентности, который защищает платежи и отправку писем.
Почему асинхронный ответ ещё не успех
Векторные и поисковые движки часто выполняют массовое удаление асинхронно. Qdrant позволяет удалять points по фильтру и ждать применения операции. OpenSearch `delete_by_query` может вернуть task ID; документация отдельно описывает мониторинг задачи, конфликты версий, частичные ошибки и refresh.
Поэтому HTTP 200 или `acknowledged` означает только принятие команды. Оркестратор должен дождаться терминального состояния, проверить список failures и затем убедиться, что изменение видно чтению. Для OpenSearch это означает учитывать refresh; для любого движка — выполнить контрольный запрос по `document_id`.
Массовые wildcard-удаления опасны. Фильтр должен включать tenant, коллекцию и точный `document_id`. Перед выполнением полезен dry run: посчитать ожидаемое число фрагментов и остановиться, если оно резко отличается от реестра индексации.
Tombstone защищает от возвращения документа
В потоковой архитектуре удаление должно распространяться как событие. Debezium для PostgreSQL создаёт событие DELETE и затем tombstone с тем же ключом и `null`-значением, чтобы системы с log compaction могли убрать старое состояние.
Внутри RAG полезен собственный tombstone-реестр: `document_id`, версия удаления, время и область. Любой worker перед upsert проверяет его. Даже если старое сообщение задержалось или задача вернулась из dead-letter queue, документ не будет создан заново.
Tombstone хранит минимум данных и не должен содержать удаляемый текст. Его срок жизни выбирают длиннее максимального времени хранения очередей, ретраев и импортных пакетов. Иначе очень старое сообщение сможет воскресить запись после истечения запрета.
Кэши, журналы и резервные копии
Кэш готовых ответов нужно связывать с источниками. Если запись хранит только запрос и ответ, нельзя адресно понять, какие результаты зависели от удалённого документа. Решение — сохранять список `document_id` или версию базы знаний и удалять связанные записи.
Операционные журналы по умолчанию не должны содержать полный контекст модели. Для диагностики обычно достаточно хэшей, идентификаторов, метрик и кодов причин. Если содержимое всё же логируется, оно включается в карту хранения и процедуру удаления.
Резервные копии требуют отдельной политики. Мгновенное переписывание всех backup часто технически и экономически неразумно. Реалистичная схема: исключить удалённые данные из восстановления через tombstone-журнал, ограничить срок хранения backup и проверить процедуру restore в изолированной среде. В квитанции честно указывают, что удалено из активных систем, а что исчезнет по расписанию хранения.
Как проверить, что удаление действительно завершено
Контроль должен искать не только исходный текст. После завершения система выполняет:
- запрос источника по `document_id`;
- проверку очередей, retry и dead-letter;
- фильтр векторного и полнотекстового индексов;
- поиск зависимых cache entries;
- тестовый RAG-запрос с характерной фразой документа;
- сверку числа удалённых chunks с реестром индексации.
Результат — короткая квитанция: идентификатор операции, время, затронутые хранилища, ожидаемое и фактическое число объектов, ошибки и срок удаления из backup. Сам удалённый текст в квитанцию не копируют.
Для чувствительных сценариев добавляют принцип двух лиц: один сотрудник инициирует удаление, другой подтверждает область. Это полезно и против случайного удаления всей клиентской коллекции.
Экономика и границы
Модельный пилот можно оценить так: пять хранилищ, 20 удалений в месяц, в среднем 15 минут ручной проверки каждого — 25 часов в год. При условной полной стоимости часа 2 000 рублей это 50 000 рублей прямого времени, без учёта инцидентов. Автоматизация оправдана, если разработка и сопровождение дешевле и снижает риск ошибок.
Но строить сложную платформу ради двух удалений в год не нужно. Минимальный вариант — стабильный `document_id`, запрет повторной индексации, удаление по фильтру, очистка кэша и проверочный запрос. Реестр состояний можно хранить в той же транзакционной базе, где живут задания индексации.
Важно различать удаление из RAG и «разучивание» базовой модели. Если документ использовался только как внешний контекст, достаточно очистить производные хранилища. Если на данных дообучали веса, задача становится machine unlearning или переобучением и требует отдельного решения.
Что сделать руководителю
Попросите команду взять один тестовый документ и показать его полный маршрут: где лежит исходник, chunks, embeddings, ответы и логи. Затем удалите его в непроизводственной среде, намеренно задержите старое задание и проверьте, что оно не воскресит данные.
Критерий готовности простой: документ перестаёт участвовать в ответах сразу после блокировки, все активные копии удаляются с подтверждением, а восстановление и повторная доставка не возвращают его. Только тогда кнопка «Удалить» означает процесс, а не надежду.
