Danh sách bài viết

Bài 16: Qdrant và Weaviate — alternatives open source

Khi không muốn lock-in Pinecone hoặc cần self-host hoàn toàn, Qdrant và Weaviate là hai lựa chọn open source trưởng thành. Bài này hướng dẫn thực hành cả hai: cài Docker/embedded, tạo collection, upsert, query với filter (Qdrant), hybrid search BM25 + vector (Weaviate), payload index, và bảng so sánh 4 vector DB để chọn đúng tool cho đúng bài toán.

27/05/2026
1 lượt xem
1

Mục tiêu bài học

Sau bài này bạn sẽ:

  • Hiểu Qdrant và Weaviate khác nhau ở đâu về kiến trúc và use case.
  • Chạy được cả hai qua Docker hoặc embedded mode (không cần cloud account).
  • Tạo collection, upsert vector, query với filter trên Qdrant.
  • Tạo collection, insert object, thực hiện hybrid search trên Weaviate v4 SDK.
  • Biết khi nào chọn ChromaDB, Pinecone, Qdrant hay Weaviate.

Phiên bản tham chiếu: qdrant-client 1.10+, weaviate-client 4.x (v4 SDK, Python 3.8+), Weaviate server 1.27.x.

2

Qdrant là gì

Qdrant là vector database open source viết bằng Rust, license Apache 2.0. Repo: qdrant/qdrant trên GitHub.

Đặc điểm kỹ thuật nổi bật:

  • Payload indexes: index trên metadata field để filter không còn là linear scan — đây là điểm khác biệt rõ nhất so với ChromaDB khi dataset lớn.
  • Scalar quantization built-in: nén vector INT8 hoặc binary trực tiếp trong storage mà không cần bước preprocessing riêng.
  • gRPC + REST: hai giao thức song song; gRPC thấp latency hơn khi upsert bulk.
  • Named vectors: 1 point có thể lưu nhiều vector cùng lúc (ví dụ: dense + sparse), hữu ích cho hybrid search tự triển khai.

Khi nên dùng Qdrant:

  • Self-host trên VPS, bare metal, hoặc Kubernetes — kiểm soát hoàn toàn data.
  • Cần filter metadata phức tạp (AND/OR/NOT, range, geo) với hiệu năng tốt ở scale vài chục triệu vector.
  • Muốn scalar/binary quantization mà không tùy chỉnh nhiều.

Qdrant cũng có managed cloud (Qdrant Cloud, free tier 1GB), nhưng điểm mạnh thực sự là self-host.

3

Cài đặt và khởi động Qdrant

Cách 1: Docker (server mode)

# Port 6333 = REST API, port 6334 = gRPC
docker run -p 6333:6333 -p 6334:6334 \
  -v $(pwd)/qdrant_storage:/qdrant/storage \
  qdrant/qdrant

Sau khi chạy, dashboard có tại http://localhost:6333/dashboard.

Cách 2: Embedded mode (không cần server)

pip install qdrant-client
from qdrant_client import QdrantClient

# Lưu data vào thư mục local, không cần server
client = QdrantClient(path="./qdrant_local")

Embedded mode dùng tốt cho prototype và unit test — tương đương SQLite trong world SQL. Dữ liệu persist qua các lần chạy.

Cách 3: Connect server

from qdrant_client import QdrantClient

client = QdrantClient(host="localhost", port=6333)

# Kiểm tra kết nối
print(client.get_collections())
4

Tạo collection

Qdrant gọi mỗi "bảng" vector là collection. Schema vector được định nghĩa khi tạo — không thể thay đổi dimension sau khi collection đã có data.

from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance

client = QdrantClient(host="localhost", port=6333)

client.create_collection(
    collection_name="ai-docs",
    vectors_config=VectorParams(
        size=384,               # phải khớp với embedding model
        distance=Distance.COSINE,
    ),
)

Các giá trị Distance: COSINE, DOT, EUCLID, MANHATTAN.

Kiểm tra collection đã tạo:

