Skip to content

Commit a589f8b

Browse files
phernandezclaude[bot]claude
authored
feat: enhance search_notes tool documentation with comprehensive syntax examples (#186)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent c2f4b63 commit a589f8b

2 files changed

Lines changed: 178 additions & 60 deletions

File tree

src/basic_memory/mcp/tools/search.py

Lines changed: 115 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,18 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
4545
- Boolean OR: `meeting OR discussion`
4646
- Boolean NOT: `project NOT archived`
4747
- Grouped: `(project OR planning) AND notes`
48+
- Exact phrases: `"weekly standup meeting"`
49+
- Content-specific: `tag:example` or `category:observation`
4850
4951
## Try again with:
5052
```
51-
search_notes("INSERT_CLEAN_QUERY_HERE")
53+
search_notes("{clean_query}")
5254
```
5355
54-
Replace INSERT_CLEAN_QUERY_HERE with your simplified search terms.
56+
## Alternative search strategies:
57+
- Break into simpler terms: `search_notes("{' '.join(clean_query.split()[:2])}")`
58+
- Try different search types: `search_notes("{clean_query}", search_type="title")`
59+
- Use filtering: `search_notes("{clean_query}", types=["entity"])`
5560
""").strip()
5661

5762
# Project not found errors (check before general "not found")
@@ -85,24 +90,39 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
8590
8691
No content found matching '{query}' in the current project.
8792
88-
## Suggestions to try:
93+
## Search strategy suggestions:
8994
1. **Broaden your search**: Try fewer or more general terms
9095
- Instead of: `{query}`
9196
- Try: `{simplified_query}`
9297
93-
2. **Check spelling**: Verify terms are spelled correctly
94-
3. **Try different search types**:
95-
- Text search: `search_notes("{query}", search_type="text")`
96-
- Title search: `search_notes("{query}", search_type="title")`
97-
- Permalink search: `search_notes("{query}", search_type="permalink")`
98-
99-
4. **Use boolean operators**:
100-
- Try OR search for broader results
101-
102-
## Check what content exists:
103-
- Recent activity: `recent_activity(timeframe="7d")`
104-
- List files: `list_directory("/")`
105-
- Browse by folder: `list_directory("/notes")` or `list_directory("/docs")`
98+
2. **Check spelling and try variations**:
99+
- Verify terms are spelled correctly
100+
- Try synonyms or related terms
101+
102+
3. **Use different search approaches**:
103+
- **Text search**: `search_notes("{query}", search_type="text")` (searches full content)
104+
- **Title search**: `search_notes("{query}", search_type="title")` (searches only titles)
105+
- **Permalink search**: `search_notes("{query}", search_type="permalink")` (searches file paths)
106+
107+
4. **Try boolean operators for broader results**:
108+
- OR search: `search_notes("{' OR '.join(query.split()[:3])}")`
109+
- Remove restrictive terms: Focus on the most important keywords
110+
111+
5. **Use filtering to narrow scope**:
112+
- By content type: `search_notes("{query}", types=["entity"])`
113+
- By recent content: `search_notes("{query}", after_date="1 week")`
114+
- By entity type: `search_notes("{query}", entity_types=["observation"])`
115+
116+
6. **Try advanced search patterns**:
117+
- Tag search: `search_notes("tag:your-tag")`
118+
- Category search: `search_notes("category:observation")`
119+
- Pattern matching: `search_notes("*{query}*", search_type="permalink")`
120+
121+
## Explore what content exists:
122+
- **Recent activity**: `recent_activity(timeframe="7d")` - See what's been updated recently
123+
- **List directories**: `list_directory("/")` - Browse all content
124+
- **Browse by folder**: `list_directory("/notes")` or `list_directory("/docs")`
125+
- **Check project**: `get_current_project()` - Verify you're in the right project
106126
""").strip()
107127

