Skip to content

new: QdrantServerless client prototype - #1393

Draft
generall wants to merge 14 commits into
devfrom
serverless-client-prototype
Draft

new: QdrantServerless client prototype#1393
generall wants to merge 14 commits into
devfrom
serverless-client-prototype

Conversation

@generall

@generall generall commented Sep 1, 2026

Copy link
Copy Markdown
Member

Stacked on #1394 (CI congruence fixes) to keep integration tests green; merge #1394 first, then retarget this PR to dev.

What

Prototype of a dedicated client for Qdrant Serverless, parallel to the regular QdrantClient, per the serverless client proposal.

from qdrant_client.serverless import QdrantServerless, DenseVectorConfig, Distance

client = QdrantServerless(url="https://serverless....qdrant.io", api_key="...")

client.create_collection(          # serverless-specific input
    "my-collection",
    dense_vectors=DenseVectorConfig(size=1536, distance=Distance.COSINE),
)
client.get_collection("my-collection")   # serverless-specific output
client.query_points("my-collection", query=[...])  # same as regular qdrant

Why

Serverless is not 100% identical to a regular cluster deployment: collection management exposes only a simplified tenant-facing config (no quantization/WAL/segments), and point operations don't support read consistency, shard selection, write ordering, or filtered updates. A separate client keeps those parameters out of the serverless interface instead of forcing one client to paper over the divergence.

How

Everything lives in a dedicated qdrant_client/serverless/ module; nothing is added to the top-level package, so regular users don't pay any import cost.

  • gRPC stubs generated from qdrant-cloud-public-api's serverless/collections.proto with the same pinned grpcio-tools 1.62.0 as the existing stubs (tools/generate_serverless_grpc_client.sh regenerates). The proto is renamed to serverless_collections.proto because the protobuf descriptor pool registers files by name and collections.proto is already taken by the regular client — without the rename, importing both clients in one process crashes. Generated types stay internal.
  • Models: hand-written pydantic models (v1.10+ compatible) mirroring the tenant-facing config — DenseVectorConfig, SparseVectorConfig, PrecisionTier, the eight payload-index types, CollectionConfig, CollectionInfo, CollectionSummary. The existing Distance / TokenizerType enums are reused so values look familiar. These may later be replaced with models generated from a serverless OpenAPI spec.
  • Client: QdrantServerless wraps an internal QdrantRemote(prefer_grpc=True, check_compatibility=False) and delegates point operations to it, reusing all existing conversion machinery; collection methods talk to the new CollectionsService stub on the same channel (one place for TLS, api-key metadata, user-agent). The API divergence is expressed purely in signatures:
    • point methods (query_points, upsert, retrieve, scroll, count, delete, set_payload, delete_payload) are the regular ones minus consistency / shard_key_selector / ordering
    • delete / payload methods take ids only, since serverless rejects filtered updates
    • create_collection accepts a bare config as the unnamed "" vector and returns the service's result string ("created" / "already exists"); get_collection returns CollectionInfo with exists / point_count instead of raising on missing collections
    • default grpc_port is 443, not 6334

Not included (mechanical follow-ups)

query_batch_points / batch_update_points / update_vectors, update_collection (no such RPC yet).

Async client

AsyncQdrantServerless is generated from the sync client the same way the regular async client is: tools/async_client_generator/serverless_generator.py runs as part of tools/generate_async_client.sh, delegates to AsyncQdrantRemote, awaits the CollectionsService stub RPCs on the aio channel, and drops the sync context manager (the regular async client has none either). The generated file is verified by tests/async-client-consistency-check.sh alongside the other generated clients.

Testing

  • tests/test_serverless.py: model↔gRPC round-trips, proto3 optional-field presence, offline client construction (headers, port, TLS)
  • mypy clean on the new module
  • not yet run against a live serverless endpoint

🤖 Generated with Claude Code

@netlify

netlify Bot commented Sep 1, 2026

Copy link
Copy Markdown

Deploy Preview for poetic-froyo-8baba7 ready!

