Skip to content

WebAssembly SDK

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.

Install
npm install qql-wasm
QQLA browser-safe query sourceTry in playground
QUERY 'browser retrieval' FROM docs USING dense LIMIT 5;

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.

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);

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;

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(); // string
const astObject = stmt.toObject(); // plain JS object
console.log(stmt.toString()); // canonical, re-parseable QQL
console.log(stmt.toReadableString()); // readable preview; long vectors truncated
console.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:

MemberReturnsUse it for
new Stmt(source)owned handleParse exactly one statement
injectFilter(field, op, value)voidInject a trusted predicate (mutates in place)
shardKey getter / setterstring, integer (BigInt), or nullAssign or read QQL SHARD routing (integers stay numeric keys)
bind(params?)bound StmtSubstitute :name / ? parameters; params optional
boundbooleanWhether params were already bound; passing new params to a bound Stmt throws QQL-BIND-ALREADY-BOUND
toJSON()stringSerialize the AST to JSON
toObject()objectSerialize the AST to a JS object
toString()stringCanonical, re-parseable QQL
toReadableString()stringReadable preview (truncated vectors, may not re-parse)
explain()stringExecution plan for this statement
compileRoute(params?){stmt_type, method, path, payload}Compile this statement to a Qdrant REST route (optionally binding params)
compileRouteBytes()Uint8ArrayTransferable route compile
free() / [Symbol.dispose]()Release WASM memory (using works in TypeScript)

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.

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.

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); // clear

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(); // true

setEmbedder(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,
);

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.telemetry reads aggregated server time and usage ({ time_s, usage } or null) so browser code never hand-rolls the sum.
  • ScoredPoint: Typed hits with .id, .score, .payload, .text (derived from payload.text), .collection, .vector (dense / sparse / multi-dense / named when WITH VECTOR), plus .get(key, default). .shard_key is a legacy passthrough, always null on the typed path.
  • buildError: Unpacks JSON error strings into Error objects carrying .code, .kind, .span, plus .fields and .request_id when present.
  • wrapReport: Returns the input if already an ExecutionReport, else wraps the plain object.
  • executeHits(client, query, options): One-shot reads returning ScoredPoint[] directly. Prefer this over manual execute plus hits(0) when only hits are needed.
  • scrollCursor(client, collection, options): Lazy async generator over SCROLL ... 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 report
const hits = await executeHits(client, "QUERY 'search text' FROM docs LIMIT 5");
// Telemetry without hand-rolled aggregation
const 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 telemetry
const analysis = await client.explainAnalyze("QUERY 'x' FROM docs USING dense LIMIT 5");
console.log(analysis.plan, analysis.phases, analysis.server_time_s, analysis.usage);

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.

APIReturnsUse it for
init / initSyncinitialized WASM moduleOne-time module initialization
parse, isValid, tokenizeAST values, boolean, token listStrict local parser primitives
analyze(source)validity, tokens, AST, routes, explain, errorEditors and offline diagnostics
compile / compileQuery, explain, inject_filterroute, text, rewritten ASTOne-shot offline planning helpers (compileQuery is an alias of compile)
compileBytes, explainBytesUint8ArrayTransferable worker or IPC payloads
bind(query, params?, { truncateVectors })bound source stringSubstitute :name (object) or ? (array) placeholders offline; params optional — omitted returns the query unchanged
formatQuerycanonical source stringNormalize clause order, casing, and whitespace
new Stmt(source)owned statement handleinjectFilter, bind, route compile, and shard assignment
Stmt.injectFilter, Stmt.shardKeymutated statement handleTrusted filtering and optional routing
Stmt.bind, Stmt.compileRoutebound handle, routePrepared-statement binding and offline route compile
Stmt.toString, Stmt.toReadableString, Stmt.explaintextCanonical form, readable preview, and per-statement plan
Stmt.toObject, toJSON, compileRouteAST or routeInspect a single owned statement
Stmt.compileRouteBytesUint8ArrayTransferable single-statement route
new Client, execute, executeStmtexecution report promiseDirect browser-to-Qdrant REST calls (report carries telemetry; wrap with dx.js for typed accessors)
Client.compile, Client.explainroute, plan textInspect work before sending traffic
Client.explainAnalyzeplan plus measured timingsStatic plan, client phases, and server telemetry for one statement
setEmbedder, setHttpEmbedder, setHttpMultiEmbedder, setHttpImageEmbedder, setHttpReranker, hasEmbedderconfigured clientResolve text, multi, image, and rerank input from JavaScript or HTTP (see the embedder ladder in the API surface reference)
Client.setRemoteEmbedderconfigured clientAlias for setHttpEmbedder
dx.js ExecutionReporttyped accessors plus telemetryhits / points / ids / facet / count / groups (negative index counts from the end; groups returns [{ id, hits }]), telemetry, wrapReport, executeHits, scrollCursor
setRouteAffinity, routeAffinityconfigured client, keyPin reads via X-Qdrant-Route-Affinity (Qdrant 1.19+)
free() / [Symbol.dispose]released WASM allocationDispose Client and Stmt handles

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.