Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Tier 5, complete, and the release the transport seam was built for. `kdrant-tran
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
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`
Expand Down Expand Up @@ -63,9 +63,9 @@ API at all. See [STABILITY.md](STABILITY.md#upgrading-from-1-x).
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 `.proto` files, vendored verbatim at v1.18.2, rather than
taken from `io.qdrant:client`grpc-kotlin emits suspend functions and `Flow`s, 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.
taken from `io.qdrant:client`. grpc-kotlin emits suspend functions and `Flow`s, 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.
Expand All @@ -74,7 +74,7 @@ API at all. See [STABILITY.md](STABILITY.md#upgrading-from-1-x).
`linuxArm64`, `linuxX64` and `mingwX64`. 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 — that is what the transport seam was for so the migration moved
logic were already free of the JVM, which is what the transport seam was for, so the migration moved
sources into `commonMain` and changed one declaration. The engines stay JVM-only, because Ktor CIO and
grpc-java are.
- A `commonTest` suite that runs on every target, covering the places a platform could actually
Expand All @@ -97,11 +97,12 @@ API at all. See [STABILITY.md](STABILITY.md#upgrading-from-1-x).
- `kdrant-core`'s `-javadoc.jar` holds 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.
- Eleven `QdrantTransport` operations 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`, snapshot download and upload, and the five shard-scope snapshot operations. On the
- Fourteen `QdrantTransport` operations 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 an `UnsupportedOperationException` naming the operation and pointing at REST,
rather than degrading quietly — a snapshot download that returns nothing is a backup that does not
rather than degrading quietly. A snapshot download that returns nothing is a backup that does not
exist. The REST engine is unchanged.


Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Thanks for your interest in contributing.
- Keep changes focused and covered by tests.
- Follow the existing style (`.editorconfig`: 4-space indentation, 120-column lines, no wildcard
imports).
- If you change the public API, run `./gradlew apiDump` and commit the updated `*.api` files CI
- If you change the public API, run `./gradlew apiDump` and commit the updated `*.api` files. CI
runs `apiCheck` and will fail on an untracked API change.
- Add an entry under `[Unreleased]` in `CHANGELOG.md`.

Expand Down
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
[![Website](https://img.shields.io/badge/website-nacodestudios.it-232B45?labelColor=0B0E17)](https://nacodestudios.it/en/project/kdrant)

Qdrant's official JVM client is built for Java: every call returns a `ListenableFuture`, requests are
assembled with protobuf builders, and the wire is decided for you gRPC, with a shaded Netty on your
assembled with protobuf builders, and the wire is decided for you: gRPC, with a shaded Netty on your
classpath whether you needed the throughput or not. Kdrant is the client you'd actually want to write
Kotlin against: `suspend` functions, a type-safe DSL, `kotlinx-serialization` models, and a wire you
pick. The default engine is pure-Kotlin REST and pulls in no gRPC, no protobuf and no Netty; the gRPC
Expand Down Expand Up @@ -135,9 +135,10 @@ val qdrant: QdrantClient =

Every example below reads the same either way, because the API above the wire is the same API. Two
differences are worth knowing before you switch. The **port** is 6334, not 6333, and nothing rewrites
it for you. And Qdrant serves eleven operations over HTTP only — telemetry, Prometheus metrics, the
issues endpoint, snapshot recovery, snapshot download and upload, and the shard-scope snapshots — which
the gRPC engine refuses by name rather than degrading quietly.
it for you. And Qdrant serves fourteen operations over HTTP only: telemetry, Prometheus metrics, the
two issues calls, snapshot recovery, the snapshot and storage-snapshot transfers, and the six
shard-scope snapshot operations. The gRPC engine refuses each of them by name rather than degrading
quietly.

`kdrant-core` is a Kotlin Multiplatform library and publishes one artifact per target. A Gradle build
resolves the right one from the `kdrant-core` coordinate and needs no change. A Maven build names the
Expand Down Expand Up @@ -337,10 +338,10 @@ kdrant-core QdrantClient, models, DSLs, KdrantException, QdrantTranspor
+-- kdrant-transport-grpc grpc-kotlin KdrantGrpc(host) JVM
```

That is the arrangement the second engine tested. Adding gRPC changed **no line of `kdrant-core`**, and
the same behavioural suite runs against both engines against the same Qdrant, so the choice between them
is a footprint and throughput decision rather than a feature one — except for the operations Qdrant
serves over HTTP only, which the gRPC engine names.
That is the arrangement the second engine tested. Adding gRPC changed no line of `kdrant-core`, and the
same behavioural suite runs against both engines against the same Qdrant, so the choice between them is
a footprint and throughput decision rather than a feature one. The exception is the set of operations
Qdrant serves over HTTP only, which the gRPC engine names.

It is also why the core compiles for iOS, macOS, Linux and Windows: code that never knew there was a
wire has nothing platform-specific to port. The engines stay JVM-only, because Ktor CIO and grpc-java
Expand All @@ -353,7 +354,7 @@ the target would ship a DSL with nothing to send.
**Shipped (`2.0.0`).** Tier 5 closes the two things the transport seam existed to make possible.
`kdrant-transport-grpc` is an opt-in gRPC engine behind the same `QdrantClient`, generated from Qdrant's
own `.proto` files rather than wrapping the official client, so a REST build still resolves no gRPC, no
protobuf and no Nettychecked on every build, not asserted. And `kdrant-core` moved to Kotlin
protobuf and no Netty, checked on every build rather than asserted. And `kdrant-core` moved to Kotlin
Multiplatform: the JVM plus eight Kotlin/Native targets, a migration that changed no public API at
all. Both engines are now held to one shared behavioural suite against a real Qdrant. The major bump is
for the artifact layout, not the API: `kdrant-core`'s JVM classes moved to `kdrant-core-jvm`, which a
Expand All @@ -371,8 +372,8 @@ engine, and typed-payload DX. The [CHANGELOG](CHANGELOG.md) has the version-by-v

**Next.** Nothing is claimed yet; the board is where it gets decided.

The plan lives on the [Kdrant board](https://github.com/orgs/NaCode-Studios/projects/4) one item per milestone, each with its
exit criterion — and every tier is a [milestone](https://github.com/NaCode-Studios/Kdrant/milestones) in this repository. See
The plan lives on the [Kdrant board](https://github.com/orgs/NaCode-Studios/projects/4), one item per milestone, each with its
exit criterion. Every tier is a [milestone](https://github.com/NaCode-Studios/Kdrant/milestones) in this repository. See
[STABILITY.md](STABILITY.md) for the versioning and stability policy.

## Building and testing
Expand Down
37 changes: 19 additions & 18 deletions STABILITY.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
# Stability & versioning

This document is the written stability contract for Kdrant what "stable" means, what changes are
This document is the written stability contract for Kdrant: what "stable" means, what changes are
allowed in which releases, and what a major bump costs you. It complements the [board](https://github.com/orgs/NaCode-Studios/projects/4) (where the
project is going) and the [CHANGELOG](CHANGELOG.md) (what has already shipped).

## Semantic Versioning

Kdrant follows [Semantic Versioning 2.0.0](https://semver.org). Given `MAJOR.MINOR.PATCH`:

- **MAJOR** incompatible public-API changes.
- **MINOR** backwards-compatible additions (new operations, new optional parameters, new overloads).
- **PATCH** backwards-compatible bug fixes.
- **MAJOR**: incompatible public-API changes.
- **MINOR**: backwards-compatible additions (new operations, new optional parameters, new overloads).
- **PATCH**: backwards-compatible bug fixes.

### What counts as "public API"

The public API is exactly what the [binary-compatibility-validator](https://github.com/Kotlin/binary-compatibility-validator)
tracks in the committed `*.api` files (`kdrant-core/api`, both transport engines, and the ecosystem
modules; the gRPC module's generated protobuf and stub classes are excluded, because their surface is
Qdrant's to change and not ours to promise). Every public/protected symbol is in those dumps; `./gradlew apiCheck` fails the build on any
untracked change, so **API breakage is never silent**. Anything not in the `*.api` files — `internal`
declarations, or symbols annotated `@InternalKdrantApi` — is not public API and may change at any time.
untracked change, so API breakage is never silent. Anything not in the `*.api` files, meaning
`internal` declarations and symbols annotated `@InternalKdrantApi`, is not public API and may change at
any time.

The **wire behaviour** of an engine (the requests it sends and the responses it parses) is also part of
the contract: a change that alters what goes on the wire for an existing operation is treated as a
Expand All @@ -28,9 +29,9 @@ tests validate every request body the REST engine builds against Qdrant's own Op
to the version the CI matrix runs against, and a shared behavioural suite runs both engines against a
real Qdrant, so a wire change is a failing build rather than a silent difference.

**The two engines are held to the same behaviour, with one stated exception.** Qdrant serves eleven
operations over HTTP onlytelemetry, Prometheus metrics, the issues endpoint, `recoverSnapshot`,
snapshot download and upload, and the five shard-scope snapshot operations. On the gRPC engine each
The two engines are held to the same behaviour, with one stated exception. Qdrant serves fourteen
operations over HTTP only: telemetry, Prometheus metrics, the two issues calls, `recoverSnapshot`, the
snapshot and storage-snapshot transfers, and the six shard-scope snapshot operations. On the gRPC engine each
throws an `UnsupportedOperationException` naming itself and naming REST. That list is part of this
contract: an operation leaving it is an additive change, and an operation joining it would be a
breaking one.
Expand All @@ -40,14 +41,14 @@ breaking one.
Two Kotlin details are additive to `apiCheck` but not binary-compatible for every caller, and it is
better to say so than to discover it later.

**`QdrantClient` and `QdrantTransport` are interfaces you call, not interfaces you implement.** Kdrant
`QdrantClient` and `QdrantTransport` are interfaces you call, not interfaces you implement. Kdrant
adds operations to both as Qdrant grows, and a new member on a Kotlin interface breaks a class that
implemented it against an older jar. If you need a decorator, delegate to the interface (`by transport`)
so the compiler tells you what a new release added; if you need a stub in a test, generate it. A
third-party wire engine is a supported use of `QdrantTransport`, but it is a use that recompiles against
each minor.

**Adding a field to a public `data class` changes its generated `copy` and `componentN`.** New response
Adding a field to a public `data class` changes its generated `copy` and `componentN`. New response
fields arrive as Qdrant returns more, and while the constructor keeps its defaults and source keeps
compiling, code that called `copy()` against an older jar needs recompiling. Kdrant does not add fields
gratuitously and each one is listed in the [CHANGELOG](CHANGELOG.md), but a minor upgrade is a
Expand All @@ -68,22 +69,22 @@ Within a major version:

## Upgrading from `1.x`

`2.0.0` breaks two things, and **source compatibility is not one of them**: every `1.x` call site
compiles unchanged.
`2.0.0` breaks two things, and source compatibility is not one of them: every `1.x` call site compiles
unchanged.

**Where `kdrant-core`'s JVM classes are published.** The module is Kotlin Multiplatform now, so its own
The first is where `kdrant-core`'s JVM classes are published. The module is Kotlin Multiplatform now, so its own
coordinate carries Gradle module metadata and the classes live in `kdrant-core-jvm`. Gradle reads that
metadata and resolves the variant, so a Gradle build changes nothing but the version number. Maven does
not read it, so a Maven build naming `kdrant-core` gets no classes and must move to `kdrant-core-jvm`.
Depending on `kdrant-transport-rest` or `kdrant-transport-grpc`, which is what the README recommends, is
unaffected either way.

**`ScrollRequest` and `SearchRequest` gained a `shardKey` parameter.** That is the case
The second is that `ScrollRequest` and `SearchRequest` gained a `shardKey` parameter. That is the case
[above](#what-may-still-change-in-a-minor) arriving for real: the constructor keeps its defaults and
source keeps compiling, but the generated `copy` and `componentN` changed, so code that called `copy()`
on either type against a `1.x` jar has to be recompiled. Nothing else in the `*.api` dump was removed.

**The multiplatform migration itself changed no public API.** The dump is identical either side of it,
The multiplatform migration itself changed no public API. The dump is identical either side of it,
which is worth stating because it is the part that sounds like it should have broken something. What
broke is the two lines above.

Expand All @@ -92,10 +93,10 @@ formula and MMR reranking, shard-scope snapshots and the Koog module are new ope

## Java interoperability

Kdrant is deliberately **Kotlin-coroutine-first** — that is the wedge (see the [README](README.md)). The
Kdrant is deliberately Kotlin-coroutine-first, which is the wedge (see the [README](README.md)). The
public API is `suspend` functions and `Flow`s, which are callable from Java but not idiomatic there.

**No bundled `CompletableFuture` facade.** Mirroring ~40 suspend operations into a
There is no bundled `CompletableFuture` facade. Mirroring ~40 suspend operations into a
blocking or future-returning Java API is a large, duplicated surface to maintain, and it is not the audience
Kdrant optimises for. Java callers who need it should bridge with the standard tools:

Expand Down
13 changes: 7 additions & 6 deletions docs/migrating-from-qdrant-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ same API. What that engine cannot do is listed under [What this is not](#what-th

## What this is not

The gRPC engine is not the whole of Qdrant. Eleven operations are served over HTTP only — telemetry,
Prometheus metrics, the issues endpoint, snapshot recovery, snapshot download and upload, and the
shard-scope snapshots — and on the gRPC engine each throws rather than pretending. If you need any of
The gRPC engine is not the whole of Qdrant. Fourteen operations are served over HTTP only: telemetry,
Prometheus metrics, the two issues calls, snapshot recovery, the snapshot and storage-snapshot
transfers, and the six shard-scope snapshot operations. On the gRPC engine each of them throws rather
than pretending. If you need any of
them, use the REST engine for the whole client or for that one call. The footprint trade-off between
the two is set out in the [README](../README.md#footprint-vs-the-official-client).

Expand All @@ -41,13 +42,13 @@ dependencies {
## Creating a client

```java
// io.qdrant:clientgRPC, port 6334
// io.qdrant:client, gRPC on port 6334
QdrantClient client =
new QdrantClient(QdrantGrpcClient.newBuilder("localhost").withApiKey("<apikey>").build());
```

```kotlin
// KdrantREST, port 6333
// Kdrant, REST on port 6333
val qdrant = Kdrant(host = "localhost", port = 6333) {
apiKey = "<apikey>"
useTls = true
Expand Down Expand Up @@ -204,7 +205,7 @@ backoff, honouring `Retry-After`; what surfaces is the failure that survived the
| `io.qdrant:client` | Kdrant |
| --- | --- |
| `createCollectionAsync(name, params)` | `createCollection(name) { vector { … } }` |
| | `ensureCollection(name) { vector { … } }` |
| no equivalent | `ensureCollection(name) { vector { … } }` |
| `deleteCollectionAsync(name)` | `deleteCollection(name)` |
| `collectionExistsAsync(name)` | `collectionExists(name)` |
| `getCollectionInfoAsync(name)` | `getCollection(name)` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ import kotlin.random.Random
* services.
*
* **The seam is wider than the protocol.** `QdrantTransport` was shaped by Qdrant's REST API, and
* eleven of its operations have no gRPC equivalent: telemetry, Prometheus metrics, the issues
* endpoint, snapshot recovery, snapshot download and upload, and the whole shard-scope snapshot group.
* fourteen of its operations have no gRPC equivalent: telemetry, Prometheus metrics, the two issues
* calls, snapshot recovery, the snapshot and storage-snapshot transfers, and the six shard-scope
* snapshot operations.
* They are not silently degraded here. Each throws an [UnsupportedOperationException] naming the
* operation and pointing at the REST engine, because a snapshot download that quietly returns nothing
* is a backup that quietly does not exist. A `KdrantException` would have been the wrong type: nothing
Expand Down