# PgVector Store

PostgreSQL with pgvector extension support for Semantica vector storage and similarity search.

## Overview

The `PgVectorStore` provides native PostgreSQL vector storage using the [pgvector](https://github.com/pgvector/pgvector) extension. It supports multiple distance metrics (cosine similarity, L2/Euclidean, inner product), index types (IVFFlat, HNSW), and JSONB metadata storage with filtering.

## Features

- **Distance Metrics**: Cosine, L2 (Euclidean), Inner Product
- **Index Types**: IVFFlat, HNSW (for approximate nearest neighbor search)
- **Metadata Storage**: JSONB for flexible metadata with filtering support
- **Connection Pooling**: Efficient connection management with psycopg3/psycopg2
- **Batch Operations**: Bulk insert, update, delete support
- **Idempotent Index Creation**: Safe to call multiple times

## Setup

### Prerequisites

1. PostgreSQL 13+ with pgvector extension installed
2. Python dependencies: `psycopg3` (preferred) or `psycopg2-binary`, `pgvector`

### Installing Dependencies

```bash
# Install with pgvector support
pip install semantica[vectorstore-pgvector]

# Or install manually
pip install psycopg[binary] pgvector
# Or for psycopg2
pip install psycopg2-binary pgvector
```

### PostgreSQL Setup

1. Install pgvector extension (if not already installed):

```sql
-- Using apt (Debian/Ubuntu)
sudo apt-get install postgresql-16-pgvector

-- Using homebrew (macOS)
brew install pgvector

-- Or build from source
```

2. Create the extension in your database:

```sql
CREATE EXTENSION vector;
```

3. Verify installation:

```sql
SELECT * FROM pg_extension WHERE extname = 'vector';
```

### Docker Quickstart

```bash
docker run -d \
    --name pgvector \
    -e POSTGRES_PASSWORD=postgres \
    -p 5432:5432 \
    ankane/pgvector:latest
```

## Connection String Format

Standard PostgreSQL connection string:

```
postgresql://user:password@host:port/database
```

Examples:

```python
# Local development
"postgresql://postgres:postgres@localhost:5432/semantica"

# With SSL
"postgresql://user:pass@host/db?sslmode=require"

# Connection parameters
"postgresql://user:pass@localhost/db?connect_timeout=10&application_name=semantica"
```

## Usage

### Basic Usage

```python
from semantica.vector_store import PgVectorStore
import numpy as np

# Initialize store
store = PgVectorStore(
    connection_string="postgresql://postgres:postgres@localhost:5432/semantica",
    table_name="document_vectors",
    dimension=768,
    distance_metric="cosine",
    pool_size=10
)

# Add vectors
vectors = [np.random.rand(768).astype(np.float32) for _ in range(100)]
metadata = [{"doc_id": i, "category": "article"} for i in range(100)]
ids = store.add(vectors, metadata)

# Search
query = np.random.rand(768).astype(np.float32)
results = store.search(query, top_k=10)

# Results format: [{"id": "...", "score": 0.95, "metadata": {...}}, ...]
for result in results:
    print(f"ID: {result['id']}, Score: {result['score']:.4f}")

# Close store
store.close()
```

### Context Manager

```python
with PgVectorStore(
    connection_string="postgresql://...",
    table_name="vectors",
    dimension=768,
    distance_metric="cosine"
) as store:
    vectors = [np.random.rand(768).astype(np.float32)]
    ids = store.add(vectors, [{"source": "test"}])
    # Store automatically closed on exit
```

### Metadata Filtering

```python
# Search with metadata filter
results = store.search(
    query_vector,
    top_k=10,
    filter={"category": "science", "published": True}
)
```

### Update and Delete

```python
# Update vectors and metadata
new_vectors = [np.random.rand(768).astype(np.float32)]
new_metadata = [{"updated": True}]
store.update(["vec_0"], new_vectors, new_metadata)

# Update metadata only
store.update(["vec_0"], metadata=[{"tag": "updated"}])

# Delete vectors
store.delete(["vec_0", "vec_1"])
```

### Retrieve by ID

```python
results = store.get(["vec_0", "vec_1"])
# Returns: [{"id": "vec_0", "vector": np.array(...), "metadata": {...}}, ...]
```

### Index Creation

```python
# Create HNSW index for approximate nearest neighbor search
store.create_index(
    index_type="hnsw",
    params={"m": 16, "ef_construction": 64}
)

# Create IVFFlat index
store.create_index(
    index_type="ivfflat",
    params={"lists": 100}
)
```

Index creation is idempotent - calling multiple times is safe.

### Statistics

```python
stats = store.get_stats()
# Returns: {
#     "table_name": "document_vectors",
#     "dimension": 768,
#     "distance_metric": "cosine",
#     "vector_count": 1000,
#     "indexes": [...],
#     "psycopg_version": "3"
# }
```

## Distance Metrics

| Metric | Operator | Description | Use Case |
|--------|----------|-------------|----------|
| `cosine` | `<=>` | Cosine distance (1 - cosine similarity) | Semantic similarity, text embeddings |
| `l2` | `<->` | Euclidean distance | Geometric distance, clustering |
| `inner_product` | `<#>` | Negative inner product | Maximum inner product search |

**Note**: Scores returned by `search()` are normalized to similarity (higher = better) regardless of metric.

## Index Types

### HNSW (Hierarchical Navigable Small World)

- **Best for**: High-dimensional vectors, high recall requirements
- **Pros**: Fast search, good recall, incremental build
- **Cons**: Higher memory usage, slower build

```python
store.create_index(index_type="hnsw", params={
    "m": 16,              # Number of connections per layer (default: 16)
    "ef_construction": 64  # Build-time accuracy/speed tradeoff (default: 64)
})
```

### IVFFlat (Inverted File with Flat Index)

- **Best for**: Large datasets, memory-constrained environments
- **Pros**: Lower memory usage, tunable speed/accuracy
- **Cons**: Requires training, slower incremental updates

```python
store.create_index(index_type="ivfflat", params={
    "lists": 100  # Number of inverted lists (default: 100)
})
```

**Note**: IVFFlat requires at least as many vectors as lists for training.

## Schema

The vector table schema:

```sql
CREATE TABLE IF NOT EXISTS {table_name} (
    id TEXT PRIMARY KEY,
    vector VECTOR({dimension}),
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMP DEFAULT NOW()
);
```

## Migration Notes

### From Other Vector Stores

```python
# Export from existing store
from semantica.vector_store import FAISSStore

faiss_store = FAISSStore(dimension=768)
# ... load existing index

# Migrate to PgVectorStore
pg_store = PgVectorStore(
    connection_string="postgresql://...",
    table_name="migrated_vectors",
    dimension=768,
    distance_metric="cosine"
)

# Get all vectors from source
all_ids = list(faiss_store.index.vector_ids)
all_vectors = [...]  # Get vectors from source
all_metadata = [faiss_store.index.metadata.get(id, {}) for id in all_ids]

# Batch insert
pg_store.add(all_vectors, all_metadata, all_ids)
```

### Backup and Restore

Use PostgreSQL native backup tools:

```bash
# Backup
pg_dump -h localhost -U postgres -d semantica -t document_vectors > vectors_backup.sql

# Restore
psql -h localhost -U postgres -d semantica < vectors_backup.sql
```

## Configuration

### Connection Pool Settings

```python
store = PgVectorStore(
    connection_string="postgresql://...",
    table_name="vectors",
    dimension=768,
    distance_metric="cosine",
    pool_size=10,        # Max connections in pool
    max_overflow=10      # Extra connections beyond pool_size
)
```

### Environment Variables

```bash
# Connection string via environment
export SEMANTICA_PGVECTOR_URL="postgresql://user:pass@host/db"
```

```python
import os

store = PgVectorStore(
    connection_string=os.getenv("SEMANTICA_PGVECTOR_URL"),
    table_name="vectors",
    dimension=768,
    distance_metric="cosine"
)
```

## Error Handling

Common errors and solutions:

| Error | Cause | Solution |
|-------|-------|----------|
| `ProcessingError: pgvector extension is not installed` | pgvector not in PostgreSQL | Run `CREATE EXTENSION vector;` |
| `ValidationError: Unsupported distance metric` | Invalid metric | Use: `cosine`, `l2`, `inner_product` |
| `ValidationError: dimension mismatch` | Vector dim != store dim | Ensure consistent dimensions |
| `ProcessingError: Failed to initialize connection pool` | Connection issue | Check connection string, network |

## Performance Tuning

1. **Use indexes for large datasets** (>10k vectors)
2. **Tune HNSW parameters**: Higher `m` and `ef_construction` = better recall, slower build
3. **Connection pool size**: Set based on concurrent workload
4. **Batch operations**: Use `add()` with lists instead of individual inserts

## Testing

Tests require a running PostgreSQL with pgvector:

```bash
# Start PostgreSQL with Docker
docker run -d \
    --name pgvector-test \
    -e POSTGRES_PASSWORD=postgres \
    -p 5432:5432 \
    ankane/pgvector:latest

# Run tests
pytest tests/vector_store/test_pgvector_store.py -v

# Or with specific connection string
TEST_PGVECTOR_URL="postgresql://postgres:postgres@localhost:5432/test" \
    pytest tests/vector_store/test_pgvector_store.py -v
```

## See Also

- [pgvector Documentation](https://github.com/pgvector/pgvector)
- [PgVector Python Client](https://github.com/pgvector/pgvector-python)
- [psycopg Documentation](https://www.psycopg.org/)
- [Vector Store Usage Guide](../vector_store_usage.md)
