@mantlejs/qdrant
Qdrant vector database adapter for Mantle JS. Provides QdrantRepository<T> — an abstract VectorRepository<T> base class that stores entities as Qdrant points with payload metadata and auto-creates collections on first write.
Installation
Section titled “Installation”npm install @mantlejs/qdrant @qdrant/js-client-restConcepts
Section titled “Concepts”Vector storage
Section titled “Vector storage”Qdrant is an open-source, self-hosted vector database optimised for high-performance similarity search. Each record is stored as a Qdrant point: a high-dimensional float array (the embedding) plus a JSON payload. QdrantRepository maps a Mantle entity to a Qdrant point by using the entity id as the point ID and serialising all other fields as the point payload.
Zero-vector placeholders
Section titled “Zero-vector placeholders”Embedding generation is intentionally decoupled from the repository. Calling save() or saveAll() inserts a zero-vector placeholder so the record is immediately findable by payload filters. Call upsertVector(id, vector, data) to attach the real embedding once it is available.
Collections
Section titled “Collections”A Qdrant database is organised into named collections. Subclasses declare readonly collectionName: string. The collection is created automatically with cosine-distance configuration on the first write — no manual setup required for development or testing.
Quick start
Section titled “Quick start”import { mantle } from "@mantlejs/mantle";import { qdrant, QdrantRepository } from "@mantlejs/qdrant";
const app = mantle().configure( qdrant({ url: process.env.QDRANT_URL, apiKey: process.env.QDRANT_API_KEY }),);
interface Document extends Record<string, unknown> { id: string; title: string; body: string;}
class DocumentRepository extends QdrantRepository<Document> { readonly collectionName = "documents"; readonly vectorSize = 1536; // must match the embedding model output}
const repo = new DocumentRepository(app);
// Insert a record (zero-vector placeholder)const doc = await repo.save({ title: "Intro to Qdrant", body: "..." });
// Attach a real embedding once generatedawait repo.upsertVector(doc.id, embeddingVector, { title: doc.title, body: doc.body });
// Similarity searchconst similar = await repo.findSimilar(queryVector, 10, { where: { category: "guide" } });qdrant(config?)
Section titled “qdrant(config?)”Returns a MantlePlugin. Call via app.configure(qdrant(config)).
app.configure( qdrant({ url: process.env.QDRANT_URL, // optional — default "http://localhost:6333" apiKey: process.env.QDRANT_API_KEY, // optional — for Qdrant Cloud }),);Side effects:
- Creates a
QdrantClientand stores it atapp.get("qdrant")
QdrantConfig
Section titled “QdrantConfig”| Field | Type | Default | Description |
|---|---|---|---|
url |
string |
QDRANT_URL env or http://localhost:6333 |
Qdrant server URL |
apiKey |
string |
QDRANT_API_KEY env |
API key for Qdrant Cloud (omit for local) |
All other fields are passed through to QdrantClient from @qdrant/js-client-rest.
QdrantRepository<T, D> (abstract class)
Section titled “QdrantRepository<T, D> (abstract class)”Implements VectorRepository<T, D> for Qdrant.
abstract class QdrantRepository<T extends Record<string, unknown>, D = Partial<T>> implements VectorRepository<T, D> { abstract readonly collectionName: string; abstract readonly vectorSize: number;}Subclasses must declare collectionName and vectorSize. All VectorRepository<T> and Repository<T> methods are provided as concrete implementations.
VectorRepository<T> methods
Section titled “VectorRepository<T> methods”| Method | Description |
|---|---|
findSimilar(vector, topK, params?) |
ANN search — returns the top-K most similar records |
upsertVector(id, vector, data) |
Attach (or replace) the embedding for a record |
deleteVector(id) |
Delete the record and its embedding (alias for deleteById) |
Every findSimilar result carries the Qdrant match score as _score. Higher is more similar for
Cosine and Dot collections (the default here is Cosine); for Euclid collections Qdrant reports a
distance, where lower is more similar.
Repository<T> methods
Section titled “Repository<T> methods”| Method | Description |
|---|---|
findAll(params?) |
Scroll through records with optional filtering & sorting |
findPage(params?) |
One page via native scroll cursors — pass the returned cursor back for the next page. skip/sort are rejected with BadRequest (Qdrant cannot combine order_by with scroll cursors) |
findById(id) |
Fetch a single record by ID; returns null if not found |
save(data) |
Insert with a zero-vector placeholder |
saveAll(data[]) |
Batch insert (single Qdrant upsert call) |
updateById(id, data) |
Replace the full payload, preserving the stored vector; throws NotFound if absent |
patchById(id, data) |
Merge partial fields, preserving the stored vector; throws NotFound if absent |
deleteById(id) |
Delete a record; throws NotFound if absent |
count(params?) |
Exact count of records matching optional QueryParams |
Error mapping
Section titled “Error mapping”Qdrant’s client doesn’t expose a rich exception taxonomy the way DynamoDB or MongoDB do, so
wrapError() is intentionally simple:
| Thrown value | Error thrown |
|---|---|
Already a MantleError (e.g. NotFound from a missing record, or thrown by a hook or custom repository method) |
passed through unchanged |
Any other Error |
GeneralError |
A non-Error throw |
GeneralError |
Instance properties
Section titled “Instance properties”| Property | Type | Default | Description |
|---|---|---|---|
collectionName |
string |
— | Required. Qdrant collection name |
vectorSize |
number |
— | Required. Embedding dimensionality |
idField |
string |
"id" |
Payload key used as the point ID |
timestamps |
boolean |
true |
Auto-write createdAt / updatedAt fields |
QueryParams.where operators
Section titled “QueryParams.where operators”| Mantle operator | Qdrant filter |
|---|---|
{ field: value } |
match.value |
{ field: null } |
is_null |
{ field: [a, b] } |
match.any |
$gt / $gte / $lt / $lte |
range |
$ne |
must_not match.value |
$ne: null |
must_not is_null |
$in |
match.any |
$nin |
must_not match.any |
$contains (scalar) |
match.value — Qdrant already matches “any element equals this value” when the field is an array, so this is the same shape as plain equality |
$contains (array) |
one ANDed match.value per element — every element required, not “any of” |
$contains (object) |
recurses into each key as a nested dot-path, flattening to leaf match.value/ANDed-array conditions the same way |
$or |
should |
$and |
must |
Nested fields ("metadata.owner.name") are addressed natively — Qdrant’s payload filter key
accepts dot-path strings directly, for every operator above, no translation needed.
import type { QdrantConfig, WhereClause } from "@mantlejs/qdrant";| Type | Description |
|---|---|
QdrantConfig |
Options passed to qdrant() plugin |
WhereClause |
QueryParams.where clause type |
Development
Section titled “Development”npx nx build qdrant # compilenpx nx test qdrant # run testsnpx nx lint qdrant # lintPublishing
Section titled “Publishing”Build before publishing:
npx nx build qdrantFirst publish (scoped packages require --access public):
cd packages/qdrantnpm publish --access publicSubsequent releases — bump version in packages/qdrant/package.json, then:
cd packages/qdrantnpm publish