Configure Multitenancy

Many applications serve multiple users, customers, or organizations (“tenants”) from a shared deployment. Each tenant’s data must stay isolated: when a tenant searches, they should only see their own data. Qdrant offers several solutions for keeping tenants’ data separated within a shared deployment.

Creating a separate collection for each tenant is rarely the most efficient approach. Each collection carries its own resource overhead, so creating many collections can quickly become expensive. Only create multiple collections when you have a limited number of tenants that need strict isolation.

Instead, keep all tenants in a single collection and use one of these three approaches to isolate them:

  • Partition by payload filters points by a payload field that identifies the tenant. This is efficient for a large number of small, similarly-sized tenants.
  • User-defined sharding gives each tenant its own dedicated shard. This trades some resource overhead for stronger isolation, and is best suited to a smaller number of larger tenants.
  • Tiered multitenancy combines the two: small tenants share a single shard while large tenants get promoted to their own dedicated shard.

Partition by Payload

To partition data in a shared collection, add a payload field that identifies the tenant to each point. You can then filter by this field to ensure that each tenant only sees their own data.

This example uses group_id as the tenant field. Start by creating a keyword payload index for the tenant field:

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=models.KeywordIndexType.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;

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.KeywordIndexParams;
import io.qdrant.client.grpc.Collections.PayloadIndexParams;
import io.qdrant.client.grpc.Collections.PayloadSchemaType;

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;
using Qdrant.Client.Grpc;

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),
		}),
})

The is_tenant=true parameter is optional, but specifying it gives Qdrant additional information about the collection’s usage patterns. When set, Qdrant organizes the storage structure to co-locate vectors of the same tenant together, which can significantly improve performance by utilizing sequential reads during queries.

Tenants defragmentation with is_tenant

Grouping tenants together by tenant ID, if is_tenant=true is used, enables more efficient disk reads (curly brackets). Rather than many random seeks across the file, Qdrant can read the data for a specific tenant with a sequential read.

Next, insert points with the tenant ID in the payload:

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 static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ValueFactory.value;
import static io.qdrant.client.VectorsFactory.vectors;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.PointStruct;
import java.util.List;
import java.util.Map;

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"}),
		},
	},
})

Query with a filter on group_id to return only one tenant’s data:

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 static io.qdrant.client.ConditionFactory.matchKeyword;
import static io.qdrant.client.QueryFactory.nearest;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.QueryPoints;
import java.util.List;

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"),
		},
	},
})

Calibrate Performance

Indexing speed can become a bottleneck when many tenants share a collection, since their vectors are all indexed together. To avoid this bottleneck, consider disabling the global vector index for the collection and building it only for individual groups instead.

By adopting this strategy, Qdrant indexes vectors for each tenant independently, significantly accelerating the process.

To implement this approach:

  1. Set payload_m in the HNSW configuration to a non-zero value, such as 16.
  2. Set m in the HNSW configuration to 0. This disables the global index for the collection.
    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)),
    	},
    })
    

Limitations

  • Global requests (without the group_id filter) are slower, since they require scanning all groups to identify the nearest neighbors.
  • When using sparse vector search with the IDF modifier, payload-based partitioning alone doesn’t isolate IDF statistics. By default, all tenants share the same shard-wide term frequencies. Use the idf search parameter to scope statistics to a single tenant.

Per-Tenant IDF Statistics

Available as of v1.19.0

BM25 and miniCOIL sparse vector searches use the inverse document frequency (IDF) to score matching documents, giving rarer terms more weight than common ones. Calculating the IDF requires two statistics: the total number of documents and the number of documents containing each term.

By default, these statistics are computed across the entire shard being queried. When using payload-filter-based multitenancy, this blends every tenant’s vocabulary into one set of statistics, so a term’s IDF no longer reflects its rarity within a specific tenant’s data.

The idf search parameter lets you correct this by narrowing the population — the IDF corpus — that Qdrant computes statistics over. It accepts a payload filter that scopes the data.

This filter is independent of the retrieval filter. The filter that determines the IDF corpus is typically broader than the retrieval filter. For example, here, the IDF is calculated over all of a tenant’s data even when the retrieval filter narrows further by year:

