Tìm kiếm từ khóa đặt câu hỏi: tài liệu có chứa những từ cụ thể nào? Tìm kiếm vector đặt một câu hỏi khác: những mục nào nằm gần truy vấn trong không gian mà mô hình đã học? Nhờ thay đổi đó, hệ thống có thể tìm được bài viết về “đổi mật khẩu” khi người dùng nhập “đặt lại thông tin đăng nhập”, dù hai cụm từ gần như không trùng nhau. Cùng một cơ chế còn có thể ghép nối hình ảnh, sản phẩm, mã nguồn, phiếu hỗ trợ và nhiều loại đối tượng khác.

Embedding rốt cuộc chỉ là một danh sách số, còn vector index chỉ là cấu trúc dữ liệu để tìm các danh sách nằm gần nhau. Không thành phần nào “hiểu” ý nghĩa như con người. Kết quả hữu ích đến từ cả chuỗi quyết định: mô hình được huấn luyện để giữ lại quan hệ gì, input được chuẩn bị ra sao, dùng hàm khoảng cách nào, cấu hình tìm kiếm gần đúng thế nào, và đánh giá relevance bằng dữ liệu thực tế nào.

Từ đối tượng đến representation có thể tìm kiếm

Embedding model ánh xạ một đối tượng a thành vector có độ rộng cố định f(a) trong R^d. Mô hình văn bản có thể sinh 768 hoặc 1.536 tọa độ số thực; mô hình ảnh có thể dùng kích thước và hình học khác. Từng tọa độ riêng lẻ thường không có diễn giải ổn định kiểu “tọa độ 42 biểu thị kiến thức về database”. Ý nghĩa phân bố trên toàn vector; quan hệ hữu ích xuất hiện qua vị trí tương đối giữa các vector.

Representation phải bao phủ thông tin mà người dùng muốn tìm. Với một bài hỗ trợ, chỉ embed tiêu đề sẽ bỏ lỡ chi tiết trong nội dung. Embed cả cuốn tài liệu thành một vector lại trộn lẫn nhiều chủ đề. Pipeline phổ biến sẽ trích xuất phần text có ích, giữ heading cần thiết, chia thành các chunk liền mạch về ngữ nghĩa với một phần overlap vừa phải, rồi lưu vector của mỗi chunk cùng document ID, section, ngôn ngữ, tenant, quyền truy cập và model version. Chunk tìm được luôn phải dẫn ngược về nguồn chuẩn.

Chunking là một phần của retrieval model. Chunk quá nhỏ cho vector chính xác nhưng thiếu ngữ cảnh; chunk quá lớn có đủ ngữ cảnh nhưng chứa nhiều intent. Ranh giới heading và paragraph thường tốt hơn việc cắt mù theo số ký tự. Hãy đo kích thước bằng tokenizer của embedding model, vì giới hạn và chi phí của model được tính theo token chứ không theo độ dài string JavaScript.

Normalization có hai nghĩa khác nhau. Input normalization là bước làm sạch theo miền nghiệp vụ trước khi embed: chuẩn hóa Unicode, ổn định khoảng trắng, bỏ navigation chrome, hoặc thêm prefix query:passage: nếu model yêu cầu. Vector normalization đưa một vector khác zero về độ dài L2 bằng một:

unit(x)=xx2\operatorname{unit}(x) = \frac{x}{\lVert x \rVert_2}

Unit normalization loại bỏ độ lớn, khiến dot product bằng cosine similarity. Cách này chỉ phù hợp khi magnitude không mang thông tin. Đừng normalize chỉ vì thao tác ấy nghe có vẻ sạch sẽ; hãy theo đúng contract của model và áp dụng cùng policy khi indexing lẫn querying.

Warning

Vector từ các model version, dimension hoặc quy ước input prefix khác nhau không nằm trong cùng một hệ tọa độ đáng tin cậy. Hãy lưu model và preprocessing version với từng record, rồi re-embed sang index mới thay vì trộn vector không tương thích.

Cosine, dot product và khoảng cách L2

