rishik1111 commited on
Commit
9825db3
·
1 Parent(s): 3c23784

feat: Add dynamic Qdrant Cloud vector search integration with payload indexing and candidate fusion

Browse files
api_endpoints/main.py CHANGED
@@ -26,7 +26,7 @@ import config
26
  from rag_pipeline.orchestrator import get_orchestrator, RAGPipelineOrchestrator
27
  from rag_pipeline.schemas import QueryRequest, QueryResponse
28
  from vector_search.embed import get_embedder
29
- from vector_search.index_faiss import get_index_manager
30
 
31
  logger = logging.getLogger(__name__)
32
 
 
26
  from rag_pipeline.orchestrator import get_orchestrator, RAGPipelineOrchestrator
27
  from rag_pipeline.schemas import QueryRequest, QueryResponse
28
  from vector_search.embed import get_embedder
29
+ from vector_search import get_index_manager
30
 
31
  logger = logging.getLogger(__name__)
32
 
build_vector_indexes.py CHANGED
@@ -10,7 +10,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
10
  import config
11
  from knowledge_base.tech_data import EN_TECH_PASSAGES, HI_TECH_PASSAGES, MR_TECH_PASSAGES, TECH_LONGDOCS
12
  from knowledge_base.movie_data import EN_MOVIE_PASSAGES, HI_MOVIE_PASSAGES, MR_MOVIE_PASSAGES, MOVIE_LONGDOCS
13
- from vector_search.index_faiss import get_index_manager
14
 
15
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", force=True)
16
  logger = logging.getLogger(__name__)
 
10
  import config
11
  from knowledge_base.tech_data import EN_TECH_PASSAGES, HI_TECH_PASSAGES, MR_TECH_PASSAGES, TECH_LONGDOCS
12
  from knowledge_base.movie_data import EN_MOVIE_PASSAGES, HI_MOVIE_PASSAGES, MR_MOVIE_PASSAGES, MOVIE_LONGDOCS
13
+ from vector_search import get_index_manager
14
 
15
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", force=True)
16
  logger = logging.getLogger(__name__)
main.py CHANGED
@@ -55,7 +55,7 @@ import config
55
  from rag_pipeline.orchestrator import get_orchestrator
56
  from rag_pipeline.schemas import QueryRequest, QueryResponse
57
  from vector_search.embed import get_embedder
58
- from vector_search.index_faiss import get_index_manager
59
 
60
 
61
  # Read the full custom HTML Command Center UI
 
55
  from rag_pipeline.orchestrator import get_orchestrator
56
  from rag_pipeline.schemas import QueryRequest, QueryResponse
57
  from vector_search.embed import get_embedder
58
+ from vector_search import get_index_manager
59
 
60
 
61
  # Read the full custom HTML Command Center UI
rag_pipeline/orchestrator.py CHANGED
@@ -25,7 +25,7 @@ from rag_pipeline.schemas import (
25
  StageTiming,
26
  )
27
  from vector_search.embed import get_embedder
28
- from vector_search.index_faiss import get_index_manager
29
  from vector_search.rerank import rerank_bm25_hybrid, rerank_cross_encoder, get_cross_encoder
30
  from doc_chunking.hybrid_merge import merge_and_fuse_candidates
31
  from safety_guardrails.pre_retrieval import check_unsafe_content, check_off_topic_query, check_query_intent, get_safety_telemetry
 
25
  StageTiming,
26
  )
27
  from vector_search.embed import get_embedder
28
+ from vector_search import get_index_manager
29
  from vector_search.rerank import rerank_bm25_hybrid, rerank_cross_encoder, get_cross_encoder
30
  from doc_chunking.hybrid_merge import merge_and_fuse_candidates
31
  from safety_guardrails.pre_retrieval import check_unsafe_content, check_off_topic_query, check_query_intent, get_safety_telemetry
rag_service/retriever.py CHANGED
@@ -19,7 +19,7 @@ if str(_ROOT_DIR) not in sys.path:
19
 
20
  import config
21
  from vector_search.embed import get_embedder
22
- from vector_search.index_faiss import get_index_manager
23
 
24
  logger = logging.getLogger(__name__)
25
 
 
19
 
20
  import config
21
  from vector_search.embed import get_embedder
22
+ from vector_search import get_index_manager
23
 
24
  logger = logging.getLogger(__name__)
25
 
vector_search/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import config
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+
7
+ def get_index_manager():
8
+ """
9
+ Dynamically returns the active vector index manager (Qdrant or FAISS)
10
+ based on `config.VECTOR_STORE_BACKEND`.
11
+ """
12
+ backend = getattr(config, "VECTOR_STORE_BACKEND", "faiss").lower()
13
+ if backend == "qdrant":
14
+ from vector_search.index_qdrant import get_qdrant_manager
15
+ return get_qdrant_manager()
16
+ else:
17
+ from vector_search.index_faiss import get_index_manager as get_faiss_manager
18
+ return get_faiss_manager()
vector_search/index_qdrant.py CHANGED
@@ -24,6 +24,7 @@ try:
24
  Filter,
