Merge pull request 'perf(citation-verification): cache view + parallelize backend retrieval' (#369) from worktree-citation-verify-perf into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m33s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 10s

This commit was merged in pull request #369.
This commit is contained in:
2026-06-30 17:36:37 +00:00
2 changed files with 27 additions and 8 deletions

View File

@@ -18,6 +18,7 @@ rows. Reuses the one corpus search + the one authority query + the one radar (G2
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
from uuid import UUID from uuid import UUID
@@ -65,25 +66,37 @@ async def build_view(case_number: str) -> dict:
except Exception as e: # noqa: BLE001 — radar is best-effort except Exception as e: # noqa: BLE001 — radar is best-effort
logger.warning("citation_verification radar failed for %s: %s", case_number, e) logger.warning("citation_verification radar failed for %s: %s", case_number, e)
out_args: list[dict] = [] async def _fetch(a: dict) -> tuple[list[dict], dict]:
n_verified = 0 """Per-argument corpus search + batched authority — run concurrently.
for a in args:
aid = str(a["id"]) Each call is one Voyage embed + one vector search (+ one batched authority
query); independent across arguments, so they fan out rather than waterfall.
"""
title = (a.get("argument_title") or "").strip() title = (a.get("argument_title") or "").strip()
topic = (a.get("legal_topic") or "").strip() topic = (a.get("legal_topic") or "").strip()
query = f"{ctx} {title}. {topic}".strip() query = f"{ctx} {title}. {topic}".strip()
hits: list[dict] = []
hits = []
try: try:
hits = await precedent_library.search_library( hits = await precedent_library.search_library(
query=query, limit=_SUGGEST_PER_ISSUE, include_halachot=True) query=query, limit=_SUGGEST_PER_ISSUE, include_halachot=True)
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
logger.warning("citation_verification search failed (%s): %s", title[:30], e) logger.warning("citation_verification search failed (%s): %s", title[:30], e)
# Resolve the authority breakdown for the hit set in one batched query. # Resolve the authority breakdown for the hit set in one batched query.
clids = [UUID(str(h["case_law_id"])) for h in hits clids = [UUID(str(h["case_law_id"])) for h in hits
if h.get("case_law_id") and float(h.get("score", 0) or 0) >= _SUGGEST_FLOOR] if h.get("case_law_id") and float(h.get("score", 0) or 0) >= _SUGGEST_FLOOR]
authority = await db.citation_authority(clids) if clids else {} authority = await db.citation_authority(clids) if clids else {}
return hits, authority
# Fan out the expensive per-argument retrieval concurrently — was a sequential
# waterfall (N args × Voyage embed + vector search each). gather preserves order.
fetched = await asyncio.gather(*(_fetch(a) for a in args)) if args else []
out_args: list[dict] = []
n_verified = 0
for a, (hits, authority) in zip(args, fetched):
aid = str(a["id"])
title = (a.get("argument_title") or "").strip()
topic = (a.get("legal_topic") or "").strip()
seen: set[str] = set() seen: set[str] = set()
supporting: list[dict] = [] supporting: list[dict] = []

View File

@@ -84,7 +84,13 @@ export function useCitationVerification(caseNumber: string | undefined) {
{ signal }, { signal },
), ),
enabled: Boolean(caseNumber), enabled: Boolean(caseNumber),
staleTime: 10_000, // The view is an expensive synthesized build (per-argument Voyage embed +
// vector search on the backend). Keep it fresh for 5 min and do NOT refetch
// on remount — Radix Tabs unmounts inactive TabsContent, so every tab switch
// back used to trigger a full rebuild. Verify mutations invalidate the query,
// so edits still pull fresh data.
staleTime: 5 * 60_000,
refetchOnMount: false,
}); });
} }