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

1""" 

2Hybrid Search Engine Implementation. 

3 

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""" 

8 

9from __future__ import annotations 

10 

11from typing import Any 

12 

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) 

23 

24logger = LoggingConfig.get_logger(__name__) 

25 

26 

27class HybridSearchEngine(HybridEngineAPI): 

28 """Refactored hybrid search service using modular components.""" 

29 

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. 

47 

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__) 

73 

74 # Centralized initialization via builder 

75 from .components.builder import initialize_engine_components 

76 from .models import HybridProcessingConfig as _HPC 

77 

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 ) 

94 

95 @property 

96 def openai_client(self) -> Any: 

97 return self._openai_client 

98 

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 

116 

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 

125 

126 # All other public and internal methods are provided by HybridEngineAPI