0

Qdrant từ cơ bản đến nâng cao: Vector Search, Hybrid Search và tối ưu hiệu năng

1. Tổng quan

Gần đây mình có dịp học và thực hành với Qdrant, một vector database khá nổi trong mảng AI/LLM. Bài này mình viết lại những gì đã học được, theo đúng trình tự lúc mình tìm hiểu: từ khái niệm cơ bản, đến cách đánh index, rồi hybrid search, và cuối cùng là các kỹ thuật tối ưu khi dữ liệu lớn dần lên.

Trước tiên nói qua một chút vì sao vector database lại được nhắc nhiều thời gian gần đây. Các mô hình embedding (kiểu OpenAI, Cohere, hay sentence-transformers) có thể biến văn bản, hình ảnh thành các vector số. Hai đoạn văn có ý nghĩa gần nhau thì vector của chúng cũng "gần" nhau trong không gian nhiều chiều. Vấn đề là khi có hàng triệu vector, làm sao tìm ra những vector gần nhau nhanh mà không phải so sánh từng cái một? Đó là lý do các vector database ra đời.

Qdrant là một trong số đó, viết bằng Rust nên chạy khá nhanh và ổn định. So với vài cái tên quen thuộc khác: Pinecone là SaaS, dễ dùng nhưng ít tùy biến; Weaviate mạnh về module hóa; Milvus scale tốt nhưng vận hành hơi phức tạp; còn pgvector thì tiện vì gắn thẳng vào Postgres nhưng yếu hơn khi data lớn. Điểm mình thấy Qdrant ổn là filtering theo metadata được tích hợp thẳng vào quá trình tìm kiếm vector, và có sẵn cơ chế nén dữ liệu (quantization) khá hữu ích khi cần tiết kiệm chi phí.

2. Nền tảng vector search

2.1. Points, Vectors và Payloads

Trong Qdrant, đơn vị dữ liệu nhỏ nhất gọi là Point. Một Point gồm 3 phần:

  • ID: định danh, có thể là số hoặc UUID
  • Vector: mảng số thực, ví dụ [0.12, -0.45, 0.88, ...]
  • Payload: dữ liệu đi kèm dạng JSON, kiểu {"title": "Inception", "year": 2010}

Nếu quen với SQL thì cứ nghĩ Point giống một row, chỉ khác là ngoài các cột dữ liệu bình thường (payload), nó còn mang theo một vector để so sánh độ giống nhau. Các Point này được gom vào Collection — tương đương table.

2.2. Distance metrics

Đây là phần dễ bị bỏ qua nhưng lại ảnh hưởng nhiều đến chất lượng kết quả. Có 3 loại phổ biến:

  • Cosine: đo góc giữa 2 vector, không quan tâm độ dài. Phần lớn mô hình embedding hiện nay (kiểu OpenAI) được huấn luyện để dùng cosine, nên đây thường là lựa chọn mặc định an toàn.
  • Euclidean: đo khoảng cách thẳng, có tính đến độ lớn của vector.
  • Dot Product: tính nhanh, hay dùng khi vector đã được chuẩn hóa sẵn.

Việc lựa chọn metric phù hợp với model embedding là rất quan trọng, vì mỗi model có thể được huấn luyện với một cách đo độ tương đồng nhất định. Nếu chọn metric không phù hợp, hệ thống có thể không báo lỗi nhưng chất lượng tìm kiếm sẽ bị ảnh hưởng đáng kể.

2.3. Chia nhỏ văn bản (chunking)

Trước khi vector hóa, văn bản dài cần chia nhỏ ra vì mô hình embedding có giới hạn độ dài đầu vào, với lại một đoạn quá dài thì vector đại diện cho nó cũng bị "loãng" nghĩa.

Vài cách hay dùng:

  • Fixed-size: chia theo số ký tự/token cố định — đơn giản nhưng dễ cắt ngang câu
  • Overlap: các đoạn liền kề chồng lấn một phần để không mất ngữ cảnh
  • Semantic: chia theo ranh giới nghĩa (đoạn văn, heading) — tốt hơn nhưng làm phức tạp hơn

Thực tế mình thấy chọn chiến lược chunking đúng ảnh hưởng đến chất lượng search nhiều hơn cả việc chọn model embedding nào.

2.4. Demo: tìm phim theo mô tả

Demo đầu tiên là xây một công cụ tìm phim bằng mô tả tự nhiên, kiểu gõ "phim về giấc mơ lồng giấc mơ" thay vì phải nhớ chính xác tên. Các bước:

  1. Vector hóa mô tả từng phim
  2. Đẩy vào Qdrant (vector + payload gồm tên, thể loại, năm)
  3. Khi user gõ tìm kiếm, vector hóa câu đó rồi tìm các vector gần nhất

Thử so với tìm kiếm từ khóa truyền thống thì thấy rõ semantic search ăn đứt trong các trường hợp mô tả ý tưởng chứ không gõ đúng từ khóa.

3. Indexing và hiệu năng

3.1. HNSW là gì

