Skip to main content

Vector and Hybrid Search

Store embeddings alongside your data and run semantic similarity search powered by pgvector, with optional hybrid search that fuses vector similarity and full-text search.

Vector search is ideal for semantic retrieval, recommendations, and Retrieval-Augmented Generation (RAG): you generate embeddings with your own model (OpenAI, Cohere, a local model, etc.), store them in a vector column, and query by nearest neighbour.

How It Works

  1. Define one or more vector columns on a table by adding x-vector-metadata to a field.
  2. Insert rows containing the embedding as a plain array of numbers.
  3. Search by passing a pre-embedded query vector to the __vector_near filter.

The Data Service automatically creates an HNSW index at table-creation time, so nearest-neighbour queries stay fast as your data grows.

Pre-embedded vectors

The Data Service does not generate embeddings for you. Embed your text/images on the client (or in a Function) and send the resulting float array.

Requires a flat_table datasource

Vector columns are only supported on tables in a flat_table datasource (they map to real pgvector columns). Tables on a jsonb datasource cannot define vector columns or run __vector_near queries.

Defining a Vector Column

Add a field of type: "any" with an x-vector-metadata object. Only dimensions is required; everything else has a sensible default.

{
"name": "documents",
"json_schema": {
"fields": [
{ "name": "id", "type": "string", "format": "uuid", "constraints": { "required": true } },
{ "name": "title", "type": "string" },
{ "name": "content", "type": "string" },
{
"name": "embedding",
"type": "any",
"x-vector-metadata": {
"dimensions": 1536,
"distance": "cosine",
"index_type": "hnsw",
"storage_type": "vector"
}
}
],
"primaryKey": ["id"]
}
}

Create the table:

POST /api/apps/{app-slug}/datatables/

x-vector-metadata options

KeyRequiredDefaultDescription
dimensionsYesNumber of dimensions in the embedding. 12000 for vector storage, up to 4000 for halfvec.
distanceNocosineDistance metric: cosine, l2 (Euclidean), or ip (inner product).
index_typeNohnswANN index type. Only hnsw is supported.
storage_typeNovectorvector (full float32 precision, max 2000 dimensions) or halfvec (half precision — ~half the storage, supports up to 4000 dimensions, with a small recall trade-off).
Dimensions above 2000 need halfvec

storage_type: "vector" is limited to 2000 dimensions. If your embedding is larger (e.g. OpenAI text-embedding-3-large at 3072 dims), set storage_type: "halfvec" — otherwise table creation is rejected.

Advanced: tuning the HNSW index

The index is built with sensible defaults (m: 16, ef_construction: 64). To trade build time and memory for recall, override them with an optional index_params object:

{
"name": "embedding",
"type": "any",
"x-vector-metadata": {
"dimensions": 1536,
"distance": "cosine",
"index_params": { "m": 32, "ef_construction": 128 }
}
}

Higher m / ef_construction improve recall at the cost of a slower, larger index. Leave them unset unless you're tuning for a specific workload.

Choosing storage_type

Use vector for most cases. Choose halfvec for very high-dimensional embeddings or when storage size matters more than a small loss in recall.

The metric you set here is baked into the index. Searching with a different metric returns an error (see Errors) — so pick the metric that matches how your embedding model was trained (most modern text embeddings use cosine).

Adding a vector column to an existing table

You don't have to define the vector column up front. Add an x-vector-metadata field to an existing table with a schema update (PATCH) and the Data Service migrates the table for you — it runs ALTER TABLE ADD COLUMN and builds the HNSW index automatically:

PATCH /api/apps/{app-slug}/datatables/{table-name}/

Existing rows get NULL for the new column until you backfill them with embeddings. Rows without an embedding are simply not returned by __vector_near searches.

Building the index on a large table

The HNSW index is created as part of the schema migration. On a table that already holds a lot of rows this can take a while and hold a lock. For very large tables, add the empty vector column first, backfill the embeddings, and build the HNSW index separately during a maintenance window.

Inserting Embeddings

Embeddings are inserted like any other field — as a JSON array of numbers:

POST /api/apps/blog-app/datatables/documents/data/

