File size: 5,323 Bytes
744603c | 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 | r"""
One-off backfill: compute embeddings for rows in the DuckDB that are missing them.
Run once (with the app AND DBeaver closed, so the file isn't locked):
d:\2026\July\Agentic-RAG-with-LangGraph-and-Ollama\.venv\Scripts\python.exe backfill_embeddings.py
"""
import os
import time
import duckdb
import pandas as pd
from dotenv import load_dotenv
load_dotenv()
def env(key):
val = os.getenv(key)
if val is None or val == "":
raise RuntimeError(f"Missing required config '{key}' in .env")
return val
# All config comes from .env (no hardcoded values here).
DUCKDB_PATH = env("DUCKDB_PATH")
TABLE = env("TABLE")
HNSW_INDEX = f"{TABLE}_embedding_hnsw"
# Subset + throttling for the Gemini free tier (100 embed requests/minute).
MAX_ROWS = int(env("EMBED_MAX_ROWS")) # cap rows per run
CHUNK = int(env("EMBED_CHUNK")) # texts per API request
SLEEP = float(env("EMBED_SLEEP")) # seconds between chunks
# Must match duckdb_app.py's embedding backend (set in .env).
EMBED_PROVIDER = env("EMBED_PROVIDER")
EMBED_MODEL = env("EMBED_MODEL")
EMBED_DIM = int(env("EMBED_DIM"))
def embed_documents(texts):
if EMBED_PROVIDER == "google":
from langchain_google_genai import GoogleGenerativeAIEmbeddings
emb = GoogleGenerativeAIEmbeddings(model=EMBED_MODEL,
output_dimensionality=EMBED_DIM)
return emb.embed_documents(list(texts))
from sentence_transformers import SentenceTransformer
model = SentenceTransformer(EMBED_MODEL)
return [v.tolist() for v in model.encode(list(texts), normalize_embeddings=True)]
def build_embed_text(r):
return (
f"{r['make']} {r['model']} {r['modelYear']} | "
f"manufacturer: {r['manufacturer']} | component: {r['component']} | "
f"vin: {r['vin']} | crash: {r['crash']} | fire: {r['fire']} | "
f"injuries: {r['numberOfInjuries']} | deaths: {r['numberOfDeaths']} | "
f"filed: {r['dateComplaintFiled']} | summary: {r['summary']}"
)
def main():
con = duckdb.connect(DUCKDB_PATH)
con.execute("INSTALL vss; LOAD vss;")
con.execute("SET hnsw_enable_experimental_persistence = true;")
# Ensure columns exist (in case this DB predates them).
con.execute(f"ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS embed_text VARCHAR")
con.execute(f"ALTER TABLE {TABLE} ADD COLUMN IF NOT EXISTS embedding FLOAT[{EMBED_DIM}]")
# If the embedding dimension changed, recreate the column to match.
row = con.execute(
"SELECT data_type FROM information_schema.columns "
"WHERE table_name = ? AND column_name = 'embedding'", [TABLE]).fetchone()
want = f"FLOAT[{EMBED_DIM}]"
if row and row[0] != want:
print(f"Embedding dim changed {row[0]} -> {want}; recreating column…")
con.execute(f"DROP INDEX IF EXISTS {HNSW_INDEX}")
con.execute(f"ALTER TABLE {TABLE} DROP COLUMN embedding")
con.execute(f"ALTER TABLE {TABLE} ADD COLUMN embedding {want}")
# Ensure the HNSW vector index exists (UPDATEs below maintain it).
con.execute(f"""
CREATE INDEX IF NOT EXISTS {HNSW_INDEX}
ON {TABLE} USING HNSW (embedding) WITH (metric = 'cosine')
""")
# Subset: only embed up to MAX_ROWS per run (free-tier friendly).
todo = con.execute(f"""
SELECT odiNumber, make, model, modelYear, manufacturer, component, summary,
vin, crash, fire, numberOfInjuries, numberOfDeaths, dateComplaintFiled
FROM {TABLE}
WHERE embedding IS NULL
LIMIT {MAX_ROWS}
""").fetchdf()
if todo.empty:
print("Nothing to backfill — all selected rows already have embeddings.")
return
todo["embed_text"] = todo.apply(build_embed_text, axis=1)
total = len(todo)
print(f"Backfilling {total} rows (subset, MAX_ROWS={MAX_ROWS}) with "
f"{EMBED_PROVIDER} ({EMBED_MODEL}), chunk={CHUNK}…")
done = 0
# Process in small chunks, committing each so progress survives a quota error.
for start in range(0, total, CHUNK):
batch = todo.iloc[start:start + CHUNK]
try:
vecs = embed_documents(batch["embed_text"].tolist())
except Exception as e:
print(f" Stopped at {done}/{total} — {type(e).__name__}: {e}")
print(" Re-run later to continue where it left off (resumable).")
break
updates = pd.DataFrame({
"odiNumber": batch["odiNumber"].values,
"embed_text": batch["embed_text"].values,
"embedding": list(vecs),
})
con.register("upd", updates)
con.execute(f"""
UPDATE {TABLE} AS t
SET embed_text = u.embed_text, embedding = u.embedding
FROM upd AS u WHERE t.odiNumber = u.odiNumber
""")
con.unregister("upd")
done += len(batch)
print(f" embedded {done}/{total}")
time.sleep(SLEEP) # throttle to respect the rate limit
embedded = con.execute(
f"SELECT COUNT(*) FROM {TABLE} WHERE embedding IS NOT NULL").fetchone()[0]
remaining = con.execute(
f"SELECT COUNT(*) FROM {TABLE} WHERE embedding IS NULL").fetchone()[0]
print(f"Done. Embedded so far: {embedded} · still NULL: {remaining}")
if __name__ == "__main__":
main()
|