Vector query (kNN) #
Finds the k nearest neighbors of a query vector — similarity search
over dense_vector / sparse_vector fields. The query vector can be
supplied directly, or as natural-language text that the serving layer
embeds with the field’s configured model (schema-driven semantic
search).
How it works #
Execution depends on where the documents live:
- Realtime (not yet flushed) — an exact brute-force scan over the
mutable store: every vector is scored, a size-
kmin-heap keeps the best, and a filter (see below) is tested before any distance is computed. - Flushed segments — the field’s
index_typedecides:flatscans the stored f32 vectors (exact,O(N·dims)); the graph types (hnsw_flat,turbo_quant_hnsw,bbq_hnsw) traverse the persisted HNSW graph with distances computed on the flat vectors — scores stay exact and only anef-bounded neighborhood is evaluated; the quantized scans (turbo_quant,bbq_flat) score compact codes for ak × oversamplingcandidate pool and rescore it with full precision (approximate). The per-type breakdown is in Index types.
Scores follow the field’s similarity (overridable per query, but it
must match the schema): cosine is the cosine similarity in
[-1.0, 1.0]; dot_product the raw dot product; l2 scores as the
negated squared Euclidean distance — 0 means identical vectors.
Higher is always more similar, so one top-k heap serves all three.
k bounds the vector leg, the request’s size bounds the page — a
standalone vector query returns min(k, size) hits. Setting
min_score changes the shape entirely: every vector above the
threshold is returned, no heap, no k.
Filters (bool with the vector clause in must and other
clauses in filter) never silently lose results: the brute-force
paths test the filter first and skip the distance; the graph path
widens its beam to roughly k / selectivity (clamped to the segment
size) and post-filters; and when a filter passes under ~5% of the
segment and supports ordered iteration, the engine switches to a
filter-led exact scan over just the filter’s documents — cheaper than
widening the beam a thousandfold.
When the request carries "query" text instead of a vector, the
serving layer embeds it through the field’s configured model before
execution (see
embedding endpoints).
Examples #
POST /my-collection/_search
{
"query": {
"vector": {
"field": "embedding",
"query_vector": [0.12, 0.45, 0.1, 0.0, -0.3, 0.2, 0.05, -0.1],
"k": 10
}
}
}
Text in, embedding server-side (requires an embedding endpoint — see below):
POST /my-collection/_search
{
"query": {
"vector": {
"field": "embedding",
"query": "find documents about search engines",
"k": 10
}
}
}
Embedding endpoints (server-side inference) #
Endpoints are declared in pizza.yml under
node.embedding
— the full key reference lives in the configuration page.
Pizza embeds text through external, OpenAI-compatible embedding services — configure them once and both the write path (schema-driven) and the search path (text queries) work with plain text:
node:
embedding:
endpoints:
- name: openai
url: "https://api.openai.com/v1/embeddings"
api_key: "sk-…"
models: ["text-embedding-3-small"]
- name: local
url: "http://127.0.0.1:11434/v1/embeddings" # Ollama
models: ["nomic-embed-text"]
Each models entry may also declare its dims, its features
(embedding, the default, or generation — served through the
POST /_chat
facade once the endpoint declares chat_url) and its inputs (text,
the default, or image) — see
AI Services.
- Write time — a
dense_vectorfield mapped withvector_options.source(a text field) andmodelderives its vector from the source text when the document does not supply one; explicit client vectors always win. - Search time — a
vectorquery (or hybridretrievevector leg) carrying"query"text and no vector is embedded before execution. The model comes from the field schema, so the search must target exactly one collection. - API —
POST /_embeddingexposes the same capability with the standard OpenAI request/response shapes ({"model", "input"}in, thedata[].embeddinglist out). - A model id routes to the endpoint listing it; with exactly one
endpoint configured, every model routes there (the backend decides
what it serves). Schema declarations (
semantic_text, semantic multi-fields, source-mappeddense_vector) are stricter: the model must be listed by exactly one service, or the service must be named — see AI services.
Declarative form — semantic_text
#
The simplest wiring is the
semantic_text
field type: one declaration, plain-text writes, pure-semantic search
(no inverted index on the field):
PUT /my-collection
{
"schema": {
"properties": {
"body": {
"type": "semantic_text",
"service": "zai",
"model": "GLM-4.6V",
"dims": 3072
}
}
}
}
dims may be omitted when the service’s models entry declares it;
service disambiguates same-named models across services.
Declarative form — the semantic multi-field #
The explicit form underneath: declare a dense_vector sub-field on
the text field itself, and the parent becomes the source implicitly
(adding "index": false on the parent reproduces the pure-semantic
semantic_text layout; by default the parent stays BM25-searchable —
hybrid out of the box):
PUT /my-collection
{
"schema": {
"properties": {
"body": {
"type": "text",
"fields": {
"embedding": {
"type": "dense_vector",
"vector_options": { "model": "text-embedding-3-small", "dims": 1536 }
}
}
}
}
}
}
Writes stay plain-text ({"body": "some text"}) — the vector derives
server-side into body.embedding, and vector/knn queries naming the
parent auto-rewrite to the sub-field (the plain dotted path works too).
Preconditions, each enforced with a 400 at schema-apply time:
- The parent must be a top-level
textorkeywordfield. - The sub-field must be
dense_vector(sparse_vectorsub-fields are not supported). vector_options.modelis required and must be listed by exactly one service (or the namedvector_options.servicemust list it) — list the model under a service in the embedding endpoints configuration first.vector_options.dimsis required unless the service’smodelsentry declares it (probe a service once with_testto learn the width).vector_options.sourcemust not be set — the source is implicitly the parent field.
Semantic query #
The semantic query removes the last piece of boilerplate — the field
name. (Full parameter reference on the
semantic query
page.) A collection whose schema declares a semantic field — a
semantic_text field, a semantic
multi-field, or the standalone dense_vector with source + model —
is searchable with plain text alone:
POST /my-collection/_search
{"query": {"semantic": "find documents about search engines"}}
# object form — k / field / similarity / min_score all optional
{"query": {"semantic": {"query": "search engines", "k": 10}}}
# composes like any leaf — hybrid search without the retriever DSL
{"query": {"bool": {"should": [
{"semantic": "search engines"},
{"match": {"field": "title", "query": "engines"}}
]}}}
- The field resolves from the schema: exactly one semantic field is
inferred; with several, name one via the semantic query’s
field(400 names the candidates otherwise). - The text embeds server-side through the field’s configured model — the same inference path as explicit text vector queries.
- The sugar is translated at the gateway into the native
vectorquery; the search must target exactly one collection (the schema is where the field and model live).
Quantized search with per-query overrides:
POST /my-collection/_search
{
"query": {
"vector": {
"field": "embedding",
"query_vector": [0.12, 0.45, ...],
"k": 10,
"num_candidates": 200,
"vector_options": {
"index_type": "turbo_quant",
"bit_width": 4,
"oversampling": 5.0
}
}
}
}
Query parameters #
Parameters accepted inside the vector query object.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
field | string | yes | — | Vector field name (dense_vector or sparse_vector). |
query_vector | number array | one of | — | The query embedding. (The retrieve-style retriever DSL spells this key vector; inside a vector query it is query_vector.) On a sparse_vector field, non-zero positions are read as dimension ids (position-encoded form). |
query_sparse | object / pair array | one of | — | Sparse query as dimension-id → value pairs: {"5": 0.3, "17": 0.9} or [[5, 0.3], [17, 0.9]]. For sparse_vector fields; takes precedence over query_vector. |
query | string | one of | — | Natural-language text; the field’s configured embedding model converts it into the query vector when query_vector is absent. |
k | integer | no | 10 | Nearest neighbors the vector leg produces. The final page is capped by the request’s size — a standalone vector query returns min(k, size) hits; in a retrieve/fuse pipeline k bounds each leg and size bounds the merged result. |
num_candidates | integer | no | unset | Graph beam width for the query (HNSW ef). Unset uses the field’s ef_search. Values below k are clamped up to k. Higher = better recall, more work. |
similarity | string | no | cosine | Similarity metric for ranking: cosine, l2 or dot_product. Must match the field schema’s similarity. |
min_score | number | no | — | Minimum score threshold. When set, returns all vectors above the score instead of top-k — useful for hybrid text+vector intersection. |
vector_options | object | no | — | Per-query overrides of the field schema’s index/search knobs. See below. |
k vs size, live over the embeddings dataset (24 documents in
three well-separated clusters): ask the vector leg for the 8 nearest
neighbors but cap the page at 3 — exactly 3 hits come back, ranked by
similarity:
Per-query vector_options
#
Every key is optional; each overrides the field schema’s value for this query only.
| Key | Type | Applies to | Description |
|---|---|---|---|
index_type | string | all | Forces a specific read path for this query: flat, hnsw_flat, bbq_flat, bbq_hnsw, turbo_quant, turbo_quant_hnsw. E.g. flat forces an exact scan even when the segment carries quantized codes. |
bit_width | integer | TurboQuant | Quantization width (2, 3, 4) for the realtime layer’s on-the-fly scan. Persisted segments serve codes at the schema’s width — set it there. |
oversampling | number | quantized | Candidate-pool multiplier before rescoring (k × oversampling candidates). |
rescore_vector | boolean | quantized | Rerank the candidate pool with full-precision vectors (recommended true). |
ef_search | integer | graph types | Query-time graph beam width. The top-level num_candidates takes precedence when both are set. |
m, ef_construction | — | — | Build-time graph parameters — schema-level only, ignored in queries. |
Field schema parameters #
The vector field declares dimensionality, similarity and index type
once in the collection schema. Parameters live in the property itself
and in its vector_options object:
PUT /my-collection
{
"schema": {
"properties": {
"embedding": {
"type": "dense_vector",
"source": false,
"vector_options": {
"dims": 128,
"similarity": "cosine",
"index_type": "turbo_quant_hnsw",
"bit_width": 4,
"oversampling": 3.0,
"rescore_vector": true,
"m": 16,
"ef_construction": 128,
"ef_search": 64
}
}
}
}
}
Property-level parameters #
| Parameter | Type | Default | Description |
|---|---|---|---|
type | string | — | dense_vector (or sparse_vector). |
source | boolean | true | When false, the raw vector is excluded from _source — it never appears in GET or search-hit rendering, saving storage for large embeddings. Search keeps working through the dedicated vector sections, and merges/reshards recover the vectors from FIELD_VECTOR. Note: this is the storage switch; the embedding input field is vector_options.source below. |
column, store | boolean | false | Generic storage switches; dense vectors do not use columnar or separate-store paths — leave unset. |
vector_options parameters
#
| Parameter | Type | Range / values | Default | Description |
|---|---|---|---|---|
dims | integer | 1..=4096 | — | Vector dimensionality (required for dense_vector; for sparse_vector it is the dimension-id space hint). TurboQuant additionally requires dims >= 8 and dims % 8 == 0. |
similarity | string | cosine, l2, dot_product | cosine | Similarity metric used at index and query time. |
index_type | string | see table below | flat | Index strategy: brute-force, graph, and/or quantization. |
bit_width | integer | 2, 3, 4 | 4 | TurboQuant scalar-quantization width. 2-bit is experimental and requires rescore_vector: true. |
oversampling | number | ≥ 1.0 | 3.0 | Candidate-pool multiplier for quantized scans (k × oversampling candidates before rescore). Low-dimension BitQ fields need a higher factor (e.g. 8.0) to hold recall. |
rescore_vector | boolean | — | true | Rerank quantized candidates with full-precision vectors. |
m | integer | 2..=256 | 16 | HNSW graph connectivity — max neighbors per node per layer. Higher = better recall, more build time and memory. |
ef_construction | integer | 1..=4096 | 128 | HNSW build-time beam width. |
ef_search | integer | 1..=4096 | 64 | HNSW query-time beam width (the default ef for queries against this field). |
source | string | field name | — | Embedding input: a text field whose content is embedded into this vector field at index time (schema-driven embeddings). |
model | string | model id | — | Embedding model — a HuggingFace id for local ONNX inference (e.g. all-MiniLM-L6-v2) or the model name for remote endpoints. Effective together with source. |
Out-of-range values are rejected with HTTP 400 at collection creation.
Index types #
Six index types cover the exact / approximate × unquantized /
quantized space. Both quantization families persist their codes in the
segment (FIELD_TURBOQUANT, FIELD_BBQ), and every graph variant
persists the graph (FIELD_HNSW).
index_type | Quantization | Compression | Graph | Precision of served scores | Parameters that apply |
|---|---|---|---|---|---|
flat | none | 1x | no | exact | dims, similarity |
hnsw_flat | none | 1x | yes | exact | dims, similarity, m, ef_construction, ef_search |
bbq_flat | BitQ binary (1 bit/dim) | ~32x | no | approximate + rescore | dims, similarity, oversampling, rescore_vector |
bbq_hnsw | BitQ binary (1 bit/dim) | ~32x | yes | exact (graph) / approx (codes fallback) | dims, similarity, oversampling, rescore_vector, m, ef_construction, ef_search |
turbo_quant | TurboQuant scalar (2–4 bit) | ~8x @ 4-bit | no | approximate + rescore | dims (≥8, %8==0), similarity, bit_width, oversampling, rescore_vector |
turbo_quant_hnsw | TurboQuant scalar (2–4 bit) | ~8x @ 4-bit | yes | exact (graph) / approx (codes fallback) | dims (≥8, %8==0), similarity, bit_width, oversampling, rescore_vector, m, ef_construction, ef_search |
Choosing between them:
- Exact results needed, small-to-medium data —
flat. - Exact results, larger data —
hnsw_flat(graph traversal, ~15x faster than the flat scan on a 2 000 × 128d segment, scores identical to brute force). - Minimal memory footprint —
bbq_flat/bbq_hnsw(~32x compression). BitQ works at any dimensionality; budgetoversamplingfor recall. - Balanced quantization, zero training —
turbo_quant/turbo_quant_hnsw(~8x at 4-bit). The graph variant measured 12 176 QPS vs 830 (exact flat) with recall@10 0.982 vs the quantized scan’s 0.801 on a 2 000 × 128d segment.
How each type executes #
flatscans the stored f32 vectors — exact, O(N·dims).turbo_quant/bbq_flatscan the quantized codes for a candidate pool ofk × oversampling, then rescore that pool with the flat vectors (rescore_vector: true).- Graph types (
hnsw_flat,turbo_quant_hnsw,bbq_hnsw) traverse the persisted HNSW graph once the segment is memory-mapped. Distances are computed on the flat vectors, so scores are exact and only anef-bounded neighborhood is evaluated; the quantized-code scan (for the two quantized variants) serves as fallback when a segment predates the graph or fails to load it. - Filtered kNN (
bool { must: [vector], filter: [...] }) widens the graph beam proportionally to the filter’s selectivity (ef ≈ max(ef, k · n / |filter|)) and traverses the same graph; quantized-only fields fall back to an exact scan over the filter set.
Storage footprint #
Two facts drive on-disk size (measured at 1 024d, bytes per vector, whole segment file / vector count):
| Configuration | B/vector | vs raw f32 |
|---|---|---|
| raw f32 payload (reference) | 4 096 | 1.00x |
flat | 8 294 | 2.02x |
flat + source: false | 4 134 | 1.01x |
turbo_quant | 9 342 | 2.28x |
turbo_quant_hnsw + source: false | 5 336 | 1.30x |
- By default the vector is stored twice — the
FIELD_VECTORsection (the scan/rescore source of truth) and a copy inside the stored document (_source).source: falseremoves the_sourcecopy: that is the disk-footprint lever. - Quantization buys query speed and scan I/O, not disk: the codes
are an additional section (TurboQuant 4-bit ≈ 0.5 B/coordinate,
BitQ ≈ 0.125 B/coordinate) and the HNSW graph adds ≈ 150 B/vector at
m: 16. The flat section must stay — merges, resharding and rescoring rebuild from it (quantization is lossy). - The practical speed + storage configuration is a quantized graph
index with
source: false: 1.30x the raw payload (vs 2.02x default flat) with ~12–15x query speedup.
Segments holding vector data always merge in the FireV11 format, which preserves the quantized and graph sections across compaction. Rolling upgrades must reach every data node before relying on this: nodes running older builds still merge vector-bearing segments through the legacy FireV10 path, which drops the quantized sections.
Sparse vectors #
sparse_vector fields store per-document (dimension id, value) pairs
— the natural shape for learned-sparse embeddings (SPLADE-style) and
tag/feature vectors where most dimensions are zero. Only the non-zero
entries are stored, so a sparse field costs memory proportional to the
number of populated dimensions, not to dims.
PUT /my-collection
{
"schema": {
"properties": {
"tags": {
"type": "sparse_vector",
"vector_options": {
"dims": 65536,
"similarity": "dot_product"
}
}
}
}
}
Documents accept two value forms (they serialize to the same rows):
PUT /my-collection/_doc/s1
{ "title": "one", "tags": {"2": 1.0, "5": 1.0} } # object form
PUT /my-collection/_doc/s2
{ "title": "two", "tags": [[2, 0.5], [7, 1.0]] } # pair-array form
Queries take the same two forms through query_sparse:
POST /my-collection/_search
{
"query": {
"vector": {
"field": "tags",
"query_sparse": {"2": 1.0, "5": 1.0},
"k": 10,
"similarity": "dot_product"
}
}
}
How a sparse field executes:
- Scoring is a merge-join over the sorted id lists (dot product or cosine) — O(nnz(query) + nnz(doc)) per document, no dense materialization anywhere.
- Realtime documents are searchable immediately through the
mutable store’s sparse index; flush builds a dedicated
FIELD_SPARSEsection (CSR layout: one row per document, zero-copy from the memory-mapped segment). - Filtered kNN (
bool { must: [vector], filter: [...] }) scores only the filter’s candidate set. - Merges and
source: falseare safe: compaction rebuilds sparse rows from theFIELD_SPARSEsection, so excluding the field from_source(the storage lever, same as dense) never loses the data. - The dense index types and their quantization/graph parameters
(
index_type,bit_width,oversampling,m, …) do not apply to sparse fields;dimsandsimilarityare the relevant knobs. Storage is 8 bytes per non-zero entry plus 8 bytes of per-document row overhead.
Combine several vector queries (multi-field, mixed dense/sparse) with
the
multi_vector query.
Use cases #
- Semantic similarity search. Match by meaning where keywords
fail — synonyms, paraphrases, cross-lingual text. The teaching
embeddingsdataset is built for this: 24 documents in three well-separated 8-document clusters (search,storage,vector), and querying with the search-cluster centroid atk = 8returns exactly the eightsearchdocuments. - Hybrid retrieval. Vector recall beside BM25 precision, fused
with RRF — the
retrieve/fusepipeline runs vector legs and text legs side by side over the same dataset. - Recommendations and more-like-this. Embed the item the user is looking at and take its nearest neighbors as the candidate list — no query text ever exists.
- Thresholded recall for pipelines.
min_scorereturns all vectors above a similarity floor, feeding deduplication, clustering or RAG chunk selection where a fixedkwould be arbitrary. - Learned-sparse ranking.
sparse_vectorfields carry SPLADE-style weighted term ids and score by dot product — the same kNN machinery, memory proportional to the populated dimensions.
Related #
multi_vector— several vector fields (or dense + sparse) searched at once and fused into one ranking.semantic— the same query with the field and model resolved from the schema; the request is plain text.- Hybrid search pipeline — vector legs
inside the
retrieve/fuse/rescorechain. - Hybrid search tutorial — a
walkthrough over the
embeddingsdataset.