Skip to content

# Vector Search Architecture

## Overview

Vector search in Provisa is organized as three independent phases, each delivering standalone value: (REQ-431)

1. Native vector search — model registry, embedding column declaration, `cosine_similarity()` UDF
2. Non-native fallback — transparent pgvector cache materialization for sources without native capability
3. 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.

## Requirements

REQ-419 through REQ-431. See `docs/arch/requirements.md`.