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
4 changes: 3 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ ktlint_standard_statement-wrapping = disabled
ktlint_standard_if-else-bracing = disabled
ktlint_standard_argument-list-wrapping = disabled

[**/src/test/**/*.kt]
[**/src/{test,commonTest,jvmTest,jsTest,nativeTest}/**/*.kt]
# Test fixtures (long inline JSON assertions, verbose set-up) get a little more room than production code.
# The multiplatform source-set names are listed alongside src/test: kdrant-core's tests live in
# commonTest and jvmTest, and without them the tests there would be held to the production limit.
max_line_length = 140

[*.{yml,yaml,json,toml}]
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,33 @@ jobs:
**/build/test-results/
retention-days: 7

# The Linux build above compiles kdrant-core for the JVM, JS, Linux and Windows, and skips the Apple
# targets because no Linux host can build them. Without this job an iOS or macOS break would first be
# seen at release time, on the one runner that does compile them.
apple-targets:
name: Apple targets (kdrant-core)
runs-on: macos-latest
steps:
- uses: actions/checkout@v7

- name: Set up JDK 17
uses: actions/setup-java@v5
with:
java-version: "17"
distribution: temurin

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6

- name: Compile and test the Apple targets
run: >
./gradlew
:kdrant-core:compileKotlinIosArm64
:kdrant-core:compileKotlinIosX64
:kdrant-core:iosSimulatorArm64Test
:kdrant-core:macosArm64Test
--no-daemon --stacktrace

qdrant-compat:
name: Integration (${{ matrix.qdrant }})
runs-on: ubuntu-latest
Expand Down
23 changes: 15 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ permissions:

jobs:
publish:
runs-on: ubuntu-latest
# macOS, because kdrant-core is multiplatform and only a macOS host can compile the Apple targets.
# A Linux runner would silently skip them and publish a release missing its iOS and macOS klibs.
runs-on: macos-latest
steps:
- uses: actions/checkout@v7

