v1.2.0
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.
Added
-
Metadata-filter translation for the framework adapters (M26).
kdrant-spring-aiandkdrant-langchain4j
used 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 3339range
variant 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 an
IllegalArgumentExceptionrather than dropped, so a filter never silently widens a result set. -
SearchBuilder.filter(Filter)andPrefetchBuilder.filter(Filter), plus
QdrantClient.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) }, plusorderByDatetime
for 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):
maxConnectionsPerRouteandkeepAliveTime
are 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 koverHtmlReport
covers 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, the
ListenableFuture-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.
Fixed
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.
Internal
- 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_ideaover
ktlint_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.
Install
dependencies {
implementation("io.github.nacode-studios:kdrant-transport-rest:1.2.0")
}Full changelog: https://github.com/NaCode-Studios/Kdrant/blob/v1.2.0/CHANGELOG.md