Spaces:
Paused
Paused
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 +1 -1
- build_vector_indexes.py +1 -1
- main.py +1 -1
- rag_pipeline/orchestrator.py +1 -1
- rag_service/retriever.py +1 -1
- vector_search/__init__.py +18 -0
- vector_search/index_qdrant.py +87 -31
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
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
| 116 |
"""
|
| 117 |
Execute Qdrant cosine vector search with optional payload language filtering.
|
|
|
|
| 118 |
"""
|
| 119 |
-
if
|
|
|
|
|
|
|
|
|
|
| 120 |
return []
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
qdrant_filter = None
|
| 126 |
if lang_filter and lang_filter.strip():
|
|
@@ -133,22 +155,42 @@ class StrategyQdrantIndex:
|
|
| 133 |
]
|
| 134 |
)
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
-
results: List[
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 239 |
top_k=top_k,
|
| 240 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|