Skip to content
Merged
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
10 changes: 7 additions & 3 deletions REQUIREMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **Note:** This document is automatically generated and verified against the live test suite by `scripts/generate_requirements.py` and `tests/backend/test_requirements_sync.py`.

**Test Verification Baseline:** **1052 Automated Tests** (699 Pytest Backend + 303 Vitest Frontend + 50 Playwright E2E).
**Test Verification Baseline:** **1056 Automated Tests** (703 Pytest Backend + 303 Vitest Frontend + 50 Playwright E2E).

---

Expand Down Expand Up @@ -738,7 +738,7 @@ Asserts both Markdown and PDF docs are matched under doc_type='doc'._
- `test_vector_document_validation` - _Verify required field validation in VectorDocument._
- `test_vector_search_result_creation_and_payload` - _Verify VectorSearchResult creation and payload access._

#### `tests/backend/test_vector_store_chroma.py` (34 tests)
#### `tests/backend/test_vector_store_chroma.py` (36 tests)
- `TestChromaVectorStoreInit::test_init_in_memory`
- `TestChromaVectorStoreInit::test_init_persistent_disk`
- `TestChromaVectorStoreInit::test_init_remote_success`
Expand All @@ -755,6 +755,7 @@ Asserts both Markdown and PDF docs are matched under doc_type='doc'._
- `TestChromaVectorStoreOperations::test_search_metadata_filtering`
- `TestChromaVectorStoreOperations::test_delete_by_path`
- `TestChromaVectorStoreOperations::test_delete_by_repo`
- `TestChromaVectorStoreOperations::test_get_capabilities_and_search_mode_validation`
- `TestChromaVectorStoreOperations::test_get_stats_and_health_check`
- `test_init_in_memory`
- `test_init_persistent_disk`
Expand All @@ -772,6 +773,7 @@ Asserts both Markdown and PDF docs are matched under doc_type='doc'._
- `test_search_metadata_filtering`
- `test_delete_by_path`
- `test_delete_by_repo`
- `test_get_capabilities_and_search_mode_validation`
- `test_get_stats_and_health_check`

#### `tests/backend/test_vector_store_manager.py` (38 tests)
Expand Down Expand Up @@ -1112,7 +1114,7 @@ and leaves the prior indexed state intact without data loss._
- `test_pdf_read_file_content`
- `test_real_asd_ste100_pdf_preview_or_indexing`

#### `tests/test_pgvector_store.py` (54 tests)
#### `tests/test_pgvector_store.py` (56 tests)
- `TestPgVectorStoreImports::test_import_pgvector_store`
- `TestPgVectorStoreImports::test_manager_supported_providers_includes_postgres_and_pgvector`
- `TestPgVectorStoreLifecycle::test_initialization_defaults`
Expand All @@ -1121,6 +1123,7 @@ and leaves the prior indexed state intact without data loss._
- `TestPgVectorStoreLifecycle::test_ensure_collection_failure`
- `TestPgVectorStoreLifecycle::test_health_check_success`
- `TestPgVectorStoreLifecycle::test_health_check_failure`
- `TestPgVectorStoreLifecycle::test_get_capabilities_and_search_mode_validation`
- `TestPgVectorStoreLifecycle::test_get_stats_success`
- `TestPgVectorStoreLifecycle::test_get_stats_failure`
- `TestPgVectorStoreLifecycle::test_close_method`
Expand Down Expand Up @@ -1148,6 +1151,7 @@ and leaves the prior indexed state intact without data loss._
- `test_ensure_collection_failure`
- `test_health_check_success`
- `test_health_check_failure`
- `test_get_capabilities_and_search_mode_validation`
- `test_get_stats_success`
- `test_get_stats_failure`
- `test_close_method`
Expand Down
2 changes: 1 addition & 1 deletion app/api/routers/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ async def api_test_search(payload: SearchRequest):
if not query:
return JSONResponse(status_code=400, content={"error": "Query required"})

