From 01c9b32d364fcd672587fa25f411305c4440709c Mon Sep 17 00:00:00 2001 From: phernandez Date: Sat, 31 Jan 2026 21:02:01 -0600 Subject: [PATCH 1/2] fix: support tag: syntax in search Signed-off-by: phernandez --- src/basic_memory/services/search_service.py | 11 +++ tests/services/test_search_service.py | 84 ++++++++++++++++++--- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/basic_memory/services/search_service.py b/src/basic_memory/services/search_service.py index 8b100b32..c3d6ec49 100644 --- a/src/basic_memory/services/search_service.py +++ b/src/basic_memory/services/search_service.py @@ -1,6 +1,7 @@ """Service for search operations.""" import ast +import re from datetime import datetime from typing import List, Optional, Set, Dict, Any @@ -79,6 +80,16 @@ async def search(self, query: SearchQuery, limit=10, offset=0) -> List[SearchInd 2. Pattern match: handles * wildcards in paths 3. Text search: full-text search across title/content """ + # Support tag: shorthand by mapping to tags filter + if query.text: + text = query.text.strip() + if text.lower().startswith("tag:"): + tag_values = re.split(r"[,\s]+", text[4:].strip()) + tags = [t for t in tag_values if t] + if tags: + query.tags = tags + query.text = None + if query.no_criteria(): logger.debug("no criteria passed to query") return [] diff --git a/tests/services/test_search_service.py b/tests/services/test_search_service.py index 30614456..d34a282a 100644 --- a/tests/services/test_search_service.py +++ b/tests/services/test_search_service.py @@ -345,9 +345,9 @@ async def test_boolean_not_search(search_service, test_graph): # Should find "Root Entity" but not "Connected Entity" for result in results: - assert "connected" not in result.permalink.lower(), ( - "Boolean NOT search returned excluded term" - ) + assert ( + "connected" not in result.permalink.lower() + ), "Boolean NOT search returned excluded term" @pytest.mark.asyncio @@ -366,9 +366,9 @@ async def test_boolean_group_search(search_service, test_graph): "root" in result.title.lower() or "connected" in result.title.lower() ) - assert contains_entity and contains_root_or_connected, ( - "Boolean grouped search returned incorrect results" - ) + assert ( + contains_entity and contains_root_or_connected + ), "Boolean grouped search returned incorrect results" @pytest.mark.asyncio @@ -398,9 +398,9 @@ async def test_boolean_operators_detection(search_service): for query_text in non_boolean_queries: query = SearchQuery(text=query_text) - assert not query.has_boolean_operators(), ( - f"Incorrectly detected boolean operators in: {query_text}" - ) + assert ( + not query.has_boolean_operators() + ), f"Incorrectly detected boolean operators in: {query_text}" # Tests for frontmatter tag search functionality @@ -514,6 +514,72 @@ async def test_extract_entity_tags_no_tags_key(search_service, session_maker): assert tags == [] +@pytest.mark.asyncio +async def test_search_tag_prefix_maps_to_tags_filter(search_service, entity_service): + """`tag:foo` prefix should translate to tags filter and return tagged entities.""" + from basic_memory.schemas import Entity as EntitySchema + + tagged_entity, _ = await entity_service.create_or_update_entity( + EntitySchema( + title="Tagged Note Missing", + directory="tags", + entity_type="note", + content="# Tagged Note", + entity_metadata={"tags": ["tier1", "alpha"]}, + ) + ) + + await search_service.index_entity(tagged_entity) + + results = await search_service.search(SearchQuery(text="tag:tier1")) + + assert any(r.permalink == tagged_entity.permalink for r in results) + + +@pytest.mark.asyncio +async def test_search_tag_prefix_with_nonexistent_tag_returns_empty(search_service, entity_service): + """`tag:missing` should return no results when tags do not match.""" + from basic_memory.schemas import Entity as EntitySchema + + tagged_entity, _ = await entity_service.create_or_update_entity( + EntitySchema( + title="Tagged Note", + directory="tags", + entity_type="note", + content="# Tagged Note", + entity_metadata={"tags": ["tier1", "alpha"]}, + ) + ) + + await search_service.index_entity(tagged_entity) + + results = await search_service.search(SearchQuery(text="tag:missing")) + + assert not results + + +@pytest.mark.asyncio +async def test_search_tag_prefix_multiple_tags_requires_all(search_service, entity_service): + """`tag:tier1,alpha` should match entities containing all listed tags.""" + from basic_memory.schemas import Entity as EntitySchema + + tagged_entity, _ = await entity_service.create_or_update_entity( + EntitySchema( + title="Multi Tagged Note", + directory="tags/multi", + entity_type="note", + content="# Tagged Note", + entity_metadata={"tags": ["tier1", "alpha"]}, + ) + ) + + await search_service.index_entity(tagged_entity) + + results = await search_service.search(SearchQuery(text="tag:tier1,alpha")) + + assert any(r.permalink == tagged_entity.permalink for r in results) + + @pytest.mark.asyncio async def test_search_by_frontmatter_tags(search_service, session_maker, test_project): """Test that entities can be found by searching for their frontmatter tags.""" From 78a03963169a35d6147233402bc8e7ddef9401dc Mon Sep 17 00:00:00 2001 From: phernandez Date: Sat, 31 Jan 2026 21:07:02 -0600 Subject: [PATCH 2/2] fix: support tag search on postgres Signed-off-by: phernandez --- src/basic_memory/repository/postgres_search_repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/basic_memory/repository/postgres_search_repository.py b/src/basic_memory/repository/postgres_search_repository.py index 29a3d04b..cfbc9920 100644 --- a/src/basic_memory/repository/postgres_search_repository.py +++ b/src/basic_memory/repository/postgres_search_repository.py @@ -332,7 +332,7 @@ async def search( like_param_single = f"{base_param}_{j}_like_single" params[like_param_single] = f"%'{val}'%" tag_conditions.append( - f"({json_expr} @> :{tag_param}::jsonb " + f"({json_expr} @> CAST(:{tag_param} AS jsonb) " f"OR {text_expr} LIKE :{like_param} " f"OR {text_expr} LIKE :{like_param_single})" )