Vector Search Architecture¶
Overview¶
Vector search in Provisa is organized as three independent phases, each delivering standalone value: (REQ-431)
- Native vector search — model registry, embedding column declaration,
cosine_similarity()UDF - Non-native fallback — transparent pgvector cache materialization for sources without native capability
- Declarative embedding generation — generate and maintain embedding columns from source text
Phase 1: Native Vector Search¶
Model Registry¶
Declared in provisa.yaml. All embedding and query vectorization operations must reference a registered model. (REQ-419)
models:
embedding:
- id: text-embedding-3-small
provider: openai
dimensions: 1536
api_key_env: OPENAI_API_KEY
enabled: true
- id: nomic-embed-text
provider: ollama
base_url: http://localhost:11434
dimensions: 768
enabled: true
- id: all-minilm-l6-v2
provider: huggingface
dimensions: 384
enabled: true
Supported provider types: (REQ-420)
- openai — OpenAI-compatible REST API
- ollama — local Ollama server
- huggingface — HuggingFace sentence-transformers model loaded locally (air-gap friendly)
Embedding Column Declaration¶
A column is declared as an embedding vector by setting embedding: true on an existing column: (REQ-421)
columns:
description_vec:
type: array(float)
embedding: true
embedding_model: text-embedding-3-small # must reference model registry
embedding_dimensions: 1536
embedding_source: description # optional: originating text column
embedding_source is informational — documents what text generated the vector. Not required for externally generated embeddings. (REQ-421)
Source Capability Detection¶
At source registration time, Provisa auto-detects native vector support: (REQ-422)
| Source | Detection | Native operator |
|---|---|---|
| PostgreSQL | Check for pgvector extension | <=> (cosine), <-> (L2), <#> (inner product) |
| MongoDB | Check Atlas tier | $vectorSearch |
| Snowflake | Check Cortex availability | VECTOR_COSINE_SIMILARITY() |
| All others | None detected | Fallback path (Phase 2) |
cosine_similarity() UDF¶
Users always write:
SELECT *, cosine_similarity(description_vec, :query_vector) AS score
FROM products
ORDER BY score DESC
LIMIT 10
Provisa translates per source capability at query time: (REQ-423)
| Source capability | Generated SQL |
|---|---|
| pgvector | description_vec <=> :query_vector |
| Snowflake Cortex | VECTOR_COSINE_SIMILARITY(description_vec, :query_vector) |
| MongoDB Atlas | $vectorSearch operator |
| None | Route to pgvector fallback cache (Phase 2) |
Query-Time Vectorization¶
Both text input and raw vector input are supported: (REQ-430)
# Text input — Provisa calls embedding model, generates query vector
products(similar_to: "red running shoes", limit: 10)
# Raw vector input — caller provides vector directly
products(near_vector: [0.123, -0.456, ...], limit: 10)
Model Locking¶
Once an embedding column is generated with a model, that model is locked for that column. Provisa rejects queries using incompatible models or dimensions: (REQ-429)
ERROR: column description_vec was generated with text-embedding-3-small (1536d).
Query vector uses nomic-embed-text (768d). Dimension mismatch.
Re-embed the column or declare a separate embedding column.
Phase 2: Non-Native Fallback¶
For sources with no native vector capability, Provisa transparently materializes the embedding column to an internal pgvector-enabled PostgreSQL cache. (REQ-424)
Fallback Flow¶
cosine_similarity() against non-vector source
↓
Detect: source has no native vector capability
↓
Check pgvector cache: provisa_vector_cache.{source}_{table}_{column}_{hash}
↓
┌───────────┬──────────────┐
▼ ▼ ▼
HIT STALE MISS
↓ ↓ ↓
Query cache Rebuild Materialize:
directly cache - Pull embedding col + PKs from source
- Write to pgvector table
- Build HNSW index
- Record cache metadata
↓
Query pgvector cache
↓
JOIN PKs back to source for remaining columns
↓
Return governed result — caller unaware of fallback
Cache Invalidation¶
| Trigger | Behavior |
|---|---|
| TTL expiry | Configurable per table; stale cache not served beyond TTL |
| Mutation on source | Invalidate affected rows |
| Manual refresh | Admin API endpoint |
| Row count drift | If source row count diverges beyond threshold, full rebuild |
| Model change | Full rebuild required |
| Schema change | Full rebuild required |
(REQ-425)
Phase 3: Declarative Embedding Generation¶
Provisa generates and maintains embedding columns from source text. The embedding column may not exist in the source — Provisa creates and owns it. (REQ-427)
Declaration¶
columns:
description_embedding:
type: embedding
model: text-embedding-3-small
dimensions: 1536
generated_from: |
SELECT CONCAT(name, ' ', description, ' ', category)
FROM {{table}}
WHERE {{pk}} = :pk
schedule: "0 2 * * *" # nightly refresh
incremental: true # only re-embed changed rows
generated_from Subquery¶
The generated_from value is a SQL subquery with these constraints: (REQ-427)
- Must return exactly one text value
- Validated at declaration time against a sample row
- Template variables available: {{table}}, {{pk}}
- May reference multiple columns, apply transformations, include joins
Examples:
-- Multi-column concatenation
SELECT CONCAT(title, ' ', body, ' ', author_name)
-- With join for richer context
SELECT CONCAT(p.name, ' ', c.name, ' ', b.name)
FROM products p
JOIN categories c ON p.category_id = c.id
JOIN brands b ON p.brand_id = b.id
WHERE p.id = :pk
-- Conditional composition by row type
SELECT CASE
WHEN type = 'article' THEN CONCAT(title, ' ', body)
WHEN type = 'video' THEN CONCAT(title, ' ', transcript)
END
Storage¶
Generated embeddings are stored in the internal pgvector PostgreSQL instance (same infrastructure as Phase 2 fallback cache). No source write access required. (REQ-427)
Incremental Refresh¶
- Track last-embedded PK + timestamp per column
- On schedule: fetch only rows where
updated_at > last_embedded_at - Re-embed changed rows, upsert into pgvector table
- Full rebuild triggered by: model change, schema change affecting subquery, manual admin trigger
(REQ-428)
Governance¶
Embedding columns participate in the full Provisa governance stack: (REQ-426)
| Governance feature | Applies to embedding columns |
|---|---|
| RLS | ✅ — row filters applied before similarity search |
| Column masking | ❌ — not applicable (see note below) |
| Domain boundary | ✅ — embedding columns respect domain traversal rules |
| Sensitivity tiers | ✅ — restricted tier blocks cosine_similarity() for unauthorized roles |
| Per-role schema visibility | ✅ — embedding columns hidden from roles without visibility |
Column masking does not apply to embedding columns — a vector cannot be partially masked and remain semantically meaningful. (REQ-652) Access control is enforced via visibility (hide entirely), RLS (filter rows), sensitivity tier (block search), and domain boundary rules. An embedding column is either visible and searchable for a given role, or absent from the schema entirely. (REQ-426)
Deployment Considerations¶
| Deployment | Embedding models available |
|---|---|
| Cloud / SaaS | OpenAI-compatible APIs (via API key) |
| Air-gapped | Ollama, local HuggingFace models |
| Regulated / on-prem | Ollama or local HuggingFace only — no external API calls |
The model registry makes the embedding pipeline fully portable across deployment types. (REQ-419, REQ-420) An air-gapped enterprise substitutes local models; the rest of the pipeline is identical.