DEV Community

AS
AS

Posted on

Kdrant 2.0: I removed the reason to use the official client

In the post where I introduced Kdrant there was a paragraph I never liked writing:

For raw throughput and streaming, gRPC/HTTP2 still wins. If that's your bottleneck, use the official client.

It was true, and it was also the one line in the post that sent people somewhere else. Kdrant 2.0 answers it. There's a gRPC engine now, sitting behind the same QdrantClient as the REST one.

Same client, your choice of wire

val qdrant = Kdrant(host = "localhost")        // REST, port 6333
val qdrant = KdrantGrpc(host = "localhost")    // gRPC, port 6334
Enter fullscreen mode Exit fullscreen mode

That's the migration. Every call after that line is identical, because the API was never protocol-aware: the wire has been behind a QdrantTransport interface since the first release.

Let me be precise about what that's worth, because "we have a clean abstraction" costs nothing to say. Adding the gRPC engine changed zero lines of kdrant-core. Not a handful of small changes. The diff on that module is empty. It's the only test of a seam that means anything, and running it required writing a second implementation.

The engine generates its own stubs from Qdrant's .proto files instead of wrapping io.qdrant:client. Two reasons for that. grpc-kotlin emits suspend functions and Flows, which is the shape the transport already had, so there's no ListenableFuture to adapt back into coroutines. And generating decides the dependency set rather than inheriting it: this engine resolves grpc-okhttp, not the shaded Netty jar that accounts for about 9 MB of the official client on its own.

If you don't want gRPC you pay nothing for it. It's a separate artifact, and a build that depends on kdrant-transport-rest resolves no gRPC, no protobuf and no Netty. That's checked on every build rather than asserted in a README. There's a Gradle task that fails if anything from those groups shows up on the REST engine's runtime classpath, and I confirmed it works by adding grpc-stub to that module and watching it name the five artifacts that arrived.

The test suite turned out to be the interesting part

Two engines are only interchangeable if something proves it, and the existing tests couldn't. They assert HTTP request bodies, which a gRPC engine can't satisfy by construction.

So there's a shared suite now: about 30 behavioural tests that take a QdrantClient from outside, talk to a real Qdrant in Docker, and never mention a protocol. Both engines run the same file against the same server.

It earned its keep on the first run. One test failed on gRPC, and only on gRPC: an ordered scroll came back with no payloads at all.

The cause is worth knowing if you ever write against Qdrant directly. It defaults with_payload to true on scroll and retrieve. The REST engine just omits the field when you haven't asked for anything, so you get that default. My gRPC engine was translating "the caller said nothing" into an explicit enable = false, which is a different request. Every scroll and every retrieve that didn't ask for a payload was quietly coming back empty.

You don't find that by reading code, and no unit test on either engine would have caught it. It needs a test that runs on both and compares them against a server that has opinions.

The core is multiplatform

kdrant-core builds for the JVM and eight Kotlin/Native targets, covering iOS, macOS, Linux and Windows. Same reason as everything above: code that never knew there was a wire has nothing platform-specific to port. Moving it into commonMain changed exactly one declaration, the default dispatcher, because only the JVM publishes Dispatchers.IO.

I want to be clear about what that does and doesn't buy you today. The engines are still JVM-only, since Ktor CIO and grpc-java are. An Android app and an iOS app can share their models, their filter DSL and their query building, and the iOS side brings its own transport. That is not the same as "Kdrant runs on iOS", and I'd rather say so than let a list of target names imply it.

Kotlin/JS is deliberately absent. With no JS engine it would ship a DSL with nothing to send, and its test tooling was the only npm dependency graph in the repo, which arrived carrying a high-severity advisory. One line brings it back the day there's an engine worth having there.

What upgrading costs you

Every 1.x call site compiles unchanged, so source compatibility is intact. Binary compatibility is not, in two places, and it's worth being exact about both.

The first is the artifact layout. kdrant-core is multiplatform now, so its coordinate carries Gradle module metadata and the JVM classes live in kdrant-core-jvm. On Gradle you change the version number and nothing else, because Gradle reads that metadata and picks the variant. On Maven, if you named kdrant-core directly, you move to kdrant-core-jvm. Depending on kdrant-transport-rest or kdrant-transport-grpc, which is what I'd recommend anyway, spares you both.

The second is smaller and easier to miss. ScrollRequest and SearchRequest gained a shardKey parameter when cluster support landed, which changes their generated copy and componentN. The constructor keeps its defaults so your code still compiles, but if you call copy() on either type against a jar built for 1.x, recompile.

The multiplatform migration itself changed no public API at all. The *.api dump is identical either side of it, which is worth saying because it's the part that sounds like it should have broken something.

One limit to know before you switch engines. Qdrant serves eleven operations over HTTP only: telemetry, Prometheus metrics, the issues endpoint, snapshot recovery, snapshot download and upload, and the shard-scope snapshots. On the gRPC engine each one throws, naming itself and naming REST. A snapshot download that quietly returned nothing would be a backup that quietly doesn't exist, so I'd rather it fail loudly.

Try it

dependencies {
    implementation("io.github.nacode-studios:kdrant-transport-rest:2.0.0")
    // or, for gRPC:
    implementation("io.github.nacode-studios:kdrant-transport-grpc:2.0.0")
}
Enter fullscreen mode Exit fullscreen mode

There are Spring Boot, Spring AI, LangChain4j, Koog and Micrometer modules too, all on the same version.

Feedback still welcome, and especially from anyone running the gRPC engine under real load. That's the case I built it for and the one I can't properly test on my own.

Top comments (0)