diff --git a/CHANGELOG.md b/CHANGELOG.md index eb9d1f2..b95c719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` @@ -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. @@ -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 @@ -101,7 +101,7 @@ API at all. See [STABILITY.md](STABILITY.md#upgrading-from-1-x). 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 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index efe36ae..0303085 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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`. diff --git a/README.md b/README.md index d9d7a57..1512188 100644 --- a/README.md +++ b/README.md @@ -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 @@ -47,7 +47,7 @@ your own embedding model; Kdrant does not generate embeddings. > **See it end to end.** [`example-rag`](example-rag/) is a small runnable Retrieval-Augmented-Generation > service (ingest, embed, store, retrieve) built on Kdrant, with a `docker-compose` for Qdrant. -> **Status — `2.0`, stable.** Both engines are feature-complete: collections, `upsert`, the modern +> **Status: `2.0`, stable.** Both engines are feature-complete: collections, `upsert`, the modern > `/points/query` search (nearest, hybrid fusion, recommend/discover/context, batch, groups), sparse > and multi-vectors, `scroll`, payload and vector management, aliases, snapshots, cluster and sharding, > service/analytics endpoints, resilient retries, and the full filter DSL, plus Spring Boot, Spring AI, @@ -135,9 +135,9 @@ 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 eleven operations over HTTP only: telemetry, Prometheus metrics, the +issues endpoint, snapshot recovery, snapshot download and upload, and the shard-scope snapshots. 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 @@ -337,10 +337,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 @@ -353,7 +353,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 Netty — checked 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 @@ -371,8 +371,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 diff --git a/STABILITY.md b/STABILITY.md index bcec11e..0b3f253 100644 --- a/STABILITY.md +++ b/STABILITY.md @@ -1,6 +1,6 @@ # 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). @@ -8,9 +8,9 @@ project is going) and the [CHANGELOG](CHANGELOG.md) (what has already shipped). 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" @@ -18,8 +18,9 @@ The public API is exactly what the [binary-compatibility-validator](https://gith 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 @@ -28,8 +29,8 @@ 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 only — telemetry, Prometheus metrics, the issues endpoint, `recoverSnapshot`, +The two engines are held to the same behaviour, with one stated exception. Qdrant serves eleven +operations over HTTP only: telemetry, Prometheus metrics, the issues endpoint, `recoverSnapshot`, snapshot download and upload, and the five 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 @@ -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 @@ -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. @@ -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: diff --git a/docs/migrating-from-qdrant-client.md b/docs/migrating-from-qdrant-client.md index 41cc187..c63eba3 100644 --- a/docs/migrating-from-qdrant-client.md +++ b/docs/migrating-from-qdrant-client.md @@ -16,9 +16,9 @@ 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, +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 +shard-scope snapshots. 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). @@ -41,13 +41,13 @@ dependencies { ## Creating a client ```java -// io.qdrant:client — gRPC, port 6334 +// io.qdrant:client, gRPC on port 6334 QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder("localhost").withApiKey("").build()); ``` ```kotlin -// Kdrant — REST, port 6333 +// Kdrant, REST on port 6333 val qdrant = Kdrant(host = "localhost", port = 6333) { apiKey = "" useTls = true @@ -204,7 +204,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)` |