Skip to content

Rust SDK

Use qql-core when your service needs a transport-free parser and AST. Add qql when the same process should execute plans against Qdrant.

Install parser and runtime
cargo add qql-core qql
QQLTenant-scoped product searchTry in playground
QUERY 'laptops' FROM products USING dense LIMIT 10;

The parser requires complete input. Retain the typed Stmt and rewrite that object before planning or execution. The Filter injection guide details how the injected predicate lands in each statement type.

use qql_core::ast::{inject_filter, ComparisonOp, Value};
use qql_core::parser::Parser;
let mut stmt = Parser::parse(query)?;
inject_filter(
&mut stmt,
"tenant_id",
ComparisonOp::Eq,
Value::Str("org_99".into()),
)?;
// Optional request routing. This is not tenant isolation.
stmt.set_shard_key(Some("org_99".into()));

Executor::execute accepts one QQL source string. Use OnError::Stop to halt at the first failure or OnError::Continue to collect a report for every statement in a script.

use qql::executor::{Executor, OnError};
let executor = Executor::rest("http://localhost:6333", None)?;
let report = executor.execute(query, OnError::Stop).await?;
assert!(report.ok);
println!("{} operations succeeded", report.succeeded);

Rust keeps typed twins: execute_with_params / execute_with_positional_params on Executor, and qql_core::params::{bind_named, bind_positional} for standalone binding. Python, Node, and WASM collapse those into one bind(query, params) plus execute(..., params=...).

use std::collections::HashMap;
use qql::executor::{Executor, OnError};
use qql_core::ast::Value;
use qql_core::params::{bind_named, bind_positional};
let executor = Executor::rest("http://localhost:6333", None)?;
// Named parameters (:name)
let mut params = HashMap::new();
params.insert("cat".into(), Value::Str("laptops".into()));
params.insert("lim".into(), Value::Int(10));
let report = executor.execute_with_params(
"QUERY 'ultrabook' FROM products WHERE category = :cat LIMIT :lim",
&params,
OnError::Stop,
).await?;
// Positional parameters (?)
let pos_params = vec![Value::Str("laptops".into()), Value::Int(10)];
let report2 = executor.execute_with_positional_params(
"QUERY 'ultrabook' FROM products WHERE category = ? LIMIT ?",
&pos_params,
OnError::Stop,
).await?;
// Standalone binding (qql_core::params)
let bound = bind_named("QUERY :q FROM products LIMIT :lim", |k| match k {
"q" => Some(Value::Str("thinkpad".into())),
"lim" => Some(Value::Int(5)),
_ => None,
})?;

For text input, configure an embedder or provide a QUERY VECTOR / explicit vector value. The runtime also exposes Executor::grpc behind its gRPC feature.

Bulk ingest takes point values — payload as data, never SQL text. One :rows template is prepared once (schema fetched once), then each chunk splices through the point-splice path with no re-parse:

let rows: Vec<qql_core::ast::Value> = vec![
// {id: 1, vector: {dense: [...]}, …payload} per entry
];
let report = executor.upsert_many("docs", rows, 100, OnError::Stop).await?;

Chunks move (never clone) through the point-splice path; the peak live-set is the caller's rows vector by construction — for million-row ingests, chunk the calls, since upsert_many chunks transport, not memory.

Rust has no scroll helper type; loop SCROLL ... AFTER yourself with the same clause order the Python and Node helpers emit (WHERE then AFTER then SHARD then WITH VECTOR then LIMIT). Keep at most one page buffered and stop on the first empty page. Guard against a non-advancing cursor.

let mut cursor: Option<qql_plan::PlanPointId> = None;
loop {
let page = match &cursor {
None => "SCROLL FROM products LIMIT 100".to_string(),
// Integer ids interpolate directly; quote string ids.
Some(id) => format!("SCROLL FROM products AFTER {id} LIMIT 100"),
};
let report = executor.execute(&page, OnError::Stop).await?;
let hits = report.first_hits().unwrap_or_default();
if hits.is_empty() {
break;
}
let next = hits.last().map(|h| h.id.clone());
if next == cursor {
break;
}
for hit in &hits {
println!("hit {}", hit.id);
}
cursor = next;
}

For the managed equivalent, see Python Client.scroll_cursor and Node scrollCursor on their SDK pages.

Executor::explain_analyze (plus _with_named_params / _with_params twins) runs one statement and returns the static plan plus measured client phase timings and honest server telemetry. Every ExecResponse also carries telemetry: Option<ServerTelemetry> (time_s + hardware/inference usage, None where the route reports nothing), and typed result accessors (hits(), points(), ids(), facet(), count(), groups(), collections(), collection(), shard_keys(), quotas()) read from a cached typed representation — no JSON round-trip on the gRPC leg. The response model is closed (ExecData has no raw passthrough; the legacy *_json accessors are removed) and REST responses are parsed strictly per operation, failing closed with QQL-BACKEND-ENVELOPE on a missing or mistyped field. SearchHit carries id, score, payload, collection, and vector only — no text, version, or shard_key; derive text from payload["text"]:

let analysis = executor.explain_analyze(
"QUERY 'ultrabook' FROM products USING dense LIMIT 10",
OnError::Stop,
).await?;
println!("server: {:?}s", analysis.server_time_s);
println!("dispatch: {}ms", analysis.phases.dispatch_ms);

Qdrant 1.19+ can pin subsequent reads to a stable replica using the X-Qdrant-Route-Affinity header (HTTP) or equivalent gRPC metadata. This is transport configuration, not a QQL clause — set it on the client when you construct REST or gRPC access:

use qql::rest::RestQdrant;
// use qql::grpc::GrpcQdrant; // with the `grpc` feature
let client = RestQdrant::new("http://localhost:6333", None)
.with_route_affinity("session-or-user-id");
// GrpcQdrant::new(...).with_route_affinity("session-or-user-id")

Pass a stable session or user identifier so Qdrant can hash affinity consistently. Empty strings are treated as unset. The same helper exists on GrpcQdrant, and the Python (pyqql.Client(route_affinity=…)), Node (new Client({ routeAffinity })), and WASM (client.setRouteAffinity(key)) clients expose the equivalent option.

Parser::parsefn(&str) -> Result<Stmt, QqlError>
required

Parse exactly one complete statement.

Parser::parse_allfn(&str) -> Result<Vec<Stmt>, QqlError>
required

Parse one semicolon-delimited script.

inject_filterfn(&mut Stmt, &str, ComparisonOp, Value) -> Result<(), QqlError>
required

Add a trusted predicate to a parsed statement.

Executor::executeasync fn(&self, &str, OnError) -> Result<ExecutionReport>
required

Plan and execute one statement or a script against the configured backend.

SurfaceFunction or typePurpose
ParseParser::parse, Parser::parse_allStrict single-statement or script parsing
Inspectlexer::Lexer, explain, explain_allTokens and human-readable AST explanation
Rewriteinject_filter, Stmt::set_shard_keyTrusted AST mutation before planning
Planqql_plan::plan::plan, to_rest_routeOffline IR and Qdrant REST projection
ExecuteExecutor::execute, execute_batch, execute_nodeSource, batch, or owned-statement execution
TransportRestQdrant::with_route_affinity, GrpcQdrant::with_route_affinityPin reads via X-Qdrant-Route-Affinity
EmbedEmbedder, resolve_embeddingsResolve text, sparse, multi, image, or rerank inputs

For offline route inspection, use qql_plan::plan::plan followed by qql_plan::plan::to_rest_route before configuring any transport.