Skip to content

Edge Rust

Remote Qdrant version: Rust SDK.

The qql-edge crate combines the qql runtime executor with qdrant-edge (in-process HNSW) and, optionally, fastembed-rs (local ONNX embeddings). The result is a normal [qql::executor::Executor] whose backend is embedded in your process.

qql-edge
Cargo
cargo add qql-edge

Features:

FeatureDefaultProvides
fastembed-localyesLocal ONNX embedding via fastembed-rs
http-embeddingnoOpenAI-compatible HTTP embeddings (http_executor)

With neither feature, only custom_executor is available.

local_executor gives you a dense-only offline executor using the default model BGESmallENV15 (384-dimensional), with payloads in memory when you pass false:

use qql_edge::local_executor;
use qql::executor::OnError;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut executor = local_executor("./qdrant_data", false)?;
let report = executor
.execute("CREATE COLLECTION docs HYBRID", OnError::Stop)
.await?;
assert!(report.ok);
executor
.execute(
"UPSERT INTO docs VALUES {id: 1, text: 'hello from edge'}",
OnError::Stop,
)
.await?;
let report = executor
.execute("QUERY 'hello' FROM docs USING dense LIMIT 5", OnError::Stop)
.await?;
println!("found {} hits", report.results[0].data.map(|d| d.to_string()).unwrap_or_default());
executor.close().await?; // flush before deleting the data directory
Ok(())
}

OnError::Stop halts at the first failing statement; OnError::Continue collects a report for every statement in a script.

FunctionEmbedderUse case
local_executor(path, on_disk_payload)FastEmbed (default dense model)Fully offline dense search
local_executor_with_options(path, options)FastEmbed + optional sparse/multi/image/rerankerOffline dense + sparse + ColBERT + CLIP + cross-encoder
http_executor(path, on_disk_payload, url, key, model, dim)OpenAI-compatible HTTP endpointLocal storage, remote embeddings
http_executor_with_multi(path, on_disk_payload, url, key, model, dim, multi_url, multi_key, multi_model, multi_dim)HTTP + optional multi/ColBERT endpointLocal storage, remote dense + multi
custom_executor(path, on_disk_payload, embedder)Any Arc<dyn Embedder>GPU, ensemble, caching, your own inference
list_embedding_models()Catalog of loadable ONNX models

All constructors return Result<Executor, qql_core::error::QqlError>.

LocalExecutorOptions maps one-to-one onto the five fastembed slots plus storage behavior:

use qql_edge::{local_executor_with_options, LocalExecutorOptions};
use qql::executor::OnError;
let mut executor = local_executor_with_options(
"./qdrant_data",
LocalExecutorOptions {
on_disk_payload: true,
wal_segment_capacity: Some(4 * 1024 * 1024), // shrink the 32 MiB WAL segments
model: Some("bge-small-en-v1.5".into()), // dense, 384-d
sparse_model: Some("splade".into()), // real ONNX sparse
multi_model: Some("bge-m3".into()), // ColBERT multivector
image_model: None, // no CLIP vision
reranker_model: Some("bge-reranker-base".into()), // CROSS RERANK
cache_dir: Some("/var/cache/qql-models".into()),
show_download_progress: true,
},
)?;
executor
.execute("CREATE COLLECTION docs HYBRID", OnError::Stop)
.await?;

Models are locked at construction: a USING MODEL '…' clause that names a model the embedder does not own fails at execution time instead of silently switching models. See models.

Storage and search stay local; only embeddings are remote. Works with any OpenAI-compatible endpoint — Ollama (/v1/embeddings), OpenAI, Cohere, Together, Mistral:

use qql_edge::http_executor;
use qql::executor::OnError;
let mut executor = http_executor(
"./qdrant_data",
false,
"http://localhost:11434/v1/embeddings", // endpoint
"", // api key (empty = none)
"nomic-embed-text", // model
768, // dimension
)?;

Plug in any Arc<dyn Embedder>. If your embedder reports a dimension, edge auto-sizes CREATE COLLECTION HYBRID; otherwise pass a QqlConfig with embedding_dimension set via Executor::with_embedder.

FastEmbedder wraps fastembed-rs behind the qql_embed::Embedder trait and is what every local constructor builds internally. You can construct it directly for full control:

use qql_edge::{FastEmbedder, FastEmbedderOptions};
let embedder = FastEmbedder::try_with_options(FastEmbedderOptions {
model: Some("ClipVitB32".into()), // CLIP text
image_model: Some("clip-vision".into()), // CLIP vision
..Default::default()
})?;

Useful accessors: model_name(), model_code(), dimension(), multi_dimension(), image_dimension(), has_sparse(), has_multi(), has_image(), has_reranker().

For lower-level control you can build the backend directly. EdgeQdrant implements the QdrantOps trait that REST and gRPC clients also implement:

use qql_edge::EdgeQdrant;
let backend = EdgeQdrant::new("./qdrant_data", true); // base_path, on_disk_payload

EdgeQdrant::new(base_path, on_disk_payload) opens shards lazily per collection and persists them under base_path. with_wal_segment_capacity shrinks the pre-allocated WAL segments (bytes): it seeds shards without a persisted capacity, then the persisted value wins. Combined with an embedder and a QqlConfig, wrap it in a runtime Executor via Executor::with_embedder. The close() method (also on Executor) drains and flushes all open shards.

Snapshot seeding helpers live at the crate root: qql_edge::unpack_snapshot (unpack a downloaded snapshot archive into a directory) and qql_edge::inspect_shard (load it and return counts). These wrap the engine's snapshot API — never unpack or merge archives by hand.

Response shaping matches the native SDKs' typed ExecData report: each operation reads exactly its Qdrant OpenAPI response field and emits the same shapes ([{id, score, payload, vector?}] with no separate text, {"groups": [...]}, {"count": n}, {"collections": [...]}, typed CollectionInfo, {"shard_keys": [...]} , quota config). Hits carry id, score (shortest f32 round-trip), payload, collection (cross-collection only), and vector (only with WITH VECTOR) — there is no separate text field; derive it from payload["text"]. 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.

Operationdata shape
Query / scroll / points[{id, score, payload, collection?, vector?}] (points carry no separate text)
Grouped query{"groups": [{id, hits: [...]}]}
Facet[{value, count}]
Count / mutation count{"count": N} (status-only mutations serialize null)
SHOW COLLECTIONS{"collections": [<name>, ...]} (list of names)
SHOW COLLECTIONtyped CollectionInfo (status, points_count, indexed_vectors_count, …)
SHOW SHARD KEYS{"shard_keys": [...]}
Quotasquota config object

Offline-unsupported features fail with stable QQL-EDGE-UNSUPPORTED-* codes plus a remediation hint — never a silent no-op. String point IDs that are not UUIDs fail with QQL-EDGE-INVALID-POINT-ID. See capabilities for the catalog.

Call executor.close().await? before deleting the data directory so qdrant-edge can flush the WAL and segments while the files still exist. See persistence.