Skip to main content

Vector Search

The vector search endpoint provides typed, read-only pgvector similarity search brokered through the FoundryDB controller. It composes a single SELECT from validated inputs and executes it inside a READ ONLY transaction with a statement timeout and row cap. Your application never needs a direct database connection.

Vector search is available on PostgreSQL services only. The service must be in Running status.

How It Works

You send a POST request with a query vector (or text to embed server-side) and the name of the table and column to search. The controller validates all identifiers and parameters, then routes the query to the service's primary node via the agent. Results are returned synchronously, ordered by ascending distance.

pgvector similarity search · query → HNSW → top-k
TOP-K vector → HNSW index → filter → nearest rows
Queryvector | textANN · HNSWcosine <=>AND filter →Top-kby distance
Query / top-kServer-side embedANN search · tableHNSW indexEquality filter (WHERE)index / predicate edge (dashed)

The query path has five stages. A request carries either a pre-computed vector or a query_text. When you send text, the controller embeds it server-side using the referenced pipeline's model into a query vector. That vector drives an approximate-nearest-neighbour (ANN) scan over the table's pgvector HNSW index, ranked by the distance metric you select. Equality filters are ANDed into the WHERE clause, so they narrow the same vector search rather than running separately. Finally the top_k nearest rows are returned, ordered by ascending distance. Use the switch in the diagram to see how the chosen metric changes the operator and the index operator class.

Endpoints

POST /managed-services/{id}/vector-search

Authentication uses the same basic auth credentials as the rest of the platform API.

Request Body

FieldTypeRequiredDescription
tablestringYesTable containing the vector column.
database_namestringNoDatabase to query. Defaults to the engine default, or the pipeline's database when query_text is used.
schemastringNoSchema of the table. Defaults to public.
embedding_columnstringNoName of the vector column. Defaults to embedding.
vectorfloat[]Exactly one of vector or query_textQuery vector (max 8192 dimensions, all components must be finite).
query_textstringExactly one of vector or query_textText to embed server-side using the referenced pipeline. Requires pipeline_id. Max 8192 characters.
pipeline_idUUIDWhen using query_textEmbedding pipeline used to embed the text. Must belong to this service.
top_kintegerNoNumber of rows to return. Range 1-100. Defaults to 10.
metricstringNoDistance metric: cosine (default), l2, or ip.
filtersarrayNoEquality filters ANDed into the WHERE clause. Max 32 filters.
include_columnsstring[]NoColumns to return. Defaults to all columns. Max 100.

Filters

Each filter object requires:

{ "column": "category", "op": "eq", "value": "security" }

Only equality (eq) is supported. Values can be string, number, or boolean.

Filters do not run as a separate query. Each filter becomes an AND col = value predicate in the same SELECT as the vector ordering, so the result is the top-k nearest rows that also satisfy every filter. Multiple filters are ANDed together (there is no OR); a row must match all of them to be returned. Because the distance ordering and the predicates are applied together, a very selective filter combined with a small top_k keeps the result tight and fast.

One practical note: with an HNSW index, filters are applied alongside the approximate scan. If a filter is extremely selective (it matches only a tiny fraction of rows), the ANN traversal may visit many candidates before finding enough matches, or in rare cases return fewer than top_k rows. If you routinely filter on a column, add a standard B-tree index on that column so the planner can combine it efficiently, and consider raising the search effort (hnsw.ef_search) for recall-sensitive queries.

Distance Metrics

Valuepgvector OperatorUse When
cosine<=>Normalized embeddings, semantic similarity (most common).
l2<->Euclidean distance, un-normalized vectors.
ip<#>Inner product (negated), max-margin retrieval.

Choosing a metric

Results are always ordered by ascending distance, so the same top_k semantics apply to every metric: the smallest distance is the closest match.

  • cosine (default). Compares the angle between vectors and ignores magnitude. This is the right choice for most text and image embeddings, which model semantic similarity by direction. Many embedding models already return unit-length (normalized) vectors, in which case cosine is the natural fit. If you are unsure, start here.
  • l2 (Euclidean). Compares straight-line distance and is sensitive to magnitude. Use it when the length of a vector carries meaning (for example feature vectors that are not unit-normalized) or when the model documentation recommends Euclidean distance.
  • ip (inner product). pgvector returns the negated inner product so that, like the other operators, smaller is closer. Use it for max-margin or maximum-inner-product retrieval, and only when your vectors are normalized: on un-normalized vectors the inner product favours longer vectors regardless of direction, which is rarely what you want.

