Skip to content

Node.js SDK

@veristamp/nqql is a native N-API binding for Node.js 18 or newer. It accepts source strings, parsed statements, and arrays of independent inputs.

Install
npm install @veristamp/nqql
QQLScoped runbook searchTry in playground
QUERY 'incident response' FROM runbooks USING dense LIMIT 5;
const { Client, isValid } = require("@veristamp/nqql");
if (!isValid(query)) {
throw new Error("QQL source is invalid");
}
const client = new Client({ url: "http://localhost:6333" });
const report = await client.execute(query);
console.log(report.ok, report.succeeded, report.results);

Pass params in the execution options as an object for named placeholders (:name) or an array for positional placeholders (?):

const { Client, bind, parse } = require("@veristamp/nqql");
const client = new Client({ url: "http://localhost:6333" });
// Named parameter substitution (:name)
const report = await client.execute(
"QUERY TEXT :query FROM runbooks WHERE category = :cat LIMIT :limit",
{ params: { query: "incident response", cat: "ops", limit: 5 } }
);
// Positional parameter substitution (?)
const reportPos = await client.execute(
"QUERY TEXT ? FROM runbooks WHERE category = ? LIMIT ?",
{ params: ["incident response", "ops", 5] }
);
// Standalone binding (accepts a string or a Stmt; Stmt + truncateVectors returns a string)
const boundQql = bind("QUERY TEXT :q FROM docs LIMIT :lim", { q: "search", lim: 10 });

Parse once, then reuse the statement. stmt.bind(params) returns a new bound Stmt, stmt.compileRoute(params?) lowers the bound statement to its route, and stmt.toString() renders canonical QQL while stmt.toReadableString() truncates long vectors for logs:

const [stmt] = parse("QUERY TEXT :q FROM runbooks WHERE category = :cat LIMIT :lim");
const bound = stmt.bind({ q: "incident response", cat: "ops", lim: 5 });
const route = stmt.compileRoute({ q: "incident response", cat: "ops", lim: 5 });
console.log(route.method, route.path);
console.log(stmt.toString()); // canonical, re-parseable QQL
console.log(stmt.toReadableString()); // readable preview

Nested objects expand to dotted keys ({ loc: { lat: 1.0, lon: 2.0 } } binds :loc.lat / :loc.lon), and flat dotted keys work the same way. For a multi-statement batch, pass params as an array with one entry per statement — the length must match the statement count exactly, and each entry is an object (named) or an array of scalars (positional):

const batch = await client.execute(
[
"QUERY TEXT :q FROM runbooks LIMIT 5",
"QUERY TEXT :q FROM incidents LIMIT 10",
],
{ params: [{ q: "database" }, { q: "network" }] }
);

Binding failures throw errors with stable QQL-BIND-* codes — mixed placeholder styles, missing parameters, extra positional values, or wrong types (see the error code reference).

compileQuery(query, params?) and client.compile(query, params?) compile a template to its route in one step, and isValid runs the full parse + plan gate.

Pass point objects — payload as data, never SQL text. One :rows template is prepared once, then each batchSize chunk splices through the point-splice path with no re-parse and no per-batch schema fetch. Prefer this over hand-rolled batch loops:

const rows = [
{ id: 1, vector: { dense: [0.1, 0.2, 0.3] }, tag: "a" },
{ id: 2, vector: { dense: [0.4, 0.5, 0.6] }, tag: "b" },
];
const report = await client.upsertMany("docs", rows, { batchSize: 100 });
console.log(report.succeeded, report.failed);

Row vectors accept plain arrays, packed Float32Array / Float64Array, integer typed arrays for sparse indices, and the flat { data: [...], dim: N } multivector form (same contract as Python and WASM). A plain number[] of 32+ elements also binds as a packed F32Array with one copy instead of a per-element walk; shorter lists and nested shapes keep exact list semantics, and payload values are never repacked. Raw binary without a float view fails closed with a wrap-first TypeError. Each object has the same shape as an inline VALUES {…} row, so a misshapen row fails closed with QQL-BIND-TYPE-MISMATCH (missing idQQL-VALIDATION-UPSERT-ID) instead of landing partial data. The full contract lives once in the API surface reference.

client.executeHits and the module-level executeHits return ScoredPoint objects directly, and ExecutionReport adds typed accessors to the report:

const { Client, executeHits } = require("@veristamp/nqql");
const client = new Client({ url: "http://localhost:6333" });
// 1. Hits as typed ScoredPoint objects (id, score, payload, text, collection, vector)
for (const hit of await client.executeHits("QUERY TEXT 'incident' FROM runbooks LIMIT 5")) {
console.log(hit.id, hit.score, hit.get("severity")); // payload access via hit.get(key)
}
// hit.text is derived from payload.text (null when absent/non-string); the
// typed hit has no separate text field. hit.vector holds dense / sparse /
// multi-dense / named vectors when WITH VECTOR, else null. hit.shard_key is a
// legacy passthrough, always null on the typed path.
// 2. Facet, count, point, and group accessors per statement index
// Negative indexes count from the end (-1 is the last statement).
// Out-of-range reads return [] (or 0 for count) instead of throwing.
const report = await client.execute("FACET severity FROM runbooks LIMIT 10");
console.log(report.facet()); // [{ value: ..., count: ... }, ...]
const countReport = await client.execute("COUNT FROM runbooks WHERE status = 'open'");
console.log(countReport.count());
const pointReport = await client.execute("QUERY POINTS (1, 2, 3) FROM runbooks");
console.log(pointReport.points());
console.log(pointReport.groups(-1)); // [{ id: <group key>, hits: [...] }, ...]
// 3. Per-result server telemetry (Qdrant time plus hardware and inference usage
// when reported; null where the route reports nothing)
console.log(pointReport.telemetry); // { time_s, usage } or null

