Danh sách bài viết

Bài 36: Embedding model — chọn model phù hợp

Bài 6 đã giới thiệu khái niệm text embedding: chuyển text thành dense vector. Bài này tập trung vào câu hỏi thực dụng của RAG: chọn embedding model nào? Sáu tiêu chí (quality, dimension, speed, cost, multilingual, domain), khảo sát họ model 2024-2026 — API (OpenAI text-embedding-3-small/large, Cohere v3, Voyage-3, Google text-embedding-004) và open-source (BGE family, E5, GTE, Nomic), lựa chọn cho tiếng Việt (BGE-M3, multilingual-e5, Cohere multilingual-v3, PhoBERT, paraphrase-multilingual-mpnet-base-v2), code với sentence-transformers và OpenAI SDK, input_type của Cohere/Voyage, Matryoshka embedding (truncate dim mà giữ semantic), benchmark MTEB, so sánh speed và cost, decision matrix theo case, domain-specific (CodeBERT, BioBERT, Legal-BERT), nguyên tắc fine-tune, embedding consistency, L2 normalize, batch encoding, kiểm tra chất lượng, hybrid sparse + dense, các pitfall hay gặp khi triển khai.

25/05/2026
13 phút đọc
6 lượt xem
1

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

Sau bài này, bạn cần:

  • Liệt kê 6 tiêu chí chọn embedding model và đánh giá được model cho use case của mình.
  • Phân biệt API-based vs open-source về cost, latency, privacy, vendor lock-in.
  • Biết ít nhất 3 lựa chọn cho tiếng Việt: BGE-M3, multilingual-e5, Cohere multilingual-v3.
  • Viết code embed text bằng sentence-transformers, OpenAI SDK, Cohere SDK.
  • Hiểu khái niệm input_type (Cohere / Voyage) và lý do nó tồn tại.
  • Hiểu Matryoshka embedding: truncate dim mà vẫn giữ semantic.
  • Biết MTEB là gì, dùng leaderboard để so sánh nhanh model.
  • Lập được decision matrix nhanh: prototype, production English, multilingual, budget tight, best quality.
  • Tránh các pitfall: đổi model giữa index và query, embed text quá dài, quên input_type, không cache.
2

Recap Bài 6 — embedding là gì

Nhắc nhanh để bài này tự lập:

  • Embedding = hàm text → vector ∈ R^d, với d thường 384, 768, 1024, 1536, 3072.
  • Similarity giữa 2 text đo bằng cosine similarity của 2 vector: \( \cos(u, v) = \dfrac{u \cdot v}{\lVert u \rVert\, \lVert v \rVert} \).
  • Hai text gần nghĩa → vector gần nhau (cosine cao). Hai text khác nghĩa → vector xa nhau.
  • Embedding model đã được huấn luyện trên corpus lớn (Common Crawl, Wikipedia, paper, code) bằng contrastive learning — kéo pair gần nghĩa lại gần, đẩy pair khác nghĩa ra xa.

Trong RAG, embedding làm "map" để vector DB tìm chunk liên quan với query. Chất lượng embedding ảnh hưởng trực tiếp đến retrieval — nếu vector của câu hỏi không gần vector của đoạn chứa câu trả lời, RAG sai từ gốc. Vì vậy chọn embedding là một trong những quyết định nặng ký nhất khi xây RAG.

Bài 6 đã cover khái niệm, code cơ bản và vector arithmetic. Bài này tập trung vào tiêu chí chọn trong bối cảnh RAG production 2025-2026.

3

Sáu tiêu chí chọn embedding model

  1. Quality — khả năng capture semantic. Đo bằng MTEB hoặc benchmark trên dữ liệu của chính bạn (golden set Q&A).
  2. Dimension — kích thước vector. Dim càng lớn càng tốn RAM / disk vector DB; ảnh hưởng cost storage và tốc độ similarity search.
  3. Speed — throughput embedding (text/giây). Quan trọng cho phase Index lớn và query latency.
  4. Cost — với API: USD / 1M token. Với open-source self-host: chi phí GPU / RAM / dev-ops.
  5. Multilingual — có hỗ trợ ngôn ngữ bạn cần (tiếng Việt, code, v.v.) không. Model "English-only" embed tiếng Việt thường rất kém.
  6. Domain — general vs specialized (code, medical, legal). Model general có thể vẫn ổn cho domain hẹp, nhưng model chuyên biệt thường tốt hơn nếu có sẵn.