POST /collections/books/points/query
{
    "query": {
        "text": "time travel",
        "model": "qdrant/bm25"
    },
    "using": "title-bm25",
    "filter": {
        "must": [
            { "key": "tenant", "match": { "value": "acme" } },
            { "key": "year", "match": { "value": 2024 } }
        ]
    },
    "params": {
        "idf": {
            "corpus": {
                "must": [
                    { "key": "tenant", "match": { "value": "acme" } }
                ]
            }
        }
    },
    "limit": 10,
    "with_payload": true
}
from qdrant_client import QdrantClient, models

client = QdrantClient(
    url="https://xyz-example.qdrant.io:6333",
    api_key="<your-api-key>",
    cloud_inference=True,
)

client.query_points(
    collection_name="books",
    query=models.Document(text="time travel", model="qdrant/bm25"),
    using="title-bm25",
    query_filter=models.Filter(
        must=[
            models.FieldCondition(key="tenant", match=models.MatchValue(value="acme")),
            models.FieldCondition(key="year", match=models.MatchValue(value=2024)),
        ]
    ),
    search_params=models.SearchParams(
        idf=models.IdfCorpusParams(
            corpus=models.Filter(
                must=[
                    models.FieldCondition(
                        key="tenant", match=models.MatchValue(value="acme")
                    ),
                ]
            )
        )
    ),
    limit=10,
    with_payload=True,
)
client.query("books", {
  query: {
    text: "time travel",
    model: "qdrant/bm25",
  },
  using: "title-bm25",
  filter: {
    must: [
      { key: "tenant", match: { value: "acme" } },
      { key: "year", match: { value: 2024 } },
    ],
  },
  params: {
    idf: {
      corpus: {
        must: [{ key: "tenant", match: { value: "acme" } }],
      },
    },
  },
  limit: 10,
  with_payload: true,
});
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{
    Condition, Document, Filter, IdfParamsBuilder, Query, QueryPointsBuilder, SearchParamsBuilder,
};

client
    .query(
        QueryPointsBuilder::new("books")
            .query(Query::new_nearest(Document::new("time travel", "qdrant/bm25")))
            .using("title-bm25")
            .filter(Filter::must([
                Condition::matches("tenant", "acme".to_string()),
                Condition::matches("year", 2024),
            ]))
            .params(SearchParamsBuilder::default().idf(
                IdfParamsBuilder::default().corpus(Filter::must([Condition::matches(
                    "tenant",
                    "acme".to_string(),
                )])),
            ))
            .limit(10)
            .with_payload(true)
            .build(),
    )
    .await?;
import static io.qdrant.client.ConditionFactory.match;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import static io.qdrant.client.QueryFactory.nearest;
import static io.qdrant.client.WithPayloadSelectorFactory.enable;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Common.Filter;
import io.qdrant.client.grpc.Points.*;

QdrantClient client =

