Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/basic_memory/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ class BasicMemoryConfig(BaseSettings):
ge=0.0,
le=1.0,
)
default_search_type: Literal["text", "vector", "hybrid"] | None = Field(
default=None,
description="Default search type for search_notes when not specified per-query. "
"Valid values: text, vector, hybrid. "
"When unset, defaults to 'hybrid' if semantic search is enabled, otherwise 'text'.",
)

# Database connection pool configuration (Postgres only)
db_pool_size: int = Field(
Expand Down
20 changes: 10 additions & 10 deletions src/basic_memory/mcp/tools/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@
)


def _semantic_search_enabled_for_text_search() -> bool:
"""Resolve semantic-search enablement in both MCP and CLI invocation paths."""
def _default_search_type() -> str:
"""Pick default search mode from config, falling back to auto-detection.

Priority: config default_search_type > auto-detect (hybrid if semantic enabled, else text).
"""
try:
return get_container().config.semantic_search_enabled
config = get_container().config
except RuntimeError:
# Trigger: MCP container is not initialized (e.g., `bm tool search-notes` direct call).
# Why: CLI path still needs the same semantic-default behavior as MCP server path.
# Outcome: load config directly and keep text-mode retrieval behavior consistent.
return ConfigManager().config.semantic_search_enabled
config = ConfigManager().config

if config.default_search_type:
return config.default_search_type

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


def _format_search_error_response(
Expand Down
66 changes: 64 additions & 2 deletions tests/mcp/test_tool_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,7 @@ async def search(self, payload, page, page_size):
@dataclass
class StubConfig:
semantic_search_enabled: bool = True
default_search_type: str | None = None

@dataclass
class StubContainer:
Expand Down Expand Up @@ -849,6 +850,7 @@ async def search(self, payload, page, page_size):
@dataclass
class StubConfig:
semantic_search_enabled: bool = False
default_search_type: str | None = None

@dataclass
class StubContainer:
Expand Down Expand Up @@ -909,6 +911,7 @@ async def search(self, payload, page, page_size):
@dataclass
class StubConfig:
semantic_search_enabled: bool = True
default_search_type: str | None = None

@dataclass
class StubContainer:
Expand Down Expand Up @@ -976,7 +979,11 @@ def raise_runtime_error():
lambda: type(
"StubConfigManager",
(),
{"config": type("Cfg", (), {"semantic_search_enabled": True})()},
{
"config": type(
"Cfg", (), {"semantic_search_enabled": True, "default_search_type": None}
)()
},
)(),
)

Expand Down Expand Up @@ -1037,7 +1044,11 @@ def raise_runtime_error():
lambda: type(
"StubConfigManager",
(),
{"config": type("Cfg", (), {"semantic_search_enabled": False})()},
{
"config": type(
"Cfg", (), {"semantic_search_enabled": False, "default_search_type": None}
)()
},
)(),
)

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

# "note_type" aliased to "type", "priority" passes through unchanged
assert captured_payload["metadata_filters"] == {"type": "spec", "priority": "high"}


def test_default_search_type_uses_config_value():
"""_default_search_type should return config.default_search_type when set."""
import sys
from unittest.mock import MagicMock, patch

search_module = sys.modules["basic_memory.mcp.tools.search"]

mock_config = MagicMock()
mock_config.default_search_type = "vector"
mock_config.semantic_search_enabled = True
mock_container = MagicMock()
mock_container.config = mock_config

with patch.object(search_module, "get_container", return_value=mock_container):
assert search_module._default_search_type() == "vector"


def test_default_search_type_falls_back_to_hybrid_when_semantic_enabled():
"""When default_search_type is None and semantic is enabled, default to hybrid."""
import sys
from unittest.mock import MagicMock, patch

search_module = sys.modules["basic_memory.mcp.tools.search"]

mock_config = MagicMock()
mock_config.default_search_type = None
mock_config.semantic_search_enabled = True
mock_container = MagicMock()
mock_container.config = mock_config

with patch.object(search_module, "get_container", return_value=mock_container):
assert search_module._default_search_type() == "hybrid"


def test_default_search_type_falls_back_to_text_when_semantic_disabled():
"""When default_search_type is None and semantic is disabled, default to text."""
import sys
from unittest.mock import MagicMock, patch

search_module = sys.modules["basic_memory.mcp.tools.search"]

mock_config = MagicMock()
mock_config.default_search_type = None
mock_config.semantic_search_enabled = False
mock_container = MagicMock()
mock_container.config = mock_config

with patch.object(search_module, "get_container", return_value=mock_container):
assert search_module._default_search_type() == "text"
16 changes: 16 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,22 @@ def test_semantic_min_similarity_bounds_validation(self):
with pytest.raises(Exception):
BasicMemoryConfig(semantic_min_similarity=1.1)

def test_default_search_type_defaults_to_none(self):
"""default_search_type should be None by default (auto-detect)."""
config = BasicMemoryConfig()
assert config.default_search_type is None

def test_default_search_type_accepts_valid_values(self):
"""default_search_type accepts text, vector, hybrid."""
for search_type in ("text", "vector", "hybrid"):
config = BasicMemoryConfig(default_search_type=search_type)
assert config.default_search_type == search_type

def test_default_search_type_rejects_invalid_values(self):
"""default_search_type rejects unknown values."""
with pytest.raises(Exception):
BasicMemoryConfig(default_search_type="invalid")


class TestFormattingConfig:
"""Test file formatting configuration options."""
Expand Down
Loading