VPS-এ vector database চালাতে আসল খরচ কত?
আপনার app ও index একই VPS-এ থাকলে latency প্রধান সমস্যা নয়। pgvector, Qdrant, Chroma ও brute force তুলনা করুন, আর vector-এর সংখ্যা ধরে RAM মাপুন।
একটি VPS-এ vector database চালাতে প্রকৃত খরচ কত
VPS (virtual private server)-এ vector database চালালে managed vendor-রা যে সমস্যার সমাধান বিক্রি করে, সেই সমস্যাটি আর থাকে না। আপনার application এবং index একই machine-এ থাকে। তাই search request network-এর মধ্য দিয়ে না গিয়ে loopback socket ব্যবহার করে। তখন প্রকৃত খরচ হিসেবে থাকে text-কে vector-এ রূপান্তর করার খরচ। এর সঙ্গে আরও দুটি খরচ যুক্ত হয়: index তৈরি করতে লাগা সময় এবং এটি service দেওয়ার সময় যে RAM ধরে রাখে।
এতে কোন সিদ্ধান্তগুলো গুরুত্বপূর্ণ, তা বদলে যায়। Region এবং endpoint-এর round trip আর আপনার প্রধান উদ্বেগ থাকে না। vector-এর সংখ্যা, dimension এবং প্রতি vector-এর চার bytes-এর গুণফল গুরুত্বপূর্ণ হয়ে ওঠে। কারণ এর ওপর নির্ভর করে আপনি প্রতি মাসে যে memory ভাড়া নেন, তাতে index-টি fit করবে কি না।
একটি মেশিনে মিল-খোঁজা ক্যোয়েরিতে আসলে মিলিসেকেন্ড কোথায় ব্যয় হয়
একটি self-hosted stack-এর মধ্য দিয়ে একটি similarity query অনুসরণ করুন।
- একটি embedding model query text-কে vector-এ রূপান্তর করে। CPU-তে একটি ছোট string-এর জন্য এতে দশ থেকে কয়েকশ মিলিসেকেন্ড লাগে। GPU-তে সময়টি এক অঙ্কের মিলিসেকেন্ডে নেমে আসে।
- vector-টি store-এ পাঠানো হয়। loopback TCP বা Unix domain socket ব্যবহার করলে এতে এক মিলিসেকেন্ডের ভগ্নাংশ সময় লাগে।
- store তার index অনুসন্ধান করে এবং সবচেয়ে কাছাকাছি row-গুলো ফেরত দেয়।
- আপনার code মিলে যাওয়া text পড়ে এবং একটি prompt তৈরি করে।
এই তালিকায় সাধারণত Step 1-এর সময়ই সবচেয়ে বেশি। hosted vendor-রা Step 2 নিয়ে প্রতিযোগিতা করে, কিন্তু একটি মেশিনে এই ধাপের সময় প্রায় নেই বললেই চলে। সময়ের বিভাজন অনুমান করবেন না। নিজের server-এ দুই প্রান্তের সময় মাপুন।
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"}'এরপর search query চালানোর আগে \timing on-কে psql-এর মধ্যে চালান। প্রথম command-টি যদি embed: 0.184312s প্রিন্ট করে এবং psql-এর উত্তর হয় Time: 4.201 ms, তাহলে index tuning করা ভুল কাজ: আপনার latency-এর কারণ embedding model। Ollama দিয়ে embedding model স্থানীয়ভাবে চালানো হলে Step 1, Step 2 এবং Step 3-এর মতো একই CPU-তে চলে। ফলে দুই অর্ধেক একই core এবং একই RAM-এর জন্য প্রতিযোগিতা করে। এই store-কে ঘিরে থাকা ingest ও retrieval loop self-hosted RAG pipeline guide-এ ব্যাখ্যা করা হয়েছে। RAG-এর অর্থ retrieval augmented generation: আপনি নিজের document-গুলো search করেন এবং সেরা মিলগুলো prompt-এ যুক্ত করেন।
প্রায় এক লাখের কম vector হলে সবগুলো scan করুন
Exhaustive scan প্রতিটি stored vector-এর সঙ্গে query তুলনা করে। সংজ্ঞা অনুযায়ী এর recall নিখুঁত। এতে কোনো index বা build step দরকার হয় না, এবং আপনার data পরিবর্তিত হলেও index পুরোনো হয়ে যাওয়ার সমস্যা থাকে না।
কখন এটি আর উপযোগী থাকে না, তা arithmetic থেকেই বোঝা যায়। প্রতি query-তে একটি scan n * d * 4 byte পড়ে, যেখানে n হলো vector-এর সংখ্যা এবং d হলো dimension। 768 dimension-এর 100,000 vector হলে প্রতি query-তে 307 MB পড়তে হয়, যা আধুনিক CPU কয়েক দশক millisecond-এ stream করতে পারে। কিন্তু 5 million vector হলে প্রতি query-তে 15 GB পড়তে হয়; তখন সেটিকে আর query বলা যায় না।
তাই vector-গুলো SQLite-এ সংরক্ষণ করুন এবং comparison 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]]উভয় দিককেই unit length-এ scale করা হয়েছে। তাই dot product-ই cosine similarity, এবং score যত বেশি হবে, match তত কাছাকাছি হবে। প্রতি query-তে একবার করে load না করে startup-এর সময় mat একবার load করুন। এতে SQLite read সম্পূর্ণভাবে hot path-এর বাইরে চলে যায়।
এটি বাতিল করার আগে নিজের server-এ সময় মেপে দেখুন।
import time
t = time.perf_counter()
search(q)
print(f"{(time.perf_counter() - t) * 1000:.1f} ms")বাস্তব সীমাবদ্ধতাগুলো স্পষ্ট: পুরো matrix RAM-এ ধরে রাখার জন্য একটি process দরকার, এবং এতে metadata filtering বা concurrent writer পরিচালনার কোনো ব্যবস্থা নেই। এর যেকোনো একটি কারণে অসুবিধা হলে অন্য সমাধানে যান। SQLite নিজেই একটি সক্ষম server-side store। production-এ SQLite ব্যবহারের guide-এ এটি বিস্তারিতভাবে ব্যাখ্যা করা হয়েছে। আর আপনার প্রকৃত workload যদি row পরিবেশনের বদলে column scan করা হয়, তাহলে DuckDB এবং SQLite-এর তুলনা বেশি উপযোগী।
pgvector, যখন আপনি ইতিমধ্যে Postgres চালাচ্ছেন
আপনার অ্যাপ্লিকেশনে যদি ইতিমধ্যে একটি Postgres database থাকে, তাহলে pgvector-এ নতুন উপাদান সবচেয়ে কম যোগ হয়। এটি একটি extension, আলাদা service নয়। যে row-কে vector বর্ণনা করে, তার পাশের একটি সাধারণ table-এই vector থাকে। তাই filtered search-এর জন্য দ্বিতীয় কোনো system-এর সঙ্গে data sync রাখার প্রয়োজন হয় না; এটি একটি WHERE clause দিয়েই করা যায়।
Ubuntu 24.04-এর universe component-এ এটি আছে।
sudo apt update
sudo apt install -y postgresql-16-pgvector
sudo -u postgres psql -d yourdb -c 'CREATE EXTENSION vector;'August 2026 অনুযায়ী ওই package-টি pgvector 0.6.0, যা upstream release-এর তুলনায় বেশ পুরোনো। বিশেষ করে iterative index scan-এর জন্য 0.8 প্রয়োজন। তাই PostgreSQL project-এর নিজস্ব repository থেকে সেগুলো নিন।
sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
sudo apt install -y postgresql-17-pgvector17-এর জায়গায় আপনার server-এর major version বসান। sudo -u postgres psql -tAc 'SHOW server_version' এটি দেখায়। ভুল major version-এর জন্য তৈরি extension package install করলে CREATE EXTENSION ব্যর্থ হবে, কারণ Postgres শুধু চলমান version-এর share directory-তেই খোঁজ করে:
ERROR: could not open extension control file "/usr/share/postgresql/16/extension/vector.control": No such file or directorySchema সাধারণ SQL-এর মতোই, শুধু একটি নতুন type যোগ হয়।
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;<=> হলো cosine distance, <-> হলো L2 (Euclidean) distance, এবং <#> হলো negative inner product। আপনার embedding model যে distance-এর জন্য training পেয়েছে, সেটিই ব্যবহার করুন। ভুলটি বেছে নিলেও কোনো error হবে না, তবে ফলাফল নীরবে খারাপ হবে।
কোনো index না থাকলে query-টি প্রতিটি row-এর ওপর exact search চালায়। এটি উপরের brute force পদ্ধতির Postgres সংস্করণ এবং এর recall-ও একইভাবে perfect। max_parallel_workers_per_gather বাড়ালে এতে আরও core ব্যবহার করা যায়। প্রথমে এটি করুন, পরে index তৈরি করুন। তাহলে index-এর সঙ্গে তুলনা করার জন্য একটি recall baseline থাকবে।
Qdrant, যখন index database-এর চেয়ে বড় হয়ে যায়
Qdrant হলো Rust-এ লেখা একটি dedicated vector store। Index এত বড় হলে Qdrant উপযোগী, যখন আপনি চান না যে এর build আপনার application-এর Postgres-এর সঙ্গে resource নিয়ে প্রতিযোগিতা করুক। pgvector-এ নেই এমন payload filtering ও quantisation প্রয়োজন হলেও Qdrant উপযোগী।
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/qdrantPort 6333 REST (representational state transfer) API এবং /dashboard-এ dashboard সরবরাহ করে। Port 6334 gRPC সরবরাহ করে। Public VPS-এ দুটি বিষয় গুরুত্বপূর্ণ। Qdrant-এর নিজস্ব documentation অনুযায়ী, service ডিফল্টভাবে “কোনো encryption বা authentication ছাড়াই” চলে। Quickstart-এর -p 6333:6333 প্রতিটি interface-এ bind করে। Docker এটি ufw rule-এর বাইরে publish করে, কারণ Docker নিজস্ব forwarding rule লেখে। 127.0.0.1-এ bind করুন এবং API key সেট করুন। Public IP-তে কোনো key ছাড়াই পৌঁছানো যায় এমন Qdrant instance আপনার document-এর public copy তৈরি করে।
curl -s http://127.0.0.1:6333/collections -H "api-key: $QDRANT_API_KEY"সুস্থ অবস্থার উত্তর {"result":{"collections":[]},"status":"ok","time":0.00002}-এর মতো দেখায়। {"status":{"error":"Unauthorized"}} ফিরে এলে header name বা key ভুল। একেবারেই কোনো output না এলে container চলছে না, অথবা অন্য কোথাও bind করা আছে। আপনার server-এর জন্য ওই container-এর configuration উপযুক্ত কি না, তা সাধারণ stateful-service সংক্রান্ত প্রশ্ন। তাই Docker বনাম host database-এর তুলনা এখানে অপরিবর্তিতভাবে প্রযোজ্য।
Chroma এবং এটি কী কাজে ব্যবহৃত হয়
Chroma শূন্য থেকে একটি কার্যকর retrieval demo তৈরি করার সবচেয়ে সংক্ষিপ্ত পথ।
pip install chromadb
chroma run --path /srv/chromaএটি port 8000-এ service দেয়, এবং chromadb.HttpClient(host="localhost", port=8000) এর সঙ্গে সংযোগ করে। Chroma একটি default embedding function সরবরাহ করে। তাই প্রথম prototype-এর জন্য আলাদা model server একেবারেই প্রয়োজন হয় না।
এই trade-off সম্পর্কে স্পষ্ট থাকুন। Chroma ব্যবহার করা সহজ, কারণ এটি এই guide-এর আলোচিত সিদ্ধান্তগুলো আড়াল করে: কোন distance metric ব্যবহার করবেন এবং ফলাফল কত RAM ধরে রাখবে। Prototype-এর জন্য এটি সঠিক। কিন্তু যে সিস্টেমের জন্য আপনাকে alert পেতে হবে, তার জন্য এটি সঠিক নয়। আপনার data যদি ইতিমধ্যে Postgres-এ থাকে, তাহলে সেটিকে Chroma-তে সরালে একটি অতিরিক্ত process এবং synchronisation সমস্যা তৈরি হবে। অথচ pgvector-এর যে সমস্যা নেই, তা সমাধান করার জন্য এই পরিবর্তনের প্রয়োজন নেই।
ইনডেক্সের জন্য কত RAM প্রয়োজন
কাঁচা vector থেকে হিসাব শুরু করুন। এগুলোই সর্বনিম্ন প্রয়োজনীয়তা, এবং কোনো tuning এগুলো কমাতে পারে না।
bytes = number_of_vectors * dimensions * 4প্রতি dimension-এর জন্য 4 byte করে একটি 32-bit float লাগে। Qdrant-এর capacity planning documentation metadata এবং optimisation-এর সময় তৈরি হওয়া অস্থায়ী segment-এর জন্য 1.5 multiplier যোগ করে:
memory_size = number_of_vectors * vector_dimension * 4 bytes * 1.5বাস্তব embedding model-গুলো যে dimension তৈরি করে, সেই মাপগুলোর জন্য এক million vector-এ এই formula প্রয়োগ করলে ফলাফল হয়:
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
}
]এগুলো formula থেকে পাওয়া gibibyte (GiB) মান, সরাসরি মাপা memory নয়। এগুলোকে memory-তে যে পরিমাণ জায়গা খালি রাখতে হবে, সেই পরিমাণ হিসেবে ধরুন। nomic-embed-text-এর মতো 768-dimension model এক million chunk-এর জন্য প্রায় 4.29 GiB চায়। এটি 8 GB plan-এ Postgres-এর জন্য কিছু জায়গা রেখে চালানো যায়। একই corpus 3072 dimension-এ 17.17 GiB চায় এবং 8 GB plan-এ তা সম্ভব নয়।
এখানে গুরুত্বপূর্ণ নিয়ন্ত্রণযোগ্য বিষয়টি chart-এর প্রথম column, শেষটি নয়। Dimension অর্ধেক করলে এর পরের প্রতিটি ধাপে প্রয়োজনীয় byte-ও স্থায়ীভাবে অর্ধেক হয়। Public leaderboard-এ সামান্য কম score করা 768-dimension model একটি VPS-এ প্রায়ই ভালো engineering choice। এরপর pgvector-এর halfvec type 16-bit float সংরক্ষণ করে, ফলে byte আবার অর্ধেক হয়, এবং এটি একটি expression-এর মাধ্যমে index তৈরি করে:
CREATE INDEX ON chunks USING hnsw ((embedding::halfvec(768)) halfvec_cosine_ops);Model বেছে নেওয়ার আগে একটি সীমা জানা দরকার। pgvector-এর vector type সর্বোচ্চ 16,000 dimension গ্রহণ করে, কিন্তু এর HNSW এবং IVFFlat index সর্বোচ্চ 2,000 dimension পর্যন্ত কাজ করে। এর বেশি হলে halfvec cast index করতে হয়, যা 4,000 dimension পর্যন্ত পৌঁছায়; অন্যথায় কোনো index তৈরি করা যায় না।
বিল্ডের সময় m এবং ef_construction-এর খরচ
HNSW (hierarchical navigable small world) হলো সেই index, যেটির দিকে pgvector এবং Qdrant—দুটিই সাধারণত যায়। এটি একটি স্তরভিত্তিক graph। প্রতিটি vector একটি node, যার কাছাকাছি node-গুলোর সঙ্গে link থাকে। সবকিছু পড়ার বদলে search সেই link ধরে query-এর দিকে এগিয়ে যায়।
m নির্ধারণ করে প্রতিটি node কতগুলো link রাখবে। Faiss documentation অনুযায়ী HNSW-এর memory ব্যবহার প্রতি vector-এ (d * 4 + m * 2 * 4) bytes এবং সেখানে m 4 থেকে 64-এর মধ্যে রাখার পরামর্শ দেওয়া হয়েছে। 768 dimensions এবং এক million vector দিয়ে হিসাবটি চালান।
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
}
]এই পার্থক্যের আকারটি গুরুত্বপূর্ণ। Default m = 16 থেকে m = 64-এ গেলে প্রতি vector-এ link-এর জন্য 512 bytes যোগ হয়। 3072 bytes vector data-এর তুলনায় মোট memory 2.98 GiB থেকে 3.34 GiB হয়। এটি প্রায় 12 percent। এই dimensions-এ m আপনার memory ব্যবহারের প্রধান কারণ নয়। Vector-গুলোই প্রধান কারণ।
m-এর প্রকৃত খরচ হলো build time এবং insert time। কারণ একটি node যোগ করার সময় এতগুলো neighbour খুঁজে link করতে হয়। ef_construction নির্ধারণ করে প্রতিটি node বসানোর সময় builder কত বড় candidate list বিবেচনা করবে। এটি বাড়ালে graph উন্নত হয় এবং build ধীর হয়। তবে finished index-এর আকার এতে একেবারেই বদলায় না।
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 নির্ধারণ করে build কয়েক মিনিট নেবে, নাকি কয়েক ঘণ্টা। কারণ পর্যাপ্ত memory থাকলে pgvector memory-তেই graph তৈরি করে। memory যথেষ্ট না হলে pgvector তা জানায়:
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.pgvector যে notice প্রিন্ট করে, এটিই সবচেয়ে কার্যকর line। এর অর্থ build অনেক ধীর path-এ চলে গেছে। তাই এটি cancel করুন, উপরে হিসাব করা RAM-এর পরিমাণের চেয়ে বেশি মানে setting বাড়ান, এবং আবার শুরু করুন। দ্বিতীয় session থেকে progress monitor করুন:
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS pct
FROM pg_stat_progress_create_index;HNSW প্রথমে initializing এবং পরে loading tuples report করে। disk ব্যস্ত না থাকা অবস্থায় কোনো build দীর্ঘ সময় কম percentage-এ থাকলে সেটি maintenance_work_mem সমস্যা, আটকে থাকা query নয়।
পরিকল্পনার সময় দুটি বিষয় মনে রাখুন। pgvector-এর README অনুযায়ী IVFFlat-এর তুলনায় HNSW-এর "has slower build times and uses more memory"। এর বিনিময়ে speed এবং recall-এর মধ্যে আরও ভালো trade-off পাওয়া যায়। আর খালি table-এ HNSW তৈরি করা যায়। কিন্তু IVFFlat-কে আগে representative data-এর ওপর k-means চালাতে হয়। তাই খালি table-এ এটি তৈরি করলে recall খারাপ হয়। নতুন schema-তে আগেই তৈরি করার জন্য HNSW-ই উপযুক্ত।
ef_search: build-এর পরে যে knob সামঞ্জস্য করবেন
m এবং ef_construction index-এর মধ্যে স্থায়ীভাবে সংরক্ষিত থাকে। ef_search তা নয়। graph অনুসন্ধানের সময় এটি কতজন candidate রাখা হবে তা নির্ধারণ করে। আপনি এটি প্রতি session বা প্রতি query অনুযায়ী পরিবর্তন করতে পারেন।
SET hnsw.ef_search = 100;
SELECT id, body FROM chunks ORDER BY embedding <=> $1 LIMIT 5;ডিফল্ট মান 40। এটি বাড়ালে recall বাড়ে এবং latency-ও বাড়ে। কমালে দুটিই কমে। rebuild ছাড়া এটিই একমাত্র recall নিয়ন্ত্রণ যা পরিবর্তন করা যায়। তাই যেসব query-র সঠিক উত্তর আগে থেকেই জানা আছে, সেগুলোর একটি নির্দিষ্ট সেট ব্যবহার করে এটি সামঞ্জস্য করুন। recall আর উন্নত না হওয়া পর্যন্ত মান বাড়ান।
একটি সমস্যার কথা মনে রাখুন। selective WHERE clause-এর সঙ্গে ef_search ভালোভাবে কাজ করে না। কারণ index নির্দিষ্ট সংখ্যক candidate ফেরত দেয় এবং পরে filter প্রয়োগ করা হয়। অধিকাংশ row বাদ দেওয়া একটি filter-এর কারণে LIMIT-এর চেয়ে কম ফলাফল পাওয়া যেতে পারে, যদিও matching row table-এ থেকে যায়। pgvector 0.8 iterative scan দিয়ে এই সমস্যার সমাধান করে:
SET hnsw.iterative_scan = relaxed_order;এরপর limit পূরণ না হওয়া পর্যন্ত index-এ আরও candidate খোঁজার জন্য পুনরায় scan করা হয়। এই প্রক্রিয়া hnsw.max_scan_tuples পর্যন্ত চলতে পারে, যার ডিফল্ট মান 20000। strict_order exact distance ordering বজায় রাখে, তবে এর খরচ বেশি। Ubuntu 0.6.0 package-এ এই feature নেই। filter ব্যবহারের সময় কিছু row অনুপস্থিত থাকলেই আপনি বিষয়টি বুঝতে পারবেন।
কেন index-টি RAM-এ ফিট করতে হবে
HNSW search একটি graph-এর ওপর দিয়ে traversal করে। প্রতিটি hop আগের node-এর সঙ্গে সম্পর্কহীন কোনো স্থানে সংরক্ষিত একটি node পড়ে। তাই access pattern প্রায় random হয় এবং read-ahead কোনো উপকার করে না। Graph RAM-এ থাকলে প্রতিটি hop একটি memory reference। Graph RAM-এ না থাকলে কোনো hop disk read-এ পরিণত হতে পারে। ফলে কয়েকশো node পড়া একটি search কয়েকশো read-এ পরিণত হয়।
Qdrant-এর documentation বিষয়টি স্পষ্টভাবে উল্লেখ করে: "আপনি RAM-এ অর্ধেক সংখ্যক vector সংরক্ষণ করলে search latency মোটামুটি দ্বিগুণ হবে।" এই বক্তব্যকে ভিত্তি করে পরিকল্পনা করুন।
Index সত্যিই RAM-এ ফিট না করলে প্রতিটি বিকল্পের মধ্যে আপস থাকে। কোন আপস করবেন, তা উদ্দেশ্যপূর্ণভাবে নির্ধারণ করুন।
- Vector-গুলো memory-map করুন, যাতে operating system বেশি ব্যবহৃত page cache করে এবং কম ব্যবহৃত page disk-এ রাখে। এটি সহনীয় রাখতে নিচে দ্রুত NVMe (non-volatile memory express) storage প্রয়োজন।
- Quantise করুন। প্রতিটি dimension চার byte-এর পরিবর্তে এক byte হিসেবে সংরক্ষণ করুন। এতে vector-এর byte সংখ্যা চার ভাগের এক ভাগ হয়, তবে recall সামান্য এবং পরিমাপযোগ্যভাবে কমে।
- pgvector-এ
halfvec-এ cast করুন। এতে byte সংখ্যা অর্ধেক হয় এবং এক-byte quantisation-এর তুলনায় recall কমে কম। - ছোট model দিয়ে embedding তৈরি করুন। এটি সবচেয়ে সস্তা সমাধান। তবে অনেকে এটি এড়িয়ে যান, কারণ এতে corpus-টি আবার embed করতে হয়।
ব্যর্থতার ধরন এবং যে বার্তাগুলো দেখবেন
could not open extension control file। চলমান Postgres major version-এর জন্য pgvector package ইনস্টল করা নেই। sudo -u postgres psql -tAc 'SHOW server_version' দিয়ে version দেখুন এবং মিল থাকা postgresql-NN-pgvector ইনস্টল করুন।
ERROR: expected 768 dimensions, not 1536। column type এবং model-এর মধ্যে অমিল আছে। আপনি embedding model পরিবর্তন করেছেন, কিন্তু পুনরায় embedding তৈরি করেননি। এখানে আংশিক সমাধান সম্ভব নয়, কারণ দুটি ভিন্ন model-এর vector একেবারেই তুলনাযোগ্য নয়। তাই প্রতিটি row নতুন করে তৈরি করতে হবে।
Query ধীর এবং EXPLAIN sequential scan দেখায়। index operator class এবং query operator একে অপরের সঙ্গে মেলে না। vector_cosine_ops শুধু <=>-এর জন্য কাজ করে। Query-তে EXPLAIN ANALYZE চালিয়ে Index Scan using ... on chunks খুঁজুন। এর পরিবর্তে Seq Scan on chunks দেখলে, আপনি যে operator দিয়ে query করেন তার সঙ্গে মেলে এমন operator class ব্যবহার করে index পুনর্নির্মাণ করুন।
LIMIT-এর চেয়ে কম row এবং একটি WHERE clause উপস্থিত। এটি উপরের filtering trap। hnsw.ef_search-এর মান বাড়ান, অথবা pgvector 0.8 ব্যবহার করে hnsw.iterative_scan সেট করুন।
Index build শেষ হওয়ার সময় process বন্ধ হয়ে যায় এবং psql-এ কোনো error থাকে না। maintenance_work_mem মেশিনের অধিকাংশ memory-তে সেট করা থাকলে, এবং shared_buffers ও আপনার application-ও memory ব্যবহার করতে চাইলে, শেষ পর্যন্ত kernel-এর out-of-memory killer process বন্ধ করে। sudo dmesg -T | grep -i 'killed process'-এ postgres-এর নাম উল্লেখ করা line দেখা যাবে। Setting-এর মান কমান, অথবা বড় plan-এ index build করে dump restore করুন।
একটি বেছে নেওয়া
আপনি যদি ইতিমধ্যে Postgres চালান এবং আপনার ভেক্টরের সংখ্যা কয়েক মিলিয়নের কম হয়, তাহলে pgvector ব্যবহার করুন। Index-টি ডেটার পাশেই থাকে, filtering একটি WHERE clause, এবং আপনার বিদ্যমান backup-ই এটি অন্তর্ভুক্ত করে। Index-এর আকারের কারণে যদি এর জন্য আলাদা memory ceiling প্রয়োজন হয়, অথবা আপনার ভারী payload filtering দরকার হয়, তাহলে এর পাশে Qdrant চালান এবং পরিচালনা করার জন্য একটি দ্বিতীয় service রাখুন।
প্রায় এক লাখের কম ভেক্টর থাকলে কিছু install করার আগে brute-force scan-এর পরিমাপ করুন। ওই আকারে perfect recall-সহ এবং কোনো build step ছাড়া exhaustive search কোনো আপস নয়। এটিই সঠিক সমাধান। এর পরিবর্তে approximate index ব্যবহার করলে এমন কয়েক মিলিসেকেন্ডের বিনিময়ে tuning এবং RAM pressure সামলাতে হবে, যে সময়ের সাশ্রয় আপনার হচ্ছিলই না।
FAQ
আমার কি একটি dedicated vector database দরকার, নাকি Postgres যথেষ্ট?
আপনার data যদি ইতিমধ্যে Postgres-এ থাকে, তাহলে অধিকাংশ তুলনায় যে সময়সীমা বলা হয় তার চেয়ে অনেক বেশি সময় pgvector যথেষ্ট থাকে। এটি সাধারণ column-এ vector সংরক্ষণ করে। তাই filtered search একটি WHERE clause, এবং আপনার বিদ্যমান backup-ই index-কে অন্তর্ভুক্ত করে। যখন vector workload-এর জন্য আলাদা memory ceiling দরকার হয়, অথবা pgvector-এ নেই এমন payload filtering ও quantisation প্রয়োজন হয়, তখন Qdrant-এর মতো dedicated store ব্যবহার করুন।
একটি VPS-এ কতগুলি vector রাখা যায়?
অনুমান না করে number_of_vectors * dimensions * 4 bytes * 1.5 ব্যবহার করে হিসাব করুন। 768-dimension-এর 1 million vector-এর জন্য প্রায় 4.3 GiB লাগে। তাই 8 GB plan-এ Postgres-এর জন্যও কিছু memory রেখে এগুলি রাখা যায়। 3072-dimension-এর 1 million vector-এর জন্য প্রায় 17 GiB লাগে। এর জন্য অনেক বড় plan দরকার। এই হিসাবকে সবচেয়ে বেশি পরিবর্তন করে আপনার embedding model-এর dimension। তাই memory bill বিবেচনায় রেখে model নির্বাচন করুন।
Index একই machine-এ থাকলেও আমার vector search ধীর কেন?
একটি box-এ network আপনার সমস্যা নয়। তাই যে দুটি বিষয় প্রভাব ফেলে, সেগুলি পরীক্ষা করুন। প্রথমে embedding call-টি আলাদাভাবে সময় মেপে দেখুন। কারণ CPU-তে query vector তৈরি করতে অনেক সময় search-এর চেয়েও বেশি লাগতে পারে। দ্বিতীয়ত, index RAM-এ আছে কি না পরীক্ষা করুন। HNSW search একটি graph-এর মধ্যে এলোমেলোভাবে বিভিন্ন node-এ যায়। তাই graph disk-এ চলে গেলে প্রতিটি ধাপ disk read-এ পরিণত হতে পারে। Qdrant-এর নিজস্ব নির্দেশনা অনুযায়ী, RAM-এ রাখা vector-এর সংখ্যা অর্ধেক হলে search latency প্রায় দ্বিগুণ হয়।
আমার কি আদৌ একটি HNSW index তৈরি করা উচিত?
প্রায় এক লক্ষের কম vector থাকলে সাধারণত নয়। একটি exhaustive scan প্রতি query-তে n * d * 4 byte পড়ে। 768 dimension-এর 100,000 vector-এর ক্ষেত্রে এটি 307 MB। আধুনিক CPU perfect recall বজায় রেখে এবং কোনো build step ছাড়াই কয়েক দশ millisecond-এর মধ্যে এই data stream করতে পারে। প্রথমে নিজের hardware-এ scan-এর সময় মেপে দেখুন। Benchmark article-এ বলা হয়েছে বলে নয়, measured scan time সত্যিই বেশি ধীর হলে index তৈরি করুন।
m বাড়ালে আসলে আমার কী খরচ হয়?
Memory-এর চেয়ে build time এবং insert time অনেক বেশি বাড়ে। 768 dimension-এ default m = 16 থেকে m = 64-এ গেলে প্রতি vector-এ graph link-এর জন্য অতিরিক্ত 512 byte লাগে, যেখানে vector data-এর আকার 3072 byte। ফলে মোট memory প্রায় 12 শতাংশ বাড়ে। তবে প্রতিটি insert-কে চার গুণ বেশি neighbour খুঁজে link করতে হয়। প্রথমে ef_search tune করুন। এটি পরিবর্তন করতে কোনো অতিরিক্ত খরচ নেই এবং rebuild-এরও প্রয়োজন হয় না।