Vector Store Integration#
Dify's vector store layer is built around the BaseVector abstract class. Every backend implements a common contract: 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. As of mid-2026, 38+ backends are registered (pgvector, Milvus, Qdrant, Weaviate, Chroma, OpenSearch, Elasticsearch, MatrixOne, Tencent VectorDB, TiDB, OceanBase, and more).
Related KB articles: Retrieval Filtering and Scoring (pipeline above this layer) Β· Hybrid Search Β· Milvus Integration Β· Weaviate Vector Store Β· Summary Index
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 threshold filters without recomputing scores.
Note for hybrid search:
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. Because semantics differ, score_threshold values are not portable across backends.
Distance-to-Score (score = f(distance))#
| Backend | Score formula | Source |
|---|---|---|
| pgvector | 1 - distance (cosine <=>) | pgvector.py L202-204 |
| Weaviate | 1.0 - distance | |
| Chroma | 1 - distance (Euclidean) | |
MatrixOne (search_by_vector) | 1.0 / (1.0 + distance) (L2) | matrixone_vector.py |
MatrixOne (search_by_full_text) | 1 - distance | matrixone_vector.py L208 |
MatrixOne uses different formulas for its two search paths β 1/(1+distance) for ANN vector search and 1βdistance for full-text search β because the two paths return distances with different ranges.
Native Score (no conversion)#
| Backend | Score source |
|---|---|
| Milvus | result["distance"] (inner-product / HNSW) stored as-is |
| Qdrant | result.score from the Qdrant client |
| OpenSearch | hit["_score"] (BM25 or script_score) |
| Elasticsearch | hit["_score"] (KNN score, cosine similarity) |
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.
In-query + post-query (Qdrant only)#
Qdrant passes score_threshold directly to _client.search(score_threshold=...), pruning at the index level. A redundant Python check is also applied. As an edge case, score_threshold >= 1 short-circuits immediately to an empty list without a DB round-trip.
document_ids_filter Translation#
All backends translate document_ids_filter to backend-native filter syntax before the query β never post-hoc.
| Backend | Filter mechanism |
|---|---|
| pgvector | SQL WHERE meta->>'document_id' IN (...) L186-190 |
| Milvus | filter='metadata["document_id"] in [...]' L261-265 |
| Weaviate | Filter.by_property(DOCUMENT_ID_PROPERTY).contains_any(ids) |
| Chroma | where={"document_id": {"$in": ids}} |
| Elasticsearch | terms filter inside the KNN body |
| MatrixOne | filter={"document_id": {"$in": ids}} via MoVectorClient L166-168 |
Tencent VectorDB bug (fixed 2025-08-07): In full-text search mode, the 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 (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 and guarded by a Redis lock (
vector_indexing_lock_{collection_name}), with a 1-hour cache to avoid recreating the full-text index on every call. - See
test_matrixone_vector.pyfor unit coverage.
Metadata Type Constraints and the 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 then opened a second DB session to record the error while the caller-owned session still held a row lock on the summary record β resulting in a deadlock that occupied a worker slot indefinitely.
Fix (PR #41916, open as of 2026-09-07):
- In
tencent_vector.pyadd_texts(), convertmetadata["is_summary"] = Trueβ1(integer) before upsert, leaving other backends unchanged. - In
summary_index_service.pyvectorize_summary(), when a caller-owned session is provided, write the error status back on that session and re-raise β do not open a newerror_sessionthat would compete for the same row lock. Standalone callers (no provided session) still get a new error session as before.
This pattern (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 |
|---|---|
api/core/rag/datasource/vdb/vector_base.py | BaseVector abstract class β the shared contract |
api/core/rag/datasource/vdb/vector_type.py | VectorType enum β all registered backends |
api/providers/vdb/vdb-pgvector/β¦/pgvector.py | pgvector: cosine distance, post-query threshold |
api/providers/vdb/vdb-milvus/β¦/milvus_vector.py | Milvus: inner-product/HNSW, post-query threshold |
api/providers/vdb/vdb-matrixone/β¦/matrixone_vector.py | MatrixOne: L2/MySQL, 1/(1+d) score formula |
api/services/summary_index_service.py | Summary vectorization pipeline, session ownership fix |
api/core/rag/datasource/retrieval_service.py | Calls search_by_vector; formats mixed summary/chunk results |