client
    .queryAsync(
        QueryPoints.newBuilder()
            .setCollectionName("books")
            .setQuery(
                nearest(
                    Document.newBuilder()
                        .setText("time travel")
                        .setModel("qdrant/bm25")
                        .build()))
            .setUsing("title-bm25")
            .setFilter(
                Filter.newBuilder()
                    .addMust(matchKeyword("tenant", "acme"))
                    .addMust(match("year", 2024))
                    .build())
            .setParams(
                SearchParams.newBuilder()
                    .setIdf(
                        IdfParams.newBuilder()
                            .setCorpus(
                                Filter.newBuilder()
                                    .addMust(matchKeyword("tenant", "acme"))
                                    .build())
                            .build())
                    .build())
            .setLimit(10)
            .setWithPayload(enable(true))
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
using static Qdrant.Client.Grpc.Conditions;

await client.QueryAsync(
    collectionName: "books",
    query: new Document { Text = "time travel", Model = "qdrant/bm25" },
    usingVector: "title-bm25",
    filter: new Filter
    {
        Must =
        {
            MatchKeyword("tenant", "acme"),
            Match("year", 2024),
        },
    },
    searchParams: new SearchParams
    {
        Idf = new IdfParams
        {
            Corpus = new Filter
            {
                Must = { MatchKeyword("tenant", "acme") },
            },
        },
    },
    payloadSelector: true,
    limit: 10
);
client.Query(context.Background(), &qdrant.QueryPoints{
	CollectionName: "books",
	Query: qdrant.NewQueryNearest(
		qdrant.NewVectorInputDocument(&qdrant.Document{
			Model: "qdrant/bm25",
			Text:  "time travel",
		}),
	),
	Using: qdrant.PtrOf("title-bm25"),
	Filter: &qdrant.Filter{
		Must: []*qdrant.Condition{
			qdrant.NewMatch("tenant", "acme"),
			qdrant.NewMatchInt("year", 2024),
		},
	},
	Params: &qdrant.SearchParams{
		Idf: &qdrant.IdfParams{
			Corpus: &qdrant.Filter{
				Must: []*qdrant.Condition{
					qdrant.NewMatch("tenant", "acme"),
				},
			},
		},
	},
	Limit:       qdrant.PtrOf(uint64(10)),
	WithPayload: qdrant.NewWithPayload(true),
})
  • idf defaults to global (shard-wide statistics), the same as omitting it.
  • Create a payload index and/or tenant index for any fields you want to use in the idf filter. On Qdrant Cloud, strict mode is enabled by default and filters on unindexed fields are rejected.
  • Only applicable to queries on a sparse vector with the IDF modifier enabled; using idf on a vector without it returns an error.
  • If the corpus filter matches no points, IDF statistics do not fall back to shard-wide statistics. Instead, every term gets the same constant weight, so ranking degenerates to plain TF with no rarity signal.
  • When using user-defined sharding, routing a search request to a single tenant’s dedicated shard already scopes IDF to that tenant’s data. This shard locality also applies to the idf filter: if it matches points that live in a different shard than the one being queried, Qdrant does not reach across shards to satisfy it. It silently computes statistics from whatever overlap exists locally, which can be empty or partial.

User-Defined Sharding

Available as of v1.7.0

Instead of filtering tenants by a payload field, another way to separate tenants is to give each tenant its own dedicated shard. Qdrant lets you specify the shard for each point individually, so operations for a tenant only ever touch that tenant’s shard. This trades some resource overhead (each shard has its own storage and index structures) for stronger isolation, and works best for a modest number of large tenants.

To use this approach, create a collection with custom sharding enabled:

PUT /collections/{collection_name}
{
    "shard_number": 1,
    "sharding_method": "custom"
    // ... other collection parameters
}
from qdrant_client import QdrantClient, models

client.create_collection(
    collection_name="{collection_name}",
    shard_number=1,
    sharding_method=models.ShardingMethod.CUSTOM,
    # ... other collection parameters
)
import { QdrantClient } from "@qdrant/js-client-rest";

client.createCollection("{collection_name}", {
    shard_number: 1,
    sharding_method: "custom",
    // ... other collection parameters
});
use qdrant_client::qdrant::{
    CreateCollectionBuilder, Distance, ShardingMethod, VectorParamsBuilder,
};
use qdrant_client::Qdrant;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .vectors_config(VectorParamsBuilder::new(300, Distance::Cosine))
            .shard_number(1)
            .sharding_method(ShardingMethod::Custom.into()),
    )
    .await?;
import static io.qdrant.client.ShardKeyFactory.shardKey;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.ShardingMethod;