info = client.get_collection("ai-docs")
print(info.config.params.vectors)
# VectorParams(size=384, distance=Distance.COSINE, ...)
5

Upsert points

Đơn vị dữ liệu trong Qdrant là point — gồm id (int hoặc UUID), vector, và payload (dict tùy ý).

from qdrant_client.models import PointStruct
import numpy as np

# Giả sử đã có embedding model, tạo vector giả cho ví dụ
def fake_embed(text: str, dim: int = 384) -> list[float]:
    rng = np.random.default_rng(abs(hash(text)) % (2**31))
    vec = rng.random(dim).astype(np.float32)
    return (vec / np.linalg.norm(vec)).tolist()

docs = [
    {"id": 1, "text": "FastAPI async endpoints", "source": "doc1.pdf", "page": 3},
    {"id": 2, "text": "Docker multi-stage build", "source": "doc2.pdf", "page": 7},
    {"id": 3, "text": "RAG pipeline overview",    "source": "doc1.pdf", "page": 12},
    {"id": 4, "text": "Qdrant payload filters",   "source": "doc3.pdf", "page": 1},
]

points = [
    PointStruct(
        id=doc["id"],
        vector=fake_embed(doc["text"]),
        payload={
            "text":   doc["text"],
            "source": doc["source"],
            "page":   doc["page"],
        },
    )
    for doc in docs
]

operation_info = client.upsert(
    collection_name="ai-docs",
    points=points,
)
print(operation_info.status)  # UpdateStatus.COMPLETED

Lưu ý: upsert — nếu id đã tồn tại thì cập nhật, chưa tồn tại thì insert. Không có insert riêng.

6

Query với filter

Query thuần vector (không filter)

query_vec = fake_embed("deployment pipeline")

results = client.query_points(
    collection_name="ai-docs",
    query=query_vec,
    limit=3,
).points

for r in results:
    print(r.id, r.score, r.payload["text"])

Query với filter (qdrant-client 1.7+)

from qdrant_client.models import Filter, FieldCondition, MatchValue

results = client.query_points(
    collection_name="ai-docs",
    query=query_vec,
    query_filter=Filter(
        must=[
            FieldCondition(key="source", match=MatchValue(value="doc1.pdf"))
        ]
    ),
    limit=5,
).points

for r in results:
    print(r.id, r.score, r.payload)

Cấu trúc filter:

  • must — AND logic: tất cả điều kiện phải đúng.
  • should — OR logic: ít nhất một điều kiện đúng.
  • must_not — NOT logic: loại bỏ các point thỏa điều kiện.

Các loại condition:

  • MatchValue(value=...) — so sánh bằng (keyword, int, bool).
  • MatchText(text=...) — full-text match (cần text index).
  • Range(gte=..., lte=...) — numeric range.
  • IsEmpty() — payload field là null/không tồn tại.
  • GeoBoundingBox(top_left=..., bottom_right=...) — geo filter.

Ví dụ kết hợp AND + range:

from qdrant_client.models import Range

results = client.query_points(
    collection_name="ai-docs",
    query=query_vec,
    query_filter=Filter(
        must=[
            FieldCondition(key="source", match=MatchValue(value="doc1.pdf")),
            FieldCondition(key="page", range=Range(gte=1, lte=10)),
        ]
    ),
    limit=5,
).points
7

Payload indexing

Vấn đề: Mặc định Qdrant lưu payload như một JSON blob. Khi filter, server phải đọc từng payload để kiểm tra điều kiện — tức là O(N) linear scan. Với 1M+ vector, filter theo source có thể mất vài giây.

Giải pháp: Tạo payload index cho field thường xuyên dùng trong filter.

from qdrant_client.models import PayloadSchemaType

# Index keyword cho field "source"
client.create_payload_index(
    collection_name="ai-docs",
    field_name="source",
    field_schema=PayloadSchemaType.KEYWORD,
)

# Index integer cho field "page"
client.create_payload_index(
    collection_name="ai-docs",
    field_name="page",
    field_schema=PayloadSchemaType.INTEGER,
)

