Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,13 @@ jobs:
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6

# Both engines run the same client contract from kdrant-testkit, against the same image.
- name: Integration tests against ${{ matrix.qdrant }}
env:
QDRANT_IMAGE: ${{ matrix.qdrant }}
run: ./gradlew :kdrant-transport-rest:test --tests "*IntegrationTest*" --no-daemon --stacktrace
run: >
./gradlew :kdrant-transport-rest:test :kdrant-transport-grpc:test
--tests "*IntegrationTest*" --no-daemon --stacktrace

dependency-review:
name: Dependency review
Expand Down
26 changes: 23 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ All notable changes to this project are documented in this file. The format is b
every query. Here Qdrant runs the search. The module depends only on Koog's stable `rag-base`, not on
the `rag-vector` beta. Koog's `namespace` becomes a payload field and a filter, so one collection can
hold several of them.

### Added

- Cluster and sharding (M32), the gap the migration guide used to name as having no Kdrant equivalent.
`collectionClusterInfo(name)` reads how a collection's shards are spread across peers, including the
transfers in flight; `updateCollectionCluster(name, operation)` moves, replicates, aborts or drops a
Expand Down Expand Up @@ -47,8 +44,31 @@ All notable changes to this project are documented in this file. The format is b
between a backup that fits in a window and one that does not. Shard ids come from
`collectionClusterInfo`.


- **`kdrant-transport-grpc`** (M31), the opt-in gRPC engine. `KdrantGrpc(host)` returns the same
`QdrantClient` the REST factory does, over Qdrant's `Collections`, `Points`, `Snapshots` and `Health`
services on port **6334**. REST stays the recommended engine; reach for this one when throughput or
long-lived streaming is the bottleneck, which is the case the README used to concede to the official
client. Nothing changes for a REST user: the module is separate, and a build that does not ask for it
resolves no gRPC, no protobuf and no Netty.
The stubs are generated from Qdrant's own `.proto` files, vendored verbatim at v1.18.2, rather than
taken from `io.qdrant:client` — grpc-kotlin emits suspend functions and `Flow`s, which is the shape
the transport seam already has, and generating decides the dependency set instead of inheriting a
shaded Netty jar that is most of the official client's footprint.
- Both engines are held to one **shared client contract** (`kdrant-testkit`), which runs the same 30
behavioural tests against a real Qdrant over each protocol. The REST tests that came before it
asserted HTTP bodies, which a gRPC engine cannot satisfy by construction.

### Changed

- Eleven `QdrantTransport` operations have no gRPC equivalent, because the seam was shaped by Qdrant's
REST API and Qdrant serves these over HTTP only: `telemetry`, `metrics`, `listIssues`, `clearIssues`,
`recoverSnapshot`, snapshot download and upload, and the five shard-scope snapshot operations. On the
gRPC engine each throws an `UnsupportedOperationException` naming the operation and pointing at REST,
rather than degrading quietly — a snapshot download that returns nothing is a backup that does not
exist. The REST engine is unchanged.


- Releases publish to Maven Central only. The secondary publication to GitHub Packages is gone: it
carried the same artifacts to a registry that requires authentication even for public packages, so it
was a second place to keep in sync and no second way for anyone to depend on Kdrant. Versions up to
Expand Down
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ Dependency stacks verified against `io.qdrant:client:1.18.3`:
| Models | `kotlinx-serialization` data classes | generated protobuf messages |
| GraalVM native / cold start | friendly (no Netty/protobuf reflection config) | needs gRPC/Netty/protobuf native config; heavier cold start |

For raw throughput and streaming, gRPC/HTTP2 still wins. Reach for the official client when that is
your bottleneck. For typical RAG and embedding-search workloads, Kdrant trades it for a fraction of
the footprint and idiomatic Kotlin.
For raw throughput and streaming, gRPC/HTTP2 still wins. That case now has an answer inside Kdrant:
`kdrant-transport-grpc` is an opt-in engine behind the same `QdrantClient`, so the column above stays
true of the default and a build that does not ask for gRPC never pays for it. For typical RAG and
embedding-search workloads, REST trades the wire for a fraction of the footprint and idiomatic Kotlin.

## Installation

Expand Down Expand Up @@ -273,15 +274,18 @@ val hits = catching { qdrant.search("articles") { query(queryVector) } }.getOrEl

## Architecture

Two modules keep protocol concerns out of the public API:
Three modules keep protocol concerns out of the public API:

