qql-wasm makes the parser and planner available without a server round-trip. Use analyze for editor feedback and offline route inspection; construct a Client only when your application is ready to call a Qdrant REST endpoint.
npm install qql-wasmQUERY 'browser retrieval' FROM docs USING dense LIMIT 5;Analyze source locally
Section titled “Analyze source locally”Initialize once per JavaScript realm. analyze returns strict validity, tokens, the AST, compiled routes, an explanation, and a structured error with byte offsets.
import init, { analyze, isValid, parse } from "qql-wasm";
await init();
const info = analyze(query);if (!info.valid) { console.error(info.error.code, info.error.message);} else { console.log(info.routes, info.explain);}
console.log(isValid(query));console.log(parse(query));The analyze result is one object with every diagnostic and compiled route in a single pass:
interface AnalysisResult { valid: boolean; statements_count: number; tokens: Token[]; // [{ kind, text, pos, end, len }] ast: unknown[] | null; // parsed statements, null when invalid route: CompiledRoute | null; // first statement's route routes: CompiledRoute[]; // every statement's route explain: string | null; // human-readable plan, null when invalid error: AnalysisError | null; // null when valid}
interface Token { kind: string; text: string; pos: number; // start byte offset end: number; // end byte offset len: number;}
interface CompiledRoute { stmt_type: string; method: string; path: string; payload: unknown | null;}
interface AnalysisError { code: string; message: string; start: number | null; end: number | null;}route is the compiled route of the first statement; routes lists every statement in a script. error carries the validation code plus byte offsets into the source for editor underlining.
Compile and explain bytes
Section titled “Compile and explain bytes”When a route or explanation must cross a worker or process boundary, use the byte variants instead of building JavaScript objects. Both are safe, JS-owned Uint8Array buffers that transfer to a Worker or over IPC with zero JS-object overhead.
import init, { compileBytes, explainBytes } from "qql-wasm";
await init();
// JSON-encoded CompiledRoute.const routeBytes = compileBytes(query);worker.postMessage(routeBytes, [routeBytes.buffer]);
// UTF-8 explain text.const explainBytesUtf8 = explainBytes(query);const explainText = new TextDecoder().decode(explainBytesUtf8);Format source
Section titled “Format source”formatQuery(source) returns the canonical form of a QQL string — normalized clause order, keyword casing, whitespace, and escaping. It throws on a parse error. This is the same formatter the qql fmt CLI command and the VS Code Format Document command use.
import { formatQuery } from "qql-wasm";
await init();
console.log(formatQuery("query text 'x' from docs using dense limit 5"));// QUERY 'x' FROM docs USING dense LIMIT 5;The Stmt class
Section titled “The Stmt class”new Stmt(source) parses exactly one statement and returns an owned handle. Mutate the handle locally — inject a trusted filter, assign routing — then compile or execute it. The class is a JavaScript realm object; the parsed AST stays in WASM memory.
import init, { Stmt } from "qql-wasm";
await init();
const stmt = new Stmt(query);
// Trusted predicate injection (void; throws on invalid operator or value).stmt.injectFilter("tenant_id", "=", "acme");
// Routing only — never a security boundary.stmt.shardKey = "acme";console.log(stmt.shardKey);
// Bind :name (object) or ? (array) parameters; returns a new bound Stmt.const bound = stmt.bind({ q: "search", lim: 5 });
const route = stmt.compileRoute({ q: "search", lim: 5 }); // { stmt_type, method, path, payload }const astJson = stmt.toJSON(); // stringconst astObject = stmt.toObject(); // plain JS object
console.log(stmt.toString()); // canonical, re-parseable QQLconsole.log(stmt.toReadableString()); // readable preview; long vectors truncatedconsole.log(stmt.explain()); // execution plan for this statement
// Transferable byte compile.const routeBytes = stmt.compileRouteBytes();
stmt.free(); // or use `using stmt = new Stmt(query)` with [Symbol.dispose]Methods:
| Member | Returns | Use it for |
|---|---|---|
new Stmt(source) | owned handle | Parse exactly one statement |
injectFilter(field, op, value) | void | Inject a trusted predicate (mutates in place) |
shardKey getter / setter | string, integer (BigInt), or null | Assign or read QQL SHARD routing (integers stay numeric keys) |
bind(params?) | bound Stmt | Substitute :name / ? parameters; params optional |
bound | boolean | Whether params were already bound; passing new params to a bound Stmt throws QQL-BIND-ALREADY-BOUND |
toJSON() | string | Serialize the AST to JSON |
toObject() | object | Serialize the AST to a JS object |
toString() | string | Canonical, re-parseable QQL |
toReadableString() | string | Readable preview (truncated vectors, may not re-parse) |
explain() | string | Execution plan for this statement |
compileRoute(params?) | {stmt_type, method, path, payload} | Compile this statement to a Qdrant REST route (optionally binding params) |
compileRouteBytes() | Uint8Array | Transferable route compile |
free() / [Symbol.dispose]() | — | Release WASM memory (using works in TypeScript) |
Execute with an explicit lifecycle
Section titled “Execute with an explicit lifecycle”The WASM client is REST/fetch only. new Client(url?, apiKey?) takes the Qdrant REST URL and an optional API key; both default to null, and the URL defaults to http://localhost:6333.
import { Client } from "qql-wasm";
const client = new Client("http://localhost:6333", null);
try { const report = await client.execute(query); console.log(report.ok, report.results);} finally { client.free();}execute(query | Stmt | (query | Stmt)[], options) accepts a single statement or script string, a pre-parsed Stmt, or an array of independent strings/Stmts. ExecuteOptions is { onError?, params? } — onError is "stop" (default) or "continue", and params is an object for :name or an array for ? — the same shape as bind(query, params). For a batch or script, params may be an array with one entry per statement; the length must match the statement count exactly. executeStmt(stmt, options) runs a pre-parsed Stmt handle. compile(query) and explain(query) inspect a route or plan without touching the network. explainAnalyze(query, options) runs one statement and returns the static plan plus measured client timings and server telemetry (see below).
The report is a plain { ok, results, succeeded, failed, telemetry } object. Wrap it with the dx.js helpers for typed hits() / facet() / count() / groups() accessors, telemetry reads, and the executeHits(client, query, options) one-shot instead of hand-rolling either. Row vectors accept plain arrays, Float32Array / Float64Array (packed, one copy), integer typed arrays, and the flat { data, dim } multivector form everywhere params enter. 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. Statement indexes in the typed accessors use Python list semantics on all hosts (-1 is the last statement; out-of-range returns [] or 0).
Response shaping is strict and canonical, matching the native SDKs' typed ExecData report: every operation reads exactly its Qdrant OpenAPI response field (result.points, the bare result array for QUERY POINTS, result.groups, result.count, facet result.hits, result.collections, result.shard_keys, result.config, CollectionInfo) and emits the same JSON shapes (hits carry no separate text, groups are { groups: [{ id, hits }] }, counts are { count }, collections and shard keys are { collections } / { shard_keys }). A missing or mistyped backend field fails the statement with QQL-BACKEND-ENVELOPE instead of silently returning an empty or synthesized result. Server telemetry (time, usage) stays optional and lenient.
Bulk ingest
Section titled “Bulk ingest”const rows = [ { id: 1, vector: { dense: new Float32Array([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 });One :rows template is prepared once, then each chunk splices through the point-splice path with no re-parse. The full typed-array contract lives once in the API surface reference: same row shapes on Python, Node, and WASM, same QQL-BIND-TYPE-MISMATCH for a non-array rows, same QQL-VALIDATION-UPSERT-BATCH for batchSize < 1.
await client.execute("QUERY TEXT :q FROM docs LIMIT :lim", { params: { q: "search", lim: 5 },});Before executing, the client fetches the collection topology to resolve USING vector roles, then embeds text through the configured embedder. Execution is REST-only — there is no gRPC or edge backend in WASM.
Pin reads with route affinity
Section titled “Pin reads with route affinity”Qdrant 1.19 supports read affinity: pin reads to a stable replica so a user or session sees a consistent view. The WASM client applies the X-Qdrant-Route-Affinity header on every request. Pass null or "" to clear.
import { Client } from "qql-wasm";
const client = new Client("http://localhost:6333", null);client.setRouteAffinity("session-acme-42");console.log(client.routeAffinity); // "session-acme-42"
client.setRouteAffinity(null); // clearProvide text embeddings from the host
Section titled “Provide text embeddings from the host”Use an OpenAI-compatible HTTP endpoint or supply a JavaScript callback. The callback receives a batch of strings and returns one dense vector per string.
import { Client } from "qql-wasm";
const client = new Client("http://localhost:6333", null);client.setEmbedder(async (texts) => { const response = await fetch("/api/embed", { method: "POST", body: JSON.stringify({ texts }), }); return response.json();});
// Or configure a hosted OpenAI-compatible endpoint.client.setHttpEmbedder( "https://embeddings.example.com/v1/embeddings", "text-embedding-3-small", 1536, null,);
client.hasEmbedder(); // truesetEmbedder(fn) takes a JS callback (texts: string[]) => Promise<number[][]> — compatible with Transformers.js pipelines and @huggingface/transformers. The callback is called once with the full batch, so prefer a model that embeds batches. setHttpEmbedder(endpoint, model, dimension, apiKey?) configures any OpenAI-compatible {"model", "input": [...]} endpoint and sends the whole batch in one request. setRemoteEmbedder is an alias with the same signature. hasEmbedder() reports whether any embedder is configured.
setHttpMultiEmbedder(endpoint, model, dimension, apiKey?) configures a multi-vector endpoint returning nested [[...]] bags (flat arrays are rejected). setHttpImageEmbedder(endpoint, model, dimension, apiKey?) configures an image endpoint. setHttpReranker(endpoint, model, apiKey?) configures a cross-encoder rerank endpoint. Browser calls need CORS-enabled endpoints.
client.setHttpMultiEmbedder( "https://embeddings.example.com/v1/multi", "colbert-model", 96, null,);client.setHttpImageEmbedder( "https://embeddings.example.com/v1/image", "clip-model", 512, null,);client.setHttpReranker( "https://rerank.example.com/rerank", "bge-reranker", null,);Typed DX layer (dx.js)
Section titled “Typed DX layer (dx.js)”For structured, typed result handling matching @veristamp/nqql and pyqql, @veristamp/qql-wasm includes the dx.js helper module:
import init, { Client } from "qql-wasm";import { ExecutionReport, ScoredPoint, buildError, executeHits } from "qql-wasm/dx";
await init();
const client = new Client("http://localhost:6333");const raw = await client.execute("QUERY 'search text' FROM docs LIMIT 5;");const report = new ExecutionReport(raw);
console.log(`Success: ${report.ok}, hits: ${report.succeeded}`);for (const hit of report.hits(0)) { console.log(hit.id, hit.score, hit.payload);}The module exports:
ExecutionReport: Accessors for statement hits (.hits(stmt)/.points(stmt)), facets (.facet(stmt)), counts (.count(stmt)), and groups (.groups(stmt)returning[{ id, hits }]). Negative indexes count from the end.report.telemetryreads aggregated server time and usage ({ time_s, usage }ornull) so browser code never hand-rolls the sum.ScoredPoint: Typed hits with.id,.score,.payload,.text(derived frompayload.text),.collection,.vector(dense / sparse / multi-dense / named whenWITH VECTOR), plus.get(key, default)..shard_keyis a legacy passthrough, always null on the typed path.buildError: Unpacks JSON error strings intoErrorobjects carrying.code,.kind,.span, plus.fieldsand.request_idwhen present.wrapReport: Returns the input if already anExecutionReport, else wraps the plain object.executeHits(client, query, options): One-shot reads returningScoredPoint[]directly. Prefer this over manualexecuteplushits(0)when only hits are needed.scrollCursor(client, collection, options): Lazy async generator overSCROLL ... AFTER :cursor(at most one page buffered). Same options shape as Node (batchSize,where,params,withPayload,withVector,shardKey).
import init, { Client } from "qql-wasm";import { ExecutionReport, executeHits } from "qql-wasm/dx";
await init();const client = new Client("http://localhost:6333");
// One-shot hits without touching the reportconst hits = await executeHits(client, "QUERY 'search text' FROM docs LIMIT 5");
// Telemetry without hand-rolled aggregationconst report = new ExecutionReport(await client.execute("QUERY 'x' FROM docs LIMIT 5"));console.log(report.telemetry); // { time_s, usage } or null
// Profile before optimizing: static plan plus client phases and server telemetryconst analysis = await client.explainAnalyze("QUERY 'x' FROM docs USING dense LIMIT 5");console.log(analysis.plan, analysis.phases, analysis.server_time_s, analysis.usage);Memory
Section titled “Memory”Every Client and Stmt owns WASM allocations. Call free() when a handle is no longer needed, especially in long-lived pages or when replacing connection settings. Stmt also implements [Symbol.dispose], so using in TypeScript releases it automatically.
| API | Returns | Use it for |
|---|---|---|
init / initSync | initialized WASM module | One-time module initialization |
parse, isValid, tokenize | AST values, boolean, token list | Strict local parser primitives |
analyze(source) | validity, tokens, AST, routes, explain, error | Editors and offline diagnostics |
compile / compileQuery, explain, inject_filter | route, text, rewritten AST | One-shot offline planning helpers (compileQuery is an alias of compile) |
compileBytes, explainBytes | Uint8Array | Transferable worker or IPC payloads |
bind(query, params?, { truncateVectors }) | bound source string | Substitute :name (object) or ? (array) placeholders offline; params optional — omitted returns the query unchanged |
formatQuery | canonical source string | Normalize clause order, casing, and whitespace |
new Stmt(source) | owned statement handle | injectFilter, bind, route compile, and shard assignment |
Stmt.injectFilter, Stmt.shardKey | mutated statement handle | Trusted filtering and optional routing |
Stmt.bind, Stmt.compileRoute | bound handle, route | Prepared-statement binding and offline route compile |
Stmt.toString, Stmt.toReadableString, Stmt.explain | text | Canonical form, readable preview, and per-statement plan |
Stmt.toObject, toJSON, compileRoute | AST or route | Inspect a single owned statement |
Stmt.compileRouteBytes | Uint8Array | Transferable single-statement route |
new Client, execute, executeStmt | execution report promise | Direct browser-to-Qdrant REST calls (report carries telemetry; wrap with dx.js for typed accessors) |
Client.compile, Client.explain | route, plan text | Inspect work before sending traffic |
Client.explainAnalyze | plan plus measured timings | Static plan, client phases, and server telemetry for one statement |
setEmbedder, setHttpEmbedder, setHttpMultiEmbedder, setHttpImageEmbedder, setHttpReranker, hasEmbedder | configured client | Resolve text, multi, image, and rerank input from JavaScript or HTTP (see the embedder ladder in the API surface reference) |
Client.setRemoteEmbedder | configured client | Alias for setHttpEmbedder |
dx.js ExecutionReport | typed accessors plus telemetry | hits / points / ids / facet / count / groups (negative index counts from the end; groups returns [{ id, hits }]), telemetry, wrapReport, executeHits, scrollCursor |
setRouteAffinity, routeAffinity | configured client, key | Pin reads via X-Qdrant-Route-Affinity (Qdrant 1.19+) |
free() / [Symbol.dispose] | released WASM allocation | Dispose Client and Stmt handles |
Parameter binding
Section titled “Parameter binding”bind(query, params?), Stmt.bind(params?), and execute(…, { params }) take an object for :name or an array for ? — the same contract as every other SDK. Vector params accept plain arrays, Float32Array / Float64Array (packed, one copy), integer typed arrays (sparse indices), and the flat { data, dim } multivector form; raw ArrayBuffer without a float view fails closed. parseJson returns the AST as a raw JSON string with no object allocation.