Gọi xy là hai vector có d tọa độ. Ba phép đo sau xuất hiện nhiều nhất trong vector retrieval.

Dot product tăng khi những tọa độ cùng hướng có tích lớn:

dot(x,y)=i=1dxiyi\operatorname{dot}(x,y) = \sum_{i=1}^{d} x_i y_i

Giá trị càng cao càng tương đồng. Dot product sử dụng cả hướng lẫn magnitude, nên một vector có norm lớn có thể xếp trên vector nhỏ hơn nhưng cùng hướng tốt hơn. Một số embedding model cố ý mã hóa confidence hoặc popularity trong norm; số khác thì không.

Cosine similarity chia dot product cho norm của cả hai vector:

cosine(x,y)=dot(x,y)x2y2\operatorname{cosine}(x,y) = \frac{\operatorname{dot}(x,y)}{\lVert x \rVert_2\lVert y \rVert_2}

Với vector khác zero, kết quả nằm từ -1 đến 1; giá trị lớn hơn biểu thị hướng gần nhau hơn. Trên các unit vector, cosine similarity và dot product tạo cùng một thứ hạng.

Euclidean distance, thường gọi là L2 distance, đo khoảng cách đường thẳng:

L2(x,y)=i=1d(xiyi)2\operatorname{L2}(x,y) = \sqrt{\sum_{i=1}^{d}(x_i-y_i)^2}

Giá trị càng thấp càng tương đồng. Với unit vector, bình phương khoảng cách L2 và cosine có quan hệ đơn điệu:

L2(x,y)2=22cosine(x,y)\operatorname{L2}(x,y)^2 = 2 - 2\operatorname{cosine}(x,y)

Vì vậy, chuyển giữa hai phép đo không đổi thứ hạng nếu mọi vector được normalize đúng. Khi điều kiện ấy không đúng, kết quả có thể thay đổi hoàn toàn.

Phép đo Kết quả tốt hơn Dùng magnitude Trường hợp thường gặp Lỗi phổ biến
Cosine similarity Cao hơn Không Text embedding mà hướng mang ngữ nghĩa Chia cho norm bằng zero
Dot product Cao hơn Có, trừ unit vector Model được train cho maximum inner product So raw vector khi model yêu cầu normalization
L2 distance Thấp hơn Mô hình spatial hoặc multimodal dùng Euclidean geometry Sort giảm dần như similarity

Metric của index phải khớp với khuyến nghị của model và application code. Một database có thể cung cấp “cosine distance” dưới dạng 1 - cosine_similarity; khi đó số nhỏ hơn mới tốt hơn dù API chứa chữ cosine. Hãy xác minh semantics chính xác thay vì đoán theo tên operator.

Tính similarity và tìm kiếm chính xác bằng code

Exact search tính score với mọi vector đã lưu, nên mỗi query tốn O(Nd) với N vector, mỗi vector rộng d. Đây là baseline đúng và đơn giản nhất; nó vẫn thực dụng cho collection nhỏ, đánh giá offline, hoặc candidate set đã được metadata thu hẹp. Implementation tốt phải từ chối dimension không khớp và zero vector thay vì âm thầm trả về NaN.

python
from __future__ import annotations

from math import sqrt
from typing import Iterable, Sequence


def _same_dimension(x: Sequence[float], y: Sequence[float]) -> None:
    if len(x) == 0 or len(x) != len(y):
        raise ValueError("vectors must be non-empty and have equal dimensions")


def dot_product(x: Sequence[float], y: Sequence[float]) -> float:
    _same_dimension(x, y)
    return sum(left * right for left, right in zip(x, y, strict=True))


def l2_distance(x: Sequence[float], y: Sequence[float]) -> float:
    _same_dimension(x, y)
    return sqrt(sum((left - right) ** 2 for left, right in zip(x, y, strict=True)))


def cosine_similarity(x: Sequence[float], y: Sequence[float]) -> float:
    _same_dimension(x, y)
    x_norm = sqrt(sum(value * value for value in x))
    y_norm = sqrt(sum(value * value for value in y))
    if x_norm == 0.0 or y_norm == 0.0:
        raise ValueError("cosine similarity is undefined for a zero vector")
    return dot_product(x, y) / (x_norm * y_norm)


def exact_search(
    query: Sequence[float],
    records: Iterable[tuple[str, Sequence[float]]],
    limit: int = 5,
) -> list[tuple[str, float]]:
    if limit < 0:
        raise ValueError("limit must be non-negative")
    scored = [
        (record_id, cosine_similarity(query, vector))
        for record_id, vector in records
    ]
    return sorted(scored, key=lambda item: (-item[1], item[0]))[:limit]

ID tie-break làm kết quả test và pagination ổn định. Với nhiều lần search, hãy normalize document vector một lần khi ingest, normalize mỗi query một lần, rồi dùng dot product trong vòng lặp. Tính lại norm của mọi document ở từng request chỉ lãng phí CPU.

TypeScript dưới đây áp dụng cùng cách tối ưu: index giữ unit vector và exact top-k search dùng dot product.

ts
type VectorRecord = {
  id: string;
  vector: readonly number[];
};

function normalize(vector: readonly number[]): number[] {
  if (vector.length === 0) throw new Error('vector must not be empty');
  const norm = Math.hypot(...vector);
  if (norm === 0) throw new Error('cannot normalize a zero vector');
  return vector.map((value) => value / norm);
}

function dotProduct(left: readonly number[], right: readonly number[]): number {
  if (left.length === 0 || left.length !== right.length) {
    throw new Error('vectors must be non-empty and have equal dimensions');
  }
  return left.reduce((sum, value, index) => sum + value * right[index]!, 0);
}

function exactSearch(
  query: readonly number[],
  records: readonly VectorRecord[],
  limit = 5,
): Array<{ id: string; score: number }> {
  if (!Number.isInteger(limit) || limit < 0) {
    throw new Error('limit must be a non-negative integer');
  }

  const unitQuery = normalize(query);
  return records
    .map((record) => ({
      id: record.id,
      score: dotProduct(unitQuery, record.vector),
    }))
    .sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
    .slice(0, limit);
}

function buildIndex(records: readonly VectorRecord[]): VectorRecord[] {
  return records.map((record) => ({
    id: record.id,
    vector: normalize(record.vector),
  }));
}

Code giả định stored vector đã được normalize qua buildIndex. Với numerical workload thật, nên dùng Float32Array, batched matrix operation hoặc vector database thay vì spread mảng rất rộng vào Math.hypot. Dù vậy baseline vẫn rất quan trọng: so approximate index với exact result trên cùng một corpus đóng băng để tính recall.

Tip

Hãy giữ brute-force implementation trong evaluation suite ngay cả khi production đã dùng ANN. Nó là oracle giúp phân biệt index nhanh đang bỏ sót neighbor với embedding model vốn đã biểu diễn sai bài toán.

Vì sao cần approximate nearest-neighbor index

Quét mười triệu vector 1.536 chiều đòi hỏi hàng tỷ phép nhân cộng cho mỗi query cùng lượng memory bandwidth rất lớn. Approximate nearest-neighbor search, hay ANN, bỏ qua phần lớn vector và chấp nhận đôi lúc bỏ lỡ kết quả exact tốt nhất. Câu hỏi thực tế không phải ANN có chính xác tuyệt đối hay không, mà nó giữ được bao nhiêu recall ở mức latency và memory hệ thống chịu được.

Với query q, hãy so top k gần đúng với oracle exact search trên cùng corpus đóng băng:

recall@k(q)=ANNk(q)Exactk(q)k\operatorname{recall@k}(q) = \frac{\left|\operatorname{ANN}_k(q) \cap \operatorname{Exact}_k(q)\right|}{k}

HNSW, viết tắt của Hierarchical Navigable Small World, xây một proximity graph nhiều tầng. Tầng trên thưa giúp nhảy xa trong không gian; tầng dưới dày hơn để tinh chỉnh neighborhood. Query đi vào gần đỉnh, tham lam theo những cạnh có triển vọng, hạ dần qua các tầng rồi mở rộng candidate frontier ở đáy. Nhiều neighbor và công xây dựng lớn hơn thường tăng recall nhưng tốn build time lẫn memory. Query parameter lớn hơn, thường là efSearch, tăng recall bằng cách đánh đổi latency.

IVF, hay inverted file index, trước hết train các coarse centroid rồi gán vector vào partition gần nhất. Khi query, nó chỉ tìm trong nprobe partition gần query. Probe nhiều hơn làm recall và latency cùng tăng. IVF có thể kết hợp product quantization, gọi tắt là PQ, để nén các vector subspace thành code ngắn. Compression giảm mạnh memory và có thể tận dụng cache tốt hơn, nhưng tạo thêm distance error. Centroid và codebook phải được train trên dữ liệu đại diện; distribution shift có thể khiến partition mất cân bằng.

Phương pháp Cách query Memory Cập nhật Phù hợp nhất
Exact scan Chấm điểm mọi vector Chỉ raw vector Đơn giản Corpus nhỏ và quality oracle
HNSW Duyệt proximity graph Graph overhead cao Insert tăng dần tốt; delete cần cẩn thận Serving latency thấp, recall cao
IVF Probe một số coarse partition Vừa Thường hợp batch train và rebuild Collection lớn cần điều chỉnh probing
IVF-PQ Probe partition đã nén Thấp Train và rebuild phức tạp Corpus cực lớn, bị giới hạn memory

Cấu hình ANN phụ thuộc workload. Hãy báo cáo cùng lúc recall at k, p50/p95/p99 latency, index byte trên mỗi vector, build duration, update visibility và throughput. Chỉ tối ưu average latency sẽ che tail stall; chỉ tối ưu recall có thể tạo index quá lớn để giữ trong memory.

Luồng indexing, querying và metadata filter

Retrieval system trong production có hai luồng liên quan. Indexing biến canonical object thành chunk và vector có version. Querying áp dụng cùng model contract, thi hành filter, lấy candidate, rồi có thể rerank bằng model đắt hơn.

flowchart LR subgraph Indexing S[Source objects] --> C[Clean and chunk] C --> E[Embedding model] E --> U[Normalize if required] U --> I[(Vector index)] C --> M[(Metadata store)] end subgraph Querying Q[User query] --> QE[Same embedding model] QE --> QU[Same normalization] F[Tenant and access filters] --> R[ANN retrieval] QU --> R R --> RR[Rerank candidates] RR --> O[Results with source links] end I --> R M --> R

Metadata filter là ranh giới correctness chứ không phải phần trang trí. Tenant, visibility, region, language, object type và deletion status có thể quyết định caller được phép thấy gì. Nên pre-filter khi engine tích hợp filter vào traversal. Post-filter top 10 ANN có thể chỉ còn hai mục hợp lệ dù nhiều mục được phép khác nằm sâu hơn. Lấy candidate set lớn hơn có ích nhưng không đảm bảo completeness. Filter có selectivity rất cao có thể cần index riêng theo partition, graph traversal hiểu filter, hoặc exact search trên filtered subset.

Update cũng cần semantics rõ ràng. Khi source đổi, pipeline nên ghi content version mới, sinh thành công toàn bộ vector, publish chúng rồi mới retire version cũ. Xóa source phải xóa chunk, vector, cache và backup theo retention policy. Eventual consistency chỉ chấp nhận được nếu product định nghĩa cách phát hiện stale result và thời hạn để revocation có hiệu lực.

Hybrid retrieval thường tốt hơn từng phương pháp riêng lẻ. Lexical search rất mạnh với identifier, error code, tên và rare term chính xác; embedding bắt paraphrase và intent rộng. Có thể retrieve từ cả hai, hợp nhất rank bằng reciprocal rank fusion, rồi rerank candidate set có giới hạn. Đừng bắt vector giải bài toán exact match mà bản chất của nó làm mờ đi.

Đánh giá, drift, quyền riêng tư và anti-pattern

Hãy bắt đầu đánh giá bằng tập query cùng relevance judgment riêng cho tác vụ. Tập này cần có intent phổ biến, paraphrase khó, identifier chính xác, nhiều ngôn ngữ, trường hợp không có đáp án và permission boundary. Metric offline hữu ích gồm recall at k, precision at k, mean reciprocal rank và normalized discounted cumulative gain. Với ANN, phải đo riêng index recall so với exact nearest neighbor; nếu không, lỗi của model và lỗi do approximation bị trộn lẫn.

Metric online cần gắn retrieval với kết quả của người dùng: tỷ lệ giải quyết thành công, reformulation, mức chấp nhận click hoặc citation, thời gian xử lý và safe abstention. A/B test cần guardrail cho latency, empty-result rate, cost và sự cố unauthorized result. Similarity score không phải xác suất đã calibrate; threshold chép từ model hay corpus khác không có ý nghĩa đáng tin cậy. Hãy chọn threshold từ validation data có nhãn và xem riêng theo language, tenant, content type, query class.

Drift xuất hiện khi source content, ngôn ngữ truy vấn, user intent hoặc embedding model thay đổi. Theo dõi vector norm, missing embedding, chunk length, partition size, score distribution, filter selectivity và retrieval metric theo thời gian. Re-embedding là một data migration: dựng shadow index, đánh giá, dual-read một mẫu traffic, đổi alias atomically, và giữ khả năng rollback tới khi đủ tin cậy.

Embedding có thể làm lộ thuộc tính nhạy cảm và giữ lại nhiều thông tin nguồn hơn dự kiến. Khi input nhạy cảm, hãy xem vector là dữ liệu cá nhân hoặc bí mật dẫn xuất. Cần mã hóa khi truyền và lưu, cô lập tenant, giảm metadata, audit access, đặt retention, đồng thời bảo đảm delete lan tới mọi derived index. Không gửi secret sang embedding API bên ngoài nếu chưa có data-processing agreement được duyệt và threat analysis rõ ràng. Cũng không nên log raw query hoặc nearest chunk mặc định chỉ vì chúng tiện debug.

Các anti-pattern thường lặp lại:

  • Trộn vector từ nhiều model trong một search space.
  • Đổi chunking hoặc normalization mà không version và reindex.
  • Dùng cosine chỉ vì phổ biến thay vì vì model yêu cầu.
  • Xem ANN top-k là deterministic hoặc đầy đủ dưới filter chặt.
  • Đánh giá bằng vài query chọn tay, không có exact hay lexical baseline.
  • Lưu vector mà thiếu source ID, permission, model version hoặc deletion path.
  • Thêm vector database trước khi chứng minh semantic retrieval cải thiện tác vụ.

Note

Những retrieval bug gây hậu quả lớn thường nằm ngoài nearest-neighbor algorithm: permission cũ, chunk hỏng, preprocessing không nhất quán, thiếu delete, hoặc evaluation set không giống production traffic.

Điều cần nhớ

  • Embedding là representation gắn với một model; giá trị nằm ở quan hệ trong learned space, không nằm ở cách diễn giải từng tọa độ.
  • Coverage và chunking quyết định thứ có thể được tìm thấy. Mỗi vector phải giữ source link, metadata, permission và model version.
  • Cosine bỏ qua magnitude, dot product có thể sử dụng nó, còn L2 đo khoảng cách. Metric và normalization phải khớp model contract.
  • Exact search là correctness baseline. HNSW và IVF đổi một phần recall lấy latency thấp hơn; quantization đổi thêm accuracy lấy memory.
  • Cần tune recall, tail latency, throughput, memory, freshness và filter behavior như một hệ thống thống nhất.
  • Hãy đánh giá bằng task data có nhãn, theo dõi drift, migration model qua shadow index và giữ lexical retrieval cho exact term.
  • Embedding kế thừa độ nhạy cảm của source data. Access control, delete, retention và auditability phải bao phủ cả vector dẫn xuất.