@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.
npm install @veristamp/nqqlQUERY 'incident response' FROM runbooks USING dense LIMIT 5;Execute a query
Section titled “Execute a query”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);Parameter binding & prepared queries
Section titled “Parameter binding & prepared queries”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 QQLconsole.log(stmt.toReadableString()); // readable previewNested 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.
Bulk ingest
Section titled “Bulk ingest”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 id → QQL-VALIDATION-UPSERT-ID) instead of landing partial data. The full contract lives once in the API surface reference.
Typed results
Section titled “Typed results”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 nullLazy scroll cursor & streams
Section titled “Lazy scroll cursor & streams”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 routingfor 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 pipingclient.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.
Profile before you optimize
Section titled “Profile before you optimize”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 reportedRetain the parsed statement for policy
Section titled “Retain the parsed statement for policy”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.
Inspect or batch work
Section titled “Inspect or batch work”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" });| API | Use it for |
|---|---|
new Client({ url, apiKey, useGrpc, routeAffinity, embedder }) | Configure a reusable native client |
Client.execute, executeStmt | Source, statement, script, or array execution |
Client.executeHits, module executeHits | Typed ScoredPoint[] hits without the report |
report.hits(stmt) / points / ids / facet / count / groups | Typed accessors per statement index (negative index counts from the end; groups returns [{ id, hits }] with ScoredPoint hits; see the API surface reference) |
report.telemetry | Per-result server telemetry (time_s plus hardware and inference usage) when reported |
Client.explainAnalyze | Static plan plus measured client and server timings for one statement |
Client.scrollCursor / scrollCursor, Client.scrollStream / scrollStream | Lazy async scroll iteration and pull-driven WHATWG stream |
ScoredPoint | Typed 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, compile | Plan 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.toReadableString | Canonical QQL vs truncated preview |
Stmt.injectFilter, Stmt.shardKey | Trusted filtering and optional routing |
Stmt.toObject, toJson, toJSON | AST inspection and serialization |
isValid, tokenize, explain | Lightweight 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 |
parseJson | High-throughput JSON forwarding |
injectFilter, execute | One-shot host convenience functions |
HttpEmbedder | A reusable HTTP embedder configuration (dense plus multi, image, and rerank endpoints; see the embedder ladder in the API surface reference) |
Pin reads with route affinity
Section titled “Pin reads with route affinity”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",});