qdrant_dart 0.3.0
qdrant_dart: ^0.3.0 copied to clipboard
An idiomatic, REST-first Dart SDK for Qdrant.
qdrant_dart #
An idiomatic, REST-first Dart SDK for Qdrant.
Status: v0.3.0 is release-ready for collection lifecycle, typed search, payload and vector maintenance, filtered deletion and counts, and bounded batch ingestion.
Why this exists #
Qdrant has an OpenAPI and gRPC interface, but Dart developers need more than generated protocol classes: a small, predictable client with typed errors, practical pagination, and real compatibility checks against Qdrant.
qdrant_dart targets trusted Dart services, CLIs, and controlled application
backends. Do not put a Qdrant Cloud API key in a mobile or browser app.
Product boundary #
- REST is the first transport. gRPC is a later performance feature, not a prerequisite for a usable SDK.
- The public API is handwritten and Dart-idiomatic; protocol generation, if used, stays an internal implementation detail.
- Qdrant remains the database and the source of API semantics. This package will not add local vector storage, embedding generation, ORM behaviour, or a RAG framework.
- Each supported endpoint must be covered by a real Qdrant integration test, not only mocked HTTP tests.
v0.1 scope #
- Client configuration, API-key authentication, timeouts, and typed errors.
- Collections: create, inspect, list, and delete.
- Points: upsert, retrieve, delete, scroll, and query with payload filters.
- A Docker-backed compatibility suite pinned to a declared Qdrant version.
gRPC, cluster administration, snapshots, shard management, and local embedding inference are explicitly out of scope for v0.1.
See CHANGELOG.md for release notes.
Compatibility #
Development is verified against a minimum supported Qdrant v1.12.0 and the
current target v1.18.2. tool/qdrant-min-version
and tool/qdrant-version are the sources of truth used
by the compatibility harness.
The SDK supports HTTP/HTTPS client configuration, API-key authentication,
request timeouts, typed failure reporting, and collection lifecycle operations
plus point upsert, retrieval, ID- or filter-based deletion, exact or approximate
counts, ID-ordered scrolling, and dense-vector queries with match/range payload
filters, nested Boolean groups, and point-ID conditions against
qdrant/qdrant:v1.18.2. Payload data can be set, overwritten, partially
deleted, or cleared by point IDs or filters.
Collection creation and point operations support one default dense vector or
named dense and sparse vectors, and payload indexes can be created, inspected,
and deleted. Selected vectors can be updated or named vectors deleted without
replacing the rest of a point. Sparse-vector configuration currently uses
Qdrant's defaults; nested payload filters and collection tuning are not yet
supported. Large point iterables can be upserted in bounded sequential batches
without hiding any per-batch update result.
Client setup #
Use the client only in a trusted Dart service or CLI. Read API keys from the server-side environment rather than embedding them in a Flutter or browser application.
import 'dart:io';
import 'package:qdrant_dart/qdrant_dart.dart';
final client = QdrantClient(
baseUrl: Uri.parse('https://your-qdrant.example'),
apiKey: Platform.environment['QDRANT_API_KEY'],
);
try {
await client.collections.create(
'movies',
vectors: CollectionVectors.dense(
DenseVectorParams(size: 4, distance: Distance.cosine),
),
);
await client.payloadIndexes.create(
'movies',
'year',
schema: PayloadSchemaType.integer,
);
final update = await client.points.upsert('movies', [
Point(
id: 1,
vector: [0.9, 0.1, 0.1, 0.2],
payload: {'title': 'The Matrix', 'year': 1999},
),
Point(
id: 2,
vector: [0.1, 0.9, 0.2, 0.1],
payload: {'title': 'The Matrix Reloaded', 'year': 2003},
),
]);
print(update.status);
final stored = await client.points.retrieve(
'movies',
[1],
withVectors: const VectorSelector.all(),
);
print(stored.single.payload?['title']);
await for (final point in client.points.scrollAll('movies')) {
print(point.id);
}
final matches = await client.points.query(
'movies',
DenseVector([0.9, 0.1, 0.1, 0.2]),
filter: Filter(
must: [
HasIdCondition([1]),
FieldCondition.match('year', 1999),
],
),
withPayload: true,
);
print(matches.single.score);
await client.points.setPayload(
'movies',
{'featured': true},
PointSelector.ids([1]),
);
await client.points.overwritePayload(
'movies',
{'title': 'The Matrix', 'year': 1999},
PointSelector.ids([1]),
);
await client.points.deletePayload(
'movies',
['year'],
PointSelector.ids([1]),
);
await client.points.clearPayload(
'movies',
PointSelector.filter(
Filter(must: [FieldCondition.match('title', 'The Matrix')]),
),
);
print(await client.points.count('movies'));
await client.points.deleteByFilter(
'movies',
Filter(must: [FieldCondition.range('year', gte: 2000)]),
);
await client.points.delete('movies', [1]);
await client.payloadIndexes.delete('movies', 'year');
final movies = await client.collections.get('movies');
print(movies.pointsCount);
await client.collections.delete('movies');
} finally {
client.close();
}
Named dense and sparse vectors share the same point and query APIs:
await client.collections.create(
'documents',
vectors: CollectionVectors.named(
dense: {
'text': DenseVectorParams(size: 4, distance: Distance.cosine),
},
sparse: const {'keywords': SparseVectorParams()},
),
);
await client.points.upsert('documents', [
Point.named(
id: 1,
vectors: {
'text': DenseVector([0.9, 0.1, 0.1, 0.2]),
'keywords': SparseVector(indices: [1, 5], values: [0.8, 0.4]),
},
),
]);
await client.points.updateVectors('documents', [
PointVectorUpdate.named(
id: 1,
vectors: {'text': DenseVector([0.8, 0.2, 0.1, 0.3])},
),
]);
final sparseMatches = await client.points.query(
'documents',
SparseVector(indices: [1, 5], values: [0.8, 0.4]),
using: 'keywords',
withVectors: VectorSelector.named(['text']),
);
await client.points.deleteVectors(
'documents',
['keywords'],
PointSelector.ids([1]),
);
For larger inputs, bound each request without first copying the entire iterable:
final updates = await client.points.upsertInBatches(
'documents',
generatedPoints,
batchSize: 100,
);
for (final update in updates) {
print(update.status);
}
Batches are sent sequentially. If one request fails, Qdrant does not roll back earlier successful batches.
When an operation fails, catch [QdrantException]. It includes the HTTP status when Qdrant responded, its error message when available, and the request method and URL. It never includes the API key.
Development #
Install Dart and Docker, then run:
dart pub get
dart format --output=none --set-exit-if-changed .
dart analyze --fatal-infos
dart test --exclude-tags integration
./tool/test-compatibility.sh
The compatibility script runs the real-server integration suite against both declared Qdrant versions. Each image starts on a random localhost port and is removed after its tests finish.
Releasing #
Releases are tag-driven. Update pubspec.yaml and CHANGELOG.md in the same
commit, then create and push an annotated v<version> tag whose version exactly
matches the package version. The publish workflow reruns the full CI suite and,
only after it passes, uses GitHub OIDC and Dart's official reusable workflow to
publish to pub.dev. No long-lived publishing token is stored in GitHub.
Automated publishing must be enabled on the package's pub.dev Admin page for
the songxychn/qdrant-dart repository with tag pattern v{{version}} and the
required GitHub Actions environment pub.dev.
For contributors and agents #
Read PROJECT.md for the roadmap and AGENTS.md for the delivery rules before adding code.