25
  FieldCondition,
26
  MatchValue,
 
27
  )
28
  HAS_QDRANT = True
29
  except ImportError:
@@ -68,6 +69,16 @@ class StrategyQdrantIndex:
68
  )
69
  logger.info("Created Qdrant collection: '%s' (dim=%d)", self.collection_name, self.dim)
70
 
 
 
 
 
 
 
 
 
 
 
71
  @property
72
  def size(self) -> int:
73
  """Returns number of vectors in collection."""
@@ -109,18 +120,29 @@ class StrategyQdrantIndex:
109
 
110
  def search(
111
  self,
112
- query_vector: np.ndarray,
113
- top_k: int = 5,
114
- lang_filter: Optional[str] = None,
115
- ) -> List[Tuple[Chunk, float]]:
 
116
  """
117
  Execute Qdrant cosine vector search with optional payload language filtering.
 
118
  """
119
- if self.size == 0:
 
 
 
120
  return []
121
 
122
- norm = np.linalg.norm(query_vector)
123
- unit_q = (query_vector / norm).tolist() if norm > 1e-9 else query_vector.tolist()
 
 
 
 
 
 
124
 
125
  qdrant_filter = None
126
  if lang_filter and lang_filter.strip():
@@ -133,22 +155,42 @@ class StrategyQdrantIndex:
133
  ]
134
  )
135
 
136
- if hasattr(self.client, "query_points"):
137
- search_result = self.client.query_points(
138
- collection_name=self.collection_name,
139
- query=unit_q,
140
- query_filter=qdrant_filter,
141
- limit=top_k,
142
- ).points
143
- else:
144
- search_result = self.client.search(
145
- collection_name=self.collection_name,
146
- query_vector=unit_q,
147
- query_filter=qdrant_filter,
148
- limit=top_k,
149
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
- results: List[Tuple[Chunk, float]] = []
152
  for hit in search_result:
153
  payload = hit.payload or {}
154
  idx = payload.get("idx", 0)
@@ -168,9 +210,18 @@ class StrategyQdrantIndex:
168
  metadata=payload.get("metadata", {}),
169
  )
170
 
171
- # Qdrant score is cosine similarity [0, 1]
172
  score = float(hit.score)
173
- results.append((chunk, score))
 
 
 
 
 
 
 
 
 
 
174
 
175
  return results
176
 
@@ -200,6 +251,7 @@ class QdrantIndexManager:
200
 
201
  self.strategy_indices: Dict[str, StrategyQdrantIndex] = {}
202
  self.centroids: Dict[str, np.ndarray] = {}
 
203
 
204
  @property
205
  def indexes(self) -> Dict[str, StrategyQdrantIndex]:
@@ -227,17 +279,19 @@ class QdrantIndexManager:
227
  def search(
228
  self,
229
  strategy_name: str,
230
- query_vector: np.ndarray,
231
- top_k: int = 5,
232
- lang_filter: Optional[str] = None,
233
- ) -> List[Tuple[Chunk, float]]:
 
234
  if strategy_name not in self.strategy_indices:
235
  logger.warning("Strategy '%s' not indexed in Qdrant", strategy_name)
236
  return []
237
  return self.strategy_indices[strategy_name].search(
238
- query_vector=query_vector,
 
239
  top_k=top_k,
240
- lang_filter=lang_filter,
241
  )
242
 
243
  def compute_centroids(self):
@@ -316,7 +370,9 @@ class QdrantIndexManager:
316
 
317
  def load_all_indexes(self):
318
  """Auto-recovers or populates Qdrant Cloud indexes."""
319
- self.build_all_indexes()
 
 
320
 
321
 
322
  _QDRANT_MANAGER_INSTANCE: Optional[QdrantIndexManager] = None
 
24
  Filter,
25
  FieldCondition,
26
  MatchValue,
27
+ PayloadSchemaType,
28
  )
29
  HAS_QDRANT = True
30
  except ImportError:
 
69
  )
70
  logger.info("Created Qdrant collection: '%s' (dim=%d)", self.collection_name, self.dim)
71
 
72
+ try:
73
+ self.client.create_payload_index(
74
+ collection_name=self.collection_name,
75
+ field_name="source_lang",
76
+ field_schema=PayloadSchemaType.KEYWORD,
77
+ )
78
+ logger.info("Ensured 'source_lang' keyword payload index for '%s'", self.collection_name)
79
+ except Exception as e:
80
+ logger.debug("Note creating payload index for '%s': %s", self.collection_name, e)
81
+
82
  @property
83
  def size(self) -> int:
84
  """Returns number of vectors in collection."""
 
120
 