Quy tắc thực tế: không có model "tốt nhất" cho mọi case. Phải benchmark trên golden set của chính bạn (~30-100 Q&A) trước khi chốt.

4

API-based embedding (2024-2026)

Provider     Model                          Dim        Giá (USD / 1M token)
─────────────────────────────────────────────────────────────────────────────
OpenAI       text-embedding-3-small         1536       $0.02
OpenAI       text-embedding-3-large         3072       $0.13
Cohere       embed-english-v3.0             1024       $0.10
Cohere       embed-multilingual-v3.0        1024       $0.10
Voyage AI    voyage-3                       1024       $0.06
Voyage AI    voyage-3-lite                  512        $0.02
Google       text-embedding-004             768        $0.025

(Giá tham khảo tháng 05/2026, kiểm tra trang giá chính thức trước khi quyết định.)

Ưu điểm API:

  • Không cần GPU hay infra ML.
  • Auto-scale, throughput cao.
  • Cập nhật model tự động (cẩn thận — xem phần consistency ở bước 16).

Nhược điểm API:

  • Phụ thuộc network — mỗi embed là một HTTP call.
  • Vendor lock-in: đổi nhà cung cấp = re-index toàn bộ vector DB.
  • Data rời khỏi network của bạn — không phù hợp với data nhạy cảm.
  • Cost tỉ lệ với volume; với corpus rất lớn (10M+ doc) self-host thường rẻ hơn.
5

Open-source embedding

Bốn họ phổ biến trên Hugging Face (xếp theo lượng dùng 2024-2026):

  • BGE family (BAAI, Bắc Kinh) — bge-small-en-v1.5 (384 dim, ~30M params), bge-base-en-v1.5 (768 dim, ~110M), bge-large-en-v1.5 (1024 dim, ~335M), và BGE-M3 (multilingual, dense + sparse + multi-vector).
  • E5 family (Microsoft) — e5-small-v2, e5-base-v2, e5-large-v2, và multilingual-e5-large hỗ trợ 100+ ngôn ngữ. Yêu cầu prefix "query: " / "passage: " tương ứng với input.
  • GTE family (Alibaba) — gte-small, gte-base, gte-large; phiên bản mới gte-Qwen2-7B-instruct dựa trên LLM Qwen2 cho chất lượng cao hơn nhưng nặng hơn.
  • Nomic embednomic-embed-text-v1.5, hỗ trợ Matryoshka (truncate dim 64-768).

Ưu điểm open-source:

  • Cost edge: setup xong embed gần như miễn phí mỗi call (chỉ điện + GPU amortized).
  • Data privacy: chạy local, không gửi đi đâu.
  • Latency thấp khi self-host trong cùng cluster với vector DB.
  • Có quyền fine-tune trên domain riêng.

Nhược điểm: cần GPU (hoặc CPU mạnh cho model nhỏ), tự lo MLOps, quản lý version model.

6

Multilingual và tiếng Việt

Model "English-only" (bge-large-en, e5-large-v2, text-embedding-3-* ưu tiên English) khi embed tiếng Việt vẫn ra vector, nhưng chất lượng kém — câu hỏi tiếng Việt thường không retrieve được passage tiếng Việt đúng nghĩa. Cho RAG tiếng Việt, chọn từ danh sách multilingual:

  • BGE-M3 (BAAI, arXiv 2402.03216) — multilingual 100+ ngôn ngữ; output 3 kiểu: dense (1024 dim), sparse (ColBERT-like), multi-vector. Tốt cho hybrid search. Tiếng Việt benchmark tốt.
  • multilingual-e5-large (Microsoft) — 1024 dim, 100+ ngôn ngữ, yêu cầu prefix query: / passage:. Phiên bản multilingual-e5-large-instruct hỗ trợ instruction.
  • Cohere embed-multilingual-v3.0 — API, 1024 dim, 100+ ngôn ngữ, hỗ trợ input_type. Production-ready, ít hassle deploy.
  • paraphrase-multilingual-mpnet-base-v2 — sentence-transformers baseline, 768 dim, 50+ ngôn ngữ. Nhỏ, chạy CPU OK, dùng làm baseline rất tốt.
  • PhoBERT (VinAI, arXiv 2003.00744) — không phải sentence-embedding sẵn; là encoder tiếng Việt. Có thể tự build sentence embedding bằng mean pooling, hoặc dùng vinai/phobert-base-v2 wrap qua sentence-transformers + fine-tune. Bản thân nó không tối ưu cho retrieval ra-of-the-box.