Sau khi tạo index, filter trên các field này chạy O(log N) thay vì O(N).

Các field_schema hợp lệ: KEYWORD, INTEGER, FLOAT, BOOL, GEO, TEXT (full-text), DATETIME.

Rule of thumb: Field nào xuất hiện trong must/should của query với dataset > 100k vector thì nên index.

8

Weaviate là gì

Weaviate là vector database open source viết bằng Go, license BSD-3-Clause. Repo: weaviate/weaviate.

Đặc điểm kỹ thuật nổi bật:

  • Hybrid search tích hợp sẵn: kết hợp BM25 (keyword) và dense vector trong một API call, không cần triển khai riêng.
  • Module system: text2vec-openai, text2vec-cohere, text2vec-transformers, generative-openai — gắn trực tiếp vào schema, vectorization tự động khi insert.
  • Multi-tenancy native: 1 collection có thể tách data thành nhiều tenant riêng biệt — tốt cho SaaS.
  • GraphQL + REST + gRPC: ba giao thức; v4 SDK Python wrap gRPC mặc định.

Khi nên dùng Weaviate:

  • Cần hybrid search (BM25 + vector) mà không muốn tự implement.
  • Dùng module ecosystem: tự động embed khi insert, gọi LLM trả kết quả generative.
  • Multi-tenancy: nhiều khách hàng dùng chung cluster, dữ liệu cách ly.
  • GraphQL là ngôn ngữ query quen thuộc của team.
9

Cài đặt và khởi động Weaviate

Cách 1: Docker (server mode)

# Port 8080 = REST/GraphQL, port 50051 = gRPC
docker run -p 8080:8080 -p 50051:50051 \
  -e AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true \
  -e PERSISTENCE_DATA_PATH=/var/lib/weaviate \
  cr.weaviate.io/semitechnologies/weaviate:1.27.0

Lưu ý: Weaviate 1.23+ yêu cầu expose cả port gRPC 50051. Nếu chỉ mở 8080 thì v4 SDK Python sẽ báo lỗi kết nối.

Cách 2: Embedded mode

pip install weaviate-client
import weaviate

# Embedded: khởi động Weaviate server nhúng ngay trong process Python
client = weaviate.connect_to_embedded()
print(client.is_ready())  # True

Embedded mode tự tải binary Weaviate nếu chưa có, lưu vào ~/.local/share/weaviate. Phù hợp cho local dev, không dùng production.

Cách 3: Connect server

import weaviate

client = weaviate.connect_to_local(host="localhost", port=8080, grpc_port=50051)
print(client.is_ready())

Nhớ đóng connection khi xong:

client.close()

# Hoặc dùng context manager:
with weaviate.connect_to_local() as client:
    # ... làm gì đó
    pass
10

Tạo collection (v4 SDK)

Quan trọng: Weaviate v4 SDK (weaviate-client >= 4.0) có API hoàn toàn khác v3. Nếu tìm trên Stack Overflow và thấy client.schema.create_class() hay client.data_object.create() — đó là v3, không dùng nữa. Bài này chỉ dùng v4.

Tạo collection không dùng vectorizer module (tự cung cấp vector)

import weaviate
from weaviate.classes.config import Configure, Property, DataType

client = weaviate.connect_to_local()

# Tên collection phải bắt đầu bằng chữ hoa (PascalCase)
client.collections.create(
    name="AiDocs",
    properties=[
        Property(name="text",   data_type=DataType.TEXT),
        Property(name="source", data_type=DataType.TEXT),
        Property(name="page",   data_type=DataType.INT),
    ],
    vectorizer_config=Configure.Vectorizer.none(),  # tự cung cấp vector
)
print("Collection created")

Tạo collection với vectorizer module (tự động embed khi insert)

import os

client.collections.create(
    name="AiDocsAuto",
    properties=[
        Property(name="text",   data_type=DataType.TEXT),
        Property(name="source", data_type=DataType.TEXT),
    ],
    vectorizer_config=Configure.Vectorizer.text2vec_openai(
        model="text-embedding-3-small",
    ),
    # Cần OPENAI_APIKEY env khi server chạy
)

