Semantic query

Semantic query #

Semantic search with zero field knowledge: the vector field and the embedding model resolve from the collection schema, and the request is plain text — the retrieval question asked without naming a field, a model, or a vector.

Examples #

POST /my-collection/_search
{"query": {"semantic": "find documents about search engines"}}

The clause composes inside bool like any other leaf — the text is embedded and the clause rewritten before execution:

POST /my-collection/_search
{
  "query": {
    "bool": {
      "should": [
        { "semantic": "search engines" },
        { "match": { "field": "title", "query": "engines" } }
      ]
    }
  }
}

How it works #

semantic is sugar over the native vector query, resolved and rewritten at the gateway in three steps:

graph LR Q["semantic: 'search engines'
plain text, no field"] --> G("gateway") G -->|"resolve from the schema:
the vector field + its model"| E("embed server-side") E --> R["the clause rewrites into
a native vector query"]

  1. Field resolution. The collection’s schema is scanned for semantic fields (a semantic_text field, a semantic multi-field, or a dense_vector with source + model). Exactly one makes it the default; several require the query’s field to name one.
  2. Embedding. The text goes through the field’s configured model — the same inference path (and the same model) that derived the documents’ vectors at write time, so both sides of the search live in one embedding space.
  3. Rewrite. The clause becomes a vector query on the resolved field, with k, num_candidates, similarity and min_score passed straight through. From there the standard kNN machinery executes: index type, quantization overrides and filters all behave exactly as on an explicit vector query.

Because the field and model live in the mapping, the query never names them — and the search must target exactly one collection (see Notes).

Three steps, server config first:

1. Configure an embedding endpoint — the server embeds through external OpenAI-compatible services (OpenAI, vLLM, Ollama, TEI, …), declared in pizza.yml under node.embedding:

node:
  embedding:
    endpoints:
      - name: openai
        url: https://api.openai.com/v1/embeddings
        api_key: sk-…
        models: ["text-embedding-3-small"]
    timeout_ms: 30000
    max_batch_texts: 64

A model id routes to the endpoint that lists it as an embedding model; with exactly one endpoint configured, every model routes there (default_endpoint overrides) — that lenient routing serves ad-hoc /_embedding calls. Schema declarations are stricter: the model must be listed as an embedding model by exactly one service, or the mapping names service explicitly (see AI Services). Endpoints behind self-signed TLS set insecure_skip_verify: true (per endpoint). The registry can also be re-managed at runtime — see AI Services — and every node.embedding key is in the configuration reference.

2. Map a semantic field — the simplest form is the semantic_text type, which collapses field + wiring into one declaration (pure semantic — the field is not inverted-indexed):

PUT /my-collection
{
  "schema": {
    "properties": {
      "body": {
        "type": "semantic_text",
        "service": "openai",
        "model": "text-embedding-3-small",
        "dims": 1536
      }
    }
  }
}

Two explicit forms count as semantic fields too — a dense_vector sub-field on a top-level text/keyword field (the parent is the source, and stays BM25-searchable — hybrid out of the box), or a standalone dense_vector whose vector_options declare source (the document’s text field to embed) and model:

PUT /my-collection
{
  "schema": {
    "properties": {
      "title": { "type": "text" },
      "embedding": {
        "type": "dense_vector",
        "vector_options": {
          "dims": 768,
          "similarity": "cosine",
          "source": "title",
          "model": "text-embedding-3-small"
        }
      }
    }
  }
}

3. Write and search — documents indexed into the collection get their embedding derived automatically at write time for the field’s model (you don’t supply vectors); the semantic query embeds its text through the same path at search time. The /_embedding API exposes the inference directly.

Wire forms #

String shorthand — just the text:

{ "semantic": "find documents about search engines" }

Object form — any subset of the parameters:

{ "semantic": { "query": "search engines", "k": 10, "similarity": "cosine" } }

Parameters for semantic #

  • query
    (Required, string) The natural-language text to embed and search with.
  • field
    (Optional, string) The semantic field to search. Resolved from the schema when omitted: with exactly one semantic field it is the default; with several, the request must name one (a 400 lists the candidates otherwise).
  • k
    (Optional, integer, default: 10) Nearest neighbors to return.
  • num_candidates
    (Optional, integer) Graph beam width for the query (HNSW ef); see vector.
  • similarity
    (Optional, string) cosine (default), l2 or dot_product — must match the field schema.
  • min_score
    (Optional, float) Return everything above this similarity instead of a fixed top-k — useful when the semantic clause feeds a hybrid intersection.

Use cases #

  • Plain-text search over meaning. Synonyms and paraphrases match where keywords fail — “cheap flights” finds “affordable travel” — with no embedding code on the client.
  • Hybrid retrieval without the retriever DSL. A semantic should clause beside match inside one bool gives lexical + vector fusion in the plainest possible request (the retrieve/fuse pipeline is the heavier, stage-by-stage counterpart).
  • Zero-boilerplate prototyping. Map one semantic_text field, write plain documents, search with plain strings — the embedding pipeline is entirely server-side, so demos and MVPs skip the embedding client altogether.
  • Cross-lingual retrieval. With a multilingual model, documents and queries in different languages share one embedding space — the query stays a plain string either way.

Notes #

  • The search must target exactly one collection — the schema is where the field and model live; multi-collection targets answer 400.
  • A collection whose schema declares no semantic field answers 400 with instructions to map a semantic_text field (or a dense_vector with vector_options.source and model).
  • The query never names a service or model — the binding lives in the mapping, and query-time inference uses the same one (writes and searches stay consistent).
  • The full vector parameter surface (vector_options overrides, quantization, sparse vectors) is on the vector query page.
  • vector — the native query underneath: full parameter reference, index types, quantization, sparse vectors.
  • multi_vector — several semantic fields searched and fused in one request.
  • Hybrid search pipeline — retrieve / fuse / rescore stages for full hybrid control.
  • Hybrid search tutorial — a walkthrough mixing text and vector legs on the embeddings dataset.
Calendar October 6, 2026
Edit Edit this page