108128
# Server/API errors
@@ -151,25 +171,36 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
151171
152172
Error searching for '{query}': {error_message}
153173
154-
## General troubleshooting:
155-
1. **Check your query**: Ensure it uses valid search syntax
156-
2. **Try simpler terms**: Use basic words without special characters
174+
## Troubleshooting steps:
175+
1. **Simplify your query**: Try basic words without special characters
176+
2. **Check search syntax**: Ensure boolean operators are correctly formatted
157177
3. **Verify project access**: Make sure you can access the current project
158-
4. **Check recent activity**: `recent_activity(timeframe="7d")` to see if content exists
159-
160-
## Alternative approaches:
161-
- Browse files: `list_directory("/")`
162-
- Try different search type: `search_notes("{query}", search_type="title")`
163-
- Search with filters: `search_notes("{query}", types=["entity"])`
164-
165-
## Need help?
166-
- View recent changes: `recent_activity()`
167-
- List projects: `list_projects()`
168-
- Check current project: `get_current_project()`"""
178+
4. **Test with simple search**: Try `search_notes("test")` to verify search is working
179+
180+
## Alternative search approaches:
181+
- **Different search types**:
182+
- Title only: `search_notes("{query}", search_type="title")`
183+
- Permalink patterns: `search_notes("{query}*", search_type="permalink")`
184+
- **With filters**: `search_notes("{query}", types=["entity"])`
185+
- **Recent content**: `search_notes("{query}", after_date="1 week")`
186+
- **Boolean variations**: `search_notes("{' OR '.join(query.split()[:2])}")`
187+
188+
## Explore your content:
189+
- **Browse files**: `list_directory("/")` - See all available content
190+
- **Recent activity**: `recent_activity(timeframe="7d")` - Check what's been updated
191+
- **Project info**: `get_current_project()` - Verify current project
192+
- **All projects**: `list_projects()` - Switch to different project if needed
193+
194+
## Search syntax reference:
195+
- **Basic**: `keyword` or `multiple words`
196+
- **Boolean**: `term1 AND term2`, `term1 OR term2`, `term1 NOT term2`
197+
- **Phrases**: `"exact phrase"`
198+
- **Grouping**: `(term1 OR term2) AND term3`
199+
- **Patterns**: `tag:example`, `category:observation`"""
169200

170201