scrollCursor pages lazily through a collection — at most one page is ever buffered. scrollStream wraps it in a pull-driven WHATWG ReadableStream with native backpressure. Available as Client methods or standalone functions:

const { Client, scrollCursor, scrollStream } = require("@veristamp/nqql");
const client = new Client({ url: "http://localhost:6333" });
// Async iteration with parameterized filter and shard routing
for await (const point of client.scrollCursor("docs", {
batchSize: 500,
where: "status = :status AND price < :max_price",
params: { status: "active", max_price: 150.0 },
shardKey: "tenant-a",
})) {
await processPoint(point);
}
// WHATWG ReadableStream piping
client.scrollStream("docs", { batchSize: 500 }).pipeTo(writableStream);

Options: batchSize (default 100), where (QQL filter fragment), params (named parameter bindings), shardKey (custom shard partition routing), withPayload (default true — payloads are included unless stripped client-side, since SCROLL has no WITH PAYLOAD spelling), withVector (default false — appends WITH VECTOR). Non-standard and hyphenated collection names are safely escaped automatically.

client.explainAnalyze() runs one statement and returns the static plan plus measured client phase timings and honest server telemetry (absent means null, never an error):

const analysis = await client.explainAnalyze(
"QUERY TEXT :q FROM docs USING dense LIMIT :lim",
{ params: { q: "neural nets", lim: 10 } }
);
console.log(analysis.plan);
console.log(analysis.phases); // parse_ms / dispatch_ms / ...
console.log(analysis.server_time_s); // seconds Qdrant spent, when reported
const { Client, parse } = require("@veristamp/nqql");
const client = new Client({ url: "http://localhost:6333" });
const [stmt] = parse(query);
stmt.injectFilter("tenant_id", "=", "acme");
stmt.shardKey = "acme";
const report = await client.execute(stmt);

injectFilter permits =, >, >=, <, and <=. shardKey is routing, not authorization; keep the injected tenant predicate for isolation. See the Filter injection guide for the per-statement behavior.

const {
Client,
compileQuery,
explain,
parseJson,
} = require("@veristamp/nqql");
const route = compileQuery(query);
console.log(route.method, route.path, route.payload);
console.log(explain(query));
// Fast JSON-string output when forwarding the AST instead of creating JS objects.
const astJson = parseJson(query);
const client = new Client({ url: "http://localhost:6333" });
const report = await client.execute([
"COUNT FROM runbooks;",
"COUNT FROM incidents;",
], { onError: "continue" });
APIUse it for
new Client({ url, apiKey, useGrpc, routeAffinity, embedder })Configure a reusable native client
Client.execute, executeStmtSource, statement, script, or array execution
Client.executeHits, module executeHitsTyped ScoredPoint[] hits without the report
report.hits(stmt) / points / ids / facet / count / groupsTyped accessors per statement index (negative index counts from the end; groups returns [{ id, hits }] with ScoredPoint hits; see the API surface reference)
report.telemetryPer-result server telemetry (time_s plus hardware and inference usage) when reported
Client.explainAnalyzeStatic plan plus measured client and server timings for one statement
Client.scrollCursor / scrollCursor, Client.scrollStream / scrollStreamLazy async scroll iteration and pull-driven WHATWG stream
ScoredPointTyped hit with id, score (f32 shortest round-trip), payload, text (derived from payload.text), collection, vector (shard_key is a legacy passthrough, always null on the typed path)
Client.explain, explainStmt, compilePlan and route inspection (compile accepts params)
parse(source)Native Stmt handles for AST mutation
Stmt.bind(params?)Bound Stmt from :name / ? placeholders
Stmt.compileRoute(params?)Compile a prepared statement to its route without re-parsing
Stmt.toString / Stmt.toReadableStringCanonical QQL vs truncated preview
Stmt.injectFilter, Stmt.shardKeyTrusted filtering and optional routing
Stmt.toObject, toJson, toJSONAST inspection and serialization
isValid, tokenize, explainLightweight parser and diagnostic tools (isValid runs parse + plan)
compileQuery(query, params?)Qdrant route inspection without network I/O (optionally binding params)
bind(query, params?, { truncateVectors })Substitute :name / ? placeholders into a string or Stmt
parseJsonHigh-throughput JSON forwarding
injectFilter, executeOne-shot host convenience functions
HttpEmbedderA reusable HTTP embedder configuration (dense plus multi, image, and rerank endpoints; see the embedder ladder in the API surface reference)

Qdrant 1.19 read affinity pins reads to a stable replica so a user or session sees a consistent view. nqql passes the key at construction as the X-Qdrant-Route-Affinity header (REST) or gRPC metadata x-qdrant-route-affinity. Empty strings are unset.

const { Client, execute } = require("@veristamp/nqql");
const client = new Client({
url: "http://localhost:6333",
routeAffinity: "session-acme-42",
});
console.log(client.routeAffinity); // "session-acme-42"
// One-shot convenience accepts the same option.
const report = await execute(query, {
url: "http://localhost:6333",
routeAffinity: "session-acme-42",
});