Khuyến nghị mặc định cho RAG tiếng Việt: BGE-M3 (open-source, miễn phí, mạnh) hoặc Cohere multilingual-v3 (API, ít vận hành). Đánh giá thực tế bằng golden set tiếng Việt — không tin theo lời quảng cáo.

7

sentence-transformers — chạy local

Thư viện sentence-transformers (Reimers & Gurevych 2019, arXiv 1908.10084) là interface chuẩn cho embedding model local. Phiên bản 5.x (2025) hỗ trợ hầu hết model trên Hugging Face.

pip install sentence-transformers
from sentence_transformers import SentenceTransformer

# 1. Load model (lần đầu sẽ tải weight về cache local)
model = SentenceTransformer("BAAI/bge-base-en-v1.5")

texts = [
    "RAG kết hợp retrieval với LLM.",
    "Vector database lưu embedding.",
]
embeddings = model.encode(texts)
print(embeddings.shape)  # (2, 768)
print(type(embeddings))   # numpy.ndarray

Một số tham số encode hay dùng:

embeddings = model.encode(
    texts,
    batch_size=64,            # gom batch để tận dụng GPU
    show_progress_bar=True,
    normalize_embeddings=True,  # L2 normalize (xem bước 17)
    convert_to_numpy=True,
)

Với BGE / E5, kiểm tra README model: một số yêu cầu prefix riêng cho query vs passage để retrieval tối ưu.

8

OpenAI Embeddings API

pip install openai
export OPENAI_API_KEY="sk-..."
from openai import OpenAI

client = OpenAI()

response = client.embeddings.create(
    model="text-embedding-3-small",
    input=["text 1", "text 2"],
)

embeddings = [d.embedding for d in response.data]
print(len(embeddings[0]))  # 1536

Tham số đáng chú ý của OpenAI v3:

  • dimensions — truncate vector về dim nhỏ hơn (nhờ Matryoshka). Ví dụ dimensions=512 cho text-embedding-3-large sẽ giảm 6× storage.
  • encoding_format"float" (mặc định) hoặc "base64" (tiết kiệm băng thông).
response = client.embeddings.create(
    model="text-embedding-3-large",
    input=texts,
    dimensions=512,  # cắt từ 3072 xuống 512
)

Lưu ý: input tối đa 8191 token cho v3. Text dài hơn sẽ bị API từ chối — phải chunk trước.

9

Cohere API và input_type

pip install cohere
export COHERE_API_KEY="..."
import cohere

co = cohere.Client()

# Khi index doc
response = co.embed(
    texts=["RAG kết hợp retrieval với LLM.", "Vector DB lưu embedding."],
    model="embed-multilingual-v3.0",
    input_type="search_document",
)
doc_embeddings = response.embeddings

# Khi query
response = co.embed(
    texts=["RAG là gì?"],
    model="embed-multilingual-v3.0",
    input_type="search_query",
)
query_embedding = response.embeddings[0]

input_type (Cohere v3 và Voyage):

  • search_document — đoạn doc trong corpus.
  • search_query — query của user.
  • classification, clustering — các use case khác.

Lý do: doc và query có phân phối khác nhau (doc thường dài, statement; query thường ngắn, dạng câu hỏi). Model áp dụng prefix nội bộ khác cho mỗi loại, đẩy hai phân phối lại gần nhau trong vector space. Nếu quên đặt input_type đúng → retrieval kém rõ rệt. Đây là pitfall hay gặp khi mới chuyển từ OpenAI sang Cohere.

10

Dimensionality choice và Matryoshka

Trade-off cơ bản:

  • Dim lớn (1024-3072): chất lượng cao hơn, tốn RAM vector DB, similarity search chậm hơn.
  • Dim nhỏ (256-512): search nhanh, ít storage, chất lượng giảm.

Ví dụ ước lượng storage 1M chunk (mỗi vector float32 = 4 bytes):

Dim       Bytes / vector    1M vector
────────────────────────────────────────
384       1,536             1.5 GB
768       3,072             3.0 GB
1024      4,096             4.1 GB
1536      6,144             6.1 GB
3072      12,288            12.3 GB

