diff --git a/tests/services/test_search_service.py b/tests/services/test_search_service.py index 4e9fd95e..b546353e 100644 --- a/tests/services/test_search_service.py +++ b/tests/services/test_search_service.py @@ -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 \ No newline at end of file + assert len(results) == 1 \ No newline at end of file