Module 5: Vector Database Landscape for AI Engineers
Capsule 04: Feature Comparison for RAG
🎯 Capsule objective
Compare the technical features that most impact a RAG system in production — metadata filtering, hybrid search, multi-tenancy, batch operations, SDKs, and scaling — and understand how support varies across providers.
In the previous capsules you mapped the provider landscape and understood the managed vs self-hosted trade-off. Now it's time to evaluate the features that directly affect the quality, security, and efficiency of your RAG pipeline. A vector database can be cheap and easy to operate, but if it doesn't support advanced metadata filtering or multi-tenancy, your RAG will have serious problems in production.
This capsule is deliberately table-heavy: comparison tables are the most useful tool for a quick evaluation. Each table comes with context on why that feature matters for RAG and code examples that show API differences between providers.
By the end of this capsule:
- ✅ You'll evaluate 6 critical features for RAG across 5 providers
- ✅ You'll distinguish between nominal support and real support of each feature
- ✅ You'll identify which features are "non-negotiable" for your case
- ✅ You'll have reference tables for your decision tree
Estimated time: 25-30 minutes
📋 The 6 Critical Features for RAG
What makes a feature "critical" for RAG?
It's critical if it affects at least one of these four goals of a RAG system in production:
| Goal | Key question |
|---|---|
| Retrieval precision | Are the retrieved documents relevant? |
| p95 latency | Is retrieval fast enough for the UX? |
| Security / isolation | Can one tenant's data leak to another? |
| Operational cost | Does operation scale without blowing the budget? |
Feature map
Critical Features for RAG
│
├── 1. Metadata Filtering → Precision
├── 2. Hybrid Search → Precision
├── 3. Multi-tenancy → Security
├── 4. Batch Operations → Cost
├── 5. SDKs and DX → Speed
└── 6. Scaling → Cost + Latency
1️⃣ Metadata Filtering
Why does it matter for RAG?
Metadata filtering reduces the search space before computing similarity. Without filtering, your RAG searches across all vectors — with filtering, it searches only the relevant subset.
Real impact:
Without filtering:
Query: "How do I configure authentication?"
Searches in: 500K vectors (all docs)
Result: Mixes docs from v1, v2, v3 of the product
→ LLM generates an answer with outdated info
With filtering (version="v3"):
Query: "How do I configure authentication?"
Searches in: 80K vectors (only v3)
Result: Relevant and up-to-date docs
→ LLM generates a precise answer
Metadata filtering comparison
| Capability | ChromaDB | Pinecone | Weaviate | Qdrant | Milvus |
|---|---|---|---|---|---|
Equality filters (=) | ✅ | ✅ | ✅ | ✅ | ✅ |
Range filters (>, <) | ✅ | ✅ | ✅ | ✅ | ✅ |
IN filters (list) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Logical AND / OR | ✅ | ✅ | ✅ | ✅ | ✅ |
| NOT / negation | ✅ | ✅ | ✅ | ✅ | ✅ |
| Nested filters (objects) | ❌ | ❌ | ✅ | ✅ | ❌ |
| Full-text search in metadata | ❌ | ❌ | ✅ | ✅ | ✅ |
| Geo filtering | ❌ | ❌ | ✅ | ✅ | ❌ |
| Dedicated payload index | N/A | N/A | Auto | ✅ Manual | ✅ Manual |
| Performance with filters | ⚠️ Degrades | ✅ Good | ✅ Good | ✅ Excellent | ✅ Good |
Code: Filtering in each provider
# ChromaDB — where syntax
results = collection.query(
query_texts=["authentication"],
n_results=5,
where={
"$and": [
{"version": {"$eq": "v3"}},
{"language": {"$eq": "en"}}
]
}
)
# Pinecone — MongoDB-like syntax
results = index.query(
vector=embedding,
top_k=5,
filter={
"$and": [
{"version": {"$eq": "v3"}},
{"language": {"$eq": "en"}}
]
}
)
# Weaviate — typed filter API
from weaviate.classes.query import Filter
response = collection.query.near_text(
query="authentication",
limit=5,
filters=(
Filter.by_property("version").equal("v3") &
Filter.by_property("language").equal("en")
)
)
# Qdrant — model-based filters
from qdrant_client.models import Filter, FieldCondition, MatchValue
results = client.query_points(
collection_name="docs",
query=embedding,
limit=5,
query_filter=Filter(
must=[
FieldCondition(key="version", match=MatchValue(value="v3")),
FieldCondition(key="language", match=MatchValue(value="en"))
]
)
)
# Milvus — SQL-like string
results = client.search(
collection_name="docs",
data=[embedding],
limit=5,
filter='version == "v3" and language == "en"'
)
RAG insight: Qdrant stands out in filtering performance because it supports dedicated payload indexes that avoid a full-scan of the metadata. Weaviate has the most expressive filters (geo, nested objects). ChromaDB covers the basics but can degrade with complex filters over large collections.
2️⃣ Hybrid Search
Why does it matter for RAG?
Hybrid search combines keyword match (BM25) with semantic search (vector) to capture both exact matches ("GPT-4", "INV-2024-001") and conceptual ones ("how to improve performance"). In RAG for technical documentation, hybrid search improves accuracy by 15-25%.
Hybrid search comparison
| Capability | ChromaDB | Pinecone | Weaviate | Qdrant | Milvus |
|---|---|---|---|---|---|
| Native hybrid search | ❌ | ⚠️ Sparse | ✅ Built-in | ✅ Sparse | ✅ |
| Keyword index (BM25) | ❌ | ❌ | ✅ Inverted | ❌ | ✅ |
| Sparse vectors | ❌ | ✅ | ❌ | ✅ | ✅ |
| Fusion algorithm | N/A | Manual | RRF (default) | Manual | RRF/Weighted |
| Alpha tuning | N/A | N/A | ✅ (0-1) | Manual | ✅ |
| Custom ranking | N/A | ❌ | ✅ | ✅ | ✅ |
Support levels:
- Native (Weaviate): A single endpoint, automatic fusion, adjustable alpha.
- Sparse vectors (Pinecone, Qdrant): You send dense + sparse vectors, manual or semi-auto fusion.
- Not supported (ChromaDB): You need to implement BM25 externally (rank_bm25 library).
Code: Hybrid search per provider
# Weaviate — native hybrid with alpha
response = collection.query.hybrid(
query="GPT-4 API rate limits",
alpha=0.5, # 0 = pure keyword, 1 = pure vector
limit=10
)
# Pinecone — sparse + dense vectors
results = index.query(
vector=dense_embedding, # semantic
sparse_vector={ # keyword
"indices": [102, 5483, 9201],
"values": [0.8, 0.5, 0.3]
},
top_k=10
)
# Qdrant — sparse vectors with named vectors
results = client.query_points(
collection_name="docs",
query=embedding,
using="dense",
limit=10
)
# + separate query with a sparse vector, manual fusion
# ChromaDB — does NOT support hybrid, workaround with external BM25
from rank_bm25 import BM25Okapi
tokenized = [doc.split() for doc in all_documents]
bm25 = BM25Okapi(tokenized)
keyword_scores = bm25.get_scores(query.split())
semantic_results = collection.query(
query_texts=[query], n_results=20
)
# Manual fusion (RRF or weighted)
RAG insight: If your RAG handles technical documentation with IDs, versions, or product names, hybrid search is not optional — it's essential. Weaviate is the most mature option for hybrid. Pinecone and Qdrant support it via sparse vectors but require more work.
3️⃣ Multi-tenancy
Why does it matter for RAG?
Multi-tenancy determines how you isolate data between users, organizations, or customers. In a SaaS RAG, if Tenant A can see Tenant B's documents, you have a data leakage — a serious security problem.
Multi-tenancy strategies
Strategy 1: Collection per tenant
┌──────────┐ ┌──────────┐ ┌──────────┐
│Tenant A │ │Tenant B │ │Tenant C │
│Collection│ │Collection│ │Collection│
└──────────┘ └──────────┘ └──────────┘
Pro: Total isolation
Con: Management overhead, doesn't scale to 1000+ tenants
Strategy 2: Metadata filtering
┌─────────────────────────────────┐
│ Shared Collection │
│ [tenant_id=A] [tenant_id=B] │
│ [tenant_id=A] [tenant_id=C] │
└─────────────────────────────────┘
Pro: Simple, scales to many tenants
Con: Filter overhead, risk of forgetting the filter (leakage)
Strategy 3: Namespace / Partition
┌─────────────────────────────────┐
│ Index │
│ ┌─────────┐ ┌─────────┐ │
│ │Namespace│ │Namespace│ │
│ │ "A" │ │ "B" │ │
│ └─────────┘ └─────────┘ │
└─────────────────────────────────┘
Pro: Native logical isolation
Con: Per-provider limits
Multi-tenancy comparison
| Capability | ChromaDB | Pinecone | Weaviate | Qdrant | Milvus |
|---|---|---|---|---|---|
| Main strategy | Collections | Namespaces | Native tenants | Payload filter | Partitions |
| Data isolation | Medium | High | High | Medium-High | High |
| Tenant limit | ~100 collections | 10K namespaces | 100K+ tenants | No soft limit | 4096 partitions |
| Inactive tenant offload | ❌ | ❌ | ✅ (activity-based) | ❌ | ❌ |
| Tenant-level metrics | ❌ | ❌ | ✅ | ❌ | ❌ |
| Data leakage risk | Medium | Low | Low | Medium* | Low |
*Qdrant with a payload filter requires the developer to always include the tenant filter — if they forget, there's leakage.
Code: Multi-tenancy per provider
# Pinecone — native namespaces
index.upsert(
vectors=[{"id": "doc_1", "values": emb, "metadata": {"content": "..."}}],
namespace="tenant_acme" # isolation by namespace
)
results = index.query(
vector=query_emb, top_k=5,
namespace="tenant_acme" # searches only in this tenant
)
# Weaviate — native multi-tenancy (v1.20+)
import weaviate.classes as wvc
collection = client.collections.create(
name="Documents",
multi_tenancy_config=wvc.config.Configure.multi_tenancy(enabled=True),
vectorizer_config=wvc.config.Configure.Vectorizer.text2vec_openai()
)
collection.tenants.create([
wvc.tenants.Tenant(name="tenant_acme"),
wvc.tenants.Tenant(name="tenant_globex"),
])
tenant_collection = collection.with_tenant("tenant_acme")
tenant_collection.data.insert(properties={"content": "..."})
# Qdrant — payload filter (manual, no native namespace)
client.upsert(
collection_name="docs",
points=[PointStruct(
id=1, vector=emb,
payload={"tenant_id": "acme", "content": "..."}
)]
)
results = client.query_points(
collection_name="docs",
query=query_emb, limit=5,
query_filter=Filter(must=[
FieldCondition(key="tenant_id", match=MatchValue(value="acme"))
])
)
# ChromaDB — collection per tenant
tenant_collection = client.get_or_create_collection(name="tenant_acme")
tenant_collection.add(documents=["..."], ids=["doc_1"])
RAG insight: If you build RAG for SaaS (multiple customers), multi-tenancy is a security requirement, not a feature. Weaviate has the most mature implementation with offload of inactive tenants. Pinecone with namespaces is simple and secure. Qdrant requires developer discipline to never forget the filter.
4️⃣ Batch Operations
Why does it matter for RAG?
Ingesting documents in RAG is inherently a batch problem: you chunked 10,000 documents into 50,000 chunks → you need to insert 50,000 vectors. The speed and efficiency of batch operations directly affects the setup time and the cost of re-indexing.
Batch operations comparison
| Capability | ChromaDB | Pinecone | Weaviate | Qdrant | Milvus |
|---|---|---|---|---|---|
| Batch upsert | ✅ | ✅ | ✅ | ✅ | ✅ |
| Max batch size | ~41K* | 100 vectors | No limit** | No limit** | No limit** |
| Parallel upsert | Manual | ✅ Async | ✅ Batch API | ✅ Async | ✅ |
| Streaming ingestion | ❌ | ❌ | ❌ | ❌ | ✅ |
| Progress tracking | ❌ | ❌ | ❌ | ❌ | ❌ |
| Ingestion rate (1536-dim) | ~5K/s local | ~1K/s API | ~10K/s local | ~15K/s local | ~20K/s local |
*ChromaDB has a per-request size limit from SQLite. **Limited by server memory.
Code: Efficient batch ingestion
# ChromaDB — batch with chunks
def batch_insert_chromadb(collection, documents, batch_size=1000):
for i in range(0, len(documents), batch_size):
batch = documents[i:i + batch_size]
collection.add(
documents=[d["text"] for d in batch],
metadatas=[d["metadata"] for d in batch],
ids=[d["id"] for d in batch]
)
print(f"Inserted {min(i + batch_size, len(documents))}/{len(documents)}")
# Pinecone — batch with a limit of 100
def batch_insert_pinecone(index, vectors, batch_size=100):
for i in range(0, len(vectors), batch_size):
batch = vectors[i:i + batch_size]
index.upsert(vectors=batch)
# Qdrant — batch with upload_points
def batch_insert_qdrant(client, collection_name, points, batch_size=1000):
client.upload_points(
collection_name=collection_name,
points=points,
batch_size=batch_size,
parallel=4 # concurrent threads
)
# Milvus — bulk insert
def batch_insert_milvus(client, collection_name, data, batch_size=5000):
for i in range(0, len(data), batch_size):
batch = data[i:i + batch_size]
client.insert(collection_name=collection_name, data=batch)
RAG insight: Pinecone has the most restrictive batch size (100 vectors per request), which makes the initial ingestion slower via API. For massive loads (> 100K docs), Qdrant and Milvus self-hosted are significantly faster. ChromaDB is efficient for moderate sizes.
5️⃣ SDKs and Developer Experience
Why does it matter for RAG?
The quality of the SDK determines how much time your team spends integrating the vector DB with your RAG pipeline. A poorly designed SDK generates bugs, frustration, and time lost debugging.
SDK comparison
| Capability | ChromaDB | Pinecone | Weaviate | Qdrant | Milvus |
|---|---|---|---|---|---|
| Python SDK | ✅ Excellent | ✅ Excellent | ✅ Good (v4) | ✅ Excellent | ✅ Good |
| TypeScript SDK | ✅ | ✅ | ✅ | ✅ | ✅ |
| Go SDK | ❌ | ✅ | ✅ | ✅ | ✅ |
| Rust SDK | ❌ | ✅ | ❌ | ✅ (official) | ❌ |
| Java SDK | ❌ | ✅ | ✅ | ✅ | ✅ |
| Async support | ❌ | ✅ | ✅ | ✅ | ✅ |
| Type hints (Python) | Partial | ✅ | ✅ | ✅ Complete | Partial |
| Auto-embedding | ✅ | ❌ | ✅ (modules) | ❌ | ❌ |
| LangChain integration | ✅ | ✅ | ✅ | ✅ | ✅ |
| LlamaIndex integration | ✅ | ✅ | ✅ | ✅ | ✅ |
| SDK documentation | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
RAG insight: ChromaDB wins on absolute simplicity (auto-embedding, 5 lines to get working). Qdrant has the best documentation and type hints. Weaviate requires more boilerplate but offers more power (schema, modules). For teams that use LangChain/LlamaIndex, the SDK difference matters less because the framework abstracts the API. The detailed comparison of code patterns per provider is in capsule 02.
6️⃣ Scaling
Why does it matter for RAG?
Your RAG starts with 10K documents in the MVP. In 6 months it has 500K. In a year, 5M. The ability to scale the vector DB without re-architecting the whole system is critical.
Scaling comparison
| Capability | ChromaDB | Pinecone | Weaviate | Qdrant | Milvus |
|---|---|---|---|---|---|
| Max recommended scale | ~1M | 100M+ | 100M+ | 100M+ | Billions |
| Automatic sharding | ❌ | ✅ | ✅ | Manual | ✅ |
| Replication | ❌ | ✅ (multi-AZ) | ✅ | ✅ (Raft) | ✅ |
| Horizontal scaling | ❌ | ✅ (auto) | ✅ | ✅ | ✅ |
| Quantization | ❌ | ❌ | PQ | Scalar/PQ/Binary | SQ8/PQ |
| On-disk index | ❌ | N/A (cloud) | ✅ | ✅ (mmap) | ✅ (DiskANN) |
| GPU acceleration | ❌ | ❌ | ❌ | ❌ | ✅ |
| Segment management | ❌ | Auto | Auto | Manual/Auto | Auto |
Impact of quantization on costs
| Type | Example (10M × 1536-dim) | Memory | Savings | Accuracy loss |
|---|---|---|---|---|
| No quantization (float32) | 57.3 GB | — | — | — |
| Scalar (Qdrant) | 14.3 GB | 75% | < 1% | Recommended |
| Product (Weaviate/Milvus) | ~1.8 GB | 96% | 2-5% | Acceptable |
| Binary (Qdrant) | ~1.8 GB | 96% | 3-7% | Requires rescoring |
RAG insight: If your RAG grows to > 5M vectors, quantization becomes the most important cost factor. Qdrant leads with 3 types of quantization. Milvus scales further with GPU and DiskANN. ChromaDB is not an option at this scale.
📊 Master Table: Feature Comparison for RAG
| Feature | ChromaDB | Pinecone | Weaviate | Qdrant | Milvus |
|---|---|---|---|---|---|
| Metadata filtering | ✅ Basic | ✅ Good | ✅ Advanced | ✅ Excellent | ✅ Good |
| Hybrid search | ❌ | ⚠️ Sparse | ✅ Native | ✅ Sparse | ✅ |
| Multi-tenancy | ⚠️ Collections | ✅ Namespaces | ✅ Native | ⚠️ Payload | ✅ Partitions |
| Batch operations | ✅ | ⚠️ Slow | ✅ | ✅ Fast | ✅ Fast |
| SDK quality | ✅ Simple | ✅ Good | ✅ Good | ✅ Excellent | ✅ Good |
| Scaling | ❌ Limited | ✅ Auto | ✅ | ✅ | ✅ Enterprise |
| Quantization | ❌ | ❌ | ✅ PQ | ✅ 3 types | ✅ SQ8/PQ |
| Observability | ❌ | ✅ Console | ✅ Metrics | ✅ Dashboard | ✅ Attu |
Quick reading of the table
- ChromaDB: Excellent to start, limited for production.
- Pinecone: Solid across the board except hybrid search and quantization.
- Weaviate: The most complete in features (native hybrid, multi-tenancy, PQ).
- Qdrant: Best in filtering and quantization, hybrid search improving.
- Milvus: The most scalable, but higher operational complexity.
🔍 How to Use This Comparison Without Bias
3-step process
Step 1: Mark 2-3 "non-negotiable" features for your RAG.
non_negotiables = {
"saas_multitenant": ["multi_tenancy"],
"documentation_rag": ["hybrid_search", "metadata_filtering"],
"internal_search": ["scaling", "batch_operations"],
"prototype": [] # for prototypes, no feature is a blocker
}
my_case = "documentation_rag"
print(f"Non-negotiable features: {non_negotiables[my_case]}")
# → Non-negotiable features: ['hybrid_search', 'metadata_filtering']
Step 2: Eliminate options that don't cover the minimums.
Case: documentation_rag (hybrid search + metadata filtering)
ChromaDB: ❌ No hybrid search → ELIMINATED
Pinecone: ⚠️ Hybrid via sparse vectors (requires extra work)
Weaviate: ✅ Native hybrid + advanced filtering
Qdrant: ✅ Sparse vectors + excellent filtering
Milvus: ✅ Hybrid + good filtering
Candidates: Weaviate, Qdrant, Milvus (Pinecone with reservations)
Step 3: Only then compare cost and operations among the remaining candidates.
🔧 Evaluation troubleshooting
1. "Everyone says they support everything"
Cause: Marketing vs reality. "Supports metadata filtering" can mean basic equality filters or a complete system with geo-filtering and nested objects.
Solution: For each "non-negotiable" feature, look for:
- Official documentation with code examples
- Documented limits (max filters per query, max tenants, etc.)
- Open GitHub issues related to that feature
- Independent benchmarks (not from the provider itself)
2. "I don't know how much hybrid search weighs in my case"
Cause: You haven't measured the distribution of query types.
Solution: Analyze 100 real queries from your system (or projected):
- Count how many contain proper names, IDs, or exact terms
- If > 20% are "specific", hybrid search will improve your accuracy significantly
- If < 5% are specific, pure semantic is enough
3. "Multi-tenancy seems like overkill"
Cause: You only have one tenant today — but what about in 6 months?
Solution: If your product is B2B or SaaS, multi-tenancy is not optional. Implementing it later is a costly migration. If your product is B2C (a single tenant), metadata filtering with user_id is enough and formal multi-tenancy is unnecessary.
4. "How much accuracy do I lose with quantization?"
Cause: Fear of degrading the RAG's quality by compressing vectors.
Solution: The typical loss with Scalar Quantization is < 1% in recall@10. With Product Quantization it's 2-5%. For RAG, this loss is generally acceptable. Run an A/B test: compare recall of 100 queries with and without quantization before deciding.
✏️ Exercises
Exercise 1: Non-negotiable features
For your RAG project (real or hypothetical), define:
- The 3 most important features from the list of 6.
- The minimum acceptable level for each one.
- Which providers survive the filter.
Reference solution (case: RAG for SaaS technical support)
-
Non-negotiable features:
- Multi-tenancy (data isolation per customer)
- Hybrid search (queries with ticket IDs + conceptual)
- Metadata filtering (filter by product, version, language)
-
Minimum level:
- Multi-tenancy: native isolation (not just a metadata filter)
- Hybrid search: built-in or sparse vectors
- Metadata filtering: AND/OR + ranges
-
Providers that survive:
- ✅ Weaviate (native multi-tenancy, native hybrid, advanced filtering)
- ⚠️ Pinecone (good namespaces, hybrid via sparse, good filtering)
- ⚠️ Milvus (partitions, hybrid, good filtering)
- ❌ ChromaDB (limited multi-tenancy, no hybrid)
- ⚠️ Qdrant (multi-tenancy via payload filter, sparse vectors)
Exercise 2: Custom scoring table
Assign a score (1-5) to each provider for the 6 features, weighting by importance for your case:
| Feature | Weight | ChromaDB | Pinecone | Weaviate | Qdrant | Milvus |
|---|---|---|---|---|---|---|
| Metadata filtering | ||||||
| Hybrid search | ||||||
| Multi-tenancy | ||||||
| Batch operations | ||||||
| SDK quality | ||||||
| Scaling | ||||||
| Weighted total |
Reference solution (case: internal documentation RAG, small team)
| Feature | Weight | ChromaDB | Pinecone | Weaviate | Qdrant | Milvus |
|---|---|---|---|---|---|---|
| Metadata filtering | 4 | 3 (12) | 4 (16) | 5 (20) | 5 (20) | 4 (16) |
| Hybrid search | 5 | 1 (5) | 3 (15) | 5 (25) | 4 (20) | 4 (20) |
| Multi-tenancy | 1 | 2 (2) | 4 (4) | 5 (5) | 3 (3) | 4 (4) |
| Batch operations | 3 | 3 (9) | 2 (6) | 4 (12) | 5 (15) | 5 (15) |
| SDK quality | 3 | 5 (15) | 4 (12) | 3 (9) | 5 (15) | 3 (9) |
| Scaling | 2 | 1 (2) | 5 (10) | 4 (8) | 4 (8) | 5 (10) |
| Weighted total | 45 | 63 | 79 | 81 | 74 |
Result: Qdrant and Weaviate lead. For a small team without a need for multi-tenancy, Qdrant wins on filtering + SDK + batch.
Exercise 3: Implement advanced filtering
Using ChromaDB, implement a query that:
- Searches for documents about "performance optimization"
- Filters by
version >= 3ANDlanguage = "es" - Returns the 3 most relevant results with scores
Solution
import chromadb
client = chromadb.PersistentClient(path="./exercise_filter")
collection = client.get_or_create_collection(
name="docs",
metadata={"hnsw:space": "cosine"}
)
collection.add(
documents=[
"Query optimization in databases",
"Performance improvement in REST APIs",
"Performance tuning for distributed systems",
"CSS guide for designers",
"How to optimize performance in Python v2",
],
metadatas=[
{"version": 3, "language": "es"},
{"version": 4, "language": "es"},
{"version": 3, "language": "en"},
{"version": 2, "language": "es"},
{"version": 2, "language": "es"},
],
ids=["d1", "d2", "d3", "d4", "d5"]
)
results = collection.query(
query_texts=["performance optimization"],
n_results=3,
where={
"$and": [
{"version": {"$gte": 3}},
{"language": {"$eq": "es"}}
]
}
)
for doc, dist, meta in zip(
results["documents"][0],
results["distances"][0],
results["metadatas"][0]
):
score = 1 - dist
print(f"Score: {score:.4f} | v{meta['version']} | {doc}")
Expected result: Only d1 and d2 pass the filter (version >= 3 AND language = "es"). d3 fails by language, d4 and d5 by version.
Exercise 4: Design a multi-tenant strategy
Your company has 3 customers (Acme, Globex, Initech) with separate documents. Design the multi-tenancy strategy for:
- Scenario A: ChromaDB (development)
- Scenario B: Pinecone (production)
For each one, write the insert and query code that guarantees isolation.
Solution
# Scenario A: ChromaDB — collection per tenant
import chromadb
client = chromadb.PersistentClient(path="./multi_tenant")
tenants = ["acme", "globex", "initech"]
for tenant in tenants:
col = client.get_or_create_collection(name=f"docs_{tenant}")
col.add(
documents=[f"Internal document from {tenant}"],
ids=[f"{tenant}_doc_1"]
)
acme_results = client.get_collection("docs_acme").query(
query_texts=["internal document"], n_results=5
)
# Returns only Acme's docs — isolation by collection
# Scenario B: Pinecone — namespace per tenant
from pinecone import Pinecone
pc = Pinecone(api_key="...")
index = pc.Index("production-rag")
for tenant in ["acme", "globex", "initech"]:
index.upsert(
vectors=[{"id": f"{tenant}_1", "values": embedding, "metadata": {"text": "..."}}],
namespace=tenant
)
results = index.query(
vector=query_embedding,
top_k=5,
namespace="acme" # isolation by namespace
)
# Returns only the "acme" namespace's docs
Key difference: ChromaDB creates separate collections (more management overhead). Pinecone uses namespaces within the same index (more efficient, with native isolation).
Exercise 5: Feature decision matrix
Given the following case, recommend a provider with justification:
Case: An e-commerce company. 5M products. Queries with an exact product name + conceptual ("comfortable shoes for running"). 200 brands (multi-tenant per brand). A team of 10 with 2 DevOps. Medium budget.
Solution
Analysis of non-negotiable features:
- Hybrid search — Mandatory (exact name + conceptual) → Eliminates ChromaDB
- Multi-tenancy — 200 brands → Needs robust support → Eliminates Qdrant (payload filter to 200 tenants is viable but not ideal)
- Scaling — 5M vectors → All except ChromaDB
Final candidates: Weaviate, Pinecone, Milvus
Scoring:
| Feature | Weaviate | Pinecone | Milvus |
|---|---|---|---|
| Hybrid search | ✅ Native (alpha) | ⚠️ Sparse vectors | ✅ |
| Multi-tenancy 200 brands | ✅ Native (offload) | ✅ Namespaces | ✅ Partitions |
| Scaling 5M | ✅ | ✅ | ✅ (overkill) |
| DevOps team | ✅ Self-hosted viable | ✅ Zero ops | ⚠️ Complex |
Recommendation: Weaviate — native hybrid search is the main differentiator for e-commerce. Multi-tenancy with offload of inactive brands reduces costs. The team has 2 DevOps who can operate self-hosted.
Alternative: Pinecone if the team prefers zero ops, accepting that hybrid search requires more work with sparse vectors.
🔗 Connection with the project (Decision Tree)
This capsule gives you the technical criteria for the decision nodes of your decision tree:
Do you need hybrid search?
├── YES → Weaviate (native) or Qdrant/Milvus (sparse)
└── NO → Anything works
│
Do you need native multi-tenancy?
├── YES → Weaviate or Pinecone
└── NO → Qdrant or ChromaDB (if scale < 1M)
Combine these criteria with those of managed vs self-hosted (previous capsule) and costs (next capsule) to complete your decision tree.
📝 Summary
- Metadata filtering is universal, but the depth varies: Qdrant and Weaviate lead with payload indexes and geo-filtering.
- Hybrid search is essential for RAG with specific queries — Weaviate is the most mature option with native RRF fusion.
- Multi-tenancy is a security requirement in SaaS — Weaviate has the most complete implementation with tenant offload.
- Batch operations matter for massive ingestion — Pinecone is the slowest (100 vectors/request), Qdrant and Milvus the fastest.
- SDK quality affects development speed — ChromaDB wins on simplicity, Qdrant on type safety and documentation.
- Scaling and quantization determine long-term cost — Qdrant leads in quantization, Milvus in absolute scale.
- No provider wins in all categories; define your "non-negotiables" first and compare afterward.
📚 Additional resources
- ChromaDB Filtering Docs — Where filters syntax
- Pinecone Metadata Filtering — Filter operators
- Weaviate Hybrid Search — Alpha, fusion, BM25
- Qdrant Filtering — Payload conditions
- Qdrant Quantization Guide — Scalar, PQ, Binary
- Milvus Multi-tenancy — Partition-based isolation
- ANN Benchmarks — Independent performance comparison
- VectorHub (Superlinked) — Educational resources on vector search
Reading time: 25-30 minutes Next: 05-costs-and-tradeoffs.md