Khi có hàng triệu vector, so sánh tuần tự từng cái là không ổn về tốc độ. Qdrant dùng HNSW (Hierarchical Navigable Small World) làm thuật toán index mặc định — một cấu trúc đồ thị nhiều lớp, cho phép tìm kiếm gần đúng (approximate) nhanh hơn nhiều so với brute-force.

HNSW multi-layer structure

Nguồn: Qdrant – HNSW Indexing Fundamentals

Cách hoạt động đại khái: có nhiều lớp đồ thị chồng lên nhau, lớp trên thưa (ít điểm, kết nối xa), lớp dưới dày (nhiều điểm, kết nối gần). Tìm kiếm bắt đầu từ lớp trên để nhảy nhanh đến gần khu vực đích, sau đó đi xuống các lớp dưới để tinh chỉnh dần.

Sơ đồ dưới đây tóm lại luồng từ lúc dữ liệu được đưa vào collection, xây HNSW index, cho đến lúc query thực tế chạy qua index đó (kèm cả payload filter).

Đánh đổi chính của HNSW là giữa tốc độ, độ chính xác và bộ nhớ. Muốn chính xác hơn thì tốn thêm bộ nhớ và thời gian xây index.

3.2. Filtering đi cùng vector search

Điểm mình khá thích ở Qdrant là filter theo payload được áp dụng ngay trong lúc tìm kiếm vector, chứ không phải lọc sau khi đã có kết quả. Nghe có vẻ tiểu tiết nhưng thực ra ảnh hưởng lớn: nếu lọc sau, có khi top-K kết quả gần nhất toàn bị loại hết bởi điều kiện filter, kết quả trả về là rỗng hoặc quá ít.

Ví dụ: tìm sản phẩm giống về hình ảnh, nhưng chỉ trong khoảng giá nào đó và còn hàng — điều kiện pricein_stock được áp dụng ngay khi duyệt đồ thị HNSW, không phải lọc riêng ở bước sau.

3.3. Demo: tinh chỉnh tham số HNSW

Có 3 tham số hay phải nghịch:

  • m: số kết nối tối đa mỗi node — tăng lên thì chính xác hơn nhưng tốn RAM hơn
  • ef_construct: độ rộng tìm kiếm lúc build index — ảnh hưởng chất lượng index và thời gian ingest
  • ef_search: độ rộng tìm kiếm lúc query — ảnh hưởng trực tiếp đến độ chính xác và độ trễ mỗi lần search

Nghịch qua các giá trị này thấy rõ trade-off giữa recall (tìm đủ hay không) và tốc độ phản hồi.

4. Hybrid search

4.1. Sparse vector là gì

Vector search ngữ nghĩa (dense vector) rất tốt trong việc bắt "ý nghĩa" chung, nhưng lại yếu ở những chỗ cần khớp chính xác — kiểu tìm mã sản phẩm, tên riêng, hay từ khóa kỹ thuật hiếm gặp. Đây là lúc sparse vector hữu ích.

Sparse vector biểu diễn văn bản dưới dạng vector số chiều rất lớn (bằng kích thước từ vựng) nhưng phần lớn giá trị là 0 — kiểu giống TF-IDF hoặc BM25 truyền thống. Ví dụ từ vựng có 50.000 từ thì vector cũng có 50.000 chiều, nhưng một câu chỉ chứa vài chục từ nên chỉ vài chục chiều có giá trị khác 0, còn lại toàn số 0. Đó là lý do gọi là "sparse" (thưa).

Vì đa số là số 0, nếu lưu và duyệt kiểu dense vector (mảng đầy đủ) thì rất phí. Nên Qdrant dùng cấu trúc inverted index để lưu — cùng cơ chế mà các search engine từ khóa như Elasticsearch hay Lucene dùng từ lâu. Ý tưởng đơn giản: thay vì lưu "point này chứa những từ nào" (forward index), inverted index lưu ngược lại "từ này xuất hiện ở những point nào". Cụ thể mỗi từ (hay token) trong từ vựng có một danh sách (posting list) chứa ID của tất cả point nào có giá trị khác 0 ở chiều tương ứng với từ đó, kèm theo trọng số (weight) của từ trong point đó.

Ví dụ có 3 câu: "áo khoác chống nước", "áo thun cotton", "quần chống nước". Với từ "chống_nước", inverted index sẽ lưu thẳng: chống_nước → [point 1, point 3]. Khi tìm kiếm chứa từ "chống_nước", chỉ cần tra thẳng vào danh sách này chứ không phải duyệt qua từng point một để kiểm tra có chứa từ đó không. Đây chính là lý do tìm kiếm theo từ khóa nhanh — không phải so khớp toàn bộ 50.000 chiều, mà chỉ tra cứu trực tiếp các từ có trong câu truy vấn rồi gộp kết quả lại.

4.2. Hybrid search và Universal Query API

Hybrid search là kết hợp cả dense vector (bắt ngữ nghĩa) và sparse vector (bắt từ khóa) trong một truy vấn, rồi hợp nhất kết quả — hay dùng thuật toán Reciprocal Rank Fusion (RRF). Sơ đồ dưới minh họa luồng này: truy vấn chia thành 2 nhánh song song, chạy xong thì gộp lại ở bước fusion.

