Fabriq
1.x
Docs/Fabriq/Vector
Open

Reading5 min
Updated2 Aug 2026
Sourcev1/(data-planes)/vector.mdx

The vector plane stores and searches embeddings for similarity queries — semantic search, recommendation, deduplication. It is implemented in adapters/postgres on pgvector, backed by an HNSW index over the fabriq_embeddings table. Fabriq stores and searches vectors; it does not produce them — the caller supplies embeddings as []float32 from whatever model they run.

type VectorQuerier interface {
	Upsert(ctx context.Context, entity, id string, embedding []float32, meta map[string]any) error
	Similar(ctx context.Context, q VectorQuery, into any) error
	Delete(ctx context.Context, entity, id string) error
	DeleteByMeta(ctx context.Context, entity string, filter map[string]string) error
	Get(ctx context.Context, entity, id string) ([]float32, error)
}

Reach it through f.Vector(). Every operation is tenant-scoped structurally, like the rest of the Postgres adapter (SET LOCAL app.tenant_id plus an explicit tenant_id predicate) — see Tenancy.

Upsert01

Upsert writes (or overwrites) one embedding keyed by (entity, id), with optional metadata stored alongside it. An empty embedding is rejected.

embedding := embedModel.Encode("centrifugal pump, 50 HP, stainless") // []float32 from your model

err := f.Vector().Upsert(ctx, "asset", assetID, embedding, map[string]any{
	"site_id": siteID,
	"kind":    "pump",
})

The upsert is keyed on (tenant_id, entity, id): re-upserting the same id replaces the embedding and metadata.

Similar02

Similar runs a cosine nearest-neighbour search through the HNSW index. It scans into *[]query.VectorMatch, best match first.

type VectorQuery struct {
	Entity    string
	Embedding []float32
	K         int
	// Filter restricts matches to embeddings whose meta contains all of these
	// key/value pairs (exact-match, AND-of-equals). Empty/nil = no filter.
	Filter map[string]string
}

type VectorMatch struct {
	ID    string
	Score float64 // cosine similarity, higher is closer
	Meta  map[string]any
}
queryEmbedding := embedModel.Encode("high-pressure water pump")

var matches []query.VectorMatch
err := f.Vector().Similar(ctx, query.VectorQuery{
	Entity:    "asset",
	Embedding: queryEmbedding,
	K:         10,
}, &matches)
if err != nil {
	return err
}
for _, m := range matches {
	// m.ID is the aggregate id; m.Score is cosine similarity (higher = closer);
	// m.Meta is the metadata stored at Upsert.
}

Score is cosine similarity in [0, 1]-ish terms — higher is closer (the adapter returns 1 - cosine_distance). K defaults to 10 when unset. Results are scoped to the (tenant, entity) pair.

Metadata filtering03

VectorQuery.Filter narrows a search to embeddings whose stored meta contains every key/value pair in the filter — an AND-of-equals predicate compiled to a JSONB containment check (meta @> '{…}'). An empty or nil filter means "no filter". The match values are compared as strings against the stored metadata.

var matches []query.VectorMatch
err := f.Vector().Similar(ctx, query.VectorQuery{
	Entity:    "asset",
	Embedding: queryEmbedding,
	K:         10,
	Filter:    map[string]string{"kind": "pump", "site_id": siteID},
}, &matches)
// only embeddings whose meta has kind=pump AND site_id=<siteID> are returned.

A GIN index on fabriq_embeddings.meta (migration 0025) keeps containment filters efficient. Filtering happens inside the same tenant-scoped query, so it can only narrow the result set — never widen the tenant or scope boundary.

Delete & DeleteByMeta04

Delete removes one embedding by (entity, id) — deleting a missing id is a no-op. DeleteByMeta removes every embedding for (tenant, entity) whose meta matches the filter.

// Remove a single embedding.
err := f.Vector().Delete(ctx, "asset", assetID)

// Remove every embedding for the entity that matches the metadata filter.
err = f.Vector().DeleteByMeta(ctx, "asset", map[string]string{"site_id": siteID})
Warning

An empty filter passed to DeleteByMeta deletes all embeddings for (tenant, entity). This is intentional (mirrors a "delete everything in this namespace" call) but scope it deliberately. The delete is always bounded by the tenant — it can never cross the tenant boundary.

Get05

Get returns the stored embedding for (entity, id), or a *fabriqerr.NotFoundError on miss — useful for re-ranking, dedup checks, or moving a vector between stores.

emb, err := f.Vector().Get(ctx, "asset", assetID)

Beyond Postgres06

The port is uniform across every backend: the in-memory fabriqtest.FakeVector, the sharded router, and the remote/gRPC transport all implement the same interface — including metadata Filter and DeleteByMeta (the remote transport carries the filter as JSON). A shared conformance suite (RunVector) gates the fake and the real Postgres adapter on identical behavior, so a backend cannot silently drift.

Note

m.ID is the aggregate id you passed to Upsert. To get the full row, hydrate from Postgres with Relational().GetMany("asset", ids, &assets) — one batched query for the whole match set.