govbridge-api / scripts /backfill_768.py
harshrawat18's picture
feat: sprint 17 - linguistic hegemony & vector synchronization
99cc0f4
Raw
History Blame Contribute Delete
5.85 kB
"""
GovBridge India β€” Phase 2: Backfill 768-dim Embeddings
Sprint 17: Zero-Downtime Expand-Contract Migration
PREREQUISITES:
1. Migration 007 has been executed (embedding_v2 column exists)
2. Nomic model is available (pip install sentence-transformers einops)
USAGE:
cd /workspaces/govbridge
python3 gov_backend/scripts/backfill_768.py
BEHAVIOR:
- Reads ALL rows from document_chunks where embedding_v2 IS NULL
- Generates 768-dim embeddings using nomic-embed-text-v1
- UPSERTs in batches of 50 rows using keyset pagination
- Also backfills query_cache.query_embedding_v2
- Safe to re-run (idempotent via IS NULL filter)
"""
import os
import sys
import time
import torch
# Ensure gov_backend is on the Python path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from config import settings
from supabase import create_client
from sentence_transformers import SentenceTransformer
# ── Configuration ────────────────────────────────────────────
BATCH_SIZE = 50 # Rows per batch (free-tier Supabase safe)
SLEEP_BETWEEN = 0.5 # Seconds between batches (prevent rate limit)
MAX_ROWS = 5000 # Safety ceiling per run
def main():
supabase_url = settings.SUPABASE_URL
supabase_key = settings.SUPABASE_KEY
if not supabase_url or not supabase_key:
print("❌ FATAL: Missing SUPABASE_URL or SUPABASE_KEY in environment")
sys.exit(1)
print("⏳ Loading Nomic Embedding Model (768-dim)...")
model = SentenceTransformer(
'nomic-ai/nomic-embed-text-v1',
trust_remote_code=True
)
print("βœ… Nomic model loaded")
supabase = create_client(supabase_url, supabase_key)
# ── Phase 2A: Backfill document_chunks.embedding_v2 ──────
print("\n═══ PHASE 2A: Backfilling document_chunks.embedding_v2 ═══")
result = supabase.table('document_chunks') \
.select('id, chunk_text, scheme_title') \
.is_('embedding_v2', 'null') \
.limit(MAX_ROWS) \
.execute()
chunks = result.data or []
total = len(chunks)
if total == 0:
print("πŸŽ‰ document_chunks: All rows already have embedding_v2")
else:
print(f"Found {total} chunks to backfill")
for i in range(0, total, BATCH_SIZE):
batch = chunks[i:i + BATCH_SIZE]
batch_num = (i // BATCH_SIZE) + 1
total_batches = (total + BATCH_SIZE - 1) // BATCH_SIZE
# Nomic requires "search_document:" prefix for document embeddings
texts = [
f"search_document: {c.get('chunk_text', '') or c.get('scheme_title', '')}"
for c in batch
]
print(f"πŸ”„ Batch {batch_num}/{total_batches} ({len(batch)} rows)...")
with torch.inference_mode():
embeddings = model.encode(
texts,
normalize_embeddings=True,
show_progress_bar=False,
batch_size=32
).tolist()
# UPSERT each row's embedding_v2
for chunk, embedding in zip(batch, embeddings):
supabase.table('document_chunks') \
.update({'embedding_v2': embedding}) \
.eq('id', chunk['id']) \
.execute()
print(f"βœ… Batch {batch_num}/{total_batches} complete")
time.sleep(SLEEP_BETWEEN)
print(f"\nπŸŽ‰ document_chunks backfill complete: {total} rows updated")
# ── Phase 2B: Backfill query_cache.query_embedding_v2 ────
print("\n═══ PHASE 2B: Backfilling query_cache.query_embedding_v2 ═══")
cache_result = supabase.table('query_cache') \
.select('id, query_text') \
.is_('query_embedding_v2', 'null') \
.limit(MAX_ROWS) \
.execute()
cache_rows = cache_result.data or []
cache_total = len(cache_rows)
if cache_total == 0:
print("πŸŽ‰ query_cache: All rows already have query_embedding_v2")
else:
print(f"Found {cache_total} cache entries to backfill")
for i in range(0, cache_total, BATCH_SIZE):
batch = cache_rows[i:i + BATCH_SIZE]
batch_num = (i // BATCH_SIZE) + 1
# Nomic requires "search_query:" prefix for query embeddings
texts = [
f"search_query: {c.get('query_text', '')}"
for c in batch
]
with torch.inference_mode():
embeddings = model.encode(
texts,
normalize_embeddings=True,
show_progress_bar=False,
batch_size=32
).tolist()
for row, embedding in zip(batch, embeddings):
supabase.table('query_cache') \
.update({'query_embedding_v2': embedding}) \
.eq('id', row['id']) \
.execute()
time.sleep(SLEEP_BETWEEN)
print(f"\nπŸŽ‰ query_cache backfill complete: {cache_total} rows updated")
# ── Summary ──────────────────────────────────────────────
print("\n" + "═" * 60)
print("MIGRATION PHASE 2 COMPLETE")
print(f" document_chunks: {total} rows backfilled")
print(f" query_cache: {cache_total} rows backfilled")
print("═" * 60)
print("\nNEXT STEPS:")
print(" 1. Run migration 008 (concurrent index creation)")
print(" 2. Verify indexes are VALID")
print(" 3. Run migration 009 (contract β€” column swap)")
if __name__ == '__main__':
main()