feat(similarity): add precomputed similarity cache, fix fusion N+1, add 429 retry

- Add similarity/ package (compute.py, lookup.py) with numpy-based
  pairwise cosine similarity and cached lookup
- database.py: create embeddings + similarity_cache tables in _init_database(),
  add store_similarity_batch/get_cached_similarities/clear_similarity_cache helpers
- pipeline/fusion.py: replace N+1 per-motion embedding SELECT with single
  bulk JOIN using DuckDB QUALIFY window function
- ai_provider.py: retry HTTP 429 with Retry-After header support
- migrations/2026-03-22-add-similarity-cache.sql: make executable
- Add tests for similarity compute, db helpers, and 429 retry (34 pass, 2 skip)
This commit is contained in:
2026-03-22 03:02:25 +01:00
parent a248807e03
commit a78bee9b0a
11 changed files with 879 additions and 38 deletions
+41 -23
View File
@@ -30,20 +30,50 @@ def fuse_for_window(
# MotionDatabase always exposes the path it uses
conn = duckdb.connect(db.db_path)
# Fetch svd vectors for the window and entity_type=motion
rows = conn.execute(
"SELECT entity_id, vector FROM svd_vectors WHERE window_id = ? AND entity_type = ?",
(window_id, "motion"),
).fetchall()
# debug
_logger.debug("Found %d svd rows for window %s", len(rows), window_id)
# Perform a single query that joins SVD vectors (for motions in the window)
# with the latest text embedding per motion (optionally filtered by model).
# We use a CTE to pick the latest embedding per motion_id.
if model:
sql = (
"WITH latest_embeddings AS ("
" SELECT motion_id, vector FROM ("
" SELECT motion_id, vector, ROW_NUMBER() OVER (PARTITION BY motion_id ORDER BY created_at DESC) AS rn"
" FROM embeddings WHERE model = ?"
" ) WHERE rn = 1)"
" SELECT sv.entity_id, sv.vector as svd_vector, le.vector as embedding_vector"
" FROM svd_vectors sv"
" LEFT JOIN latest_embeddings le ON CAST(sv.entity_id AS INTEGER) = le.motion_id"
" WHERE sv.window_id = ? AND sv.entity_type = 'motion'"
)
params = (model, window_id)
else:
sql = (
"WITH latest_embeddings AS ("
" SELECT motion_id, vector FROM ("
" SELECT motion_id, vector, ROW_NUMBER() OVER (PARTITION BY motion_id ORDER BY created_at DESC) AS rn"
" FROM embeddings"
" ) WHERE rn = 1)"
" SELECT sv.entity_id, sv.vector as svd_vector, le.vector as embedding_vector"
" FROM svd_vectors sv"
" LEFT JOIN latest_embeddings le ON CAST(sv.entity_id AS INTEGER) = le.motion_id"
" WHERE sv.window_id = ? AND sv.entity_type = 'motion'"
)
params = (window_id,)
rows = conn.execute(sql, params).fetchall()
_logger.debug(
"Found %d svd rows for window %s (joined with latest embeddings)",
len(rows),
window_id,
)
inserted = 0
skipped_missing_text = 0
skipped_missing_svd = 0
errors = 0
for entity_id, svd_json in rows:
for entity_id, svd_json, emb_json in rows:
# Parse SVD vector
try:
svd_vec = json.loads(svd_json)
except Exception:
@@ -51,25 +81,13 @@ def fuse_for_window(
skipped_missing_svd += 1
continue
# Look up text embedding for this motion (most recent). If model is provided
# filter by model as well.
if model:
emb_row = conn.execute(
"SELECT vector FROM embeddings WHERE motion_id = ? AND model = ? ORDER BY created_at DESC LIMIT 1",
(int(entity_id), model),
).fetchone()
else:
emb_row = conn.execute(
"SELECT vector FROM embeddings WHERE motion_id = ? ORDER BY created_at DESC LIMIT 1",
(int(entity_id),),
).fetchone()
if not emb_row:
# If there is no embedding joined, skip
if not emb_json:
skipped_missing_text += 1
continue
try:
text_vec = json.loads(emb_row[0])
text_vec = json.loads(emb_json)
except Exception:
_logger.exception("Invalid text embedding JSON for motion %s", entity_id)
skipped_missing_text += 1