Coverage for src/qdrant_loader_mcp_server/search/hybrid/engine.py: 91%

45 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-08 06:06 +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 ): 

45 """Initialize the hybrid search service. 

46 

47 Args: 

48 qdrant_client: Qdrant client instance 

49 openai_client: OpenAI client instance 

50 collection_name: Name of the Qdrant collection 

51 vector_weight: Weight for vector search scores (0-1) 

52 keyword_weight: Weight for keyword search scores (0-1) 

53 metadata_weight: Weight for metadata-based scoring (0-1) 

54 min_score: Minimum combined score threshold 

55 knowledge_graph: Optional knowledge graph for integration 

56 enable_intent_adaptation: Enable intent-aware adaptive search 

57 search_config: Optional search configuration for performance optimization 

58 processing_config: Optional processing configuration controlling hybrid processing behaviors 

59 """ 

60 self.qdrant_client = qdrant_client 

61 # Use a property-backed attribute so test fixtures can inject a client after init 

62 self._openai_client = None 

63 # Assign via setter to allow future propagation when pipeline is ready 

64 self.openai_client = openai_client 

65 self.collection_name = collection_name 

66 self.vector_weight = vector_weight 

67 self.keyword_weight = keyword_weight 

68 self.metadata_weight = metadata_weight 

69 self.min_score = min_score 

70 self.logger = LoggingConfig.get_logger(__name__) 

71 

72 # Centralized initialization via builder 

73 from .components.builder import initialize_engine_components 

74 from .models import HybridProcessingConfig as _HPC 

75 

76 effective_processing_config = processing_config or _HPC() 

77 initialize_engine_components( 

78 self, 

79 qdrant_client=qdrant_client, 

80 openai_client=openai_client, 

81 collection_name=collection_name, 

82 vector_weight=vector_weight, 

83 keyword_weight=keyword_weight, 

84 metadata_weight=metadata_weight, 

85 min_score=min_score, 

86 knowledge_graph=knowledge_graph, 

87 enable_intent_adaptation=enable_intent_adaptation, 

88 search_config=search_config, 

89 processing_config=effective_processing_config, 

90 ) 

91 

92 @property 

93 def openai_client(self) -> Any: 

94 return self._openai_client 

95 

96 @openai_client.setter 

97 def openai_client(self, client: Any) -> None: 

98 # Store locally 

99 self._openai_client = client 

100 # Best-effort propagate to vector search service and prefer explicit client over provider 

101 try: 

102 vss = getattr(self, "vector_search_service", None) 

103 if vss is not None: 

104 try: 

105 vss.openai_client = client 

106 if client is not None: 

107 # Prefer explicit OpenAI client; disable provider to avoid NotImplemented stubs 

108 vss.embeddings_provider = None 

109 except Exception: 

110 pass 

111 except Exception: 

112 pass 

113 

114 async def search(self, *args, **kwargs): # type: ignore[override] 

115 try: 

116 return await super().search(*args, **kwargs) 

117 except Exception as e: 

118 self.logger.error( 

119 "Error in hybrid search", error=str(e), query=kwargs.get("query") 

120 ) 

121 raise 

122 

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