{ "prefetch": [ { "query": { "nearest": [[-0.83, 0.32, …, 0.83]] }, "using": "dense_vec", "filter": { "must": [{ "key": "status", "match": { "value": "active" } }] }, "limit": 100 }, { "query": { "nearest": { "indices": [3719, 12808, 41172], "values": [0.72, 1.34, 0.41] } }, "using": "bm25_vec", "filter": { "must": [{ "key": "status", "match": { "value": "active" } }] }, "limit": 100 } ], "query": { "fusion": "rrf" }, "limit": 10}Open source · MIT · v0.4.0
SQL for Qdrant. QQL vector-search language
One declarative statement for hybrid search, filters, mutations and schema. Runs everywhere Qdrant does.
curl -fsSL https://…/install.sh | sh
QUERY 'chest pain' FROM medicalUSING denseWHERE department = 'cardio'SHARD 'hospital-east'LIMIT 5;- Query forms
- 12
- Conformance statements
- 276
- Runtimes & tools
- 6
- WASM parser, in-browser
- 2.5 MB
Same query. No boilerplate.
Pick REST JSON or any SDK client. The QQL stays a few lines.
results = client.query_points( collection_name="docs", prefetch=[ models.Prefetch( query=[[-0.83, 0.32, …, 0.83]], using="dense_vec", filter=status_filter, limit=100, ), models.Prefetch( query=models.SparseVector( indices=[3719, 12808, 41172], values=[0.72, 1.34, 0.41], ), using="bm25_vec", filter=status_filter, limit=100, ), ], query=models.FusionQuery(fusion=models.Fusion.RRF), limit=10,)const results = await client.query("docs", { prefetch: [ { query: [[-0.83, 0.32, …, 0.83]], using: "dense_vec", filter: { must: [{ key: "status", match: { value: "active" } }] }, limit: 100, }, { query: { indices: [3719, 12808, 41172], values: [0.72, 1.34, 0.41] }, using: "bm25_vec", filter: { must: [{ key: "status", match: { value: "active" } }] }, limit: 100, }, ], query: { fusion: "rrf" }, limit: 10,});let response = client.query( QueryPointsBuilder::new("docs") .add_prefetch( PrefetchQueryBuilder::default() .query(Query::new_nearest(vec![-0.83, 0.32, /* …, */ 0.83])) .using("dense_vec") .filter(active_filter.clone()) .limit(100u64), ) .add_prefetch( PrefetchQueryBuilder::default() .query(Query::new_nearest(SparseVector { indices: vec![3719, 12808, 41172], values: vec![0.72, 1.34, 0.41], })) .using("bm25_vec") .filter(active_filter) .limit(100u64), ) .query(Query::new_fusion(Fusion::Rrf)) .limit(10u64),).await?;res, err := client.Query(ctx, &qdrant.QueryPoints{ CollectionName: "docs", Prefetch: []*qdrant.PrefetchQuery{ { Query: qdrant.NewQueryDense([]float32{-0.83, 0.32 /* …, */, 0.83}), Using: qdrant.Ptr("dense_vec"), Filter: statusFilter, Limit: qdrant.Ptr(uint64(100)), }, { Query: qdrant.NewQuerySparse([]uint32{3719, 12808, 41172}, []float32{0.72, 1.34, 0.41}), Using: qdrant.Ptr("bm25_vec"), Filter: statusFilter, Limit: qdrant.Ptr(uint64(100)), }, }, Query: qdrant.NewQueryFusion(qdrant.Fusion_RRF), Limit: qdrant.Ptr(uint64(10)),})QUERY 'distributed consensus' FROM docsUSING HYBRID DENSE dense_vec SPARSE bm25_vec FUSION RRFWHERE status = 'active'LIMIT 10;{ "shard_key": "tenant-corp-99", "query": { "nearest": "dense_vec" }, "filter": { "must": [ { "key": "department", "match": { "value": "finance" } } ] }, "limit": 5, "with_payload": true}results = client.query_points( collection_name="filings", shard_key_selector="tenant-corp-99", query=dense_vector, query_filter=models.Filter(must=[ models.FieldCondition(key="department", match=models.MatchValue(value="finance")), ]), limit=5, with_payload=True,)const results = await client.query("filings", { shardKey: "tenant-corp-99", query: denseVector, filter: { must: [{ key: "department", match: { value: "finance" } }] }, limit: 5, withPayload: true,});let response = client.query( QueryPointsBuilder::new("filings") .shard_key_selector("tenant-corp-99") .query(Query::new_nearest(dense_vector)) .filter(Filter::must([Condition::matches("department", "finance")])) .limit(5u64) .with_payload(true),).await?;res, err := client.Query(ctx, &qdrant.QueryPoints{ CollectionName: "filings", ShardKeySelector: &qdrant.ShardKeySelector{ ShardKeys: []*qdrant.ShardKey{qdrant.NewShardKey("tenant-corp-99")}, }, Query: qdrant.NewQueryNearest(denseVector), Filter: &qdrant.Filter{ Must: []*qdrant.Condition{qdrant.NewMatch("department", "finance")}, }, Limit: qdrant.Ptr(uint64(5)), WithPayload: qdrant.NewWithPayload(true),})QUERY 'annual financial risks' FROM filingsWHERE department = 'finance'SHARD 'tenant-corp-99'LIMIT 5;{ "query": { "nearest": "dense_vec" }, "filter": { "must": [ { "should": [ { "key": "category", "match": { "value": "hardware" } }, { "key": "category", "match": { "value": "tools" } } ] }, { "key": "price", "range": { "lte": 1200.0 } }, { "key": "in_stock", "match": { "value": true } } ] }, "limit": 20}results = client.query_points( collection_name="products", query=product_vector, query_filter=models.Filter(must=[ models.Filter(should=[ models.FieldCondition(key="category", match=models.MatchValue(value="hardware")), models.FieldCondition(key="category", match=models.MatchValue(value="tools")), ]), models.FieldCondition(key="price", range=models.Range(lte=1200.0)), models.FieldCondition(key="in_stock", match=models.MatchValue(value=True)), ]), limit=20,)const results = await client.query("products", { query: productVector, filter: { must: [ { should: [ { key: "category", match: { value: "hardware" } }, { key: "category", match: { value: "tools" } }, ], }, { key: "price", range: { lte: 1200.0 } }, { key: "in_stock", match: { value: true } }, ], }, limit: 20,});let filter = Filter::must([ Condition::should([ Condition::matches("category", "hardware"), Condition::matches("category", "tools"), ]), Condition::range("price", Range { lte: Some(1200.0), ..Default::default() }), Condition::matches("in_stock", true),]);
let response = client.query( QueryPointsBuilder::new("products") .query(Query::new_nearest(product_vector)) .filter(filter) .limit(20u64),).await?;res, err := client.Query(ctx, &qdrant.QueryPoints{ CollectionName: "products", Query: qdrant.NewQueryNearest(productVector), Filter: &qdrant.Filter{ Must: []*qdrant.Condition{ qdrant.NewFilterCondition(&qdrant.Filter{ Should: []*qdrant.Condition{ qdrant.NewMatch("category", "hardware"), qdrant.NewMatch("category", "tools"), }, }), qdrant.NewRange("price", &qdrant.Range{Lte: qdrant.Ptr(1200.0)}), qdrant.NewMatch("in_stock", true), }, }, Limit: qdrant.Ptr(uint64(20)),})QUERY 'industrial robotics' FROM productsWHERE (category = 'hardware' OR category = 'tools') AND price <= 1200.0 AND in_stock = trueLIMIT 20;Native panes inline precomputed vectors from an out-of-band embedding step the SDKs and REST API can't express; QQL resolves text in-process. All panes are truncated (…); line counts exclude that pipeline on both sides.
Declarative queries compile directly to optimized Qdrant REST and gRPC payloads with zero runtime overhead.
Full comparison in the docs →How a statement runs.
One pass from source to dispatch. The plan is the contract between every runtime.
- 01
Parse
qql-core lexes and parses into a typed AST. Malformed clauses fail with a span, never a silent default.
- 02
Validate
Named vectors resolve against the collection schema. Unknown USING kinds fail closed.
- 03
Plan
plan() lowers the AST to one transport-neutral PlannedOperation.
- 04
Dispatch
The same plan projects to REST JSON, gRPC protobuf, or the in-process edge backend.
One grammar. The whole surface.
Hybrid retrieval, faceting, and formula scoring. Twelve query forms over one typed grammar.
QUERY TEXT 'vector database' FROM docs USING HYBRID DENSE dense SPARSE bm25 FUSION RRF LIMIT 10;QUERY FORMULA MAX(MIN($score * 2.0, 10.0), 0.0) + ACOSH(popularity + 1.0) DEFAULTS (score = 0.0, popularity = 0)FROM papers LIMIT 10;FACET room_type FROM stays WHERE price < 150 LIMIT 5 EXACT true;One statement. Every runtime.
Same plan, six surfaces: CLI, Python, Node, Rust, WASM, and VS Code.
Questions
What is QQL?
A typed query language for Qdrant. One surface for retrieval, filtering, mutations, schema, and policy-safe AST rewriting.
Which runtimes ship today?
Rust crates, native Python and Node.js bindings, a ~2.5 MB WASM package, the qql CLI, and a VS Code extension with live diagnostics.
How does multitenancy work?
Parse untrusted QQL, then inject a trusted tenant filter into the AST before planning. SHARD routing is a separate locality concern and can run alongside the filter.
Does QQL replace Qdrant?
No. QQL plans operations for Qdrant and dispatches them over REST or gRPC, or evaluates the supported subset through the in-process edge backend.
Is it production-ready?
It is young: v0.4.0. Fail-closed defaults, OpenAPI contract tests, a conformance corpus, and a public gaps document. The API surface is stabilizing, not frozen.
Can I try it without a cluster?
The WASM parser runs fully offline in the browser. To execute, point the playground at Docker or your Qdrant URL, or run qql-edge for local HNSW storage.
WASM parser · MIT
Parse a query in the browser.
The playground runs the real WASM parser locally: tokens, AST, plan and wire JSON. Point it at Docker or your Qdrant URL to execute.
No cluster · no signup