Представим договор поставки № 18/К с ООО «Лесной двор». Его загрузили в базу знаний, потом контрагент потребовал убрать документ, и администратор стёр записи из векторного индекса. Поиск по коллекции больше ничего не находит. Через час юрист открывает историю чата, листает вверх и видит вчерашний ответ ассистента: сумма неустойки, срок оплаты и цитата из пункта 6.2 на месте. Документа в индексе нет, а его содержание продолжает жить в сохранённом ответе.
Так выглядит вторая жизнь данных. RAG устроен так, что один файл при загрузке расходится по нескольким местам: исходник лежит в хранилище, чанки превращаются в точки индекса, запросы оставляют записи в журнале, часть ответов оседает в кеше, а регулярный снимок коллекции уносит копию на отдельный диск. Каждое из этих мест удаляется своим способом и в своё время. Дальше мы проведём через них один вымышленный документ и посмотрим, что нужно сделать в каждом, в каком порядке и когда можно сказать, что данных не осталось нигде, кроме явно разрешённых копий.
Договор № 18/К и все числа ниже условные: они нужны, чтобы расчёты были наглядными, а не чтобы описывать чью-то реальную систему. Если вы только собираете такую систему, сначала загляните в материал про то, как поднять RAG по своим документам на сервере. Многое из того, что мы разберём, проще заложить при загрузке, чем разгребать после удаления.
Сначала оригинал: с чего начинается след
Удаление начинается не с команды к индексу, а с выяснения, что именно мы удаляем. У документа в системе должен быть устойчивый идентификатор, который не зависит от имени файла. Файл «Договор_18К_финал_v3.pdf» мог быть переименован, перезалит под другим именем или существовать в двух версиях. Поэтому в нашем примере у договора есть идентификатор doc-0418 и контрольная сумма содержимого. Именно по ним мы будем искать следы дальше.
Первый вопрос: где лежит оригинал и сколько у него экземпляров. Типичная картина выглядит так:
- основной файл в папке или объектном хранилище, откуда его забирает загрузчик;
- предыдущие версии и корзина, если файловое хранилище их ведёт (в сервисах вроде Nextcloud это нужно проверить отдельно: удалённый файл может ещё лежать в корзине и в истории версий);
- временная копия, которую загрузчик скачал для разбора и забыл удалить;
- текст, извлечённый из PDF и сохранённый рядом как промежуточный результат.
Есть ещё одна причина начинать именно с исходника: если оставить его на месте, любая плановая синхронизация может спокойно проиндексировать файл заново. Вы удалите точки утром, а ночное задание «подтянуть новые и изменённые документы» вернёт их к обеду. Поэтому первое действие по документу такое: зафиксировать doc-0418, его хэш и путь, а затем остановить повторную индексацию. Либо вынести файл из наблюдаемой папки, либо добавить идентификатор и хэш в список запрещённых к загрузке. Только после этого имеет смысл трогать индекс.
Заметьте, что на этом этапе мы ещё ничего не стёрли. Мы составили карту: какие хранилища затрагивает документ. Без неё удаление превращается в гадание, и «вроде всё убрали» остаётся единственным критерием успеха.
Один файл, четырнадцать точек
Теперь индекс. При загрузке договор был разрезан на чанки. Допустим, получилось четырнадцать фрагментов, и каждый стал точкой в коллекции векторной базы. У точки есть идентификатор, вектор и полезная нагрузка (payload) со служебными полями. Чтобы потом не искать иголку в стоге, в payload каждой точки нужно было записать doc_id, номер чанка и версию документа. Это решение принимается при первой загрузке, и от него зависит вся дальнейшая лёгкость удаления.
Есть два способа держать связь между документом и его точками.
- Манифест загрузки. При индексации вы сохраняете в обычной базе или файле список: документ
doc-0418, четырнадцать идентификаторов точек. Удалять тогда можно по списку идентификаторов, и вы точно знаете, сколько точек должно исчезнуть. - Фильтр по полю payload. Каждая точка несёт
doc_id, и удаление идёт по условию «все точки, у которыхdoc_idравенdoc-0418». Манифест не нужен, но придётся полагаться на то, что поле заполнено везде без исключений.
Надёжнее комбинация: фильтр делает удаление полным, а манифест позволяет проверить, что результат совпал с ожиданием. Если манифест говорит «четырнадцать», а до удаления фильтр показывает шестнадцать, значит, где-то остались чанки от другой версии файла, и это стоило выяснить до, а не после.
Вторая причина сверяться заранее: чанки могут пережить редактирование документа. Если договор перезагружали после правки, а старые точки не удалили, в индексе лежит две редакции. Тогда «удалить документ» означает «удалить все версии под этим doc_id», и фильтр по идентификатору документа справляется с этим лучше, чем список точек одной версии.
Кеш и история: две разные памяти
Вернёмся к юристу и вчерашнему ответу. Здесь смешиваются две сущности, которые удобно называть одним словом «кеш», хотя устроены они по-разному и удаляются по-разному.
Кеш нужен, чтобы не считать одно и то же дважды. Это может быть кеш результатов поиска (запрос → найденные чанки), кеш готовых ответов или семантический кеш, который отдаёт старый ответ на похожий вопрос. Содержимое кеша производное: его всегда можно сбросить и пересобрать, ничего не потеряв, кроме скорости. Опасность в том, что он отдаёт ответ, не заглядывая в индекс. Если семантический кеш помнит ответ про неустойку по договору № 18/К, он выдаст его, даже когда точек давно нет.
Как чистить кеш, зависит от ключей. Если записи помечены идентификаторами документов, на которые опирались, достаточно удалить все записи с меткой doc-0418. Если помечены только хэшем запроса, выбирать нечего: сбрасывается весь кеш или его пространство имён. Для небольшой системы это дёшево: несколько часов ответы будут считаться заново, и всё. Для большой системы стоит заранее предусмотреть метку «на каких документах основан ответ», иначе ради одного договора придётся выбрасывать весь накопленный кеш.
История ответов устроена иначе. Это журнал: кто, когда и о чём спросил, что ответила система и на какие фрагменты сослалась. В нём нет цели ускорять работу, он нужен людям: чтобы вернуться к найденному, разобрать ошибку, показать коллеге. Поэтому историю нельзя просто «сбросить», и то, что в ней оказалось, не пересоберётся из индекса. Вчерашний ответ с цитатой из пункта 6.2 хранится в виде готового текста, и удаление точек его не касается.
С историей возможны три разных решения, и выбор между ними определяется принятой у вас политикой:
- удалить записи целиком, если в них не было ничего, кроме работы с этим документом;
- вычистить из записей цитаты и пересказы документа, оставив вопрос, время и пометку «содержание удалено»;
- оставить записи, если политика прямо разрешает хранить историю общением ограниченный срок, и записать дату, когда она исчезнет вместе с остальным журналом.
Чтобы найти нужные записи, пригодится то же самое, что помогало с кешем: в журнале каждого ответа должен лежать список doc_id использованных источников. Тогда запрос «все ответы, где среди источников есть doc-0418» делается за секунду. Без этого придётся искать по текстовым признакам, а это ненадёжно: цитата могла быть пересказана своими словами.
Отдельно стоит поискать журналы приложения. При отладке часто включают запись полного промпта, а в промпте лежат те самые чанки. Такой журнал живёт по своим правилам ротации и обычно не связан с историей чата, но содержит ровно тот же текст. Если он ведётся, его срок хранения и содержимое входят в карту следов. То же относится к внешним языковым моделям: если запросы с фрагментами договора отправлялись стороннему API, срок хранения запросов определяется условиями того сервиса, и из вашей системы его не изменить. Эту точку можно только записать в карту и учитывать при ответе на запрос об удалении.
Удаляем points по связанному ID и дожидаемся применения
К этому моменту у нас есть карта, остановлена повторная загрузка, и мы знаем, что искать. Теперь собственно индекс. В Qdrant точки удаляются по идентификаторам или по фильтру, а параметр wait позволяет дождаться применения изменения, а не получить ответ о принятой в очередь операции. Для нашей задачи эта разница существенна: ответ «принято» ещё не означает, что проверочный поиск уже не найдёт договор.
Удаление по фильтру для нашего документа выглядит так (имя коллекции и поле doc_id в примере условные, у вас они свои):
POST /collections/company_docs/points/delete?wait=true
{
"filter": {
"must": [
{ "key": "doc_id", "match": { "value": "doc-0418" } }
]
}
}Если используете манифест, вместо фильтра в тело передаётся список идентификаторов из него. Форму запроса сверяйте с версией вашего сервера: ключи и пути у API со временем могут меняться, а проверка перед удалением обходится дешевле ошибки.
Теперь о порядке действий, потому что он важен. Кеш и историю логично чистить после индекса, а не до. Представьте обратный порядок: вы сбросили кеш, но точки ещё живы. Первый же пользователь, спросивший про неустойку, заставит систему найти чанки договора, собрать ответ и снова положить его в кеш. Вы убрали кеш зря. Если же сначала снять точки и дождаться применения, новый ответ уже не сможет опереться на договор, и очистка кеша окончательна.
Поэтому рабочая последовательность для одного документа такая:
- Зафиксировать
doc_id, хэш и путь, заблокировать повторную загрузку. - Удалить точки с
wait=true. - Убедиться, что точек с этим
doc_idне осталось. - Удалить записи кеша, связанные с документом, либо сбросить кеш.
- Обработать историю ответов и журналы по принятой политике.
- Убрать исходник и его временные копии.
Исходник в этом списке последний не из прихоти: пока он на месте, можно повторить разбор и убедиться, что набор точек, которые мы удалили, совпадает с набором, который дал бы повторный разбор. Когда проверка пройдена, файл можно убирать.
Как проверять? Две вещи. Первая: подсчёт или выборка точек по фильтру doc_id = doc-0418 должны давать ноль. Вторая, не менее важная: проверочные запросы с характерными словами из договора. Например, ищем название контрагента или редкую формулировку из пункта 6.2. Если среди найденного нет ни одного фрагмента из договора, а ответ ассистента не содержит его данных, индексный слой чист. Обратите внимание на слова «индексный слой». Пока мы не вышли за пределы рабочей коллекции, и про снимки речь впереди.
Один оговорённый момент: удаление точки из коллекции означает, что она перестаёт участвовать в поиске и выдаче. Мы не обещаем и не должны обещать, что байты тут же исчезнут с диска: за физическую очистку хранилище отвечает собственными фоновыми процессами, и их сроки зависят от настроек и нагрузки. Для процедуры это значит, что «удалено из рабочего индекса» и «физически стёрто со всех носителей» остаются разными утверждениями, и в записи об удалении нужно использовать первое.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Снимок помнит то, чего уже нет
Теперь самое неприятное место. Допустим, у нас настроен ежедневный снимок коллекции в 03:00 с хранением четырнадцать суток. Это условный пример, он понадобится для расчёта. Снимок фиксирует состояние коллекции на момент создания, а значит, содержит все четырнадцать точек договора и переживёт любые последующие операции в рабочей коллекции. Удалить точки и удалить снимок — два разных действия. Первое меняет живую коллекцию, второе выполняется отдельной операцией по имени снимка, и одно не вызывает другое.
Вызов выглядит примерно так (имя снимка условное):
DELETE /collections/company_docs/snapshots/company_docs-2026-10-31-03-00.snapshotТеперь посчитаем на нашем примере. Договор лежал в индексе сорок дней, удалён в 11:00 сегодняшнего дня D. К этому моменту в хранилище лежат четырнадцать ежедневных снимков, и все содержат договор: самый свежий создан в 03:00 дня D, самый старый в 03:00 тринадцатью сутками раньше. Каждый протухнет по расписанию через четырнадцать суток после создания, а значит, последний снимок с договором исчезнет в 03:00 дня D+14. Получается, что при простом ожидании данные договора в снимках будут жить ещё до четырнадцати суток после того, как их убрали из индекса, то есть до 03:00 дня D+14.
Это не ошибка процедуры, а следствие того, чем снимок полезен. Он существует, чтобы вернуть коллекцию к прежнему состоянию. И здесь возникает выбор с ценой в обе стороны.
| Решение | Когда договор исчезнет из копий | Что теряется |
|---|---|---|
| Ждать истечения срока хранения | В 03:00 дня D+14 | Ничего: все точки восстановления сохранены |
| Создать новый снимок после удаления, затем удалить все старые | В день D, как только завершится удаление старых снимков | Глубина отката: сутки назад откатиться нельзя, точки восстановления накапливаются заново |
| Удалить только часть снимков, например самые старые | Не раньше срока оставшихся снимков | Частично глубина отката, а цель удаления всё равно не достигнута |
Третий вариант на практике хуже первых двух: он портит восстановление и не решает задачу, поэтому его описывают, чтобы не выбирать по ошибке.
Теперь о том, чем взвешивать между первым и вторым вариантом. Все четырнадцать снимков хранят не только договор, но и сотни других документов, проиндексированных за эти дни. Если сегодня ночью повредится диск или коллекция будет случайно испорчена обновлением, снимок за позавчера возвращает базу знаний целиком. Удалив все старые снимки, вы платите отсутствием такой страховки: пока новые снимки не накопятся, откат возможен только до свежего, а он создан уже после вашей чистки. Если же вы ждёте, вы четырнадцать суток держите копию, которую вам, возможно, не разрешено хранить дольше.
Решение определяется политикой, а не техникой. Если принятый порядок прямо говорит, что копии, созданные до удаления, хранятся до истечения своего срока (до четырнадцати суток) и после восстановления удаление применяется заново, то правильный вариант первый: ждать, а записать в журнал дату окончательного исчезновения. Если политика требует убрать документ из всех копий немедленно, вариант второй: сначала создать чистый снимок, и только когда он готов и проверен, удалять старые. Порядок «сначала новый, потом удаление старых» не случаен: пока нового снимка нет, удаление старых оставляет вас вообще без точки восстановления.
У второго варианта есть ещё одна прикладная сторона, которую легко пропустить, это место на диске. Чтобы сделать чистый снимок до удаления старых, на диске должно уместиться на один снимок больше, чем обычно. Размер снимка зависит от коллекции, поэтому прикиньте его по своим данным, а не по чужим цифрам. Для сервера с небольшим диском это существенно: на тарифе вроде Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) ресурсы не безграничны, и вместе с рабочей коллекцией, исходниками и моделями в этот объём нужно уложить временный пик из пятнадцати снимков вместо четырнадцати. Если запас узкий, имеет смысл заранее подобрать конфигурацию с большим диском, например Uab (2 vCPU, 4 ГБ, 80 ГБ) или выше, либо вынести снимки на отдельный том. Конкретная ёмкость определяется вашей нагрузкой, а не названием тарифа: перечень ресурсов описывает размер сервера, но не гарантирует, что ваша коллекция и её копии поместятся. Параметры VPS с NVMe и root-доступом, на которых можно настроить такую схему самому, приведены на странице аренды VPS MAATRIX. Наличие автоматических резервных копий в тарифах там не заявлено, поэтому расписание снимков и их хранение остаются вашей настройкой.
Как не воскресить документ при восстановлении
Допустим, вы выбрали ожидание. Тогда у вас четырнадцать дней существуют копии, из которых договор может вернуться в рабочую коллекцию. Сценарий очень простой: на девятый день что-то ломается, вы восстанавливаете коллекцию из снимка пятидневной давности, и договор № 18/К снова появляется в поиске, как будто его и не удаляли. Никто ничего не нарушал: так работает восстановление. Но юристу, которому обещали удаление, это объяснить будет трудно.
Защита одна: удаление должно быть записано отдельно от самого индекса и применяться заново после любого восстановления. Для этого нужен журнал удалений: список идентификаторов документов, которые нельзя оставлять в рабочей коллекции. Порядок восстановления тогда выглядит так:
- Восстановить коллекцию из выбранного снимка, но не открывать её для пользователей.
- Прочитать журнал удалений и для каждой записи заново выполнить удаление точек по
doc_idсwait=true. - Выполнить проверочные запросы для документов из журнала. Результат должен быть пустым.
- Сбросить кеш, потому что в нём могли остаться ответы, построенные на восстановленных до чистки точках.
- Только после этого подключить пользователей.
Шаг про кеш легко упустить: если кеш живёт отдельно от коллекции, он мог пережить восстановление и хранит ответы с удалёнными данными, а если он хранится на том же носителе, он мог откатиться вместе с ней. В обоих случаях после повторной чистки лучше сбросить его, чем гадать.
Есть и вторая дорога возвращения: восстановление исходников. Если файлы возвращаются из резервной копии хранилища, они снова оказываются в папке, а плановая синхронизация радостно проиндексирует их. Здесь выручает запрет по хэшу и идентификатору, который мы поставили в самом начале: загрузчик перед индексацией проверяет журнал удалений и пропускает файл. Именно поэтому запись об удалении должна содержать хэш содержимого: имя файла при восстановлении может измениться, а содержимое нет.
Получается, что журнал удалений выполняет функцию, которую не выполняет ни индекс, ни снимок: он помнит, чего в системе быть не должно. И это то место, где снимок с «воскресшим» документом перестаёт быть проблемой: хранить его опасно, пока восстановление не применяет журнал, и безопасно, когда применяет.
Запись об удалении без самого текста
Остаётся один деликатный вопрос. Чтобы доказать, что документ удалён, нужна запись об этом, но записывать в неё содержание документа бессмысленно: журнал удалений не должен становиться ещё одной копией договора. Поэтому в запись идут только факты об операции и ничего о смысле файла.
| Поле | Пример | Зачем нужно |
|---|---|---|
| Идентификатор документа | doc-0418 | По нему повторяется удаление после восстановления |
| Хэш содержимого | SHA-256 файла | Не даёт загрузить тот же файл под другим именем |
| Основание | Номер обращения на удаление | Объясняет, почему документ убран, без пересказа содержания |
| Время и исполнитель | Дата, роль или учётная запись | Позволяет восстановить цепочку действий |
| Чистка по хранилищам | Индекс: 14 из 14 точек; кеш: сброшен; история: 3 записи очищены; исходник: удалён | Показывает, что сделано в каждом месте |
| Снимки | Последний снимок с документом истекает в 03:00 дня D+14 | Фиксирует разрешённую копию и срок её исчезновения |
В такой записи нет ни фрагмента договора, ни его названия в читаемом виде (если название тоже считается чувствительным, достаточно идентификатора и хэша). Через два месяца любой человек, открывший запись, поймёт, что произошло и что осталось. А если кто-то спросит, почему документ мог встретиться в восстановленном снимке, ответ найдётся в той же строке: срок копии и правило повторного удаления.
Последняя строка таблицы требует ещё одного действия в день D+14: когда последний снимок с договором истёк, в записи ставится отметка о завершении. Её стоит поручить расписанию или напоминанию, а не памяти администратора, потому что без отметки запись навсегда остаётся «в процессе».
Пять хранилищ одного документа: где данные ещё живут и когда исчезнут
Теперь можно ответить на вопрос, с которого мы начали, и пройти весь путь договора № 18/К по хранилищам. Условные исходные данные те же: удаление в 11:00 дня D, ежедневные снимки в 03:00, хранение четырнадцать суток, выбран вариант с ожиданием истечения срока.
| Хранилище | Что там лежало | Чем удаляется | Когда перестаёт существовать |
|---|---|---|---|
| Исходник | Файл, его версии, корзина, временные копии | Удаление файла и очистка корзины и версий | В день D, после проверки индекса |
| Индекс | 14 точек с doc_id = doc-0418 | Удаление points по идентификаторам или фильтру с wait=true | В день D, когда проверочный поиск даёт пустой результат |
| Журнал и история | Ответы с цитатами, журналы промптов | Очистка или удаление записей по принятой политике | В день D для очищенных записей; для оставленных по политике срок записан в журнале удалений |
| Кеш | Ответы, построенные на договоре | Сброс записей с меткой документа или всего кеша после удаления точек | В день D |
| Снимки коллекции | Состояние коллекции со всеми 14 точками | Отдельная операция удаления снимка по его имени, либо истечение срока хранения | В 03:00 дня D+14 при ожидании; в день D, если создан чистый снимок и старые удалены |
Итоговый ответ складывается из двух частей. Из рабочих систем договор уходит в день удаления, но только если соблюдён порядок: сначала заблокирована повторная загрузка, затем удалены точки с ожиданием применения, затем очищены кеш и история. Из резервных копий договор исчезает не командой к индексу, а собственной операцией над снимками или естественным концом срока хранения. До этого момента он остаётся в копиях, которые политика явно разрешает держать, и ровно потому, что эти копии нужны для восстановления остальных документов. Запись в журнале удалений и повторное применение этого журнала после любого восстановления гарантируют, что разрешённая копия не превратится в возвращённый документ.
Когда в вашей записи стоит дата последнего истёкшего снимка и отметка о завершении, на вопрос «где остались копии и до какого дня» вы отвечаете одной строкой. Если отвечать нечего, значит, процедуру стоит доработать до следующего удаления.