All notable changes to this project are documented in this file. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- An ingest whose source dies now hands out the checkpoint it earned. The batches still in flight when the source threw were cancelled where they stood, so whether a run reported any checkpoint at all depended on which request happened to come back first, and a run killed early enough could report none — leaving nothing to resume from in exactly the case the token exists for. The source's failure is now held until those batches have drained and reported, then thrown, which is what a batch failure already did and for the same reason: a batch cancelled after the server accepted it is a point the collection holds and no token counts.
2.2.0 - 2026-08-06
Tiers 8 and 9, complete. The theme is the distance between a request this client can build and one that
has been shown to work, and the client's behaviour where a cluster is not healthy. matchPhrase now
matches something. Sparse and hybrid search meet a server. Metrics report from both engines. The public
API is tracked per native target rather than only for the JVM, and the compatibility promise says what
it means for a klib. A degraded cluster reports itself. And there is a binary.
- The payload index parameters Qdrant takes (M43).
createPayloadIndex(name, field, schema)sent a field name and a type, which was the entire request the client could build. A builder overload sends the rest: a text index chooses its tokenizer, token lengths, lowercasing and phrase matching; an integer index chooses whether it answers lookups, ranges or both; keyword and uuid indexes takeisTenant, integer, float and datetime takeisPrincipal, and all of them takeonDisk. The four-argument call is unchanged and still there. Two of those decide whether a query works rather than how fast it is.matchPhrasewas in the filter DSL and deliberately absent from the contract, because Qdrant matches a phrase only against an index built withphrase_matching: trueand nothing could ask for one, so the filter was accepted and matched nothing. The contract now asserts it, over both engines, and the comment explaining why it could not is deleted rather than reworded. - Sparse, multi-vector and hybrid search in the shared contract (M44).
sparseVector,querySparse,queryMultiand reciprocal-rank fusion were public API for four releases with nothing but body assertions behind them, which is the gap between a request that is well formed and one that works. The contract creates a collection carrying a dense and a sparse vector, upserts points with both, runs an RRF hybrid query and a DBSF one, exercises IDF scoring where the server rescales what was sent, stores and queries a multi-vector collection, and asserts that a query naming a vector the collection lacks is refused. All of it over both engines against a real Qdrant. - Server-side inference (M48). Qdrant takes
Document,ImageandInferenceObjectwherever it takes a vector, and Kdrant could not express the request.VectorData.InferenceandQueryInterface.Inferencecarry one, withdocument(...)on the point builder andqueryDocument(...)on the search builder for the common case. Kdrant still embeds nothing: it bundles no model and takes no dependency on an inference library, and this carries a request naming what to embed and with which model exactly as a filter carries what to match. The round trip needs a provider configured on the server, so it runs where one exists and is skipped where it is not, while both request shapes are validated against Qdrant's own OpenAPI document on every build. - An ingest that owns batching, concurrency and resume (M55).
QdrantClient.ingesttakes aFlowand owns the parts that were the caller's: batching by count and by serialized size, a bound on requests in flight, retry of a batch rather than of the stream, and anIngestCheckpointnaming the points the server acknowledged. The count advances only over an unbroken prefix, so a batch acknowledged while an earlier one is still being retried never moves it and a resumed run cannot skip a point that was never written. The token is a lower bound rather than an equality, and the difference is the whole safety argument. A batch that was in flight when a run died may already have been applied, and an acknowledgement that never came back cannot move a checkpoint, so the collection can hold more than the token claims. Resuming therefore re-sends a few points that are already there, which upsert makes free; a token that could over-claim would skip points and lose them without a trace.batchUpdateandupsertare unchanged and still there. - TLS a native consumer can configure (M47).
useTlsdecided whether to speak HTTPS and nothing decided which certificate to accept, and the documented escape hatch,configureClient, hands back a star-projected Ktor config that cannot open an engine-specific block.KdrantConfig.trustAnchorstakesTrustAnchors.System, aPembundle, or a set of pinned public keys. The JVM honours all three, Linux honours a PEM bundle through libcurl, and iOS, macOS and Windows honour the system store their platform owns. A configuration a target cannot honour is refused when the client is built, naming the platform and the store to put the certificate in, rather than falling back to system trust and looking like it complied. The KDoc names the store each engine reads. KdrantException.retryable, and three failures a degraded cluster used to hide (M52).ReadOnlyis a node refusing writes while still serving reads, which is not the same event as a credential being refused;ShardUnavailableis a shard with no live replica, where the collection exists and part of it is unreachable;PartiallyAppliedreports how many points a split upsert wrote before it failed, so the choice between re-sending everything and losing the rest is made with a number. Every exception in the hierarchy now says whether the same request could succeed later.kdrant-testkitgrewQdrantCluster, a real two-node cluster in Docker, andDegradedClusterContract, which stops a node and lowers a strict-mode ceiling and asserts what the client does. Both engines run it.StrictModeConfigoncreateCollectionandupdateCollection: the per-collection limits Qdrant enforces on the requests it accepts, including the disk and memory ceilings past which a node refuses writes and keeps serving reads. Added as part of M52, because it is what makes that state reachable from a test rather than from an incident. Its KDoc names which server versions enforce which limit, which is not decoration. Qdrant 1.18 refuses writes over the disk ceiling and Qdrant 1.19 does not, having deprecated that family in favour of a global quota API. The degraded-cluster contract found that by passing against the pinned version and failing againstlatest, which is the version matrix earning its place on its first run.kdrant-cli(M49). One static binary per platform for the operations that are not requests:migrate,snapshot create|list|download|restore|delete,collectionsandscroll. No JVM, no classpath, no install step. It is not published to Maven Central; the binaries are built on two runners, run against a real Qdrant before they are allowed near a release, attested with SLSA provenance and attached to the GitHub Release with their checksums. It cannot embed, so it migrates what does not need re-embedding, and the help text says so.kdrant migratecreates the target from the source's own vectors rather than asking for a size and a distance the person at the terminal did not choose, and--shardsand--replicasoverride, which is what makeskdrant migrate a b --shards 4a re-shard. The first release build found that it did not: the migration refused because the target did not exist, which is true of every collection nobody has made yet and therefore of the whole command.- The client contract against four Qdrant versions (M50).
QdrantVersionMatrixIntegrationTestruns the shared contract against the four most recent minors and writes the result into the README between generated markers. It does not fail the build: a cell that is red against an older server is the information the matrix exists to publish, and a matrix that fails a release for being honest is a matrix that gets deleted. The blocking check is unchanged, against the pinned version andlatest. - A benchmark against
io.qdrant:client(M51). Every number this repository published compared Kdrant to Kdrant.OfficialClientComparisonBenchmarkruns both clients against the same server, the same data and the same JVM over a single search, a batch search, an upsert of a large batch and a full scroll. The official client is a benchmark dependency and reaches no published POM.
- Metrics moved to the transport seam (M45).
kdrant-micrometerwas a Ktor client plugin, so a client built withKdrantGrpcpublished no metrics at all and nothing said so. It is now aQdrantTransportdecorator, the same seamkdrant-oteltraces, installed the same way:decorateTransport = kdrantMetrics(registry). The tags change with it, and the cost belongs here rather than in a release note nobody reads. Theoperationtag stops being a route template such as/collections/{collection}/points/queryand becomes the operation,query, which is the vocabularydb.operation.namealready uses on the span.methodandstatusare gone, because neither exists on a gRPC call and a tag present on one engine is a tag no dashboard can rely on.outcomegainsCANCELLEDand reports the exception subclass instead of an HTTP class. Dashboards built on the old values need editing. Installing throughconfigureClientstill works and is deprecated, with the replacement in the message. - A TLS failure is a
KdrantException. A refused certificate is not anIOExceptionon every platform, soCertPathValidatorExceptionandCertificateExceptionused to escape the transport seam and reach a caller who had been told every failure there is aKdrantException. They are wrapped inKdrantException.Transportnow, with the cause kept. STABILITY.mdstates the2.xpromise per artifact type (M53), including what a minor may and may not do to a klib, why narrowing a declaration to fewer targets is a removal, what a Kotlin compiler upgrade is allowed to do, and what happens when a Qdrant change forces a signature change.- The Kotlin/JS exclusion is argued in the README (M54) rather than in a comment in a build file. A browser cannot reach a Qdrant without CORS on the server, a Qdrant reachable from a browser is reachable from anyone, and an API key shipped to a browser is a published key. The README names what would change the answer; the build-file comment is deleted rather than duplicated.
KdrantMetrics, the Ktor client plugin, andHttpClientConfig<*>.kdrantMetrics(...)that installs it. Both keep working for one minor. UsedecorateTransport = kdrantMetrics(registry), which covers both engines.
STABILITY.mdgains an Upgrading from2.1section naming the twelve lines this release removes from the API dumps, all of which are a defaulted parameter changing the signature Kotlin emits or a class becoming open, and none of which is a capability going away. The count came fromgit diff v2.1.0 v2.2.0 -- '*/api/*.api'rather than from an impression.- The public API is tracked per native target (M46).
apiDumpwrites a.klib.apibeside the JVM dump for every multiplatform module, merged and target-annotated, so a declaration present on some targets and not others is a diff rather than silence. The dump can only be regenerated on a host that builds the Apple targets:CONTRIBUTING.mdsays so, and the macOS job in CI runsapiCheckfor exactly that reason. kdrant-micrometergained a Testcontainers integration test that runs the same operation over both engines and compares the meter ids, which is the claim the module now rests on.- The release workflow builds the CLI binaries on two runners, proves each against a real Qdrant, and attaches them to the GitHub Release with SLSA provenance and checksums.
- The benchmarks workflow exposes Qdrant's gRPC port, because the official client speaks it.
- Dependency and action bumps that landed between the tier work and the tag: Ktor 3.5.1 to 3.5.2,
langchain4j 1.18.0 to 1.18.1, Guava 33.5.0 to 33.6.0, and
io.qdrant:client1.15.0 to 1.18.3, which is a benchmark dependency and reaches no published POM. Two GitHub Actions moved by a major:actions/attest-build-provenancev3 to v4 andactions/download-artifactv7 to v8, both inrelease.yml. A grouped minor or patch bump with green CI needs no entry; a major does, and a workflow that ships no API still decides how this project releases.
2.1.0 - 2026-08-02
Tier 7, complete. Four claims that were previously compiled, argued or asserted are now things a build
proves: the REST engine runs on every target kdrant-core does and the shared contract runs from a
native binary, a scoped token is a credential the client knows about, a native image is built and made
to search, and every published POM names the platforms that module actually has.
kdrant-transport-restis multiplatform (M38).kdrant-corehad compiled for eight Kotlin/Native targets since2.0.0, and not one of them could send a request: the engine lived insrc/mainand was Ktor CIO on the JVM, so an iOS build got the models, the query DSL and the filter builders with nothing to put them on the wire.RestQdrantTransportis incommonMainnow and the engine is chosen per target — CIO on the JVM, unchanged; Darwin on iOS and macOS; Curl on Linux; WinHttp on Windows. An iOS or Linux consumer depends on the samekdrant-transport-restcoordinate a JVM one does. Two engine choices have consequences worth knowing before a stack trace tells you: Darwin is NSURLSession and inherits App Transport Security, so a plaintexthttp://Qdrant is refused by the platform before Kdrant sees the request, and Curl links against the system libcurl, which a slim container image may not have. Kotlin/JS stays out for the reason already written inkdrant-core.kdrant-testkitis multiplatform, which is what makes the above more than a compilation exercise. The behavioural contract both engines are held to moved tocommonMainasQdrantClientContractSuite, which knows no test framework; the JUnit and Testcontainers wrapper stays on the JVM and still declares one test per behaviour. CI runs the same suite from alinuxX64and amacosArm64binary against a real Qdrant.- Scoped access (M39).
KdrantConfigtakes abearerTokenbesideapiKey, mutually exclusive with it: a Qdrant JWT narrowed to read-only, to named collections, or to a payload filter deciding which points a caller may see at all. Both engines send it —Authorization: Bearerover REST, the same header as gRPC metadata — because the credential belongs to the config rather than to the wire.kdrant-testkitsigns one for tests throughQdrantJwt; minting tokens for a running system stays Qdrant's job. KdrantException.Forbidden, a subclass ofUnauthorized, for HTTP 403 and gRPCPERMISSION_DENIED. A read-only token refused on a write is a different fact from a missing key, and only one of them is worth retrying. Being a subclass keeps an existingcatch (e: KdrantException.Unauthorized)catching it and keeps awhenover the sealed hierarchy exhaustive.kdrant-otel(M41), a new module: one OpenTelemetry client span per operation, on the transport seam, so one implementation covers both engines and a third would inherit it. Attributes follow OpenTelemetry's database conventions rather than an invented vocabulary. No payload value, vector or filter reaches an attribute, and a failed span carries the exception type rather than the server's message, because Qdrant quotes the request back in its errors. It depends on the OpenTelemetry API, never the SDK, so the exporter stays the consumer's.kdrant-migrate(M42), a new module:migrateCollection(from, to, alias)copies a collection into one with a different vector size, verifies the result, and moves an alias so readers cross in one step. It resumes from a cursor after an interruption rather than starting over, and the alias moves only after the counts match and a sample of queries returns the same neighbours from both collections above a stated recall. A failed check throwsMigrationVerificationFailedwith the numbers in it and leaves the alias where it was: a tool that swaps because the copy finished without throwing is a tool that will one day point production at an empty collection.decorateTransportonKdrant(...)andKdrantGrpc(...), the hookkdrant-otelneeds and the place a caching or rate-limiting decorator of your own goes.ScrollBuilder.startAt, the id cursor a resumable job over a collection needs. It came out of M42 and is worth more than the migration.- A GraalVM native image (M40).
example-native-imageis compiled with--no-fallbackin CI and made to answer a real search against a real Qdrant, so the README's claim is a job that fails the day a dependency starts reflecting rather than a sentence in a table. Measured: 37 ms from process start to first search, in a 42 MB static binary. The comparison table quotes that instead of the word friendly. Building it settled the claim in the second of the two ways it could go. One thing does reflect: Ktor resolves a serializer from the response type at run time, and kotlinx-serialization answers by looking for the compiler-generated$$serializer, which a native image cannot find unless the class is registered.kdrant-transport-restnow ships that registration in its own jar, generated from the classes on the classpath rather than written by hand, so a model added tomorrow is in the file the same day and a consumer building a native image writes nothing.
- Every published module's POM description now names the platforms that module actually has, and
verifyPublishedDescriptionfails the build when one stops being true. The description Maven Central serves forkdrant-core:2.0.0ends with "Core module for RAG and embedding search on the JVM", which klibs.io was about to put next to badges reading iOS, macOS, Linux and Windows generated from the same artifact's own tooling metadata. It did not go stale by accident: everything else moved at2.0.0and the POM did not, because nothing reads it. The em dashes went with the correction. - A credential no longer requires TLS when the host is a loopback address. A key sent in the clear
across a network is a key someone else has; a request to
127.0.0.1never reaches a network. This only accepts configurations that were previously rejected, and it is what makes a local Qdrant with an API key work without a certificate. - Three signatures changed shape, and all three need a recompile rather than a jar swap.
Kdrant(...)andKdrantGrpc(...)gaineddecorateTransport, andKdrantConfiggainedbearerToken. Every parameter is optional and every2.0.0call site compiles unchanged, but a default parameter changes the signature Kotlin emits, so an application compiled against2.0.0that swaps in the2.1.0jar without rebuilding will not find them. This is the case STABILITY.md already describes for data classes, now stated for functions and constructors too.git diff v2.0.0 v2.1.0 -- '*/api/*.api'shows the seven removed lines, and nothing else was removed. KdrantException.Unauthorizedisopen, soForbiddencan extend it. Opening a class removes nothing a caller could use.kdrant-transport-rest's JVM classes are published askdrant-transport-rest-jvm, the same movekdrant-coremade at2.0.0. A Gradle build resolves the variant from the plain coordinate and changes nothing; a Maven build namingkdrant-transport-resthas to move to the-jvmone.
ScrollRequest.offsetwas documented as the id to start after. It is inclusive, which is what the paging code has always relied on and what Qdrant returns asnext_page_offset.- The count of operations Qdrant serves over HTTP only was eleven in five places and is fourteen.
It was written into a KDoc once and never counted, and from there it reached the README, this
changelog, the stability policy and the migration guide.
grep -o 'restOnly("[a-zA-Z]*")' | sort -u | wc -lsettles it, and the number now comes from that rather than from memory. STABILITY.mdsaid2.0.0broke nothing but the artifact layout. It broke two things: that, andScrollRequest/SearchRequestgaining ashardKeyparameter, which changed their generatedcopyandcomponentN. The upgrade section names both.
- The release workflow's linked-artifacts step can no longer fail a release. On the
v2.0.0tag it returned 404 for a digest that does have an attestation and took down a job whose jars were already published and attested. A metadata step that can undo a successful publish is worth less than the metadata, so it iscontinue-on-errorwith a per-artifact fallback; the run's log still says which records were not written. - The same step's artifact list now names
kdrant-transport-rest-jvm, and gainskdrant-otel,kdrant-migrate-jvmandkdrant-koog, which had been missing since the step shipped. - CI gained three jobs: the client contract from a
linuxX64and amacosArm64binary, and the GraalVM native image. The two native jobs setKDRANT_QDRANT_REQUIRED, which turns the contract's skip into a failure, because a job that was meant to run it and silently skipped would report green for having proven nothing.
2.0.0 - 2026-07-31
Tier 5, complete, and the release the transport seam was built for. kdrant-transport-grpc is an
opt-in gRPC engine behind the same QdrantClient, and kdrant-core compiles for the JVM and eight
Kotlin/Native targets. Adding a second engine changed no line of kdrant-core.
Two things make this a major. kdrant-core's JVM classes moved to kdrant-core-jvm, because the
module is multiplatform now: a Gradle build changes only the version number, a Maven build naming
kdrant-core has to move. And ScrollRequest and SearchRequest gained a shardKey parameter, which
changed their generated constructor and copy, so code that called copy() on either against a 1.x
jar has to be recompiled. Source stays compatible. The multiplatform migration itself changed no public
API at all. See STABILITY.md.
kdrant-koog(M37), a new module: a Koog document storage backed by Kdrant, so a Koog RAG agent can keep its documents in Qdrant. It implements Koog's search-side storage interfaces (WriteStorage,LookupStorage,SearchStorage,DeletionStorage) rather thanVectorStorageBackend, which has no search method: Koog's ownEmbeddingStorageranks by streaming every stored document out of the backend and scoring in memory, and doing that through a vector database would mean paying for an index and then pulling the whole collection over the network on every query. Here Qdrant runs the search. The module depends only on Koog's stablerag-base, not on therag-vectorbeta. Koog'snamespacebecomes a payload field and a filter, so one collection can hold several of them.- 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 shard; andcreateShardKey/deleteShardKeymanage custom sharding keys. The placement calls return once the transfer is accepted, not once it has finished, which the KDoc says rather than leaving it to be discovered. ShardKeyandshardKeyonsearchandscroll, so a query that concerns one region or one tenant reads that key's shards instead of all of them. A numeric key stays a number on the wire; quoting it would make Qdrant read it as a different key.ReplicaStatedecodes an unrecognized state from a newer Qdrant toUNKNOWNrather than failing the whole cluster-info response, the same toleranceCollectionStatusalready had.- Formula reranking and MMR (M35), scoped in M16 and not shipped with it.
formula(expression)rescores the candidates aprefetchproduced with arithmetic over their score and payload: multiply by a popularity field, add a bonus for points matching a condition, decay by recency or by distance. TheExpressionAST covers Qdrant's full operator set, including the three decay curves andgeo_distance.mmr(diversity)reranks a vector query for variety instead of letting ten results about the same thing crowd the top. Both are validated against Qdrant's published schema by the contract tests, and the bounds Qdrant documents (diversity in0..1, a positive decay scale, a midpoint in0..1) are checked where they are written rather than on the round trip. - Shard-scope snapshots (M36), deferred out of M20 when snapshots first shipped:
createShardSnapshot,listShardSnapshots,deleteShardSnapshot,recoverShardSnapshot, plus streamingdownloadShardSnapshotanduploadShardSnapshot. On a sharded collection the existing whole-collection snapshot is every shard at once, which on a large collection is the difference between a backup that fits in a window and one that does not. Shard ids come fromcollectionClusterInfo. kdrant-transport-grpc(M31), the opt-in gRPC engine.KdrantGrpc(host)returns the sameQdrantClientthe REST factory does, over Qdrant'sCollections,Points,SnapshotsandHealthservices 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.protofiles, vendored verbatim at v1.18.2, rather than taken fromio.qdrant:client. grpc-kotlin emits suspend functions andFlows, 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 (M31,
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. kdrant-coreis a Kotlin Multiplatform library (M25). It builds for the JVM and for eight Kotlin/Native targets:iosArm64,iosSimulatorArm64,iosX64,macosArm64,macosX64,linuxArm64,linuxX64andmingwX64. Kotlin/JS is deliberately not among them: there is no JS engine, so the target would ship models with nothing to send them over, and its test tooling is the only npm dependency graph this repository would have. The models, DSLs, error hierarchy and client logic were already free of the JVM, which is what the transport seam was for, so the migration moved sources intocommonMainand changed one declaration. The engines stay JVM-only, because Ktor CIO and grpc-java are.- A
commonTestsuite that runs on every target, covering the places a platform could actually differ: the hand-written serializers, the uint64 point id, integer payload values above 2^53, and the config's validation.
-
kdrant-core's artifact layout changed with the multiplatform move. Thekdrant-corecoordinate now carries Gradle module metadata and the JVM classes live inkdrant-core-jvm. A Gradle build resolves the right variant from the same coordinate and needs no change; a Maven build names the artifact directly and must move tokdrant-core-jvm, which the BOM now constrains as well. The migration changed no public API: the*.apidump is identical either side of it. -
The default dispatcher is platform-dependent, and is the one declaration the migration had to split. It stays
Dispatchers.IOon the JVM. On Kotlin/Native it isDispatchers.Default, because the coroutines library still keeps its native IO dispatcher internal. Passing your own dispatcher works as before, everywhere. -
Releases are built on macOS. Only a macOS host can compile the Apple targets, so a Linux runner would publish a release quietly missing its iOS and macOS klibs.
-
kdrant-core's-javadoc.jarholds Dokka's HTML output rather than Javadoc HTML: the Dokka Javadoc generator refuses a multiplatform project. Maven Central requires the jar to exist rather than to be Javadoc, and HTML is what a Kotlin reader wants. -
Fourteen
QdrantTransportoperations 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, the snapshot and storage-snapshot transfers, and the six shard-scope snapshot operations. On the gRPC engine each throws anUnsupportedOperationExceptionnaming 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 and including
1.2.0remain on GitHub Packages and are not withdrawn.
- Release notes are extracted from this file by the release workflow rather than written by hand. A release body composed separately is a second copy of what the changelog owns, and the two eventually disagree; derived from here it cannot. The workflow fails the release if the tag has no section.
- Every published jar is recorded as a linked artifact, so the repository's Packages panel names what it built and where it went. Metadata, not a distribution channel: nothing is hosted there.
- The set of artifacts the provenance attestation covers is derived from the build instead of listed in the workflow. A hardcoded list stops covering a module the day one is added and nothing goes red, which is what happened when the gRPC engine arrived.
1.2.0 - 2026-07-31
Tier 6, complete. The framework adapters honour metadata filters, deployment scripts get
ensureCollection, an ordered scroll and batchUpdate, observability ships instead of being
reachable by hand, the wire format is held to Qdrant's own schema by contract tests, and switching
from the official client has a guide and measured numbers behind it.
Upgrading is a recompile, not a jar swap. apiCheck reads this release as additive, but new members
on QdrantClient and QdrantTransport break a class that implemented them against 1.1.0, and the
fields added to CollectionInfo, ScrollRequest and Record change their generated copy. Source
stays compatible; see STABILITY.md.
-
Metadata-filter translation for the framework adapters (M26).
kdrant-spring-aiandkdrant-langchain4jused to throw on any filter expression, which meant a filtered RAG application was not the drop-in swap the modules advertised. Both now translate their framework's filter model into Kdrant's:Filter.Expression.toKdrantFilter()for Spring AI andFilter.toKdrantFilter()for LangChain4j, wired intosimilaritySearch,VectorStore.delete(Expression)andEmbeddingStore.search. Boolean chains flatten into a singlemust/shouldclause, comparisons pick Qdrant's numeric or RFC 3339rangevariant from the value's runtime type, and Spring AI'sIS NULLmaps tois_empty(which, unlike Qdrant'sis_null, also covers a missing key). A value Qdrant cannot express is rejected with anIllegalArgumentExceptionrather than dropped, so a filter never silently widens a result set. -
SearchBuilder.filter(Filter)andPrefetchBuilder.filter(Filter), plusQdrantClient.delete(name, selector, wait)— the entry points a translator needs to pass an already-built filter, alongside the existing DSL forms. -
ensureCollection(name) { ... }(M27): creates the collection if it is missing and otherwise checks that the one already there has the dense vector names, sizes and distances, and the sparse vector names, that were asked for. Returns whether it created the collection, absorbs a create that lost a race to another process, and fails loudly on a mismatch rather than leaving an application to discover the wrong vector size on its first upsert. Everything the server defaults (HNSW, optimizers, quantization) is deliberately not compared. -
The enriched
getCollectionread-back that check reads:CollectionInfo.config(vectors, sparse vectors, shard number, replication factor, on-disk payload) andCollectionInfo.payloadSchema. An index type a future Qdrant adds is kept as its wire string rather than failing the whole response. -
An ordered
scroll(M27):scroll("docs") { orderBy("ts", Direction.DESC) }, plusorderByDatetimefor RFC 3339 keys,startFromto resume a partly consumed pass, andRecord.orderValue. Qdrant returns no page cursor for an ordered scroll, so the client pages on the order value and drops the points a page repeats at the boundary; each point is still emitted exactly once. A scroll that cannot advance — more points tied on one order value than fit in a page — fails with a message saying so instead of silently truncating. -
batchUpdate(name, wait) { ... }(M27): one request applying an ordered, mixed sequence of point, vector and payload operations. Ordered but not transactional: a later operation sees the effect of an earlier one, but a failure part-way through leaves the earlier operations applied. -
ScrollBuilder.filter(Filter), matching the search builders. -
kdrant-micrometer(M28), a new module:configureClient = { kdrantMetrics(registry) }times every request askdrant.requests, tagged with the operation, HTTP method, status and outcome. The operation tag is the route template, not the URL — collection, field and snapshot names become placeholders, so a deployment with thousands of collections does not become thousands of time series. -
X-Request-Idcorrelation (M28):Kdrant(host, requestId = { ... })sets the header from the caller's own trace id, so a Kdrant call can be followed into Qdrant's logs. Off by default, since sending a new header on every request would change the bytes on the wire for everyone. -
Connection-pool settings on the REST engine factory (M28):
maxConnectionsPerRouteandkeepAliveTimeare parameters ofKdrant(...), not ofKdrantConfig, which stays transport-neutral. This is where the pool settings declined onKdrantConfigland. -
Contract tests against Qdrant's OpenAPI schema (M29). Every request body the REST engine builds is captured from a real client call and validated against the schema Qdrant publishes for that endpoint, with unknown properties treated as failures. Qdrant's document is vendored under
kdrant-transport-rest/src/test/resourcesand pinned to the version the CI matrix runs against, so refreshing it is how a wire change that would otherwise pass silently becomes a failing build. -
Kover coverage (M29), which the Kotlin 2.4 incompatibility had deferred.
./gradlew koverHtmlReportcovers the six published modules; CI runs it on JDK 17 and enforces a 75% line floor — a floor to catch a module arriving untested, not a number to inch towards. Current line coverage is 82.8%. -
SLSA build provenance on release (M29): the release workflow assembles the jars, attests them with
actions/attest-build-provenance, and only then publishes, so the attestation covers the exact files that reach Maven Central and GitHub Packages. -
A migration guide from
io.qdrant:client(M30), mapping the official client operation by operation, with the differences that actually bite: the port, theListenableFuture-to-suspendshift, protobuf builders against the DSL, and where the official client is still the right tool. -
A dispatchable
Benchmarksworkflow (M30) that runs the JMH harness against a chosen Qdrant image on a clean runner and uploads the results, and the first measured numbers from it:searchp50 1.97 ms / p99 5.40 ms,upsertp50 3.37 ms / p99 9.81 ms against Qdrantv1.18.2. Published with the conditions they were taken under, including the ones that make them a floor rather than a capacity figure: no network between client and server, a 1 000-point collection, and no concurrency. -
The design rationale in STABILITY.md (M30) now states what a
1.xupgrade actually guarantees:QdrantClientandQdrantTransportare interfaces to call rather than implement, and a field added to a public data class changes its generatedcopy, so a minor is a recompile rather than a jar swap.
Directionnow serializes as Qdrant's lowercaseasc/desc. It was only ever written through the hand-rolled query serializer, which spelled it correctly, so no shipped request was affected; the enum itself would have sentASCthe moment anything else serialized it.
- ktlint
12.1.2→14.2.0. Version 14 turns onclass-signatureandfunction-signature, which collapse a multi-line parameter list onto one line and push the supertype onto its own; both are disabled in.editorconfig, for the same reason the codebase pickedintellij_ideaoverktlint_officialin the first place. Two files were rewritten before the rules were turned off. - detekt's
LongParameterList.functionThresholdraised from 8 to 12. TheKdrant(...)factory is a settings surface likeKdrantConfig, where every parameter pastportis an independently defaulted option, so the two now get the same allowance.
1.1.0 - 2026-07-20
kdrant-spring-ainow targets Spring AI2.0(was1.0) andkdrant-spring-boot-starternow targets Spring Boot4.1(was3.4). Kdrant's own public API is unchanged (the*.apidumps are identical and all adapter tests pass against the new majors), but these two adapter modules now require the newer framework generation (Spring Framework 7 / Jakarta EE 11 for the starter). Applications still on Spring AI 1.x or Spring Boot 3.x should pin those modules to1.0.0until they upgrade.kdrant-coreandkdrant-transport-restare unaffected.kdrant-langchain4jnow builds against LangChain4j1.18.0(was1.0.0) — a backwards-compatible minor upgrade.
- Toolchain & tooling: Kotlin
2.4.10, Gradle9.6.1, kotest6.2.2, plus assorted minor/patch dependency bumps; CI actions run on Node 24. The Kotlin modules now compile withallWarningsAsErrors, so any compiler deprecation fails the build — keeping the code warning-clean across future dependency upgrades.
1.0.0 - 2026-07-20
Kdrant's 1.0: the REST client is feature-complete and its public API is now stable under Semantic
Versioning — see STABILITY.md. On top of 0.2.0 (M10–M18), this release adds M19–M24
(aliases, snapshots, service/analytics endpoints, granular transport & observability, a no-boxing hot
path, quality/CI hardening, the Spring Boot / Spring AI / LangChain4j integrations, and the catching
helper).
- Aliases (M19):
updateAliases { createAlias(collection, alias); deleteAlias(alias); renameAlias(from, to) }, applied by the server as one atomic batch — the primitive behind zero-downtime reindexing (build a new collection, then swap the alias in a single step). PluslistAliases()andlistCollectionAliases(name). - Service & health endpoints (M19):
healthz()/readyz()/livez()(Kubernetes-style probes that return aBooleanand never throw on a not-ready status),listCollections(),telemetry()andlistIssues()(raw JSON, since the shape is server-version-specific),clearIssues(), andmetrics()(Prometheus text-exposition format). - Analytics (M19):
facet(name, key, limit, exact) { filter }— distinct payload-value counts (a histogram over a key) — and the distance-matrix endpointssearchMatrixPairs(name) { sample; limit; using; filter }andsearchMatrixOffsets(...)(explicit edge-list and sparse-coordinate forms) for clustering/visualization. - Snapshots & backup/restore (M20):
createSnapshot/listSnapshots/deleteSnapshot/recoverSnapshot(location, priority, checksum)for a collection, pluscreateStorageSnapshot/listStorageSnapshots/deleteStorageSnapshotfor the whole storage. Binary transfer is streamed, so a multi-GB backup is never buffered in memory:downloadSnapshot(...)/downloadStorageSnapshot(...)return a coldFlow<ByteArray>, anduploadSnapshot(name, data: Flow<ByteArray>, ...)streams a snapshot file back as a multipart upload.SnapshotPriority(NO_SYNC/SNAPSHOT/REPLICA) sets the source of truth when recovering into a replicated collection. Note: unlike the mutationwaitflags, snapshotwaitdefaults totrue, matching the Qdrant server default. - Granular transport & observability (M21): a
configureClientescape hatch on theKdrant(...)factory (anHttpClientConfig<*>hook to install your own plugins — metrics, tracing — tune the CIO engine, or override any default);connectTimeout/socketTimeouton the client config; and optional request/response logging vialogLevel = LogLevel.…, which always redacts theapi-keyheader so the key never reaches the logs. - Streaming ingest (M21):
upsert(name, points: Flow<PointStruct>)andupsert(name, points: Sequence<PointStruct>)— ingest a large or unbounded source without materializing it all in memory; the engine chunks it by the configured batch size (sequential, not atomic across chunks, like the DSLupsert). - Ergonomics (M24):
catching { … }— a coroutine-saferunCatchingthat returnsResult<T>but re-throwsCancellationExceptioninstead of trapping it. The exception-based API stays the primary style. - No-boxing hot path (M21): the DSL
vector(f1, f2, …)/vector(*floatArray)(upsert) andquery(f1, f2, …)(search) now keep the values in aFloatArrayand serialize it directly, avoiding a boxedFloatper element (VectorData.DenseArray/QueryInterface.VectorArray). Upsert batching is byte-aware: a batch is bounded by both the point count and a serialized-size cap (maxUpsertBytes, default ~30 MiB), so Qdrant's ~32 MiB REST limit is respected even for high-dimensional vectors.
0.2.0 - 2026-07-20
- delete-by-filter data loss: a delete whose filter clauses were all empty (e.g.
delete(c) { must { } }, ormust { if (cond) … }wherecondis false at runtime) is no longer sent as a match-all filter that would delete every point in the collection. Empty clause blocks now normalize away, and delete-by-filter rejects an all-empty filter before issuing any request. collectionExistsnow returnsfalseon a404instead of throwing, matching its documented contract.KdrantException.CollectionNotFoundnow carries the server's error message when the server provides one.
- The client rejects a configuration that sets an
apiKeywithoutuseTls, so an API key is never sent over plaintext HTTP.
- Collection config tuning:
updateCollection { optimizers = …; hnsw = …; quantization = … }(PATCH), andoptimizers/quantization(QuantizationConfig.Scalar/.Binary) oncreateCollection. - Payload field indexes (
createPayloadIndex(field, PayloadSchemaType.KEYWORD)/deletePayloadIndex), so filtering on a field scales instead of doing a full scan; and payload mutationssetPayload/overwritePayload/deletePayload/clearPayloadover a points-or-filter selector. - Vector mutations:
updateVectors(write new vectors to existing points, keeping payload) anddeleteVectors(remove named vectors from the selected points). - Advanced retrieval queries on
search:recommend { positive(...); negative(...); strategy = ... },discover { target(...); context(...) }, andcontext { pair(...) }. Examples (VectorInput) accept a dense/sparse vector or a point id. - Batch and grouped search:
searchBatch { search { } … }(several searches in one round-trip, hits per search) andsearchGroups(groupBy = …) { }returningList<PointGroup>. - Sparse & multi-vectors:
VectorData.Sparse/MultiDense,sparseVector(name) { modifier = Modifier.IDF }and per-vectormultivectorincreateCollection, andquerySparse(...)/queryMulti(...)— enabling true dense+sparse hybrid search combined with M14 fusion. Response decoding now degrades an unknown vector shape toVectorData.Rawinstead of failing the whole response. - Modern
/points/querysearch: a polymorphicquery(nearest by vector or by point id,orderBy,sample), nestableprefetch { }sub-requests, and hybrid-search fusion (rrf(k, weights)/dbsf()), pluslookupFromfor cross-collection id lookups. The previousquery(vector)call is unchanged. - Typed payload access:
kdrantJson(public defaultJson),ScoredPoint.payloadAs<T>()/Record.payloadAs<T>(), andQdrantClient.searchAs<T>(): List<Hit<T>>to decode hit payloads straight into your own types. - Collection conveniences:
getCollectionOrNull, race-tolerantcreateCollectionIfNotExists(...): Boolean, and acreateCollection(name, size, distance = COSINE)shorthand. PayloadBuilderindex-assignment sugar:payload["key"] = value(operator set, acceptsnull).- Automatic retries with exponential backoff + jitter for transient failures (HTTP 429/502/503/504 and
transient I/O errors), honoring the server's
Retry-Afterheader. Tunable viamaxRetries,retryBaseDelay, andretryMaxDelayon the client config (maxRetries = 0disables retries). - Finer error taxonomy:
KdrantException.RateLimited(429, carryingRetry-After),ServiceUnavailable(503),ServerError(other 5xx), andAlreadyExists(409). - Client-side validation of collection parameters: vector
size,shardNumber, andreplicationFactormust be positive, with error messages that echo the received value. kdrant-bom— a Bill of Materials module to keepkdrant-coreandkdrant-transport-reston one aligned version.
- Server errors (HTTP 5xx other than 503) now surface as
KdrantException.ServerErrorinstead ofTransport, which is now reserved for connection-level I/O failures. HTTP408maps toTimeoutand409toAlreadyExists. local.propertiesis no longer tracked in version control.
0.1.0 - 2026-07-10
- Coroutine-first
QdrantClientwith a pluggableQdrantTransportseam and a default REST/Ktor engine. - Collection operations:
createCollection(DSL) anddeleteCollection. upsertDSL supporting dense and named vectors, heterogeneous payloads, and automatic batching under the REST request-size limit.search(nearest-vector query over the unified query API) with a DSL for the query vector, filter, limit, payload projection, and search params.scrollexposed as a coldFlow<Record>that transparently follows the pagination cursor.deleteby point ids or by filter.- Collection introspection:
collectionExistsandgetCollection(status and point counts). count(optionally filtered) andretrievepoints by id.- Complete filter DSL:
must/should/mustNot/minShouldwith every Qdrant condition type (match/any/except/text, numeric and datetime ranges,values_count, geo box/radius/polygon,is_empty/is_null,has_id,has_vector, per-elementnested, and recursive sub-filters). - Typed error hierarchy
KdrantException.