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})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.
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.