Настройка многопользовательского режима#

Рекомендуемое количество коллекций: В большинстве случаев достаточно использовать одну коллекцию с разделением по полезной нагрузке. Такой подход называется многопользовательским режимом. Он эффективен для большинства пользователей, но требует дополнительной настройки.

Причина создания нескольких коллекций: При ограниченном числе пользователей и в случае необходимости их изоляции. Этот подход гибкий, но может оказаться более затратным, так как создание множества коллекций может привести к избыточности ресурсов. Кроме того, необходимо убедиться, что они никаким образом не влияют друг на друга, включая производительность.

Предусловия#

Пререквизиты:

  • доступ к серверу: 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) будет независимо индексировать векторы для каждого пользователя, значительно ускоряя процесс.

Чтобы реализовать этот подход, следует выполнить следующие действия:

  1. В настройках HNSW установите значение параметра payload_m отличным от нуля, например равным 16.

  2. В настройках 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)),
    },
    })
    
  3. Создайте индекс полезной нагрузки ключевых слов для поля 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) будут выполняться медленнее, так как им придется сканировать все группы для определения ближайших соседей.

Результат#

Параметр

Результат

Изоляция данных

Векторы разных пользователей разделены по полю group_id, обеспечивая конфиденциальность и изолированность

Производительность

Оптимизация HNSW (payload_m=16, m=0) ускоряет индексацию и поиск для отдельных групп, минимизируя задержки

Эффективность ресурсов

Использование одной коллекции вместо множества снижает избыточность и оптимизирует использование памяти/диска

Гибкость фильтрации

Возможность динамического ограничения запросов по group_id без изменения структуры данных

Ограничения

Глобальные запросы (без фильтра) выполняются медленнее, так как сканируются все группы

  • Многопользовательский режим: разделение данных по group_id позволяет использовать одну коллекцию для всех пользователей. Индексирование только по ключевым полям (group_id) с is_tenant=true улучшает производительность за счет локализации векторов.

  • Настройка HNSW: отключение глобального индекса (m=0) и включение локального (payload_m=16) ускоряет обработку запросов для конкретных групп. Особенно эффективно при высокой плотности векторов в одной группе.

  • Примеры использования:

    • До настройки: запросы к коллекции с миллионами векторов могут замедляться из-за глобального индекса.

    • После настройки: поиск по group_id работает быстрее, так как индекс строится только для нужной группы.

  • Требования к индексированию:

    • обязательное создание индекса для group_id с типом keyword;

    • параметр is_tenant=true помогает оптимизировать хранение и поиск.