Expand All @@ -36,6 +38,7 @@ jobs:
with:
subject-path: |
kdrant-core/build/libs/*.jar
kdrant-transport-grpc/build/libs/*.jar
kdrant-transport-rest/build/libs/*.jar
kdrant-spring-boot-starter/build/libs/*.jar
kdrant-spring-ai/build/libs/*.jar
Expand Down Expand Up @@ -64,18 +67,22 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
for module in kdrant-core kdrant-transport-rest kdrant-spring-boot-starter \
kdrant-spring-ai kdrant-langchain4j kdrant-micrometer; do
jar="$module/build/libs/$module-$VERSION.jar"
# kdrant-core is multiplatform, so its JVM jar is published as kdrant-core-jvm and the
# kdrant-core coordinate carries only Gradle metadata. The record names the artifact that
# exists; the digest has to match a file, not a coordinate.
for artifact in kdrant-core-jvm kdrant-transport-rest kdrant-transport-grpc \
kdrant-spring-boot-starter kdrant-spring-ai kdrant-langchain4j kdrant-micrometer; do
module="${artifact%-jvm}"
jar="$module/build/libs/$artifact-$VERSION.jar"
[ -f "$jar" ] || { echo "::error::$jar not found"; exit 1; }
digest="sha256:$(sha256sum "$jar" | cut -d' ' -f1)"
echo "recording $module $VERSION $digest"
digest="sha256:$(shasum -a 256 "$jar" | cut -d' ' -f1)"
echo "recording $artifact $VERSION $digest"
gh api -X POST "/orgs/${GITHUB_REPOSITORY_OWNER}/artifacts/metadata/storage-record" \
-f name="$module" \
-f name="$artifact" \
-f version="$VERSION" \
-f digest="$digest" \
-f registry_url="https://repo1.maven.org/maven2/" \
-f artifact_url="https://repo1.maven.org/maven2/io/github/nacode-studios/$module/$VERSION/$module-$VERSION.jar" \
-f artifact_url="https://repo1.maven.org/maven2/io/github/nacode-studios/$artifact/$VERSION/$artifact-$VERSION.jar" \
--silent
done

Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,34 @@ All notable changes to this project are documented in this file. The format is b
- Both engines are held to one **shared client contract** (`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-core` is a Kotlin Multiplatform library** (M25). It builds for the JVM and for eight
Kotlin/Native targets: `iosArm64`, `iosSimulatorArm64`, `iosX64`, `macosArm64`, `macosX64`,
`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
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
differ: the hand-written serializers, the uint64 point id, integer payload values above 2^53, and the
config's validation.

### Changed

- **`kdrant-core`'s artifact layout changed with the multiplatform move.** The `kdrant-core` coordinate
now carries Gradle module metadata and the JVM classes live in `kdrant-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 to `kdrant-core-jvm`, which the BOM now constrains as well. The JVM
public API is unchanged, byte for byte — `apiCheck` reports no diff.
- The default dispatcher is platform-dependent, and is the one declaration the migration had to split.
It stays `Dispatchers.IO` on the JVM. On Kotlin/Native it is `Dispatchers.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.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
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,13 @@ dependencies {
}
```

`kdrant-transport-rest` brings in `kdrant-core` transitively; it is the only dependency you add.
`kdrant-transport-rest` brings in `kdrant-core` transitively; it is the only dependency you add. To use
the gRPC engine instead, depend on `kdrant-transport-grpc` and build the client with `KdrantGrpc(host)`;
nothing else in this README changes, because the API above the wire is the same API.

`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
artifact directly and wants `kdrant-core-jvm`. The engines and adapters are JVM-only and are unaffected.

You also need a running Qdrant. For local development:

Expand Down Expand Up @@ -282,6 +288,13 @@ Three modules keep protocol concerns out of the public API:
| `kdrant-transport-rest` | The default REST engine (Ktor CIO) implementing `QdrantTransport`, plus the `Kdrant(...)` factory. |
| `kdrant-transport-grpc` | The opt-in gRPC engine over Qdrant's own protobuf services, plus the `KdrantGrpc(...)` factory. Depend on it only if you want it. |

`kdrant-core` builds for the JVM and for eight Kotlin/Native targets: iOS, macOS, Linux and Windows.
That is what the transport seam bought — the models, DSLs and client logic never knew about a wire, so
they compile anywhere Kotlin does. The engines stay JVM-only, because Ktor CIO and grpc-java are, so a
multiplatform consumer today shares its models and its query building and supplies its own transport.
Kotlin/JS is left out for that reason rather than an accident of effort: with no JS engine the target
would ship a DSL with nothing to send.

The DSLs and client logic live in `kdrant-core` and are independent of the protocol; only an engine
module knows about a wire. Both engines are held to the same behavioural test suite, so the choice is
a footprint and throughput decision rather than a feature one — with the exception of the operations
Expand All @@ -305,8 +318,10 @@ from `1.1.0` is a recompile rather than a jar swap — see
[STABILITY.md](STABILITY.md#what-may-still-change-in-a-minor); the [CHANGELOG](CHANGELOG.md) has the
version-by-version detail.

**Merged, unreleased.** The opt-in gRPC engine, `kdrant-transport-grpc`. It is in `main` and has not
shipped; see the [CHANGELOG](CHANGELOG.md#unreleased) for what it changes.
**Merged, unreleased.** The opt-in gRPC engine, `kdrant-transport-grpc`, and the move of `kdrant-core`
to Kotlin Multiplatform. Both are in `main` and neither has shipped; see the
[CHANGELOG](CHANGELOG.md#unreleased) for what they change and what the multiplatform move does to
`kdrant-core`'s artifact coordinates.

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
Expand Down
6 changes: 6 additions & 0 deletions STABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ so the compiler tells you what a new release added; if you need a stub in a test
third-party wire engine is a supported use of `QdrantTransport`, but it is a use that recompiles against
each minor.

**A multiplatform artifact is named per target.** `kdrant-core` publishes Gradle module metadata under
its own coordinate and the JVM classes under `kdrant-core-jvm`. Gradle reads the metadata and resolves
the variant; Maven does not, and a Maven build naming `kdrant-core` gets no classes. That is a
coordinate change rather than an API change — the JVM public API is unchanged — but it is the kind that
belongs in a major, which is where it landed.

**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
Expand Down
20 changes: 18 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.dokka)
alias(libs.plugins.dokka.javadoc) apply false
Expand Down Expand Up @@ -73,12 +74,27 @@ configure(
}

// Treat every Kotlin compiler warning — deprecations included — as a build error, so these
// modules stay warning-clean across dependency and toolchain upgrades.
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
// modules stay warning-clean across dependency and toolchain upgrades. Every compilation task
// rather than only the JVM one: kdrant-core also compiles for JS and for nine native targets, and
// a warning that only appears there would otherwise never fail a build.
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {
compilerOptions {
allWarningsAsErrors.set(true)
}
}

// Detekt reads declared source directories rather than the Kotlin source sets, and its defaults
// name src/main and src/test, which a multiplatform module does not have.
extensions.configure<io.gitlab.arturbosch.detekt.extensions.DetektExtension> {
source.setFrom(
files(
"src/main/kotlin", "src/test/kotlin",
"src/commonMain/kotlin", "src/commonTest/kotlin",
"src/jvmMain/kotlin", "src/jvmTest/kotlin",
"src/jsMain/kotlin", "src/nativeMain/kotlin",
).filter { it.exists() },
)
}
}

// Aggregate the documented modules into one multi-module HTML API site (published to GitHub Pages).
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version
testcontainers-qdrant = { module = "org.testcontainers:testcontainers-qdrant", version.ref = "testcontainers" }

kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }

spring-boot-autoconfigure = { module = "org.springframework.boot:spring-boot-autoconfigure", version.ref = "springBoot" }
spring-boot-configuration-processor = { module = "org.springframework.boot:spring-boot-configuration-processor", version.ref = "springBoot" }
Expand Down Expand Up @@ -75,6 +76,7 @@ mockk = { module = "io.mockk:mockk", version.ref = "mockk" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.18.1" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
Expand Down
4 changes: 4 additions & 0 deletions kdrant-bom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ plugins {
dependencies {
constraints {
api("io.github.nacode-studios:kdrant-core:${project.version}")
// kdrant-core is multiplatform: a Gradle consumer resolves the right variant from the
// kdrant-core coordinate, but a Maven consumer names the JVM artifact directly and would
// otherwise get no constraint from this platform.
api("io.github.nacode-studios:kdrant-core-jvm:${project.version}")
api("io.github.nacode-studios:kdrant-transport-rest:${project.version}")
api("io.github.nacode-studios:kdrant-transport-grpc:${project.version}")
api("io.github.nacode-studios:kdrant-spring-boot-starter:${project.version}")
Expand Down
63 changes: 50 additions & 13 deletions kdrant-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,30 +1,67 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.dokka)
alias(libs.plugins.dokka.javadoc)
// No dokka-javadoc here: its generator refuses a multiplatform project outright. Maven Central
// requires a -javadoc.jar to exist rather than to be Javadoc, so the publishing plugin fills it
// with Dokka's HTML output, which is what a Kotlin reader wants anyway.
alias(libs.plugins.maven.publish)
}

kotlin {
jvmToolchain(17)
explicitApi()
}

dependencies {
api(libs.kotlinx.coroutines.core)
api(libs.kotlinx.serialization.json)
// The JVM stays the reference target: it is what every engine and adapter in this repository
// compiles against, and `kdrant-core-jvm` is the artifact those modules resolve.
jvm {
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
}

// Everything the two dependencies support and someone might plausibly run a Qdrant client on.
// watchOS and tvOS are left out: they cost one line each and neither has a use case that would
// justify carrying the klibs, so they wait for someone to ask.
//
// Kotlin/JS is left out for a stronger reason. There is no JS engine — Ktor CIO and grpc-java are
// both JVM-only — so kdrant-core on JS would be models and a DSL with nothing to send them over.
// The target is not free: its Karma and webpack test tooling is the only npm dependency graph this
// repository has, and it arrived carrying a high-severity advisory in a package no published
// artifact contains. One line brings it back the day someone writes a JS engine.
iosArm64()
iosSimulatorArm64()
iosX64()
macosArm64()
macosX64()
linuxArm64()
linuxX64()
mingwX64()

sourceSets {
commonMain.dependencies {
api(libs.kotlinx.coroutines.core)
api(libs.kotlinx.serialization.json)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
jvmTest.dependencies {
implementation(project.dependencies.platform(libs.junit.bom))
implementation(libs.junit.jupiter)
runtimeOnly(libs.junit.platform.launcher)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotest.property)
}
}
}

testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
testRuntimeOnly(libs.junit.platform.launcher)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.kotest.property)
java {
toolchain { languageVersion.set(JavaLanguageVersion.of(17)) }
}

tasks.test {
tasks.named<Test>("jvmTest") {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.kdrant

import kotlinx.coroutines.CoroutineDispatcher

/**
* The dispatcher a client runs on when the caller does not choose one.
*
* This is the only platform-dependent declaration in the core, and the reason it has to be one is that
* only the JVM publishes an IO dispatcher. That is the right default where it exists: a Qdrant client
* blocks on a socket rather than on a CPU, so its work belongs on the elastic pool and not on the one
* sized to the number of cores.
*
* On Kotlin/Native it is `Dispatchers.Default`, because the IO dispatcher that exists there is still
* internal to the coroutines library.
*/
internal expect val ioDispatcher: CoroutineDispatcher
Loading