The metric must match the index. An HNSW index is built for one operator class (cosine, L2, or inner product), so a query using a different metric cannot use that index and will fall back to a slower exact scan. Pick the metric first, then build the matching index (see below).

The metric you send must also match how the vectors were produced. When you use query_text, the controller embeds with the same provider, model, and dimensions that created the indexed vectors, so distances are directly comparable. Mixing embedding models, or comparing across different dimensions, produces meaningless distances.

Response

{
"columns": [
{ "name": "id", "type": "int8" },
{ "name": "content", "type": "text" },
{ "name": "distance", "type": "float8" }
],
"rows": [
[42, "PostgreSQL supports TLS for all client connections.", 0.031]
],
"row_count": 1,
"truncated": false,
"execution_ms": 4,
"metric": "cosine",
"top_k": 10
}

A distance column is always appended to the selected columns. truncated is true if a row or byte cap cut the result short.

Example: Query with a Pre-Computed Vector

curl -u $USER:$PASS \
-X POST https://api.foundrydb.com/managed-services/$SERVICE_ID/vector-search \
-H "Content-Type: application/json" \
-d '{
"table": "articles_embeddings",
"embedding_column": "embedding",
"vector": [0.021, -0.134, 0.007],
"top_k": 5,
"metric": "cosine",
"include_columns": ["source_row_id", "text_content"],
"filters": [
{ "column": "category", "op": "eq", "value": "security" }
]
}'

Example: Query with Text (Server-Side Embedding)

When you provide query_text instead of vector, the controller embeds the text using the referenced pipeline's model and dimensions before running the search. This keeps embedding logic out of your application.

curl -u $USER:$PASS \
-X POST https://api.foundrydb.com/managed-services/$SERVICE_ID/vector-search \
-H "Content-Type: application/json" \
-d '{
"table": "articles_embeddings",
"query_text": "how to secure a database connection",
"pipeline_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"top_k": 5
}'

The pipeline_id must belong to this service. The embedding is produced using the same provider, model, and dimensions that created the indexed vectors, so distances are comparable.

Setting Up Your Table

The vector column must be created with the pgvector extension. An HNSW index on the column is strongly recommended for production use:

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE articles_embeddings (
source_row_id BIGINT PRIMARY KEY,
text_content TEXT,
category TEXT,
embedding vector(1536)
);

CREATE INDEX ON articles_embeddings
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

HNSW index notes

HNSW (Hierarchical Navigable Small World) builds a multi-layer graph of vectors and is the recommended index for production search. It trades exact results for speed: queries are approximate, so a row's true nearest neighbours are returned with high probability, not certainty. Without an index, pgvector falls back to an exact sequential scan, which is correct but scans every row and does not scale.

  • Operator class. The index is built for one distance metric. Use vector_cosine_ops for cosine, vector_l2_ops for l2, and vector_ip_ops for ip. A query whose metric does not match the index operator class cannot use the index. If you query a table with more than one metric, build one HNSW index per metric you use.
  • m (default 16) is the number of connections per layer. Higher m improves recall and speeds up search at the cost of a larger index and slower builds. 16 is a good default; raise it (for example to 24-32) for high-recall workloads.
  • ef_construction (default 64) is how many candidates are considered while building. Higher values build a better graph (better recall) but take longer to create. 64-128 is typical.
  • hnsw.ef_search is a query-time setting that controls how many candidates the search explores. Larger values raise recall at the cost of latency. It defaults to 40; raise it per session when you need more accurate top-k results.
  • Build cost. Index creation reads every existing row. For large tables, create the index after bulk-loading the vectors rather than before, so the load is not slowed by incremental index maintenance.
  • Dimensions. The vector column has a fixed dimension (vector(1536) above). Your query vector must have the same dimension, and query_text embeddings are produced at the pipeline's configured dimension. A mismatch is rejected with a 400.

Error Codes

HTTP StatusMeaning
400Invalid request: wrong service type, missing table, unsupported metric, bad identifier, vector dimension mismatch, filter error, or embedding failure.
401Authentication required.
404Service not found, no VM instances, or referenced pipeline not found.
500Storage, encryption, or token-metering failure.

What's Next