Coverage for src / qdrant_loader_mcp_server / search / hybrid / engine.py: 91%
46 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-10 09:41 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-10 09:41 +0000
1"""
2Hybrid Search Engine Implementation.
4This module implements the main HybridSearchEngine class that orchestrates
5vector search, keyword search, intent classification, knowledge graph integration,
6cross-document intelligence, and faceted search capabilities.
7"""
9from __future__ import annotations
11from typing import Any
13from ...config import SearchConfig
14from ...utils.logging import LoggingConfig
15from .api import HybridEngineAPI
16from .models import (
17 DEFAULT_KEYWORD_WEIGHT,
18 DEFAULT_METADATA_WEIGHT,
19 DEFAULT_MIN_SCORE,
20 DEFAULT_VECTOR_WEIGHT,
21 HybridProcessingConfig,
22)
24logger = LoggingConfig.get_logger(__name__)
27class HybridSearchEngine(HybridEngineAPI):
28 """Refactored hybrid search service using modular components."""
30 def __init__(
31 self,
32 qdrant_client: Any,
33 openai_client: Any,
34 collection_name: str,
35 vector_weight: float = DEFAULT_VECTOR_WEIGHT,
36 keyword_weight: float = DEFAULT_KEYWORD_WEIGHT,
37 metadata_weight: float = DEFAULT_METADATA_WEIGHT,
38 min_score: float = DEFAULT_MIN_SCORE,
39 # Enhanced search parameters
40 knowledge_graph: Any = None,
41 enable_intent_adaptation: bool = True,
42 search_config: SearchConfig | None = None,
43 processing_config: HybridProcessingConfig | None = None,
44 embedding_model: str = "text-embedding-3-small",
45 ):
46 """Initialize the hybrid search service.
48 Args:
49 qdrant_client: Qdrant client instance
50 openai_client: OpenAI client instance
51 collection_name: Name of the Qdrant collection
52 vector_weight: Weight for vector search scores (0-1)
53 keyword_weight: Weight for keyword search scores (0-1)
54 metadata_weight: Weight for metadata-based scoring (0-1)
55 min_score: Minimum combined score threshold
56 knowledge_graph: Optional knowledge graph for integration
57 enable_intent_adaptation: Enable intent-aware adaptive search
58 search_config: Optional search configuration for performance optimization
59 processing_config: Optional processing configuration controlling hybrid processing behaviors
60 """
61 self.qdrant_client = qdrant_client
62 # Use a property-backed attribute so test fixtures can inject a client after init
63 self._openai_client = None
64 # Assign via setter to allow future propagation when pipeline is ready
65 self.openai_client = openai_client
66 self.collection_name = collection_name
67 self.vector_weight = vector_weight
68 self.keyword_weight = keyword_weight
69 self.metadata_weight = metadata_weight
70 self.min_score = min_score
71 self.embedding_model = embedding_model
72 self.logger = LoggingConfig.get_logger(__name__)
74 # Centralized initialization via builder
75 from .components.builder import initialize_engine_components
76 from .models import HybridProcessingConfig as _HPC
78 effective_processing_config = processing_config or _HPC()
79 initialize_engine_components(
80 self,
81 qdrant_client=qdrant_client,
82 openai_client=openai_client,
83 collection_name=collection_name,
84 vector_weight=vector_weight,
85 keyword_weight=keyword_weight,
86 metadata_weight=metadata_weight,
87 min_score=min_score,
88 knowledge_graph=knowledge_graph,
89 enable_intent_adaptation=enable_intent_adaptation,
90 search_config=search_config,
91 processing_config=effective_processing_config,
92 embedding_model=embedding_model,
93 )
95 @property
96 def openai_client(self) -> Any:
97 return self._openai_client
99 @openai_client.setter
100 def openai_client(self, client: Any) -> None:
101 # Store locally
102 self._openai_client = client
103 # Best-effort propagate to vector search service and prefer explicit client over provider
104 try:
105 vss = getattr(self, "vector_search_service", None)
106 if vss is not None:
107 try:
108 vss.openai_client = client
109 if client is not None:
110 # Prefer explicit OpenAI client; disable provider to avoid NotImplemented stubs
111 vss.embeddings_provider = None
112 except Exception:
113 pass
114 except Exception:
115 pass
117 async def search(self, *args, **kwargs): # type: ignore[override]
118 try:
119 return await super().search(*args, **kwargs)
120 except Exception as e:
121 self.logger.error(
122 "Error in hybrid search", error=str(e), query=kwargs.get("query")
123 )
124 raise
126 # All other public and internal methods are provided by HybridEngineAPI