feat(rag): hybrid retrieval (vector + full-text), retire HyDE

Recall no longer depends on a per-query HyDE LLM call — it comes from the index.
Each chunks_<type> table gets a generated `tsv` column + GIN index (migration
031; the engine CREATE TABLE matches so fresh tables get it too).
VectorStore.hybrid_search fuses pgvector cosine with Postgres full-text in one
query: score = min(1, cosine + 0.3 * normalized_ts_rank). A vector-only match
keeps its cosine score (so decisions/reviewer thresholds are unchanged), a
keyword match adds a bounded boost (the recall win), and a keyword-only match
stays low. Empty/garbage query text degrades to pure vector.

HyDE is removed from the search hot path: _compute_query_embedding now embeds
the query directly, and _generate_hyde_passage / rag_use_hyde /
IndexConfig.use_hyde are deleted. So a search is one local embed + one indexed
SQL — no LLM round-trip. The raw query text is threaded through the
embed-once + concurrent fan-out (search_with_embedding(embedding, query_text)).

Verified live via a real pgvector round-trip: vector ranking + keyword boost +
[0,1] scores + empty-query fallback all correct. Adds wiring + fan-out unit
tests; the fusion SQL itself is verified live (needs pgvector, not gated in CI).
This commit is contained in:
Renn F
2026-06-15 06:37:27 +02:00
parent d7aee91b39
commit de82e06b3c
7 changed files with 264 additions and 96 deletions
+3 -7
View File
@@ -131,11 +131,6 @@ class Settings(BaseSettings):
default=1024, ge=100, description="Chunk size for journals/reflections"
)
rag_chunk_overlap: int = Field(default=128, ge=0)
rag_use_hyde: bool = Field(
default=True,
description="Use HyDE (hypothetical document embeddings). "
"Makes one LLM call per query for better semantic matching.",
)
rag_auto_update_enabled: bool = Field(default=True)
rag_auto_update_interval: int = Field(
default=300, ge=60, description="Seconds between auto-updates"
@@ -165,10 +160,11 @@ class Settings(BaseSettings):
description="Embedding dimensions (1024 for qwen3-embedding)",
)
# Local LLM for RAG (HyDE, reranking, etc.)
# Local LLM for RAG answer synthesis
local_llm_model: str = Field(
default="glm-5:cloud",
description="Local LLM for HyDE/RAG (non-thinking models are faster)",
description="Local LLM for RAG answer synthesis "
"(non-thinking models are faster)",
)
local_llm_base_url: str = Field(
default="http://roboco-ollama:11434/v1",