Cover image for Kdrant: an idiomatic, coroutine-first Kotlin client for Qdrant

AS

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

Enter fullscreen mode Exit fullscreen mode

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 suspend function, 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 KdrantException you 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")
}

Enter fullscreen mode Exit fullscreen mode

Spin up Qdrant locally:

docker run -p 6333:6333 qdrant/qdrant

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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 QdrantClient bean
  • 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

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.