சொந்தமாக VPS-ல் RAG pipeline உருவாக்குவது எப்படி?
உங்கள் VPS-ல் pgvector மற்றும் Ollama பயன்படுத்தி RAG pipeline அமைக்கும் முறை. Chunking, embedding, HNSW indexing மற்றும் தரவுகளை மீட்டெடுக்கும் SQL குறியீடுகளை இதில் காணலாம்.
Self-hosted RAG pipeline-ன் கட்டமைப்பு
RAG (retrieval augmented generation) pipeline ஐந்து நிலைகளைக் கொண்டது: ஆவணங்களைச் சிறு பகுதிகளாகப் பிரித்தல் (chunking), அந்தப் பகுதிகளுக்கு embedding உருவாக்குதல், vector-களைச் சேமித்தல், கேட்கப்படும் கேள்விக்குத் தொடர்புடைய பகுதிகளை மீட்டெடுத்தல், மற்றும் அந்தப் பகுதிகளை ஒரு language model-க்கு அனுப்பி பதிலை உருவாக்குதல். நீங்கள் ஏற்கனவே வாடகைக்கு எடுத்துள்ள VPS-ல், முதல் நான்கு நிலைகளும் அந்த server-லேயே இயங்கும். PostgreSQL-ன் pgvector extension vector-களைச் சேமிக்கும், Ollama மூலம் இயங்கும் சிறிய embedding model உரையை vector-களாக மாற்றும். கடைசி நிலை மட்டுமே server-க்கு வெளியே செல்லும்.
இந்தக் கட்டமைப்பே இந்த வழிகாட்டியின் நோக்கம். Chunking என்பது சாதாரண CPU வேலை. Embedding என்பது 137 மில்லியன் parameters கொண்ட ஒரு model; இது சில நூறு megabytes RAM-ஐ மட்டுமே பயன்படுத்தும். சேமிப்பகம் என்பது ஒரு Postgres table; இதில் ஒரு row-ஐ எழுதுவதற்கு முன்பே, அதன் அளவை நீங்கள் கணக்கிட்டுவிடலாம். லட்சக்கணக்கான chunks கொண்ட தரவுகளுக்கு, இவை அனைத்தும் ஒரு சாதாரண VPS-லேயே இயங்கும். Generation நிலை இதற்கு மாறுபட்டது, ஏனெனில் ஒவ்வொரு கேள்விக்கும் இது ஒரு குறிப்பிட்ட செலவை ஏற்படுத்தும்.
RAG pipeline-ல் எந்தெந்த பகுதிகள் செலவை உருவாக்குகின்றன
DigitalOcean-ன் end to end RAG tutorial ஒரு managed vector database மற்றும் hosted embedding model-ஐப் பயன்படுத்துகிறது. அதன் செலவு குறித்த பகுதி மேலோட்டமானது: மீண்டும் மீண்டும் கேட்கப்படும் கேள்விகளை cache செய்தல், பெறப்படும் chunks-ன் எண்ணிக்கையைக் குறைத்தல், generation-க்கு முன் rerank செய்தல் போன்றவை அதில் கூறப்பட்டுள்ளன. இந்த அறிவு சரியானதுதான். ஆனால், கணக்கீட்டையே மாற்றக்கூடிய ஒரு வழியை அது தவிர்க்கிறது; அது, நீங்கள் ஏற்கனவே பணம் செலுத்திப் பயன்படுத்தும் server-லேயே embedding model-ஐ இயக்குவது.
விலையை விட tokens எண்ணிக்கையைக் கணக்கிடுங்கள், ஏனெனில் விலைப்பட்டியல் மாறினாலும் token எண்ணிக்கை மாறாது. 400 tokens கொண்ட 100,000 chunks கொண்ட ஒரு corpus-ஐ எடுத்துக்கொள்வோம். அதில் 10,000 கேள்விகள் கேட்கப்படுகின்றன, ஒவ்வொரு பதிலுக்கும் 8 chunks model-க்கு அனுப்பப்படுகின்றன, 100 tokens கொண்ட கேள்வி மற்றும் instruction block உள்ளது, மேலும் 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-ஐயும் embed செய்வதற்கு 40 மில்லியன் tokens தேவைப்படும், இது ஒருமுறை மட்டுமே நடக்கும். அந்த 10,000 கேள்விகளுக்கு இதைப் பகிர்ந்தால், ஒரு கேள்விக்கு 4,000 tokens ஆகும். ஒரு லட்சம் கேள்விகளைக் கேட்டால், இது 400-ஆகக் குறையும். ஆனால் generation செலவு குறையாது. நீங்கள் பதிலளிக்கும் ஒவ்வொரு கேள்விக்கும் இது 3,300 tokens உள்ளீடாகவும், 400 tokens வெளியீடாகவும் செலவாகும்.
எனவே, மீண்டும் மீண்டும் நடக்கும் செயல்பாடுகளுக்கே அதிக செலவாகும். Embedding படிநிலையை நீங்களே கையாளுங்கள், ஏனெனில் அதற்கு ஒருமுறை மட்டுமே செலவு, மேலும் உங்கள் VPS ஏற்கனவே இயங்கிக்கொண்டிருக்கிறது. Generation படிநிலையை விலைக்கு வாங்குங்கள், ஏனெனில் அங்குதான் சிறந்த model-க்கு நீங்கள் செலுத்தும் பணம் மதிப்புமிக்கதாக இருக்கும். Caching-ம் இதே காரணத்திற்காக முக்கியமானது: ஒரு cache hit, செலவு குறையாத ஒரே படிநிலையைத் தவிர்க்க உதவுகிறது. KV cache மற்றும் prompt cache-க்கு இடையிலான வேறுபாடு இவற்றில் எதை நீங்கள் மீண்டும் பயன்படுத்தலாம் என்பதைத் தீர்மானிக்கிறது. ஒரு RAG prompt-ல் நிலையான instruction block-ஐத் தொடர்ந்து மாறும் chunk block இருக்கும்; இந்த அமைப்பே அதிகப் பலனைத் தரும்.
Chunking: நிலையான அளவு மற்றும் overlap ஏன் சரியான இயல்புநிலை (default)
Chunk என்பது நீங்கள் மீட்டெடுக்கும் (retrieve) அலகு, எனவே அதன் அளவே அதற்குப் பிந்தைய அனைத்து செயல்பாடுகளையும் தீர்மானிக்கிறது. ஒரு chunk-ன் embedding ஒரு குறிப்பிட்ட விஷயத்தைப் பற்றி மட்டுமே இருக்க வேண்டும்; ஏனெனில் embedding என்பது ஒரு வெளியில் (space) உள்ள ஒற்றைப் புள்ளி. நான்கு தலைப்புகளை உள்ளடக்கிய ஒரு chunk, அந்த நான்கு தலைப்புகளுக்கும் இடையில் அமைந்து, எதனுடனும் சரியாகப் பொருந்தாமல் போய்விடும். அதேசமயம், ஒரு chunk தானாகவே ஒரு கேள்விக்கு பதிலளிக்கும் அளவுக்குப் பெரியதாகவும் இருக்க வேண்டும்; ஏனெனில் language model அந்த chunk-ஐ மட்டுமே பார்க்கிறது, அதைச் சுற்றியுள்ள முழு ஆவணத்தையும் பார்ப்பதில்லை.
300 வார்த்தைகள் மற்றும் 50 வார்த்தைகள் overlap என்ற அளவில் தொடங்கவும். ஆங்கிலத்தில் ஒரு வார்த்தைக்கு தோராயமாக 1.3 tokens இருக்கும், எனவே 300 வார்த்தைகள் என்பது சுமார் 400 tokens ஆகும். ஒரு வாக்கியம் எல்லையில் (boundary) பாதியாகப் பிரிவதைத் தவிர்க்கவே இந்த overlap பயன்படுத்தப்படுகிறது; இல்லையெனில், அந்தப் பாதியால் எந்தக் கேள்விக்கும் பதில் அளிக்க முடியாது.
ஆவணங்களில் கட்டமைப்பு (structure) இருந்தால், முதலில் அதன் அடிப்படையில் பிரிக்கவும். தலைப்புகள் (headings) மற்றும் பத்திகளின் (paragraphs) அடிப்படையில் பிரிக்கவும். ஒரு பகுதி (section) மிக நீளமாக இருந்தால் மட்டுமே, அதற்குள் நிலையான அளவு (fixed size) விதியைப் பயன்படுத்தவும். ஒரு வாக்கியத்தின் நடுவில் தொடங்கும் chunk, இறுதிப் பதிலில் வாசிப்பதற்குச் சிரமமாக இருக்கும்; ஏனெனில் நீங்கள் கொடுத்ததை அப்படியே model மேற்கோள் காட்டும்.
அளவீடு செய்யத் தொடங்கும் முன் chunking-ஐ மாற்ற வேண்டாம். நிலையான அளவு மற்றும் overlap முறை தீர்மானிக்கக்கூடியது (deterministic) மற்றும் மீண்டும் இயக்குவதற்கு எளிதானது; இதுவே நீங்கள் மேம்படுத்த வேண்டிய அடிப்படை (baseline). முதலில் scoring query-ஐ உருவாக்கிவிட்டு, அதன் பிறகு ஒவ்வொன்றாக மாற்றங்களைச் செய்யவும்.
ஒரே server-ல் embedding செய்தல் மற்றும் அதன் RAM மற்றும் latency செலவுகள்
curl -fsSL https://ollama.com/install.sh | sh
ollama pull nomic-embed-textnomic-embed-text என்பது 137 மில்லியன் parameters கொண்டது மற்றும் ஆகஸ்ட் 2026 நிலவரப்படி 274 MB அளவுள்ள download ஆகும். இதை வைத்து ஒரு table-ஐ வடிவமைக்கும் முன், அது என்ன output தருகிறது என்பதைச் சரிபார்க்கவும்.
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-ஐ print செய்யும். உங்கள் column type அந்த எண்ணுடன் சரியாகப் பொருந்த வேண்டும்.
இந்த model-ல் உள்ள இரண்டு அமைப்புகள் பயனர்களைக் குழப்பமடையச் செய்யும்.
Task prefix கட்டாயமானது. Nomic-ன் model card-ல் உள்ளபடி, உள்ளீடு "கட்டாயமாக task instruction prefix-ஐக் கொண்டிருக்க வேண்டும்". ஆவணங்கள் (documents) search_document: முன்னொட்டுடனும், கேள்விகள் (questions) search_query: முன்னொட்டுடனும் embedding செய்யப்பட வேண்டும். இவற்றைத் தவிர்த்தால் எந்த error-ம் வராது: உங்களுக்கு vectors கிடைக்கும், ஆனால் retrieval தரம் குறைந்துவிடும், இதற்கான காரணத்தை எந்த log-லும் பார்க்க முடியாது.
நீளமான உள்ளீடுகள் தானாகவே வெட்டப்படும். /api/embed endpoint ஒரு truncate field-ஐ எடுத்துக்கொள்கிறது, இதன் default மதிப்பு true ஆகும். Ollama தொகுத்துள்ள இந்த model 2K context-ஐக் குறிப்பிடுகிறது. அதைவிட நீளமான chunk அந்த எல்லையில் வெட்டப்பட்டு embedding செய்யப்படும், எனவே அதன் இறுதிப் பகுதி தேடலுக்குக் கிடைக்காது. நீங்கள் testing செய்யும்போது "truncate": false-ஐ அனுப்பவும், அப்போதுதான் அளவு அதிகமான chunk-கள் தோல்வியடையும், தவறான முடிவுகளைத் தராது.
கோரிக்கைகளை (requests) batch செய்யவும், model-ஐ resident-ஆக வைத்திருக்கவும்.
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 chunk-களை ஒரே கோரிக்கையில் அனுப்புவது சிறந்தது. ஏனெனில் HTTP round trip மற்றும் model lookup ஒருமுறை மட்டுமே நடக்கும். keep_alive என்பது ஒரு கோரிக்கைக்குப் பிறகு model எவ்வளவு நேரம் memory-ல் இருக்க வேண்டும் என்பதைக் கட்டுப்படுத்துகிறது, இதன் default மதிப்பு 5 நிமிடங்கள். அது காலாவதியானால், அடுத்த கோரிக்கையின்போது மீண்டும் load time செலவாகும்.
உங்கள் server-ல் முக்கியமான இரண்டு எண்களை நீங்களே அளவிடவும். அவை உங்கள் 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 என்பது loaded model-ன் resident அளவைக் காட்டும். keep_alive அதை வைத்திருக்கும் வரை அந்த RAM பயன்பாட்டில் இருக்கும். time output-ஐ batch size-ஆல் வகுத்தால், ஒரு chunk-க்கான வினாடிகள் கிடைக்கும். அதை மொத்த chunk எண்ணிக்கையால் பெருக்கினால், indexing-க்கான மொத்த நேரம் கிடைக்கும். CPU மட்டுமே உள்ள திட்டத்தில், 100,000 chunk-களைக் கொண்ட corpus-ஐ முடிக்க மணிநேரங்கள் ஆகலாம். இது ஒருமுறை மட்டுமே நடக்கும் என்பதால், nice -n 19 மூலம் இரவு நேரத்தில் இதை இயக்கலாம். மணிநேரங்கள் எடுப்பது உங்களுக்குப் பிரச்சினையென்றால், GPU-வை வாடகைக்கு எடுப்பது லாபகரமானதா என்பதை API tokens-உடன் ஒப்பிட்டு கணக்கிட வேண்டும்.
அந்த server-ல் ஏற்கனவே ஒரு chat model இயங்கிக்கொண்டிருந்தால், embedding model இரண்டாவது resident model-ஆகச் செயல்படும், இதனால் RAM பயன்பாடு கூடும். Ollama-வை VPS-ல் இயக்குவது generation பக்கத்தின் அளவீடுகளை விளக்குகிறது, மேலும் ஒரே நேரத்தில் பல பயனர்கள் பயன்படுத்தும்போது self-hosted model எப்படிச் செயல்படும் என்பது பல பயனர்கள் ஒரே நேரத்தில் கேட்கும்போது என்ன நடக்கும் என்பதை விளக்குகிறது. Embedding model சிறியது என்பதால், அது மற்றவற்றுடன் இணைந்து இயங்க முடியும்.
Indexing script, ஆரம்பம் முதல் முடிவு வரை
Ubuntu 24.04-ல், virtual environment-க்கு வெளியே ஒரு சாதாரண pip install கட்டளை error: externally-managed-environment பிழையுடன் நிற்கும். ஏனெனில் system 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) அணுகி, document id மற்றும் அதன் உரையை (text) வழங்கும். மற்ற அனைத்தும் pipeline-ன் பகுதியாகும்.
சேமிப்பகம்: pgvector schema மற்றும் அதன் அளவு
Ubuntu 24.04-ல் postgresql-16-pgvector பதிப்பு 0.6.0-ஆக வருகிறது, இது halfvec வகையை விட பழையது. தற்போதைய build-க்கு PostgreSQL திட்டத்தின் சொந்த repository-ஐப் பயன்படுத்தவும்.
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-pgvectorpackage பெயரில் உள்ள எண் உங்கள் server-ன் major பதிப்புடன் ஒத்துப்போக வேண்டும். பிறகு role, database மற்றும் extension-ஐ உருவாக்கவும்.
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), model-ன் output-உடன் ஒத்துப்போக வேண்டும். 1024 dimension vector-ஐ அந்த column-ல் insert செய்ய முயன்றால், Postgres அதை expected 768 dimensions, not 1024 பிழையுடன் நிராகரிக்கும்; இது இந்த முழு pipeline-லும் மிகத் தெளிவான பிழைச் செய்தியாகும். உருவாக்கப்பட்ட fts column-ஐப் பராமரிக்க எந்தச் செலவும் இல்லை, இது பிற்காலத்தில் keyword search செய்ய உதவும்.
சேமிப்பகம் என்பது கணித அடிப்படையிலானது. pgvector ஆவணங்களின்படி ஒரு vector என்பது 4 * dimensions + 8 bytes மற்றும் ஒரு halfvec என்பது 2 * dimensions + 8 ஆகும். கீழே உள்ள dimension எண்ணிக்கைகள் ஒவ்வொரு model-ன் வெளியிடப்பட்ட output அளவு ஆகும்.
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 dimensions-ல் ஒவ்வொரு vector-ம் 3,080 bytes ஆகும், எனவே 100,000 chunks 294 MiB vector தரவைச் சேமிக்கும். 1536 dimension கொண்ட hosted model மூலம் உருவாக்கப்பட்ட அதே corpus-க்கு 587 MiB தேவைப்படும், அதன் index-ம் அதற்கேற்ப வளரும். Half precision இரண்டையும் பாதியாகக் குறைக்கும்: halfvec(768) அந்த corpus-ஐ 147 MiB-ல் சேமிக்கும். இது recall-ஐப் பாதிக்குமா என்பது கீழே உள்ள scoring query மூலம் ஒரே ஓட்டத்தில் தெரிந்துவிடும்.
இந்த அளவீடுகள் vector column-க்கு மட்டுமே பொருந்தும். Text, row overhead மற்றும் indexes ஆகியவை கூடுதலாகச் சேரும், எனவே உண்மையான table அளவைச் சரிபார்க்கவும்.
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 மற்றும் user accounts வசதியுடன் கூடிய அதே extension வேண்டுமென்றால், self-hosted Supabase stack என்பது pgvector ஏற்கனவே இயக்கப்பட்ட Postgres ஆகும், மேலும் இந்த வழிகாட்டியில் உள்ள ஒவ்வொரு query-யும் அதில் எந்த மாற்றமும் இன்றி வேலை செய்யும்.
Indexing: கவனத்தில் கொள்ள வேண்டிய HNSW அமைப்புகள்
சில ஆயிரம் வரிசைகளுக்குக் குறைவாக இருக்கும்போது, index-ஐத் தவிர்க்கவும். Exact search ஒவ்வொரு வரிசையையும் வாசிக்கும்; அந்த அளவில் அது போதுமான வேகத்தில் இருக்கும், மேலும் அதன் recall துல்லியமாக இருக்கும். Sequential scan போதுமான வேகத்தில் இல்லை என்று உணரும்போது index-ஐச் சேர்க்கவும். இதில் உள்ள வர்த்தகத்தைப் புரிந்துகொள்ளுங்கள்: ஒரு approximate index தோராயமான அண்டை வரிசைகளையே (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-ன் இயல்புநிலை அமைப்புகள் (defaults). இவற்றை அதிகரிப்பது recall-ஐ மேம்படுத்தும், ஆனால் build நேரத்தையும் index அளவையும் அதிகரிக்கும். உங்கள் model unit length vectors-ஐ வெளியிடுகிறது என்று உறுதியாகத் தெரியவில்லை என்றால், <=> operator-உடன் vector_cosine_ops-ஐப் பயன்படுத்தவும். ஏனெனில், cosine distance vector-ன் நீளத்தைப் புறக்கணிக்கும், ஆனால் inner product அவ்வாறு செய்யாது.
Build செயல்முறையைக் கவனிக்கவும். graph maintenance_work_mem-ஐ விடப் பெரியதாகும்போது, pgvector அதைத் தெரிவிக்கும்:
NOTICE: hnsw graph no longer fits into maintenance_work_mem after 100000 tuples
DETAIL: Building will take significantly more time.இது பிழை அல்ல, build முழுமையடையும். ஆனால், அது மிக மெதுவான பாதையில் செல்லும். Index-ஐ உருவாக்கும் session-ல் மட்டும் maintenance_work_mem-ஐ அதிகரிக்கவும், server-ன் இயல்புநிலை அமைப்பை மாற்ற வேண்டாம். ஏனெனில், இந்த அமைப்பு ஒவ்வொரு maintenance செயல்பாட்டிற்கும் பொருந்தும், மேலும் உலகளாவிய அளவில் (global) அதிக மதிப்பை வைப்பது server-ன் நினைவகத்தை (memory) காலி செய்துவிடும். இரண்டாவது session-லிருந்து நீண்ட build செயல்முறையைப் பின்தொடரவும்.
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%"
FROM pg_stat_progress_create_index;பிறகு, முடிக்கப்பட்ட index-ஐ server-ல் உள்ள நினைவகத்துடன் ஒப்பிடவும்.
SELECT pg_size_pretty(pg_relation_size('chunks_embedding'));
SHOW shared_buffers;HNSW search ஒரு graph-ஐ ஊடுருவுகிறது. எனவே, இது ஒரு குறிப்பிட்ட வரம்பை வாசிப்பதற்குப் பதிலாக, index முழுவதும் சிதறிக்கிடக்கும் பக்கங்களைத் தொடுகிறது. நினைவகத்தில் பொருந்தாத ஒரு index, ஒவ்வொரு query-யையும் disk வாசிப்பாக மாற்றும். இந்த மெதுவான செயல்பாட்டையே பயனர்கள் கவனிப்பார்கள். server-க்கான ஒரே அளவு விதி இதுதான்: index மற்றும் நீங்கள் வழங்கும் வரிசைகள் அனைத்தும் 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;அதிக மதிப்பு graph-ன் அதிகப் பகுதிகளைத் தேடும், சிறந்த அண்டை வரிசைகளைக் கண்டறியும், ஆனால் latency-ஐ அதிகரிக்கும். இது ஒரு session அமைப்பு, எனவே index-ஐப் பாதிக்காமல் ஒரு குறிப்பிட்ட query-க்கு மட்டும் இதை அதிகரிக்கலாம்.
ஒரு query index-ஐப் பயன்படுத்தவில்லை என்றால், அதன் plan அதைத் தெளிவாகக் காட்டும்.
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM chunks ORDER BY embedding <=> $1 LIMIT 8;இங்கு sequential scan நடப்பதற்கு பெரும்பாலும் storage-தான் காரணம். 768 பரிமாணங்கள் கொண்ட vector 3,080 bytes அளவுடையது. இது Postgres-ன் inline சேமிப்புத் திறனை விட அதிகம் என்பதால், அந்த மதிப்பு TOAST table-க்கு (பெரிய மதிப்புகளுக்கான out-of-line store) நகர்த்தப்படுகிறது. pgvector-ன் குறிப்பின்படி, planner தனது cost மதிப்பீடுகளில் out-of-line சேமிப்பைக் கணக்கிடுவதில்லை. இதனால் serial scan மலிவானதாகத் தோன்றலாம். ALTER TABLE chunks ALTER COLUMN embedding SET STORAGE PLAIN; vector-களை inline-ஆக வைத்திருக்கும். இது மாற்றத்திற்குப் பிறகு எழுதப்படும் வரிசைகளுக்கு மட்டுமே பொருந்தும், எனவே ஏற்கனவே உள்ள வரிசைகளைத் திருத்த (table rewrite) வேண்டும்.
Retrieval: ஒரு வினவல், இரண்டு சிக்னல்கள்
Vector search என்பது கேள்வியின் பொருளை ஒத்த உரையைத் தேடுகிறது. ஆனால், இது துல்லியமான சரங்களைக் கண்டறிவதில் பலவீனமானது: ஒரு பகுதி எண் (part number), பிழைக் குறியீடு (error code), அல்லது குடும்பப் பெயர் (surname) ஆகியவற்றில் இது சரியாகச் செயல்படாது. Keyword search இதற்கு நேர்மாறானது, இதை Postgres ஏற்கனவே செய்கிறது. இரண்டாவது அமைப்பை இயக்குவதற்குப் பதிலாக, இரண்டையும் ஒரே வினவலில் இணைக்கவும்.
Reciprocal rank fusion என்பது செயல்படும் மிக எளிமையான ஒருங்கிணைப்பு முறையாகும். ஒவ்வொரு முடிவும் அது இடம்பெறும் ஒவ்வொரு பட்டியலிலிருந்தும் 1 / (60 + rank)-ஐப் பெறுகிறது, மேலும் அந்த இரண்டு மதிப்பெண்களும் கூட்டப்படுகின்றன. இதற்கு மதிப்பெண் இயல்பாக்கம் (score normalisation) தேவையில்லை, ஏனெனில் இது தூரங்களை விட நிலைகளையே (positions) கணக்கில் கொள்கிறது.
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 என்பது உரையாக உள்ள கேள்வி. இவை இரண்டும் உங்கள் application-லிருந்து பிணைக்கப்படுகின்றன (bound). websearch_to_tsquery நிறுத்தற்குறிகளால் (punctuation) பாதிக்கப்படாமல் உண்மையான பயனர் கேள்வியை ஏற்றுக்கொள்கிறது, ஆனால் to_tsquery அவ்வாறு செய்வதில்லை. மேலும் ஒரு விஷயம்: HNSW scan-க்கு மேல் ஒரு WHERE வடிகட்டியைச் சேர்ப்பது, நீங்கள் கேட்டதை விடக் குறைவான வரிசைகளைத் தரக்கூடும், ஏனெனில் குறியீட்டு முறை (index) முதலில் தேடப்பட்டு, வடிகட்டி அதன் பிறகுதான் இயங்குகிறது. SET hnsw.iterative_scan = relaxed_order;, போதுமான வரிசைகள் கிடைக்கும் வரை pgvector-ஐத் தொடர்ந்து ஸ்கேன் செய்ய வைக்கிறது.
Retrieval தரம் சிறப்பாக உள்ளதா என்பதை எவ்வாறு கண்டறிவது?
இது பெரும்பாலான RAG வழிகாட்டிகளில் தவிர்க்கப்படும் ஒரு படியாகும். மற்ற தேர்வுகள் பயனுள்ளதாக இருந்ததா என்பதை உறுதிப்படுத்தும் ஒரே வழி இதுதான். இதற்கு ஒரு சிக்கலான evaluation framework தேவையில்லை. உங்களுக்குத் தேவையானது 30 கேள்விகள் மற்றும் ஒவ்வொரு கேள்விக்கும் பதிலளிக்கும் chunk-ன் id மட்டுமே.
இவற்றை நீங்களே கைமுறையாக எழுதுங்கள். உங்கள் corpus குறித்து பயனர்கள் கேட்கும் உண்மையான கேள்விகளை எடுத்துக்கொள்ளுங்கள். ஒவ்வொன்றையும் இயக்கி, வரும் முடிவுகளைப் படித்து, சரியான 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 செய்து சேமிக்கவும். பின்னர், முழு தொகுப்பையும் ஒரே query-ஆக மதிப்பிடவும்.
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 ஆகும். அதாவது, நீங்கள் model-க்கு அனுப்பும் window-க்குள் எத்தனை முறை சரியான பதில் இருந்தது என்பதைக் குறிக்கும். MRR (mean reciprocal rank) என்பது சரியான chunk-ன் இடத்தின் தலைகீழ் மதிப்பின் சராசரி ஆகும். இதில் விடுபட்ட பதில்களுக்கு பூஜ்ஜியம் வழங்கப்படுகிறது. எனவே, எட்டாவது இடத்தில் இருப்பதை விட, முதல் இடத்தில் இருக்கும் பதிலுக்கு இது அதிக முக்கியத்துவம் அளிக்கிறது. நீங்கள் chunk size-ஐ மாற்றும்போதோ, embedding model-ஐ மாற்றும்போதோ அல்லது keyword search-ஐச் சேர்க்கும்போதோ இந்த இரண்டு எண்களும் மாறும். இதன் மூலம் மாற்றங்கள் எந்த திசையில் உள்ளன என்பதை நீங்கள் அறியலாம்.
Recall at 10-க்கு எல்லாவற்றையும் விட அதிக முக்கியத்துவம் கொடுங்கள். ஏனெனில், model-க்கு கிடைக்காத ஒரு தகவலை வைத்து அதனால் பதிலளிக்க முடியாது. Recall at 10 மதிப்பு 0.9 ஆக இருந்தும் பதில்கள் தவறாக இருந்தால், தவறு prompt-ல் அல்லது model-ல் உள்ளது, retrieval-ல் இல்லை. இந்த ஒரு தெளிவு, பல நாட்களாக நீங்கள் செய்யும் ஊகங்களைத் தவிர்க்க உதவும்.
Index-ஐத் தனித்தனியாகச் சரிபார்க்கவும். Approximate search-ஆல் recall குறைய வாய்ப்புள்ளது. pgvector மூலம் இதை நீங்கள் அறியலாம்: அதே query-ஐ exact search மூலம் இயக்கி, id-களை ஒப்பிட்டுப் பாருங்கள்.
BEGIN;
SET LOCAL enable_indexscan = off; -- use exact search
SELECT id FROM chunks ORDER BY embedding <=> $1 LIMIT 10;
COMMIT;பத்தில் ஒன்பது id-கள் பொதுவாக இருந்தால் ef_search சரியாக உள்ளது என்று அர்த்தம். பத்தில் நான்கு மட்டுமே பொதுவாக இருந்தால், அதன் மதிப்பை உயர்த்த வேண்டும்.
Reranking மற்றும் generation: API-ன் வருவாய் ஈட்டும் இடம்
Reranker என்பது ஒரு மாறுபட்ட வகை model ஆகும். இது கேள்வியையும் ஒரு chunk-ஐயும் ஒன்றாகப் படித்து, அந்த இணைப்பிற்கு மதிப்பெண் (score) வழங்குகிறது. இது தனித்தனியாகக் கணக்கிடப்பட்ட இரண்டு embeddings-ஐ ஒப்பிடுவதை விடச் சிறந்தது, ஆனால் முழு corpus-க்கும் இதைப் பயன்படுத்துவது மிக மெதுவானது. இதனால்தான் இது இந்த இடத்தில் பயன்படுத்தப்படுகிறது. இது retrieval மூலம் பெறப்பட்ட 40 candidates-ஐ மட்டுமே பார்க்கிறது, table-ல் உள்ள 100,000 chunks-ஐ அல்ல. எனவே, ஒரு hosted reranking API ஒவ்வொரு கேள்விக்கும் 40 சிறிய இணைகளுக்குக் கட்டணம் வசூலிக்கிறது மற்றும் மிகவும் தவறான முடிவுகளை (false positives) விலையுயர்ந்த கட்டத்திற்குச் செல்லும் முன்பே நீக்கிவிடுகிறது.
Generation என்பது தொடர்ச்சியான செலவாகும், இதை இரண்டு வழிகளில் குறைக்கலாம். குறைவான chunks-ஐ அனுப்பவும்; பதில்களை இழக்காமல் எவ்வளவு குறைவாக அனுப்ப முடியும் என்பதை அறிய recall at 10-ஐப் பயன்படுத்தவும். Prompt-ன் முன்பகுதியை byte-க்கு-byte மாறாமல் வைத்திருக்கவும், அப்போதுதான் provider-ன் prompt cache சரியாகச் செயல்படும்; retrieved chunks-ஐ அந்த நிலையான பகுதிக்குப் பின் வைக்கவும். கேள்விகளின் அடிப்படையில் ஏற்கனவே உருவாக்கப்பட்ட பதில்களை cache செய்யவும், ஏனெனில் கடந்த வாரம் நீங்கள் உருவாக்கிய token-தான் மிகக் குறைந்த செலவில் கிடைக்கும் token ஆகும்.
சர்வரின் அளவை நிர்ணயித்தல் மற்றும் அதன் திறன் எப்போது முடிவுக்கு வருகிறது
இங்கே கொடுக்கப்பட்டுள்ள ஒவ்வொரு அளவு நிர்ணய விதியும், நீங்கள் மதிப்பீடு செய்வதற்குப் பதிலாக அளவிட வேண்டிய ஒன்றாகும்.
- RAM என்பது மிக முக்கியமான கட்டுப்பாடு:
ollama ps-லிருந்து பெறப்படும் resident model அளவு, அதனுடன் HNSW index அளவு, மற்றும்shared_buffersஆகியவற்றின் கூட்டுத்தொகை, அத்துடன் connections மற்றும் page cache-க்குத் தேவையான கூடுதல் இடவசதி (headroom) இருக்க வேண்டும். - Disk-க்கு
pg_total_relation_size('chunks')-ன் இரு மடங்கு அளவு தேவைப்படுகிறது, ஏனெனில் ஒரு index-ஐ மீண்டும் உருவாக்கும்போது (rebuilding) இரண்டு பிரதிகளும் ஒரே நேரத்தில் சேமிக்கப்படும். - CPU, reindex செய்ய எடுக்கும் நேரத்தைத் தீர்மானிக்கிறது; நீங்கள் அளந்த ஒரு chunk-க்கான வினாடிகளை, மொத்த chunk எண்ணிக்கையால் பெருக்கினால் இந்த நேரம் கிடைக்கும்.
- Embedding model-ஐ மாற்றும்போது ஏற்கனவே சேமிக்கப்பட்ட அனைத்து vector-களும் செல்லாததாகிவிடும் என்பதால், நீங்கள் எதிர்பார்ப்பதை விட அடிக்கடி reindexing செய்ய வேண்டியிருக்கும்.
இந்த வடிவமைப்பு எப்போது அதன் எல்லையை அடையும் என்பதை முன்கூட்டியே அறிய முடியும். HNSW index-ஐ நீங்கள் வாங்கக்கூடிய RAM-க்குள் அடக்க முடியாதபோது, query latency என்பது disk seek-ஆக மாறிவிடும்; எந்தவொரு setting-உம் இதைச் சரிசெய்ய முடியாது. ஒரு table பல வாடிக்கையாளர்களுக்கு (tenants) சேவை அளிக்கும்போது, ஒவ்வொரு query-யும் tenant அடிப்படையில் filter செய்யப்பட்டால், அந்த table-ஐ partitioning செய்வதுதான் தீர்வாகும், இது ஒரு கடினமான பணி. Indexing எழுதும் பணிகளும் (writes) பயனர் query-களும் ஒரே server-ல் மோதிக்கொண்டால், database-ஐ மாற்றுவதற்கு முன்பாக embedding worker-ஐ இரண்டாவது server-க்கு மாற்றவும். இதில் ஏதேனும் ஒன்று நடக்கும் வரை, நீங்கள் ஏற்கனவே வாடகைக்கு எடுத்துள்ள VPS-ல் Postgres மற்றும் pgvector-ஐப் பயன்படுத்துவது ஒரு சரியான தயாரிப்பு நிலை (production) தீர்வாகும். மேலே உள்ள எண்கள், உங்கள் வரம்பு எவ்வளவு தூரத்தில் உள்ளது என்பதை உங்களுக்குத் தெரிவிக்கும்.
FAQ
என்னால் ஒரு VPS-ல் RAG pipeline-ஐ இயக்க முடியுமா, அல்லது எனக்கு vector database தேவையா?
நூறாயிரக்கணக்கான chunks கொண்ட தரவுகளுக்கு ஒரு VPS போதுமானது. 768 dimensions-ல், 100,000 chunks என்பது 294 MiB vector தரவு, அதனுடன் text மற்றும் HNSW index ஆகியவற்றை உள்ளடக்கியது; இது சாதாரண plan-ன் RAM-ல் அடங்கிவிடும். row count-ஐ விட memory-யே முக்கிய வரம்பு, ஏனெனில் HNSW search index-ல் தாவித் தாவிச் செல்லும்; எனவே index RAM-ல் அடங்காதபோது latency அதிகரிக்கும். index-ல் pg_relation_size-ஐ free -m-உடன் ஒப்பிட்டுப் பார்த்தால் உங்கள் நிலை என்னவென்று தெரியும்.
எனது ஆவணங்களை embed செய்ய GPU தேவையா?
நீங்கள் ஒருமுறை மட்டும் embed செய்துவிட்டு, பிறகு query செய்கிறீர்கள் என்றால் தேவையில்லை. nomic-embed-text போன்ற 137 மில்லியன் parameter கொண்ட model-ஐ CPU-விலேயே இயக்கலாம்; ஒரு பெரிய corpus-ஐ முழுமையாகச் செயலாக்க சில மணிநேரங்கள் ஆகும், அதை நீங்கள் இரவு நேரத்தில் செய்து முடிக்கலாம். ஆவணங்கள் தொடர்ந்து வந்து கொண்டே இருந்தாலோ, அல்லது ஒரே server-ல் generation-ஐயும் இயக்க விரும்பினாலோ மட்டுமே GPU தேவைப்படும். உங்கள் server-ல் /api/embed-ஐப் பயன்படுத்தி ஒரு batch-ன் நேரத்தைக் கணக்கிட்டு, அதை உங்கள் மொத்த chunk எண்ணிக்கையால் பெருக்கிக் கொள்ளுங்கள், ஏனெனில் vCPU எண்ணிக்கை மாறுபடுவதால் பொதுவான புள்ளிவிவரங்கள் உங்களுக்கு உதவாது.
எனது vector query ஏன் HNSW index-க்குப் பதிலாக sequential scan-ஐப் பயன்படுத்துகிறது?
EXPLAIN (ANALYZE, BUFFERS) மூலம் plan-ஐப் படியுங்கள். இதற்குப் பொதுவான காரணம் storage ஆகும்: pgvector-ன் cost estimates-ல் out-of-line storage கணக்கில் கொள்ளப்படுவதில்லை, இதனால் serial scan மலிவானதாகத் தோன்றும். 768 dimension vector என்பது 3,080 bytes என்பதால், அது இயல்பாகவே TOAST table-ல் சேமிக்கப்படும். ALTER TABLE chunks ALTER COLUMN embedding SET STORAGE PLAIN; புதிய rows-ஐ inline-ஆக வைத்திருக்கும். மற்ற இரண்டு காரணங்கள்: index-உடன் பொருந்தாத operator (ஏனெனில் vector_cosine_ops மூலம் உருவாக்கப்பட்ட index-ஐ <=> மட்டுமே பயன்படுத்தும்), மற்றும் ORDER BY ... LIMIT இல்லாத query (ஏனெனில் approximate index என்பது ordered nearest neighbour query-களுக்கு மட்டுமே பயன்படும்).
எனது retrieval சரியாக வேலை செய்கிறதா என்பதை எப்படி அறிவது?
30 கேள்விகளைக் கொண்ட ஒரு gold set-ஐ உருவாக்குங்கள்; ஒவ்வொன்றையும் அதற்குப் பதிலளிக்கும் chunk-ன் id-உடன் இணைத்து, கேள்விக்கான embeddings-ஐயும் அதனுடன் சேமிக்கவும். பிறகு recall at 10-ஐ அளவிடுங்கள் (அதாவது, முதல் 10 முடிவுகளில் சரியான chunk எத்தனை முறை வருகிறது என்பது), மற்றும் MRR-ஐக் கணக்கிடுங்கள் (இது சரியான முடிவை முதலில் தரவரிசைப்படுத்துவதற்கு முக்கியத்துவம் அளிக்கும்). chunk size, embedding model அல்லது rank fusion ஆகியவற்றில் செய்யும் மாற்றங்கள் பயனுள்ளதா என்பதை இந்த இரண்டு எண்களே உங்களுக்குத் தெரிவிக்கும். இவை இல்லாமல் நீங்கள் settings-ஐ மாற்றிக்கொண்டு, சில பதில்களை வைத்து மட்டும் உங்கள் முடிவை நம்ப வேண்டியிருக்கும்.