search_mode = payload.search_mode or "hybrid"
search_mode = (payload.search_mode or "hybrid").lower().strip()
hits = search_service.execute_hybrid_search(
query_text=query,
doc_type=payload.type,
Expand Down
8 changes: 6 additions & 2 deletions app/mcp/handlers/search_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@ async def handle_search_code(
try:
from app.services.auth import enforce_tool_permission, Role
enforce_tool_permission(Role.VIEWER)

requested_mode = (mode or "hybrid").lower().strip()
hits = _get_tools_attr("execute_hybrid_search", execute_hybrid_search)(
query_text=query,
doc_type="code",
repo=repo,
language=language,
limit=limit,
dense_weight=dense_weight,
search_mode=mode or "hybrid"
search_mode=requested_mode
)
if not hits:
return f"No matching code snippets found for query: '{query}'."
Expand Down Expand Up @@ -101,6 +103,8 @@ async def handle_search_docs(
try:
from app.services.auth import enforce_tool_permission, Role
enforce_tool_permission(Role.VIEWER)

requested_mode = (mode or "hybrid").lower().strip()
hits = _get_tools_attr("execute_hybrid_search", execute_hybrid_search)(
query_text=query,
doc_type="doc",
Expand All @@ -109,7 +113,7 @@ async def handle_search_docs(
tag=tag,
limit=limit,
dense_weight=dense_weight,
search_mode=mode or "hybrid"
search_mode=requested_mode
)
if not hits:
return f"No matching documentation found for query: '{query}'."
Expand Down
17 changes: 17 additions & 0 deletions app/services/database/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,29 @@ def _set_sqlite_pragmas(dbapi_connection, connection_record):
)

if database_url is None or database_url == os.getenv("DATABASE_URL"):
if _GLOBAL_ENGINE is not None and _GLOBAL_ENGINE_URL != url:
try:
_GLOBAL_ENGINE.dispose()
except Exception:
pass
_GLOBAL_ENGINE = engine
_GLOBAL_ENGINE_URL = url

return engine


def dispose_global_engine() -> None:
"""Disposes cached global database engine and clears references."""
global _GLOBAL_ENGINE, _GLOBAL_ENGINE_URL
if _GLOBAL_ENGINE is not None:
try:
_GLOBAL_ENGINE.dispose()
except Exception:
pass
_GLOBAL_ENGINE = None
_GLOBAL_ENGINE_URL = None


@contextmanager
def get_connection(engine: Optional[Engine] = None):
"""
Expand Down
20 changes: 19 additions & 1 deletion app/services/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,24 @@ def execute_hybrid_search(

try:
store = get_vector_store()
try:
caps = store.get_capabilities()
if isinstance(caps, dict):
supported = caps.get("search_modes", ["hybrid", "semantic", "lexical"])
else:
supported = ["hybrid", "semantic", "lexical"]
except Exception:
supported = ["hybrid", "semantic", "lexical"]

mode = (search_mode or "hybrid").lower().strip()
if mode not in supported:
if mode == "hybrid" and "semantic" in supported:
mode = "semantic"
else:
raise ValueError(
f"Search mode '{search_mode}' is not supported by the active vector store backend ({store.__class__.__name__}). Supported modes: {supported}"
)

results = store.search(
query_text=query_text.strip(),
doc_type=doc_type,
Expand All @@ -331,7 +349,7 @@ def execute_hybrid_search(
tag=tag,
limit=limit,
dense_weight=dense_weight,
search_mode=search_mode
search_mode=mode
)
_enrich_code_ast_metadata(results)
return results
Expand Down
7 changes: 7 additions & 0 deletions app/services/vector_store/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ def search(
"""Performs vector search returning ranked results."""
pass

def get_capabilities(self) -> Dict[str, Any]:
"""Returns capabilities supported by this vector store backend."""
return {
"search_modes": ["semantic"],
"supports_weighting": False,
}

@abstractmethod
def get_stats(self) -> Dict[str, Any]:
"""Returns collection info including point count, backend, and mode."""
Expand Down
21 changes: 20 additions & 1 deletion app/services/vector_store/chroma_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,12 +254,21 @@ def search(
tag: Optional[str] = None,
limit: int = 5,
dense_weight: Optional[float] = None,
search_mode: str = "hybrid"
search_mode: str = "semantic"
) -> List[VectorSearchResult]:
"""Performs dense vector search returning ranked results."""
if not query_text or not query_text.strip():
return []

mode = (search_mode or "semantic").lower().strip()
caps = self.get_capabilities()
supported = caps.get("search_modes", ["semantic"])
if mode not in supported:
raise ValueError(
f"Search mode '{search_mode}' is not supported by Chroma vector store. "
f"Supported modes: {supported}"
)

try:
if self.collection is None:
if not self.ensure_collection():
Expand Down Expand Up @@ -328,6 +337,13 @@ def search(
logger.error(f"Error searching Chroma collection '{self.collection_name}': {e}")
return []

def get_capabilities(self) -> Dict[str, Any]:
"""Returns capabilities supported by Chroma vector store backend."""
return {
"search_modes": ["semantic"],
"supports_weighting": False,
}

def get_stats(self) -> Dict[str, Any]:
"""Returns collection info including point count, backend, and mode."""
try:
Expand All @@ -340,6 +356,7 @@ def get_stats(self) -> Dict[str, Any]:
"points_count": 0,
"status": "not_initialized",
"location": self.location,
"capabilities": self.get_capabilities(),
}
count = self.collection.count()
return {
Expand All @@ -350,6 +367,7 @@ def get_stats(self) -> Dict[str, Any]:
"points_count": count,
"status": "ok",
"location": self.location,
"capabilities": self.get_capabilities(),
}
except Exception as e:
logger.error(f"Error getting stats for Chroma collection '{self.collection_name}': {e}")
Expand All @@ -360,6 +378,7 @@ def get_stats(self) -> Dict[str, Any]:
"exists": False,
"error": str(e),
"location": self.location,
"capabilities": self.get_capabilities(),
}

def health_check(self) -> Tuple[bool, str]:
Expand Down
3 changes: 3 additions & 0 deletions app/services/vector_store/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,13 @@ def get_vector_store_config(cls) -> Dict[str, Any]:
store = cls.get_vector_store()
healthy, health_msg = store.health_check()
stats = store.get_stats()
capabilities = store.get_capabilities()
except Exception as e:
logger.error(f"Error connecting to vector store during config load: {e}")
healthy = False
health_msg = "Error connecting to vector store."
stats = {"error": "Error retrieving vector store statistics."}
capabilities = {"search_modes": ["semantic"], "supports_weighting": False}

return {
"provider": cfg["provider"],
Expand All @@ -155,6 +157,7 @@ def get_vector_store_config(cls) -> Dict[str, Any]:
"healthy": healthy,
"health_message": health_msg,
"stats": stats,
"capabilities": capabilities,
}

@classmethod
Expand Down
20 changes: 19 additions & 1 deletion app/services/vector_store/pgvector_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,14 +273,23 @@ def search(
tag: Optional[str] = None,
limit: int = 5,
dense_weight: Optional[float] = None,
search_mode: str = "hybrid",
search_mode: str = "semantic",
) -> List[VectorSearchResult]:
"""
Performs cosine distance vector search returning ranked results.
"""
if not query_text or not query_text.strip():
return []

mode = (search_mode or "semantic").lower().strip()
caps = self.get_capabilities()
supported = caps.get("search_modes", ["semantic"])
if mode not in supported:
raise ValueError(
f"Search mode '{search_mode}' is not supported by PgVector vector store. "
f"Supported modes: {supported}"
)

try:
query_vec = get_dense_embedding(query_text.strip())
query_vec_str = str(query_vec)
Expand Down Expand Up @@ -352,6 +361,13 @@ def search(
logger.error(f"Error searching PgVectorStore table '{self.table_name}': {e}", exc_info=True)
return []

def get_capabilities(self) -> Dict[str, Any]:
"""Returns capabilities supported by PgVector vector store backend."""
return {
"search_modes": ["semantic"],
"supports_weighting": False,
}

def get_stats(self) -> Dict[str, Any]:
"""Returns table statistics including row count and backend mode."""
try:
Expand All @@ -366,6 +382,7 @@ def get_stats(self) -> Dict[str, Any]:
"points_count": int(count),
"vectors_count": int(count),
"location": self.location,
"capabilities": self.get_capabilities(),
}
except Exception as e:
logger.error(f"Error getting stats from PgVectorStore: {e}")
Expand All @@ -377,6 +394,7 @@ def get_stats(self) -> Dict[str, Any]:
"exists": False,
"error": str(e),
"location": self.location,
"capabilities": self.get_capabilities(),
}

def health_check(self) -> Tuple[bool, str]:
Expand Down
14 changes: 13 additions & 1 deletion app/services/vector_store/qdrant_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,9 @@ def search(
query_filter = qmodels.Filter(must=must_conditions) if must_conditions else None

mode = (search_mode or "hybrid").lower().strip()
if mode not in ("hybrid", "semantic", "lexical"):
caps = self.get_capabilities()
supported = caps.get("search_modes", ["hybrid", "semantic", "lexical"])
if mode not in supported:
mode = "hybrid"

if mode == "semantic":
Expand Down Expand Up @@ -486,6 +488,13 @@ def search(
logger.error(f"Error searching Qdrant collection '{self.collection_name}': {e}")
return []

def get_capabilities(self) -> Dict[str, Any]:
"""Returns capabilities supported by Qdrant vector store backend."""
return {
"search_modes": ["hybrid", "semantic", "lexical"],
"supports_weighting": True,
}

def get_stats(self) -> Dict[str, Any]:
"""Returns collection info including point count, backend, and mode."""
try:
Expand All @@ -498,6 +507,7 @@ def get_stats(self) -> Dict[str, Any]:
"points_count": 0,
"status": "not_found",
"location": self.location,
"capabilities": self.get_capabilities(),
}
info = self.client.get_collection(self.collection_name)
points_count = info.points_count or 0
Expand All @@ -511,6 +521,7 @@ def get_stats(self) -> Dict[str, Any]:
"vectors_count": vectors_count,
"status": str(info.status),
"location": self.location,
"capabilities": self.get_capabilities(),
}
except Exception as e:
logger.error(f"Error getting stats for Qdrant collection '{self.collection_name}': {e}")
Expand All @@ -521,6 +532,7 @@ def get_stats(self) -> Dict[str, Any]:
"exists": False,
"error": str(e),
"location": self.location,
"capabilities": self.get_capabilities(),
}

def health_check(self) -> Tuple[bool, str]:
Expand Down
Loading
Loading