Matryoshka Representation Learning (Kusupati et al. 2022, arXiv 2205.13147): train model sao cho prefix vector vẫn giữ semantic. Truncate v[:k] không phá hoàn toàn chất lượng.

embedding_full = embed(text)       # ví dụ 3072 dim
embedding_512  = embedding_full[:512]
# embedding_512 vẫn semantically meaningful — chất lượng giảm
# ~3-5% trên MTEB chứ không sụp đổ.

Model hỗ trợ Matryoshka: text-embedding-3-small/large (OpenAI), nomic-embed-text-v1.5, một số phiên bản BGE và mxbai-embed-large-v1. Đối với model không train Matryoshka, truncate vector tùy ý sẽ giảm chất lượng đáng kể — đừng làm bừa.

11

MTEB — đo chất lượng

MTEB (Massive Text Embedding Benchmark, Muennighoff et al. 2022, arXiv 2210.07316) là benchmark chuẩn cho embedding model:

  • 56-58 dataset (số đếm đã tăng theo phiên bản).
  • 8 task: retrieval, reranking, classification, clustering, pair classification, STS, summarization, bitext mining.
  • Leaderboard public trên Hugging Face: huggingface.co/spaces/mteb/leaderboard.

Top model 2025 (thay đổi liên tục, kiểm tra leaderboard trước khi quyết định):

  • NV-Embed-v2 (NVIDIA) — top open-source theo MTEB.
  • BGE-M3 — top multilingual.
  • voyage-3-large — top API.
  • gte-Qwen2-7B-instruct — LLM-based embedding, chất lượng cao, nặng.

Lưu ý hai mặt: MTEB là benchmark chung, không nhất thiết khớp domain của bạn. Một số model "leak" dữ liệu test vào pretrain (đặc biệt với benchmark phổ biến). Quy tắc: dùng MTEB để thu hẹp danh sách ứng viên, sau đó benchmark trên dữ liệu thật của bạn.

12

Speed và cost so sánh

Throughput (ước lượng, phụ thuộc nhiều vào hardware và độ dài input):

Model size          Hardware          Throughput
─────────────────────────────────────────────────────────
Small (32-128M)     1× A100 (80GB)    ~10,000 text/s
Base (~300M)        1× A100           ~3,000 text/s
Large (300-500M)    1× A100           ~1,000-2,000 text/s
7B LLM-embed        1× A100           ~100-300 text/s
API (batched)       —                 latency 100-500ms / call

Cost so sánh trên 1M token (chỉ embedding, không tính LLM):

Lựa chọn                          Cost / 1M token
─────────────────────────────────────────────────
OpenAI text-embedding-3-small     $0.02
Voyage-3-lite                     $0.02
Google text-embedding-004         $0.025
Voyage-3                          $0.06
Cohere embed-v3.0                 $0.10
OpenAI text-embedding-3-large     $0.13
Self-host BGE-large (A100 g5)     ~$0 mỗi call sau setup
                                  (~$1-3/h GPU)

Ví dụ corpus 1M chunk × 300 token = 300M token. Với text-embedding-3-small: $6 cho phase Index một lần. Với 3-large: $39. Self-host BGE: gần $0 (vài giờ GPU). Quyết định phụ thuộc tần suất reindex và sensitivity của data.

13

Decision matrix theo case

Case                              Lựa chọn mặc định
─────────────────────────────────────────────────────────────────
Prototype / small project         text-embedding-3-small (OpenAI)
Production English                Voyage-3 hoặc BGE-large self-host
Multilingual / tiếng Việt         BGE-M3 hoặc Cohere multilingual-v3
Budget tight, on-prem             nomic-embed-text-v1.5 local
Best quality, tự host được        NV-Embed-v2
Data nhạy cảm (medical, legal)    Self-host BGE-M3 hoặc model domain
Code search                       jina-embeddings-v2-code,
                                  voyage-code-3 (nếu API ok)
Mobile / edge                     all-MiniLM-L6-v2 (22M, 384 dim)