client
    .createCollectionAsync(
        CreateCollection.newBuilder()
            .setCollectionName("{collection_name}")
            // ... other collection parameters
            .setShardNumber(1)
            .setShardingMethod(ShardingMethod.Custom)
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

await client.CreateCollectionAsync(
	collectionName: "{collection_name}",
	// ... other collection parameters
	shardNumber: 1,
	shardingMethod: ShardingMethod.Custom
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client.CreateCollection(context.Background(), &qdrant.CreateCollection{
	CollectionName: "{collection_name}",
	// ... other collection parameters
	ShardNumber:    qdrant.PtrOf(uint32(1)),
	ShardingMethod: qdrant.ShardingMethod_Custom.Enum(),
})

Then create a shard for each tenant, using the tenant ID as the shard key (API reference):

PUT /collections/{collection_name}/shards
{
  "shard_key": "{shard_key}"
}
from qdrant_client import QdrantClient, models

client.create_shard_key("{collection_name}", "{shard_key}")
import { QdrantClient } from "@qdrant/js-client-rest";

client.createShardKey("{collection_name}", {
    shard_key: "{shard_key}"
});
use qdrant_client::qdrant::{
    CreateShardKeyBuilder, CreateShardKeyRequestBuilder
};
use qdrant_client::Qdrant;

client
    .create_shard_key(
        CreateShardKeyRequestBuilder::new("{collection_name}")
            .request(CreateShardKeyBuilder::default().shard_key("{shard_key}".to_string())),
    )
    .await?;
import static io.qdrant.client.ShardKeyFactory.shardKey;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateShardKey;
import io.qdrant.client.grpc.Collections.CreateShardKeyRequest;

client.createShardKeyAsync(CreateShardKeyRequest.newBuilder()
                .setCollectionName("{collection_name}")
                .setRequest(CreateShardKey.newBuilder()
                                .setShardKey(shardKey("{shard_key}"))
                                .build())
                .build()).get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

await client.CreateShardKeyAsync(
    "{collection_name}",
    new CreateShardKey { ShardKey = new ShardKey { Keyword = "{shard_key}", } }
    );
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client.CreateShardKey(context.Background(), "{collection_name}", &qdrant.CreateShardKey{
	ShardKey: qdrant.NewShardKey("{shard_key}"),
})

To route a point to its tenant’s shard, provide the shard_key field in the upsert request:

PUT /collections/{collection_name}/points
{
    "points": [
        {
            "id": 1111,
            "vector": [0.1, 0.2, 0.3]
        },
    ],
    "shard_key": "user_1"
}
from qdrant_client import QdrantClient, models

client.upsert(
    collection_name="{collection_name}",
    points=[
        models.PointStruct(
            id=1111,
            vector=[0.1, 0.2, 0.3],
        ),
    ],
    shard_key_selector="user_1",
)
import { QdrantClient } from "@qdrant/js-client-rest";

client.upsert("{collection_name}", {
    points: [
        {
            id: 1111,
            vector: [0.1, 0.2, 0.3],
        },
    ],
    shard_key: "user_1",
});
use qdrant_client::qdrant::{PointStruct, UpsertPointsBuilder};
use qdrant_client::Payload;

client
    .upsert_points(
        UpsertPointsBuilder::new(
            "{collection_name}",
            vec![PointStruct::new(
                111,
                vec![0.1, 0.2, 0.3],
                Payload::default(),
            )],
        )
        .shard_key_selector("user_1".to_string()),
    )
    .await?;
import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ShardKeySelectorFactory.shardKeySelector;
import static io.qdrant.client.VectorsFactory.vectors;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.PointStruct;
import io.qdrant.client.grpc.Points.UpsertPoints;
import java.util.List;

client
    .upsertAsync(
        UpsertPoints.newBuilder()
            .setCollectionName("{collection_name}")
            .addAllPoints(
                List.of(
                    PointStruct.newBuilder()
                        .setId(id(111))
                        .setVectors(vectors(0.1f, 0.2f, 0.3f))
                        .build()))
            .setShardKeySelector(shardKeySelector("user_1"))
            .build()
    )
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

await client.UpsertAsync(
	collectionName: "{collection_name}",
	points: new List<PointStruct>
	{
		new() { Id = 111, Vectors = new[] { 0.1f, 0.2f, 0.3f } }
	},
	shardKeySelector: new ShardKeySelector { ShardKeys = { new List<ShardKey> { "user_1" } } }
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client.Upsert(context.Background(), &qdrant.UpsertPoints{
	CollectionName: "{collection_name}",
	Points: []*qdrant.PointStruct{
		{
			Id:      qdrant.NewIDNum(111),
			Vectors: qdrant.NewVectors(0.1, 0.2, 0.3),
		},
	},
	ShardKeySelector: &qdrant.ShardKeySelector{
		ShardKeys: []*qdrant.ShardKey{
			qdrant.NewShardKey("user_1"),
		},
	},
})

Specify the same shard_key in query requests to search only within that tenant’s shard.

Shards require significant resources, so keep the number of tenants low enough that each can have its own shard. For large numbers of tenants, use partition by payload or tiered multitenancy instead.

Tiered Multitenancy

Available as of v1.16.0

In some real-world applications, tenants aren’t always equally distributed. For example, a SaaS application might have a few large customers and many small ones. Large tenants might require more resources and isolation, while small tenants shouldn’t add too much overhead.

One solution is to add application-level logic to separate tenants into different collections based on their size or resource requirements. There is, however, a downside to this approach: you might not know in advance which tenants will be large and which will stay small. Additionally, application-level logic increases system complexity and requires an additional source of truth for managing tenant placement.

To address this problem, Qdrant provides a built-in mechanism called tiered multitenancy. With tiered multitenancy, you can implement two levels of tenant isolation within a single collection:

  • Keep small tenants together in a single shared shard.
  • Isolate large tenants into their own dedicated shards.

There are three components in Qdrant that allow you to implement tiered multitenancy:

  • User-defined Sharding allows you to create named shards within a collection. It allows you to isolate large tenants into their own shards.
  • Fallback shards - a special routing mechanism that allows you to route requests to either a dedicated shard (if it exists) or to a shared fallback shard. It allows you to keep requests unified, without the need to know whether a tenant is dedicated or shared.
  • Tenant promotion - a mechanism that allows you to move tenants from the shared fallback shard to their own dedicated shard when they grow large enough. This process is based on Qdrant’s internal shard transfer mechanism, which makes promotion completely transparent for the application. The promotion process supports both read and write requests.
Tiered multitenancy with tenant promotion

Tiered multitenancy with tenant promotion

Configure Tiered Multitenancy

To take advantage of tiered multitenancy, you need to create a collection with user-defined (also known as custom) sharding and create a fallback shard in it.

PUT /collections/{collection_name}
{
    "shard_number": 1,
    "sharding_method": "custom"
    // ... other collection parameters
}
from qdrant_client import QdrantClient, models

client.create_collection(
    collection_name="{collection_name}",
    shard_number=1,
    sharding_method=models.ShardingMethod.CUSTOM,
    # ... other collection parameters
)
import { QdrantClient } from "@qdrant/js-client-rest";

client.createCollection("{collection_name}", {
    shard_number: 1,
    sharding_method: "custom",
    // ... other collection parameters
});
use qdrant_client::qdrant::{
    CreateCollectionBuilder, Distance, ShardingMethod, VectorParamsBuilder,
};
use qdrant_client::Qdrant;

client
    .create_collection(
        CreateCollectionBuilder::new("{collection_name}")
            .vectors_config(VectorParamsBuilder::new(300, Distance::Cosine))
            .shard_number(1)
            .sharding_method(ShardingMethod::Custom.into()),
    )
    .await?;
import static io.qdrant.client.ShardKeyFactory.shardKey;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateCollection;
import io.qdrant.client.grpc.Collections.ShardingMethod;

client
    .createCollectionAsync(
        CreateCollection.newBuilder()
            .setCollectionName("{collection_name}")
            // ... other collection parameters
            .setShardNumber(1)
            .setShardingMethod(ShardingMethod.Custom)
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

await client.CreateCollectionAsync(
	collectionName: "{collection_name}",
	// ... other collection parameters
	shardNumber: 1,
	shardingMethod: ShardingMethod.Custom
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client.CreateCollection(context.Background(), &qdrant.CreateCollection{
	CollectionName: "{collection_name}",
	// ... other collection parameters
	ShardNumber:    qdrant.PtrOf(uint32(1)),
	ShardingMethod: qdrant.ShardingMethod_Custom.Enum(),
})

Start by creating a fallback shard, which will be used to store small tenants. Let’s name it default.

PUT /collections/{collection_name}/shards
{
  "shard_key": "default"
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.create_shard_key("{collection_name}", "default")
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.createShardKey("{collection_name}", {
    shard_key: "default"
});
use qdrant_client::qdrant::{
    CreateShardKeyBuilder, CreateShardKeyRequestBuilder
};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

client
    .create_shard_key(
        CreateShardKeyRequestBuilder::new("{collection_name}")
            .request(CreateShardKeyBuilder::default().shard_key("default".to_string())),
    )
    .await?;
import static io.qdrant.client.ShardKeyFactory.shardKey;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateShardKey;
import io.qdrant.client.grpc.Collections.CreateShardKeyRequest;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client.createShardKeyAsync(CreateShardKeyRequest.newBuilder()
                .setCollectionName("{collection_name}")
                .setRequest(CreateShardKey.newBuilder()
                                .setShardKey(shardKey("default"))
                                .build())
                .build()).get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateShardKeyAsync(
    "{collection_name}",
    new CreateShardKey { ShardKey = new ShardKey { Keyword = "default", } }
    );
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
	Host: "localhost",
	Port: 6334,
})

client.CreateShardKey(context.Background(), "{collection_name}", &qdrant.CreateShardKey{
	ShardKey: qdrant.NewShardKey("default"),
})

Since the collection will allow both dedicated and shared tenants, you still need to configure payload-based tenancy the same way as described in the Partition by Payload section. Specifically, create a payload index for the group_id field with is_tenant=true.

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=models.KeywordIndexType.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;

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.KeywordIndexParams;
import io.qdrant.client.grpc.Collections.PayloadIndexParams;
import io.qdrant.client.grpc.Collections.PayloadSchemaType;

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;
using Qdrant.Client.Grpc;

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),
		}),
})

