qql-embed
Shared embedding resolution layer. Contains the host-agnostic [Embedder] trait,
a hash-based BM25 [SparseEmbedder], and [resolve_embeddings] — the recursive
AST rewriter that converts text query/upsert inputs into vectors.
No Qdrant I/O, no HTTP client, no transport code. Used by qql-runtime
(HttpEmbedder), qql-edge (FastEmbedder), and qql-wasm (JS/fetch adapters).
Embedder trait
Dense embedding is always batched by model — every statement's text inputs
are collected into EmbeddingJob structs (qql-plan::embedding::extract_jobs),
grouped by model name, and sent as one batch per model.
resolve_embeddings — AST rewriter
use ;
let mut stmt = parse.unwrap;
resolve_embeddings.await?;
// stmt now has text → dense vector for point[0]
Resolution happens in these cases:
| Statement | Input source | Output |
|---|---|---|
QUERY 'text' ... USING name AS DENSE |
Bare string or TEXT '...' |
Query input rewrites to dense vector |
QUERY 'text' ... USING name AS SPARSE |
Bare string or TEXT '...' |
Query input rewrites to sparse vector |
QUERY HYBRID TEXT '...' |
Hybrid text | Dense + sparse vector pair |
UPSERT ... USING DENSE MODEL 'm' |
Payload text field |
Dense vector per point |
UPSERT ... USING HYBRID |
Payload text field |
Dense + sparse vectors per point |
UPSERT ... EMBED title INTO vec |
Explicit source field | Dense/sparse via embed directive |
| Auto-embed (no USING) | Payload text/body/content → default dense + sparse vectors |
Dense + sparse |
UPSERT with explicit VECTOR |
— | No embedding needed |
QUERY NEAREST VECTOR [...] |
— | No embedding needed |
QUERY NEAREST POINT 42 |
— | No embedding needed |
Vector roles and default names
Query targets carry a typed optional role (DENSE or SPARSE). Arbitrary
names such as semantic_v2 and lexical_v2 are supported; embedding behavior
never depends on a target literally being named dense or sparse.
DENSE_VECTOR_NAME:"dense"(constant)SPARSE_VECTOR_NAME:"sparse"(constant)
These constants are used only when materializing a new default topology or when an explicit target has not yet been resolved by the runtime.
SparseEmbedder — local BM25
Hash-based term-frequency tokenizer with IDF-like weighting. No network, no model downloads, no external dependencies. Used automatically as the sparse embedding backend for hybrid queries and hybrid upserts.
use SparseEmbedder;
let embedder = new;
let sv = embedder.embed_sparse.await?;
// sv.indices: [u32; N], sv.values: [f32; N]
Known WASM limitation
qql-wasm re-exports qql-embed types but the #[wasm_bindgen] API surface
does not include resolve_embeddings — the WASM runtime (Client) delegates
embedding to JS-side HTTP calls. See qql-wasm/src/lib.rs for details.
Features
std(default):std::error::Errorimpl- All types are
Send + Syncon non-wasm targets;?Sendon wasm32
Verification
Tests cover:
- Dense query text → vector resolution
- Hybrid query (dense + sparse) resolution
- UPSERT text payload → dense/sparse auto-embedding
- UPSERT USING DENSE MODEL / HYBRID resolution
- EMBBED directive with explicit source field and target vector name
- Sparse BM25 tokenization and IDF weighting
- Unused embedding detection (dense_iter exhaustion check)