fix: use max() instead of min() in LinkResolver search fallback

The search fallback (strategy 5) was using min() to select the best match
from FTS results, which selected the WORST match instead of the best.
On both SQLite and Postgres backends, higher scores indicate better matches
(FTS rank, vector similarity normalized to [0,1], hybrid fusion). Using max()
correctly returns the highest-scoring result.

Fixes #640

Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
This commit is contained in:
claude[bot]
2026-03-02 16:42:11 +00:00
parent 8b7f39ee9b
commit 0b915b76ce
2 changed files with 50 additions and 1 deletions
+1 -1
View File
@@ -302,7 +302,7 @@ class LinkResolver:
if results:
# Look for best match
best_match = min(results, key=lambda x: x.score) # pyright: ignore
best_match = max(results, key=lambda x: x.score) # pyright: ignore
logger.trace(
f"Selected best match from {len(results)} results: {best_match.permalink}"
)
+49
View File
@@ -2,6 +2,7 @@
import uuid
from datetime import datetime, timezone
from unittest.mock import AsyncMock, patch
import pytest
@@ -10,6 +11,7 @@ import pytest_asyncio
from basic_memory.models.knowledge import Entity as EntityModel
from basic_memory.repository import EntityRepository
from basic_memory.schemas.base import Entity as EntitySchema
from basic_memory.schemas.search import SearchItemType, SearchResult
from basic_memory.services.link_resolver import LinkResolver
@@ -901,3 +903,50 @@ async def test_resolve_link_non_uuid_falls_through(link_resolver, test_entities,
result = await link_resolver.resolve_link("Core Service")
assert result is not None
assert result.permalink == f"{project_prefix}/components/core-service"
# ============================================================================
# Regression tests: search fallback score selection (Issue #640)
# ============================================================================
@pytest.mark.asyncio
async def test_search_fallback_selects_best_match_not_worst(
link_resolver, search_service, test_entities, project_prefix
):
"""Regression test for GitHub Issue #640.
The search fallback (strategy 5) must select the highest-scoring match
using max(), not the lowest-scoring match using min().
Scenario: Searching for a partial title triggers FTS which returns multiple
results with different scores. The result with the highest score (best match)
must be returned, not the result with the lowest score (worst match).
"""
better_permalink = f"{project_prefix}/components/auth-service"
worse_permalink = f"{project_prefix}/specs/core-features"
# Construct two mock search results with controlled scores
better_result = SearchResult(
title="Auth Service",
type=SearchItemType.ENTITY,
score=0.9, # High score = better match
permalink=better_permalink,
file_path="components/Auth Service.md",
)
worse_result = SearchResult(
title="Core Features",
type=SearchItemType.ENTITY,
score=0.1, # Low score = worse match
permalink=worse_permalink,
file_path="specs/Core Features.md",
)
# "Auth Serv" has no exact title or permalink match, so strategy 5 is triggered.
# Mock search to return both results in worst-first order to ensure max() is tested.
with patch.object(search_service, "search", new=AsyncMock(return_value=[worse_result, better_result])):
result = await link_resolver.resolve_link("Auth Serv", use_search=True, strict=False)
assert result is not None
# Must return the entity with the HIGHEST score, not the lowest
assert result.permalink == better_permalink