Quy tắc thực dụng:

  • Bắt đầu prototype bằng text-embedding-3-small — nhanh, rẻ, đủ tốt cho 80% case English.
  • Nếu RAG tiếng Việt, đổi sang BGE-M3 hoặc Cohere multilingual-v3 ngay từ đầu.
  • Khi scale lên (50K+ doc, tần suất reindex thường xuyên), cân nhắc self-host BGE/E5.
  • Đo trước, đổi sau — đừng over-engineer ban đầu.
14

Domain-specific embedding

  • Codemicrosoft/codebert-base, jinaai/jina-embeddings-v2-base-code, Voyage voyage-code-3. Hiểu syntax, function signature, identifier.
  • Medical / biomedicaldmis-lab/BioBERT, BiomedNLP/BiomedNLP-PubMedBERT, NeuML/pubmedbert-base-embeddings, MedEmbed. Train trên PubMed.
  • Legalnlpaueb/legal-bert-base-uncased, law-ai/InLegalBERT. Train trên contract, case law.
  • FinanceFinBERT, finance-embeddings-investopedia.
  • Scientificallenai/specter2, SciBERT.

Khi nào dùng: domain có nhiều thuật ngữ chuyên ngành (drug name, statute reference, ticker), corpus đủ lớn để embedding general không bắt nghĩa được. Khi nào không cần: domain không quá hẹp, general embedding đã >85% recall trên golden set.

Nếu không có model domain sẵn → fine-tune (bước tiếp).

15

Fine-tune embedding model

Khi dữ liệu domain rất lệch khỏi pretrain corpus, fine-tune embedding có thể cải thiện đáng kể recall (5-20% trên MTEB-style benchmark domain).

Dữ liệu cần: triple (query, positive_doc, negative_doc). Có thể tạo bằng cách:

  • Người gắn nhãn pair query ↔ doc đúng.
  • Sinh tự động: dùng LLM viết câu hỏi cho mỗi doc; negative là doc khác trong batch (in-batch negative).
  • Mining hard negative: chọn doc gần query nhưng không phải đáp án — khó học hơn, hiệu quả hơn.

Loss phổ biến: contrastive loss, triplet loss, MultipleNegativesRankingLoss (in-batch negative).

from sentence_transformers import SentenceTransformer, losses, InputExample
from torch.utils.data import DataLoader

model = SentenceTransformer("BAAI/bge-base-en-v1.5")

train_examples = [
    InputExample(texts=["query A", "positive doc A"]),
    InputExample(texts=["query B", "positive doc B"]),
    # ...
]
loader = DataLoader(train_examples, shuffle=True, batch_size=32)
loss = losses.MultipleNegativesRankingLoss(model)

model.fit(
    train_objectives=[(loader, loss)],
    epochs=3,
    warmup_steps=100,
)
model.save("./bge-base-domain-tuned")

Lưu ý: fine-tune không phải bước đầu tiên. Trước hết: chọn base tốt, hybrid + rerank, chunking đúng. Khi đã cạn các đòn dễ, mới fine-tune.

16

Embedding consistency

Quy tắc bất di bất dịch: vector trong DB phải được embed bởi CÙNG model (cùng version) như vector query. Nếu sai → vector ở "không gian khác", retrieval gần như ngẫu nhiên.

Hệ quả thực tế:

  • Đổi embedding model (kể cả từ 3-small sang 3-large) → re-index toàn bộ. Không có "patch" cho vector cũ.
  • API provider có thể âm thầm update model. OpenAI gắn version vào tên (text-embedding-ada-002, text-embedding-3-small) — không tự đổi giữa version. Nhưng vẫn nên pin tên rõ ràng trong code.
  • Đối với open-source: ghi snapshot revision (commit hash trên Hugging Face) khi load: SentenceTransformer("BAAI/bge-base-en-v1.5", revision="abcd123").
  • Lưu metadata embedding model + version + dim kèm collection trong vector DB. Khi query, verify khớp.

Plan upfront: trước khi index 1M doc, hãy hỏi: "6 tháng nữa tôi đổi model thì re-index có khả thi không?" Nếu corpus có khả năng đổi model trong tương lai, lưu raw text để dễ re-embed.

17

Normalize và batch encoding

L2 normalize = chia mỗi vector cho độ dài của nó: \( \hat{v} = v / \lVert v \rVert \). Sau normalize, cosine similarity = dot product → nhanh hơn cho vector DB.

import numpy as np

vec = model.encode("text")
norm = np.linalg.norm(vec)
print(norm)  # nếu xấp xỉ 1.0 → đã normalized

