Skip to content
Open
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
11 changes: 11 additions & 0 deletions src/basic_memory/services/search_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Service for search operations."""

import ast
import re
from datetime import datetime
from typing import List, Optional, Set, Dict, Any

Expand Down Expand Up @@ -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:<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 []
Expand Down
84 changes: 75 additions & 9 deletions tests/services/test_search_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
Loading