Query Tiered Multitenant Collection

Now you can start uploading data into the collection. Unlike basic payload-based multitenancy, you need to specify the Shard Key Selector in each request to reach the correct shard.

The Shard Key Selector will specify two keys:

  • target shard - name of the tenant’s dedicated shard (which may or may not exist).
  • fallback shard - name of the shared fallback shard (in this example, default).
PUT /collections/{collection_name}/points
{
    "points": [
        {
            "id": 1,
            "payload": {"group_id": "user_1"},
            "vector": [0.9, 0.1, 0.1]
        }
    ],
    "shard_key": {
        "fallback": "default",
        "target": "user_1"
    }
}
client.upsert(
    collection_name="{collection_name}",
    points=[
        models.PointStruct(
            id=1,
            payload={"group_id": "user_1"},
            vector=[0.9, 0.1, 0.1],
        ),
    ],
    shard_key_selector=models.ShardKeyWithFallback(
        target="user_1",
        fallback="default"
    )
)
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],
    }
  ],
  shard_key: {
    target: "user_1",
    fallback: "default"
  }
});
use qdrant_client::Qdrant;
use qdrant_client::qdrant::{PointStruct, ShardKeySelectorBuilder, UpsertPointsBuilder};

let client = Qdrant::from_url("http://localhost:6334").build()?;

