File size: 5,848 Bytes
99cc0f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""
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()