# Normalize thủ công
vec_norm = vec / np.linalg.norm(vec)

Hầu hết model BGE / E5 / OpenAI v3 đã output normalized. Vẫn nên check bằng np.linalg.norm. Với sentence-transformers, ép bằng tham số:

embeddings = model.encode(texts, normalize_embeddings=True)

Batch encoding — embed một lần nhiều text nhanh hơn nhiều so với loop từng cái, vì tận dụng song song GPU:

# Chậm
embeddings = [model.encode(t) for t in texts]  # tránh

# Nhanh
embeddings = model.encode(texts, batch_size=64)

Batch size tối ưu phụ thuộc VRAM: A100 (80GB) thường chạy được batch 128-256 cho model base; T4 (16GB) thường 32-64.

18

Inspect chất lượng embedding

Test nhanh chất lượng trước khi commit cho corpus lớn:

import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-base-en-v1.5")

pairs = [
    # Cùng nghĩa, kỳ vọng cosine cao (> 0.7)
    ("How to reset password?", "What is the procedure for resetting my password?"),
    # Khác nghĩa, kỳ vọng cosine thấp (< 0.5)
    ("How to reset password?", "Best Italian restaurant in Hanoi"),
]

for a, b in pairs:
    va, vb = model.encode([a, b], normalize_embeddings=True)
    cos = float(np.dot(va, vb))
    print(f"{cos:+.3f}  |  {a}  ||  {b}")

Heuristic rough:

  • Paraphrase gần như giống nhau → cos > 0.8.
  • Cùng chủ đề, diễn đạt khác → 0.6-0.8.
  • Khác chủ đề rõ → < 0.5.
  • Nếu paraphrase cho < 0.6 → model không phù hợp domain / ngôn ngữ.

Visualize 2D bằng UMAP để mắt thường nhìn cụm — chi tiết ở Bài 6.

19

Hybrid sparse + dense

Dense embedding (BGE, OpenAI) capture semantic tốt nhưng yếu cho keyword hiếm, mã sản phẩm, tên function. Sparse representation (BM25, SPLADE) match chính xác từ khoá. Hai cái bổ sung nhau.

  • BM25 — sparse vector dựa trên term frequency, không cần học, chạy nhanh trên Lucene / Tantivy / Elasticsearch.
  • SPLADE (Formal et al. 2021, arXiv 2107.05720) — sparse vector học được, mỗi text → vector vài nghìn chiều, hầu hết = 0, các chiều khác zero là term quan trọng (cả synonym / expansion).
  • BGE-M3 — output cả dense + sparse trong một model duy nhất → tiện cho hybrid.

Cách kết hợp (chi tiết ở Bài 39 — similarity search và Bài 35 — hybrid search):

Query → Dense retrieve (top-K)  ─┐
                                  ├─→ RRF / weighted merge → top-K final
Query → Sparse (BM25, SPLADE) ────┘

Khi chọn embedding cho RAG, nếu biết hybrid là yêu cầu (corpus nhiều keyword kỹ thuật), ưu tiên model output cả dense và sparse như BGE-M3.

20

Pitfall hay gặp

  • Đổi model giữa indexing và querying — vector ở không gian khác, retrieval random. Lỗi này hay xảy ra khi dev đổi model "để test" mà quên reindex.
  • Embed text quá dài — API/sentence-transformers truncate âm thầm. text-embedding-3-* max 8191 token; BGE-base max 512 token. Truncate bỏ phần cuối → embedding bị thiếu thông tin. Phải chunk trước (Bài 38).
  • Quên input_type với Cohere/Voyage — chất lượng retrieval giảm 10-20%. Luôn set rõ search_document cho index, search_query cho query.
  • Quên prefix với E5 — model E5 yêu cầu prefix "query: " / "passage: ". Quên → retrieval kém.
  • Compute embedding mỗi query mà không cache — query lặp lại nhiều (FAQ chatbot) → cache theo hash của query, tiết kiệm 50-90% gọi API.
  • Lẫn vector dim — đổi text-embedding-3-large dim=3072 sang dim=512 giữa chừng. Cộng/concat sai dim → bug khó debug.
  • Trust theo benchmark public — MTEB top không bảo đảm tốt nhất cho domain bạn. Benchmark trên golden set của chính bạn.
  • Không monitor — embedding API có thể fail, rate limit, drift chất lượng. Log latency và error rate.
