Vấn đề thực tế khi dựng PoC cho hệ thống AI
Cuối quý trước, team mình nhận task gấp: dựng bản PoC tìm kiếm đa phương thức (multimodal search) cho catalog 200.000 sản phẩm thời trang. Người dùng có thể upload một bức ảnh chụp vội hoặc gõ mô tả ngắn để tìm mẫu đồ tương đồng cả về kiểu dáng lẫn màu sắc.
Phản xạ đầu tiên của dân backend là dựng ngay một cụm Milvus hoặc Qdrant độc lập. Thế nhưng, thực tế hạ tầng gõ cửa ngay ngày đầu:
- Ngốn RAM dữ dội: Phần lớn vector database độc lập giữ toàn bộ index HNSW trên RAM để tối ưu latency dưới 10ms. Khi ingest thử 150.000 vector CLIP 512 chiều kèm metadata, container ăn trọn hơn 6GB RAM. Máy chủ dev 8GB lập tức chạm ngưỡng swap và crash.
- DevOps overhead quá lớn: Mọi dev trong team đều phải kéo image Docker nặng hàng gigabyte về máy, cấu hình map port và quản lý health check. Deploy demo lên VPS staging rẻ tiền thì service rớt liên tục vì OOM killer.
- Metadata filtering phức tạp: Dữ liệu vừa có vector, vừa có metadata dạng bảng như giá tiền, danh mục, số lượng tồn kho. Việc kết hợp tìm kiếm ngữ nghĩa với bộ lọc cứng (filter) trên các engine chuyên dụng thường đòi hỏi cấu hình index kép khá rườm rà.
Bài toán PoC hay ứng dụng microservice nội bộ không cần tới cả cụm cluster phân tán. Lúc này, giải pháp nhúng (embedded database) mới là thứ team thực sự cần.
Tại sao phần lớn Vector Database lại nặng nề?
Nhìn sâu vào tầng kiến trúc, các hệ thống vector database truyền thống thường vấp phải hai điểm nghẽn:
1. Cơ chế giữ trọn index trên RAM (In-memory Indexing)
Thuật toán HNSW (Hierarchical Navigable Small World) xây dựng đồ thị liên kết giữa các điểm vector. Nhược điểm chí mạng là toàn bộ đồ thị này phải nằm hoàn toàn trong bộ nhớ chính. Với vector 768 chiều (mô hình text-embedding-3-small hoặc BERT), một triệu bản ghi ngốn từ 4GB đến 8GB RAM chỉ riêng cho cấu trúc đồ thị, chưa tính dữ liệu thô.
2. Độ trễ từ kiến trúc Client-Server
Mô hình client-server buộc ứng dụng phải giao tiếp với database qua HTTP hoặc gRPC. Cơ chế này sinh thêm network hop và chi phí serialize dữ liệu nhị phân. Nếu bạn chỉ muốn tích hợp semantic search vào một script Python offline hoặc tool CLI, việc bắt máy client chạy một background daemon là rào cản rất lớn.
Các hướng tiếp cận lưu trữ Vector hiện nay
Để thoát khỏi bài toán OOM mà vẫn giữ tiến độ bàn giao PoC, team mình đã cân nhắc ba phương án:
1. Dùng thư viện thuần RAM (FAISS, Chroma in-memory)
FAISS của Meta chạy rất nhanh và hoàn toàn miễn phí. Bạn có thể dump index ra file .bin hoặc pickle.
Hạn chế: FAISS không hỗ trợ lọc metadata dạng SQL, thiếu cơ chế transaction ACID và khó cập nhật từng record riêng lẻ.
2. Tận dụng Extension trên RDBMS (pgvector)
Cài thêm pgvector vào PostgreSQL có sẵn là lựa chọn quen thuộc với nhiều team backend.
Hạn chế: HNSW trên pgvector ngốn RAM tương đương các engine chuyên biệt. Bạn vẫn cần một instance Postgres riêng biệt, không thể đóng gói gọn gàng thành một thư viện chạy nhúng trong app.
3. LanceDB — Vector Database nhúng trên định dạng Lance
LanceDB chọn hướng đi giống hệt SQLite hay DuckDB: chạy in-process ngay trong tiến trình ứng dụng. Dữ liệu được ghi thẳng xuống thư mục đĩa cục bộ hoặc object storage (S3, Cloudflare R2).
Giải pháp: Triển khai LanceDB cho Multimodal Search
LanceDB giải quyết đúng các điểm nghẽn của team nhờ ba đặc tính kỹ thuật:
- Disk-based ANN Index: Xây dựng trên định dạng cột Lance (tương thích Apache Arrow). Cơ chế này tối ưu cho việc đọc ngẫu nhiên trên ổ SSD, cho phép query trực tiếp từ đĩa mà không cần nạp toàn bộ vector vào RAM.
- Zero-infrastructure: Không cần Docker, không cần cấu hình port. Cài đặt qua
pipvà chạy thẳng. - Hỗ trợ đa phương thức tự nhiên: Lưu trữ text, vector, link ảnh và metadata trong cùng một schema nhất quán.
Bước 1: Chuẩn bị môi trường
Khởi tạo virtual environment và cài đặt các package cần thiết:
python3 -m venv venv
source venv/bin/activate
pip install lancedb pyarrow sentence-transformers pillow
Bước 2: Khởi tạo database cục bộ
Chỉ cần chỉ định một đường dẫn thư mục, LanceDB sẽ tự động tạo cấu trúc file tương ứng:
import lancedb
# Kết nối tới database lưu tại thư mục cục bộ
db = lancedb.connect("./data/lancedb_demo")
print("Danh sách bảng hiện có:", db.table_names())
Bước 3: Xây dựng pipeline nhúng dữ liệu (Text & Image)
Dưới đây là pipeline hoàn chỉnh: dùng mô hình CLIP để chuyển văn bản thành vector 512 chiều, sau đó lưu trực tiếp vào LanceDB.
import lancedb
from sentence_transformers import SentenceTransformer
# 1. Tải model CLIP hỗ trợ cả text và ảnh
print("Đang tải model CLIP-ViT-B-32...")
model = SentenceTransformer('clip-ViT-B-32')
# 2. Dữ liệu mẫu sản phẩm
products = [
{"id": 1, "name": "Giày chạy bộ nam", "category": "shoes", "price": 1200000, "text": "giày thể thao màu đen êm chân chạy marathon"},
{"id": 2, "name": "Áo khoác gió chống nước", "category": "clothing", "price": 650000, "text": "áo khoác dù mỏng nhẹ đi mưa phượt xe máy"},
{"id": 3, "name": "Balo laptop chống sốc", "category": "accessories", "price": 450000, "text": "balo đựng laptop 15.6 inch màu xám công sở chống nước"},
{"id": 4, "name": "Giày sneaker nữ trắng", "category": "shoes", "price": 890000, "text": "giày thể thao nữ màu trắng đế bệt đi học đi làm"}
]
# 3. Sinh vector embeddings từ văn bản
texts = [p["text"] for p in products]
embeddings = model.encode(texts)
for i, product in enumerate(products):
product["vector"] = embeddings[i].tolist()
# 4. Ghi dữ liệu vào LanceDB
db = lancedb.connect("./data/lancedb_demo")
tbl = db.create_table("products", data=products, mode="overwrite")
print(f"Đã ingest thành công {len(tbl)} bản ghi.")
Bước 4: Truy vấn Hybrid (Vector Search kết hợp SQL Filter)
LanceDB hỗ trợ lọc dữ liệu theo cú pháp SQL chuẩn ngay trong quá trình tính toán khoảng cách vector:
user_query = "giày tập thể dục giá dưới 1 triệu"
# Encode câu truy vấn sang vector 512 chiều
query_vector = model.encode(user_query).tolist()
# Tìm kiếm vector kèm pre-filter: lọc giá trước khi so sánh vector
results = (tbl.search(query_vector)
.where("price < 1000000", prefilter=True)
.limit(2)
.to_pandas())
print("\n--- KẾT QUẢ TÌM KIẾM ---")
for _, row in results.iterrows():
print(f"Sản phẩm: {row['name']} | Giá: {row['price']:,}đ | Khoảng cách: {row['_distance']:.4f}")
Bước 5: Tạo Index IVF-PQ khi dữ liệu tăng trưởng
Khi bảng dữ liệu đạt từ vài chục nghìn đến hàng triệu dòng, hãy tạo index IVF-PQ (Inverted File with Product Quantization) để tăng tốc truy vấn mà không làm phình dung lượng RAM:
# Tạo index IVF-PQ trên cột vector
# Lưu ý: Cần tối thiểu vài nghìn vector để thuật toán train các cluster hiệu quả
tbl.create_index(
metric="cosine",
num_partitions=64, # Số cụm Voronoi
num_sub_vectors=16 # Nén vector gốc xuống 16 đoạn con (quantization)
)
Kinh nghiệm thực tế khi đưa vào sản xuất
Sau khi đưa PoC lên môi trường staging, team rút ra ba bài học quan trọng:
- Tận dụng kiến trúc Serverless với S3: Bạn có thể truyền trực tiếp URI như
s3://my-bucket/lancedbvào hàm kết nối. Toàn bộ logic chạy trên AWS Lambda, còn dữ liệu nằm tại S3. Chi phí hạ tầng gần như bằng 0 khi không có traffic. - Bật
prefilter=Trueđúng lúc: Nếu điều kiện lọc loại bỏ trên 80% dữ liệu, prefilter giúp bỏ qua việc tính toán vector không cần thiết. Ngược lại, nếu bộ lọc quá lỏng, postfilter đôi khi cho tốc độ quét vector thô nhanh hơn. - Sao lưu dữ liệu nhanh gọn: LanceDB lưu dữ liệu dưới dạng cấu trúc thư mục gồm các file Lance và manifest. Bạn chỉ cần dùng
rclonehoặcaws s3 syncđể sao lưu định kỳ, không cần lệnh dump hay khóa bảng.

