Настройка многопользовательского режима#
Рекомендуемое количество коллекций: В большинстве случаев достаточно использовать одну коллекцию с разделением по полезной нагрузке. Такой подход называется многопользовательским режимом. Он эффективен для большинства пользователей, но требует дополнительной настройки.
Причина создания нескольких коллекций: При ограниченном числе пользователей и в случае необходимости их изоляции. Этот подход гибкий, но может оказаться более затратным, так как создание множества коллекций может привести к избыточности ресурсов. Кроме того, необходимо убедиться, что они никаким образом не влияют друг на друга, включая производительность.
Предусловия#
Пререквизиты:
доступ к серверу: SSH или физический доступ;
установленный продукт: сервер должен быть запущен (кроме операций с файлами данных);
исходный код: скрипты находятся в
/src/bin/репозитория продукта;rust: установленный компилятор для сборки утилит.
Привилегии:
файловая система: права на чтение/запись в
storage_path;API-ключ: обязателен ключ с правами
adminдля большинства операций;сетевой доступ: доступность API-порта (порт по умолчанию 6333).
Ключевые требования:
API-ключ с правами
adminдля настройки RBAC.
Последовательность выполнения#
Разделение по полезной нагрузке#
Если экземпляр совместно используется несколькими пользователями, возможно, потребуется разделить векторы по пользователям. Это делается для того, чтобы каждый пользователь мог получить доступ только к своим собственным векторам и не видел векторов других пользователей.
Примечание
Ключ необязательно должен называться group_id. Можно выбрать имя, которое наилучшим образом соответствует структуре данных и соглашениям о наименовании.
PUT /collections/{collection_name}/points
{
"points": [
{
"id": 1,
"payload": {"group_id": "user_1"},
"vector": [0.9, 0.1, 0.1]
},
{
"id": 2,
"payload": {"group_id": "user_1"},
"vector": [0.1, 0.9, 0.1]
},
{
"id": 3,
"payload": {"group_id": "user_2"},
"vector": [0.1, 0.1, 0.9]
},
]
}
client.upsert(
collection_name="{collection_name}",
points=[
models.PointStruct(
id=1,
payload={"group_id": "user_1"},
vector=[0.9, 0.1, 0.1],
),
models.PointStruct(
id=2,
payload={"group_id": "user_1"},
vector=[0.1, 0.9, 0.1],
),
models.PointStruct(
id=3,
payload={"group_id": "user_2"},
vector=[0.1, 0.1, 0.9],
),
],
)
import { QdrantClient } from "@qdrant/js-client-rest";
const client = new QdrantClient({ host: "localhost", port: 6333 });
client.upsert("{collection_name}", {
points: [
{
id: 1,
payload: { group_id: "user_1" },
vector: [0.9, 0.1, 0.1],
},
{
id: 2,
payload: { group_id: "user_1" },
vector: [0.1, 0.9, 0.1],
},
{
id: 3,
payload: { group_id: "user_2" },
vector: [0.1, 0.1, 0.9],
},
],
});
use qdrant_client::qdrant::{PointStruct, UpsertPointsBuilder};
use qdrant_client::Qdrant;
let client = Qdrant::from_url("http://localhost:6334").build()?;
client
.upsert_points(UpsertPointsBuilder::new(
"{collection_name}",
vec![
PointStruct::new(1, vec![0.9, 0.1, 0.1], [("group_id", "user_1".into())]),
PointStruct::new(2, vec![0.1, 0.9, 0.1], [("group_id", "user_1".into())]),
PointStruct::new(3, vec![0.1, 0.1, 0.9], [("group_id", "user_2".into())]),
],
))
.await?;
import java.util.List;
import java.util.Map;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.PointStruct;
QdrantClient client =
new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
client
.upsertAsync(
"{collection_name}",
List.of(
PointStruct.newBuilder()
.setId(id(1))
.setVectors(vectors(0.9f, 0.1f, 0.1f))
.putAllPayload(Map.of("group_id", value("user_1")))
.build(),
PointStruct.newBuilder()
.setId(id(2))
.setVectors(vectors(0.1f, 0.9f, 0.1f))
.putAllPayload(Map.of("group_id", value("user_1")))
.build(),
PointStruct.newBuilder()
.setId(id(3))
.setVectors(vectors(0.1f, 0.1f, 0.9f))
.putAllPayload(Map.of("group_id", value("user_2")))
.build()))
.get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
var client = new QdrantClient("localhost", 6334);
await client.UpsertAsync(
collectionName: "{collection_name}",
points: new List<PointStruct>
{
new()
{
Id = 1,
Vectors = new[] { 0.9f, 0.1f, 0.1f },
Payload = { ["group_id"] = "user_1" }
},
new()
{
Id = 2,
Vectors = new[] { 0.1f, 0.9f, 0.1f },
Payload = { ["group_id"] = "user_1" }
},
new()
{
Id = 3,
Vectors = new[] { 0.1f, 0.1f, 0.9f },
Payload = { ["group_id"] = "user_2" }
}
}
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Upsert(context.Background(), &qdrant.UpsertPoints{
CollectionName: "{collection_name}",
Points: []*qdrant.PointStruct{
{
Id: qdrant.NewIDNum(1),
Vectors: qdrant.NewVectors(0.9, 0.1, 0.1),
Payload: qdrant.NewValueMap(map[string]any{"group_id": "user_1"}),
},
{
Id: qdrant.NewIDNum(2),
Vectors: qdrant.NewVectors(0.1, 0.9, 0.1),
Payload: qdrant.NewValueMap(map[string]any{"group_id": "user_1"}),
},
{
Id: qdrant.NewIDNum(3),
Vectors: qdrant.NewVectors(0.1, 0.1, 0.9),
Payload: qdrant.NewValueMap(map[string]any{"group_id": "user_2"}),
},
},
})
Используйте фильтр вместе с group_id для фильтрации векторов каждого пользователя.
POST /collections/{collection_name}/points/query
{
"query": [0.1, 0.1, 0.9],
"filter": {
"must": [
{
"key": "group_id",
"match": {
"value": "user_1"
}
}
]
},
"limit": 10
}
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://localhost:6333")
client.query_points(
collection_name="{collection_name}",
query=[0.1, 0.1, 0.9],
query_filter=models.Filter(
must=[
models.FieldCondition(
key="group_id",
match=models.MatchValue(
value="user_1",
),
)
]
),
limit=10,
)
import { QdrantClient } from "@qdrant/js-client-rest";
const client = new QdrantClient({ host: "localhost", port: 6333 });
client.query("{collection_name}", {
query: [0.1, 0.1, 0.9],
filter: {
must: [{ key: "group_id", match: { value: "user_1" } }],
},
limit: 10,
});
use qdrant_client::qdrant::{Condition, Filter, QueryPointsBuilder};
use qdrant_client::Qdrant;
let client = Qdrant::from_url("http://localhost:6334").build()?;
client
.query(
QueryPointsBuilder::new("{collection_name}")
.query(vec![0.1, 0.1, 0.9])
.limit(10)
.filter(Filter::must([Condition::matches(
"group_id",
"user_1".to_string(),
)])),
)
.await?;
import java.util.List;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.Filter;
import io.qdrant.client.grpc.Points.QueryPoints;
import static io.qdrant.client.QueryFactory.nearest;
import static io.qdrant.client.ConditionFactory.matchKeyword;
QdrantClient client =
new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());
client.queryAsync(
QueryPoints.newBuilder()
.setCollectionName("{collection_name}")
.setFilter(
Filter.newBuilder().addMust(matchKeyword("group_id", "user_1")).build())
.setQuery(nearest(0.1f, 0.1f, 0.9f))
.setLimit(10)
.build())
.get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
using static Qdrant.Client.Grpc.Conditions;
var client = new QdrantClient("localhost", 6334);
await client.QueryAsync(
collectionName: "{collection_name}",
query: new float[] { 0.1f, 0.1f, 0.9f },
filter: MatchKeyword("group_id", "user_1"),
limit: 10
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
client, err := qdrant.NewClient(&qdrant.Config{
Host: "localhost",
Port: 6334,
})
client.Query(context.Background(), &qdrant.QueryPoints{
CollectionName: "{collection_name}",
Query: qdrant.NewQuery(0.1, 0.1, 0.9),
Filter: &qdrant.Filter{
Must: []*qdrant.Condition{
qdrant.NewMatch("group_id", "user_1"),
},
},
})
Калибровка производительности#
Скорость индексирования может стать узким местом в этом случае, поскольку вектор каждого пользователя будет проиндексирован в ту же самую коллекцию. Чтобы избежать этой проблемы, рассмотрите возможность отключения построения глобального индекса векторов для всей коллекции и создания его только для отдельных групп.
При использовании такого подхода Platform V Vector DB (далее - Vector DB) будет независимо индексировать векторы для каждого пользователя, значительно ускоряя процесс.
Чтобы реализовать этот подход, следует выполнить следующие действия:
В настройках HNSW установите значение параметра
payload_mотличным от нуля, например равным 16.В настройках HNSW установите параметр
mравным 0. Это отключит построение глобального индекса для всей коллекции.PUT /collections/{collection_name} { "vectors": { "size": 768, "distance": "Cosine" }, "hnsw_config": { "payload_m": 16, "m": 0 } }from qdrant_client import QdrantClient, models client = QdrantClient(url="http://localhost:6333") client.create_collection( collection_name="{collection_name}", vectors_config=models.VectorParams(size=768, distance=models.Distance.COSINE), hnsw_config=models.HnswConfigDiff( payload_m=16, m=0, ), )import { QdrantClient } from "@qdrant/js-client-rest"; const client = new QdrantClient({ host: "localhost", port: 6333 }); client.createCollection("{collection_name}", { vectors: { size: 768, distance: "Cosine", }, hnsw_config: { payload_m: 16, m: 0, }, });use qdrant_client::qdrant::{ CreateCollectionBuilder, Distance, HnswConfigDiffBuilder, VectorParamsBuilder, }; use qdrant_client::Qdrant; let client = Qdrant::from_url("http://localhost:6334").build()?; client .create_collection( CreateCollectionBuilder::new("{collection_name}") .vectors_config(VectorParamsBuilder::new(768, Distance::Cosine)) .hnsw_config(HnswConfigDiffBuilder::default().payload_m(16).m(0)), ) .await?;import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantGrpcClient; import io.qdrant.client.grpc.Collections.CreateCollection; import io.qdrant.client.grpc.Collections.Distance; import io.qdrant.client.grpc.Collections.HnswConfigDiff; import io.qdrant.client.grpc.Collections.VectorParams; import io.qdrant.client.grpc.Collections.VectorsConfig; QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build()); client .createCollectionAsync( CreateCollection.newBuilder() .setCollectionName("{collection_name}") .setVectorsConfig( VectorsConfig.newBuilder() .setParams( VectorParams.newBuilder() .setSize(768) .setDistance(Distance.Cosine) .build()) .build()) .setHnswConfig(HnswConfigDiff.newBuilder().setPayloadM(16).setM(0).build()) .build()) .get();using Qdrant.Client; using Qdrant.Client.Grpc; var client = new QdrantClient("localhost", 6334); await client.CreateCollectionAsync( collectionName: "{collection_name}", vectorsConfig: new VectorParams { Size = 768, Distance = Distance.Cosine }, hnswConfig: new HnswConfigDiff { PayloadM = 16, M = 0 } );import ( "context" "github.com/qdrant/go-client/qdrant" ) client, err := qdrant.NewClient(&qdrant.Config{ Host: "localhost", Port: 6334, }) client.CreateCollection(context.Background(), &qdrant.CreateCollection{ CollectionName: "{collection_name}", VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{ Size: 768, Distance: qdrant.Distance_Cosine, }), HnswConfig: &qdrant.HnswConfigDiff{ PayloadM: qdrant.PtrOf(uint64(16)), M: qdrant.PtrOf(uint64(0)), }, })Создайте индекс полезной нагрузки ключевых слов для поля
group_id.PUT /collections/{collection_name}/index { "field_name": "group_id", "field_schema": { "type": "keyword", "is_tenant": true } }client.create_payload_index( collection_name="{collection_name}", field_name="group_id", field_schema=models.KeywordIndexParams( type="keyword", is_tenant=True, ), )client.createPayloadIndex("{collection_name}", { field_name: "group_id", field_schema: { type: "keyword", is_tenant: true, }, });use qdrant_client::qdrant::{ CreateFieldIndexCollectionBuilder, KeywordIndexParamsBuilder, FieldType }; use qdrant_client::{Qdrant, QdrantError}; let client = Qdrant::from_url("http://localhost:6334").build()?; client.create_field_index( CreateFieldIndexCollectionBuilder::new( "{collection_name}", "group_id", FieldType::Keyword, ).field_index_params( KeywordIndexParamsBuilder::default() .is_tenant(true) ) ).await?;import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantGrpcClient; import io.qdrant.client.grpc.Collections.PayloadIndexParams; import io.qdrant.client.grpc.Collections.PayloadSchemaType; import io.qdrant.client.grpc.Collections.KeywordIndexParams; QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build()); client .createPayloadIndexAsync( "{collection_name}", "group_id", PayloadSchemaType.Keyword, PayloadIndexParams.newBuilder() .setKeywordIndexParams( KeywordIndexParams.newBuilder() .setIsTenant(true) .build()) .build(), null, null, null) .get();using Qdrant.Client; var client = new QdrantClient("localhost", 6334); await client.CreatePayloadIndexAsync( collectionName: "{collection_name}", fieldName: "group_id", schemaType: PayloadSchemaType.Keyword, indexParams: new PayloadIndexParams { KeywordIndexParams = new KeywordIndexParams { IsTenant = true } } );import ( "context" "github.com/qdrant/go-client/qdrant" ) client, err := qdrant.NewClient(&qdrant.Config{ Host: "localhost", Port: 6334, }) client.CreateFieldIndex(context.Background(), &qdrant.CreateFieldIndexCollection{ CollectionName: "{collection_name}", FieldName: "group_id", FieldType: qdrant.FieldType_FieldTypeKeyword.Enum(), FieldIndexParams: qdrant.NewPayloadIndexParams( &qdrant.KeywordIndexParams{ IsTenant: qdrant.PtrOf(true), }), })
Параметр is_tenant=true является необязательным, однако его указание предоставляет хранилищу дополнительную информацию о шаблонах использования, которые будет применять коллекция. Хранилище организует свою структуру таким образом, чтобы разместить векторы одного и того же арендатора рядом, что в некоторых случаях может существенно повысить производительность.
Ограничения#
Одним из недостатков данного подхода является то, что глобальные запросы (без фильтра group_id) будут выполняться медленнее, так как им придется сканировать все группы для определения ближайших соседей.
Результат#
Параметр |
Результат |
|---|---|
Изоляция данных |
Векторы разных пользователей разделены по полю |
Производительность |
Оптимизация HNSW ( |
Эффективность ресурсов |
Использование одной коллекции вместо множества снижает избыточность и оптимизирует использование памяти/диска |
Гибкость фильтрации |
Возможность динамического ограничения запросов по |
Ограничения |
Глобальные запросы (без фильтра) выполняются медленнее, так как сканируются все группы |
Многопользовательский режим: разделение данных по
group_idпозволяет использовать одну коллекцию для всех пользователей. Индексирование только по ключевым полям (group_id) сis_tenant=trueулучшает производительность за счет локализации векторов.Настройка HNSW: отключение глобального индекса (
m=0) и включение локального (payload_m=16) ускоряет обработку запросов для конкретных групп. Особенно эффективно при высокой плотности векторов в одной группе.Примеры использования:
До настройки: запросы к коллекции с миллионами векторов могут замедляться из-за глобального индекса.
После настройки: поиск по
group_idработает быстрее, так как индекс строится только для нужной группы.
Требования к индексированию:
обязательное создание индекса для
group_idс типомkeyword;параметр
is_tenant=trueпомогает оптимизировать хранение и поиск.