اپنے VPS پر RAG پائپ لائن کیسے بنائیں
اپنے VPS پر مکمل RAG پائپ لائن سیٹ اپ کریں۔ اس گائیڈ میں pgvector اسکیما، HNSW انڈیکسنگ، اور مقامی ایمبیڈنگ ماڈل کے ساتھ ڈیٹا ریٹریول کا مکمل SQL کوڈ شامل ہے جو کارکردگی ثابت کرتا ہے۔
خود میزبان (self-hosted) RAG پائپ لائن کی ساخت
ایک RAG (retrieval augmented generation) پائپ لائن پانچ مراحل پر مشتمل ہوتی ہے: دستاویزات کے ٹکڑے (chunks) کرنا، ان ٹکڑوں کو embed کرنا، vectors کو ذخیرہ کرنا، کسی سوال کے لیے قریب ترین ٹکڑوں کو تلاش کرنا، اور ان ٹکڑوں کو ایک لینگویج ماڈل کو بھیجنا جو جواب لکھتا ہے۔ آپ کے کرائے پر موجود VPS پر، پہلے چار مراحل اسی مشین پر چلتے ہیں۔ PostgreSQL بمعہ pgvector ایکسٹینشن vectors کو محفوظ رکھتی ہے، اور Ollama کے ذریعے چلنے والا ایک چھوٹا ایمبیڈنگ ماڈل متن کو vectors میں تبدیل کرتا ہے۔ صرف آخری مرحلے کو مشین سے باہر جانے کی ضرورت ہوتی ہے۔
یہ تقسیم ہی اس گائیڈ کا بنیادی نکتہ ہے۔ Chunking خالصتاً CPU کا کام ہے۔ ایمبیڈنگ 137 ملین پیرامیٹرز کا ایک ماڈل ہے جو چند سو میگا بائٹس RAM میں سما جاتا ہے۔ اسٹوریج ایک Postgres ٹیبل ہے جس کا سائز آپ پہلی قطار لکھنے سے پہلے ہی ریاضی کے ذریعے معلوم کر سکتے ہیں۔ لاکھوں ٹکڑوں پر مشتمل مواد کے لیے، یہ سب کچھ ایک عام VPS پر چل سکتا ہے۔ جنریشن کا معاملہ مختلف ہے، کیونکہ اس میں ہر سوال پر مستقل بنیادوں پر لاگت آتی ہے۔
RAG پائپ لائن کے کون سے حصے اصل میں مہنگے پڑتے ہیں
DigitalOcean کا end to end RAG ٹیوٹوریل ایک managed vector database اور hosted embedding ماڈل کے استعمال کا مشورہ دیتا ہے، اور اس کا لاگت والا حصہ صرف عمومی نوعیت کا ہے: بار بار پوچھے گئے سوالات کو cache کریں، retrieved chunks کی تعداد کم رکھیں، اور generation سے پہلے rerank کریں۔ یہ مشورہ درست ہے۔ لیکن یہ اس آپشن کو نظر انداز کرتا ہے جو حساب کتاب کو مکمل بدل دیتا ہے، یعنی embedding ماڈل کو اسی سرور پر چلانا جس کے لیے آپ پہلے ہی ادائیگی کر رہے ہیں۔
ڈالرز کے بجائے tokens گنیں، کیونکہ قیمتوں کی فہرست تبدیل ہونے پر token کی تعداد پرانا نہیں ہوتی۔ فرض کریں کہ 400 tokens فی chunk کے حساب سے 100,000 chunks کا ایک corpus ہے، اس پر 10,000 سوالات پوچھے جاتے ہیں، ہر جواب کے لیے 8 chunks ماڈل کو بھیجے جاتے ہیں، 100 tokens کا سوال اور انسٹرکشن بلاک ہے، اور 400 tokens کے جوابات ہیں۔
The data behind this chart
[
{
"label": "Embed the corpus (once)",
"tokens_millions": 40,
"tokens_per_question": "4,000"
},
{
"label": "Embed each question",
"tokens_millions": 0.2,
"tokens_per_question": "20"
},
{
"label": "Generation input",
"tokens_millions": 33,
"tokens_per_question": "3,300"
},
{
"label": "Generation output",
"tokens_millions": 4,
"tokens_per_question": "400"
}
]پورے corpus کی embedding 40 ملین tokens بنتی ہے، اور یہ کام صرف ایک بار ہوتا ہے۔ ان 10,000 سوالات پر تقسیم کریں تو یہ فی سوال 4,000 tokens بنتے ہیں۔ اگر آپ ایک لاکھ سوالات پوچھیں تو یہ تعداد 400 تک گر جاتی ہے۔ Generation کی لاگت کبھی کم نہیں ہوتی۔ یہ ہر اس سوال پر 3,300 tokens in اور 400 tokens out خرچ کرتی ہے جس کا آپ کبھی جواب دیں گے۔
لہذا پیسہ اس مرحلے پر خرچ ہوتا ہے جو بار بار دہرایا جاتا ہے۔ Embedding کے مرحلے کو خود سنبھالیں، کیونکہ آپ اس کی قیمت صرف ایک بار ادا کرتے ہیں اور VPS تو ویسے بھی چل رہا ہوتا ہے۔ Generation کے مرحلے کو خریدیں، کیونکہ وہاں ایک بہتر ماڈل اصل قیمت کا مستحق ہوتا ہے۔ Caching اسی وجہ سے اہم ہے: ایک cache hit اس واحد مرحلے کو چھوڑ دیتی ہے جس کی لاگت کبھی بھی amortise نہیں ہوتی۔ KV cache اور prompt cache کے درمیان فرق یہ طے کرتا ہے کہ آپ اس کا کون سا حصہ دوبارہ استعمال کر سکتے ہیں، اور RAG پرامپٹ میں ایک مستقل انسٹرکشن بلاک ہوتا ہے جس کے بعد ایک تبدیل ہونے والا chunk بلاک آتا ہے، اور یہی وہ ساخت ہے جسے اس سے سب سے زیادہ فائدہ ہوتا ہے۔
Chunking: فکسڈ سائز اور اوورلیپ ڈیفالٹ کے لیے بہترین انتخاب کیوں ہے
Chunk وہ بنیادی اکائی ہے جسے آپ retrieve کرتے ہیں، لہذا اس کا سائز downstream کے تمام مراحل کا تعین کرتا ہے۔ اس کا سائز اتنا چھوٹا ہونا چاہیے کہ اس کی embedding صرف ایک موضوع پر مرکوز ہو، کیونکہ embedding اسپیس میں ایک واحد نقطہ ہوتی ہے، اور اگر ایک chunk چار مختلف موضوعات پر محیط ہو تو وہ ان کے درمیان کہیں واقع ہوگا اور کسی کے بھی قریب نہیں ہوگا۔ اس کا سائز اتنا بڑا ہونا چاہیے کہ وہ خود ہی کسی سوال کا جواب دے سکے، کیونکہ language model صرف chunk کو دیکھتا ہے، اس کے ارد گرد موجود پورے document کو نہیں۔
300 الفاظ سے شروعات کریں جس میں 50 الفاظ کا اوورلیپ ہو۔ انگریزی میں تقریباً 1.3 ٹوکن فی لفظ ہوتے ہیں، لہذا 300 الفاظ تقریباً 400 ٹوکن کے برابر ہیں۔ اوورلیپ اس لیے ضروری ہے کیونکہ جو جملہ boundary پر آتا ہے وہ بصورت دیگر آدھا کٹ جائے گا، اور دونوں میں سے کوئی بھی حصہ سوال کا جواب دینے کے قابل نہیں رہے گا۔
اگر دستاویزات میں ڈھانچہ موجود ہو تو پہلے structure کی بنیاد پر تقسیم کریں۔ headings پر توڑیں، پھر paragraphs پر، اور فکسڈ سائز کا اصول صرف اسی حصے پر لاگو کریں جو اب بھی بہت طویل ہو۔ جو chunk جملے کے درمیان سے شروع ہوتا ہے وہ حتمی جواب میں برا لگتا ہے، کیونکہ ماڈل وہی کچھ quote کرتا ہے جو آپ اسے فراہم کرتے ہیں۔
Chunking کو تب تک تبدیل نہ کریں جب تک آپ اسے measure نہ کر سکیں۔ اوورلیپ کے ساتھ فکسڈ سائز کا طریقہ کار deterministic ہے اور اسے دوبارہ چلانا سستا ہے، جو اسے ایک ایسا baseline بناتا ہے جسے آپ بہتر بنا سکتے ہیں۔ پہلے scoring query کو بہتر بنائیں، پھر ایک وقت میں صرف ایک چیز تبدیل کریں۔
ایک ہی سرور پر ایمبیڈنگ اور اس کی RAM اور لیٹنسی کی قیمت
curl -fsSL https://ollama.com/install.sh | sh
ollama pull nomic-embed-textnomic-embed-text میں 137 ملین پیرامیٹرز ہیں اور اگست 2026 تک اس کا ڈاؤن لوڈ سائز 274 MB ہے۔ اس کے گرد ٹیبل ڈیزائن کرنے سے پہلے چیک کریں کہ یہ کیا آؤٹ پٹ دیتا ہے۔
curl -s http://127.0.0.1:11434/api/embed \
-d '{"model": "nomic-embed-text", "input": "search_document: hello"}' |
python3 -c 'import json,sys; print(len(json.load(sys.stdin)["embeddings"][0]))'یہ 768 پرنٹ کرتا ہے۔ آپ کے کالم کی قسم کو بالکل اس نمبر سے مطابقت رکھنی چاہیے۔
اس ماڈل کی دو سیٹنگز لوگوں کو اکثر الجھا دیتی ہیں۔
ٹاسک پریفکس اختیاری نہیں ہے۔ Nomic کے ماڈل کارڈ کے مطابق ان پٹ میں "ٹاسک انسٹرکشن پریفکس" شامل ہونا لازمی ہے۔ دستاویزات کو search_document: کے ساتھ اور سوالات کو search_query: کے ساتھ ایمبیڈ کیا جاتا ہے۔ اگر آپ انہیں چھوڑ دیں تو کوئی ایرر نہیں آئے گا: آپ کو ویکٹرز مل جائیں گے، لیکن ریٹریول کوالٹی گر جائے گی اور لاگز میں اس کی کوئی وجہ درج نہیں ہوگی۔
طویل ان پٹ خاموشی سے کاٹ دی جاتی ہے۔ /api/embed اینڈ پوائنٹ truncate فیلڈ لیتا ہے جس کی ڈیفالٹ ویلیو true ہے، اور Ollama کی طرف سے پیک کیا گیا ماڈل 2K کانٹیکسٹ کا دعویٰ کرتا ہے۔ اس سے لمبا چنک حد پر کاٹ دیا جاتا ہے اور پھر بھی ایمبیڈ ہو جاتا ہے، جس کی وجہ سے اس کا آخری حصہ سرچ ایبل نہیں رہتا۔ ٹیسٹنگ کے دوران "truncate": false بھیجیں، تاکہ حد سے بڑا چنک پاس ہونے کے بجائے فیل ہو جائے۔
درخواستوں کو بیچ (batch) کریں اور ماڈل کو میموری میں موجود رکھیں۔
curl -s http://127.0.0.1:11434/api/embed -d '{
"model": "nomic-embed-text",
"input": ["search_document: first chunk", "search_document: second chunk"],
"keep_alive": "30m"
}' > /dev/nullinput ایک لسٹ قبول کرتا ہے، اور 32 چنکس پر مشتمل ایک درخواست 32 الگ الگ درخواستوں سے بہتر ہے، کیونکہ HTTP راؤنڈ ٹرپ اور ماڈل لک اپ 32 بار کے بجائے صرف ایک بار ہوتا ہے۔ keep_alive کنٹرول کرتا ہے کہ درخواست کے بعد ماڈل کتنی دیر تک میموری میں رہے گا، اور اس کی ڈیفالٹ ویلیو 5 منٹ ہے۔ جب یہ وقت ختم ہو جاتا ہے، تو اگلی درخواست کو دوبارہ لوڈنگ ٹائم برداشت کرنا پڑتا ہے۔
اپنے سرور پر ان دو اہم نمبروں کی پیمائش کریں۔ ان کا انحصار آپ کے vCPU کی تعداد پر ہے، اس لیے کوئی بھی شائع شدہ اعداد و شمار آپ کے سسٹم سے مطابقت نہیں رکھیں گے۔
ollama ps
time curl -s http://127.0.0.1:11434/api/embed \
-d '{"model":"nomic-embed-text","input":"search_document: ... one real chunk ..."}' > /dev/nullollama ps لوڈ شدہ ماڈل کا ریزیڈنٹ سائز پرنٹ کرتا ہے، جو کہ وہ RAM ہے جو آپ نے اس وقت تک مختص کی ہے جب تک keep_alive اسے ہولڈ رکھتا ہے۔ time کی آؤٹ پٹ کو بیچ سائز پر تقسیم کرنے سے آپ کو فی چنک سیکنڈز مل جائیں گے۔ اسے کل چنکس کی تعداد سے ضرب دیں تاکہ انڈیکسنگ کی کل لاگت معلوم ہو سکے۔ صرف CPU والے پلان پر، 100,000 چنکس کے کارپس میں منٹوں کے بجائے گھنٹے لگ سکتے ہیں۔ یہ ٹھیک ہے، کیونکہ یہ کام صرف ایک بار ہوتا ہے اور اسے nice -n 19 کے تحت رات بھر چلایا جا سکتا ہے۔ اگر گھنٹوں کا وقت قابل قبول نہیں ہے، تو اصل سوال یہ ہے کہ کیا GPU کرائے پر لینا فائدہ مند ہے، اور یہ API ٹوکنز کے مقابلے میں بریک ایون کیلکولیشن کا معاملہ ہے، نہ کہ ترجیح کا۔
اگر سرور پہلے ہی چیٹ ماڈل چلا رہا ہے، تو ایمبیڈنگ ماڈل دوسرا ریزیڈنٹ ماڈل ہوگا اور RAM کا استعمال بڑھ جائے گا۔ Ollama کو VPS پر چلانا جنریشن سائیڈ کی سائزنگ کا احاطہ کرتا ہے، اور سیلف ہوسٹڈ ماڈل کا کنکرنٹ صارفین کے ساتھ رویہ اس بات کی وضاحت کرتا ہے کہ جب کئی لوگ ایک ساتھ سوال کرتے ہیں تو کیا ہوتا ہے۔ ایمبیڈنگ ماڈل اتنا چھوٹا ہے کہ وہ کسی بھی دوسرے ماڈل کے ساتھ آرام سے رہ سکتا ہے۔
انڈیکسنگ اسکرپٹ، شروع سے آخر تک
Ubuntu 24.04 پر ورچوئل انوائرمنٹ کے باہر ایک سادہ pip install، error: externally-managed-environment کے ساتھ رک جاتا ہے، کیونکہ سسٹم کا Python، apt کی ملکیت ہے۔
python3 -m venv ~/rag
~/rag/bin/pip install "psycopg[binary]" pgvectorimport json, urllib.request
import psycopg
from pgvector.psycopg import register_vector
from pgvector import Vector
OLLAMA = "http://127.0.0.1:11434/api/embed"
MODEL = "nomic-embed-text"
def embed(texts, prefix="search_document: "):
payload = {"model": MODEL,
"input": [prefix + t for t in texts],
"truncate": False,
"keep_alive": "30m"}
req = urllib.request.Request(OLLAMA, data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
return json.load(resp)["embeddings"]
def split(text, size=300, overlap=50):
words = text.split()
step = size - overlap
return [" ".join(words[i:i + size]) for i in range(0, len(words), step)]
with psycopg.connect("dbname=rag user=rag") as conn:
register_vector(conn)
for doc_id, text in documents(): # your loader
pieces = split(text)
for start in range(0, len(pieces), 32):
batch = pieces[start:start + 32]
vectors = embed(batch)
with conn.cursor() as cur:
cur.executemany(
"INSERT INTO chunks (doc_id, seq, body, embedding)"
" VALUES (%s, %s, %s, %s)",
[(doc_id, start + i, body, Vector(vec))
for i, (body, vec) in enumerate(zip(batch, vectors))])
conn.commit()documents() آپ کا ہے: جو بھی آپ کی فائلز یا روز (rows) کو ریڈ کرے اور ایک ڈاکومنٹ آئی ڈی اور اس کا ٹیکسٹ فراہم کرے۔ باقی سب کچھ پائپ لائن ہے۔
اسٹوریج: pgvector اسکیمہ، اور اس کا حجم
Ubuntu 24.04 میں postgresql-16-pgvector ورژن 0.6.0 کے ساتھ آتا ہے، جو halfvec ٹائپ سے پرانا ہے۔ موجودہ بلڈ کے لیے PostgreSQL پروجیکٹ کی اپنی ریپوزٹری استعمال کریں۔
sudo apt update && sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
sudo apt install -y postgresql-17 postgresql-17-pgvectorپیکیج کے نام میں موجود نمبر کا آپ کے سرور کے میجر ورژن سے مطابقت رکھنا ضروری ہے۔ اس کے بعد رول، ڈیٹا بیس اور ایکسٹینشن بنائیں۔
sudo -u postgres createuser --pwprompt rag
sudo -u postgres createdb --owner rag rag
sudo -u postgres psql -d rag -c 'CREATE EXTENSION vector;'CREATE TABLE chunks (
id bigserial PRIMARY KEY,
doc_id text NOT NULL,
seq int NOT NULL,
body text NOT NULL,
embedding vector(768) NOT NULL,
fts tsvector GENERATED ALWAYS AS (to_tsvector('english', body)) STORED
);
CREATE INDEX chunks_fts ON chunks USING gin (fts);vector(768) کا ماڈل کی آؤٹ پٹ سے مطابقت رکھنا ضروری ہے۔ اگر آپ اس کالم میں 1024 ڈائمینشن کا ویکٹر داخل کریں تو Postgres اسے expected 768 dimensions, not 1024 کے ساتھ مسترد کر دیتا ہے، جو اس پورے پائپ لائن کا سب سے واضح ایرر میسج ہے۔ تخلیق کردہ fts کالم کو برقرار رکھنے کی کوئی قیمت نہیں ہے اور یہ بعد میں آپ کو کی ورڈ سرچ کی سہولت دیتا ہے۔
اسٹوریج کا حساب ریاضیاتی ہے۔ pgvector کے مطابق ایک vector کا حجم 4 * dimensions + 8 بائٹس اور ایک halfvec کا حجم 2 * dimensions + 8 ہے۔ نیچے دیے گئے ڈائمینشنز ہر ماڈل کی شائع شدہ آؤٹ پٹ سائز ہیں۔
The data behind this chart
[
{
"label": "384 (all-minilm)",
"bytes_per_vector": "1,544",
"vector_mib_per_100k": 147,
"halfvec_mib_per_100k": 74
},
{
"label": "768 (nomic-embed-text)",
"bytes_per_vector": "3,080",
"vector_mib_per_100k": 294,
"halfvec_mib_per_100k": 147
},
{
"label": "1024 (mxbai-embed-large)",
"bytes_per_vector": "4,104",
"vector_mib_per_100k": 391,
"halfvec_mib_per_100k": 196
},
{
"label": "1536 (hosted API model)",
"bytes_per_vector": "6,152",
"vector_mib_per_100k": 587,
"halfvec_mib_per_100k": 294
}
]768 ڈائمینشنز پر ہر ویکٹر 3,080 بائٹس کا ہوتا ہے، لہذا 100,000 چنکس 294 MiB ویکٹر ڈیٹا پر مشتمل ہوتے ہیں۔ اسی کارپس کو اگر 1536 ڈائمینشن والے ہوسٹڈ ماڈل سے ایمبیڈ کیا جائے تو اسے 587 MiB کی ضرورت ہوتی ہے، اور اس پر انڈیکس اسی تناسب سے بڑھتا ہے۔ ہاف پریسجن (half precision) دونوں کو آدھا کر دیتی ہے: halfvec(768) اس کارپس کو 147 MiB میں اسٹور کرتا ہے۔ آیا اس سے ریکال (recall) پر کوئی اثر پڑتا ہے یا نہیں، اس کا جواب نیچے دی گئی اسکورنگ کوئری ایک ہی رن میں دے دیتی ہے۔
یہ اعداد و شمار صرف ویکٹر کالم کا احاطہ کرتے ہیں۔ ٹیکسٹ، رو اوور ہیڈ اور انڈیکسز اس کے علاوہ ہوتے ہیں، لہذا اصل ٹیبل کی پیمائش کریں۔
SELECT pg_size_pretty(pg_total_relation_size('chunks')) AS total,
pg_size_pretty(pg_relation_size('chunks')) AS heap,
count(*) AS n_rows
FROM chunks;اگر آپ اسی ایکسٹینشن کو API اور یوزر اکاؤنٹس کے ساتھ استعمال کرنا چاہتے ہیں، تو ایک سیلف ہوسٹڈ Supabase اسٹیک دراصل Postgres ہے جس میں pgvector پہلے سے فعال ہوتا ہے، اور اس گائیڈ کی ہر کوئری وہاں بغیر کسی تبدیلی کے کام کرتی ہے۔
انڈیکسنگ: HNSW کی اہم ترتیبات
چند ہزار قطاروں (rows) سے کم ڈیٹا کے لیے انڈیکس کو نظر انداز کریں۔ اس سائز پر Exact search ہر قطار کو پڑھ لیتی ہے، جو کافی تیز ہے اور اس کی recall مکمل ہوتی ہے۔ جب sequential scan کی رفتار سست پڑ جائے تب انڈیکس شامل کریں، اور اس کے تجارتی پہلو کو سمجھیں: ایک approximate انڈیکس تقریباً درست پڑوسی (neighbours) تلاش کر کے دیتا ہے۔
SET maintenance_work_mem = '2GB';
SET max_parallel_maintenance_workers = 3;
CREATE INDEX chunks_embedding ON chunks
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);m = 16 اور ef_construction = 64، pgvector کی ڈیفالٹ اقدار ہیں۔ ان میں اضافہ recall کو بہتر بناتا ہے لیکن اس سے build کا وقت اور انڈیکس کا سائز بڑھ جاتا ہے۔ <=> آپریٹر کے ساتھ vector_cosine_ops استعمال کریں، جب تک کہ آپ کو یہ معلوم نہ ہو کہ آپ کا ماڈل unit length vectors خارج کرتا ہے، کیونکہ cosine distance ویکٹر کی لمبائی کو نظر انداز کرتا ہے جبکہ inner product ایسا نہیں کرتا۔
build کے عمل پر نظر رکھیں۔ جب گراف maintenance_work_mem سے بڑا ہو جائے تو pgvector اس کی اطلاع دیتا ہے:
NOTICE: hnsw graph no longer fits into maintenance_work_mem after 100000 tuples
DETAIL: Building will take significantly more time.یہ کوئی error نہیں ہے اور build مکمل ہو جاتی ہے، لیکن یہ ایک بہت سست راستے پر منتقل ہو جاتا ہے۔ انڈیکس بنانے والے سیشن میں maintenance_work_mem کی قدر بڑھائیں اور سرور کی ڈیفالٹ ویلیو کو نہ چھیڑیں، کیونکہ یہ ترتیب فی مینٹیننس آپریشن ہوتی ہے اور ایک بڑی گلوبل ویلیو سرور کی میموری ختم کر سکتی ہے۔ ایک طویل build کو دوسرے سیشن سے مانیٹر کریں۔
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%"
FROM pg_stat_progress_create_index;پھر مکمل شدہ انڈیکس کا موازنہ سرور کی میموری سے کریں۔
SELECT pg_size_pretty(pg_relation_size('chunks_embedding'));
SHOW shared_buffers;ایک HNSW سرچ گراف کے ذریعے چلتی ہے، اس لیے یہ انڈیکس میں بکھرے ہوئے صفحات (pages) کو ٹچ کرتی ہے بجائے اس کے کہ ایک رینج کو پڑھے۔ ایسا انڈیکس جو میموری میں فٹ نہ ہو، ہر query کو disk reads میں بدل دیتا ہے، اور صارفین اسی سست روی کو محسوس کرتے ہیں۔ سرور کے سائزنگ کا یہی ایک اصول ہے: انڈیکس اور وہ قطاریں جنہیں آپ درحقیقت serve کر رہے ہیں، RAM میں سما جانی چاہئیں۔ free -m اور اوپر دیا گیا سائز، یہ وہ دو اعداد ہیں جن کا موازنہ کرنا ہے۔
query کے وقت، hnsw.ef_search recall کا کنٹرول ہے اور اس کی ڈیفالٹ ویلیو 40 ہے۔
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT id, body FROM chunks ORDER BY embedding <=> $1 LIMIT 8;
COMMIT;ایک اعلیٰ قدر گراف کے زیادہ حصوں کو تلاش کرتی ہے، بہتر پڑوسی ڈھونڈتی ہے اور اس کی قیمت latency کی صورت میں چکانی پڑتی ہے۔ یہ ایک سیشن کی ترتیب ہے، لہذا آپ انڈیکس کو متاثر کیے بغیر کسی ایک query کے لیے اسے بڑھا سکتے ہیں۔
اگر کوئی query انڈیکس استعمال نہیں کر رہی، تو plan اسے ظاہر کر دیتا ہے۔
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM chunks ORDER BY embedding <=> $1 LIMIT 8;یہاں sequential scan اکثر اسٹوریج کی وجہ سے ہوتا ہے۔ ایک 768 ڈائمینشن والا ویکٹر 3,080 بائٹس کا ہوتا ہے، جو اس سے زیادہ ہے جتنا Postgres ان لائن (inline) رکھتا ہے، اس لیے یہ ویلیو TOAST ٹیبل (بڑے ڈیٹا کے لیے آؤٹ آف لائن اسٹور) میں منتقل ہو جاتی ہے۔ pgvector کا اپنا نوٹ یہ ہے کہ planner اپنی لاگت کے تخمینوں میں آؤٹ آف لائن اسٹوریج کو شمار نہیں کرتا، جس سے serial scan اپنی حقیقت سے زیادہ سستا دکھائی دے سکتا ہے۔ ALTER TABLE chunks ALTER COLUMN embedding SET STORAGE PLAIN; ویکٹرز کو ان لائن رکھتا ہے۔ یہ تبدیلی کے بعد لکھی گئی قطاروں پر لاگو ہوتا ہے، اس لیے موجودہ قطاروں کے لیے ٹیبل کو دوبارہ لکھنا (rewrite) ضروری ہے۔
بازیافت: ایک استفسار، دو سگنلز
ویکٹر سرچ (Vector search) ایسا متن تلاش کرتی ہے جس کا مفہوم سوال جیسا ہو۔ یہ درست سٹرنگز (exact strings) پر کمزور ہے: جیسے پارٹ نمبر، ایرر کوڈ، یا خاندانی نام۔ کی ورڈ سرچ (Keyword search) اس کے برعکس کام کرتی ہے، اور Postgres پہلے ہی یہ سہولت فراہم کرتا ہے۔ ایک دوسرا سسٹم چلانے کے بجائے، انہیں ایک ہی استفسار (query) میں یکجا کریں۔
Reciprocal rank fusion سب سے سادہ اور مؤثر امتزاج کا طریقہ ہے۔ ہر نتیجہ ہر اس فہرست سے 1 / (60 + rank) حاصل کرتا ہے جس میں وہ موجود ہو، اور دونوں سکورز جمع ہو جاتے ہیں۔ اس میں سکور نارملائزیشن کی ضرورت نہیں ہوتی، کیونکہ یہ فاصلوں کے بجائے پوزیشنز کو پڑھتا ہے۔
WITH semantic AS (
SELECT id, row_number() OVER (ORDER BY distance) AS rank
FROM (SELECT id, embedding <=> $1 AS distance
FROM chunks ORDER BY embedding <=> $1 LIMIT 40) s
),
keyword AS (
SELECT id, row_number() OVER (ORDER BY score DESC) AS rank
FROM (SELECT c.id, ts_rank_cd(c.fts, q) AS score
FROM chunks c, websearch_to_tsquery('english', $2) q
WHERE c.fts @@ q
ORDER BY score DESC LIMIT 40) k
)
SELECT c.id, c.body,
coalesce(1.0 / (60 + s.rank), 0) + coalesce(1.0 / (60 + k.rank), 0) AS rrf
FROM (SELECT id FROM semantic UNION SELECT id FROM keyword) u
JOIN chunks c ON c.id = u.id
LEFT JOIN semantic s ON s.id = u.id
LEFT JOIN keyword k ON k.id = u.id
ORDER BY rrf DESC
LIMIT 8;$1 اسی ماڈل سے حاصل کردہ سوال کی ایمبیڈنگ (embedding) ہے، جسے search_query: پریفکس کے ساتھ بنایا گیا ہے۔ $2 سوال بطور متن ہے۔ دونوں آپ کی ایپلیکیشن سے منسلک (bound) ہیں۔ websearch_to_tsquery ایک حقیقی صارف کے سوال کو بغیر پنکچوایشن (punctuation) کے مسائل کے قبول کرتا ہے، جو to_tsquery نہیں کر سکتا۔ ایک اور اہم بات: HNSW اسکین کے اوپر WHERE فلٹر لگانے سے آپ کی مطلوبہ تعداد سے کم قطاریں (rows) مل سکتی ہیں، کیونکہ انڈیکس پہلے تلاش کیا جاتا ہے اور فلٹر بعد میں چلتا ہے۔ SET hnsw.iterative_scan = relaxed_order; pgvector کو اس وقت تک اسکین جاری رکھنے پر مجبور کرتا ہے جب تک کہ مطلوبہ تعداد میں قطاریں حاصل نہ ہو جائیں۔
آپ کو کیسے معلوم ہوگا کہ retrieval کا معیار اچھا ہے؟
یہ وہ مرحلہ ہے جسے تقریباً ہر RAG گائیڈ نظر انداز کر دیتی ہے، اور یہی واحد طریقہ ہے جس سے آپ کو معلوم ہوتا ہے کہ آپ کے کیے گئے دیگر انتخاب کارآمد ثابت ہوئے یا نہیں۔ اس کے لیے کسی پیچیدہ evaluation framework کی ضرورت نہیں ہے۔ آپ کو صرف 30 سوالات اور ہر سوال کا جواب دینے والے chunk کی id درکار ہے۔
انہیں خود ہاتھ سے لکھیں۔ ان سوالات کا انتخاب کریں جو لوگ واقعی اس corpus کے بارے میں پوچھتے ہیں، ہر سوال کو run کریں، واپس آنے والے نتائج کو پڑھیں، اور اس chunk کی id ریکارڈ کریں جسے منتخب ہونا چاہیے تھا۔ 30 سوالات چھوٹے فرق کو تو واضح نہیں کریں گے، لیکن یہ ان بڑے فرقوں کو ضرور پکڑ لیں گے جو اہمیت رکھتے ہیں۔
CREATE TABLE gold (
id bigserial PRIMARY KEY,
question text NOT NULL,
chunk_id bigint NOT NULL REFERENCES chunks(id),
embedding vector(768) NOT NULL
);ہر سوال کو search_query: prefix کے ساتھ embed کریں، اسے store کریں، اور پھر پورے سیٹ کو ایک ہی query میں score کریں۔
WITH hits AS (
SELECT g.id,
min(r.rank) FILTER (WHERE r.id = g.chunk_id) AS hit_rank
FROM gold g
CROSS JOIN LATERAL (
SELECT top.id, row_number() OVER (ORDER BY top.distance) AS rank
FROM (SELECT c.id, c.embedding <=> g.embedding AS distance
FROM chunks c
ORDER BY c.embedding <=> g.embedding
LIMIT 10) top
) r
GROUP BY g.id
)
SELECT count(*) AS questions,
count(hit_rank) AS found_in_top_10,
round(avg(coalesce(1.0 / hit_rank, 0)), 3) AS mrr
FROM hits;found_in_top_10 کو questions پر تقسیم کرنے سے recall at 10 حاصل ہوتا ہے: یعنی کتنی بار جواب اس ونڈو کے اندر موجود تھا جو آپ نے ماڈل کو بھیجی تھی۔ MRR (mean reciprocal rank) درست chunk کی پوزیشن کے الٹ (1 تقسیم شدہ پوزیشن) کا اوسط نکالتا ہے اور ناکامی کو صفر شمار کرتا ہے، لہذا یہ جواب کو آٹھویں نمبر کے بجائے پہلے نمبر پر لانے کو زیادہ اہمیت دیتا ہے۔ جب آپ chunk کا سائز تبدیل کرتے ہیں، embedding ماڈل بدلتے ہیں یا keyword search شامل کرتے ہیں تو یہ دونوں اعداد تبدیل ہوتے ہیں، اور اب آپ دیکھ سکتے ہیں کہ وہ کس سمت میں تبدیل ہوئے۔
recall at 10 کو ہر چیز سے بڑھ کر محفوظ رکھیں، کیونکہ generator اس chunk کو استعمال نہیں کر سکتا جو اسے کبھی ملا ہی نہ ہو۔ جب recall at 10 کی ویلیو 0.9 ہو اور جوابات پھر بھی غلط ہوں، تو اس کا مطلب ہے کہ خرابی prompt یا ماڈل میں ہے، retrieval میں نہیں۔ یہ ایک تقسیم آپ کو کئی دنوں کے اندازوں سے بچا لیتی ہے۔
index کو الگ سے چیک کریں۔ Approximate search سے recall میں کمی آتی ہے، اور pgvector آپ کو دکھاتا ہے کہ کتنی کمی آئی: ایک ہی query کو exact search کے ساتھ چلائیں اور ids کا موازنہ کریں۔
BEGIN;
SET LOCAL enable_indexscan = off; -- use exact search
SELECT id FROM chunks ORDER BY embedding <=> $1 LIMIT 10;
COMMIT;دس میں سے نو ids کا مشترک ہونا اس بات کی علامت ہے کہ ef_search ٹھیک ہے۔ اگر دس میں سے صرف چار مشترک ہوں تو اس کی ویلیو بڑھا دیں۔
Reranking اور generation: جہاں API پیسے کماتا ہے
Reranker ایک مختلف قسم کا ماڈل ہے۔ یہ سوال اور ایک chunk کو ایک ساتھ پڑھتا ہے اور اس جوڑے کا اسکور نکالتا ہے، جو آزادانہ طور پر compute کیے گئے دو embeddings کا موازنہ کرنے سے بہتر ہے، اور یہ پورے corpus پر چلانے کے لیے بہت سست ہے۔ یہی وجہ ہے کہ یہ یہاں استعمال ہوتا ہے۔ یہ ان 40 امیدواروں کو دیکھتا ہے جو retrieval نے واپس کیے ہیں، نہ کہ table میں موجود 100,000 chunks کو، لہذا ایک hosted reranking API فی سوال 40 مختصر جوڑوں کا معاوضہ لیتا ہے اور مہنگے مرحلے تک پہنچنے سے پہلے بدترین false positives کو ہٹا دیتا ہے۔
Generation ایک بار بار آنے والا بل ہے، اور دو عوامل اسے متاثر کرتے ہیں۔ کم chunks بھیجیں، یہ معلوم کرنے کے لیے کہ آپ جوابات کھوئے بغیر کتنے کم chunks بھیج سکتے ہیں، recall at 10 کا استعمال کریں۔ prompt کے ابتدائی حصے کو byte-for-byte مستحکم رکھیں تاکہ provider کا prompt cache اسے hit کر سکے، اور retrieved chunks کو اس مستحکم حصے کے بعد رکھیں۔ مکمل شدہ جوابات کو بھی سوال کے لحاظ سے cache کریں، کیونکہ سب سے سستا generated token وہ ہے جو آپ نے پچھلے ہفتے generate کیا تھا۔
سرور کا سائز متعین کرنا، اور یہ کب ناکافی ہو جاتا ہے
یہاں سائزنگ کا ہر اصول وہ ہے جسے آپ پیمائش کرتے ہیں، نہ کہ اندازہ لگاتے۔
- RAM سب سے بڑی رکاوٹ ہے:
ollama psسے حاصل کردہ resident model کا سائز، جمع HNSW انڈیکس کا سائز، جمعshared_buffers، جس میں کنکشنز اور page cache کے لیے گنجائش باقی ہو۔ - Disk کے لیے
pg_total_relation_size('chunks')سے دوگنا جگہ درکار ہوتی ہے، کیونکہ انڈیکس کو دوبارہ تعمیر (rebuild) کرتے وقت دونوں کاپیاں ایک ساتھ موجود رہتی ہیں۔ - CPU ری انڈیکسنگ کے وقت کا تعین کرتا ہے، جو آپ کی پیمائش کردہ فی chunk سیکنڈز کو کل chunks کی تعداد سے ضرب دینے سے نکلتا ہے۔
- ری انڈیکسنگ آپ کی توقع سے زیادہ کثرت سے ہوتی ہے، کیونکہ embedding ماڈل کو تبدیل کرنے سے پہلے سے محفوظ شدہ ہر ویکٹر غیر مؤثر (invalidate) ہو جاتا ہے۔
یہ ڈیزائن اس مقام پر ناکافی ہو جاتا ہے جس کا اندازہ آپ پہلے سے لگا سکتے ہیں۔ جب HNSW انڈیکس اتنی RAM میں نہ سما سکے جتنی آپ خرید سکتے ہیں، تو query latency ڈسک کی تلاش (disk seeks) میں بدل جاتی ہے اور کوئی بھی سیٹنگ اسے بحال نہیں کر سکتی۔ جب ایک ٹیبل بہت سے کرایہ داروں (tenants) کو سروس فراہم کرے اور ہر سوال (query) کرایہ دار کے لحاظ سے فلٹر ہو، تو ٹیبل کو پارٹیشن کرنا ہی حل ہوتا ہے، اور یہ ایک سنجیدہ کام ہے۔ جب انڈیکسنگ کی تحریریں (writes) اور صارف کی استفسارات ایک ہی باکس پر آپس میں ٹکرائیں، تو ڈیٹا بیس کو منتقل کرنے سے پہلے embedding ورکر کو دوسرے سرور پر منتقل کر دیں۔ جب تک ان میں سے کوئی صورتحال پیش نہ آئے، Postgres کے ساتھ pgvector اس VPS پر جو آپ پہلے سے کرائے پر لے چکے ہیں، ایک پروڈکشن حل ہے، اور اوپر دیے گئے اعداد و شمار آپ کو بتاتے ہیں کہ حد کتنی دور ہے۔
FAQ
کیا میں ایک VPS پر RAG پائپ لائن چلا سکتا ہوں، یا مجھے ویکٹر ڈیٹا بیس کی ضرورت ہے؟
ایک VPS لاکھوں ٹکڑوں (chunks) پر مشتمل کارپس کے لیے کافی ہے۔ 768 ڈائمینشنز پر، 100,000 ٹکڑے 294 MiB ویکٹر ڈیٹا، نیز ٹیکسٹ اور HNSW انڈیکس پر مشتمل ہوتے ہیں، جو ایک عام پلان کی RAM میں سما جاتے ہیں۔ حد قطاروں کی تعداد کے بجائے میموری ہے، کیونکہ HNSW سرچ انڈیکس میں ادھر ادھر جمپ کرتی ہے، لہذا جب انڈیکس RAM میں سما نہ سکے تو لیٹنسی (latency) بڑھ جاتی ہے۔ انڈیکس پر pg_relation_size کا موازنہ free -m سے کریں تو آپ کو اپنی صورتحال کا اندازہ ہو جائے گا۔
کیا مجھے اپنے دستاویزات کو ایمبیڈ کرنے کے لیے GPU کی ضرورت ہے؟
اگر آپ ایک بار ایمبیڈ کریں اور بعد میں استفسار (query) کریں تو نہیں۔ 137 ملین پیرامیٹرز والا ماڈل جیسے کہ nomic-embed-text CPU پر چلتا ہے، اور ایک بڑے کارپس پر مکمل پاس میں چند گھنٹے لگتے ہیں جو آپ رات بھر میں مکمل کر سکتے ہیں۔ GPU تب اہم ہوتا ہے جب دستاویزات مسلسل آ رہی ہوں، یا جب آپ اسی باکس پر جنریشن چلانا چاہتے ہوں۔ اپنے سرور پر /api/embed کے خلاف ایک بیچ کا وقت نوٹ کریں اور اسے اپنے ٹکڑوں کی تعداد سے ضرب دیں، کیونکہ vCPU کی تعداد اتنی مختلف ہوتی ہے کہ کوئی بھی شائع شدہ اعداد و شمار آپ کی مدد نہیں کر سکتے۔
میری ویکٹر کوئری HNSW انڈیکس کے بجائے سیکوینشل اسکین کیوں استعمال کرتی ہے؟
EXPLAIN (ANALYZE, BUFFERS) کے ساتھ پلان پڑھیں۔ اس کی عام وجہ اسٹوریج ہے: pgvector نوٹ کرتا ہے کہ پلانر اپنی لاگت کے تخمینوں میں آؤٹ آف لائن اسٹوریج کو شمار نہیں کرتا، جس سے سیریل اسکین اپنی اصل قیمت سے سستا معلوم ہوتا ہے، اور 768 ڈائمینشن والا ویکٹر 3,080 بائٹس کا ہوتا ہے اس لیے یہ بائی ڈیفالٹ TOAST ٹیبل میں رہتا ہے۔ ALTER TABLE chunks ALTER COLUMN embedding SET STORAGE PLAIN; نئی قطاروں کو ان لائن رکھتا ہے۔ دیگر دو وجوہات میں ایک ایسا آپریٹر ہے جو انڈیکس سے میل نہیں کھاتا، کیونکہ vector_cosine_ops کے ساتھ بنایا گیا انڈیکس صرف <=> کے ذریعے استعمال ہوتا ہے، اور ایسی کوئری جس میں کوئی ORDER BY ... LIMIT نہ ہو، کیونکہ ایک تخمینی انڈیکس صرف آرڈرڈ نیئرسٹ نیبر (nearest neighbour) کوئریز کو پورا کرتا ہے۔
مجھے کیسے معلوم ہوگا کہ میری ریٹریول (retrieval) اچھی ہے؟
30 سوالات کا ایک گولڈ سیٹ بنائیں، ہر ایک کو اس ٹکڑے کی ID کے ساتھ جوڑیں جو اس کا جواب دیتا ہے، اور سوال کے ایمبیڈنگز کو ان کے ساتھ اسٹور کریں۔ پھر 10 پر ریکال (recall) کی پیمائش کریں، جو یہ بتاتا ہے کہ درست ٹکڑا ٹاپ 10 میں کتنی بار ظاہر ہوتا ہے، اور MRR، جو اسے پہلے نمبر پر رکھنے کا صلہ دیتا ہے۔ یہ دو اعداد و شمار ہی آپ کو بتاتے ہیں کہ ٹکڑے کے سائز، ایمبیڈنگ ماڈل یا رینک فیوژن میں تبدیلی نے مدد کی ہے یا نہیں۔ ان کے بغیر آپ صرف سیٹنگز تبدیل کر رہے ہیں اور چند جوابات کے بارے میں اپنے تاثر پر بھروسہ کر رہے ہیں۔