Qdrant có Universal Query API cho phép định nghĩa truy vấn phức hợp (nhiều loại vector, prefetch, rerank) trong một request, thay vì gọi nhiều lần rồi tự gộp kết quả ở phía ứng dụng.

5. Tối ưu và mở rộng quy mô

Khi dữ liệu tăng lên hàng chục, hàng trăm triệu vector, chi phí lưu trữ và RAM trở thành vấn đề thật sự. Qdrant có vài kiểu quantization (lượng tử hóa) để nén vector lại:

  • Scalar: chuyển số thực 32-bit sang số nguyên 8-bit, giảm khoảng 4 lần dung lượng, độ chính xác giảm không nhiều
  • Binary: nén mạnh nhất, mỗi chiều chỉ còn 1 bit — giảm dung lượng cực lớn, nhanh hơn nhiều, nhưng đánh đổi độ chính xác nhiều hơn
  • Product: chia vector thành các phần nhỏ rồi nén từng phần — cân bằng giữa nén và chất lượng Không có loại nào là "tốt nhất" tuyệt đối, chọn loại nào phụ thuộc vào mục tiêu dự án của bạn. Nếu dự án cần độ chính xác cao (ví dụ tìm kiếm y tế, pháp lý, nơi trả sai kết quả gây hậu quả lớn) thì nên ưu tiên Scalar hoặc Product, chấp nhận tiết kiệm dung lượng ít hơn để đổi lấy chất lượng ổn định. Ngược lại nếu dự án có ngân sách hạ tầng eo hẹp, dữ liệu cực lớn (hàng trăm triệu đến tỷ vector) và có thể chấp nhận sai số nhất định (như gợi ý sản phẩm, tìm kiếm sơ bộ trước khi lọc thêm), Binary là lựa chọn đáng cân nhắc vì giảm dung lượng và tăng tốc độ nhiều nhất.

Nói cách khác, ba tiêu chí cần cân đối là: độ chính xác (accuracy/recall), dung lượng lưu trữ (bao nhiêu RAM/đĩa cần dùng), và chi phí vận hành (tốc độ query, chi phí hạ tầng theo đó). Không thể tối ưu cả ba cùng lúc — được cái này thường phải đánh đổi cái kia. Cách làm thực tế là bắt đầu với Scalar (an toàn, cân bằng tốt), đo thử recall và chi phí thực tế trên tập dữ liệu của mình, rồi mới cân nhắc chuyển sang Binary nếu cần nén mạnh hơn và có thể bù lại bằng rescoring.

5.2. Rescoring để lấy lại độ chính xác

Quantization giúp nhanh và nhẹ hơn, nhưng đổi lại độ chính xác giảm. Kỹ thuật rescoring giải quyết chuyện này theo kiểu 2 bước: đầu tiên dùng vector đã nén để lọc nhanh ra một tập ứng viên lớn (ví dụ top 100), sau đó tính lại điểm chính xác bằng vector gốc chưa nén nhưng chỉ trên tập nhỏ này thôi.

Sơ đồ dưới minh họa luồng 2 bước này — nén để tìm nhanh, rồi rescoring bằng vector gốc để chốt kết quả cuối.

Cách này giữ được tốc độ của quantization mà độ chính xác vẫn gần với brute-force.

5.3. Ingest dữ liệu lớn

Vài lưu ý khi ingest dữ liệu quy mô lớn:

  • Batch upload: gửi theo lô thay vì từng điểm một, đỡ tốn overhead
  • Song song hóa: chia nhỏ dữ liệu và upload đồng thời qua nhiều luồng
  • Tắt tạm index lúc ingest: build index tốn chi phí, nếu ingest khối lượng lớn thì tắt tạm, ingest xong bật lại, đỡ mất thời gian update index liên tục

Tổng kết

Nhu cầu Dùng gì trong Qdrant
Tìm theo ý nghĩa Dense vector + Cosine/Dot
Tìm theo từ khóa chính xác Sparse vector + inverted index
Kết hợp cả hai Hybrid search qua Universal Query API
Lọc theo điều kiện Payload filtering, tích hợp sẵn trong HNSW
Dữ liệu lớn, RAM giới hạn Quantization + Rescoring
Ingest khối lượng lớn Batch + song song, tắt index tạm thời

Vài nhận xét cá nhân

Điểm mình thích ở Qdrant: filter tích hợp sâu vào lúc search, hybrid search làm được trong cùng một hệ thống chứ không phải ghép nhiều công cụ, và có sẵn công cụ nén giúp kiểm soát chi phí khi data lớn. Tài liệu rõ ràng, SDK hỗ trợ nhiều ngôn ngữ nên học cũng không quá khó.

Điểm cần lưu ý: tinh chỉnh tham số HNSW hay quantization không phải kiểu "cắm là chạy tối ưu ngay", cần hiểu trade-off mới chọn đúng. Với team chưa quen vận hành hệ thống self-hosted, quản lý cluster ở quy mô lớn vẫn tốn công sức đáng kể, dù dùng Qdrant Cloud thì đỡ hơn nhiều.

Tài liệu tham khảo


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.