21

Demo — so sánh 3 model cùng task

Mini benchmark: cùng một corpus và query, so sánh OpenAI 3-small, BGE-base, multilingual-e5 trên semantic search.

import os, numpy as np
from sentence_transformers import SentenceTransformer
from openai import OpenAI

corpus = [
    "RAG kết hợp LLM với knowledge base ngoài.",
    "Vector database lưu embedding và hỗ trợ similarity search.",
    "Best Italian restaurant in Hanoi near Hoan Kiem lake.",
    "Chunk size 200-800 token thường là điểm khởi đầu hợp lý.",
    "Đào tạo lái xe ô tô tại Hà Nội bằng B2.",
]
query = "Vector DB dùng để làm gì?"

def top1(query_vec, doc_vecs, docs):
    sims = doc_vecs @ query_vec
    i = int(np.argmax(sims))
    return docs[i], float(sims[i])

# 1. OpenAI 3-small
client = OpenAI()
def embed_openai(texts):
    r = client.embeddings.create(model="text-embedding-3-small", input=texts)
    return np.array([d.embedding for d in r.data])

# 2. BGE-base-en (English chính)
bge = SentenceTransformer("BAAI/bge-base-en-v1.5")
def embed_bge(texts):
    return bge.encode(texts, normalize_embeddings=True)

# 3. multilingual-e5-base (tiếng Việt ok)
e5 = SentenceTransformer("intfloat/multilingual-e5-base")
def embed_e5_query(text):
    return e5.encode(["query: " + text], normalize_embeddings=True)[0]
def embed_e5_passages(texts):
    return e5.encode(["passage: " + t for t in texts], normalize_embeddings=True)

for name, embed_doc, embed_query in [
    ("OpenAI 3-small", embed_openai, lambda q: embed_openai([q])[0]),
    ("BGE-base-en",    embed_bge,    lambda q: embed_bge([q])[0]),
    ("ml-e5-base",     embed_e5_passages, embed_e5_query),
]:
    doc_vecs = embed_doc(corpus)
    q_vec = embed_query(query)
    answer, sim = top1(q_vec, doc_vecs, corpus)
    print(f"[{name:18s}] sim={sim:+.3f}  →  {answer}")

Trên corpus tiếng Việt, kỳ vọng: ml-e5-base và (nếu có) BGE-M3 chọn đúng câu về vector DB; BGE-en có thể chọn lệch sang câu English; OpenAI 3-small thường vẫn đúng do hỗ trợ multilingual tốt ở mức ổn. Kết quả thực tế phụ thuộc seed, version model — chạy thử và ghi nhận.

22

Bài tập

  1. Soạn 10 câu (5 tiếng Anh + 5 tiếng Việt), một số cặp đồng nghĩa, một số khác chủ đề. Compute cosine similarity matrix 10×10 bằng multilingual-e5-base và in ra. Nhận xét: cặp đồng nghĩa có cao hơn 0.7 không? Cặp khác chủ đề có < 0.5 không?
  2. Lấy lại 10 câu ở câu 1. Embed bằng 3 model: OpenAI 3-small, BGE-base-en, multilingual-e5-base. Vẽ heatmap 3 ma trận similarity. So sánh: model nào tách cụm rõ nhất?
  3. Test cross-lingual: viết một câu tiếng Anh và bản dịch tiếng Việt tương ứng (ví dụ "What is RAG?" và "RAG là gì?"). Embed bằng BGE-en (English-only) và bằng BGE-M3 hoặc multilingual-e5. Cosine similarity giữa hai bản dịch ở mỗi model bao nhiêu? Mong đợi: multilingual cao hơn rõ.
  4. Áp dụng Matryoshka: gọi OpenAI text-embedding-3-large với dimensions=3072 rồi với dimensions=512 cho cùng 5 câu. So sánh top-1 retrieval với một query — kết quả giống nhau không? Đo % giảm chất lượng (nếu có).
  5. (Tùy chọn) Đọc trang chủ MTEB leaderboard, chọn ra 3 model multilingual top, tải về với sentence-transformers, benchmark trên golden set 20-30 cặp Q-A tiếng Việt của bạn (có thể tự soạn từ một bài blog cũ). Model nào recall@5 cao nhất?