Skip to content

Commit 3684841

Browse files
phernandezclaude
andauthored
feat(core): add default_search_type config setting (#676)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a77b51a commit 3684841

4 files changed

Lines changed: 96 additions & 12 deletions

File tree

src/basic_memory/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@ class BasicMemoryConfig(BaseSettings):
203203
ge=0.0,
204204
le=1.0,
205205
)
206+
default_search_type: Literal["text", "vector", "hybrid"] | None = Field(
207+
default=None,
208+
description="Default search type for search_notes when not specified per-query. "
209+
"Valid values: text, vector, hybrid. "
210+
"When unset, defaults to 'hybrid' if semantic search is enabled, otherwise 'text'.",
211+
)
206212

207213
# Database connection pool configuration (Postgres only)
208214
db_pool_size: int = Field(

src/basic_memory/mcp/tools/search.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@
2525
)
2626

2727

28-
def _semantic_search_enabled_for_text_search() -> bool:
29-
"""Resolve semantic-search enablement in both MCP and CLI invocation paths."""
28+
def _default_search_type() -> str:
29+
"""Pick default search mode from config, falling back to auto-detection.
30+
31+
Priority: config default_search_type > auto-detect (hybrid if semantic enabled, else text).
32+
"""
3033
try:
31-
return get_container().config.semantic_search_enabled
34+
config = get_container().config
3235
except RuntimeError:
33-
# Trigger: MCP container is not initialized (e.g., `bm tool search-notes` direct call).
34-
# Why: CLI path still needs the same semantic-default behavior as MCP server path.
35-
# Outcome: load config directly and keep text-mode retrieval behavior consistent.
36-
return ConfigManager().config.semantic_search_enabled
36+
config = ConfigManager().config
3737

38+
if config.default_search_type:
39+
return config.default_search_type
3840

39-
def _default_search_type() -> str:
40-
"""Pick default search mode from semantic-search config."""
41-
return "hybrid" if _semantic_search_enabled_for_text_search() else "text"
41+
return "hybrid" if config.semantic_search_enabled else "text"
4242

4343

4444
def _format_search_error_response(

tests/mcp/test_tool_search.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,7 @@ async def search(self, payload, page, page_size):
788788
@dataclass
789789
class StubConfig:
790790
semantic_search_enabled: bool = True
791+
default_search_type: str | None = None
791792

792793
@dataclass
793794
class StubContainer:
@@ -849,6 +850,7 @@ async def search(self, payload, page, page_size):
849850
@dataclass
850851
class StubConfig:
851852
semantic_search_enabled: bool = False
853+
default_search_type: str | None = None
852854

853855
@dataclass
854856
class StubContainer:
@@ -909,6 +911,7 @@ async def search(self, payload, page, page_size):
909911
@dataclass
910912
class StubConfig:
911913
semantic_search_enabled: bool = True
914+
default_search_type: str | None = None
912915

913916
@dataclass
914917
class StubContainer:
@@ -976,7 +979,11 @@ def raise_runtime_error():
976979
lambda: type(
977980
"StubConfigManager",
978981
(),
979-
{"config": type("Cfg", (), {"semantic_search_enabled": True})()},
982+
{
983+
"config": type(
984+
"Cfg", (), {"semantic_search_enabled": True, "default_search_type": None}
985+
)()
986+
},
980987
)(),
981988
)
982989

@@ -1037,7 +1044,11 @@ def raise_runtime_error():
10371044
lambda: type(
10381045
"StubConfigManager",
10391046
(),
1040-
{"config": type("Cfg", (), {"semantic_search_enabled": False})()},
1047+
{
1048+
"config": type(
1049+
"Cfg", (), {"semantic_search_enabled": False, "default_search_type": None}
1050+
)()
1051+
},
10411052
)(),
10421053
)
10431054

@@ -1567,3 +1578,54 @@ async def search(self, payload, page, page_size):
15671578

15681579
# "note_type" aliased to "type", "priority" passes through unchanged
15691580
assert captured_payload["metadata_filters"] == {"type": "spec", "priority": "high"}
1581+
1582+
1583+
def test_default_search_type_uses_config_value():
1584+
"""_default_search_type should return config.default_search_type when set."""
1585+
import sys
1586+
from unittest.mock import MagicMock, patch
1587+
1588+
search_module = sys.modules["basic_memory.mcp.tools.search"]
1589+
1590+
mock_config = MagicMock()
1591+
mock_config.default_search_type = "vector"
1592+
mock_config.semantic_search_enabled = True
1593+
mock_container = MagicMock()
1594+
mock_container.config = mock_config
1595+
1596+
with patch.object(search_module, "get_container", return_value=mock_container):
1597+
assert search_module._default_search_type() == "vector"
1598+
1599+
1600+
def test_default_search_type_falls_back_to_hybrid_when_semantic_enabled():
1601+
"""When default_search_type is None and semantic is enabled, default to hybrid."""
1602+
import sys
1603+
from unittest.mock import MagicMock, patch
1604+
1605+
search_module = sys.modules["basic_memory.mcp.tools.search"]
1606+
1607+
mock_config = MagicMock()
1608+
mock_config.default_search_type = None
1609+
mock_config.semantic_search_enabled = True
1610+
mock_container = MagicMock()
1611+
mock_container.config = mock_config
1612+
1613+
with patch.object(search_module, "get_container", return_value=mock_container):
1614+
assert search_module._default_search_type() == "hybrid"
1615+
1616+
1617+
def test_default_search_type_falls_back_to_text_when_semantic_disabled():
1618+
"""When default_search_type is None and semantic is disabled, default to text."""
1619+
import sys
1620+
from unittest.mock import MagicMock, patch
1621+
1622+
search_module = sys.modules["basic_memory.mcp.tools.search"]
1623+
1624+
mock_config = MagicMock()
1625+
mock_config.default_search_type = None
1626+
mock_config.semantic_search_enabled = False
1627+
mock_container = MagicMock()
1628+
mock_container.config = mock_config
1629+
1630+
with patch.object(search_module, "get_container", return_value=mock_container):
1631+
assert search_module._default_search_type() == "text"

tests/test_config.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,22 @@ def test_semantic_min_similarity_bounds_validation(self):
903903
with pytest.raises(Exception):
904904
BasicMemoryConfig(semantic_min_similarity=1.1)
905905

906+
def test_default_search_type_defaults_to_none(self):
907+
"""default_search_type should be None by default (auto-detect)."""
908+
config = BasicMemoryConfig()
909+
assert config.default_search_type is None
910+
911+
def test_default_search_type_accepts_valid_values(self):
912+
"""default_search_type accepts text, vector, hybrid."""
913+
for search_type in ("text", "vector", "hybrid"):
914+
config = BasicMemoryConfig(default_search_type=search_type)
915+
assert config.default_search_type == search_type
916+
917+
def test_default_search_type_rejects_invalid_values(self):
918+
"""default_search_type rejects unknown values."""
919+
with pytest.raises(Exception):
920+
BasicMemoryConfig(default_search_type="invalid")
921+
906922

907923
class TestFormattingConfig:
908924
"""Test file formatting configuration options."""

0 commit comments

Comments
 (0)