fix search tests wip

This commit is contained in:
phernandez
2025-01-12 22:29:51 -06:00
parent 44221e3f78
commit e49f3ab3f4
+58 -222
View File
@@ -1,4 +1,4 @@
"""Tests for search service fuzzy matching."""
"""Tests for search service."""
from datetime import datetime, timezone
@@ -71,158 +71,90 @@ async def indexed_search(search_service, test_entities):
@pytest.mark.asyncio
async def test_exact_title_match(indexed_search):
"""Test exact title matching."""
results = await indexed_search.search(SearchQuery(text="Core Service"))
async def test_basic_text_search(indexed_search):
"""Test basic search functionality works as expected."""
# Test exact word match
results = await indexed_search.search(SearchQuery(text="API"))
assert len(results) == 1
assert results[0].file_path == "components/core-service.md"
assert results[0].file_path == "docs/api/documentation.md"
# Test prefix match (serv should match service)
results = await indexed_search.search(SearchQuery(text="Serv"))
assert len(results) > 0
assert any(r.file_path == "components/core-service.md" for r in results)
@pytest.mark.asyncio
async def test_fuzzy_title_match_misspelling(indexed_search):
"""Test fuzzy matching with misspellings."""
async def test_case_insensitive_search(indexed_search):
"""Test that search is case insensitive."""
test_cases = [
("Core Servise", "components/core-service.md"), # Common misspelling
("Auth Servise", "components/auth/service.md"), # Another misspelling
("Core Servis", "components/core-service.md"), # Partial word
("Srvc Config", "config/service-config.md"), # Vowel omission
"core",
"CORE",
"Core",
]
for search_text, expected_path in test_cases:
for search_text in test_cases:
results = await indexed_search.search(SearchQuery(text=search_text))
assert len(results) > 0, f"No results found for '{search_text}'"
assert results[0].file_path == expected_path, f"Wrong match for '{search_text}'"
assert len(results) == 2, f"Failed for '{search_text}'"
file_paths = [r.file_path for r in results]
assert "components/core-service.md" in file_paths
assert "specs/features/core.md" in file_paths
@pytest.mark.asyncio
async def test_partial_word_matching(indexed_search):
"""Test matching with partial words."""
async def test_whitespace_handling(indexed_search):
"""Test that whitespace is handled correctly."""
test_cases = [
("Auth Serv", "components/auth/service.md"), # Partial service name
("Core Feat", "specs/features/core.md"), # Partial feature word
("API Doc", "docs/api/documentation.md"), # Abbreviated terms
" API ", # Extra spaces
"API Documentation", # Normal spacing
"API Documentation", # Multiple spaces
]
for search_text, expected_path in test_cases:
for search_text in test_cases:
results = await indexed_search.search(SearchQuery(text=search_text))
assert len(results) > 0, f"No results found for '{search_text}'"
assert results[0].file_path == expected_path, f"Wrong match for '{search_text}'"
assert len(results) == 1, f"Failed for '{search_text}'"
assert results[0].file_path == "docs/api/documentation.md"
@pytest.mark.asyncio
async def test_path_aware_matching(indexed_search):
"""Test path-aware matching preferences."""
test_cases = [
# Search text, context path, expected result
("Service", "components/other-service.md", "components/"), # Should prefer component directory
("Core", "specs/features/other.md", "specs/features/"), # Should prefer specs directory
("Service", None, "components/core-service.md"), # No context, should pick highest scored
]
for search_text, context, expected_prefix in test_cases:
results = await indexed_search.search(
SearchQuery(text=search_text),
context=[context] if context else None
)
assert len(results) > 0, f"No results found for '{search_text}'"
assert results[0].file_path.startswith(expected_prefix), \
f"Wrong directory preference for '{search_text}' with context '{context}'"
async def test_content_search(indexed_search):
"""Test searching in content/summary field."""
# Test matching against summary text
results = await indexed_search.search(SearchQuery(text="implementation"))
assert len(results) == 2
file_paths = [r.file_path for r in results]
assert "components/core-service.md" in file_paths
assert "components/auth/service.md" in file_paths
@pytest.mark.asyncio
async def test_multi_word_fuzzy_matching(indexed_search):
"""Test fuzzy matching with multiple words."""
test_cases = [
("Core Srvc", "components/core-service.md"), # Multiple partial words
("Auth Srvice", "components/auth/service.md"), # One full, one partial
("Cor Serv", "components/core-service.md"), # Both partial
]
for search_text, expected_path in test_cases:
results = await indexed_search.search(SearchQuery(text=search_text))
assert len(results) > 0, f"No results found for '{search_text}'"
assert results[0].file_path == expected_path, f"Wrong match for '{search_text}'"
@pytest.mark.asyncio
async def test_word_order_invariance(indexed_search):
"""Test that word order doesn't affect matching."""
test_cases = [
("Service Core", "components/core-service.md"), # Reversed order
("Config Service", "config/service-config.md"), # Reversed order
("Service Auth", "components/auth/service.md"), # Reversed order
]
for search_text, expected_path in test_cases:
results = await indexed_search.search(SearchQuery(text=search_text))
assert len(results) > 0, f"No results found for '{search_text}'"
assert results[0].file_path == expected_path, f"Wrong match for '{search_text}'"
@pytest.mark.asyncio
async def test_relevance_scoring(indexed_search):
"""Test that relevance scoring works correctly."""
# Search for "service" which should match multiple items
results = await indexed_search.search(SearchQuery(text="service"))
# Extract paths for easier assertion
paths = [r.file_path for r in results]
# Verify core service comes before service config (should have better score)
core_idx = paths.index("components/core-service.md")
config_idx = paths.index("config/service-config.md")
assert core_idx < config_idx, "Core service should rank higher than service config"
@pytest.mark.asyncio
async def test_combined_search_criteria(indexed_search):
"""Test combining fuzzy search with other search criteria."""
async def test_search_filters(indexed_search):
"""Test search filtering."""
# Search with correct type filter
results = await indexed_search.search(
SearchQuery(
text="Core Serv", # Fuzzy terms
text="service",
types=[SearchItemType.ENTITY],
entity_types=["component"]
)
)
assert len(results) == 1
assert results[0].file_path == "components/core-service.md"
@pytest.mark.asyncio
async def test_index_variants_generation(search_service, entity_service):
"""Test that index variants are generated correctly."""
# Create an entity with specific characteristics to test variant generation
entity = await entity_service.create_entity(
EntitySchema(
title="Test-Component Service",
entity_type="component",
summary="A test component",
file_path="components/test-component/service.md",
# Search with non-matching type (should return empty)
results = await indexed_search.search(
SearchQuery(
text="service",
types=[SearchItemType.DOCUMENT]
)
)
await search_service.index_entity(entity)
# Test various forms of the same content
test_cases = [
"test component", # Lowercase without hyphen
"Test-Component", # Original form
"test-comp", # Partial with hyphen
"TestComp", # CamelCase partial
"component/serv", # Path segment with partial
]
for search_text in test_cases:
results = await search_service.search(SearchQuery(text=search_text))
assert len(results) > 0, f"No results found for '{search_text}'"
assert results[0].file_path == "components/test-component/service.md"
assert len(results) == 0
# Keep the original basic test fixtures
# Basic operation tests
@pytest_asyncio.fixture
def test_entity():
"""Create a test entity"""
"""Create a basic test entity."""
class Entity:
id = 1
title = "TestComponent"
@@ -239,23 +171,9 @@ def test_entity():
return Entity()
@pytest.fixture
def test_document():
"""Create a test document"""
class Document:
id = 1
permalink = "docs/test_doc.md"
file_path = "docs/test_doc.md"
doc_metadata = {"title": "Test Document", "type": "technical"}
created_at = datetime.now(timezone.utc)
updated_at = datetime.now(timezone.utc)
return Document()
# Keep the original basic tests
@pytest.mark.asyncio
async def test_init_search_index(search_service, session_maker):
"""Test search index initialization"""
"""Test search index initialization."""
async with db.scoped_session(session_maker) as session:
result = await session.execute(
text("SELECT name FROM sqlite_master WHERE type='table' AND name='search_index';")
@@ -263,97 +181,15 @@ async def test_init_search_index(search_service, session_maker):
assert result.scalar() == "search_index"
@pytest.mark.asyncio
async def test_index_entity(search_service, test_entity):
"""Test indexing an entity"""
await search_service.index_entity(test_entity)
results = await search_service.search(SearchQuery(text="test component"))
assert len(results) == 1
assert results[0].permalink == test_entity.permalink
assert results[0].type == SearchItemType.ENTITY
@pytest.mark.asyncio
async def test_search_filtering(search_service, test_entity):
"""Test search with filters"""
await search_service.index_entity(test_entity)
results = await search_service.search(
SearchQuery(text="test", types=[SearchItemType.ENTITY], entity_types=["knowledge"])
)
assert len(results) == 1
results = await search_service.search(SearchQuery(text="test", types=[SearchItemType.DOCUMENT]))
assert len(results) == 0
@pytest.mark.asyncio
async def test_update_index(search_service, test_entity):
"""Test updating indexed content"""
"""Test updating indexed content."""
await search_service.index_entity(test_entity)
# Update entity
test_entity.summary = "Updated description with new terms"
await search_service.index_entity(test_entity)
# Search for new terms
results = await search_service.search(SearchQuery(text="new terms"))
assert len(results) == 1
@pytest.mark.asyncio
async def test_search_date_filter(search_service, test_entity):
"""Test searching with date filter"""
await search_service.index_entity(test_entity)
future = datetime.now(timezone.utc).replace(year=2026)
results = await search_service.search(SearchQuery(text="test", after_date=future))
assert len(results) == 0
@pytest.mark.asyncio
async def test_reindex_all(search_service, entity_service, session_maker):
"""Test reindexing all content."""
test_entity = await entity_service.create_entity(
EntitySchema(
title="TestComponent",
entity_type="test",
summary="A test entity description",
observations=["this is a test observation"],
),
)
async with db.scoped_session(session_maker) as session:
await session.execute(text("DELETE FROM search_index"))
await session.commit()
results = await search_service.search(SearchQuery(text="test"))
assert len(results) == 0
await search_service.reindex_all()
entity_results = await search_service.search(
SearchQuery(text="TestComponent", types=[SearchItemType.ENTITY])
)
assert len(entity_results) == 1
assert entity_results[0].permalink == test_entity.permalink
assert entity_results[0].type == SearchItemType.ENTITY
@pytest.mark.asyncio
async def test_reindex_with_background_tasks(search_service, entity_service, session_maker):
"""Test reindexing with background tasks."""
from fastapi import BackgroundTasks
await entity_service.create_entity(
EntitySchema(
title="TestEntity1",
entity_type="test",
summary="A test entity description",
observations=["this is a test observation"],
),
)
async with db.scoped_session(session_maker) as session:
await session.execute(text("DELETE FROM search_index"))
await session.commit()
background_tasks = BackgroundTasks()
await search_service.reindex_all(background_tasks=background_tasks)
await background_tasks()
all_results = await search_service.search(SearchQuery(text="test"))
assert len(all_results) == 1
assert len(results) == 1