SSD Nodes Learn Hosting plans →
تعلیمی Matt Connorتحریر: Matt Connor

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 کا مکمل راستہ دیکھیں۔

  1. embedding model query text کو vector میں تبدیل کرتا ہے۔ CPU پر مختصر string کے لیے اس میں دسیوں سے سیکڑوں milliseconds لگتے ہیں۔ GPU پر یہ وقت single digits تک ہوتا ہے۔
  2. vector کو store کو بھیجا جاتا ہے۔ loopback TCP یا Unix domain socket پر اس میں millisecond کا معمولی حصہ لگتا ہے۔
  3. store اپنے index میں تلاش کرتا ہے اور قریب ترین rows واپس کرتا ہے۔
  4. آپ کا 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 کا موازنہ زیادہ مفید مطالعہ ہے۔

pgvector، جب آپ پہلے ہی Postgres چلا رہے ہوں

اگر آپ کی application میں پہلے ہی Postgres database موجود ہے تو pgvector نئی سطحِ پیچیدگی میں سب سے کم اضافہ کرتا ہے۔ یہ ایک extension ہے، الگ service نہیں۔ Vectors ایک عام table میں ان rows کے ساتھ محفوظ رہتے ہیں جن کی وہ نمائندگی کرتے ہیں، اس لیے filtered search دوسرے system کے بجائے ایک WHERE clause بن جاتی ہے جسے الگ سے sync رکھنا پڑتا۔

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;'

اکتوبر 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-pgvector

17 کو اپنے server کے major version سے بدلیں، جسے sudo -u postgres psql -tAc 'SHOW server_version' ظاہر کرتا ہے۔ غلط major version کے لیے بنایا گیا extension package install کرنے سے CREATE EXTENSION fail ہو جاتا ہے، کیونکہ Postgres صرف چلنے والے version کی share directory میں تلاش کرتا ہے:

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

Schema عام 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 کے مطابق متعلقہ طریقہ استعمال کریں۔ غلط طریقہ منتخب کرنے سے کوئی error ظاہر نہیں ہوتا، لیکن نتائج خاموشی سے خراب ہو جاتے ہیں۔

Index کے بغیر یہ query ہر row پر exact search کرتی ہے۔ یہ اوپر بیان کردہ brute force کا Postgres ورژن ہے اور اس کی recall بھی مکمل ہوتی ہے۔ max_parallel_workers_per_gather بڑھانے سے اس کام کے لیے مزید cores استعمال ہوتے ہیں۔ پہلے یہ کریں اور index بعد میں بنائیں، کیونکہ اب آپ کے پاس recall کا baseline ہوگا جس سے index کا موازنہ کیا جا سکے گا۔

Qdrant، جب index database سے بڑا ہو جائے

Qdrant، Rust میں لکھا گیا ایک dedicated vector store ہے۔ یہ اس وقت مفید ہوتا ہے جب index اتنا بڑا ہو جائے کہ آپ اس کی build کو اپنی application کے Postgres کے ساتھ وسائل میں مقابلہ کرتے نہیں دیکھنا چاہتے، یا جب آپ payload filtering اور built-in scalar یا product quantisation چاہتے ہوں، جبکہ pgvector documentation صرف half precision اور binary quantisation فراہم کرتی ہے۔

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

Port 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 آپ کے server کے لیے درست configuration رکھتا ہے، یہ معمول کا 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 کے لیے 4 bytes ایک 32-bit float کے برابر ہیں۔ Qdrant کی collections کے بارے میں گائیڈ، جو RAM سے بڑی ہو جائیں، metadata، indexes اور optimisation کے دوران بننے والے عارضی segments کے لیے 1.5 کا multiplier شامل کرتی ہے:

memory_size = number_of_vectors * vector_dimension * 4 bytes * 1.5

یہاں یہی formula دس لاکھ vectors پر لاگو کیا گیا ہے، ان dimensions کے ساتھ جو حقیقی embedding models تیار کرتے ہیں۔