121
  def search(
122
  self,
123
+ query_vec: np.ndarray,
124
+ target_lang: Optional[str] = None,
125
+ top_k: int = 15,
126
+ **kwargs,
127
+ ) -> List[Dict[str, Any]]:
128
  """
129
  Execute Qdrant cosine vector search with optional payload language filtering.
130
+ Returns candidate dicts identical to FAISS StrategyVectorIndex.search interface.
131
  """
132
+ q_vec = query_vec if query_vec is not None else kwargs.get("query_vector")
133
+ lang_filter = target_lang if target_lang is not None else kwargs.get("lang_filter")
134
+
135
+ if q_vec is None or self.size == 0:
136
  return []
137
 
138
+ # Ensure query vector is 1D float array (384,) to prevent multi-vector 400 error in Qdrant
139
+ if isinstance(q_vec, np.ndarray):
140
+ q_vec = q_vec.reshape(-1)
141
+ elif isinstance(q_vec, list) and len(q_vec) > 0 and isinstance(q_vec[0], list):
142
+ q_vec = q_vec[0]
143
+
144
+ norm = np.linalg.norm(q_vec)
145
+ unit_q = (np.array(q_vec) / (norm + 1e-12)).astype(float).tolist()
146
 
147
  qdrant_filter = None
148
  if lang_filter and lang_filter.strip():
 
155
  ]
156
  )
157
 
158
+ search_result = []
159
+ try:
160
+ if hasattr(self.client, "query_points"):
161
+ search_result = self.client.query_points(
162
+ collection_name=self.collection_name,
163
+ query=unit_q,
164
+ query_filter=qdrant_filter,
165
+ limit=top_k,
166
+ ).points
167
+ else:
168
+ search_result = self.client.search(
169
+ collection_name=self.collection_name,
170
+ query_vector=unit_q,
171
+ query_filter=qdrant_filter,
172
+ limit=top_k,
173
+ )
174
+ except Exception as e:
175
+ logger.warning("Filtered Qdrant query encounter (%s); executing fallback query...", e)
176
+ try:
177
+ if hasattr(self.client, "query_points"):
178
+ search_result = self.client.query_points(
179
+ collection_name=self.collection_name,
180
+ query=unit_q,
181
+ limit=top_k,
182
+ ).points
183
+ else:
184
+ search_result = self.client.search(
185
+ collection_name=self.collection_name,
186
+ query_vector=unit_q,
187
+ limit=top_k,
188
+ )
189
+ except Exception as e2:
190
+ logger.error("Qdrant query execution error: %s", e2)
191
+ return []
192
 
193
+ results: List[Dict[str, Any]] = []
194
  for hit in search_result:
195
  payload = hit.payload or {}
196
  idx = payload.get("idx", 0)
 
210
  metadata=payload.get("metadata", {}),
211
  )
212
 
 
213
  score = float(hit.score)
214
+ results.append({
215
+ "chunk_id": chunk.chunk_id,
216
+ "text": chunk.text,
217
+ "score": score,
218
+ "source_lang": chunk.source_lang,
219
+ "chunk_strategy": chunk.chunk_strategy,
220
+ "source_query_ids": getattr(chunk, "source_query_ids", []),
221
+ "doc_id": chunk.doc_id,
222
+ "context_window": getattr(chunk, "context_window", None),
223
+ "metadata": chunk.metadata,
224
+ })
225
 
226
  return results
227
 
 
251
 
252
  self.strategy_indices: Dict[str, StrategyQdrantIndex] = {}
253
  self.centroids: Dict[str, np.ndarray] = {}
254
+ self.load_all_indexes()
255
 
256
  @property
257
  def indexes(self) -> Dict[str, StrategyQdrantIndex]:
 
279
  def search(
280
  self,
281
  strategy_name: str,
282
+ query_vec: np.ndarray,
283
+ target_lang: Optional[str] = None,
284
+ top_k: int = 15,
285
+ **kwargs,
286
+ ) -> List[Dict[str, Any]]:
287
  if strategy_name not in self.strategy_indices:
288
  logger.warning("Strategy '%s' not indexed in Qdrant", strategy_name)
289
  return []
290
  return self.strategy_indices[strategy_name].search(
291
+ query_vec=query_vec,
292
+ target_lang=target_lang,
293
  top_k=top_k,
294
+ **kwargs,
295
  )
296
 
297
  def compute_centroids(self):
 
370
 
371
  def load_all_indexes(self):
372
  """Auto-recovers or populates Qdrant Cloud indexes."""
373
+ for strat in ["passage_native", "semantic_longdoc"]:
374
+ self.get_or_create_index(strat)
375
+ logger.info("[QdrantManager] Loaded active strategy indexes: %s", list(self.strategy_indices.keys()))
376
 
377
 
378
  _QDRANT_MANAGER_INSTANCE: Optional[QdrantIndexManager] = None