| Module | Contents |
| --- | --- |
| `kdrant-core` | Public API (`QdrantClient`), models, DSLs, error hierarchy, and the `QdrantTransport` seam, with no wire-protocol knowledge. |
| `kdrant-transport-rest` | The default REST engine (Ktor CIO) implementing `QdrantTransport`, plus the `Kdrant(...)` factory. |
| `kdrant-transport-grpc` | The opt-in gRPC engine over Qdrant's own protobuf services, plus the `KdrantGrpc(...)` factory. Depend on it only if you want it. |

The DSLs and client logic live in `kdrant-core` and are independent of the protocol; only the
engine module knows about HTTP.
The DSLs and client logic live in `kdrant-core` and are independent of the protocol; only an engine
module knows about a wire. Both engines are held to the same behavioural test suite, so the choice is
a footprint and throughput decision rather than a feature one — with the exception of the operations
Qdrant serves over HTTP only, which the gRPC engine names rather than degrading.

## Roadmap

Expand All @@ -301,10 +305,8 @@ from `1.1.0` is a recompile rather than a jar swap — see
[STABILITY.md](STABILITY.md#what-may-still-change-in-a-minor); the [CHANGELOG](CHANGELOG.md) has the
version-by-version detail.

**Next.** Nothing is claimed for the next release yet; the board is where it gets decided.

**Later.** Kotlin Multiplatform (`commonMain`) and an optional opt-in gRPC engine, with REST staying
the default.
**Merged, unreleased.** The opt-in gRPC engine, `kdrant-transport-grpc`. It is in `main` and has not
shipped; see the [CHANGELOG](CHANGELOG.md#unreleased) for what it changes.

The plan lives on the [Kdrant board](https://github.com/orgs/NaCode-Studios/projects/4) — one item per milestone, each with its
exit criterion — and every tier is a [milestone](https://github.com/NaCode-Studios/Kdrant/milestones) in this repository. See
Expand Down
13 changes: 10 additions & 3 deletions docs/migrating-from-qdrant-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@ builders and the static factory imports (`id`, `value`, `vectors`, `range`) beco
Your collections, points, payloads and filters are untouched: both clients talk to the same server, so
you can run them side by side against the same Qdrant and move one call site at a time.

If you want to keep gRPC, only the second and third of those changes apply. Depend on
`kdrant-transport-grpc` and build the client with `KdrantGrpc(host)` instead of `Kdrant(host)`; the port
stays `6334` and everything else in this guide reads the same, because the API above the wire is the
same API. What that engine cannot do is listed under [What this is not](#what-this-is-not).

## What this is not

Kdrant does not speak gRPC. If your bottleneck is raw throughput or long-lived streaming, HTTP/2 is
still the faster wire and the official client is the right tool — the trade-off is set out in the
[README](../README.md#footprint-vs-the-official-client).
The gRPC engine is not the whole of Qdrant. Eleven operations are served over HTTP only — telemetry,
Prometheus metrics, the issues endpoint, snapshot recovery, snapshot download and upload, and the
shard-scope snapshots — and on the gRPC engine each throws rather than pretending. If you need any of
them, use the REST engine for the whole client or for that one call. The footprint trade-off between
the two is set out in the [README](../README.md#footprint-vs-the-official-client).

Cluster support covers a collection's shard distribution: reading it, moving and replicating shards,
and creating or dropping custom sharding keys. The node-level calls that administer the raft cluster
Expand Down
1 change: 1 addition & 0 deletions kdrant-bom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies {
constraints {
api("io.github.nacode-studios:kdrant-core:${project.version}")
api("io.github.nacode-studios:kdrant-transport-rest:${project.version}")
api("io.github.nacode-studios:kdrant-transport-grpc:${project.version}")
api("io.github.nacode-studios:kdrant-spring-boot-starter:${project.version}")
api("io.github.nacode-studios:kdrant-spring-ai:${project.version}")
api("io.github.nacode-studios:kdrant-langchain4j:${project.version}")
Expand Down
66 changes: 66 additions & 0 deletions kdrant-transport-grpc/api/kdrant-transport-grpc.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
public final class dev/kdrant/transport/grpc/GrpcQdrantTransport : dev/kdrant/transport/QdrantTransport {
public fun batchUpdate (Ljava/lang/String;Ljava/util/List;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun clearIssues (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun clearPayload (Ljava/lang/String;Ldev/kdrant/model/DeleteSelector;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun close ()V
public fun collectionClusterInfo (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun collectionExists (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun count (Ljava/lang/String;Ldev/kdrant/model/Filter;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun createCollection (Ljava/lang/String;Ldev/kdrant/model/CreateCollectionRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun createPayloadIndex (Ljava/lang/String;Ljava/lang/String;Ldev/kdrant/model/PayloadSchemaType;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun createShardKey (Ljava/lang/String;Ldev/kdrant/model/CreateShardKeyRequest;Ljava/lang/Integer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun createShardSnapshot (Ljava/lang/String;IZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun createSnapshot (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun createStorageSnapshot (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun delete (Ljava/lang/String;Ldev/kdrant/model/DeleteSelector;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun deleteCollection (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun deletePayload (Ljava/lang/String;Ljava/util/List;Ldev/kdrant/model/DeleteSelector;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun deletePayloadIndex (Ljava/lang/String;Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun deleteShardKey (Ljava/lang/String;Ldev/kdrant/model/ShardKey;Ljava/lang/Integer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun deleteShardSnapshot (Ljava/lang/String;ILjava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun deleteSnapshot (Ljava/lang/String;Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun deleteStorageSnapshot (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun deleteVectors (Ljava/lang/String;Ljava/util/List;Ldev/kdrant/model/DeleteSelector;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun downloadShardSnapshot (Ljava/lang/String;ILjava/lang/String;)Lkotlinx/coroutines/flow/Flow;
public fun downloadSnapshot (Ljava/lang/String;Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow;
public fun downloadStorageSnapshot (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow;
public fun facet (Ljava/lang/String;Ljava/lang/String;Ldev/kdrant/model/Filter;Ljava/lang/Integer;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun getCollection (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun healthz (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun listAliases (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun listCollectionAliases (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun listCollections (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun listIssues (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun listShardSnapshots (Ljava/lang/String;ILkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun listSnapshots (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun listStorageSnapshots (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun livez (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun metrics (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun overwritePayload (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ldev/kdrant/model/DeleteSelector;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun query (Ljava/lang/String;Ldev/kdrant/model/SearchRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun queryBatch (Ljava/lang/String;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun queryGroups (Ljava/lang/String;Ldev/kdrant/model/SearchGroupsRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun readyz (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun recoverShardSnapshot (Ljava/lang/String;ILjava/lang/String;Ldev/kdrant/model/SnapshotPriority;Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun recoverSnapshot (Ljava/lang/String;Ljava/lang/String;Ldev/kdrant/model/SnapshotPriority;Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun retrieve (Ljava/lang/String;Ljava/util/List;Ldev/kdrant/model/WithPayload;Ljava/lang/Boolean;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun scroll (Ljava/lang/String;Ldev/kdrant/model/ScrollRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun searchMatrixOffsets (Ljava/lang/String;Ldev/kdrant/model/SearchMatrixRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun searchMatrixPairs (Ljava/lang/String;Ldev/kdrant/model/SearchMatrixRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun setPayload (Ljava/lang/String;Lkotlinx/serialization/json/JsonObject;Ldev/kdrant/model/DeleteSelector;Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun telemetry (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun updateAliases (Ljava/util/List;Ljava/lang/Integer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun updateCollection (Ljava/lang/String;Ldev/kdrant/model/UpdateCollectionRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun updateCollectionCluster (Ljava/lang/String;Ldev/kdrant/model/ClusterOperation;Ljava/lang/Integer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun updateVectors (Ljava/lang/String;Ljava/util/List;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun uploadShardSnapshot (Ljava/lang/String;ILkotlinx/coroutines/flow/Flow;Ldev/kdrant/model/SnapshotPriority;Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun uploadSnapshot (Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;Ldev/kdrant/model/SnapshotPriority;Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun upsert (Ljava/lang/String;Ljava/util/List;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun upsert (Ljava/lang/String;Lkotlinx/coroutines/flow/Flow;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class dev/kdrant/transport/grpc/KdrantGrpcKt {
public static final fun KdrantGrpc (Ljava/lang/String;IILkotlin/jvm/functions/Function1;)Ldev/kdrant/QdrantClient;
public static synthetic fun KdrantGrpc$default (Ljava/lang/String;IILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ldev/kdrant/QdrantClient;
}

5 changes: 5 additions & 0 deletions kdrant-transport-grpc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ dependencies {
testRuntimeOnly(libs.junit.platform.launcher)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.grpc.inprocess)

// The shared client contract, run here against the same real Qdrant the REST engine is held to.
testImplementation(project(":kdrant-testkit"))
testImplementation(platform(libs.testcontainers.bom))
testImplementation(libs.testcontainers.qdrant)
}

// Resolved before the protobuf { } block: inside it the extension receiver shadows the catalog accessor.
Expand Down
Loading