longparser 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- longparser/__init__.py +104 -0
- longparser/chunkers/__init__.py +5 -0
- longparser/chunkers/hybrid_chunker.py +1046 -0
- longparser/extractors/__init__.py +9 -0
- longparser/extractors/base.py +62 -0
- longparser/extractors/docling_extractor.py +2065 -0
- longparser/extractors/latex_ocr.py +404 -0
- longparser/integrations/__init__.py +31 -0
- longparser/integrations/langchain.py +138 -0
- longparser/integrations/llamaindex.py +157 -0
- longparser/pipeline/__init__.py +8 -0
- longparser/pipeline/orchestrator.py +230 -0
- longparser/py.typed +0 -0
- longparser/schemas.py +247 -0
- longparser/server/__init__.py +22 -0
- longparser/server/app.py +1045 -0
- longparser/server/chat/__init__.py +39 -0
- longparser/server/chat/callbacks.py +110 -0
- longparser/server/chat/engine.py +341 -0
- longparser/server/chat/graph.py +176 -0
- longparser/server/chat/llm_chain.py +153 -0
- longparser/server/chat/retriever.py +111 -0
- longparser/server/chat/schemas.py +164 -0
- longparser/server/db.py +656 -0
- longparser/server/embeddings.py +181 -0
- longparser/server/queue.py +97 -0
- longparser/server/routers/__init__.py +0 -0
- longparser/server/schemas.py +204 -0
- longparser/server/vectorstores.py +443 -0
- longparser/server/worker.py +480 -0
- longparser/utils/__init__.py +5 -0
- longparser/utils/rtl_detector.py +93 -0
- longparser-0.1.0.dist-info/METADATA +337 -0
- longparser-0.1.0.dist-info/RECORD +36 -0
- longparser-0.1.0.dist-info/WHEEL +5 -0
- longparser-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""Pluggable vector store adapters for LongParser.
|
|
2
|
+
|
|
3
|
+
Supports: Chroma, FAISS, Qdrant.
|
|
4
|
+
Each adapter follows the BaseVectorStore interface.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import tempfile
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseVectorStore(ABC):
|
|
21
|
+
"""Abstract vector store — all adapters implement this interface."""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def add(
|
|
25
|
+
self,
|
|
26
|
+
ids: list[str],
|
|
27
|
+
embeddings: list[list[float]],
|
|
28
|
+
metadatas: list[dict],
|
|
29
|
+
documents: list[str],
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Add vectors with metadata."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def search(
|
|
36
|
+
self,
|
|
37
|
+
query_embedding: list[float],
|
|
38
|
+
top_k: int = 5,
|
|
39
|
+
filters: Optional[dict] = None,
|
|
40
|
+
) -> list[dict]:
|
|
41
|
+
"""Search for similar vectors. Returns list of {id, score, metadata, document}."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def delete_by_job(self, job_id: str, tenant_id: str = "") -> None:
|
|
46
|
+
"""Delete all vectors for a job (idempotent)."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Chroma
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
class ChromaStore(BaseVectorStore):
|
|
55
|
+
"""ChromaDB vector store adapter."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
collection_name: str = "longparser",
|
|
60
|
+
persist_directory: str = "./chroma_data",
|
|
61
|
+
index_fingerprint: str = "",
|
|
62
|
+
):
|
|
63
|
+
try:
|
|
64
|
+
import chromadb
|
|
65
|
+
except ImportError:
|
|
66
|
+
raise ImportError(
|
|
67
|
+
"chromadb is required. Install: pip install clean_rag[chroma]"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Securely isolate vector spaces based on model config
|
|
71
|
+
if index_fingerprint:
|
|
72
|
+
collection_name = f"{collection_name}_{index_fingerprint}"
|
|
73
|
+
|
|
74
|
+
self.client = chromadb.PersistentClient(path=persist_directory)
|
|
75
|
+
self.collection = self.client.get_or_create_collection(
|
|
76
|
+
name=collection_name,
|
|
77
|
+
metadata={"hnsw:space": "cosine"},
|
|
78
|
+
)
|
|
79
|
+
logger.info(f"ChromaStore: collection={collection_name}")
|
|
80
|
+
|
|
81
|
+
def add(self, ids, embeddings, metadatas, documents) -> None:
|
|
82
|
+
# Chroma metadata must be flat (no nested lists/dicts)
|
|
83
|
+
flat_metas = []
|
|
84
|
+
for m in metadatas:
|
|
85
|
+
flat = {}
|
|
86
|
+
for k, v in m.items():
|
|
87
|
+
if isinstance(v, list):
|
|
88
|
+
flat[k] = json.dumps(v)
|
|
89
|
+
else:
|
|
90
|
+
flat[k] = v
|
|
91
|
+
flat_metas.append(flat)
|
|
92
|
+
|
|
93
|
+
self.collection.upsert(
|
|
94
|
+
ids=ids,
|
|
95
|
+
embeddings=embeddings,
|
|
96
|
+
metadatas=flat_metas,
|
|
97
|
+
documents=documents,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def search(self, query_embedding, top_k=5, filters=None) -> list[dict]:
|
|
101
|
+
where = None
|
|
102
|
+
if filters:
|
|
103
|
+
# Build Chroma where clause
|
|
104
|
+
conditions = []
|
|
105
|
+
for k, v in filters.items():
|
|
106
|
+
conditions.append({k: {"$eq": v}})
|
|
107
|
+
if len(conditions) == 1:
|
|
108
|
+
where = conditions[0]
|
|
109
|
+
elif conditions:
|
|
110
|
+
where = {"$and": conditions}
|
|
111
|
+
|
|
112
|
+
results = self.collection.query(
|
|
113
|
+
query_embeddings=[query_embedding],
|
|
114
|
+
n_results=top_k,
|
|
115
|
+
where=where,
|
|
116
|
+
include=["documents", "metadatas", "distances"],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
output = []
|
|
120
|
+
if results["ids"] and results["ids"][0]:
|
|
121
|
+
for i, vid in enumerate(results["ids"][0]):
|
|
122
|
+
meta = results["metadatas"][0][i] if results["metadatas"] else {}
|
|
123
|
+
# Restore lists from JSON strings
|
|
124
|
+
for k, v in meta.items():
|
|
125
|
+
if isinstance(v, str) and v.startswith("["):
|
|
126
|
+
try:
|
|
127
|
+
meta[k] = json.loads(v)
|
|
128
|
+
except (json.JSONDecodeError, ValueError):
|
|
129
|
+
pass
|
|
130
|
+
output.append({
|
|
131
|
+
"id": vid,
|
|
132
|
+
"score": 1.0 - (results["distances"][0][i] if results["distances"] else 0),
|
|
133
|
+
"metadata": meta,
|
|
134
|
+
"document": results["documents"][0][i] if results["documents"] else "",
|
|
135
|
+
})
|
|
136
|
+
return output
|
|
137
|
+
|
|
138
|
+
def delete_by_job(self, job_id: str, tenant_id: str = "") -> None:
|
|
139
|
+
try:
|
|
140
|
+
where = {"job_id": {"$eq": job_id}}
|
|
141
|
+
if tenant_id:
|
|
142
|
+
where = {"$and": [
|
|
143
|
+
{"job_id": {"$eq": job_id}},
|
|
144
|
+
{"tenant_id": {"$eq": tenant_id}},
|
|
145
|
+
]}
|
|
146
|
+
self.collection.delete(where=where)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.warning(f"ChromaStore delete_by_job failed (idempotent): {e}")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# FAISS (per-job artifact)
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
class FAISSStore(BaseVectorStore):
|
|
156
|
+
"""FAISS vector store — per-job file-based index with atomic writes."""
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
collection_name: str = "longparser",
|
|
161
|
+
base_dir: str = "./faiss_data",
|
|
162
|
+
index_fingerprint: str = "",
|
|
163
|
+
):
|
|
164
|
+
try:
|
|
165
|
+
import faiss # noqa: F401
|
|
166
|
+
except ImportError:
|
|
167
|
+
raise ImportError(
|
|
168
|
+
"faiss-cpu is required. Install: pip install clean_rag[faiss]"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self.base_dir = Path(base_dir)
|
|
172
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
self.collection_name = collection_name
|
|
174
|
+
self.index_fingerprint = index_fingerprint
|
|
175
|
+
self._indexes: dict = {} # job_id → (index, id_map, meta_map, doc_map)
|
|
176
|
+
|
|
177
|
+
def _index_path(self, job_id: str) -> Path:
|
|
178
|
+
# Isolate semantic space using fingerprint suffix explicitly
|
|
179
|
+
dirname = f"{job_id}_{self.index_fingerprint}" if self.index_fingerprint else job_id
|
|
180
|
+
return self.base_dir / dirname
|
|
181
|
+
|
|
182
|
+
def _load_index(self, job_id: str):
|
|
183
|
+
import faiss
|
|
184
|
+
import numpy as np
|
|
185
|
+
|
|
186
|
+
idx_dir = self._index_path(job_id)
|
|
187
|
+
index_file = idx_dir / "index.faiss"
|
|
188
|
+
meta_file = idx_dir / "metadata.json"
|
|
189
|
+
|
|
190
|
+
if index_file.exists() and meta_file.exists():
|
|
191
|
+
index = faiss.read_index(str(index_file))
|
|
192
|
+
with open(meta_file) as f:
|
|
193
|
+
data = json.load(f)
|
|
194
|
+
return index, data.get("ids", []), data.get("metadatas", []), data.get("documents", [])
|
|
195
|
+
|
|
196
|
+
return None, [], [], []
|
|
197
|
+
|
|
198
|
+
def _save_index(self, job_id: str, index, ids, metadatas, documents):
|
|
199
|
+
"""Atomic write: temp → fsync → rename."""
|
|
200
|
+
import faiss
|
|
201
|
+
|
|
202
|
+
idx_dir = self._index_path(job_id)
|
|
203
|
+
idx_dir.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
|
|
205
|
+
# Write index atomically
|
|
206
|
+
with tempfile.NamedTemporaryFile(dir=idx_dir, suffix=".faiss", delete=False) as tmp:
|
|
207
|
+
tmp_path = tmp.name
|
|
208
|
+
faiss.write_index(index, tmp_path)
|
|
209
|
+
os.fsync(tmp.fileno())
|
|
210
|
+
os.rename(tmp_path, str(idx_dir / "index.faiss"))
|
|
211
|
+
|
|
212
|
+
# Write metadata atomically
|
|
213
|
+
meta = {"ids": ids, "metadatas": metadatas, "documents": documents}
|
|
214
|
+
with tempfile.NamedTemporaryFile(dir=idx_dir, suffix=".json", mode="w", delete=False) as tmp:
|
|
215
|
+
tmp_meta_path = tmp.name
|
|
216
|
+
json.dump(meta, tmp, default=str)
|
|
217
|
+
os.fsync(tmp.fileno())
|
|
218
|
+
os.rename(tmp_meta_path, str(idx_dir / "metadata.json"))
|
|
219
|
+
|
|
220
|
+
def add(self, ids, embeddings, metadatas, documents) -> None:
|
|
221
|
+
import faiss
|
|
222
|
+
import numpy as np
|
|
223
|
+
|
|
224
|
+
if not embeddings:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
# Determine job_id from first metadata
|
|
228
|
+
job_id = metadatas[0].get("job_id", "default") if metadatas else "default"
|
|
229
|
+
|
|
230
|
+
# Load or create index
|
|
231
|
+
index, existing_ids, existing_metas, existing_docs = self._load_index(job_id)
|
|
232
|
+
|
|
233
|
+
dim = len(embeddings[0])
|
|
234
|
+
if index is None:
|
|
235
|
+
index = faiss.IndexFlatIP(dim) # inner product (cosine with normalized vectors)
|
|
236
|
+
|
|
237
|
+
# Add vectors
|
|
238
|
+
vectors = np.array(embeddings, dtype="float32")
|
|
239
|
+
index.add(vectors)
|
|
240
|
+
|
|
241
|
+
# Append metadata
|
|
242
|
+
existing_ids.extend(ids)
|
|
243
|
+
existing_metas.extend(metadatas)
|
|
244
|
+
existing_docs.extend(documents)
|
|
245
|
+
|
|
246
|
+
self._save_index(job_id, index, existing_ids, existing_metas, existing_docs)
|
|
247
|
+
|
|
248
|
+
def search(self, query_embedding, top_k=5, filters=None) -> list[dict]:
|
|
249
|
+
import faiss
|
|
250
|
+
import numpy as np
|
|
251
|
+
|
|
252
|
+
job_id = filters.get("job_id", "default") if filters else "default"
|
|
253
|
+
index, ids, metadatas, documents = self._load_index(job_id)
|
|
254
|
+
|
|
255
|
+
if index is None or index.ntotal == 0:
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
query = np.array([query_embedding], dtype="float32")
|
|
259
|
+
scores, indices = index.search(query, min(top_k, index.ntotal))
|
|
260
|
+
|
|
261
|
+
output = []
|
|
262
|
+
for i, idx in enumerate(indices[0]):
|
|
263
|
+
if idx < 0 or idx >= len(ids):
|
|
264
|
+
continue
|
|
265
|
+
output.append({
|
|
266
|
+
"id": ids[idx],
|
|
267
|
+
"score": float(scores[0][i]),
|
|
268
|
+
"metadata": metadatas[idx] if idx < len(metadatas) else {},
|
|
269
|
+
"document": documents[idx] if idx < len(documents) else "",
|
|
270
|
+
})
|
|
271
|
+
return output
|
|
272
|
+
|
|
273
|
+
def delete_by_job(self, job_id: str, tenant_id: str = "") -> None:
|
|
274
|
+
"""Delete entire per-job index directory (rebuild pattern)."""
|
|
275
|
+
import shutil
|
|
276
|
+
idx_dir = self._index_path(job_id)
|
|
277
|
+
if idx_dir.exists():
|
|
278
|
+
shutil.rmtree(idx_dir)
|
|
279
|
+
logger.info(f"FAISSStore: deleted index for job {job_id}")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Qdrant
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
class QdrantStore(BaseVectorStore):
|
|
287
|
+
"""Qdrant vector store adapter."""
|
|
288
|
+
|
|
289
|
+
def __init__(
|
|
290
|
+
self,
|
|
291
|
+
collection_name: str = "longparser",
|
|
292
|
+
url: str = "http://localhost:6333",
|
|
293
|
+
index_fingerprint: str = "",
|
|
294
|
+
):
|
|
295
|
+
try:
|
|
296
|
+
from qdrant_client import QdrantClient
|
|
297
|
+
from qdrant_client.models import Distance, VectorParams
|
|
298
|
+
except ImportError:
|
|
299
|
+
raise ImportError(
|
|
300
|
+
"qdrant-client is required. Install: pip install clean_rag[qdrant]"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
self.client = QdrantClient(url=url)
|
|
304
|
+
# Securely isolate vector spaces based on model config
|
|
305
|
+
self.collection_name = f"{collection_name}_{index_fingerprint}" if index_fingerprint else collection_name
|
|
306
|
+
self._distance = Distance.COSINE
|
|
307
|
+
logger.info(f"QdrantStore: collection={collection_name}, url={url}")
|
|
308
|
+
|
|
309
|
+
def _ensure_collection(self, dim: int) -> None:
|
|
310
|
+
"""Create or validate collection. Mismatch → new collection name."""
|
|
311
|
+
from qdrant_client.models import Distance, VectorParams
|
|
312
|
+
|
|
313
|
+
collections = [c.name for c in self.client.get_collections().collections]
|
|
314
|
+
|
|
315
|
+
if self.collection_name in collections:
|
|
316
|
+
# Validate dim + metric
|
|
317
|
+
info = self.client.get_collection(self.collection_name)
|
|
318
|
+
existing_dim = info.config.params.vectors.size
|
|
319
|
+
if existing_dim != dim:
|
|
320
|
+
# Mismatch — create new collection with hash suffix
|
|
321
|
+
import hashlib
|
|
322
|
+
suffix = hashlib.md5(f"{dim}".encode()).hexdigest()[:8]
|
|
323
|
+
self.collection_name = f"{self.collection_name}_{suffix}"
|
|
324
|
+
logger.warning(
|
|
325
|
+
f"QdrantStore: dim mismatch, using collection: {self.collection_name}"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if self.collection_name not in collections:
|
|
329
|
+
self.client.create_collection(
|
|
330
|
+
collection_name=self.collection_name,
|
|
331
|
+
vectors_config=VectorParams(size=dim, distance=self._distance),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def add(self, ids, embeddings, metadatas, documents) -> None:
|
|
335
|
+
from qdrant_client.models import PointStruct
|
|
336
|
+
|
|
337
|
+
if not embeddings:
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
dim = len(embeddings[0])
|
|
341
|
+
self._ensure_collection(dim)
|
|
342
|
+
|
|
343
|
+
points = []
|
|
344
|
+
for i, (vid, emb, meta, doc) in enumerate(zip(ids, embeddings, metadatas, documents)):
|
|
345
|
+
# Flatten lists in payload for Qdrant filtering
|
|
346
|
+
payload = {**meta, "document": doc}
|
|
347
|
+
for k, v in payload.items():
|
|
348
|
+
if isinstance(v, list):
|
|
349
|
+
payload[k] = json.dumps(v)
|
|
350
|
+
|
|
351
|
+
points.append(PointStruct(
|
|
352
|
+
id=i, # Qdrant needs int or UUID
|
|
353
|
+
vector=emb,
|
|
354
|
+
payload={**payload, "vector_id": vid},
|
|
355
|
+
))
|
|
356
|
+
|
|
357
|
+
self.client.upsert(collection_name=self.collection_name, points=points)
|
|
358
|
+
|
|
359
|
+
def search(self, query_embedding, top_k=5, filters=None) -> list[dict]:
|
|
360
|
+
from qdrant_client.models import Filter, FieldCondition, MatchValue
|
|
361
|
+
|
|
362
|
+
search_filter = None
|
|
363
|
+
if filters:
|
|
364
|
+
conditions = []
|
|
365
|
+
for k, v in filters.items():
|
|
366
|
+
conditions.append(FieldCondition(key=k, match=MatchValue(value=v)))
|
|
367
|
+
if conditions:
|
|
368
|
+
search_filter = Filter(must=conditions)
|
|
369
|
+
|
|
370
|
+
results = self.client.query_points(
|
|
371
|
+
collection_name=self.collection_name,
|
|
372
|
+
query=query_embedding,
|
|
373
|
+
limit=top_k,
|
|
374
|
+
query_filter=search_filter,
|
|
375
|
+
).points
|
|
376
|
+
|
|
377
|
+
output = []
|
|
378
|
+
for hit in results:
|
|
379
|
+
payload = hit.payload or {}
|
|
380
|
+
# Restore lists
|
|
381
|
+
for k, v in payload.items():
|
|
382
|
+
if isinstance(v, str) and v.startswith("["):
|
|
383
|
+
try:
|
|
384
|
+
payload[k] = json.loads(v)
|
|
385
|
+
except (json.JSONDecodeError, ValueError):
|
|
386
|
+
pass
|
|
387
|
+
output.append({
|
|
388
|
+
"id": payload.get("vector_id", ""),
|
|
389
|
+
"score": hit.score,
|
|
390
|
+
"metadata": payload,
|
|
391
|
+
"document": payload.get("document", ""),
|
|
392
|
+
})
|
|
393
|
+
return output
|
|
394
|
+
|
|
395
|
+
def delete_by_job(self, job_id: str, tenant_id: str = "") -> None:
|
|
396
|
+
from qdrant_client.models import Filter, FieldCondition, MatchValue
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
conditions = [FieldCondition(key="job_id", match=MatchValue(value=job_id))]
|
|
400
|
+
if tenant_id:
|
|
401
|
+
conditions.append(
|
|
402
|
+
FieldCondition(key="tenant_id", match=MatchValue(value=tenant_id))
|
|
403
|
+
)
|
|
404
|
+
self.client.delete(
|
|
405
|
+
collection_name=self.collection_name,
|
|
406
|
+
points_selector=Filter(must=conditions),
|
|
407
|
+
)
|
|
408
|
+
except Exception as e:
|
|
409
|
+
logger.warning(f"QdrantStore delete_by_job failed (idempotent): {e}")
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
# Factory
|
|
414
|
+
# ---------------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
def get_vector_store(
|
|
417
|
+
backend: str,
|
|
418
|
+
collection_name: str = "longparser",
|
|
419
|
+
index_fingerprint: str = "",
|
|
420
|
+
**kwargs,
|
|
421
|
+
) -> BaseVectorStore:
|
|
422
|
+
"""Create a vector store adapter by name.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
backend: "chroma", "faiss", or "qdrant"
|
|
426
|
+
collection_name: Name for the collection/index
|
|
427
|
+
index_fingerprint: 10-char hash to isolate different embedding models
|
|
428
|
+
**kwargs: Backend-specific options
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Configured BaseVectorStore instance
|
|
432
|
+
"""
|
|
433
|
+
if backend == "chroma":
|
|
434
|
+
return ChromaStore(collection_name=collection_name, index_fingerprint=index_fingerprint, **kwargs)
|
|
435
|
+
elif backend == "faiss":
|
|
436
|
+
return FAISSStore(collection_name=collection_name, index_fingerprint=index_fingerprint, **kwargs)
|
|
437
|
+
elif backend == "qdrant":
|
|
438
|
+
return QdrantStore(collection_name=collection_name, index_fingerprint=index_fingerprint, **kwargs)
|
|
439
|
+
else:
|
|
440
|
+
raise ValueError(
|
|
441
|
+
f"Unknown vector store backend: {backend}. "
|
|
442
|
+
f"Supported: chroma, faiss, qdrant"
|
|
443
|
+
)
|