diff --git a/src/fabledassistant/services/embeddings.py b/src/fabledassistant/services/embeddings.py index 368b3ba..423917e 100644 --- a/src/fabledassistant/services/embeddings.py +++ b/src/fabledassistant/services/embeddings.py @@ -153,17 +153,22 @@ async def semantic_search_notes( if not rows: return [] - scored: list[tuple[float, Note]] = [] - for ne, note in rows: - try: - sim = _cosine_similarity(query_vec, ne.embedding) - except Exception: - continue - if sim >= threshold: - scored.append((sim, note)) + def _score() -> list[tuple[float, Note]]: + out: list[tuple[float, Note]] = [] + for ne, note in rows: + try: + sim = _cosine_similarity(query_vec, ne.embedding) + except Exception: + continue + if sim >= threshold: + out.append((sim, note)) + out.sort(key=lambda x: x[0], reverse=True) + return out[:limit] - scored.sort(key=lambda x: x[0], reverse=True) - return scored[:limit] + # Offload the O(rows) cosine scoring off the event loop so a large corpus + # doesn't stall other requests while ranking. Results are unchanged; the + # real scaling fix (ORDER BY / LIMIT in pgvector) is a separate effort. + return await asyncio.to_thread(_score) async def backfill_note_embeddings() -> None: diff --git a/src/fabledassistant/services/knowledge.py b/src/fabledassistant/services/knowledge.py index 1e0e48b..42b1141 100644 --- a/src/fabledassistant/services/knowledge.py +++ b/src/fabledassistant/services/knowledge.py @@ -140,6 +140,14 @@ async def _semantic_knowledge_search( Exact keyword matches always rank above semantic-only matches so that searching for a name like "Weston" surfaces the note with that title before conceptually related notes. + + BEST-EFFORT TOP-N, not exhaustive pagination: the ranked candidate set is + capped (keyword limit*2 + up to ~200 semantic), so `total` is the size of + that window, NOT the true match count, and matches beyond the cap are not + reachable by paging. Each page also recomputes the full merge (O(corpus) + per page). Acceptable for an interactive "best results" feed; a cached + ranked-id list or pgvector ORDER BY/LIMIT is the fix if exhaustive, + cheap pagination is ever needed. """ # 1. Keyword search — title and body ILIKE keyword_notes: list[Note] = []