Представьте двух клиентов вашего RAG-сервиса. Оба загружают документы, оба используют одну и ту же модель эмбеддингов, у обоих одинаковая размерность векторов и одна метрика. На бумаге это идеальный случай для общей коллекции: зачем плодить объекты, если конфигурация совпадает? А потом один из клиентов пишет: «Мы случайно загрузили не тот архив. Восстановите, пожалуйста, только наши данные на вчерашнее состояние». И вся схема хранения внезапно оказывается не вопросом удобства, а вопросом того, можно ли выполнить эту просьбу, не трогая соседа.
Эта статья про то, как рассуждать о таком выборе. Не «что правильнее вообще», а какая потребность клиента самая жёсткая и какая схема её выдержит. Начнём с того, чем на самом деле является коллекция, потом посмотрим на схему с фильтром, затем на несовместимые векторы, и вернёмся к снимкам и восстановлению.
Коллекция и клиент: два разных понятия
Коллекция в Qdrant — это прежде всего единица конфигурации. По документации Qdrant: Collections, векторы в одной конфигурации коллекции имеют общую размерность и метрику. То есть коллекция отвечает на вопросы: какого размера вектор, как считать близость, какие представления точки хранить. Про людей и организации она ничего не знает.
Клиент (арендатор, tenant) — понятие из вашего продукта. Это граница доступа: кто имеет право видеть какие документы, кого вы можете удалить целиком, чьи данные нужно вернуть на вчерашнее состояние. Эти две границы не обязаны совпадать. Иногда один клиент использует несколько коллекций (например, под разные модели), иногда сотня клиентов живёт в одной.
Отсюда два полюса:
- Общая коллекция. Конфигурация одна, клиенты различаются значением поля в данных. Граница доступа проводится запросом.
- Коллекция на клиента. Граница доступа совпадает с границей коллекции. Клиент отделён структурно, а не условием в запросе.
Всё остальное в статье — следствия этого различия: что вы получаете и чем платите в каждом случае.
Один клиент — одно значение в payload
В общей схеме каждая точка несёт идентификатор арендатора в payload. Именно так показано в разделе Qdrant: Multitenancy: точки разных арендаторов лежат вместе, у каждой есть идентификатор группы, а при запросе применяется фильтр по этой группе. Схематично точка выглядит так:
{
"id": 101,
"vector": [0.12, 0.98, ...],
"payload": {
"tenant_id": "acme",
"doc_id": "contract-17",
"chunk": 3
}
}А поисковый запрос обязан содержать условие вида «tenant_id равен acme». Ключевое слово здесь — «обязан». Qdrant в этой схеме не знает, что вы имели в виду: если условие в запрос не попало, поиск пойдёт по всем точкам коллекции, и ближайшими окажутся фрагменты любого клиента.
Поэтому вся безопасность общей коллекции держится на одном месте в вашем коде: там, где формируется запрос. Из этого вытекают практические правила, которые я бы считал частью самой схемы, а не «хорошими привычками»:
- Единая точка входа. Прикладной код не должен вызывать поиск напрямую. Между ним и Qdrant стоит одна функция, которая принимает идентификатор клиента как обязательный параметр и сама собирает фильтр. Без идентификатора она не вызывается.
- Идентификатор берётся из сессии, а не из тела запроса. Если tenant_id приходит от пользователя вместе с вопросом, фильтр можно подменить. Он должен определяться тем, кто аутентифицирован.
- Запись тоже под правилом. Поле tenant_id добавляется при загрузке той же обёрткой. Фрагмент без метки либо невидим для всех, либо, что хуже, виден при запросе без фильтра.
- Фильтр применяется ко всем операциям с данными: поиск, выборка по условию, удаление. Удаление «по документу» без tenant_id в условии может задеть чужой документ с таким же doc_id.
Последний пункт неочевиден и стоит пояснения. Если два клиента загрузили файл с именем «price.pdf» и вы идентифицируете документ только по имени, удаление по имени уничтожит оба. В общей схеме полезно считать ключом пару «клиент + документ», а не один из элементов.
Что выигрываете
Общая коллекция проста в эксплуатации в смысле количества объектов: один конфиг, одно место для обновления параметров, одна точка мониторинга. Новый клиент — это новое значение поля, а не новая операция в инфраструктуре. Для сервиса, где клиентов много, а данных у каждого немного, такая простота существенна: вам не нужно создавать объект на каждую регистрацию.
Чем платите
Цена — в том, что граница доступа существует только как условие. Логическая фильтрация — это не отдельная машина и не отдельное хранилище: данные соседей лежат рядом, в одной коллекции, на одном сервере. Я не буду обещать здесь ничего про изоляцию ресурсов: один тяжёлый клиент с большим объёмом делит с остальными одну коллекцию, и как это скажется на вашей нагрузке, можно узнать только замером на ваших данных.
Когда векторы не помещаются в одну коллекцию
Теперь отступим от нашей исходной сцены, где у клиентов одинаковая модель. Что, если у второго клиента модель другая? Скажем, ему нужна многоязычная модель с другой размерностью, а первому хватает компактной.
Здесь действует то самое свойство коллекции: размерность и метрика заданы на уровне конфигурации. Вектор другой длины просто не будет принят как точка той же конфигурации. Смешивать их в одном поле нельзя, и даже если размерности случайно совпали, а модели разные, ничего хорошего не выйдет: расстояния между векторами из разных пространств бессмысленны. Метрика и модель образуют пару, и сравнивать «яблоки с апельсинами» поиск не умеет.
Есть два пути.
- Именованные векторы. Документация по коллекциям описывает именованные векторы как способ хранить разные представления одной точки. Это подходит, когда у одного фрагмента текста несколько эмбеддингов: например, от двух моделей. Каждое представление имеет своё имя и свою конфигурацию. Но нужно честно понимать, что именованные векторы решают задачу «несколько представлений одной точки», а не «разные клиенты с разными моделями». Если клиент A использует только первое представление, а клиент B только второе, точки каждого будут нести пустое или ненужное место под чужое представление. Схема работает, но становится запутанной.
- Отдельные коллекции по конфигурации. Группируете клиентов не по одному, а по набору параметров: все, кто на модели X с косинусной метрикой, лежат в одной коллекции, все на модели Y — в другой. Это компромисс: коллекций немного, а несовместимые векторы не встречаются.
Практический вывод: отдельная коллекция может понадобиться не из-за требований к изоляции, а просто потому, что у клиента другая конфигурация векторов. Это самая «техническая» причина разделять, и она не обсуждается: она диктуется устройством коллекции.
Заметьте и обратное. Если вы заранее знаете, что клиентам разрешено выбирать модель, общая коллекция на всех сразу превращается в несколько общих коллекций, по одной на конфигурацию. Фильтр по tenant_id остаётся внутри каждой.
Вернёмся к просьбе восстановить одного клиента
Теперь можно разобрать ту самую просьбу из начала. API Qdrant создаёт снимок указанной коллекции, причём операция поддерживает ожидание завершения; это описано в справке Create collection snapshot. Единица снимка — коллекция. Не группа точек, не значение поля, а коллекция целиком.
Что отсюда следует, если все клиенты лежат в общей коллекции? Снимок содержит всех. Откат к снимку вернёт всех клиентов к состоянию на тот момент. Клиент B, который ни о чём не просил, потеряет всё, что загрузил после снимка. Чтобы восстановить только клиента A, придётся действовать окольными путями: поднять снимок в отдельное место, выбрать оттуда точки с tenant_id клиента A и записать их обратно, предварительно удалив его текущие данные. Это возможная процедура, но это ваша процедура, со своими рисками, и она требует аккуратности: что делать с точками, которые клиент загрузил после снимка и хотел бы сохранить, как не задеть соседей при удалении и так далее. Я не утверждаю, что в Qdrant нет иных способов; я говорю о том, что из документированной единицы снимка следует для вашей схемы.
Если же у каждого клиента своя коллекция, картина другая: снимок делается для одной коллекции, а значит, содержит данные одного клиента. Просьбу «верните только нас» можно выполнить, не затрагивая других. Так же просто делается и обратное требование: «удалите все наши данные» — это удаление одной коллекции, без поиска по условию и без вопроса, не осталось ли где-нибудь точек без метки.
Операция снимка поддерживает ожидание завершения, и это важно практически: если вы собираете процедуру «снимок перед крупной загрузкой», скрипт может дождаться завершения и только потом начинать импорт. Тогда у клиента появляется понятная точка возврата, привязанная именно к его данным.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Таблица различий
| Вопрос | Общая коллекция + tenant_id | Коллекция на клиента |
|---|---|---|
| Обновление конфигурации | Один объект: параметры меняются один раз для всех. Но все клиенты вынуждены жить с одной размерностью и метрикой. | Каждая коллекция настраивается отдельно: гибко, но изменение для всех означает повторить его столько раз, сколько у вас клиентов. |
| Доступ | Граница — условие в запросе. Ошибка в одном месте кода открывает данные всех клиентов. | Граница — имя коллекции. Ошибка выбора коллекции также опасна, но затрагивает того, чьё имя подставили, а не всех сразу. |
| Восстановление одного клиента | Снимок общий. Нужна собственная процедура выборки и перезаписи точек клиента. | Снимок и откат одной коллекции соответствуют одному клиенту. |
| Удаление клиента | Удаление по условию tenant_id; нужно убедиться, что условие покрывает всё. | Удаление коллекции целиком. |
| Разные модели эмбеддингов | Невозможно в одном поле; нужны именованные векторы или несколько коллекций. | Естественно: у каждой коллекции свои параметры. |
В строке про доступ стоит уточнить: в схеме с коллекцией на клиента вам по-прежнему нужен код, который выбирает нужную коллекцию. Если имя берётся из пользовательского ввода, вы получаете ту же уязвимость в другой форме. Структурная граница уменьшает ущерб от одной ошибки, но не отменяет необходимость определять клиента из аутентификации.
Сколько коллекций вы готовы обслуживать
Возражение против коллекции на клиента звучит обычно так: «их станет слишком много». Я не буду называть число, при котором это становится проблемой, потому что оно зависит от размера данных, настроек, железа и профиля нагрузки, и его нужно замерять на своей инсталляции, а не брать из чужих рассказов. Но можно описать, что именно растёт вместе с числом коллекций, и честно оценить, готовы ли вы это нести.
- Операции создания и настройки. При регистрации клиента вы создаёте коллекцию с нужной конфигурацией. Это нужно автоматизировать и делать идемпотентно: повторный вызов не должен ломаться, если коллекция уже есть.
- Изменение параметров. Любое общее изменение превращается в цикл по всем коллекциям. Если одна из них не обновилась, у вас получается набор слегка разных клиентов, и в этом тяжелее разбираться, чем в одной коллекции.
- Снимки. Их тоже становится много, и нужно решить, где они хранятся, как долго и как вы узнаете, что снимок конкретного клиента на месте. Часть этой работы — ваша ответственность, а не встроенная опция.
- Мониторинг. Вместо одной коллекции вы наблюдаете за множеством. Нужны агрегированные представления, иначе придётся открывать каждую вручную.
Теперь зеркальный вопрос: что растёт в общей схеме? Растёт цена ошибки фильтра. Одна пропущенная проверка в новом эндпоинте, одна забытая обёртка в скрипте администратора, и результат поиска содержит чужие фрагменты. Причём такая ошибка тихая: ничего не падает, система отвечает уверенно и красиво, просто с чужими документами. Обнаружить это можно разве что тестом, который намеренно спрашивает от имени одного клиента то, что известно только другому.
Такой тест полезен в любой схеме, но в общей он обязателен. Заведите двух служебных клиентов с заведомо различающимися документами и держите в автотестах проверку: запрос от первого по теме второго не возвращает ничего из его данных. Если тест в любой момент перестаёт проходить, вы узнаете об этом раньше клиента.
Где всё это живёт: ресурсы сервера
Схема хранения влияет на то, что нужно от сервера. Общая коллекция проще по числу объектов, но все данные соседствуют на одном диске и в одной памяти. Много коллекций не обязательно требуют больше места под данные: данные те же, но у каждой есть собственная служебная часть, и ёмкость сервера под ваш набор нужно проверять замером, а не предположением.
Если вы разворачиваете Qdrant на собственном VPS, то у VPS MAATRIX NVMe-диски, процессоры AMD EPYC и root-доступ, а тарифы идут от Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) до Djed (16 vCPU, 32 ГБ, 320 ГБ). Ресурсы тарифа — это не обещание ёмкости под определённое число клиентов: сколько векторов и коллекций выдержит конкретная конфигурация, зависит от ваших данных. Для первого эксперимента разумно взять небольшой тариф, загрузить репрезентативный объём одного клиента и посмотреть, как растёт потребление, а уже потом экстраполировать. Root-доступ пригодится и для хранения снимков отдельно от основного диска, и для скриптов, которые делают снимки по расписанию; но настраивать расписание придётся самим, автоматические резервные копии я бы не считал подразумеваемыми. Установку самого сервиса удобно начать со статьи Как установить и настроить Qdrant на VPS.
Один нюанс, который часто упускают. Два разных VPS не дают отказоустойчивость приложению автоматически, а логическое разделение по клиентам не равно физическому. Если вам нужна настоящая изоляция ресурсов, то это отдельный сервер или отдельный экземпляр Qdrant на клиента, и это уже третья схема, дороже обеих рассмотренных. Для большинства задач она избыточна, но если клиент требует именно её, коллекции и фильтры её не заменят.
Как выбирать: по самому жёсткому требованию
Теперь можно сформулировать выбор не в терминах «что красивее», а в терминах требований. Сначала выпишите, что каждый из ваших клиентов (или самый требовательный из них) может от вас потребовать:
- Независимое восстановление на прошлую точку.
- Полное удаление всех данных с доказуемым результатом.
- Собственная модель эмбеддингов или метрика.
- Структурная граница, чтобы ошибка в запросе не открывала чужие данные.
Если ни одно из этих требований не стоит, а клиентов много и каждый небольшой, общая коллекция с обязательным фильтром, единой точкой входа и тестами на утечку — рациональный выбор. Вы экономите на количестве объектов и на операциях, а риск закрываете кодом и проверками.
Если хотя бы одно требование реально и от одного клиента, не стоит подгонять всех под него или его под всех. Вариант с гибридом часто лучше обоих полюсов: основная масса клиентов живёт в общей коллекции, а тот, кому нужно независимое восстановление или своя модель, получает отдельную коллекцию. Ваша обёртка доступа при этом знает, куда направить запрос клиента: в общую коллекцию с фильтром или в личную без него (или с ним, на всякий случай). Это добавляет ветвление в код, но зато каждое решение обосновано конкретной потребностью, а не общей эстетикой.
Вернёмся к двум клиентам из начала. Модель у них одна, размерность одна, метрика одна, и по конфигурации они вполне уживаются в одной коллекции. Но один из них попросил восстановить только его данные, и это требование сильнее удобства. Пока оно звучит как разовая просьба, вы справитесь выборкой и перезаписью точек. Если оно будет повторяться или попадёт в договор, то отдельная коллекция для этого клиента окупится при первом же восстановлении, и количество строк в панели управления тут не аргумент. Выбирайте схему по самой жёсткой потребности клиента, а не по минимальному числу объектов.