fix context_service tests

This commit is contained in:
phernandez
2025-01-16 22:38:19 -06:00
parent 3a8a900032
commit e2a4fa63e5
8 changed files with 208 additions and 237 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
updated_at UNINDEXED, -- Last update
-- Configuration
tokenize=\"unicode61 separators '/'\", -- Treat / as part of tokens
tokenize='unicode61 tokenchars 0x2F', -- Hex code for /
prefix='1,2,3,4' -- Support longer prefixes for paths
);
""")
@@ -74,8 +74,10 @@ class SearchRepository:
await session.commit()
def _quote_search_term(self, term: str) -> str:
"""Add quotes if term contains special characters."""
if any(c in term for c in "/-"):
"""Add quotes if term contains special characters or /.
For FTS5, phrases with / need to be quoted to be treated as a single token.
"""
if '/' in term or '*' in term or any(c in term for c in "-"):
return f'"{term}"'
return term
@@ -83,6 +85,7 @@ class SearchRepository:
self,
search_text: Optional[str] = None,
permalink: Optional[str] = None,
permalink_match: Optional[str] = None,
types: List[SearchItemType] = None,
after_date: datetime = None,
entity_types: List[str] = None,
@@ -97,9 +100,14 @@ class SearchRepository:
params["text"] = f"{search_text}*"
conditions.append("(title MATCH :text OR content MATCH :text)")
# Handle permalink search
# Handle permalink exact search
if permalink:
params["permalink"] = permalink
conditions.append("permalink = :permalink")
# Handle permalink match search, supports *
if permalink_match:
params["permalink"] = self._quote_search_term(permalink_match)
conditions.append("permalink MATCH :permalink")
# Handle type filter
+29 -12
View File
@@ -14,6 +14,7 @@ from pydantic import BaseModel, field_validator
class SearchItemType(str, Enum):
"""Types of searchable items."""
ENTITY = "entity"
OBSERVATION = "observation"
RELATION = "relation"
@@ -21,21 +22,23 @@ class SearchItemType(str, Enum):
class SearchQuery(BaseModel):
"""Search query parameters.
Use ONE of these primary search modes:
- permalink: Exact permalink match
- permalink_pattern: Path pattern with *
- text: Full-text search of title/content
Optionally filter results by:
- types: Limit to specific item types
- entity_types: Limit to specific entity types
- after_date: Only items after date
"""
# Primary search modes (use ONE of these)
permalink: Optional[str] = None # Exact permalink match
permalink_match: Optional[str] = None # Exact permalink match
text: Optional[str] = None # Full-text search
# Optional filters
types: Optional[List[SearchItemType]] = None # Filter by item type
entity_types: Optional[List[str]] = None # Filter by entity type
@@ -51,31 +54,43 @@ class SearchQuery(BaseModel):
return v.isoformat()
return v
def no_criteria(self) -> bool:
return (
self.permalink is None
and self.permalink_match is None
and self.text is None
and self.after_date is None
and self.types is None
and self.entity_types is None
)
class SearchResult(BaseModel):
"""Search result with score and metadata."""
id: int
id: int
type: SearchItemType
score: Optional[float] = None
metadata: Optional[dict] = None
# Common fields
permalink: Optional[str] = None
file_path: Optional[str] = None
# Type-specific fields
# Type-specific fields
entity_id: Optional[int] = None # For observations
category: Optional[str] = None # For observations
from_id: Optional[int] = None # For relations
to_id: Optional[int] = None # For relations
category: Optional[str] = None # For observations
from_id: Optional[int] = None # For relations
to_id: Optional[int] = None # For relations
relation_type: Optional[str] = None # For relations
class RelatedResult(BaseModel):
type: SearchItemType
id: int
title: str
permalink: str
depth:int
depth: int
root_id: int
created_at: datetime
from_id: Optional[int] = None
@@ -88,13 +103,15 @@ class RelatedResult(BaseModel):
class SearchResponse(BaseModel):
"""Wrapper for search results."""
results: List[SearchResult]
# Schema for future advanced search endpoint
class AdvancedSearchQuery(BaseModel):
"""Advanced full-text search with explicit FTS5 syntax."""
query: str # Raw FTS5 query (e.g., "foo AND bar")
types: Optional[List[SearchItemType]] = None
entity_types: Optional[List[str]] = None
after_date: Optional[Union[datetime, str]] = None
after_date: Optional[Union[datetime, str]] = None
+16 -15
View File
@@ -8,7 +8,7 @@ from loguru import logger
from sqlalchemy import text
from basic_memory.repository.entity_repository import EntityRepository
from basic_memory.repository.search_repository import SearchRepository
from basic_memory.repository.search_repository import SearchRepository, SearchIndexRow
from basic_memory.schemas.memory import MemoryUrl
from basic_memory.schemas.search import SearchQuery, SearchItemType
@@ -64,19 +64,24 @@ class ContextService:
# Special mode for finding related content
target = memory_url.params["target"]
logger.debug(f"Finding related content for '{target}'")
# start by looking at direct relations
primary = await self.find_related_1(target)
# Pattern matching - use search
elif '*' in memory_url.relative_path():
logger.debug(f"Pattern search for '{memory_url.relative_path()}'")
primary = await self.search_repository.search(permalink_match=memory_url.relative_path())
# Direct lookup for exact path
else:
# Direct permalink lookup
logger.debug(f"Direct permalink lookup for '{memory_url.relative_path()}'")
primary = await self.find_by_permalink(memory_url.relative_path())
logger.debug(f"Found {len(primary)} primary entities")
for p in primary:
logger.debug(f"Found primary entity: {p}")
logger.debug(f"Direct lookup for '{memory_url.relative_path()}'")
primary = await self.search_repository.search(permalink=memory_url.relative_path())
# Get type_id pairs for traversal
type_id_pairs = [(r.type, r.id) for r in primary] if primary else []
logger.debug(f"type_id_pairs: {type_id_pairs}")
logger.debug(f"primary type_id_pairs: {type_id_pairs}")
# Find connected content
related = await self.find_connected(type_id_pairs, max_depth=depth, since=since)
@@ -99,14 +104,10 @@ class ContextService:
},
}
async def find_by_permalink(self, permalink: str):
"""Find an entity by exact permalink."""
return await self.search_repository.search(permalink=permalink)
async def find_related_1(self, permalink: str):
"""Find entities related to a given permalink."""
# First find the target entity
target = await self.find_by_permalink(permalink)
target = await self.search_repository.search(permalink=permalink)
if not target:
return []
+15 -16
View File
@@ -57,22 +57,21 @@ class SearchService:
2. Pattern match: handles * wildcards in paths
3. Text search: full-text search across title/content
"""
if query.no_criteria():
logger.debug("no criteria passed to query")
return []
logger.debug(f"Searching with query: {query}")
# Determine search mode based on provided parameters
if query.permalink:
# Exact permalink lookup
results = await self.repository.search(permalink=query.permalink)
# elif query.permalink_pattern:
# # Pattern matching with *
# results = await self.repository.search(
# SearchQuery(permalink_pattern=query.permalink_pattern)
# )
elif query.text:
# Full-text search
results = await self.repository.search(search_text=query.text)
else:
return [] # No search criteria provide
# permalink search
results = await self.repository.search(
search_text=query.text,
permalink=query.permalink,
permalink_match=query.permalink_match,
types=query.types,
entity_types=query.entity_types,
after_date=query.after_date,
)
return results
@@ -281,8 +280,8 @@ class SearchService:
relation_type=relation_type,
entity_id=entity_id,
category=category,
created_at= metadata.get("created_at"),
updated_at= metadata.get("updated_at"),
created_at=metadata.get("created_at"),
updated_at=metadata.get("updated_at"),
)
)