66from loguru import logger
77from fastmcp import Context
88
9+ from basic_memory .config import ConfigManager
910from basic_memory .mcp .container import get_container
1011from basic_memory .mcp .project_context import get_project_client , resolve_project_and_path
1112from basic_memory .mcp .formatting import format_search_results_ascii
1819)
1920
2021
22+ def _semantic_search_enabled_for_text_search () -> bool :
23+ """Resolve semantic-search enablement in both MCP and CLI invocation paths."""
24+ try :
25+ return get_container ().config .semantic_search_enabled
26+ except RuntimeError :
27+ # Trigger: MCP container is not initialized (e.g., `bm tool search-notes` direct call).
28+ # Why: CLI path still needs the same semantic-default behavior as MCP server path.
29+ # Outcome: load config directly and keep text-mode retrieval behavior consistent.
30+ return ConfigManager ().config .semantic_search_enabled
31+
32+
2133def _format_search_error_response (
2234 project : str , error_message : str , query : str , search_type : str = "text"
2335) -> str :
@@ -268,7 +280,8 @@ async def search_notes(
268280 - `search_notes("work-docs", "'exact phrase'")` - Search for exact phrase match
269281
270282 ### Advanced Boolean Searches
271- - `search_notes("my-project", "term1 term2")` - Find content with both terms (implicit AND)
283+ - `search_notes("my-project", "term1 term2")` - Strict implicit-AND first; retries with
284+ relaxed OR terms only if strict search returns no results
272285 - `search_notes("my-project", "term1 AND term2")` - Explicit AND search (both terms required)
273286 - `search_notes("my-project", "term1 OR term2")` - Either term can be present
274287 - `search_notes("my-project", "term1 NOT term2")` - Include term1 but exclude term2
@@ -282,7 +295,8 @@ async def search_notes(
282295 ### Search Type Examples
283296 - `search_notes("my-project", "Meeting", search_type="title")` - Search only in titles
284297 - `search_notes("work-docs", "docs/meeting-*", search_type="permalink")` - Pattern match permalinks
285- - `search_notes("research", "keyword", search_type="text")` - Full-text search (default)
298+ - `search_notes("research", "keyword", search_type="text")` - Text search (default; auto-upgrades
299+ to hybrid when semantic search is enabled)
286300
287301 ### Filtering Options
288302 - `search_notes("my-project", "query", types=["entity"])` - Search only entities
@@ -325,7 +339,8 @@ async def search_notes(
325339 page: The page number of results to return (default 1)
326340 page_size: The number of results to return per page (default 10)
327341 search_type: Type of search to perform, one of:
328- "text", "title", "permalink", "vector", "semantic", "hybrid" (default: "text")
342+ "text", "title", "permalink", "vector", "semantic", "hybrid" (default: "text";
343+ text mode auto-upgrades to hybrid when semantic search is enabled)
329344 output_format: "default" returns structured data, "ascii" returns a plain text table,
330345 "ansi" returns a colorized table for TUI clients.
331346 types: Optional list of note types to search (e.g., ["note", "person"])
@@ -345,6 +360,7 @@ async def search_notes(
345360 Examples:
346361 # Basic text search
347362 results = await search_notes("project planning")
363+ # Plain multi-term text uses strict matching first, then relaxed OR fallback if needed
348364
349365 # Boolean AND search (both terms must be present)
350366 results = await search_notes("project AND planning")
@@ -424,12 +440,8 @@ async def search_notes(
424440 search_query .text = query
425441 # Upgrade to hybrid when semantic search is available —
426442 # combines FTS keyword matching with vector similarity for better results
427- try :
428- container = get_container ()
429- if container .config .semantic_search_enabled :
430- search_query .retrieval_mode = SearchRetrievalMode .HYBRID
431- except RuntimeError :
432- pass # Container not initialized (e.g., CLI context) — stay with FTS
443+ if _semantic_search_enabled_for_text_search ():
444+ search_query .retrieval_mode = SearchRetrievalMode .HYBRID
433445 elif search_type in ("vector" , "semantic" ):
434446 search_query .text = query
435447 search_query .retrieval_mode = SearchRetrievalMode .VECTOR
0 commit comments