Bối cảnh thực tế & Giới hạn của Full-text Search
2 giờ sáng, PagerDuty nổ cảnh báo đỏ. Hệ thống tra cứu wiki và log nội bộ của công ty phản hồi chậm bất thường, tỷ lệ timeout chạm mốc 42%. Khi SSH vào server production (8 vCPU, 16GB RAM), câu lệnh LIKE '%từ khóa%' cùng cụm Elasticsearch đang ghì chặt 98% RAM do phải quét qua hơn 250.000 bản ghi phi cấu trúc.
Vấn đề lớn nhất nằm ở trải nghiệm tìm kiếm. Người dùng nhập ‘không ping được gateway’, nhưng database quan hệ chỉ đối soát chính xác cụm từ đó. Hệ thống bỏ qua toàn bộ tài liệu chứa các từ đồng nghĩa như ‘mất kết nối mạng’ hay ‘rớt gói tin’.
Hệ thống ngày một lớn. Việc ép database quan hệ gánh toàn bộ tải tìm kiếm văn bản sẽ sớm làm sập backend. Dựng một cụm cluster đồ sộ như Milvus hay trả phí theo tháng cho Pinecone thì quá cồng kềnh cho nhu cầu nội bộ. Đây là lúc một vector database gọn nhẹ chạy trực tiếp trong tiến trình Python như ChromaDB phát huy tác dụng.
ChromaDB giải quyết đúng trọng tâm: chuyển văn bản thành vector embeddings (mảng số thực biểu diễn ngữ nghĩa) và tìm kiếm qua thuật toán HNSW với độ trễ chỉ 12–25ms trên tập 50.000 records.
Cài đặt & Bẫy tương thích SQLite
Đầu tiên, hãy tạo virtual environment độc lập trên Ubuntu 22.04 LTS để cô lập dependencies của OnnxRuntime và PyTorch.
# Khởi tạo và kích hoạt môi trường ảo
python3 -m venv venv
source venv/bin/activate
# Cài đặt chromadb và sentence-transformers
pip install --upgrade pip
pip install chromadb sentence-transformers
Một lỗi kinh điển khi deploy lên server cũ (CentOS 7, Ubuntu 18.04 hay Debian 10): ChromaDB đòi hỏi SQLite >= 3.35.0. Nếu thiếu, script sẽ lập tức văng lỗi RuntimeError: Your system has an unsupported version of sqlite3.
Để vá nhanh mà không cần build lại SQLite từ source, hãy cài đặt bản binary:
pip install pysqlite3-binary
Chèn đoạn hook sau lên dòng đầu tiên của file chạy chính, trước mọi lệnh import khác:
__import__('pysqlite3')
import sys
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
import chromadb
Cấu hình chi tiết cho Production
Mặc định ChromaDB lưu dữ liệu tạm trên RAM (In-Memory). Nếu service restart, toàn bộ index sẽ biến mất. Vì vậy, ta bắt buộc phải dùng PersistentClient để ghi dữ liệu xuống đĩa cứng.
1. Khởi tạo Persistent Client và chọn Embedding Model
Với tài liệu tiếng Việt hoặc đa ngữ, model mặc định của ChromaDB (all-MiniLM-L6-v2) cho kết quả rất hạn chế. Bạn nên chuyển sang paraphrase-multilingual-MiniLM-L12-v2 để máy hiểu chính xác ngữ nghĩa tiếng Việt.
import chromadb
from chromadb.utils import embedding_functions
# Khởi tạo storage bền vững tại thư mục cục bộ
client = chromadb.PersistentClient(path="./chroma_storage")
# Khai báo model đa ngữ tối ưu cho tiếng Việt
multilingual_ef = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="paraphrase-multilingual-MiniLM-L12-v2"
)
# Tạo collection sử dụng khoảng cách Cosine
collection = client.get_or_create_collection(
name="incident_knowledgebase",
embedding_function=multilingual_ef,
metadata={"hnsw:space": "cosine"}
)
2. Đánh chỉ mục (Indexing) và gắn Metadata
Khi dữ liệu chạm ngưỡng vài chục nghìn dòng, việc gắn metadata giúp bạn lọc nhanh theo nhóm (metadata filtering) trước khi tính toán vector similarity, giảm tải đáng kể cho CPU.
# Dữ liệu sự cố thực tế
documents = [
"Server mất kết nối mạng do xung đột cấu hình gateway trong Netplan",
"Dịch vụ Nginx trả về lỗi 502 Bad Gateway vì Gunicorn quá tải worker",
"PostgreSQL từ chối kết nối do chạm ngưỡng max_connections (500/500)",
"Phân vùng /var/log chạm ngưỡng 100% disk space khiến tiến trình crash"
]
metadatas = [
{"service": "network", "severity": "critical", "ticket_id": 2041},
{"service": "web_proxy", "severity": "high", "ticket_id": 2042},
{"service": "database", "severity": "critical", "ticket_id": 2043},
{"service": "system", "severity": "medium", "ticket_id": 2044}
]
ids = ["inc_2041", "inc_2042", "inc_2043", "inc_2044"]
# Dùng upsert thay vì add để tránh lỗi Duplicate ID
collection.upsert(
documents=documents,
metadatas=metadatas,
ids=ids
)
print(f"Tổng số documents hiện có trong collection: {collection.count()}")
3. Truy vấn Semantic Search kết hợp bộ lọc
Thử nghiệm tìm kiếm bằng câu query: ‘không vào được web server’. Chú ý rằng trong tài liệu gốc không hề có từ ‘vào được’.
query_text = "không vào được web server"
# Tìm 2 kết quả gần nhất có độ nghiêm trọng từ high trở lên
results = collection.query(
query_texts=[query_text],
n_results=2,
where={"severity": {"$in": ["high", "critical"]}}
)
for idx in range(len(results['documents'][0])):
doc = results['documents'][0][idx]
distance = results['distances'][0][idx]
meta = results['metadatas'][0][idx]
doc_id = results['ids'][0][idx]
print(f"Top {idx+1} [ID: {doc_id}] - Khoảng cách Cosine: {distance:.4f}")
print(f"Nội dung: {doc}")
print(f"Metadata: {meta}")
print("-" * 45)
Đo kiểm & Vận hành Production
Chạy thử nghiệm thành công là một chuyện. Giữ cho hệ thống ổn định dưới tải cao lại là bài toán khác.
1. Kiểm tra dung lượng lưu trữ trên đĩa
Dữ liệu được tổ chức thành file SQLite (lưu metadata) và thư mục nhị phân chứa index HNSW. Bạn nên định kỳ kiểm tra kích thước thư mục:
# Kiểm tra dung lượng thư mục index
du -sh ./chroma_storage
# Kiểm tra số lượng bản ghi thực tế được đồng bộ trong SQLite
sqlite3 ./chroma_storage/chroma.sqlite3 "SELECT count(*) FROM collections;"
2. Benchmark độ trễ thực tế (Query Latency)
Cần phân biệt rõ: thời gian chậm thường nằm ở khâu tạo embedding trên CPU chứ không phải khâu tìm kiếm vector.
import time
start = time.perf_counter()
res = collection.query(query_texts=["ổ cứng bị đầy log"], n_results=1)
elapsed_ms = (time.perf_counter() - start) * 1000
print(f"Độ trễ truy vấn: {elapsed_ms:.2f} ms")
# Nếu con số này vượt quá 80ms trên server 4 vCPU, hãy chuyển embedding inference sang worker chạy GPU hoặc dùng quantization.
3. Ba lưu ý cốt lõi khi vận hành
- Mức tiêu thụ RAM: Model đa ngữ ngốn khoảng 450MB – 600MB RAM. Trên VPS 1GB–2GB RAM, tuyệt đối không chạy chung tiến trình embedding với ứng dụng web chính. Hãy tách ra chạy riêng thông qua chế độ client-server:
chroma run --path ./chroma_storage --port 8000. - Chiến lược backup: Thư mục
./chroma_storagegồm cả file SQLite lẫn index nhị phân HNSW. Trước khi rsync hoặc chụp snapshot đĩa, cần đảm bảo không có tác vụ write đang thực hiện dở dang để tránh tình trạng index bị mất đồng bộ. - Batch size khi nạp dữ liệu: Khi import hơn 10.000 records, hãy chia thành từng batch từ 250 đến 500 documents. Nạp một mảng quá lớn trong một lệnh upsert duy nhất dễ gây ra hiện tượng OOM (Out Of Memory) do RAM bị đội đột ngột trong pha vectorize.