Khi dùng module, không cần truyền vector khi insert — Weaviate tự gọi embedding API.

Liệt kê và xóa collection

# Liệt kê
for name, col in client.collections.list_all().items():
    print(name)

# Xóa
client.collections.delete("AiDocs")
11

Insert objects

Insert đơn lẻ

collection = client.collections.get("AiDocs")

uuid = collection.data.insert(
    properties={
        "text":   "FastAPI async endpoints for AI inference",
        "source": "doc1.pdf",
        "page":   3,
    },
    vector=fake_embed("FastAPI async endpoints for AI inference"),
)
print(uuid)  # UUID của object vừa tạo

Insert batch (hiệu quả hơn cho nhiều object)

from weaviate.classes.data import DataObject

docs = [
    {"text": "FastAPI async endpoints",  "source": "doc1.pdf", "page": 3},
    {"text": "Docker multi-stage build", "source": "doc2.pdf", "page": 7},
    {"text": "RAG pipeline overview",    "source": "doc1.pdf", "page": 12},
    {"text": "Weaviate hybrid search",   "source": "doc3.pdf", "page": 1},
]

objects = [
    DataObject(
        properties={k: v for k, v in d.items()},
        vector=fake_embed(d["text"]),
    )
    for d in docs
]

result = collection.data.insert_many(objects)
print(f"Inserted: {len(result.uuids)}, Errors: {len(result.errors)}")

insert_many tự chia batch 1000 object — không cần tự chia thủ công.

12

Query: vector, BM25, hybrid

Pure vector (near_vector)

from weaviate.classes.query import MetadataQuery

collection = client.collections.get("AiDocs")

results = collection.query.near_vector(
    near_vector=fake_embed("deployment pipeline"),
    limit=3,
    return_metadata=MetadataQuery(score=True, distance=True),
)

for obj in results.objects:
    print(obj.metadata.score, obj.properties["text"])

BM25 keyword

results = collection.query.bm25(
    query="fastapi async",
    limit=3,
    return_metadata=MetadataQuery(score=True),
)

for obj in results.objects:
    print(obj.metadata.score, obj.properties["text"])

BM25 tìm theo exact/stemmed keyword. Tốt cho tên riêng, từ viết tắt, code snippet.

Hybrid

results = collection.query.hybrid(
    query="fastapi async",          # text dùng cho BM25 và/hoặc vectorize
    vector=fake_embed("fastapi async"),  # nếu không truyền, Weaviate dùng text2vec module
    alpha=0.5,                      # 0.0 = 100% BM25, 1.0 = 100% vector
    limit=5,
    return_metadata=MetadataQuery(score=True),
)

for obj in results.objects:
    print(obj.metadata.score, obj.properties["text"])

Lưu ý: Nếu collection dùng Vectorizer.none(), phải truyền vector= trong hybrid query. Nếu collection dùng module, chỉ truyền query= là đủ.

Filter kết hợp với query

from weaviate.classes.query import Filter

results = collection.query.hybrid(
    query="async",
    vector=fake_embed("async"),
    alpha=0.6,
    filters=Filter.by_property("source").equal("doc1.pdf"),
    limit=5,
)
13

Hybrid search — cơ chế và tham số

Hybrid search kết hợp điểm BM25 và điểm cosine similarity qua công thức Reciprocal Rank Fusion (RRF) mặc định, hoặc weighted linear combination nếu cấu hình.

Cơ chế mặc định — RRF:

score_hybrid = 1/(k + rank_bm25) + 1/(k + rank_vector)
với k = 60 (Weaviate default)

Tham số alpha ảnh hưởng đến weight:

  • alpha=0.0 — 100% BM25, vector bị bỏ qua.
  • alpha=1.0 — 100% vector, BM25 bị bỏ qua.
  • alpha=0.5 — cân bằng, thường là điểm khởi đầu tốt.