let shard_key_selector = ShardKeySelectorBuilder::with_shard_key("user_1")
    .fallback("default")
    .build();

client
    .upsert_points(
        UpsertPointsBuilder::new(
            "{collection_name}",
            vec![
                PointStruct::new(
                    1,
                    vec![0.9, 0.1, 0.1],
                    [("group_id", "user_1".into())]
                ),
            ],
        )
        .shard_key_selector(shard_key_selector),
    )
    .await?;
import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ShardKeyFactory.shardKey;
import static io.qdrant.client.ValueFactory.value;
import static io.qdrant.client.VectorsFactory.vectors;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Points.PointStruct;
import io.qdrant.client.grpc.Points.ShardKeySelector;
import io.qdrant.client.grpc.Points.UpsertPoints;
import java.util.List;
import java.util.Map;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .upsertAsync(
        UpsertPoints.newBuilder()
            .setCollectionName("{collection_name}")
            .addAllPoints(
                List.of(
                    PointStruct.newBuilder()
                        .setId(id(1))
                        .setVectors(vectors(0.9f, 0.1f, 0.1f))
                        .putAllPayload(Map.of("group_id", value("user_1")))
                        .build()))
            .setShardKeySelector(
                ShardKeySelector.newBuilder()
                    .addShardKeys(shardKey("user_1"))
                    .setFallback(shardKey("default"))
                    .build())
            .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" }
		}
	},
	shardKeySelector: new ShardKeySelector { 
		ShardKeys = { new List<ShardKey> { "user_1" } },
		Fallback = new ShardKey { Keyword = "default" }
	}
);
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"}),
		},
	},
	ShardKeySelector: &qdrant.ShardKeySelector{
		ShardKeys: []*qdrant.ShardKey{
			qdrant.NewShardKey("user_1"),
		},
		Fallback: qdrant.NewShardKey("default"),
	},
})

