Vector Store Integration#
Dify's vector store layer is built around the BaseVector abstract class, which defines a common contract every backend must implement: search_by_vector, search_by_full_text, create, add_texts, delete_by_ids, and delete_by_metadata_field. Backends are registered as plugins under api/providers/vdb/ and resolved at dataset-open time.
The VectorType enum lists 38+ registered backends, including pgvector, Milvus, Qdrant, Weaviate, Chroma, OpenSearch, Elasticsearch (plus a Japanese variant), MatrixOne, Tencent, TiDB, OceanBase, AnalyticDB, and others.
Related articles: Retrieval Filtering and Scoring (pipeline above this layer) · Hybrid Search · Milvus Integration · Weaviate Vector Store
search_by_vector Contract#
All backends accept a consistent set of kwargs :
| kwarg | Default | Purpose |
|---|---|---|
top_k | 4–5 | Maximum candidates to return |
score_threshold | 0.0 | Minimum score gate |
document_ids_filter | None | Restrict to specific parent documents |
Returned Document objects carry metadata["score"] so the layer above can apply further threshold filtering without recomputing scores.
Hybrid search note:
RetrievalServiceforcesscore_threshold=0.0at vector retrieval time for hybrid queries, deferring threshold enforcement to post-fusion reranking. See Retrieval Filtering and Scoring .
Score Computation by Backend#
Backends fall into two camps — distance-to-score conversion and native score — so score_threshold values are not portable across backends .
Distance-to-Score (score = f(distance))#
| Backend | Score formula | Source |
|---|---|---|
| pgvector | 1 - distance (cosine <=>) | pgvector.py L199–205 |
| Weaviate | 1.0 - distance | |
| Chroma | 1 - distance (Euclidean) | |
MatrixOne (search_by_vector) | 1.0 / (1.0 + distance) (L2) | matrixone_vector.py L168–172 |
MatrixOne (search_by_full_text) | 1 - distance | matrixone_vector.py L205–209 |
MatrixOne uses different formulas across its two search paths because L2 ANN distances and full-text distances have different numeric ranges: 1/(1+d) maps L2 distance from [0, ∞) into (0, 1], while 1 - d is appropriate for the normalized distance returned by full-text search .
Native Score (no conversion)#
| Backend | Score source |
|---|---|
| Milvus | result["distance"] stored as-is (inner-product / HNSW) — note threshold uses > not >= |
| Qdrant | result.score from the Qdrant client |
| OpenSearch | hit["_score"] (BM25 or script_score) |
| Elasticsearch | hit["_score"] (cosine similarity via KNN) |
Score Threshold Filtering#
Post-query (most backends)#
pgvector, Weaviate, Chroma, MatrixOne, OpenSearch, and Elasticsearch all fetch top_k results first, then filter in Python: if score >= score_threshold . pgvector explicitly assigns metadata["score"] even before the threshold check, so the score is always available on returned docs .
In-query + post-query (Qdrant only)#
Qdrant passes score_threshold directly to _client.search(score_threshold=...), pruning at the index level, then applies a redundant Python check. As an edge case, score_threshold >= 1 short-circuits immediately to an empty list without a DB round-trip .
Milvus threshold semantics#
Milvus uses a strict > comparison (result["distance"] > score_threshold) rather than >=, so a score_threshold of exactly 0.0 still returns all results .
document_ids_filter Translation#
All backends translate document_ids_filter to backend-native filter syntax before the query :
| Backend | Filter mechanism |
|---|---|
| pgvector | SQL WHERE meta->>'document_id' IN (...) — pgvector.py L186–190 |
| Milvus | filter='metadata["document_id"] in [...]' — milvus_vector.py L261–265 |
| MatrixOne | filter={"document_id": {"$in": ids}} via MoVectorClient — matrixone_vector.py L161–163 |
| Elasticsearch | terms filter inside the KNN body — |
Tencent VectorDB bug (fixed 2025-08-07, PR #23564): In full-text search mode, document_ids_filter was previously silently dropped. The fix builds Filter.In("metadata.document_id", document_ids_filter) and passes it into hybrid_search(filter=...).
MatrixOne-Specific Notes#
matrixone_vector.py was added 2026-07-29:
- Backed by
mo_vector.MoVectorClientover a MySQL-protocol connection (mysql+pymysql://). Default metric isl2, configurable viaMATRIXONE_METRICenv var . - Client initialization is lazy (via
@ensure_clientdecorator) and guarded by a Redis lock (vector_indexing_lock_{collection_name}). Full-text index creation is cached for 1 hour (redis_client.set(..., ex=3600)) to avoid recreation on every call . - Unit coverage:
test_matrixone_vector.py.
Metadata Type Constraints: Tencent VectorDB Deadlock#
Tencent VectorDB rejects boolean values in JSON metadata. Summary index vectors include is_summary: True, which caused upserts to fail. The failure path opened a second DB session to record the error while the caller-owned session still held a row lock — resulting in a deadlock .
Fix (PR #41916):
- In
tencent_vector.pyadd_texts(), convertmetadata["is_summary"] = True→1before upsert. - In
summary_index_service.pyvectorize_summary(), when a caller-owned session is provided, write error status back on that session and re-raise — do not open a newerror_sessionthat competes for the same row lock.
This session ownership discipline in error paths applies to any backend where vectorization can fail after the caller has already flushed a row.
Key Files#
| File | Role |
|---|---|
vdb/vector_base.py | BaseVector abstract class |
vdb/vector_type.py | VectorType enum — all registered backends |
vdb-pgvector/…/pgvector.py | pgvector: cosine distance, 1-distance score, post-query threshold |
vdb-milvus/…/milvus_vector.py | Milvus: native distance score, > threshold, HNSW/IP |
vdb-matrixone/…/matrixone_vector.py | MatrixOne: L2/MySQL, 1/(1+d) score formula |
retrieval_service.py | Calls search_by_vector; hybrid score deferral |
summary_index_service.py | Summary vectorization pipeline, session ownership fix |