Add hybrid ranking to search endpoint (RRF fusion) #141
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/pal-e-api!141
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "139-hybrid-ranking-search"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds
mode(keyword/semantic/hybrid) andalpha(0.0-1.0) parameters toGET /notes/search, combining tsvector keyword search and pgvector semantic search via Reciprocal Rank Fusion (RRF). Defaultmode=keywordpreserves full backward compatibility.Changes
src/pal_e_docs/routes/notes.py-- Refactored search endpoint to support three modes. Extracted_keyword_search,_semantic_search_notes,_embed_query, and_build_filter_clauseshelpers from inline code. Mode validation runs before the PostgreSQL dialect check.src/pal_e_docs/services/search.py-- New module implementing RRF fusion (rrf_fuse) with configurable alpha weighting and k=60 constant. Pure-Python, no DB dependency.src/pal_e_docs/schemas.py-- MadeNoteSearchResult.headlineoptional (str | None = None) so semantic/hybrid results can omit it.tests/test_hybrid_search.py-- 29 tests: 13 RRF unit tests (empty lists, overlap boost, alpha extremes, clamping, tiebreaking, fallback ranks, limit), 11 endpoint validation tests (mode/alpha validation, backward compat), 2 Ollama error propagation tests, 3 schema tests.Test Plan
pytest tests/test_hybrid_search.py -v-- 29 passedpytest tests/test_search.py tests/test_semantic_search.py -v-- 15 passed (existing tests unbroken)ruff check-- cleanReview Checklist
pytest tests/ -v-- 625 passed)ruff check)/notes/semantic-searchendpoint unchangedRelated
plan-2026-02-26-tf-modularize-postgresphase-postgres-6e-hybrid-rankingCloses #139
PR #141 Review
BLOCKERS
None. The implementation is correct, well-structured, and backward compatible.
NITS
RRF docstring formula is inverted (
src/pal_e_docs/services/search.py, line 7). The module docstring says:But the code (line 90) correctly implements:
The code matches the Query parameter description (
0.0=keyword, 1.0=semantic) and the tests confirm it. The docstring formula has alpha and (1-alpha) swapped. Low-risk since the code is correct, but confusing for anyone reading the module header.DISTINCT ON+LIMITtruncation (_semantic_search_notes, lines 305-319).DISTINCT ON (n.slug)forcesORDER BY n.slugas the primary sort, andLIMIT :limitthen clips alphabetically rather than by similarity. The Python re-sort at line 323 fixes ordering but cannot recover high-similarity notes whose slugs were alphabetically past the cutoff. For hybrid mode theinternal_limit = min(limit * 3, 100)mitigates this, but a subquery approach (SELECT * FROM (DISTINCT ON subquery) ORDER BY similarity DESC LIMIT :limit) would be more correct. Note: this is a pre-existing pattern from 6d, not introduced by this PR. File a separate issue if desired.Duplicate Ollama embedding code -- The
/notes/semantic-searchendpoint (lines 516-553) still has its own inline copy of the Ollama embedding + error handling logic that is now extracted into_embed_query(lines 238-277). The/notes/semantic-searchendpoint was intentionally left untouched per the issue scope, so this is expected. Consider a follow-up cleanup issue to DRY up the two paths.pg_clientfixture duplicated --tests/test_hybrid_search.pydefines its ownpg_clientfixture (lines 229-244) that is nearly identical to the one intests/test_semantic_search.py(lines 60-83). A shared conftest fixture would reduce duplication. Non-blocking.SOP COMPLIANCE
139-hybrid-ranking-searchreferences issue #139)plan-2026-02-26-tf-modularize-postgres)Closes #139present in PR body/notes/semantic-searchendpoint untouched (confirmed: lines 498-624 identical to main)NoteSearchResult.headlinemade optional with backward-compatible default (None)Code Quality Notes
modedefaults tokeyword,alphadefaults to 0.5,headlinedefaults toNone. Existing callers see identical behavior.ge=0.0, le=1.0with defensive clamping inrrf_fuse.services/search.pyis pure Python with no DB dependency, easily unit-testable. Route logic is straightforward dispatch.VERDICT: APPROVED