The routing logic will work as follows:

  • If the target shard exists and is active, the request will be routed to it.
  • If the target shard does not exist, the request will be routed to the fallback shard.

Similarly, when querying points, specify the Shard Key Selector and filter by group_id. The group_id filter value must match the target shard key.

Promote Tenant to Dedicated Shard

When a tenant grows large enough, you can promote it to its own dedicated shard. To do this, first create a new shard for the tenant:

PUT /collections/{collection_name}/shards
{
  "shard_key": "user_1",
  "initial_state": "Partial"
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.create_shard_key(
    "{collection_name}",
    shard_key="user_1",
    initial_state=models.ReplicaState.PARTIAL
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.createShardKey("{collection_name}", {
    shard_key: "default",
    initial_state: "Partial"
});
use qdrant_client::qdrant::{
    CreateShardKeyBuilder, CreateShardKeyRequestBuilder
};
use qdrant_client::qdrant::ReplicaState;
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

client
    .create_shard_key(
        CreateShardKeyRequestBuilder::new("{collection_name}")
            .request(
                CreateShardKeyBuilder::default()
                    .shard_key("user_1".to_string())
                    .initial_state(ReplicaState::Partial)
            ),
    )
    .await?;
import static io.qdrant.client.ShardKeyFactory.shardKey;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.CreateShardKey;
import io.qdrant.client.grpc.Collections.CreateShardKeyRequest;
import io.qdrant.client.grpc.Collections.ReplicaState;
import io.qdrant.client.grpc.Common.Filter;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client.createShardKeyAsync(CreateShardKeyRequest.newBuilder()
                .setCollectionName("{collection_name}")
                .setRequest(CreateShardKey.newBuilder()
                                .setShardKey(shardKey("default"))
                                .setInitialState(ReplicaState.Partial)
                                .build())
                .build()).get();
using Qdrant.Client;
using Qdrant.Client.Grpc;

var client = new QdrantClient("localhost", 6334);

await client.CreateShardKeyAsync(
    "{collection_name}",
    new CreateShardKey { 
        ShardKey = new ShardKey { Keyword = "default" },
        InitialState = ReplicaState.Partial
    }
);
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
	Host: "localhost",
	Port: 6334,
})

client.CreateShardKey(
	context.Background(),
	"{collection_name}",
	&qdrant.CreateShardKey{
		ShardKey: qdrant.NewShardKey("default"),
		InitialState: qdrant.PtrOf(qdrant.ReplicaState_Partial),
	},
)

The shard is created in Partial state, since it still needs to receive data.

Use the replicate_points API to initiate data transfer:

POST /collections/{collection_name}/cluster
{
    "replicate_points": {
        "filter": {
            "must": {
                "key": "group_id",
                "match": {
                    "value": "user_1"
                }
            }
        },
        "from_shard_key": "default",
        "to_shard_key": "user_1"
    }
}
from qdrant_client import QdrantClient, models

client = QdrantClient(url="http://localhost:6333")

client.cluster_collection_update(
    collection_name="{collection_name}",
    cluster_operation=models.ReplicatePointsOperation(
        replicate_points=models.ReplicatePoints(
            from_shard_key="default",
            to_shard_key="user_1",
            filter=models.Filter(
                must=[
                    models.FieldCondition(
                        key="group_id",
                        match=models.MatchValue(
                            value="user_1",
                        )
                    )
                ]
            )
        )
    )
)
import { QdrantClient } from "@qdrant/js-client-rest";

