VPS پر vector database چلانے کی لاگت اور RAM
VPS پر application اور index ایک ہی machine پر ہوں تو network latency مسئلہ نہیں رہتی۔ pgvector، Qdrant، Chroma اور brute force کا موازنہ کریں اور RAM درست ناپیں۔
VPS پر vector database کی حقیقی لاگت
VPS (virtual private server) پر vector database چلانے سے وہ مسئلہ ختم ہو جاتا ہے جس کے حل کی پیشکش ہر managed vendor کرتا ہے۔ آپ کی application اور index ایک ہی machine پر ہوتے ہیں، اس لیے search request network کے بجائے loopback socket سے گزرتی ہے۔ اس کے بعد اصل لاگت باقی رہتی ہے: text کو vectors میں تبدیل کرنا۔ اس کے ساتھ دو مزید لاگتیں وابستہ ہیں: index بنانے میں لگنے والا وقت، اور وہ RAM جس میں index سروس فراہم کرنے تک موجود رہتا ہے۔
اس سے یہ بدل جاتا ہے کہ کون سے فیصلے اہم ہیں۔ Region اور endpoint round trip اب آپ کی تشویش نہیں رہتے۔ Vector count، dimensions اور four bytes کی حاصل ضرب اہم بن جاتی ہے، کیونکہ اسی سے طے ہوتا ہے کہ آپ ہر ماہ کرائے پر لی جانے والی memory میں index سما سکے گا یا نہیں۔
ایک مشین پر milliseconds اصل میں کہاں خرچ ہوتے ہیں
ایک self-hosted stack میں similarity query کا مکمل راستہ دیکھیں۔
- embedding model query text کو vector میں تبدیل کرتا ہے۔ CPU پر مختصر string کے لیے اس میں دسیوں سے سیکڑوں milliseconds لگتے ہیں۔ GPU پر یہ وقت single digits تک ہوتا ہے۔
- vector کو store کو بھیجا جاتا ہے۔ loopback TCP یا Unix domain socket پر اس میں millisecond کا معمولی حصہ لگتا ہے۔
- store اپنے index میں تلاش کرتا ہے اور قریب ترین rows واپس کرتا ہے۔
- آپ کا code matching text پڑھ کر prompt تیار کرتا ہے۔
اس فہرست میں عموماً Step 1 کا وقت سب سے زیادہ ہوتا ہے۔ Step 2 وہ مرحلہ ہے جس پر hosted vendors مقابلہ کرتے ہیں، لیکن ایک ہی مشین پر اس کا وقت تقریباً نہ ہونے کے برابر ہوتا ہے۔ اس تقسیم کے بارے میں اندازہ نہ لگائیں۔ اپنے 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 کو tune کرنا غلط کام ہے: آپ کی latency embedding model کی وجہ سے ہے۔ Ollama کے ساتھ embedding model مقامی طور پر چلانا Step 1 کو steps 2 اور 3 والے ہی CPU پر لے آتا ہے، اس لیے دونوں حصے ایک ہی cores اور اسی RAM کے لیے مقابلہ کرتے ہیں۔ اس store کے گرد موجود ingest اور retrieval loop کی وضاحت self-hosted RAG pipeline guide میں کی گئی ہے۔ RAG سے مراد retrieval augmented generation ہے: آپ اپنی دستاویزات میں تلاش کرتے ہیں اور بہترین matching متن کو prompt میں شامل کرتے ہیں۔
تقریباً ایک لاکھ vectors سے کم ہوں تو سب کو scan کریں
Exhaustive scan ہر stored vector کے ساتھ query کا موازنہ کرتا ہے۔ Recall تعریف کے لحاظ سے مکمل ہوتا ہے۔ اس کے لیے index یا build step درکار نہیں ہوتا، اور یہ آپ کے data کے پیچھے out of date نہیں ہوتا۔
حساب سے معلوم ہو جاتا ہے کہ یہ طریقہ کب ناقابلِ عمل ہو جاتا ہے۔ ہر query کے لیے scan n * d * 4 bytes پڑھتا ہے، جہاں n vectors کی تعداد اور d dimension ہے۔ 768 dimensions والے 100,000 vectors کے لیے یہ ہر query میں 307 MB بنتا ہے، جسے جدید CPU چند دسیوں milliseconds میں stream کر سکتا ہے۔ 5 million vectors پر یہ ہر query میں 15 GB ہو جاتا ہے، اور پھر یہ عملی طور پر query نہیں رہتی۔
اس لیے vectors کو SQLite میں store کریں اور 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]]دونوں sides کو unit length تک scale کیا جاتا ہے، اس لیے dot product cosine similarity ہی ہوتا ہے، اور زیادہ score قریب تر match کی نشاندہی کرتا ہے۔ mat کو ہر query پر load کرنے کے بجائے startup پر ایک بار load کریں، تو SQLite read مکمل طور پر hot path سے باہر ہو جاتا ہے۔
اس طریقے کو رد کرنے سے پہلے اپنے box پر اس کا وقت ناپیں۔
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 کی رہنمائی میں لیا گیا ہے۔ اگر آپ کا حقیقی workload rows فراہم کرنے کے بجائے columns scan کرتا ہے تو DuckDB اور SQLite کا موازنہ زیادہ مفید مطالعہ ہے۔
جب آپ پہلے سے Postgres چلا رہے ہوں تو pgvector
اگر آپ کی ایپلیکیشن میں پہلے سے Postgres database موجود ہے تو pgvector نئی انتظامی پیچیدگی سب سے کم بڑھاتا ہے۔ یہ ایک extension ہے، الگ service نہیں۔ Vectors اسی معمول کی table میں محفوظ ہوتے ہیں جس row کی وہ نمائندگی کرتے ہیں۔ اس لیے filtered search کو ہم وقت رکھنے کے لیے دوسرا system درکار نہیں ہوتا؛ یہ ایک 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 سے کافی پیچھے ہے۔ خاص طور پر iterative index scans کے لیے 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' یہ version دکھاتا ہے۔ اگر extension package غلط major version کے لیے built ہو تو 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 کی training کے مطابق درست option استعمال کریں۔ غلط option منتخب کرنے پر کوئی error نہیں آتا، لیکن نتائج خاموشی سے کم درست ہو جاتے ہیں۔
Index کے بغیر یہ query ہر row پر exact search کرتی ہے۔ یہ اوپر بیان کیے گئے brute force کا Postgres ورژن ہے اور اس کی recall بھی مکمل رہتی ہے۔ max_parallel_workers_per_gather بڑھانے سے مزید CPU cores اس کام میں استعمال ہوتے ہیں۔ پہلے یہ کریں اور index بعد میں بنائیں، کیونکہ اس طرح آپ کے پاس recall کا baseline موجود ہوگا جس کے مقابلے میں index کی کارکردگی ناپی جا سکے گی۔
Qdrant، جب index database سے بڑا ہو جائے
Qdrant، Rust میں لکھی گئی ایک dedicated vector store ہے۔ اس وقت اس کا استعمال موزوں ہوتا ہے جب index اتنا بڑا ہو جائے کہ آپ اسے اپنی application کے Postgres کے ساتھ build ہونے کی وجہ سے وسائل میں مقابلہ کرتے ہوئے نہ دیکھنا چاہیں، یا جب آپ payload filtering اور quantisation چاہتے ہوں جو pgvector فراہم نہیں کرتا۔
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 فراہم کرتا ہے، جبکہ 6334 gRPC فراہم کرتا ہے۔ Public VPS پر دو باتیں اہم ہیں۔ Qdrant کی اپنی documentation کے مطابق service بطور default "with no encryption or authentication" چلتی ہے، اور quickstart کا -p 6333:6333 ہر interface پر bind ہوتا ہے۔ Docker اسے ufw rule کے بعد بھی public کر دیتا ہے، کیونکہ Docker اپنے forwarding rules لکھتا ہے۔ 127.0.0.1 پر bind کریں اور API key مقرر کریں۔ ایسی Qdrant instance جو public IP پر بغیر key کے قابل رسائی ہو، آپ کی documents کی 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 غلط ہے، جبکہ بالکل کوئی جواب نہ ملنے کا مطلب ہے کہ container چل نہیں رہا یا کسی اور جگہ bound ہے۔ آیا یہ container آپ کے box کے لیے درست ساخت رکھتا ہے، یہ عام stateful-service کا سوال ہے، اس لیے Docker اور host database کا موازنہ یہاں بھی اسی طرح لاگو ہوتا ہے۔
Chroma، اور اس کا مقصد
Chroma، کچھ بھی نہ ہونے سے ایک فعال retrieval demo تک پہنچنے کا مختصر ترین راستہ ہے۔
pip install chromadb
chroma run --path /srv/chromaیہ port 8000 پر سروس فراہم کرتا ہے، اور chromadb.HttpClient(host="localhost", port=8000) اس سے connect ہوتا ہے۔ Chroma ایک default embedding function فراہم کرتا ہے، اس لیے پہلے prototype کے لیے الگ model server کی ضرورت نہیں ہوتی۔
اس trade-off کو واضح طور پر سمجھیں۔ Chroma اس لیے آسان ہے کہ یہ ان فیصلوں کو چھپا دیتا ہے جن پر اس guide میں بات کی گئی ہے: کون سا distance metric استعمال کرنا ہے، اور نتیجے کے لیے کتنی RAM درکار ہوگی۔ Prototype کے لیے یہ درست ہے، لیکن اس سسٹم کے لیے درست نہیں جس کے بارے میں آپ کو alert موصول ہوگا۔ اگر آپ کا data پہلے ہی Postgres میں موجود ہے تو اسے Chroma میں منتقل کرنے سے ایک اضافی process اور synchronisation کا مسئلہ پیدا ہوگا، جبکہ pgvector میں یہ مسئلہ موجود ہی نہیں۔
انڈیکس کو کتنی RAM درکار ہوگی
خام vectors سے آغاز کریں، کیونکہ یہی بنیادی حجم ہیں اور کوئی tuning انہیں کم نہیں کر سکتی۔
bytes = number_of_vectors * dimensions * 4ہر dimension کے لیے چار bytes ایک 32-bit float کے برابر ہیں۔ Qdrant کی capacity planning documentation metadata اور optimisation کے دوران بننے والے عارضی segments کے لیے 1.5 کا multiplier شامل کرتی ہے:
memory_size = number_of_vectors * vector_dimension * 4 bytes * 1.5یہ formula ایک million vectors پر ان dimensions کے ساتھ لاگو کیا گیا ہے جو حقیقی embedding models عموماً پیدا کرتے ہیں۔
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 کا نتیجہ gibibytes (GiB) میں ہے، کوئی پیمائش نہیں۔ اسے memory میں درکار مختص جگہ کے حجم کے طور پر سمجھیں۔ nomic-embed-text جیسا 768-dimension model ایک million chunks کے لیے تقریباً 4.29 GiB چاہتا ہے۔ یہ 8 GB plan میں Postgres کے لیے کچھ گنجائش کے ساتھ سما جاتا ہے۔ اسی corpus کے لیے 3072 dimensions پر 17.17 GiB درکار ہوں گے، اور یہ plan کافی نہیں ہوگا۔
اہم اختیار chart کے پہلے column میں ہے، آخری میں نہیں۔ Dimension نصف کرنے سے اس کے بعد آنے والے ہر مرحلے میں bytes بھی نصف ہو جاتے ہیں۔ 768-dimension model اگر public leaderboard پر قدرے کم score بھی حاصل کرے، تو VPS پر اکثر بہتر engineering choice ہوتا ہے۔ pgvector کی halfvec type اس کے بعد 16-bit floats محفوظ کرتی ہے، جس سے bytes دوبارہ نصف ہو جاتے ہیں، اور یہ expression کے ذریعے index بناتی ہے:
CREATE INDEX ON chunks USING hnsw ((embedding::halfvec(768)) halfvec_cosine_ops);Model منتخب کرنے سے پہلے ایک حد معلوم کر لیں۔ pgvector کی vector type زیادہ سے زیادہ 16,000 dimensions قبول کرتی ہے، لیکن اس کے HNSW اور IVFFlat indexes صرف 2,000 dimensions تک کام کرتے ہیں۔ اس حد سے زیادہ dimensions کے لیے halfvec cast کا index بنانا پڑتا ہے، جو 4,000 dimensions تک پہنچتا ہے، یا پھر index نہیں بنایا جا سکتا۔
build time میں m اور ef_construction کی لاگت
HNSW (hierarchical navigable small world) وہ index ہے جسے pgvector اور Qdrant دونوں استعمال کرتے ہیں۔ یہ ایک layered graph ہے۔ ہر vector ایک node ہوتا ہے جس کے nearby nodes سے links ہوتے ہیں، اور search ہر چیز پڑھنے کے بجائے query کی سمت ان links پر آگے بڑھتی ہے۔
m یہ بتاتا ہے کہ ہر node کتنے links برقرار رکھتا ہے۔ Faiss documentation کے مطابق HNSW کی memory فی vector (d * 4 + m * 2 * 4) bytes ہوتی ہے، اور وہ m کو 4 اور 64 کے درمیان رکھنے کی سفارش کرتی ہے۔ 768 dimensions اور ایک 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
}
]اس فرق کا اصل اثر اس کے سائز میں ہے۔ default m = 16 سے m = 64 پر جانے سے ہر vector کے links میں 512 bytes کا اضافہ ہوتا ہے، جبکہ vector data 3072 bytes ہے۔ اس طرح کل سائز 2.98 GiB سے بڑھ کر 3.34 GiB ہو جاتا ہے۔ یہ تقریباً 12 فیصد ہے۔ ان dimensions پر m وہ چیز نہیں ہے جہاں آپ کی زیادہ memory استعمال ہوتی ہے۔ اصل memory vectors استعمال کرتے ہیں۔
m کی اصل لاگت build time اور insert time ہے، کیونکہ node شامل کرتے وقت اتنے neighbours تلاش کرکے ان سے link کرنا پڑتا ہے۔ ef_construction اس candidate list کا سائز ہے جسے builder ہر node شامل کرتے وقت استعمال کرتا ہے۔ اسے بڑھانے سے graph بہتر اور build سست ہو جاتا ہے، لیکن مکمل 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 وہ setting ہے جو طے کرتی ہے کہ build میں چند منٹ لگیں گے یا کئی گھنٹے، کیونکہ جب memory کافی ہو تو pgvector graph کو memory میں بناتا ہے۔ جب 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.یہ notice pgvector کی جانب سے دکھائی جانے والی سب سے مفید line ہے۔ اس کا مطلب ہے کہ build بہت سست path پر چلا گیا ہے۔ اسے cancel کریں، setting کو اوپر حساب کی گئی RAM کی مقدار سے زیادہ کریں، اور دوبارہ شروع کریں۔ دوسری 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 ہے، نہ کہ stuck query۔
منصوبہ بندی کے لیے دو حقائق اہم ہیں۔ pgvector کی README کے مطابق HNSW، IVFFlat کے مقابلے میں "has slower build times and uses more memory"، لیکن اس کے بدلے speed اور recall کے درمیان بہتر trade-off دیتا ہے۔ HNSW کو empty table پر بھی بنایا جا سکتا ہے، جبکہ IVFFlat کو پہلے representative data پر k-means چلانا پڑتا ہے۔ اس لیے empty table پر IVFFlat بنانے سے recall کمزور رہتا ہے۔ نئے schema میں HNSW وہ index ہے جسے آپ پہلے سے بنا سکتے ہیں۔
ef_search: build کے بعد tune کیا جانے والا knob
m اور ef_construction index میں مستقل طور پر محفوظ ہوتے ہیں۔ ef_search ایسا نہیں ہے۔ یہ طے کرتا ہے کہ graph میں تلاش کے دوران search کتنے candidates برقرار رکھے گی، اور آپ اسے ہر session یا query کے لیے تبدیل کر سکتے ہیں۔
SET hnsw.ef_search = 100;
SELECT id, body FROM chunks ORDER BY embedding <=> $1 LIMIT 5;default قدر 40 ہے۔ اسے بڑھانے سے recall بڑھتا ہے اور latency بھی بڑھتی ہے۔ اسے کم کرنے سے دونوں کم ہو جاتے ہیں۔ یہ واحد recall control ہے جسے rebuild کے بغیر تبدیل کیا جا سکتا ہے۔ اس لیے اسے queries کے ایسے مقررہ مجموعے پر tune کریں جن کے درست answers آپ پہلے سے جانتے ہوں، اور اس وقت رک جائیں جب recall میں مزید بہتری نہ آئے۔
ایک اہم مسئلہ ہے۔ منتخب WHERE clause کے ساتھ ef_search کا تعامل خراب ہو سکتا ہے، کیونکہ index candidates کی ایک مقررہ تعداد واپس کرتا ہے اور filter بعد میں لاگو ہوتا ہے۔ جو filter زیادہ تر rows مسترد کر دے، وہ آپ کو LIMIT سے کم results دے سکتا ہے، حالانکہ matching rows table میں موجود ہوتی ہیں۔ pgvector 0.8 اس مسئلے کا حل iterative scans کے ذریعے فراہم کرتا ہے:
SET hnsw.iterative_scan = relaxed_order;اس کے بعد index مزید candidates کے لیے دوبارہ scan ہوتا رہتا ہے، یہاں تک کہ limit پوری ہو جائے، یا hnsw.max_scan_tuples تک پہنچ جائے۔ hnsw.max_scan_tuples کی default قدر 20000 ہے۔ strict_order distance کی درست ordering برقرار رکھتا ہے، لیکن اس کی لاگت زیادہ ہے۔ Ubuntu 0.6.0 package میں یہ feature موجود نہیں ہے، اور filter کے تحت rows کا غائب ہونا اسی کا پتا دیتا ہے۔
انڈیکس کا RAM میں فِٹ ہونا کیوں ضروری ہے
HNSW search گراف پر چلنے والا عمل ہے۔ ہر hop ایک ایسے node کو پڑھتا ہے جو پچھلے node سے غیر متعلقہ جگہ پر محفوظ ہوتا ہے، اس لیے access pattern تقریباً random ہوتا ہے اور read-ahead مدد نہیں کرتا۔ جب گراف RAM میں ہو تو ہر hop ایک memory reference ہوتا ہے۔ جب گراف RAM میں نہ رہے تو hop ایک disk read بن سکتا ہے، اور چند سو nodes کو چھونے والی search چند سو reads میں تبدیل ہو سکتی ہے۔
Qdrant کی documentation اس کی واضح صورت پیش کرتی ہے: "اگر آپ RAM میں آدنے vectors محفوظ کریں تو search latency تقریباً دوگنی ہو جائے گی۔" منصوبہ بندی اسی اصول کو سامنے رکھ کر کریں۔
جب index حقیقتاً RAM میں فِٹ نہ ہو تو ہر اختیار ایک trade-off ہے، اور اسے جان بوجھ کر منتخب کریں۔
- vectors کو memory-map کریں تاکہ operating system گرم pages کو cache میں رکھے اور غیر فعال pages کو disk پر چھوڑ دے۔ اسے قابلِ قبول رکھنے کے لیے نیچے تیز NVMe (non-volatile memory express) storage درکار ہے۔
- Quantise کریں اور ہر dimension کو چار کے بجائے ایک byte کے طور پر محفوظ کریں۔ اس سے vector bytes چار گنا کم ہو جاتے ہیں، جبکہ recall میں معمولی اور قابلِ پیمائش کمی آتی ہے۔
- pgvector میں
halfvecپر cast کریں۔ اس سے bytes نصف ہو جاتے ہیں اور ایک-byte quantisation کے مقابلے میں recall کا نقصان کم ہوتا ہے۔ - چھوٹے model کے ساتھ embed کریں۔ یہ سب سے سستا حل ہے، لیکن لوگ اسے چھوڑ دیتے ہیں کیونکہ اس کے لیے 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 models تبدیل کیے، لیکن دوبارہ embedding نہیں بنائی۔ یہاں جزوی اصلاح ممکن نہیں، کیونکہ دو مختلف models کے vectors بالکل قابلِ موازنہ نہیں ہوتے۔ اس لیے ہر row کو دوبارہ generate کرنا ہوگا۔
Query سست ہے اور EXPLAIN sequential scan دکھاتا ہے۔ Index operator class اور query operator ایک دوسرے سے مطابقت نہیں رکھتے۔ vector_cosine_ops صرف <=> کے لیے کام کرتا ہے۔ Query پر EXPLAIN ANALYZE چلائیں اور Index Scan using ... on chunks تلاش کریں۔ اگر اس کے بجائے Seq Scan on chunks نظر آئے تو index کو اس operator class کے ساتھ دوبارہ بنائیں جو آپ کے استعمال کردہ operator سے مطابقت رکھتی ہو۔
LIMIT سے کم rows ہیں، اور WHERE clause موجود ہے۔ یہ اوپر بیان کیا گیا filtering trap ہے۔ hnsw.ef_search بڑھائیں، یا pgvector 0.8 پر منتقل ہو کر hnsw.iterative_scan set کریں۔
Index build کے اختتام پر process ختم ہو جاتا ہے اور psql میں کوئی error نہیں ہوتا۔ اگر maintenance_work_mem کو machine کی زیادہ تر memory پر set کیا جائے، جبکہ shared_buffers اور آپ کی application کو بھی memory درکار ہو، تو kernel out-of-memory killer process ختم کر دیتا ہے۔ sudo dmesg -T | grep -i 'killed process' میں وہ line دکھائی دیتی ہے جس میں postgres کا نام ہوتا ہے۔ Setting کم کریں، یا بڑے plan پر index بنائیں اور dump restore کریں۔
انتخاب
اگر آپ پہلے سے Postgres چلا رہے ہیں اور آپ کے پاس چند ملین سے کم vectors ہیں، تو pgvector استعمال کریں۔ index ڈیٹا کے ساتھ ہی موجود رہتا ہے، filtering ایک WHERE clause ہے، اور آپ کا موجودہ backup پہلے ہی اسے شامل کرتا ہے۔ اگر index اتنا بڑا ہے کہ اس کے لیے الگ memory ceiling درکار ہو، یا آپ کو payload کی بھاری filtering چاہیے، تو اس کے ساتھ Qdrant چلائیں اور ایک دوسری service کے انتظام کی ذمہ داری قبول کریں۔
تقریباً ایک لاکھ vectors سے کم تعداد میں کچھ بھی install کرنے سے پہلے brute-force scan کی پیمائش کریں۔ اس حجم پر perfect recall کے ساتھ exhaustive search، اور کسی build step کے بغیر، کوئی سمجھوتا نہیں ہے۔ یہی درست انتخاب ہے۔ اس کے بجائے approximate index اختیار کرنے کا مطلب tuning اور RAM pressure قبول کرنا ہے، صرف ان milliseconds کے بدلے جو آپ پہلے خرچ ہی نہیں کر رہے تھے۔
FAQ
کیا مجھے dedicated vector database درکار ہے، یا Postgres کافی ہے؟
اگر آپ کا data پہلے ہی Postgres میں موجود ہے تو pgvector، زیادہ تر تقابلی جائزوں کے اندازے سے کہیں زیادہ عرصے تک کافی رہتا ہے۔ یہ vectors کو ایک عام column میں محفوظ کرتا ہے، اس لیے filtered search ایک WHERE clause ہے اور آپ کا موجودہ backup index کو بھی شامل کر لیتا ہے۔ Qdrant جیسے dedicated store پر اس وقت منتقل ہوں جب vector workload کے لیے اپنی memory ceiling درکار ہو، یا جب آپ کو payload filtering اور quantisation چاہیے جو pgvector فراہم نہیں کرتا۔
ایک VPS میں کتنے vectors رکھے جا سکتے ہیں؟
اندازہ لگانے کے بجائے number_of_vectors * dimensions * 4 bytes * 1.5 استعمال کرکے حساب کریں۔ 768 dimensions کے 1 million vectors تقریباً 4.3 GiB بنتے ہیں، اس لیے 8 GB plan میں Postgres کے لیے بھی گنجائش رہتی ہے۔ 3072 dimensions کے 1 million vectors تقریباً 17 GiB بنتے ہیں اور اس کے لیے کہیں بڑا plan درکار ہوتا ہے۔ اس مقدار پر سب سے زیادہ اثر embedding model کی dimension کا ہوتا ہے، اس لیے memory cost کو مدنظر رکھتے ہوئے model منتخب کریں۔
جب index اسی machine پر ہو تو میری vector search سست کیوں ہے؟
ایک ہی machine پر network آپ کا مسئلہ نہیں ہے، اس لیے ان دو چیزوں کو دیکھیں جو واقعی اثر انداز ہوتی ہیں۔ پہلے embedding call کا وقت الگ سے ناپیں، کیونکہ CPU پر query vector تیار کرنے میں اکثر خود search سے کہیں زیادہ وقت لگتا ہے۔ دوسرے، تصدیق کریں کہ index RAM میں موجود ہے۔ HNSW search graph میں random hops کرتی ہے، اس لیے graph کے disk پر منتقل ہونے کے بعد ہر hop ایک disk read بن سکتا ہے۔ Qdrant کی اپنی guidance کے مطابق RAM میں رکھے گئے vectors کی تعداد نصف کرنے سے search latency تقریباً دگنی ہو جاتی ہے۔
کیا مجھے HNSW index بنانا بھی چاہیے؟
تقریباً ایک لاکھ vectors سے کم تعداد میں نہیں۔ Exhaustive scan ہر query پر n * d * 4 bytes پڑھتا ہے۔ 768 dimensions کے 100,000 vectors پر یہ 307 MB بنتا ہے، اور جدید CPU اسے مکمل recall اور بغیر build step کے دسیوں milliseconds میں stream کر سکتا ہے۔ پہلے اپنے hardware پر scan کی رفتار ناپیں۔ Index اسی وقت بنائیں جب ناپا گیا scan time واقعی بہت سست ہو، نہ کہ اس لیے کہ کسی benchmark article میں ایسا کہا گیا ہے۔
m بڑھانے کی اصل لاگت کیا ہے؟
Memory سے کہیں زیادہ build time اور insert time۔ 768 dimensions پر default m = 16 سے m = 64 تک جانے سے ہر vector کے لیے graph links میں 512 bytes کا اضافہ ہوتا ہے، جبکہ vector data 3072 bytes کا ہوتا ہے، اس لیے مجموعی memory تقریباً 12 percent بڑھ جاتی ہے۔ تاہم ہر insert کو چار گنا زیادہ neighbours تلاش کرکے link کرنا پڑتا ہے۔ پہلے ef_search کو tune کریں، کیونکہ اسے تبدیل کرنے کے لیے کچھ خرچ نہیں ہوتا اور rebuild بھی درکار نہیں ہوتی۔