171202
@mcp.tool(
172-
description="Search across all content in the knowledge base.",
203+
description="Search across all content in the knowledge base with advanced syntax support.",
173204
)
174205
async def search_notes(
175206
query: str,
@@ -181,24 +212,60 @@ async def search_notes(
181212
after_date: Optional[str] = None,
182213
project: Optional[str] = None,
183214
) -> SearchResponse | str:
184-
"""Search across all content in the knowledge base.
215+
"""Search across all content in the knowledge base with comprehensive syntax support.
185216
186217
This tool searches the knowledge base using full-text search, pattern matching,
187218
or exact permalink lookup. It supports filtering by content type, entity type,
188-
and date.
219+
and date, with advanced boolean and phrase search capabilities.
220+
221+
## Search Syntax Examples
222+
223+
### Basic Searches
224+
- `search_notes("keyword")` - Find any content containing "keyword"
225+
- `search_notes("exact phrase")` - Search for exact phrase match
226+
227+
### Advanced Boolean Searches
228+
- `search_notes("term1 term2")` - Find content with both terms (implicit AND)
229+
- `search_notes("term1 AND term2")` - Explicit AND search (both terms required)
230+
- `search_notes("term1 OR term2")` - Either term can be present
231+
- `search_notes("term1 NOT term2")` - Include term1 but exclude term2
232+
- `search_notes("(project OR planning) AND notes")` - Grouped boolean logic
233+
234+
### Content-Specific Searches
235+
- `search_notes("tag:example")` - Search within specific tags (if supported by content)
236+
- `search_notes("category:observation")` - Filter by observation categories
237+
- `search_notes("author:username")` - Find content by author (if metadata available)
238+
239+
### Search Type Examples
240+
- `search_notes("Meeting", search_type="title")` - Search only in titles
241+
- `search_notes("docs/meeting-*", search_type="permalink")` - Pattern match permalinks
242+
- `search_notes("keyword", search_type="text")` - Full-text search (default)
243+
244+
### Filtering Options
245+
- `search_notes("query", types=["entity"])` - Search only entities
246+
- `search_notes("query", types=["note", "person"])` - Multiple content types
247+
- `search_notes("query", entity_types=["observation"])` - Filter by entity type
248+
- `search_notes("query", after_date="2024-01-01")` - Recent content only
249+
- `search_notes("query", after_date="1 week")` - Relative date filtering
250+
251+
### Advanced Pattern Examples
252+
- `search_notes("project AND (meeting OR discussion)")` - Complex boolean logic
253+
- `search_notes("\"exact phrase\" AND keyword")` - Combine phrase and keyword search
254+
- `search_notes("bug NOT fixed")` - Exclude resolved issues
255+
- `search_notes("docs/2024-*", search_type="permalink")` - Year-based permalink search
189256
190257
Args:
191-
query: The search query string
258+
query: The search query string (supports boolean operators, phrases, patterns)
192259
page: The page number of results to return (default 1)
193260
page_size: The number of results to return per page (default 10)
194261
search_type: Type of search to perform, one of: "text", "title", "permalink" (default: "text")
195262
types: Optional list of note types to search (e.g., ["note", "person"])
196263
entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
197-
after_date: Optional date filter for recent content (e.g., "1 week", "2d")
264+
after_date: Optional date filter for recent content (e.g., "1 week", "2d", "2024-01-01")
198265
project: Optional project name to search in. If not provided, uses current active project.
199266
200267
Returns:
201-
SearchResponse with results and pagination info
268+
SearchResponse with results and pagination info, or helpful error guidance if search fails
202269
203270
Examples:
204271
# Basic text search
@@ -216,16 +283,19 @@ async def search_notes(
216283
# Boolean search with grouping
217284
results = await search_notes("(project OR planning) AND notes")
218285
286+
# Exact phrase search
287+
results = await search_notes("\"weekly standup meeting\"")
288+
219289
# Search with type filter
220290
results = await search_notes(
221291
query="meeting notes",
222292
types=["entity"],
223293
)
224294
225-
# Search with entity type filter, e.g., note vs
295+
# Search with entity type filter
226296
results = await search_notes(
227297
query="meeting notes",
228-
types=["entity"],
298+
entity_types=["observation"],
229299
)
230300
231301
# Search for recent content
@@ -242,6 +312,13 @@ async def search_notes(
242312
243313
# Search in specific project
244314
results = await search_notes("meeting notes", project="work-project")
315+
316+
# Complex search with multiple filters
317+
results = await search_notes(
318+
query="(bug OR issue) AND NOT resolved",
319+
types=["entity"],
320+
after_date="2024-01-01"
321+
)
245322
"""
246323
# Create a SearchQuery object based on the parameters
247324
search_query = SearchQuery()

tests/mcp/test_tool_search.py

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from basic_memory.mcp.tools import write_note
88
from basic_memory.mcp.tools.search import search_notes, _format_search_error_response
9+
from basic_memory.schemas.search import SearchResponse
910

1011

1112
@pytest.mark.asyncio
@@ -23,9 +24,14 @@ async def test_search_text(client):
2324
# Search for it
2425
response = await search_notes.fn(query="searchable")
2526

26-
# Verify results
27-
assert len(response.results) > 0
28-
assert any(r.permalink == "test/test-search-note" for r in response.results)
27+
# Verify results - handle both success and error cases
28+
if isinstance(response, SearchResponse):
29+
# Success case - verify SearchResponse
30+
assert len(response.results) > 0
31+
assert any(r.permalink == "test/test-search-note" for r in response.results)
32+
else:
33+
# If search failed and returned error message, test should fail with informative message
34+
pytest.fail(f"Search failed with error: {response}")
2935

3036

3137
@pytest.mark.asyncio
@@ -43,9 +49,14 @@ async def test_search_title(client):
4349
# Search for it
4450
response = await search_notes.fn(query="Search Note", search_type="title")
4551

46-
# Verify results
47-
assert len(response.results) > 0
48-
assert any(r.permalink == "test/test-search-note" for r in response.results)
52+
# Verify results - handle both success and error cases
53+
if isinstance(response, str):
54+
# If search failed and returned error message, test should fail with informative message
55+
pytest.fail(f"Search failed with error: {response}")
56+
else:
57+
# Success case - verify SearchResponse
58+
assert len(response.results) > 0
59+
assert any(r.permalink == "test/test-search-note" for r in response.results)
4960

5061

5162
@pytest.mark.asyncio
@@ -63,9 +74,14 @@ async def test_search_permalink(client):
6374
# Search for it
6475
response = await search_notes.fn(query="test/test-search-note", search_type="permalink")
6576

66-
# Verify results
67-
assert len(response.results) > 0
68-
assert any(r.permalink == "test/test-search-note" for r in response.results)
77+
# Verify results - handle both success and error cases
78+
if isinstance(response, SearchResponse):
79+
# Success case - verify SearchResponse
80+
assert len(response.results) > 0
81+
assert any(r.permalink == "test/test-search-note" for r in response.results)
82+
else:
83+
# If search failed and returned error message, test should fail with informative message
84+
pytest.fail(f"Search failed with error: {response}")
6985

7086

7187
@pytest.mark.asyncio
@@ -83,9 +99,14 @@ async def test_search_permalink_match(client):
8399
# Search for it
84100
response = await search_notes.fn(query="test/test-search-*", search_type="permalink")
85101

86-
# Verify results
87-
assert len(response.results) > 0
88-
assert any(r.permalink == "test/test-search-note" for r in response.results)
102+
# Verify results - handle both success and error cases
103+
if isinstance(response, SearchResponse):
104+
# Success case - verify SearchResponse
105+
assert len(response.results) > 0
106+
assert any(r.permalink == "test/test-search-note" for r in response.results)
107+
else:
108+
# If search failed and returned error message, test should fail with informative message
109+
pytest.fail(f"Search failed with error: {response}")
89110

90111

91112
@pytest.mark.asyncio
@@ -103,9 +124,14 @@ async def test_search_pagination(client):
103124
# Search for it
104125
response = await search_notes.fn(query="searchable", page=1, page_size=1)
105126

106-
# Verify results
107-
assert len(response.results) == 1
108-
assert any(r.permalink == "test/test-search-note" for r in response.results)
127+
# Verify results - handle both success and error cases
128+
if isinstance(response, SearchResponse):
129+
# Success case - verify SearchResponse
130+
assert len(response.results) == 1
131+
assert any(r.permalink == "test/test-search-note" for r in response.results)
132+
else:
133+
# If search failed and returned error message, test should fail with informative message
134+
pytest.fail(f"Search failed with error: {response}")
109135

110136

111137
@pytest.mark.asyncio
@@ -121,8 +147,13 @@ async def test_search_with_type_filter(client):
121147
# Search with type filter
122148
response = await search_notes.fn(query="type", types=["note"])
123149

124-
# Verify all results are entities
125-
assert all(r.type == "entity" for r in response.results)
150+
# Verify results - handle both success and error cases
151+
if isinstance(response, SearchResponse):
152+
# Success case - verify all results are entities
153+
assert all(r.type == "entity" for r in response.results)
154+
else:
155+
# If search failed and returned error message, test should fail with informative message
156+
pytest.fail(f"Search failed with error: {response}")
126157

127158

128159
@pytest.mark.asyncio
@@ -138,8 +169,13 @@ async def test_search_with_entity_type_filter(client):
138169
# Search with entity type filter
139170
response = await search_notes.fn(query="type", entity_types=["entity"])
140171

141-
# Verify all results are entities
142-
assert all(r.type == "entity" for r in response.results)
172+
# Verify results - handle both success and error cases
173+
if isinstance(response, SearchResponse):
174+
# Success case - verify all results are entities
175+
assert all(r.type == "entity" for r in response.results)
176+
else:
177+
# If search failed and returned error message, test should fail with informative message
178+
pytest.fail(f"Search failed with error: {response}")
143179

144180

145181
@pytest.mark.asyncio
@@ -156,8 +192,13 @@ async def test_search_with_date_filter(client):
156192
one_hour_ago = datetime.now() - timedelta(hours=1)
157193
response = await search_notes.fn(query="recent", after_date=one_hour_ago.isoformat())
158194

159-
# Verify we get results within timeframe
160-
assert len(response.results) > 0
195+
# Verify results - handle both success and error cases
196+
if isinstance(response, SearchResponse):
197+
# Success case - verify we get results within timeframe
198+
assert len(response.results) > 0
199+
else:
200+
# If search failed and returned error message, test should fail with informative message
201+
pytest.fail(f"Search failed with error: {response}")
161202

162203

163204
class TestSearchErrorFormatting:
@@ -212,7 +253,7 @@ def test_format_search_error_generic(self):
212253

213254
assert "# Search Failed" in result
214255
assert "Error searching for 'test query': unknown error" in result
215-
assert "General troubleshooting" in result
256+
assert "## Troubleshooting steps:" in result
216257

217258

218259
class TestSearchToolErrorHandling:

0 commit comments

Comments
 (0)