{
"id": "1f3d...",
"title": "Intro to Vector Search",
"content": "Vector search finds semantically similar items...",
"embedding": [0.012, -0.034, 0.221, ...]
}

Search by passing your query embedding to {field}__vector_near. Results are ordered by nearest neighbour first.

GET /api/apps/blog-app/datatables/documents/data/?embedding__vector_near=[0.01,-0.03,0.22,...]&_topk=10

The vector value is a JSON array. Remember to URL-encode it in real requests.

Response:

{
"status": "success",
"message": "Data retrieved successfully",
"data": [
{
"id": "1f3d...",
"title": "Intro to Vector Search",
"content": "Vector search finds semantically similar items...",
"_vector_score": 0.0421,
"_similarity_score": 0.9579
}
],
"total": 1
}

Search parameters

ParameterSDK argumentDefaultDescription
_topktopk10Size of the nearest-neighbour window — the maximum number of candidates the search returns. Must be at least 1; capped at the server limit (1000 by default).
_vector_metricmetriccolumn's metricValidates the request against the column's index metric. Must match the metric the column was created with.
_vector_thresholdthresholdnoneMaximum distance cutoff — results farther than this are discarded.
_vector_ef_searchef_searchindex defaultHNSW probe depth. Higher = more accurate recall, slower query. Must be between 1 and 1000.

Result scores

Every pure-vector result row carries two scores:

FieldMeaningDirection
_vector_scoreRaw pgvector distance (useful for tuning _vector_threshold).Lower = more similar.
_similarity_scoreNormalised, human-facing similarity.Higher = more similar.

_similarity_score is derived from the distance per metric:

MetricTransformRangePerfect match
cosine1 - distance[-1, 1]1.0
l21 / (1 + distance)(0, 1]1.0
ipde-negated inner productunboundedn/a
Inner product is unbounded

For the ip metric, _similarity_score is the raw inner product (higher still means more similar), but it is not a normalised 0–1 value. Use cosine if you need a bounded similarity.

__vector_near is read-only

__vector_near is a search operator for reading data only. You can't use it in a delete or update filter (e.g. to "delete the nearest rows") — such requests are rejected. Delete and update by scalar filters (id, status, etc.) instead.

One Vector Column Per Table

A table may declare at most one vector column. To use different metrics or modalities — for example a text embedding and an image embedding, or cosine vs L2 — create separate tables, one vector column each:

// documents_text  (cosine)
{
"fields": [
{ "name": "id", "type": "string", "format": "uuid", "constraints": { "required": true } },
{ "name": "embedding", "type": "any", "x-vector-metadata": { "dimensions": 1536, "distance": "cosine" } }
],
"primaryKey": ["id"]
}
// documents_image  (l2)
{
"fields": [
{ "name": "id", "type": "string", "format": "uuid", "constraints": { "required": true } },
{ "name": "embedding", "type": "any", "x-vector-metadata": { "dimensions": 512, "distance": "l2" } }
],
"primaryKey": ["id"]
}