Name Link
🔨 Latest commit 4254cc6
🔍 Latest deploy log https://app.netlify.com/projects/poetic-froyo-8baba7/deploys/6aa12f438f5e0200087234a1
😎 Deploy Preview https://deploy-preview-1393--poetic-froyo-8baba7.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@generall
generall changed the base branch from master to dev September 1, 2026 18:15
@generall
generall force-pushed the serverless-client-prototype branch from de48a43 to 0e27d09 Compare September 1, 2026 19:20
@generall
generall changed the base branch from dev to fix/local-unindexed-text-match September 1, 2026 19:20
@generall
generall force-pushed the serverless-client-prototype branch from 0e27d09 to eb092f1 Compare September 1, 2026 19:33
@generall
generall requested a review from joein September 1, 2026 21:45
Base automatically changed from fix/local-unindexed-text-match to dev September 2, 2026 08:59
generall and others added 11 commits September 2, 2026 15:59
Serverless exposes the same point-level API as a regular cluster (minus
read consistency, shard selection, write ordering and filtered updates),
but a simplified tenant-facing collection management API.

- qdrant_client/serverless: dedicated module, nothing added to the
  top-level package
- gRPC stubs generated from qdrant-cloud-public-api's
  serverless/collections.proto (renamed to serverless_collections.proto
  to avoid a descriptor-pool filename clash with the regular client's
  collections.proto), kept internal
- hand-written pydantic models for the serverless collection config,
  reusing the existing Distance/TokenizerType enums
- point operations delegate to an internal QdrantRemote(prefer_grpc=True)
  with trimmed signatures; collection operations talk to the serverless
  CollectionsService on the same channel

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R25zh9xS78xMHgPcoFaUdw
Same treatment as qdrant_client/grpc: generated stubs have untyped defs,
and the ListCollectionsResponse.collections field shadows the collections
module in the .pyi. Also import PointStruct from qdrant_client.http.models
in the example, matching the rest of the codebase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R25zh9xS78xMHgPcoFaUdw
Match the Args/Returns docstring style of the regular client; each
method notes where the serverless API diverges from it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R25zh9xS78xMHgPcoFaUdw
Same approach as the regular client: the sync client is the source of
truth and tools/async_client_generator produces the async version.
The serverless generator delegates to AsyncQdrantRemote, awaits the
CollectionsService stub RPCs (awaitable on the aio channel the async
remote already builds), and drops the sync context manager. The file is
covered by the async-client-consistency-check like the other generated
clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R25zh9xS78xMHgPcoFaUdw
query_points delegated straight to QdrantRemote, which expects a
resolved Query model, so a plain list like [0.1, 0.2] failed with
"invalid Query model". Apply the same QdrantFastembedMixin._resolve_query
type normalization the regular client applies - type resolution only, no
client-side embedding inference: Document/Image inputs go to the server
as-is, serverless inference is server-side only.

