Skip to content

Commit f1d50c2

Browse files
authored
feat: Support tag: query shorthand in search (#535)
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 8072449 commit f1d50c2

2 files changed

Lines changed: 86 additions & 9 deletions

File tree

src/basic_memory/services/search_service.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Service for search operations."""
22

33
import ast
4+
import re
45
from datetime import datetime
56
from typing import List, Optional, Set, Dict, Any
67

@@ -79,6 +80,16 @@ async def search(self, query: SearchQuery, limit=10, offset=0) -> List[SearchInd
7980
2. Pattern match: handles * wildcards in paths
8081
3. Text search: full-text search across title/content
8182
"""
83+
# Support tag:<tag> shorthand by mapping to tags filter
84+
if query.text:
85+
text = query.text.strip()
86+
if text.lower().startswith("tag:"):
87+
tag_values = re.split(r"[,\s]+", text[4:].strip())
88+
tags = [t for t in tag_values if t]
89+
if tags:
90+
query.tags = tags
91+
query.text = None
92+
8293
if query.no_criteria():
8394
logger.debug("no criteria passed to query")
8495
return []

tests/services/test_search_service.py

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,9 @@ async def test_boolean_not_search(search_service, test_graph):
345345

346346
# Should find "Root Entity" but not "Connected Entity"
347347
for result in results:
348-
assert "connected" not in result.permalink.lower(), (
349-
"Boolean NOT search returned excluded term"
350-
)
348+
assert (
349+
"connected" not in result.permalink.lower()
350+
), "Boolean NOT search returned excluded term"
351351

352352

353353
@pytest.mark.asyncio
@@ -366,9 +366,9 @@ async def test_boolean_group_search(search_service, test_graph):
366366
"root" in result.title.lower() or "connected" in result.title.lower()
367367
)
368368

369-
assert contains_entity and contains_root_or_connected, (
370-
"Boolean grouped search returned incorrect results"
371-
)
369+
assert (
370+
contains_entity and contains_root_or_connected
371+
), "Boolean grouped search returned incorrect results"
372372

373373

374374
@pytest.mark.asyncio
@@ -398,9 +398,9 @@ async def test_boolean_operators_detection(search_service):
398398

399399
for query_text in non_boolean_queries:
400400
query = SearchQuery(text=query_text)
401-
assert not query.has_boolean_operators(), (
402-
f"Incorrectly detected boolean operators in: {query_text}"
403-
)
401+
assert (
402+
not query.has_boolean_operators()
403+
), f"Incorrectly detected boolean operators in: {query_text}"
404404

405405

406406
# Tests for frontmatter tag search functionality
@@ -514,6 +514,72 @@ async def test_extract_entity_tags_no_tags_key(search_service, session_maker):
514514
assert tags == []
515515

516516

517+
@pytest.mark.asyncio
518+
async def test_search_tag_prefix_maps_to_tags_filter(search_service, entity_service):
519+
"""`tag:foo` prefix should translate to tags filter and return tagged entities."""
520+
from basic_memory.schemas import Entity as EntitySchema
521+
522+
tagged_entity, _ = await entity_service.create_or_update_entity(
523+
EntitySchema(
524+
title="Tagged Note Missing",
525+
directory="tags",
526+
entity_type="note",
527+
content="# Tagged Note",
528+
entity_metadata={"tags": ["tier1", "alpha"]},
529+
)
530+
)
531+
532+
await search_service.index_entity(tagged_entity)
533+
534+
results = await search_service.search(SearchQuery(text="tag:tier1"))
535+
536+
assert any(r.permalink == tagged_entity.permalink for r in results)
537+
538+
539+
@pytest.mark.asyncio
540+
async def test_search_tag_prefix_with_nonexistent_tag_returns_empty(search_service, entity_service):
541+
"""`tag:missing` should return no results when tags do not match."""
542+
from basic_memory.schemas import Entity as EntitySchema
543+
544+
tagged_entity, _ = await entity_service.create_or_update_entity(
545+
EntitySchema(
546+
title="Tagged Note",
547+
directory="tags",
548+
entity_type="note",
549+
content="# Tagged Note",
550+
entity_metadata={"tags": ["tier1", "alpha"]},
551+
)
552+
)
553+
554+
await search_service.index_entity(tagged_entity)
555+
556+
results = await search_service.search(SearchQuery(text="tag:missing"))
557+
558+
assert not results
559+
560+
561+
@pytest.mark.asyncio
562+
async def test_search_tag_prefix_multiple_tags_requires_all(search_service, entity_service):
563+
"""`tag:tier1,alpha` should match entities containing all listed tags."""
564+
from basic_memory.schemas import Entity as EntitySchema
565+
566+
tagged_entity, _ = await entity_service.create_or_update_entity(
567+
EntitySchema(
568+
title="Multi Tagged Note",
569+
directory="tags/multi",
570+
entity_type="note",
571+
content="# Tagged Note",
572+
entity_metadata={"tags": ["tier1", "alpha"]},
573+
)
574+
)
575+
576+
await search_service.index_entity(tagged_entity)
577+
578+
results = await search_service.search(SearchQuery(text="tag:tier1,alpha"))
579+
580+
assert any(r.permalink == tagged_entity.permalink for r in results)
581+
582+
517583
@pytest.mark.asyncio
518584
async def test_search_by_frontmatter_tags(search_service, session_maker, test_project):
519585
"""Test that entities can be found by searching for their frontmatter tags."""

0 commit comments

Comments
 (0)