چارٹRAM for 1 million vectors, by embedding dimension
اس چارٹ کے پیچھے کا ڈیٹا
[
  {
    "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) میں ہے، کوئی حقیقی measurement نہیں۔ انہیں اس memory space کے حجم کے طور پر سمجھیں جو آپ کو پہلے سے مختص رکھنا ہوگا۔ 768-dimension model، جیسے nomic-embed-text، دس لاکھ chunks کے لیے تقریباً 4.29 GiB درکار کرتا ہے۔ یہ 8 GB plan میں Postgres کے لیے کچھ گنجائش کے ساتھ فٹ ہو جاتا ہے۔ اسی corpus کو 3072 dimensions پر رکھنے کے لیے 17.17 GiB درکار ہوتے ہیں، اس لیے یہ فٹ نہیں ہوتا۔

اصل اختیار chart کے آخری column میں نہیں بلکہ پہلے 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 تک کام کرتے ہیں۔ halfvec cast کے ذریعے یہ حد 4,000 تک پہنچ جاتی ہے۔ اس سے زیادہ کے لیے pgvector binary quantisation تجویز کرتا ہے: binary_quantize(embedding)::bit(N) expression کو bit_hamming_ops کے ساتھ index کریں۔ یہ زیادہ سے زیادہ 64,000 dimensions تک کام کرتا ہے، پھر اصل vectors کے مقابلے میں ابتدائی امیدواروں کی دوبارہ ranking کریں۔

تعمیر کے وقت m اور ef_construction کی لاگت

HNSW (hierarchical navigable small world) وہ index ہے جسے pgvector اور Qdrant دونوں استعمال کرتے ہیں۔ یہ تہہ دار graph ہوتا ہے۔ ہر vector ایک node ہوتا ہے، جس کے قریبی nodes سے links ہوتے ہیں۔ search ہر چیز پڑھنے کے بجائے query کی سمت ان links پر آگے بڑھتی ہے۔

m سے مراد ہے کہ ہر node کتنے links رکھتا ہے۔ Faiss documentation کے مطابق HNSW کی memory استعمال فی vector (d * 4 + m * 2 * 4) bytes ہوتی ہے، اور m کو 4 اور 64 کے درمیان رکھنے کی سفارش کی جاتی ہے۔ 768 dimensions اور 1 million vectors کے ساتھ یہ حساب چلائیں۔

چارٹCost of raising m at 768 dimensions, 1 million vectors
اس چارٹ کے پیچھے کا ڈیٹا
[
  {
    "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 پر جانے سے links فی vector 128 bytes سے بڑھ کر 512 bytes ہو جاتے ہیں۔ 3072 bytes کے vector data کے مقابلے میں یہ اضافی 384 bytes ہیں، اس لیے مجموعی سائز 2.98 GiB سے بڑھ کر 3.34 GiB ہو جاتا ہے۔ یہ تقریباً 12 فیصد ہے۔ ان dimensions پر m آپ کی زیادہ memory استعمال نہیں کرتا۔ vectors کرتے ہیں۔

m کی اصل لاگت build time اور insert time میں آتی ہے، کیونکہ node شامل کرنے کے لیے اتنے ہی neighbours تلاش کر کے ان سے links بنانا پڑتے ہیں۔ 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 میں minutes لگیں گے یا hours، کیونکہ pgvector graph کو اسی وقت 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 کی چھاپی ہوئی سب سے مفید سطر ہے۔ اس کا مطلب ہے کہ 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 کا مسئلہ ہے، query کے stuck ہونے کا نہیں۔

دو حقائق کو planning میں شامل کریں۔ 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 پر بنانے سے 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، زیادہ تر comparisons کے اندازوں سے کہیں زیادہ عرصے تک کافی رہتا ہے۔ یہ vectors کو ایک عام column میں محفوظ کرتا ہے، اس لیے filtered search ایک WHERE clause ہوتی ہے اور آپ کا موجودہ backup index کو بھی cover کرتا ہے۔ Qdrant جیسے dedicated store پر اس وقت منتقل ہوں جب vector workload کے لیے اپنی memory ceiling درکار ہو، یا جب payload filtering اور scalar یا product quantisation درکار ہو جو pgvector فراہم نہیں کرتا۔

ایک VPS میں کتنے vectors رکھے جا سکتے ہیں؟

اندازہ لگانے کے بجائے number_of_vectors * dimensions * 4 bytes * 1.5 استعمال کر کے حساب کریں۔ 768-dimension کے 1 million vectors تقریباً 4.3 GiB کے برابر ہوتے ہیں، اس لیے 8 GB plan میں Postgres کے لیے بھی کچھ گنجائش رہتی ہے۔ 3072-dimension کے 1 million vectors تقریباً 17 GiB کے برابر ہوتے ہیں اور ان کے لیے کہیں بڑے plan کی ضرورت ہوتی ہے۔ اس مقدار پر سب سے زیادہ اثر آپ کے embedding model کی dimension کا ہوتا ہے، اس لیے memory bill کو مدنظر رکھتے ہوئے model منتخب کریں۔

ایک ہی box پر 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 بنانا بھی چاہیے؟

تقریباً 100,000 vectors سے کم تعداد پر نہیں۔ ایک exhaustive scan ہر query کے لیے n * d * 4 bytes پڑھتا ہے۔ 768 dimensions کے 100,000 vectors پر یہ 307 MB بنتا ہے، اور جدید CPU اسے perfect recall اور کسی build step کے بغیر دسیوں milliseconds میں stream کر سکتا ہے۔ پہلے اپنے hardware پر scan کی پیمائش کریں۔ Index اسی وقت بنائیں جب measured scan time واقعی بہت سست ہو، نہ کہ صرف اس لیے کہ کسی benchmark article میں ایسا کہا گیا ہے۔

m بڑھانے کی اصل قیمت کیا ہے؟

Memory سے کہیں زیادہ build time اور insert time۔ 768 dimensions پر default m = 16 سے m = 64 تک جانے سے graph links 128 سے بڑھ کر 512 bytes فی vector ہو جاتے ہیں۔ یہ 3072 bytes کے vector data کے مقابلے میں اضافی 384 bytes ہیں، اس لیے مجموعی memory تقریباً 12 percent بڑھتی ہے۔ تاہم ہر insert کو چار گنا زیادہ neighbours تلاش کر کے link کرنا پڑتا ہے۔ پہلے ef_search کو tune کریں، کیونکہ اسے تبدیل کرنے کی کوئی لاگت نہیں اور rebuild کی ضرورت بھی نہیں ہوتی۔

کیا یہ مضمون مفید تھا؟

#vector-database#rag#pgvector#qdrant#self-hosting