Verified live against a serverless dev space: create/list/get/upsert/
query/delete all pass, sync and async. Along the way: create_collection
docstring now documents that an existing collection raises gRPC
ALREADY_EXISTS (the service errors instead of returning the "already
exists" result string the proto comment mentions), and the example
deletes a leftover collection first so it can be rerun.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R25zh9xS78xMHgPcoFaUdw
Thin alias module re-exporting qdrant_client.serverless.models, matching
the qdrant_client.models convention of the regular client; the old
import path keeps working. Examples and tests use the new path. Also
replace an API key that slipped into the committed example with the
placeholder, and exclude examples/ from mypy so examples can use the
dynamic qdrant_client.models namespace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R25zh9xS78xMHgPcoFaUdw
Drop the qdrant_client.models.serverless alias module and the model
re-exports in qdrant_client.serverless: both were manually maintained
re-export lists. qdrant_client.serverless.models is the single public
import path; the serverless package itself exports only the clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R25zh9xS78xMHgPcoFaUdw
query_batch_points and query_points_groups (both implemented by the
service, verified live) with the usual serverless trims: no consistency,
no shard selection, no cross-collection lookups. Add the update
operations the service implements that were still missing:
update_vectors, delete_vectors, overwrite_payload, clear_payload,
batch_update_points (ids-only selectors; the service rejects filters).

Update methods default to wait=False: serverless reads are eventually
consistent with writes, so waiting does not provide read-your-write.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R25zh9xS78xMHgPcoFaUdw
All id-selecting update methods take a uniform `points` parameter,
typed narrowly as Sequence[PointId] to match what the service accepts
today. When serverless adds filtered updates, the type widens to
PointsSelector without breaking callers: same name, same position,
strictly wider input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R25zh9xS78xMHgPcoFaUdw
Warn that the serverless client is experimental and should not be used yet.
@qdrant-cloud-bot

Copy link
Copy Markdown

Documented the serverless client as in development — do not use yet in package/client/models/conversions docs, the async client (regenerated), and the example.

@joein
joein force-pushed the serverless-client-prototype branch from b25151f to 9920605 Compare September 2, 2026 09:00
Bind every model field via structural pattern matching so new fields force
an update instead of being silently ignored.
@joein

joein commented Sep 4, 2026

Copy link
Copy Markdown
Member

The example code crashes with the following error:

created
[CollectionSummary(collection_name='my-collection', point_count=None)]
exists=True config=CollectionConfig(dense_vectors={'': DenseVectorConfig(size=4, distance=<Distance.COSINE: 'Cosine'>, multivector=False, precision_tier=None)}, sparse_vectors={}, payload_indexes={'color': KeywordIndex(type='keyword')}) point_count=None
Traceback (most recent call last):
  File "../qdrant/qdrant_client/examples/serverless_client.py", line 52, in <module>
    main()
  File "../qdrant/qdrant_client/examples/serverless_client.py", line 45, in main
    print(client.query_points("my-collection", query=[0.1, 0.2, 0.3, 0.4]))
  File "../qdrant/qdrant_client/qdrant_client/serverless/client.py", line 355, in query_points
    return self._remote.query_points(
  File "../qdrant/qdrant_client/qdrant_client/qdrant_remote.py", line 535, in query_points
    res: grpc.QueryResponse = self.grpc_points.Query(
  File "../qdrant/qdrant_client/venv/lib/python3.10/site-packages/grpc/_interceptor.py", line 276, in __call__
    response, ignored_call = self._with_call(
  File "../qdrant/qdrant_client/venv/lib/python3.10/site-packages/grpc/_interceptor.py", line 331, in _with_call
    return call.result(), call
  File "../qdrant/qdrant_client/venv/lib/python3.10/site-packages/grpc/_channel.py", line 447, in result
    raise self
  File "../qdrant/qdrant_client/venv/lib/python3.10/site-packages/grpc/_interceptor.py", line 314, in continuation
    response, call = self._thunk(new_method).with_call(
  File "../qdrant/qdrant_client/venv/lib/python3.10/site-packages/grpc/_channel.py", line 1182, in with_call
    return _end_unary_response_blocking(state, call, True, None)
  File "../qdrant/qdrant_client/venv/lib/python3.10/site-packages/grpc/_channel.py", line 999, in _end_unary_response_blocking
    raise _InactiveRpcError(state)  # pytype: disable=not-instantiable
grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with:
	status = StatusCode.INVALID_ARGUMENT
	details = "Not existing vector name error: "
	debug_error_string = "INVALID_ARGUMENT:Not existing vector name error: "
>

In the beginning, it failed just on the first attempt and then worked when I reran it.
I tried running it again, and got the problem with not existing vector name 5 times in a row before it worked.

Align with the latest public-api collections.proto: paginated
ListCollections (limit/offset_token/next_offset_token) plus keyword
prefix and text analysis fields (stopwords, stemmer, ascii_folding).
@qdrant-cloud-bot

Copy link
Copy Markdown

Updated this PR to match the latest serverless collections API in qdrant-cloud-public-api:

  • Pagination (ListCollections): get_collections(limit=..., offset_token=...) now returns CollectionsList with next_offset_token
  • Keyword prefix + text analysis (stopwords / stemmer / ascii_folding) on models and conversions
  • Proto sync script strips server-only buf.validate annotations

Tests: pytest tests/test_serverless.py (4 passed)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants