SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor

Chạy vector database trên VPS: pgvector hay Qdrant?

Ứng dụng và index cùng trên VPS nên latency mạng không phải vấn đề. So sánh pgvector, Qdrant, Chroma, brute force và cách tính RAM index đúng.

Chi phí thực tế của vector database trên VPS

Chạy vector database trên VPS (virtual private server) loại bỏ vấn đề mà mọi nhà cung cấp managed đều đang bán giải pháp để xử lý. Ứng dụng và index nằm trên cùng một máy, nên request tìm kiếm đi qua loopback socket thay vì qua mạng. Chi phí còn lại mới là chi phí thực sự: chuyển văn bản thành vector. Ngoài ra còn có hai chi phí khác: thời gian tạo index và lượng RAM index sử dụng trong suốt thời gian phục vụ.

Điều này làm thay đổi những quyết định cần được ưu tiên. Bạn không còn phải quan tâm đến region và thời gian round trip đến endpoint. Thay vào đó, bạn phải quan tâm đến tích của số lượng vector, số chiều và 4 byte, vì giá trị này quyết định index có vừa với lượng memory bạn thuê mỗi tháng hay không.

Một truy vấn similarity thực sự mất thời gian ở đâu trên một máy

Theo dõi một truy vấn similarity qua stack self-host.

  1. Văn bản truy vấn được embedding model chuyển thành vector. Trên CPU, một chuỗi ngắn thường mất từ vài chục đến vài trăm mili giây. Trên GPU, thời gian này chỉ còn vài mili giây.
  2. Vector được gửi đến store. Qua loopback TCP hoặc Unix domain socket, thao tác này chỉ mất một phần của một mili giây.
  3. Store duyệt index rồi trả về các row gần nhất.
  4. Code của bạn đọc phần text khớp và tạo prompt.

Step 1 thường là phần tốn nhiều thời gian nhất trong danh sách trên. Step 2 là phần mà các nhà cung cấp dịch vụ hosted thường cạnh tranh, nhưng trên một máy thì thời gian này gần như không đáng kể. Đừng đoán tỷ lệ phân bổ. Hãy đo cả hai đầu trên server của bạn.

curl http://127.0.0.1:11434/api/embed -s -o /dev/null \
  -w 'embed: %{time_total}s\n' \
  -d '{"model": "nomic-embed-text", "input": "how do I rotate the api key"}'

Sau đó chạy \timing on trong psql trước truy vấn search. Nếu lệnh đầu tiên in ra embed: 0.184312spsql trả lời Time: 4.201 ms, thì việc tuning index là sai hướng: độ trễ của bạn nằm ở embedding model. Chạy embedding model cục bộ bằng Ollama đưa step 1 lên cùng CPU với step 2 và step 3, nên cả hai nửa cùng tranh chấp các core và cùng dung lượng RAM. Vòng lặp ingest và retrieval bao quanh store này được trình bày trong hướng dẫn về pipeline RAG self-host. RAG là retrieval augmented generation: bạn search các tài liệu của mình rồi đưa những kết quả khớp nhất vào prompt.

Dưới khoảng một trăm nghìn vector, hãy quét toàn bộ

Quét toàn bộ sẽ so sánh query với mọi vector đã lưu. Recall đạt mức hoàn hảo theo định nghĩa. Cách này không cần index hay bước build, và dữ liệu không thể thay đổi khiến index trở nên lỗi thời.

Phép tính cho biết khi nào cách này không còn phù hợp. Mỗi query cần đọc n * d * 4 byte, trong đó n là số lượng vector và d là số chiều. Với 100,000 vector có 768 chiều, con số này là 307 MB cho mỗi query; CPU hiện đại có thể đọc tuần tự lượng dữ liệu đó trong vài chục mili giây. Với 5 million vector, con số này là 15 GB cho mỗi query, lúc đó đây không còn là một query đúng nghĩa nữa.

Vì vậy, hãy lưu vector trong SQLite và thực hiện phép so sánh bằng NumPy.

import sqlite3, numpy as np

db = sqlite3.connect("docs.db")
db.execute("CREATE TABLE IF NOT EXISTS docs (id INTEGER PRIMARY KEY, body TEXT, vec BLOB)")

def add(body, vec):
    v = np.asarray(vec, dtype=np.float32)
    v /= np.linalg.norm(v)
    db.execute("INSERT INTO docs (body, vec) VALUES (?, ?)", (body, v.tobytes()))
    db.commit()

def search(query_vec, k=5):
    rows = db.execute("SELECT id, body, vec FROM docs").fetchall()
    mat = np.frombuffer(b"".join(r[2] for r in rows), dtype=np.float32).reshape(len(rows), -1)
    q = np.asarray(query_vec, dtype=np.float32)
    q /= np.linalg.norm(q)
    scores = mat @ q
    return [(rows[i][0], rows[i][1], float(scores[i])) for i in np.argsort(-scores)[:k]]

Cả hai phía đều được chuẩn hóa về độ dài đơn vị, nên tích vô hướng chính là cosine similarity; điểm càng cao thì kết quả càng gần. Hãy load mat một lần khi khởi động thay vì một lần cho mỗi query, để thao tác đọc từ SQLite hoàn toàn nằm ngoài đường xử lý chính.

Hãy đo trên máy của bạn trước khi loại bỏ phương án này.

import time
t = time.perf_counter()
search(q)
print(f"{(time.perf_counter() - t) * 1000:.1f} ms")

Giới hạn thực tế là: một process phải giữ toàn bộ ma trận trong RAM, và cách này không hỗ trợ lọc metadata cũng như không có cơ chế rõ ràng cho việc ghi đồng thời. Khi một trong các vấn đề đó khiến bạn không hài lòng, hãy chuyển sang giải pháp khác. Bản thân SQLite là một kho lưu trữ nghiêm túc cho phía server; hướng dẫn dùng SQLite trong production trình bày chi tiết vấn đề này, còn nếu workload thực tế của bạn là quét các cột thay vì phục vụ các row thì bài so sánh DuckDB và SQLite sẽ hữu ích hơn.

pgvector khi bạn đã chạy Postgres

Nếu ứng dụng của bạn đã có database Postgres, pgvector tạo thêm ít thành phần mới nhất. Đây là một extension, không phải một service. Vector nằm trong một table thông thường, ngay cạnh row mà chúng mô tả. Vì vậy, truy vấn có filter chỉ cần thêm một mệnh đề WHERE thay vì phải đồng bộ với một hệ thống thứ hai.

Ubuntu 24.04 có sẵn package này trong component universe.

sudo apt update
sudo apt install -y postgresql-16-pgvector
sudo -u postgres psql -d yourdb -c 'CREATE EXTENSION vector;'

Tính đến tháng 8 năm 2026, package đó là pgvector 0.6.0, thấp hơn khá xa so với upstream. Đặc biệt, iterative index scan cần phiên bản 0.8, vì vậy hãy lấy package đó từ repository riêng của dự án PostgreSQL.

sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
sudo apt install -y postgresql-17-pgvector

Thay 17 bằng major version của server. Lệnh sudo -u postgres psql -tAc 'SHOW server_version' sẽ in ra version này. Nếu cài extension package cho sai major version, CREATE EXTENSION sẽ fail vì Postgres chỉ tìm trong share directory của version đang chạy:

ERROR:  could not open extension control file "/usr/share/postgresql/16/extension/vector.control": No such file or directory

Schema vẫn là SQL thông thường, chỉ thêm một type mới.

CREATE TABLE chunks (
  id        bigserial PRIMARY KEY,
  doc_id    bigint NOT NULL,
  body      text   NOT NULL,
  embedding vector(768)
);

SELECT id, body FROM chunks
ORDER BY embedding <=> '[0.013, -0.021, 0.004]'
LIMIT 5;

<=> là khoảng cách cosine, <-> là khoảng cách L2 (Euclidean), còn <#> là inner product âm. Hãy dùng loại mà embedding model của bạn được train để sử dụng. Nếu chọn sai, không có lỗi nào xuất hiện; kết quả chỉ âm thầm kém hơn.

Khi chưa có index, truy vấn đó là phép tìm kiếm chính xác trên mọi row. Đây là phiên bản Postgres của brute force ở trên và có recall hoàn hảo tương tự. Tăng max_parallel_workers_per_gather để dùng thêm core cho truy vấn. Hãy làm bước này trước rồi mới tạo index, vì lúc đó bạn có baseline recall để đo index.

Qdrant khi index vượt quá khả năng của database

Qdrant là một vector store chuyên dụng được viết bằng Rust. Qdrant phù hợp khi index đủ lớn khiến bạn không muốn quá trình build index tranh chấp tài nguyên với Postgres của ứng dụng, hoặc khi bạn cần payload filtering và quantisation mà pgvector không cung cấp.

docker run -d --name qdrant \
  -p 127.0.0.1:6333:6333 -p 127.0.0.1:6334:6334 \
  -e QDRANT__SERVICE__API_KEY="$(openssl rand -hex 32)" \
  -v "$(pwd)/qdrant_storage:/qdrant/storage:z" \
  qdrant/qdrant

Cổng 6333 cung cấp REST (representational state transfer) API và dashboard tại /dashboard, còn cổng 6334 cung cấp gRPC. Có 2 điểm cần lưu ý trên VPS public. Tài liệu của Qdrant cho biết service mặc định chạy “không có mã hóa hoặc xác thực”, còn -p 6333:6333 trong quickstart bind vào mọi interface. Docker có thể publish cổng này vượt qua rule của ufw vì Docker tự ghi các forwarding rule. Hãy bind vào 127.0.0.1 và đặt API key. Một instance Qdrant có thể truy cập bằng public IP nhưng không có key sẽ biến thành bản sao public của tài liệu của bạn.

curl -s http://127.0.0.1:6333/collections -H "api-key: $QDRANT_API_KEY"

Phản hồi đúng sẽ có dạng {"result":{"collections":[]},"status":"ok","time":0.00002}. Nếu nhận lại {"status":{"error":"Unauthorized"}} thì tên header hoặc key bị sai. Nếu hoàn toàn không nhận được phản hồi thì container không chạy hoặc đang bind ở vị trí khác. Việc container đó có cấu hình phù hợp với máy chủ của bạn hay không là câu hỏi thông thường đối với stateful service, vì vậy phần so sánh Docker với database trên host vẫn áp dụng nguyên trạng ở đây.

Chroma và mục đích sử dụng

Chroma là cách nhanh nhất để đi từ con số 0 đến một bản demo retrieval hoạt động.

pip install chromadb
chroma run --path /srv/chroma

Lệnh này chạy trên port 8000 và chromadb.HttpClient(host="localhost", port=8000) kết nối với nó. Chroma có sẵn embedding function mặc định, nên prototype đầu tiên không cần một model server riêng.

Cần đánh đổi rõ ràng. Chroma dễ dùng vì nó ẩn các quyết định mà hướng dẫn này đề cập: dùng distance metric nào và kết quả sẽ chiếm bao nhiêu RAM. Điều đó phù hợp với prototype, nhưng không phù hợp với hệ thống mà bạn phải xử lý sự cố khi có cảnh báo. Nếu dữ liệu của bạn đã nằm trong Postgres, chuyển dữ liệu sang Chroma sẽ thêm một process và một vấn đề đồng bộ hóa, trong khi pgvector không gặp vấn đề đó.

Cần bao nhiêu RAM cho index

Hãy bắt đầu từ các vector thô, vì chúng là mức tối thiểu và không có cách tuning nào làm giảm được.

bytes = number_of_vectors * dimensions * 4

Bốn byte tương ứng với một số thực 32-bit cho mỗi chiều. Tài liệu lập kế hoạch dung lượng của Qdrant thêm hệ số 1.5 cho metadata và các segment tạm được tạo trong quá trình tối ưu:

memory_size = number_of_vectors * vector_dimension * 4 bytes * 1.5

Dưới đây là kết quả áp dụng công thức đó cho một triệu vector, với số chiều thường có ở các model embedding thực tế.

ChartRAM for 1 million vectors, by embedding dimension
The data behind this chart
[
  {
    "label": "384 dims",
    "raw_gib": 1.43,
    "with_overhead_gib": 2.15
  },
  {
    "label": "768 dims",
    "raw_gib": 2.86,
    "with_overhead_gib": 4.29
  },
  {
    "label": "1024 dims",
    "raw_gib": 3.81,
    "with_overhead_gib": 5.72
  },
  {
    "label": "1536 dims",
    "raw_gib": 5.72,
    "with_overhead_gib": 8.58
  },
  {
    "label": "3072 dims",
    "raw_gib": 11.44,
    "with_overhead_gib": 17.17
  }
]

Đây là kết quả của công thức, tính bằng gibibyte (GiB), không phải số đo thực tế. Hãy xem đó là dung lượng bộ nhớ cần chừa ra. Model 768 chiều như nomic-embed-text với một triệu chunk cần khoảng 4.29 GiB, vừa với plan 8 GB và vẫn còn chỗ cho Postgres. Cùng corpus đó với 3072 chiều cần 17.17 GiB và không còn vừa.

Đòn bẩy nằm ở cột đầu tiên của biểu đồ, không phải cột cuối. Giảm một nửa số chiều sẽ giảm một nửa mọi byte phát sinh sau đó, vĩnh viễn. Model 768 chiều có điểm thấp hơn một chút trên public leaderboard thường vẫn là lựa chọn engineering tốt hơn trên VPS. Sau đó, kiểu halfvec của pgvector lưu các số thực 16-bit, tiếp tục giảm một nửa số byte, và tạo index thông qua một expression:

CREATE INDEX ON chunks USING hnsw ((embedding::halfvec(768)) halfvec_cosine_ops);

Có một giới hạn cần biết trước khi chọn model. Kiểu vector của pgvector nhận tối đa 16,000 chiều, nhưng index HNSW và IVFFlat của nó chỉ hỗ trợ tối đa 2,000 chiều. Với số chiều cao hơn mức đó, bạn tạo index trên giá trị cast sang halfvec, hỗ trợ tối đa 4,000 chiều, hoặc không tạo index.

Chi phí của m và ef_construction trong lúc build

HNSW (hierarchical navigable small world) là index mà cả pgvector và Qdrant đều thường dùng. Đây là một đồ thị phân tầng. Mỗi vector là một node có liên kết đến các node lân cận. Khi search, hệ thống đi theo các liên kết đó đến query thay vì đọc toàn bộ dữ liệu.

m là số liên kết mà mỗi node giữ. Tài liệu Faiss tính memory của HNSW là (d * 4 + m * 2 * 4) byte trên mỗi vector và khuyến nghị đặt m trong khoảng từ 4 đến 64. Hãy áp dụng công thức đó cho 768 dimensions và một triệu vector.

ChartCost of raising m at 768 dimensions, 1 million vectors
The data behind this chart
[
  {
    "label": "m = 8",
    "link_bytes_per_vector": 64,
    "total_gib": 2.92
  },
  {
    "label": "m = 16 (default)",
    "link_bytes_per_vector": 128,
    "total_gib": 2.98
  },
  {
    "label": "m = 32",
    "link_bytes_per_vector": 256,
    "total_gib": 3.1
  },
  {
    "label": "m = 64",
    "link_bytes_per_vector": 512,
    "total_gib": 3.34
  }
]

Điểm đáng chú ý là chênh lệch kích thước này. Tăng từ giá trị mặc định m = 16 lên m = 64 sẽ thêm 512 byte liên kết trên mỗi vector, trong khi dữ liệu vector chiếm 3072 byte. Vì vậy, tổng kích thước tăng từ 2.98 GiB lên 3.34 GiB. Mức tăng này khoảng 12 phần trăm. Với số dimensions này, m không phải phần chiếm nhiều memory. Phần đó là các vector.

Chi phí thực sự của m nằm ở build time và insert time, vì khi thêm một node, hệ thống phải tìm và liên kết node đó với số lượng hàng xóm tương ứng. ef_construction là kích thước candidate list mà builder xem xét khi thêm từng node. Tăng giá trị này tạo ra graph tốt hơn nhưng build chậm hơn. Nó không làm thay đổi kích thước của index sau khi build xong.

SET maintenance_work_mem = '4GB';
SET max_parallel_maintenance_workers = 7;
CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

maintenance_work_mem là setting quyết định build mất vài phút hay vài giờ, vì pgvector sẽ build graph trong memory nếu đủ chỗ. Khi memory không còn đủ, pgvector sẽ thông báo:

NOTICE:  hnsw graph no longer fits into maintenance_work_mem after 100000 tuples
DETAIL:  Building will take significantly more time.
HINT:  Increase maintenance_work_mem to speed up builds.

Thông báo đó là dòng hữu ích nhất mà pgvector in ra. Nó cho biết quá trình build đã chuyển sang một path chậm hơn nhiều. Hãy hủy quá trình đó, tăng setting lên cao hơn dung lượng RAM đã tính ở trên, rồi chạy lại. Theo dõi tiến trình từ session thứ hai:

SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS pct
FROM pg_stat_progress_create_index;

HNSW báo initializing rồi đến loading tuples. Nếu build đứng lâu ở một phần trăm thấp trong khi disk không bận, đó là vấn đề maintenance_work_mem chứ không phải query bị treo.

Có 2 điểm cần tính trước. README của pgvector nói HNSW “có build time chậm hơn và dùng nhiều memory hơn” IVFFlat, đổi lại tốc độ và recall tốt hơn. HNSW cũng có thể được tạo trên một table trống. Trong khi đó, IVFFlat phải chạy k-means trên dữ liệu đại diện trước, nên build trên table trống sẽ cho recall kém. Với schema mới, HNSW là loại index bạn có thể tạo trước.

ef_search: tham số bạn điều chỉnh sau khi build

mef_construction được cố định trong index. ef_search thì không. Tham số này đặt số lượng candidate mà quá trình tìm kiếm giữ lại khi duyệt graph, và bạn có thể thay đổi theo từng session hoặc query.

SET hnsw.ef_search = 100;
SELECT id, body FROM chunks ORDER BY embedding <=> $1 LIMIT 5;

Giá trị mặc định là 40. Tăng giá trị này sẽ tăng recall, đồng thời latency cũng tăng. Giảm giá trị này sẽ làm cả hai giảm. Đây là tham số duy nhất để điều chỉnh recall mà không cần build lại index. Vì vậy, hãy điều chỉnh nó bằng một tập query cố định có sẵn đáp án đúng, rồi dừng khi recall không còn cải thiện.

Có một điểm cần lưu ý. ef_search hoạt động không tốt với mệnh đề WHERE có tính chọn lọc cao, vì index trả về một số lượng candidate cố định rồi filter mới được áp dụng. Nếu filter loại bỏ phần lớn row, bạn có thể nhận được ít hơn LIMIT kết quả dù các row khớp vẫn còn trong table. pgvector 0.8 giải quyết vấn đề này bằng iterative scan:

SET hnsw.iterative_scan = relaxed_order;

Sau đó, index được scan lại để lấy thêm candidate cho đến khi đạt limit, tối đa là hnsw.max_scan_tuples, với giá trị mặc định là 20000. strict_order giữ đúng thứ tự theo khoảng cách nhưng tốn nhiều tài nguyên hơn. Đây là tính năng mà package Ubuntu 0.6.0 không có. Khi filter làm thiếu row, bạn sẽ nhận ra điều đó.

Khi index phải vừa trong RAM

Tìm kiếm HNSW là quá trình duyệt qua một graph. Mỗi bước nhảy đọc một node được lưu ở vị trí không liên quan đến node trước đó, nên kiểu truy cập gần như ngẫu nhiên và read-ahead không giúp được gì. Khi graph nằm trong RAM, mỗi bước nhảy chỉ là một lần tham chiếu bộ nhớ. Khi graph không còn nằm trong RAM, một bước nhảy có thể trở thành một lần đọc từ disk, và một truy vấn chạm đến vài trăm node sẽ tạo ra vài trăm lần đọc.

Tài liệu của Qdrant nêu rõ mối quan hệ này: "nếu bạn lưu trong RAM số vector chỉ bằng một nửa, độ trễ tìm kiếm sẽ tăng khoảng gấp đôi." Hãy lập kế hoạch dựa trên nguyên tắc đó.

Khi index thực sự không thể vừa trong RAM, mọi lựa chọn đều là một đánh đổi cần được chủ động quyết định.

  • Memory-map các vector để operating system cache những page thường dùng và giữ các page ít dùng trên disk. Để có thể chấp nhận được, bên dưới cần có storage NVMe (non-volatile memory express) nhanh.
  • Quantise, lưu mỗi dimension bằng một byte thay vì bốn byte. Cách này giảm bốn lần số byte của vector, với chi phí recall nhỏ nhưng có thể đo được.
  • Chuyển kiểu sang halfvec trong pgvector. Cách này giảm một nửa số byte và làm mất recall ít hơn quantisation một byte.
  • Dùng model nhỏ hơn để tạo embedding. Đây là cách sửa rẻ nhất nhưng thường bị bỏ qua vì phải tạo lại embedding cho toàn bộ corpus.

Các dạng lỗi và chuỗi bạn sẽ thấy

could not open extension control file. Chưa cài package pgvector cho major version đang chạy của Postgres. In version bằng sudo -u postgres psql -tAc 'SHOW server_version' rồi cài postgresql-NN-pgvector tương ứng.

ERROR: expected 768 dimensions, not 1536. Kiểu column và model không khớp. Bạn đã đổi embedding model nhưng chưa tạo embedding lại. Không có cách sửa từng phần trong trường hợp này, vì vector từ hai model khác nhau hoàn toàn không thể so sánh với nhau. Do đó phải tạo lại vector cho mọi row.

Query chạy chậm và EXPLAIN hiển thị sequential scan. Index operator class và query operator không khớp. vector_cosine_ops chỉ hỗ trợ <=>. Chạy EXPLAIN ANALYZE trên query và tìm Index Scan using ... on chunks. Nếu thay vào đó bạn thấy Seq Scan on chunks, hãy rebuild index bằng operator class khớp với operator mà bạn thực sự dùng để query.

Số row ít hơn LIMIT và có mệnh đề WHERE. Đây là lỗi filtering đã nêu ở trên. Tăng hnsw.ef_search hoặc chuyển sang pgvector 0.8 và đặt hnsw.iterative_scan.

Quá trình build index kết thúc vì process biến mất và không có lỗi trong psql. maintenance_work_mem được đặt ở mức chiếm gần hết máy, trong khi shared_buffers và application của bạn cũng cần memory, nên kernel kích hoạt out-of-memory killer. sudo dmesg -T | grep -i 'killed process' hiển thị dòng có tên postgres. Giảm setting này, hoặc build index trên plan lớn hơn rồi restore dump.

Chọn phương án

Nếu bạn đã chạy Postgres và có dưới vài triệu vector, hãy dùng pgvector. Index nằm ngay bên cạnh dữ liệu, việc lọc là một mệnh đề WHERE, và hệ thống backup hiện có đã bao gồm cả index đó. Nếu index đủ lớn để cần giới hạn bộ nhớ riêng, hoặc bạn cần lọc payload chuyên sâu, hãy chạy Qdrant bên cạnh Postgres và chấp nhận phải vận hành thêm một service.

Với dưới khoảng một trăm nghìn vector, hãy đo brute-force scan trước khi cài thêm bất cứ thứ gì. Ở quy mô đó, exhaustive search với recall hoàn hảo và không cần bước build không phải là phương án thỏa hiệp. Đó là lựa chọn đúng. Dùng approximate index thay vào đó đồng nghĩa với việc phải xử lý tuning và áp lực RAM để đổi lấy vài mili giây mà trước đó bạn không hề tốn.

FAQ

Tôi có cần một vector database riêng hay Postgres là đủ?

Nếu dữ liệu của bạn đã nằm trong Postgres, pgvector sẽ đủ dùng lâu hơn nhiều so với phần lớn các bài so sánh. pgvector lưu vector trong một cột thông thường, nên tìm kiếm có bộ lọc chỉ là một mệnh đề WHERE, và bản backup hiện có của bạn cũng bao gồm cả index. Hãy chuyển sang một kho riêng như Qdrant khi workload vector cần giới hạn bộ nhớ riêng, hoặc khi bạn cần lọc payload và quantisation mà pgvector không cung cấp.

Một VPS có thể chứa bao nhiêu vector?

Hãy tính thay vì đoán bằng number_of_vectors * dimensions * 4 bytes * 1.5. Một triệu vector 768 chiều chiếm khoảng 4.3 GiB, nên gói 8 GB vẫn chứa được số này và còn chỗ cho Postgres. Một triệu vector 3072 chiều chiếm khoảng 17 GiB và cần gói lớn hơn nhiều. Yếu tố ảnh hưởng lớn nhất đến con số này là số chiều của embedding model, vì vậy hãy chọn model đó sau khi tính đến chi phí bộ nhớ.

Tại sao tìm kiếm vector của tôi chậm khi index nằm trên cùng máy?

Trên một máy, network không phải vấn đề, nên hãy kiểm tra hai yếu tố sau. Thứ nhất, đo riêng thời gian gọi embedding, vì việc tạo query vector trên CPU thường lâu hơn nhiều so với chính thao tác tìm kiếm. Thứ hai, kiểm tra index có nằm trong RAM không. HNSW search truy cập ngẫu nhiên qua một graph, nên khi graph tràn ra disk, mỗi bước truy cập có thể trở thành một lần đọc disk. Hướng dẫn của Qdrant cũng cho biết rằng khi số vector nằm trong RAM giảm một nửa, search latency sẽ tăng khoảng gấp đôi.

Tôi có nên tạo HNSW index không?

Không nên nếu có dưới khoảng một trăm nghìn vector. Exhaustive scan đọc n * d * 4 byte cho mỗi query. Với 100,000 vector 768 chiều, con số này là 307 MB. CPU hiện đại có thể đọc tuần tự lượng dữ liệu đó trong vài chục mili giây, đạt recall hoàn hảo và không cần bước build. Trước tiên, hãy đo thời gian scan trên phần cứng của bạn. Chỉ tạo index khi thời gian scan đo được thực sự quá chậm, không phải chỉ vì một bài benchmark khuyến nghị như vậy.

Việc tăng m thực sự khiến tôi tốn gì?

Chủ yếu là thời gian build và thời gian insert, chứ không phải bộ nhớ. Với 768 chiều, tăng từ giá trị mặc định m = 16 lên m = 64 sẽ thêm 512 byte graph link cho mỗi vector, so với 3072 byte dữ liệu vector. Vì vậy, tổng bộ nhớ tăng khoảng 12 phần trăm. Tuy nhiên, mỗi insert phải tìm và liên kết số neighbour nhiều gấp 4 lần. Hãy điều chỉnh ef_search trước, vì thay đổi giá trị này không tốn chi phí và không cần rebuild.

#vector-database#rag#pgvector#qdrant#tự lưu trữ