Khi nào hybrid tốt hơn thuần vector:

  • Query chứa keyword cụ thể: tên thư viện (langchain), số version (v4.2), mã lỗi (ECONNREFUSED) — vector thường "nhòa" semantic, BM25 tìm exact tốt hơn.
  • Domain đặc thù (y tế, luật, code) — từ chuyên ngành không phổ biến trong training data của embedding model.

Khi nào thuần vector đủ:

  • Câu query tự nhiên, ngữ nghĩa rõ ràng.
  • Dataset đồng nhất, ít keyword kỹ thuật.
14

So sánh 4 vector DB

Bảng tóm tắt các tiêu chí kỹ thuật — thông tin tính đến Q1 2026:

Tiêu chí ChromaDB Pinecone Qdrant Weaviate
License Apache 2.0 Proprietary SaaS Apache 2.0 BSD-3-Clause
Self-host Không Có (chính)
Managed cloud Không Có (chính) Có (Qdrant Cloud) Có (WCS)
Hybrid search built-in Không Có (sparse+dense) Partial (sparse vectors) Có (BM25 + dense)
Quantization Không Không expose Scalar, Binary, Product SQ, PQ (1.23+)
Multi-tenancy Manual (collection-per-tenant) Namespace Collections + shard key Native (tenant API)
API Python SDK REST, gRPC REST, gRPC GraphQL, REST, gRPC
Scale điển hình < 1M vector 100M+ 10M–100M+ 10M–100M+

Chú thích:

  • Pinecone hỗ trợ sparse+dense hybrid nhưng cần upload sparse vector thủ công (SPLADE embedding) — không built-in như Weaviate BM25.
  • Qdrant hỗ trợ hybrid qua named vectors (dense + sparse field), nhưng cần tự tính sparse vector; ít turnkey hơn Weaviate.
  • Số "scale điển hình" là tham khảo dựa trên benchmark cộng đồng — phụ thuộc nhiều vào hardware và cấu hình.
15

Khi nào chọn gì

  • ChromaDB: prototype nhanh, local dev, dataset < 1M vector, không cần ops.
  • Pinecone: production không muốn quản lý infrastructure, scale tự động, chấp nhận vendor lock-in và chi phí SaaS.
  • Qdrant: self-host hoàn toàn, cần filter metadata phức tạp với hiệu năng tốt, dataset trung-lớn, muốn quantization linh hoạt.
  • Weaviate: cần hybrid search turnkey (BM25 + vector), GraphQL ecosystem, multi-tenancy native, module tự động embed hoặc gọi LLM.
16

Common pitfalls

Qdrant

  • Quên tạo payload index: Filter hoạt động ngay cả không có index, nhưng với dataset lớn sẽ chậm do linear scan. Tạo index sau khi load data ban đầu vẫn được — Qdrant sẽ build index background.
  • Dimension mismatch: Collection đã tạo với size=384 không thể upsert vector 768 chiều. Phải xóa collection và tạo lại — không có cách migrate schema.
  • ID phải unique và không đổi: Nếu dùng integer ID mà trùng, upsert sẽ overwrite point cũ mà không báo lỗi.

Weaviate

  • v3 vs v4 SDK syntax hoàn toàn khác: client.schema.create_class(), client.data_object.create(), client.query.get() đều là v3. v4 dùng client.collections.create(), collection.data.insert(), collection.query.near_vector(). Luôn kiểm tra pip show weaviate-client để xác nhận version.
  • Tên collection phải PascalCase: "ai-docs" hay "ai_docs" sẽ bị Weaviate từ chối — phải là "AiDocs".
  • Quên expose port gRPC 50051: v4 SDK dùng gRPC mặc định. Chạy Docker không mở port 50051 → connect_to_local() raise timeout ngay lập tức.
  • Dimension mismatch: Tương tự Qdrant — schema lock khi collection đã có data; phải xóa và tạo lại.
17

Bài tiếp theo

Bài 17: Index types: HNSW vs IVF vs Flat — tại sao vector search không dùng brute-force, HNSW hoạt động như thế nào, khi nào dùng Flat hay IVF, và cách cấu hình index parameter trong từng DB.