Skip to content

Guides

Vector and hybrid search

Declare vector fields and embedding profiles in a schema, then run vector, hybrid, and exhaustive queries with explicit candidate work.


An index schema may declare several vector fields. Each field pins its own dimension, similarity, normalization rule, source-field recipe, and embedding profile, so a 384-dimensional semantic field and a 768-dimensional visual field can coexist in one index without sharing an embedding space. A query selects exactly one field.

Declaring a vector field

A vector field is a repeated float with a vector option, and the embedding profile it names is declared on the message:

proto
syntax = "proto3";
package cargo;

import "ysearch/v1/schema.proto";
import "google/protobuf/timestamp.proto";

message Article {
  option (ysearch.v1.document) = {
    index: "articles"
    embedding_profiles: {
      name: "semantic-gte-small"
      mode: EMBEDDING_MODE_EXTERNAL
      model: "gte-small"
      revision: "2026-08-31"
      dimensions: 384
      pooling: "mean"
      normalize: true
      maximum_tokens: 512
      endpoint: "http://embeddings.internal:8000"
      credential_environment: "YSEARCH_EMBEDDING_CREDENTIAL"
    }
  };

  string url_hash = 1 [(ysearch.v1.field) = { key: true }];
  string title    = 2 [(ysearch.v1.field) = { indexed: true, stored: true, weight: 2.0 }];
  string headline = 3 [(ysearch.v1.field) = { indexed: true, stored: true, weight: 2.0 }];
  string excerpt  = 4;

  repeated float semantic_vector = 5 [(ysearch.v1.field) = { vector: {
    dimensions: 384
    similarity: VECTOR_SIMILARITY_COSINE
    embedding_profile: "semantic-gte-small"
    source_fields: "title"
    source_fields: "headline"
    source_fields: "excerpt"
    normalize: true
  }}];
}

The vector field's own options:

Option Meaning
dimensions the vector length this field accepts
similarity VECTOR_SIMILARITY_COSINE, VECTOR_SIMILARITY_DOT_PRODUCT, or VECTOR_SIMILARITY_EUCLIDEAN
embedding_profile the name of a profile declared on the message
source_fields the ordered fields the builder embeds when no vector was supplied
required whether a document must carry or produce a vector
normalize normalize the vector before it is stored

A document may supply the field explicitly in JSON as an array of numbers. Supplied vectors override automatic embedding. When the field is absent, the builder embeds the ordered source_fields before atomically accepting the batch — so the acknowledgment you get back already accounts for the embedding work.

Apply and load exactly as for any other schema; see schemas and ingest.

The four provider modes

Text embedding has three provider modes, plus a mode for vectors you compute yourself:

mode Where vectors come from
EMBEDDING_MODE_SUPPLIED_ONLY the documents and queries supply them; the server embeds nothing
EMBEDDING_MODE_INTERNAL the pure-Go runtime, from a manifest the profile pins
EMBEDDING_MODE_EXTERNAL the profile's OpenAI-compatible HTTP endpoint
EMBEDDING_MODE_GRPC a generic gRPC model service (runtime grpc-embedder-v1)

EXTERNAL

Calls the profile's OpenAI-compatible endpoint. Plain HTTP is refused unless embedding.external_allow_http=true, which exists for a trusted development LAN and not for anything else. The profile stores only the name of a credential environment variable — credential_environment above. The token itself is never in the schema, the command line, the logs, or a receipt.

zsh
export YSEARCH_EMBEDDING_CREDENTIAL='…'
ysearch serve --data-dir ./ys --embedding-external-allow-http

INTERNAL

Uses the pure-Go runtime when embedding.internal_enabled=true. A profile pins a manifest_uri and its SHA-256, and every artifact is size- and digest-verified before installation into the separate model cache at embedding.model_cache_dir. No Python, shared library, repository code, custom operator, or tokenizer plugin is executed. Setting embedding.model_cache_dir to the empty string disables internal embedding.

GRPC

Calls a generic gRPC model service; the runtime name is grpc-embedder-v1, and it is part of the model fingerprint. The schema pins the logical model name, the revision, and a 32-byte model_fingerprint_sha256, while the endpoint stays routing metadata. The fingerprint is part of the vector-space identity: changing the model package cannot silently reuse old vectors or cached query embeddings.