const client = new QdrantClient({ host: "localhost", port: 6333 });

client.updateCollectionCluster("{collection_name}", {
    replicate_points: {
        filter: {
            must: {
                key: "group_id",
                match: {
                    value: "user_1"
                }
            }
        },
        from_shard_key: "default",
        to_shard_key: "user_1"
    }
});
use qdrant_client::qdrant::{
    update_collection_cluster_setup_request::Operation, Condition, Filter,
    ReplicatePointsBuilder, ShardKey, UpdateCollectionClusterSetupRequest,
};
use qdrant_client::Qdrant;

let client = Qdrant::from_url("http://localhost:6334").build()?;

client
    .update_collection_cluster_setup(UpdateCollectionClusterSetupRequest {
        collection_name: "{collection_name}".to_string(),
        operation: Some(Operation::ReplicatePoints(
            ReplicatePointsBuilder::new(
                ShardKey::from("default"),
                ShardKey::from("user_1"),
            )
            .filter(Filter::must([Condition::matches(
                "group_id",
                "user_1".to_string(),
            )]))
            .build(),
        )),
        timeout: None,
    })
    .await?;
import static io.qdrant.client.ConditionFactory.matchKeyword;
import static io.qdrant.client.QueryFactory.nearest;
import static io.qdrant.client.ShardKeyFactory.shardKey;

import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections.ReplicatePoints;
import io.qdrant.client.grpc.Collections.UpdateCollectionClusterSetupRequest;
import io.qdrant.client.grpc.Common.Filter;

QdrantClient client =
    new QdrantClient(QdrantGrpcClient.newBuilder("localhost", 6334, false).build());

client
    .updateCollectionClusterSetupAsync(
        UpdateCollectionClusterSetupRequest.newBuilder()
            .setCollectionName("{collection_name}")
            .setReplicatePoints(
                ReplicatePoints.newBuilder()
                    .setFromShardKey(shardKey("default"))
                    .setToShardKey(shardKey("user_1"))
                    .setFilter(
                        Filter.newBuilder().addMust(matchKeyword("group_id", "user_1")).build())
                    .build())
            .build())
    .get();
using Qdrant.Client;
using Qdrant.Client.Grpc;
using static Qdrant.Client.Grpc.Conditions;

var client = new QdrantClient("localhost", 6334);

await client.UpdateCollectionClusterSetupAsync(new()
{
    CollectionName = "{collection_name}",
	ReplicatePoints = new()
    {
        FromShardKey = "default",
		ToShardKey = "user_1",
		Filter = MatchKeyword("group_id", "user_1")
    }
});
import (
	"context"

	"github.com/qdrant/go-client/qdrant"
)

client, err := qdrant.NewClient(&qdrant.Config{
	Host: "localhost",
	Port: 6334,
})

client.UpdateClusterCollectionSetup(context.Background(), qdrant.NewUpdateCollectionClusterReplicatePoints(
	"{collection_name}", &qdrant.ReplicatePoints{
		FromShardKey: qdrant.NewShardKey("default"),
		ToShardKey:   qdrant.NewShardKey("user_1"),
		Filter: &qdrant.Filter{
			Must: []*qdrant.Condition{
				qdrant.NewMatch("group_id", "user_1"),
			},
		},
	},
))

Once the transfer is complete, the target shard will become Active, and all requests for the tenant will be routed to it automatically. At this point it’s safe to delete the tenant’s data from the shared fallback shard to free up space.

Limitations

  • The fallback shard is limited to a single shard, though it can still be replicated across nodes for availability. This means all small tenants sharing the fallback shard must fit within the storage and write capacity of a single node. We plan to remove this restriction in a future release.
  • Similar to collections, dedicated shards introduce some resource overhead. Don’t create more than a thousand dedicated shards per cluster. The recommended threshold for promoting a tenant is the same as the indexing threshold for a single collection, which is approximately 20,000 points.
Was this page useful?

Thank you for your feedback! 🙏

We are sorry to hear that. 😔 You can edit this page on GitHub, or create a GitHub issue.