If you build on the JVM and want to use Qdrant, the official client is io.qdrant:client — and it's built for Java. Every call returns a ListenableFuture, requests are assembled with protobuf builders, and it drags a gRPC/Netty stack onto your classpath. From Kotlin, that means fighting the language:
// official Java client, from Kotlin
val future: ListenableFuture<UpdateResult> = client.upsertAsync("articles", points)
val result = future.get() // block, or bolt on a future→coroutine bridge yourself
You reach for coroutines, you get futures. You want a DSL, you get protobuf builders.
Meet Kdrant
Kdrant is the client you'd actually want to write Kotlin against:
-
Coroutine-first — every operation is a
suspendfunction, with cooperative cancellation and timeouts - Type-safe DSLs for collections, points, payloads, and filters
-
Small footprint — a pure-Kotlin REST engine on Ktor +
kotlinx-serialization; no gRPC, Netty, or protobuf -
Typed errors — a sealed
KdrantExceptionyou can handle exhaustively
It's stable (1.1.0, SemVer) and published to Maven Central under io.github.nacode-studios.
Quick start
Requires JDK 17+. One dependency:
dependencies {
implementation("io.github.nacode-studios:kdrant-transport-rest:1.1.0")
}
Spin up Qdrant locally:
docker run -p 6333:6333 qdrant/qdrant
Connect, create a collection, upsert, search:
val qdrant = Kdrant(host = "localhost", port = 6333) {
apiKey = System.getenv("QDRANT_API_KEY") // omit for a local, unauthenticated node
requestTimeout = 5.seconds
}
qdrant.use { client ->
client.createCollection("articles") {
vector { size = 1_536; distance = Distance.COSINE }
}
client.upsert("articles", wait = true) {
point(id = 1) {
vector(embedding) // your List<Float> from any embedding model
payload("title" to "Introduction", "lang" to "en", "year" to 2026)
}
}
val hits = client.search("articles") {
query(queryVector)
limit = 5
filter { must { "lang" eq "en" } }
}
}
Kdrant stores and searches vectors you already have — it does not generate embeddings.
A filter DSL that reads like Kotlin
Qdrant's full filtering model, expressed declaratively:
val query = filter {
must {
"lang" eq "en"
"year" gte 2024
"price" between 10.0..99.0
}
should {
matchAny("tag", "featured", "promo")
geoRadius("location", GeoPoint(lon = 13.40, lat = 52.52), radius = 5_000.0)
}
mustNot { "archived" eq true }
}
Decode results straight into your types
@Serializable data class Article(val title: String, val lang: String)
val articles: List<Hit<Article>> = qdrant.searchAs<Article>("articles") {
query(queryVector); limit = 5
}
val first: Article? = articles.firstOrNull()?.payload
Hybrid search (dense + sparse)
The modern /points/query engine is fully supported. Fuse several prefetch sources with Reciprocal Rank Fusion for true dense + keyword hybrid search:
val hits = qdrant.search("articles") {
prefetch { query(denseVector); using = "text"; limit = 50 }
prefetch { querySparse(indices, values); using = "keywords"; limit = 50 }
rrf() // or dbsf()
limit = 10
}
recommend / discover / context, grouped and batch search, multi-vectors, and a Flow-based scroll are all there too.
Building RAG? It plugs in.
Kdrant ships first-class integrations so you don't wire it by hand:
-
Spring Boot starter — an auto-configured
QdrantClientbean -
Spring AI — implements
VectorStore -
LangChain4j — implements
EmbeddingStore
There's a runnable example-rag service (ingest → embed → store → retrieve) with a docker-compose for Qdrant, so you can see it end to end.
The honest tradeoff
For raw throughput and streaming, gRPC/HTTP2 still wins — reach for the official client when that is your bottleneck. Kdrant trades that for idiomatic Kotlin and a much smaller footprint:
| Kdrant | Official io.qdrant:client
|
|
|---|---|---|
| Wire protocol | REST over Ktor CIO | gRPC (HTTP/2) |
| Heavy deps | none — pure Kotlin | shaded Netty, protobuf, gRPC, Guava |
| Added footprint | ~3–5 MB | ~15–20 MB |
| API style |
suspend + Flow, DSL |
ListenableFuture, protobuf builders |
| GraalVM native | friendly | needs gRPC/Netty/protobuf config |
For typical RAG and embedding-search workloads, that's a trade I'll take.
Try it
- ⭐ Repo: https://github.com/NaCode-Studios/Kdrant
- 📦 Maven Central:
io.github.nacode-studios:kdrant-transport-rest:1.1.0 - 📖 API docs: https://nacode-studios.github.io/Kdrant/
- 🪪 Apache-2.0
Feedback is very welcome — especially on API ergonomics. If there's something you'd want from a Kotlin-native Qdrant client, open an issue and let's talk.
Top comments (2)
I appreciate how Kdrant provides a coroutine-first approach, which aligns well with Kotlin's asynchronous programming model, making it easier to write efficient and readable code. The use of type-safe DSLs for collections, points, and filters is also a great feature, as it reduces the likelihood of runtime errors and improves code maintainability. I'm curious to know how the performance of Kdrant compares to the official Java client, especially in terms of latency and throughput, have you conducted any benchmarks to measure the differences?
Thanks, that means a lot — the coroutine + DSL ergonomics were basically the whole reason I built it, so it's good to hear that part landed.
Honest answer on perf: I haven't done a proper head-to-head against the official client yet. There's a JMH benchmark in the repo (the benchmarks module) that measures upsert and search latency end-to-end against a real Qdrant, p50/p90/p99, but that's Kdrant on its own, not a side-by-side — so I'd rather not throw out comparison figures I can't stand behind.
What I'd expect: gRPC should come out ahead on raw throughput and anything streaming-heavy, since HTTP/2 multiplexing and binary protobuf just carry less overhead than REST/JSON (I say as much in the README). For a single query or upsert I'd expect them to be pretty close though, because most of the time goes into Qdrant's ANN search and the network round-trip, not into how the request got serialized. The coroutines aren't really about making one call faster either — they just make firing off lots of concurrent calls far less painful than juggling ListenableFutures on a thread pool.
Where the gap is already real is footprint: ~3–5 MB with no gRPC/Netty/protobuf on the classpath, versus ~15–20 MB — which also makes GraalVM native a lot less of a headache.
A proper apples-to-apples benchmark is honestly overdue. The harness is already there, so if you end up pointing it at your own workload I'd love to see what you get.