Because model_fingerprint_sha256 is a protobuf bytes field, it is base64 in protobuf JSON. Queries are sent as interactive work and indexing batches as bulk work when embedding.grpc_work_class=auto. Plaintext cluster endpoints require embedding.grpc_allow_insecure=true; production endpoints default to TLS. A dns:///host:port endpoint lets the process-wide connection use gRPC round-robin balancing across all service addresses.

Querying

A query names one vector field and gives either text to embed or a vector to use directly.

Text

zsh
ysearch search articles --vector-field semantic_vector \
  --vector-text 'Roger Federer' --top-k 30 --ids

The coordinator embeds the text once, then sends the canonical vector to the segment executors and workers. That matters on a fleet: the embedding cost is paid once per query, not once per segment.

Hybrid

zsh
ysearch search articles 'title:federer' \
  --vector-field semantic_vector --vector-text 'Roger Federer tennis' \
  --probes 16 --candidates 1000 \
  --lexical-weight 0.5 --vector-weight 1.0 --fusion weighted

A hybrid query is an ordinary lexical query plus a vector clause. The lexical query is lowered by whichever grammar --dialect selects — see Lucene and CQP — and filters apply as usual, so filters and projection still holds.

A supplied vector

zsh
ysearch search articles --vector-field semantic_vector \
  --vector-file query-vector.json --exhaustive --top-k 20

The file is a JSON float array, bounded to 16 MiB before decoding. Use --vector-file - to read it from standard input.

The flags

Flag Default Meaning
--vector-field — the vector field used for vector or hybrid search
--vector-text — embed this text with the selected field's schema profile
--vector-file — read a JSON float array from a file, or - for stdin
--probes 0 IVF lists to probe; zero uses the server default
--candidates 0 ANN candidates to exact-rerank; zero uses the server default
--exhaustive false scan every covered vector instead of IVF-PQ candidate generation
--fusion weighted hybrid fusion: weighted or rrf
--lexical-weight 1 lexical score weight for hybrid search
--vector-weight 1 vector score weight
--tail exact tail ordering: exact or banded
--maximum-ordering-error 0 the largest score inversion the caller accepts in a banded tail
--score-ranges false include conservative score ranges for an approximate tail

--fusion rrf selects reciprocal-rank fusion, which combines the two rankings by position rather than by score and so needs no weight calibration between two differently-scaled scores.

--probes and --candidates are the two knobs that trade recall against work. --exhaustive removes the candidate-generation step entirely and is the form to use when establishing a recall baseline for a tuned configuration.

Approximate tails

--tail banded opts into a bounded approximate lexical tail. --maximum-ordering-error states the largest score inversion the caller will accept, and --score-ranges asks for conservative ranges alongside the scores.

An unsupported or too-tight request falls back to exact ordering, and the response header and trailer report the resulting ordering. Check those fields to determine which ordering the response uses.

The vector index today and next

Vector search on main is the inherited index: IVF lists with PQ codes for candidate generation, followed by an exact rerank of --candidates candidates. --probes, --candidates, and --exhaustive control it.

Since segment format 8 (milestone YS3), the builder also writes exact vector lanes for every populated cosine field: the canonical fp16 vectors, IVF centroids with their radii, and int8 codes. Those lanes serve the exact threshold scan that the planned matcher needs. The search flags above do not use them yet, and dot_product and Euclidean fields get no lanes.

The ysearch design keeps a single vector index, ivf_rabitq (IVF with RaBitQ 1-bit codes), in milestone YS16, and removes the IVF-PQ build and read paths then. Segments of formats 1 to 6 with a PQ vector section will still open for non-vector reads, but a vector query on one will fail with VECTOR_FORMAT_UNSUPPORTED (rebuild required) rather than return a partial result. See status.

Caching and identity

The process owns one provider manager and one query-vector cache across every hosted index. A repeated text query is keyed by embedding fingerprint plus a SHA-256 of the text; the raw query text is not retained in the cache key.

The cache is bounded by three settings:

Setting Default
embedding.query_cache_entries 4096
embedding.query_cache_bytes 64 MiB
embedding.query_cache_ttl 10m

Including the embedding fingerprint in the key is what makes a model change safe: a new fingerprint cannot hit an entry produced by the old model.

ID-only results

--ids remains the cheapest large-result output on a vector query for the same reason it is on a lexical one: it requests neither scores nor stored documents.

zsh
ysearch search articles --vector-field semantic_vector \
  --vector-text 'cold cache hydration' --top-k 500 --ids

Next