Search each table independently (the metric must match the column's index):

GET /.../documents_text/data/?embedding__vector_near=[...]&_topk=10
GET /.../documents_image/data/?embedding__vector_near=[...]&_topk=10&_vector_metric=l2
One vector column per table

Declaring more than one vector field in a single schema is rejected at create/update time (HTTP 400). Split additional embeddings into their own tables.

Likewise, a single query may search only one vector field — passing more than one {field}__vector_near filter in the same request is rejected (HTTP 400).

Pagination

Vector search ranks only the top _topk candidates, so pagination must stay inside that window. The page offset can never reach beyond rank _topk.

# _topk=10, 5 per page, page 2  →  ranks 6–10 ✅
GET /.../documents/data/?embedding__vector_near=[...]&_topk=10&page=2&page_size=5

If the offset reaches past the window, the request returns 400 with guidance to increase _topk:

# offset (3) >= _topk (3)  →  400
GET /.../documents/data/?embedding__vector_near=[...]&_topk=3&offset=3
{
"message": "Offset (3) exceeds the vector search window.",
"detail": "Increase _topk to at least 4 to reach this page (current _topk=3)."
}

To page deeper, raise _topk so the window covers the pages you need.

Hybrid search combines vector similarity with PostgreSQL full-text search and fuses the two rankings using Reciprocal Rank Fusion (RRF). This often beats either method alone — vector search captures semantic meaning while full-text search captures exact keyword matches.

1. Enable full-text search on the table

Add search_fields to the schema. This auto-generates a search_vector column and a GIN index.

{
"fields": [
{ "name": "id", "type": "string", "format": "uuid", "constraints": { "required": true } },
{ "name": "title", "type": "string" },
{ "name": "content", "type": "string" },
{ "name": "embedding", "type": "any", "x-vector-metadata": { "dimensions": 1536 } }
],
"primaryKey": ["id"],
"search_fields": [
{ "field": "title", "weight": "A" },
{ "field": "content", "weight": "B" }
],
"search_config": "english"
}

search_fields accepts plain field names or { "field", "weight" } objects (weights AD, highest to lowest). See Search language for search_config.

2. Query with both signals

GET /api/apps/blog-app/datatables/documents/data/?embedding__vector_near=[...]&search_vector__search=machine+learning&_hybrid_strategy=rrf&_hybrid_alpha=0.5&_topk=10

Hybrid parameters

ParameterSDK argumentDefaultDescription
_hybrid_strategystrategyFusion strategy. rrf (Reciprocal Rank Fusion) is supported. Required to enable hybrid mode.
_hybrid_alphaalpha0.5Balance between the two signals: 1.0 = pure vector, 0.0 = pure full-text, 0.5 = equal weight. Must be between 0.0 and 1.0.

Hybrid result scores

FieldMeaning
_hybrid_scoreCombined RRF relevance score the results are ranked by.
_vector_scoreThis row's contribution from the vector ranking.
_fts_scoreThis row's contribution from the full-text ranking.
note

Hybrid results are ranked by _hybrid_score (rank-based fusion) and do not include _similarity_score, which is specific to pure-vector search.

Search language (search_config)

Full-text search is language-aware. Set search_config on the schema to the PostgreSQL text-search configuration that matches your content's language. It defaults to english.

{
"search_fields": ["title", "content"],
"search_config": "spanish"
}

search_config controls stemming and stop-word handling for both the stored search_vector column and the query, so they always stay consistent. It must be one of the text-search configurations installed in PostgreSQL — for example english, spanish, french, german, portuguese, russian, or simple (no stemming). An invalid value is rejected at table creation.

Errors and Validation

The following conditions return 400 Bad Request:

ConditionExample
Query vector dimensions don't match the column3-dim vector against a 4-dim column
_vector_metric doesn't match the column's index metric_vector_metric=l2 on a cosine column
Invalid metric_vector_metric=manhattan
Empty vectorembedding__vector_near=[]
Non-finite valuesvector containing NaN or Infinity
Non-numeric elementvector containing true or a string
Searching a field that isn't a vector columnunknown_field__vector_near=[...]
_topk less than 1_topk=0
_vector_ef_search out of range_vector_ef_search=0 or _vector_ef_search=2000
_hybrid_alpha out of range_hybrid_alpha=1.5
Unknown hybrid strategy_hybrid_strategy=bm25
Offset beyond the top-k windowoffset=3 with _topk=3
dimensions above 2000 with storage_type: "vector"use storage_type: "halfvec" instead
Invalid distance, index_type, or storage_type at table creationdistance: "manhattan", index_type: "ivfflat", storage_type: "float8"
search_config isn't an installed PostgreSQL text-search configsearch_config: "klingon"

Limits and Configuration

SettingDefaultDescription
Max dimensions2000 (vector) / 4000 (halfvec)Maximum dimensions per vector column, by storage type.
Max _topk1000Upper bound on the search window per request (protects the database from excessive work). Requests above this are clamped, not rejected.

Vector search is always available — there is no setting to turn it off. The pgvector extension is provisioned by the platform, so you don't need to install anything to use vector columns.

  • Querying and Filtering — filters, sorting, and full-text search basics
  • Indexes — GIN and general index details (the HNSW vector index is created for you automatically)
  • Schema Reference — full field and schema options
  • Functions — generate embeddings server-side