সরাসরি পর্বে যান
পর্ব ৬৮ · ৯১-এর মধ্যে · মডিউল ১১

Vector index — FAISS, approximate nearest neighbor আর vector database

Vector Indexes: FAISS, Approximate Nearest Neighbour Search and Vector Databases

Taught in Bangla, this lesson computes the cost and memory of exact vector search, then builds intuition for IVF cells and nprobe, product quantization codes and HNSW's layered graph with in-page Python and NumPy. A real desktop run on the CPU indexes 100,000 Bangla Wikipedia passages embedded with multilingual e5-small in FAISS and measures recall@10, query time and index size across index types, then explains what a vector database adds around the index.

এই পর্বে যা শিখবেন

  • Brute-force search-এর খরচ আর মেমরি হাতে হিসাব করা
  • IVF: k-means-এর cell, nprobe আর recall-এর সম্পর্ক
  • Product quantization: code, codebook আর প্রতি vector-এর byte
  • HNSW: স্তর, M, efConstruction, efSearch আর লোভী খোঁজার একটি ধাপ
  • FAISS-এ আসল বাংলা embedding-এ index তুলনা, আর vector database কী কী যোগ করে

এই পর্বের desktop run: Bangla Wikipedia (Hugging Face dataset wikimedia/wikipedia, snapshot 20231101.bn, লাইসেন্স CC BY-SA) দুটি parquet ফাইলে আছে; দ্বিতীয়টির শুরু থেকে ১৫০ অক্ষরের বেশি লম্বা প্রথম ১,০০,০০০টি অনুচ্ছেদ (প্রতিটি ৬০০ অক্ষরে কাটা) এই পর্বের corpus. Embedding model intfloat/multilingual-e5-small (MIT লাইসেন্স, token ছাড়া নামানো যায়, ৩৮৪ মাত্রা), CPU-তে ৩ thread-এ। লেখার সময়ে version: Python 3.12, torch 2.15.0, sentence-transformers 6.2.0, faiss-cpu 1.15.1, qdrant-client 1.20.0. পাতায় script ছোট করে দেখানো, আউটপুট হুবহু। পাতার চারটি কোড ব্লক আপনার browser-এ চলে (Pyodide), NumPy দিয়ে নিজে লেখা ছোট index. FAISS বা embedding model পাতায় চলে না, তাই সেগুলোর ফল সত্যিকারের run থেকে হুবহু।

১ · নির্ভুল খোঁজার দাম — N × d

বড় পাইকারি বাজারে একটি নির্দিষ্ট মাপের স্ক্রু খুঁজছেন। একটি উপায়: প্রতিটি দোকানে ঢুকে জিজ্ঞেস করা। উত্তর নির্ভুল, কিন্তু ৫০০ দোকান ঘুরতে সারাদিন। অভিজ্ঞ লোক আগে জানেন হার্ডওয়্যারের গলি কোনটি, তারপর শুধু সেই গলির কয়েকটি দোকান দেখেন। মাঝে মাঝে পাশের গলির একটি ভালো দোকান বাদ পড়ে, কিন্তু সময় বাঁচে বহুগুণ। এই পর্বের পুরো বিষয় এই লেনদেন।

পর্ব ৬৭ · Sentence embedding আর semantic search-এ প্রতিটি লেখা একটি vector হয়ে যায়, আর খোঁজা মানে প্রশ্নের vector-এর সবচেয়ে কাছের k-টি vector বের করা (nearest neighbor search). Vector normalise করা থাকলে cosine আর dot product একই ক্রম দেয় (AI-এর ভিত্তি কোর্সের পর্ব ৩৩)। প্রথম vector search আর একটি ছোট FAISS তুলনা AI-এর ভিত্তি কোর্সের পর্ব ৬৯-এ দেখেছেন; এখানে প্রতিটি index-এর ভেতরের অঙ্কে নামব, আর আসল বাংলা embedding-এ মাপব।

সবচেয়ে সরল পথ: প্রশ্ন q-এর সঙ্গে সব N-টি vector-এর dot product, তারপর সেরা k. প্রতিটি dot product-এ d-টি গুণ আর যোগ, তাই প্রতি প্রশ্নে কাজ

খরচ ≈ N × d গুণ-যোগ,   মেমরি = N × d × 4 byte (float32)

এখানে N হলো vector-এর সংখ্যা, d মাত্রা (dimension), আর ৪ হলো একটি float32 সংখ্যার byte. এই পর্বের desktop corpus-এ N = ১,০০,০০০ আর d = ৩৮৪: প্রতি প্রশ্নে ৩,৮৪,০০,০০০ গুণ-যোগ, মেমরি ১,০০,০০০ × ৩৮৪ × ৪ = ১৫,৩৬,০০,০০০ byte = ১৫৩.৬ MB. Run শুরুর আগেই এই হিসাব করে নিশ্চিত হয়েছি যে embedding, তিন-চারটি index আর model একসঙ্গে ৩ GB-এর নিচে থাকবে। এক কোটি vector হলে একই হিসাবে ১৫.৩৬ GB, শুধু কাঁচা vector-এ।

সেরা k বাছতেও পুরো সাজানোর (N log N) দরকার নেই: np.argpartition গড়ে O(N) সময়ে যেকোনো ক্রমে সেরা k আলাদা করে, তারপর শুধু সেই k-টি সাজানো হয় (heap দিয়েও করা যায়, DSA কোর্সের পর্ব ২৩)। নিচের ব্লকে ২০,০০০টি ৬৪-মাত্রার কৃত্রিম, গুচ্ছবদ্ধ vector (৫০টি “বিষয়” ঘিরে ছড়ানো; আসল embedding নয়, কিন্তু আসলের মতো দলে জড়ো) আর ২০০টি প্রশ্ন। সঙ্গে recall@k: নির্ভুল সেরা k-এর কতটি আমাদের পদ্ধতি খুঁজে পেল, তার গড় ভাগ।

import numpy as np
rng = np.random.default_rng(42)
N, d, k = 20000, 64, 10
topics = rng.normal(size=(50, d))                 # ৫০টি "বিষয়" (গুচ্ছের কেন্দ্র)
X = topics[rng.integers(0, 50, N)] + 0.8 * rng.normal(size=(N, d))
X = (X / np.linalg.norm(X, axis=1, keepdims=True)).astype(np.float32)
Q = X[rng.choice(N, 200, replace=False)] + 0.3 * rng.normal(size=(200, d))
Q = (Q / np.linalg.norm(Q, axis=1, keepdims=True)).astype(np.float32)

def knn(Q, X, k):                                  # brute-force: সবার সঙ্গে dot
    S = Q @ X.T                                    # (200, N) score
    part = np.argpartition(-S, k, axis=1)[:, :k]   # O(N): যেকোনো ক্রমে সেরা k
    order = np.argsort(-np.take_along_axis(S, part, 1), axis=1)
    return np.take_along_axis(part, order, 1)

def recall_at_k(found, truth):                     # প্রতি প্রশ্নে মিল / k, তারপর গড়
    return np.mean([len(set(f) & set(t)) / len(t) for f, t in zip(found, truth)])

truth = knn(Q, X, k)
print(f"N={N}, d={d}: প্রতি প্রশ্নে {N*d:,} গুণ-যোগ, {len(Q)} প্রশ্নে {N*d*len(Q):,}")
print(f"X float32: {X.nbytes/1e6:.2f} MB = N × d × 4 byte")

scale = 127 / np.abs(X).max()                      # int8-এ চেপে রাখা
X8 = np.round(X * scale).astype(np.int8)
found8 = knn(Q, X8.astype(np.float32) / scale, k)
print(f"int8 কপি {X8.nbytes/1e6:.2f} MB, recall@10 = {recall_at_k(found8, truth):.3f}")
half = knn(Q, X[: N // 2], k)                      # অর্ধেক ডেটা দেখলে কী হয়
print(f"শুধু অর্ধেক ডেটা দেখে recall@10 = {recall_at_k(half, truth):.3f}")

ব্লকের আউটপুট:

N=20000, d=64: প্রতি প্রশ্নে 1,280,000 গুণ-যোগ, 200 প্রশ্নে 256,000,000
X float32: 5.12 MB = N × d × 4 byte
int8 কপি 1.28 MB, recall@10 = 0.975
শুধু অর্ধেক ডেটা দেখে recall@10 = 0.521

২০০টি প্রশ্নে ২৫.৬ কোটি গুণ-যোগ, অথচ ডেটা মাত্র ৫.১২ MB. দুটি পরীক্ষা পরের অংশগুলোর পূর্বাভাস। প্রতিটি সংখ্যাকে int8-এ (−১২৭ থেকে ১২৭) গোল করলে মেমরি চার ভাগের এক ভাগ, আর recall@10 ০.৯৭৫: সামান্য নির্ভুলতা হারিয়ে বড় সাশ্রয়। এটি scalar quantization (পর্ব ৫৯-এ model-এর weight-এ একই চিন্তা দেখেছেন)। আর অর্ধেক ডেটা না দেখলে recall প্রায় অর্ধেক (০.৫২১): কোন অর্ধেক বাদ দেবেন তা বুদ্ধি করে বাছতে হবে। IVF ঠিক সেটাই করে।

কখন brute-force-ই যথেষ্ট: FAISS-এর নিজের নির্দেশিকা বলে, শুধু Flat index-ই নির্ভুল ফলের নিশ্চয়তা দেয়, আর অল্প প্রশ্নে সরাসরি হিসাবই সবচেয়ে দক্ষ। বাজারঘরের কয়েকশো help নথি বা কয়েক হাজার রিভিউয়ে ANN index-এর দরকার নেই; একটি NumPy matrix গুণই মিলিসেকেন্ডে শেষ।

২ · প্রায়-নিকটতম খোঁজা — তিনটি পরিবার, তিনদিকের লেনদেন

Approximate nearest neighbor (ANN) পদ্ধতিগুলো সব vector দেখে না। বিনিময়ে মাঝে মাঝে প্রকৃত সেরা k-এর কয়েকটি হারায়। প্রতিটি পদ্ধতি তিনটি জিনিসের মধ্যে লেনদেন করে: recall (কতটা নির্ভুল), latency (প্রতি প্রশ্নে কত সময়) আর মেমরি (index কত বড়)। সঙ্গে আরও দুটি খরচ: index বানানোর সময়, আর নতুন vector যোগ করা কতটা সহজ।

Flat, IVF and HNSW search ক · Flat (brute-force) সব N-টি দেখে q খ · IVF (cell ভাগ, nprobe = 2) ২টি cell দেখে q ■ centroid · রঙহীন cell বাদ গ · HNSW (স্তরের graph) লোভী হাঁটা স্তর ২স্তর ১স্তর ০
ক: Flat প্রতিটি বিন্দু মাপে। খ: IVF আগে ভাগ করে রাখে, প্রশ্নের কাছের দুটি cell-ই শুধু দেখে। গ: HNSW ওপরের ফাঁকা স্তরে লম্বা লাফে কাছাকাছি যায়, তারপর নিচের ঘন স্তরে ছোট পা ফেলে।
পরিবারমূল চিন্তামূল knobমেমরি
Flatসব দেখানেইN × d × 4
IVFভাগ করে কাছের ভাগ দেখাnlist, nprobeপ্রায় Flat-এর সমান + centroid
PQvector চেপে ছোট codem, nbitsN × m byte (nbits = 8)
HNSWপ্রতিবেশীর graph-এ হাঁটাM, efConstruction, efSearchFlat + প্রতি vector-এ link

এগুলো জোড়াও লাগে: IVF + PQ (IVFPQ) বড় মাপের সবচেয়ে চেনা জুটি, আর HNSW দিয়ে IVF-এর centroid খোঁজাও হয়। পরের তিনটি অংশে প্রতিটি হাতে-কলমে।

৩ · IVF — cell ভাগ আর nprobe

IVF মানে inverted file. নামটি এসেছে text search-এর inverted index থেকে (সিস্টেম ডিজাইন কোর্সের পর্ব ২৭): সেখানে শব্দ থেকে নথির তালিকা, এখানে cell থেকে vector-এর তালিকা। ধাপ দুটি:

  • বানানোর সময় (train + add): k-means দিয়ে nlist-টি centroid শেখা (AI-এর ভিত্তি কোর্সের পর্ব ৬৫)। প্রতিটি vector তার সবচেয়ে কাছের centroid-এর তালিকায় যায়। প্রতিটি centroid ঘিরে জায়গাটুকু একটি Voronoi cell.
  • খোঁজার সময়: প্রশ্নকে nlist-টি centroid-এর সঙ্গে মেপে কাছের nprobe-টি cell বাছা, তারপর শুধু সেই তালিকাগুলোয় brute-force.

খরচ মোটামুটি nlist + nprobe × N / nlist দূরত্ব (cell সমান মাপের হলে)। N = ১ লাখ, nlist = ১০২৪, nprobe = ১৬ হলে ১০২৪ + ১৬ × ৯৮ ≈ ২,৫৮৬, অর্থাৎ ১ লাখের ৩ ভাগেরও কম। FAISS-এর নির্দেশিকা nlist রাখতে বলে মোটামুটি ৪√N থেকে ১৬√N, আর train-এ ৩০ × nlist থেকে ২৫৬ × nlist-টি vector. নিচের খেলনা উদাহরণে ঝুঁকিটা হাতে দেখুন।

২-মাত্রার ৯টি বিন্দু, ৪টি cell, প্রশ্ন q = (4, 6), দূরত্ব² দিয়ে
অবস্থা
পড়া vector
top-3
দূরত্ব হিসাব
recall@3
  1. বানানো. k-means চারটি centroid দিয়েছে, প্রতিটি বিন্দু কাছের centroid-এর তালিকায়। নির্ভুল top-3 আগে থেকে জেনে রাখি: B (দূরত্ব² ২), E (৪), C (৫)।
  2. Centroid মাপা. q = (4, 6) থেকে c1 পর্যন্ত (4−2)² + (6−8)² = ৮; একইভাবে c2 ২০, c3 ২৯, c4 ৩২. ক্রম: c1, c2, c3, c4.
  3. nprobe = 1. শুধু c1-এর A (১৮) আর B (২)। তালিকায় দুটিই, তাই top-3 চাইলে ফল মাত্র ২টি: B, A. প্রকৃত তিনটির একটি (B) মিলেছে, recall@3 = ১/৩.
  4. nprobe = 2. c2 যোগ: C (৫), D (৩৪)। এখন top-3: B, C, A. দুটি মিলেছে, recall@3 = ২/৩. কাজ ৮টি দূরত্ব।
  5. সীমানার ফাঁদ. E প্রশ্নের দ্বিতীয় নিকটতম, কিন্তু E নিজে c3-এর কাছে (১৩) c1-এর চেয়ে (২০)। তাই সে c3-এর তালিকায়, আর c3 প্রশ্ন থেকে তৃতীয় দূরের cell. প্রশ্ন cell-এর সীমানার কাছে থাকলে এমনই হয়।
  6. nprobe = 3. c3 যোগ হতেই B, E, C: recall ১. কাজ ১১টি দূরত্ব, exact-এর ৯টির চেয়েও বেশি! খেলনা মাপে IVF লাভ দেয় না; লাভ আসে যখন N লাখ আর প্রতি cell-এ শত শত vector.

এবার একই চিন্তা ২০,০০০টি vector-এ। k-means হাতে লেখা (scikit-learn-এর KMeans পাতায় একটি অপ্রাসঙ্গিক warning দেয়), nlist = ৬৪, তারপর nprobe বাড়িয়ে recall আর কাজ।

import numpy as np
rng = np.random.default_rng(42)
N, d, k, nlist = 20000, 64, 10, 64
topics = rng.normal(size=(50, d))
X = topics[rng.integers(0, 50, N)] + 0.8 * rng.normal(size=(N, d))
X = (X / np.linalg.norm(X, axis=1, keepdims=True)).astype(np.float32)
Q = X[rng.choice(N, 200, replace=False)] + 0.3 * rng.normal(size=(200, d))
Q = (Q / np.linalg.norm(Q, axis=1, keepdims=True)).astype(np.float32)
truth = np.argsort(-(Q @ X.T), axis=1)[:, :k]      # নির্ভুল উত্তর

# k-means হাতে (scikit-learn-এর KMeans পাতায় অপ্রয়োজনীয় warning দেয়)
C = X[rng.choice(N, nlist, replace=False)].copy()
for it in range(10):
    assign = np.argmax(X @ C.T, axis=1)            # সবচেয়ে কাছের centroid
    for c in range(nlist):
        m = X[assign == c]
        if len(m): C[c] = m.mean(0) / np.linalg.norm(m.mean(0))
lists = [np.where(assign == c)[0] for c in range(nlist)]   # inverted list
sizes = np.array([len(l) for l in lists])
print(f"nlist={nlist}: list-এর আকার min {sizes.min()}, গড় {sizes.mean():.0f}, max {sizes.max()}")

def ivf_search(q, nprobe):
    probe = np.argsort(-(C @ q))[:nprobe]          # ধাপ ১: কাছের nprobe-টি cell
    cand = np.concatenate([lists[c] for c in probe])
    s = X[cand] @ q                                # ধাপ ২: শুধু তাদের ভেতরে
    return cand[np.argsort(-s)[:k]], nlist + len(cand)

for nprobe in [1, 2, 4, 8, 16]:
    res = [ivf_search(q, nprobe) for q in Q]
    rec = np.mean([len(set(f) & set(t)) / k for (f, _), t in zip(res, truth)])
    work = np.mean([w for _, w in res])
    print(f"nprobe={nprobe:2d}  দূরত্ব/প্রশ্ন {work:7.0f} ({work/N:5.1%})  recall@10 {rec:.3f}")

ব্লকের আউটপুট:

nlist=64: list-এর আকার min 85, গড় 312, max 809
nprobe= 1  দূরত্ব/প্রশ্ন     414 ( 2.1%)  recall@10 0.637
nprobe= 2  দূরত্ব/প্রশ্ন     734 ( 3.7%)  recall@10 0.793
nprobe= 4  দূরত্ব/প্রশ্ন    1377 ( 6.9%)  recall@10 0.904
nprobe= 8  দূরত্ব/প্রশ্ন    2631 (13.2%)  recall@10 0.968
nprobe=16  দূরত্ব/প্রশ্ন    5107 (25.5%)  recall@10 0.990

তিনটি জিনিস লক্ষ করুন। (১) cell সমান নয়: সবচেয়ে ছোটটিতে ৮৫, বড়টিতে ৮০৯টি vector; বড় cell-এ পড়া প্রশ্ন বেশি সময় নেয়। (২) nprobe দ্বিগুণ করলে কাজ প্রায় দ্বিগুণ, কিন্তু recall-এর লাভ ক্রমে কমে: ১ থেকে ২-এ +০.১৫৬, ৮ থেকে ১৬-এ মাত্র +০.০২২. (৩) ১৩.২% দূরত্ব হিসাব করেই recall ০.৯৬৮. আসল embedding-এ ঠিক কোথায় থামবেন, তা ডেটা বলে দেবে; §৬-এ দেখব।

৪ · Product quantization — ১৫৩৬ byte থেকে ৪৮ byte

IVF সময় বাঁচায়, মেমরি নয়: প্রতিটি vector তখনও পুরো float32-এ থাকে। এক কোটি ৩৮৪-মাত্রার vector মানে ১৫ GB-এর বেশি RAM. Product quantization (PQ) vector-কেই চেপে ছোট করে।

ঢাকার একটি ঠিকানা পুরো লিখলে লম্বা: “বাড়ি ১২, রোড ৫, ব্লক সি, মিরপুর ১০”। কিন্তু যদি প্রতিটি অংশের জন্য একটি ছোট তালিকা থাকে (এলাকার তালিকা, ব্লকের তালিকা, রোডের তালিকা), তাহলে ঠিকানা হয়ে যায় কয়েকটি ক্রমিক নম্বর: “এলাকা ৩৭, ব্লক ৩, রোড ৫”। PQ ঠিক এই কাজ করে vector-এর টুকরো ধরে।

  • টুকরো করা: d-মাত্রার vector-কে m-টি সমান টুকরোয় ভাগ, প্রতিটি d/m মাত্রার।
  • প্রতি টুকরোয় আলাদা k-means: সাধারণত ২৫৬টি centroid, যাদের তালিকাকে বলে codebook.
  • Encode: প্রতিটি টুকরোর জায়গায় শুধু কাছের centroid-এর নম্বর (০–২৫৫), অর্থাৎ ১ byte. পুরো vector = m byte.
  • খোঁজা (ADC): প্রশ্ন চাপা হয় না। প্রতি টুকরোয় প্রশ্নের সঙ্গে ২৫৬টি centroid-এর dot product একবার হিসাব করে টেবিল বানানো; তারপর প্রতিটি ডেটা vector-এর score = m-টি টেবিল-ঘর পড়ে যোগ।
d = 384, m = 48, nbits = 8 (এই পর্বের FAISS run-এর সেটিং)
হিসাব
ফল
  1. কাঁচা vector. ৩৮৪টি float32, প্রতিটি ৪ byte: ১৫৩৬ byte প্রতি vector.
  2. টুকরো. m = ৪৮ মানে ৪৮টি টুকরো, প্রতিটিতে ৩৮৪ ÷ ৪৮ = ৮টি মাত্রা।
  3. nbits = 8. প্রতি টুকরোয় ২৮ = ২৫৬টি centroid; নম্বর রাখতে ঠিক ১ byte লাগে।
  4. Code. ৪৮টি টুকরো × ১ byte = ৪৮ byte. ১৫৩৬ ÷ ৪৮ = ৩২ গুণ ছোট।
  5. Codebook. ৪৮ টুকরো × ২৫৬ centroid × ৮ মাত্রা × ৪ byte = ৩,৯৩,২১৬ byte. পুরো index-এ একবারই থাকে, N বাড়লেও বাড়ে না।
  6. Id. IVF-এর তালিকায় প্রতিটি vector-এর id-ও থাকে; FAISS-এ সেটি ৬৪-bit, অর্থাৎ ৮ byte. তাই IVFPQ-তে প্রতি vector প্রায় ৪৮ + ৮ = ৫৬ byte.
  7. এক কোটিতে. Flat: ১০৭ × ১৫৩৬ = ১৫.৩৬ GB. PQ code: ১০৭ × ৪৮ = ০.৪৮ GB; id-সহ ০.৫৬ GB. একটি সাধারণ সার্ভারের RAM-এ আঁটে, দাম নির্ভুলতায়।

দাম কতটা, নিচের ব্লকে দেখুন। ৬৪-মাত্রার vector, m = ১৬ (প্রতি টুকরো ৪ মাত্রা), ২৫৬টি codeword: ২৫৬ byte থেকে ১৬ byte. শেষে একটি প্রচলিত কৌশল: PQ score দিয়ে ১০০টি প্রার্থী, তারপর শুধু সেই ১০০টির আসল vector দিয়ে আবার সাজানো (re-ranking).

import numpy as np
rng = np.random.default_rng(42)
N, d, k, m, ks = 20000, 64, 10, 16, 256             # m টুকরো, প্রতিটিতে 256 codeword
ds = d // m                                        # প্রতি টুকরো 4 মাত্রা
topics = rng.normal(size=(50, d))
X = topics[rng.integers(0, 50, N)] + 0.8 * rng.normal(size=(N, d))
X = (X / np.linalg.norm(X, axis=1, keepdims=True)).astype(np.float32)
Q = X[rng.choice(N, 200, replace=False)] + 0.3 * rng.normal(size=(200, d))
Q = (Q / np.linalg.norm(Q, axis=1, keepdims=True)).astype(np.float32)
truth = np.argsort(-(Q @ X.T), axis=1)[:, :k]
near = lambda A, C: np.argmin((C**2).sum(1) - 2 * A @ C.T, axis=1)   # সবচেয়ে কাছের codeword

books, codes = [], np.empty((N, m), dtype=np.uint8)
train = X[rng.choice(N, 4000, replace=False)]
for j in range(m):                                 # প্রতি টুকরোয় আলাদা k-means
    sub = train[:, j*ds:(j+1)*ds]
    C = sub[rng.choice(len(sub), ks, replace=False)].copy()
    for it in range(6):
        a = near(sub, C)
        for c in np.unique(a): C[c] = sub[a == c].mean(0)
    codes[:, j] = near(X[:, j*ds:(j+1)*ds], C)     # encode: 4টি সংখ্যা → 1 byte
    books.append(C)
Xhat = np.hstack([books[j][codes[:, j]] for j in range(m)])   # decode
print(f"প্রতি vector: float32 {d*4} byte → PQ code {codes[0].nbytes} byte; codebook মোট {m*ks*ds*4/1024:.0f} KiB")
print(f"গড় পুনর্গঠন ভুল ‖x − x̂‖ = {np.linalg.norm(X - Xhat, axis=1).mean():.3f} (‖x‖ = 1)")

def adc(q):                                        # প্রশ্ন আসল, ডেটা code: টেবিল দেখে যোগ
    T = np.stack([books[j] @ q[j*ds:(j+1)*ds] for j in range(m)])   # (m, 256)
    return T[np.arange(m), codes].sum(1)           # প্রতি vector-এ m-টি টেবিল-পড়া
rec = lambda F: np.mean([len(set(f) & set(t)) / k for f, t in zip(F, truth)])
found = [np.argsort(-adc(q))[:k] for q in Q]
short = [np.argsort(-adc(q))[:100] for q in Q]     # PQ দিয়ে 100 প্রার্থী, তারপর আসল vector
rr = [c[np.argsort(-(X[c] @ q))[:k]] for c, q in zip(short, Q)]
print(f"শুধু PQ score: recall@10 {rec(found):.3f};  PQ-তে top-100, আসল vector-এ re-rank: {rec(rr):.3f}")

ব্লকের আউটপুট:

প্রতি vector: float32 256 byte → PQ code 16 byte; codebook মোট 64 KiB
গড় পুনর্গঠন ভুল ‖x − x̂‖ = 0.296 (‖x‖ = 1)
শুধু PQ score: recall@10 0.478;  PQ-তে top-100, আসল vector-এ re-rank: 0.967

১৬ গুণ ছোট code-এ score-এর ভুল বড়: গড় পুনর্গঠন ভুল ০.২৯৬, অর্থাৎ ১ দৈর্ঘ্যের vector-এর প্রায় ৩০%, আর শুধু PQ score দিয়ে recall@10 মাত্র ০.৪৭৮. কিন্তু PQ-এর সেরা ১০০-র ভেতরে প্রকৃত সেরা ১০-এর প্রায় সবই আছে: শুধু ১০০টি আসল vector দিয়ে আবার সাজাতেই recall ০.৯৬৭. বড় ব্যবস্থায় এটি প্রচলিত নকশা: RAM-এ ছোট code, আসল vector disk-এ বা আলাদা জায়গায়, আর শেষে অল্প কয়েকটির re-ranking. Codebook ৬৪ KiB, vector যত বাড়ুক একই থাকে।

IVFPQ-তে একটি বাড়তি কৌশল: FAISS-এর IndexIVFPQ পুরো vector নয়, vector থেকে তার cell-এর centroid বিয়োগ করা অবশিষ্টাংশ (residual) চাপে। Residual ছোট আর বেশি একরকম, তাই একই ৪৮ byte-এ ভুল কম। প্রশ্ন কোন cell দেখছে, সেই অনুযায়ী টেবিলও বদলায়।

৫ · HNSW — স্তরের graph-এ লোভী হাঁটা

অচেনা শহরে একটি ঠিকানা খুঁজতে আপনি আগে highway ধরে সঠিক এলাকায় যান, তারপর বড় রাস্তা, শেষে গলি। প্রতিটি মোড়ে শুধু জিজ্ঞেস করেন, “কোন দিকে গেলে আরও কাছে?” HNSW (Hierarchical Navigable Small World) এভাবেই কাজ করে। Yury Malkov আর Dmitry Yashunin ২০১৬ সালে arXiv-এ প্রস্তাব করেন (জার্নালে ২০১৮)। এর আগের NSW graph-এ একটিই স্তর ছিল।

  • গঠন: প্রতিটি vector একটি node. প্রতিটি node-এর জন্য এলোমেলোভাবে একটি সর্বোচ্চ স্তর বাছা হয় (উঁচু স্তরের সম্ভাবনা জ্যামিতিক হারে কমে), তাই স্তর ০-এ সবাই, ওপরে অল্প কয়েকজন। প্রতিটি স্তরে node তার কাছের কয়েকটির সঙ্গে যুক্ত। Graph-এর গঠন DSA কোর্সের পর্ব ৩৯-এ।
  • M: প্রতি node-এ link-এর সংখ্যা (FAISS-এ স্তর ০-এ ২M). বেশি M মানে ভালো recall, কিন্তু বেশি মেমরি আর ধীর build. FAISS নির্দেশিকা: ৪ ≤ M ≤ ৬৪, আর মেমরি প্রতি vector-এ (d × ৪ + M × ২ × ৪) byte.
  • efConstruction: বানানোর সময় প্রতিটি নতুন node-এর প্রতিবেশী খুঁজতে কতটি প্রার্থী রাখা হয়। বড় হলে graph ভালো, build ধীর।
  • efSearch: খোঁজার সময় স্তর ০-এ কতটি প্রার্থীর তালিকা রাখা হয় (k-এর সমান বা বেশি)। এটিই খোঁজার সময়ের মূল knob, আর index আবার না বানিয়েই বদলানো যায়।

ওপরের স্তরগুলোয় খোঁজা একেবারে লোভী: বর্তমান node-এর প্রতিবেশীদের মধ্যে প্রশ্নের সবচেয়ে কাছেরটিতে যাওয়া, যতক্ষণ কাছে যাওয়া যায়। তারপর এক স্তর নিচে, একই node থেকে শুরু। নিচের stepper-এ efSearch = ১ ধরে (শুধু একটি প্রার্থী) পুরো পথ।

প্রশ্ন q = (8, 7); স্তর ১-এ P, S, T; স্তর ০-এ ১২টি node; দূরত্ব²
কোথায়
মাপা প্রতিবেশী
সিদ্ধান্ত
মোট দূরত্ব হিসাব
  1. প্রবেশ. খোঁজা শুরু হয় ওপরের স্তরের নির্দিষ্ট প্রবেশ বিন্দু P = (1, 1) থেকে। q থেকে দূরত্ব² (8−1)² + (7−1)² = ৪৯ + ৩৬ = ৮৫.
  2. স্তর ১-এ এক ধাপ. P-এর প্রতিবেশী S = (5, 4): ৯ + ৯ = ১৮, T = (9, 2): ১ + ২৫ = ২৬. সবচেয়ে কাছে S, আর ১৮ < ৮৫, তাই S-এ যাওয়া। এটিই একটি লোভী ধাপ।
  3. স্থানীয় সর্বনিম্ন. S-এর প্রতিবেশী P (৮৫) আর T (২৬), দুটিই আগে মাপা, কেউ ১৮-এর কম নয়। এই স্তরে আর এগোনো যায় না, তাই S নিয়েই স্তর ০-এ নামা।
  4. স্তর ০-এ. এখানে S-এর প্রতিবেশী বেশি: U = (6, 6) ৫, V = (4, 7) ১৬, W = (5, 2) ৩৪. U-তে যাওয়া।
  5. আবার এক ধাপ. U-এর প্রতিবেশী X = (8, 8) ১, Y = (7, 5) ৫. X-এ যাওয়া।
  6. থামা. X-এর প্রতিবেশী Z = (10, 9) ৮ আর U ৫; কেউ ১-এর কম নয়। উত্তর X, ১২টি node-এর মধ্যে ৯টির দূরত্ব হিসাব করে। efSearch বড় হলে একটির বদলে কয়েকটি প্রার্থী একসঙ্গে রাখা হয়, তাই ভুল স্থানীয় সর্বনিম্নে আটকে যাওয়ার সম্ভাবনা কমে।

HNSW-এর দুর্বলতাও জানা দরকার। সব vector আর link RAM-এ রাখতে হয়, তাই মেমরি Flat-এর চেয়েও বেশি। মোছা কঠিন: একটি node সরালে graph-এ ফাঁক তৈরি হয়, তাই অনেক ব্যবস্থা আগে “মোছা” চিহ্ন দেয়, পরে graph আবার গোছায়। আর build ধীর, যেটি §৬-এর run-এ মাপা হয়েছে।

৬ · সত্যিকারের run — ১ লাখ বাংলা অনুচ্ছেদে FAISS

খেলনা ডেটা থেকে এবার আসল বাংলা লেখায়। Corpus: Bangla Wikipedia-র ১,০০,০০০টি অনুচ্ছেদ (কীভাবে বাছা হলো, পাতার শুরুতে লেখা)। দুই রকম প্রশ্ন, প্রতিটি ১০০০টি: (ক) প্রবন্ধের শিরোনাম, “query: ” prefix-সহ, যেমন “বাগেরহাট রেলওয়ে স্টেশন” বা “হিমশৈল”; ছোট, আর অনুচ্ছেদের চেহারা থেকে আলাদা। (খ) corpus-এ নেই এমন প্রবন্ধের একটি করে অনুচ্ছেদ, “passage: ” prefix-সহ; ডেটার মতোই দেখতে, যেমন “এই লেখার মতো আরও লেখা দেখাও” ধরনের খোঁজায়। প্রতিটি index-এর recall@10 মাপা Flat-এর নির্ভুল উত্তরের বিপরীতে। সময় মাপা হয়েছে একটি একটি প্রশ্ন করে, এক thread-এ, অনলাইন সেবার মতো।

~/bazarghar-dl$ uv run python embed.py 100000     # সত্যিকারের run, CPU
articles read 71534, used 20807, passages 100000, queries 1000
mean passage chars 359.6
embed 100000 passages on CPU (3 threads): 2417 s, shape (100000, 384), float32, 153.6 MB
~/bazarghar-dl$ uv run python embed2.py
held-out passages 1000 (articles not in corpus), embed 27 s

শুধু embed করতেই CPU-তে ২৪১৭ সেকেন্ড, প্রায় ৪০ মিনিট; index বানানো তার তুলনায় সেকেন্ডের ব্যাপার। Model বদলালে এই খরচ আবার লাগবে (ভাবনার প্রশ্ন ৪)। Index বানানো আর মাপার মূল অংশ:

# মূল অংশ, ছোট করে (সত্যিকারের bench.py থেকে)
flat = faiss.IndexFlatIP(d); flat.add(X)            # নির্ভুল মাপকাঠি
ivf = faiss.IndexIVFFlat(faiss.IndexFlatIP(d), d, 1024, faiss.METRIC_INNER_PRODUCT)
ivf.train(X); ivf.add(X)                            # train = k-means, ১০২৪টি cell
ivf.nprobe = 16
pq = faiss.IndexIVFPQ(faiss.IndexFlatIP(d), d, 1024, 48, 8, faiss.METRIC_INNER_PRODUCT)
ref = faiss.IndexRefineFlat(pq); ref.k_factor = 10  # PQ-তে ১০০ প্রার্থী, আসল vector-এ re-rank
h = faiss.IndexHNSWFlat(d, 32, faiss.METRIC_INNER_PRODUCT)
h.add(X); h.hnsw.efSearch = 256                     # HNSW-এ train নেই
for i in range(len(Q)):                             # এক এক করে প্রশ্ন, faiss.omp_set_num_threads(1)
    I[i] = ix.search(Q[i:i+1], 10)[1][0]
mb = faiss.serialize_index(ix).nbytes / 1e6         # index-এর আকার
~/bazarghar-dl$ uv run python bench.py     # সত্যিকারের run, একবার
Python 3.12.13, numpy 2.5.4, faiss 1.15.1
X (100000, 384) float32 = 153.6 MB; queries: 1000 titles, 1000 held-out passages
Flat      exact        recall@10 title 1.000 passage 1.000 |  8.641 ms/q |  153.6 MB | hit@10 0.931
build IVFFlat nlist=1024: 4.0 s
IVFFlat   nprobe=1     recall@10 title 0.174 passage 0.360 |  0.048 ms/q |  156.0 MB | hit@10 0.241
IVFFlat   nprobe=4     recall@10 title 0.342 passage 0.619 |  0.065 ms/q |  156.0 MB | hit@10 0.399
IVFFlat   nprobe=16    recall@10 title 0.578 passage 0.833 |  0.212 ms/q |  156.0 MB | hit@10 0.573
IVFFlat   nprobe=64    recall@10 title 0.775 passage 0.951 |  0.642 ms/q |  156.0 MB | hit@10 0.711
IVFFlat   nprobe=256   recall@10 title 0.920 passage 0.994 |  2.377 ms/q |  156.0 MB | hit@10 0.834
build IVFPQ m=48 nbits=8 (+refine copy): 35.7 s
IVFPQ     nprobe=16    recall@10 title 0.292 passage 0.392 |  0.120 ms/q |    7.6 MB | hit@10 0.537
IVFPQ     nprobe=64    recall@10 title 0.335 passage 0.403 |  0.274 ms/q |    7.6 MB | hit@10 0.651
IVFPQ+ref np=64 kf=10  recall@10 title 0.650 passage 0.844 |  0.292 ms/q |  161.2 MB | hit@10 0.703
build HNSW M=16 efConstruction=40: 4.4 s
HNSW16    ef=16        recall@10 title 0.507 passage 0.676 |  0.076 ms/q |  168.0 MB | hit@10 0.487
HNSW16    ef=64        recall@10 title 0.734 passage 0.877 |  0.180 ms/q |  168.0 MB | hit@10 0.678
HNSW16    ef=256       recall@10 title 0.885 passage 0.965 |  0.597 ms/q |  168.0 MB | hit@10 0.797
HNSW16    ef=1024      recall@10 title 0.956 passage 0.987 |  2.899 ms/q |  168.0 MB | hit@10 0.872
build HNSW M=32 efConstruction=40: 7.4 s
HNSW32    ef=16        recall@10 title 0.658 passage 0.820 |  0.116 ms/q |  180.8 MB | hit@10 0.615
HNSW32    ef=64        recall@10 title 0.850 passage 0.952 |  0.295 ms/q |  180.8 MB | hit@10 0.772
HNSW32    ef=256       recall@10 title 0.949 passage 0.991 |  0.935 ms/q |  180.8 MB | hit@10 0.865
HNSW32    ef=1024      recall@10 title 0.985 passage 0.999 |  3.979 ms/q |  180.8 MB | hit@10 0.915

একটি run, একটি যন্ত্রে; সময় যন্ত্রভেদে বদলাবে, অনুপাতগুলোই দেখার। শেষ কলাম hit@10 খোঁজার মানের একটি মোটা মাপ: শিরোনাম-প্রশ্নের নিজের প্রবন্ধের অন্তত একটি অনুচ্ছেদ সেরা ১০-এ এলো কি না। পাঁচটি শিক্ষা:

  1. প্রশ্নের ধরন সবচেয়ে বড় প্রভাব. একই index-এ অনুচ্ছেদ-প্রশ্নের recall অনেক বেশি: IVF nprobe = ৬৪-এ ০.৯৫১ বনাম শিরোনামে ০.৭৭৫. ছোট শিরোনামের vector ডেটার গুচ্ছ থেকে দূরে পড়ে, তাই তার প্রতিবেশীরা অনেক cell-এ ছড়ানো। তাই recall মাপুন বাস্তবের মতো প্রশ্নে।
  2. HNSW এই ডেটায় এগিয়ে. M = ৩২, efSearch = ২৫৬: শিরোনামে ০.৯৪৯, অনুচ্ছেদে ০.৯৯১, ০.৯৩৫ ms; Flat-এর ৮.৬৪১ ms-এর চেয়ে প্রায় ৯ গুণ দ্রুত। M = ১৬-এর চেয়ে একই efSearch-এ recall বেশি, মেমরি ১২.৮ MB বেশি (FAISS-এর সূত্রে ১৭৯.২ MB, মাপা ১৮০.৮), build প্রায় দ্বিগুণ (৪.৪ থেকে ৭.৪ সেকেন্ড)।
  3. শুধু PQ এখানে খুব মোটা দাগের. ৪৮ byte-এ index ৭.৬ MB (Flat-এর প্রায় ২০ ভাগের এক ভাগ), কিন্তু recall ০.৪-এর নিচে, nprobe বাড়িয়েও লাভ নেই। আকারটি §৪-এর হিসাবের সঙ্গে মেলে: ১,০০,০০০ × ৫৬ byte = ৫.৬ MB, সঙ্গে ১০২৪টি centroid (১.৫৭ MB) আর codebook (০.৩৯ MB). আসল vector-এ re-ranking দিলে শিরোনামে ০.৬৫০, অনুচ্ছেদে ০.৮৪৪, কিন্তু তখন আসল vector-ও রাখতে হয় (১৬১.২ MB).
  4. Index-এর ভুল খোঁজার মানেও লাগে. Flat-এর hit@10 ০.৯৩১; HNSW32 efSearch = ৬৪-এ ০.৭৭২. প্রকৃত প্রতিবেশী হারালে প্রাসঙ্গিক অনুচ্ছেদও হারায়।
  5. Batch বদলে দেয় হিসাব. আগের একটি run-এ একই ১০০০টি প্রশ্ন একসঙ্গে একটি matrix গুণে দিলে Flat-এর গড় ছিল ০.৩৩১ ms/প্রশ্ন, এক thread-এই। Offline কাজে (যেমন রাতে সব নথির প্রতিবেশী বের করা) Flat-ই প্রায়ই যথেষ্ট।

এবার সেই ফল (হুবহু, JSON হিসেবে পাতায় রাখা) দিয়ে সিদ্ধান্ত নিন: recall@10 ≥ ০.৯৫ লক্ষ্য হলে কোন সেটিং সবচেয়ে দ্রুত? দুই ধরনের প্রশ্নে আলাদা করে।

import json
import pandas as pd
import matplotlib.pyplot as plt
pd.set_option("display.width", 72)

df = pd.DataFrame(json.load(open("faiss_bench.json")))   # সত্যিকারের run-এর ফল, হুবহু
flat_ms = df.loc[df["index"] == "Flat", "ms"].iloc[0]
for col, name in [("r_title", "শিরোনাম-প্রশ্ন"), ("r_pass", "অনুচ্ছেদ-প্রশ্ন")]:
    ok = df[(df[col] >= 0.95) & (df["index"] != "Flat")].sort_values("ms")
    b = ok.iloc[0]
    print(f"{name}: recall@10 ≥ 0.95 মানে {len(ok)}টি সেটিং; সবচেয়ে দ্রুত "
          f"{b['index']} {b['setting']} ({b[col]}, {b['ms']} ms, Flat-এর {flat_ms / b['ms']:.0f} গুণ)")

print("\nhit@10 (শিরোনামের নিজের প্রবন্ধ top-10-এ), Flat-এর তুলনায়:")
t = df[df["index"].isin(["Flat", "HNSW32"])][["index", "setting", "r_title", "hit"]]
print(t.to_string(index=False))

fig, axes = plt.subplots(1, 2, figsize=(8, 3.6), sharey=True)
for ax, col, title in [(axes[0], "r_title", "short title queries"),
                       (axes[1], "r_pass", "passage-like queries")]:
    for name, g in df[df["index"] != "Flat"].groupby("index"):
        ax.plot(g["ms"], g[col], "o-", label=name, ms=4)
    ax.axhline(0.95, color="gray", ls="--", lw=1)
    ax.axvline(flat_ms, color="black", ls=":", lw=1)
    ax.set_xscale("log"); ax.set_title(title, fontsize=10)
    ax.set_xlabel("ms per query (1 thread)")
axes[0].set_ylabel("recall@10 vs exact")
axes[1].legend(fontsize=7, loc="lower right")
fig.subplots_adjust(wspace=0.08, bottom=0.17)
plt.show()

ব্লকের আউটপুট:

শিরোনাম-প্রশ্ন: recall@10 ≥ 0.95 মানে 2টি সেটিং; সবচেয়ে দ্রুত HNSW16 ef=1024 (0.956, 2.899 ms, Flat-এর 3 গুণ)
অনুচ্ছেদ-প্রশ্ন: recall@10 ≥ 0.95 মানে 7টি সেটিং; সবচেয়ে দ্রুত HNSW32 ef=64 (0.952, 0.295 ms, Flat-এর 29 গুণ)

hit@10 (শিরোনামের নিজের প্রবন্ধ top-10-এ), Flat-এর তুলনায়:
 index setting  r_title   hit
  Flat   exact    1.000 0.931
HNSW32   ef=16    0.658 0.615
HNSW32   ef=64    0.850 0.772
HNSW32  ef=256    0.949 0.865
HNSW32 ef=1024    0.985 0.915

একই লক্ষ্য, দুই রকম উত্তর। অনুচ্ছেদের মতো প্রশ্নে সাতটি সেটিং লক্ষ্য মানে, সবচেয়ে দ্রুতটি Flat-এর ২৯ গুণ। ছোট শিরোনাম-প্রশ্নে মাত্র দুটি, আর সবচেয়ে দ্রুতটিও মাত্র ৩ গুণ দ্রুত। HNSW32 efSearch = ২৫৬ (০.৯৪৯) লক্ষ্যের ০.০০১ নিচে; ১০০০টি প্রশ্নের নমুনায় এই পার্থক্য অনিশ্চয়তার ভেতরে, তাই সীমার একদম কাছের সিদ্ধান্তে bootstrap CI দেখুন (মেশিন লার্নিং কোর্সের পর্ব ২১)। নিচের সারণিতে recall বাড়ার সঙ্গে hit@10-ও বাড়ে, ০.৬১৫ থেকে ০.৯১৫, Flat-এর ০.৯৩১-এর কাছে। ছবিতে কালো বিন্দুরেখা Flat-এর সময়, ধূসর রেখা লক্ষ্য। PQ-র রেখা লক্ষ্যের অনেক নিচে আটকে থাকে।

৭ · Vector database — index-এর চারপাশে যা লাগে

FAISS একটি library: আপনার program-এর ভেতরে, RAM-এ index. Id আর মূল লেখার হিসাব আপনার; ফাইলে রাখতে faiss.write_index; নির্দিষ্ট id-এর মধ্যে খোঁজার ব্যবস্থা (IDSelector) আছে, কিন্তু কোন id কোন জেলার, তা আপনাকেই জানতে হয়। একটি দোকানের হিসাব খাতা যেমন, আর পুরো একটি ব্যাংক যেমন: খাতায় যোগ-বিয়োগ ঠিকই হয়, কিন্তু শাখা, ভল্ট, অডিট আর একাধিক কপি ব্যাংকের কাজ। Vector database index-এর চারপাশে এই কাজগুলো যোগ করে:

  • Metadata আর ফিল্টার: প্রতিটি vector-এর পাশে payload (বিষয়, ভাষা, জেলা, অনুমতি), আর খোঁজার সাথেই শর্ত (metadata filtering; pre-filtering আর post-filtering-এর পার্থক্য DBMS কোর্সের পর্ব ৬০-এ)।
  • যোগ, হালনাগাদ, মোছা: id ধরে upsert আর delete, index আবার না বানিয়ে।
  • Disk-এ থাকা আর recovery: program বন্ধ হলেও ডেটা থাকে, crash-এর পর ফেরে।
  • ছড়ানো: বড় ডেটা কয়েকটি সার্ভারে ভাগ (sharding) আর কপি (replication), সিস্টেম ডিজাইন কোর্সের পর্ব ২২-এর মতো।

লেখার সময়ে (নভেম্বর ২০২৬) প্রচলিত কয়েকটি নাম, প্রতিটির নিজের নথি থেকে; পরিবর্তন হতে পারে, আর এটি কোনো র‍্যাংকিং নয়। Qdrant, Chroma, Milvus, Weaviate open-source vector database. pgvector PostgreSQL-এর extension; নথিতে HNSW আর IVFFlat index, আর লেখা আছে যে approximate index-এ ফিল্টার index scan-এর পরে বসে, ০.৮.০ থেকে iterative index scan চালু করা যায়। Elasticsearch আর OpenSearch search engine, যাদের নথিতে vector field-এ approximate kNN খোঁজা আছে। sqlite-vec SQLite-এর ছোট extension, নিজের README অনুযায়ী এখনও v1-এর আগের, তাই বড় বদল প্রত্যাশিত।

হাতে-কলমে দেখতে বাজারঘরের ২০টি হাতে লেখা help নথি (এই পর্বের জন্য বানানো; বাংলা, ইংরেজি আর বাংলিশ মেশানো) Qdrant-এর Python client-এর local mode-এ রাখা হলো। README অনুযায়ী local mode server ছাড়াই একই API চালায়, path দিলে disk-এ রাখে, আর এটি উন্নয়ন, prototype আর পরীক্ষার জন্য; বড় মাপে server mode-এ যেতে বলে। একই e5-small model, প্রশ্নে “query: ”, নথিতে “passage: ”।

# মূল অংশ, ছোট করে (সত্যিকারের vdb_demo.py থেকে)
db = QdrantClient(path="qdrant_db")                  # local mode, disk-এ
db.create_collection("help", vectors_config=models.VectorParams(
    size=384, distance=models.Distance.COSINE))
db.upsert("help", points=[models.PointStruct(id=d["id"], vector=v,
    payload={"topic": d["topic"], "lang": d["lang"], "text": d["text"]})
    for d, v in zip(docs, V)])
hits = db.query_points("help", query=q, limit=3)
f = models.Filter(must=[models.FieldCondition(key="topic",
    match=models.MatchValue(value="payment"))])
hits = db.query_points("help", query=q, query_filter=f, limit=3)
db.delete("help", points_selector=models.PointIdsList(points=[8]))
db.upsert("help", points=[models.PointStruct(id=21, vector=v_new, payload={...})])
~/bazarghar-dl$ uv run python vdb_demo.py create     # সত্যিকারের run
qdrant-client 1.20.0 | docs 40
points: 40
Q: ঢাকায় ডেলিভারি চার্জ কত?  (ফিল্টার নেই)
  bh04 0.920 active   ডেলিভারি চার্জ: ঢাকা সিটি
  bh28 0.918 active   বড় পণ্যের ডেলিভারি
  bh40 0.910 archived ডেলিভারি চার্জ: ঢাকা সিটি (পুরনো)
একই প্রশ্ন, status = active আর category = delivery
  bh04 0.920 active   ডেলিভারি চার্জ: ঢাকা সিটি
  bh28 0.918 active   বড় পণ্যের ডেলিভারি
  bh06 0.910 active   ডেলিভারি চার্জ: অন্যান্য জেলা
bh40 মোছার পর points: 39
~/bazarghar-dl$ uv run python vdb_demo.py reopen     # নতুন process, একই disk ফোল্ডার
disk থেকে আবার খোলা, points: 39
Q: ঢাকায় ডেলিভারি চার্জ কত?  (ফিল্টার নেই)
  bh04 0.920 active   ডেলিভারি চার্জ: ঢাকা সিটি
  bh28 0.918 active   বড় পণ্যের ডেলিভারি
  bh06 0.910 active   ডেলিভারি চার্জ: অন্যান্য জেলা

ফিল্টার ছাড়া তৃতীয় ফলটি bh40: ঢাকার পুরনো ডেলিভারি নীতি (৫০ টাকা), যা এখন archived. অর্থে এটি প্রশ্নের খুব কাছে, তাই embedding একে আলাদা করতে পারে না; RAG-এ গেলে model ভুল চার্জ বলত। status = active শর্ত খোঁজার ভেতরেই দেওয়ায় সেটি বাদ, তার জায়গায় bh06. তারপর bh40 মুছে নতুন process-এ disk থেকে খুললে সংখ্যা ৩৯, আর ফিল্টার ছাড়াও পুরনো নীতি আর আসে না। খেয়াল করুন, তিনটি score ০.৯১০ থেকে ০.৯২০-এর মধ্যে: e5-এর cosine সাধারণত উঁচুতে জড়ো থাকে, তাই score-এর একটি নির্দিষ্ট সীমা (“০.৯-এর ওপরে হলে প্রাসঙ্গিক”) নিরাপদ নয়। Local mode নথি অনুযায়ী পরীক্ষার জন্য; ৪০টি নথিতে index-এর ধরন এখানে প্রাসঙ্গিকও নয়।

Vector database-এ রাখা নথি আর মূল database-এর নথি দুটি আলাদা কপি। নীতি বদলালে দুই জায়গাতেই বদলাতে হবে; একটি ভুলে গেলে RAG পুরনো নীতি দেখাবে (পর্ব ৬৯-এ এর ফল দেখবেন)। আর নিরাপত্তার শর্ত (কে কোন নথি দেখতে পারেন) খোঁজার ফিল্টারেই দিন, পরে ছেঁকে নয়।

৮ · কোনটি কখন — একটি সিদ্ধান্তের পথ

কোনো একটি index সব কাজে সেরা নয়। নিচের প্রশ্নগুলো ক্রমে জিজ্ঞেস করুন। সংখ্যাগুলো শুরুর নিয়ম, আইন নয়; শেষ কথা নিজের ডেটায় মাপা recall আর সময়।

  1. N কত? কয়েক হাজার থেকে কয়েক লাখ, আর প্রশ্ন কম: Flat (NumPy বা IndexFlatIP). নির্ভুল, train নেই, যোগ-মোছা সহজ।
  2. সব vector float32-এ RAM-এ আঁটে, আর কম latency চাই? HNSW. মেমরি প্রতি vector-এ d × ৪ + M × ৮ byte, build ধীর, মোছা ঝামেলার।
  3. আঁটে না, বা খরচ কমাতে চান? IVFPQ (প্রয়োজনে re-ranking-সহ), বা float16/int8-এর মতো scalar quantization. nlist ≈ ৪√N থেকে ১৬√N.
  4. ডেটা ঘন ঘন বদলায়, ফিল্টার লাগে, একাধিক সার্ভার? শুধু library নয়, একটি vector database বা আপনার চেনা database-এর vector সুবিধা (§৭)।
মূল ধারণা

ANN-এর recall মাপা হয় একই embedding-এ exact search-এর বিপরীতে। এটি খোঁজার যন্ত্রের নির্ভুলতা, খোঁজার মানের নয়। Embedding model ভুল নথিকে কাছে বসালে Flat-ও ভুল দেবে। মানের মাপ (প্রাসঙ্গিক নথি পাওয়া গেল কি না) পর্ব ৬৭-এ শুরু, বিস্তার পর্ব ৭০ · RAG মূল্যায়নে।

আরও গভীরে: PQ এসেছে Hervé Jégou, Matthijs Douze আর Cordelia Schmid-এর ২০১১ সালের TPAMI প্রবন্ধ থেকে, আর cell ধরে তালিকা রাখার চিন্তা Sivic আর Zisserman-এর ২০০৩ সালের “Video Google” কাজে ছবির খোঁজায় জনপ্রিয় হয়। FAISS-এর README দুটি প্রবন্ধ উদ্ধৃত করতে বলে: Johnson, Douze আর Jégou-এর “Billion-scale similarity search with GPUs” আর ২০২৪ সালের “The Faiss library”. Disk-এ রাখা graph (যেমন DiskANN), learned quantization আর filter-সহ graph খোঁজা নিয়ে গবেষণা চলছে; প্রকাশিত তুলনা (যেমন ann-benchmarks) ডেটাভেদে বদলায়, তাই একটি তালিকার “সেরা” নিজের ডেটায় যাচাই না করে মানবেন না।

পরীক্ষা ও ইন্টারভিউতে: Flat-এর খরচ? প্রতি প্রশ্নে O(N·d), মেমরি N·d·4 byte. nprobe বাড়ালে? recall আর সময় বাড়ে, মেমরি একই। PQ-তে m = ৬৪, nbits = ৮ হলে code কত byte? ৬৪. efSearch আর efConstruction-এর পার্থক্য? প্রথমটি খোঁজার সময়ের তালিকা, rebuild ছাড়াই বদলায়; দ্বিতীয়টি বানানোর সময়ের। Post-filter-এর ঝুঁকি? k-এর কম ফল, এমনকি শূন্য।

৯ · AI-এর যুগে — vector search-এর কোড আর দাবি যাচাই

FAISS বা vector database-এর কোড AI assistant কয়েক সেকেন্ডে লিখে দেয়। কোডটি চলবে, ফলও দেবে; ভুল হলে সাধারণত নীরবে কম recall বা ভুল মেট্রিক।

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
Cosine চেয়েও IndexFlatL2 বা normalise ছাড়া IndexFlatIPলম্বা vector অকারণে এগিয়ে যায়norm ছাপিয়ে দেখা; ছোট উদাহরণে হাতে cosine
IVF-এ train বাদ, বা ১০০টি vector-এ ১০২৪টি cellত্রুটি, বা খালি আর বাজে cell৩০ × nlist-এর বেশি train vector
nprobe = ১ রেখে “recall কম” অভিযোগFAISS-এ nprobe-এর ডিফল্ট ১nprobe আর efSearch স্পষ্ট লিখে sweep
Recall-এর বদলে শুধু সময় রিপোর্টদ্রুত কিন্তু ভুল index চোখে পড়ে নাFlat-কে মাপকাঠি ধরে recall@k
পুরনো বা বানানো API (vector database client-এর নাম বদলায়)version ভেদে method বদলায়নিজের version-এর official নথি
Query আর passage-এ একই prefix, বা model বদলে পুরনো indexvector তুলনীয় থাকে নাmodel-এর নাম ও version index-এর সঙ্গে লেখা

কোড চাওয়ার সময় যাচাইও চেয়ে নিন। গ্রাহকের ব্যক্তিগত লেখা বা কোনো key hosted assistant-এ দেবেন না।

আমি বাংলা নথির জন্য vector search বানাচ্ছি: প্রায় ৫ লাখ অনুচ্ছেদ, ৩৮৪-মাত্রার normalised embedding, cosine. আমার faiss-cpu version লিখে দিচ্ছি। ১. প্রথমে মেমরির হিসাব দেখাও: Flat, HNSW (M=32) আর IVFPQ (m=48)-এ কত MB. ২. IndexFlatIP দিয়ে নির্ভুল উত্তর, তারপর IVFFlat আর HNSW বানানোর কোড দাও; nlist, nprobe, efSearch স্পষ্ট লিখবে। ৩. আলাদা ৫০০টি প্রশ্নে recall@10 আর প্রতি প্রশ্নের ms মাপার কোড দাও, এক thread-এ। ৪. যে parameter-এর ডিফল্ট বা নাম নিশ্চিত নও, তা বলো; বানিয়ে লিখবে না।

AI-এর লেখা vector index কোডের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।

প্র ০১ANN index-এর recall@10 = ০.৯৫ হলে কি বলা যায় search-এর মান ৯৫%?

না, আর এই গুলিয়ে ফেলাটা খুব সাধারণ। ANN-এর recall মাপা হয় একই embedding-এ exact search-এর ফলের বিপরীতে। প্রশ্ন একটাই: “Flat যে ১০টি দিত, তার কতটি এই index দিল?” এটি যন্ত্রের বিশ্বস্ততা। কিন্তু Flat-এর ১০টি নিজেই ঠিক কি না, তা embedding model-এর ওপর নির্ভর করে। Model যদি “ফেরত নীতি” প্রশ্নে “ডেলিভারি চার্জ”-এর নথিকে কাছে বসায়, তাহলে Flat-ও সেটাই দেবে, আর ANN সেই ভুল হুবহু নকল করলে recall ১.০০০.

তাই দুটি আলাদা মাপ লাগে। প্রথমটি index-এর: exact-এর বিপরীতে recall@k, সময় আর মেমরি; এর জন্য প্রাসঙ্গিকতার কোনো হাতে-লেখা উত্তর লাগে না, যেকোনো প্রশ্নই চলে। দ্বিতীয়টি খোঁজার মানের: হাতে লেখা প্রশ্ন আর তাদের সঠিক নথির id, তারপর recall@k, MRR বা nDCG. এটি পর্ব ৬৭-এ শুরু, পর্ব ৭০-এ বিস্তার। দুটি মাপ অবশ্য একে অপরকে ছুঁয়ে যায়। এই পর্বের run-এ HNSW-এর recall ০.৮৫০ থেকে ০.৯৮৫-এ তুলতে শিরোনাম-প্রশ্নের hit@10 ০.৭৭২ থেকে ০.৯১৫ হয়েছে: index-এর ভুল খোঁজার মানেও পড়েছে। অন্য ডেটায় হারানো প্রতিবেশীরা হয়তো সেরা ১০-এর শেষের দিকের, প্রায় সমান score-এর, তখন প্রভাব কম; কোনটি ঘটছে, মেপেই জানতে হয়। আবার উল্টোটাও হয়: কোনো বিশেষ ধরনের প্রশ্ন (যেমন বিরল জেলার নাম) বারবার সীমানার cell-এ পড়লে সেই ধরনেই recall অনেক কম, যা গড়ে লুকিয়ে থাকে।

ভালো অভ্যাস: index-এর সেটিং বাছতে exact-এর বিপরীতে recall, আর model ও পুরো pipeline বাছতে হাতে লেখা প্রাসঙ্গিকতার set. দুটিকে কখনো এক সংখ্যায় মেশাবেন না, আর রিপোর্টে লিখুন কোন recall কোন মাপকাঠির বিপরীতে।

মূল উপলব্ধি: ANN-এর recall বলে index exact search-কে কতটা নকল করে; খোঁজা আসলে ভালো কি না, তা বলে আলাদা, মানুষের লেখা প্রাসঙ্গিকতার মাপ।

প্র ০২এলোমেলো vector-এ ANN প্রায় কাজই করে না, অথচ আসল embedding-এ ভালো চলে। কেন?

AI-এর ভিত্তি কোর্সের পর্ব ৬৯-এর FAISS run-এ একই কোড দুই রকম ডেটায় চালানো হয়েছিল। গুচ্ছবদ্ধ ডেটায় HNSW efSearch = ৬৪-এ recall@10 ছিল ০.৯৮২, আর সম্পূর্ণ এলোমেলো ৩৮৪-মাত্রার vector-এ মাত্র ০.১৪৪. কারণ উঁচু মাত্রার জ্যামিতি। এলোমেলো উঁচু মাত্রায় প্রায় সব বিন্দু একে অপরের থেকে প্রায় সমান দূরে; “সবচেয়ে কাছের” আর “পঞ্চাশতম কাছের”-এর দূরত্বে পার্থক্য খুব কম। তখন graph-এ লোভী হাঁটার কোনো স্পষ্ট দিক থাকে না, আর IVF-এর cell-এর সীমানায় প্রায় সব বিন্দুই পড়ে।

আসল embedding এমন নয়। Model-কে শেখানো হয়েছে অর্থে কাছের লেখাকে কাছে বসাতে, তাই vector-গুলো একটি নিচু “অন্তর্নিহিত মাত্রা”-র কাঠামোয় জড়ো হয়: খেলার খবর এক জায়গায়, ইতিহাস আরেক জায়গায়, তার ভেতরে আবার ছোট দল। ৩৮৪টি সংখ্যা থাকলেও ডেটা আসলে অনেক কম স্বাধীন দিকে ছড়ানো। এই কাঠামোই ANN-কে শর্টকাট দেয়: k-means-এর cell অর্থবহ, আর graph-এর প্রতিবেশী সত্যিই কাছে।

দুটি বাস্তব শিক্ষা আছে। এক, কোনো ANN library-র benchmark অন্য ডেটায় হুবহু খাটে না; আপনার embedding-এর গঠন ভিন্ন হতে পারে। দুই, কিছু প্রশ্ন ডেটার কাঠামোর বাইরে পড়ে: খুব সাধারণ বা অর্থহীন প্রশ্ন (“হ্যালো”), ভিন্ন ভাষা, বা model যে ধরনের লেখা কম দেখেছে। এমন প্রশ্নে কাছের-দূরের পার্থক্য কম, আর ANN-এর recall নিচে নামে। তাই recall মাপার প্রশ্নগুলো বাস্তব ব্যবহারকারীর প্রশ্নের মতো হওয়া দরকার, শুধু corpus থেকে তোলা অনুচ্ছেদ নয়। এই পর্বের run-এ ঠিক এটাই দেখা গেল: একই IVF index-এ nprobe = ১৬-এ শিরোনাম-প্রশ্নের recall ০.৫৭৮, অনুচ্ছেদ-প্রশ্নের ০.৮৩৩.

মূল উপলব্ধি: ANN কাজ করে কারণ আসল embedding গুচ্ছবদ্ধ; তাই সেটিং বাছুন নিজের ডেটা আর বাস্তবের মতো প্রশ্নে মেপে।

প্র ০৩“শুধু রাজশাহীর নথি থেকে খোঁজো” — ANN index-এ ফিল্টার এত কঠিন কেন?

Index বানানো হয়েছে সব vector নিয়ে, কোনো শর্ত না জেনে। ফিল্টার এলে দুটি সহজ পথ, দুটিই ত্রুটিপূর্ণ। Post-filter: আগে ANN থেকে সেরা ১০টি, তারপর রাজশাহীর বাইরেরগুলো ফেলা। রাজশাহীর নথি যদি মোটের ২% হয়, তাহলে সেরা ১০-এর মধ্যে গড়ে একটিও না থাকতে পারে, আর ব্যবহারকারী পান শূন্য ফল। সমাধানের চেষ্টা হিসেবে k বাড়িয়ে ২০০ আনা হয়, কিন্তু কত বাড়াবেন তা শর্তের ওপর নির্ভর করে। Pre-filter: আগে শর্ত মানা সব id, তারপর শুধু তাদের মধ্যে খোঁজা। ফল নির্ভুল, কিন্তু HNSW-এর graph সব node নিয়ে বানানো; অন্য node বাদ দিলে graph টুকরো হয়ে যেতে পারে, লোভী হাঁটা আটকে যায়।

বাস্তব ব্যবস্থাগুলো তাই মাঝামাঝি কৌশল নেয়। শর্ত খুব সংকীর্ণ হলে (অল্প id) সরাসরি brute-force-ই দ্রুত, কারণ অল্প vector. শর্ত বড় হলে graph-এ হাঁটার সময় শর্ত না মানা node-এর মধ্য দিয়ে যাওয়া যায়, কিন্তু ফলে রাখা হয় না। DBMS কোর্সের পর্ব ৬০-এ pgvector-এর iterative index scan দেখেছেন, যেটি কম ফল পেলে আরও খোঁজে। কিছু vector database নথিতে জানায় যে payload-এর index থেকে শর্তের আকার আন্দাজ করে পরিকল্পনা বদলায়; এর খুঁটিনাটি প্রতিটির নিজের নথিতে দেখুন, কারণ version-ভেদে বদলায়।

নকশার সময়ও উপায় আছে। খুব ঘন ঘন ব্যবহৃত, বড় ভাগের শর্তে (যেমন ভাষা) আলাদা collection বা partition রাখা যায়। নিরাপত্তার শর্তে (কোন ব্যবহারকারী কোন নথি দেখতে পারেন) post-filter কখনোই যথেষ্ট নয়, কারণ ফাঁক থাকলে অন্যের নথি দেখা যায়; সেখানে শর্ত খোঁজার ভেতরেই লাগবে, আর পরীক্ষা করে দেখতে হবে।

মূল উপলব্ধি: ফিল্টার ANN-এর অনুমান ভাঙে; শর্ত কত সংকীর্ণ তা দেখে brute-force, pre-filter বা post-filter বাছুন, আর ফলের সংখ্যা ও recall দুটিই মাপুন।

প্র ০৪Embedding model বদলাতে চান। Vector index-এ কী কী ঘটে, আর নিরাপদে কীভাবে বদলাবেন?

নতুন model-এর vector পুরনোটির সঙ্গে তুলনীয় নয়, মাত্রা একই হলেও। দুটি model একই জায়গাকে ভিন্ন অক্ষে আঁকে; পুরনো index-এ নতুন model-এর প্রশ্ন পাঠালে ফল অর্থহীন, আর কোনো error আসে না। তাই model বদল মানে সব নথি আবার embed করা আর index আবার বানানো। এই পর্বের ১ লাখ অনুচ্ছেদ CPU-তে embed করতেই প্রায় ৪০ মিনিট লেগেছে; কোটি নথিতে এটি কয়েক দিনের কাজ আর বড় খরচ, hosted embedding API হলে আরও।

নিরাপদ পথ database-এর schema বদলের মতো। নতুন index পাশে বানান, পুরনোটি চালু রেখে। প্রতিটি index-এর সঙ্গে model-এর নাম, revision (Hugging Face-এ commit hash, পর্ব ৫৩), prefix (যেমন e5-এর “query: ” আর “passage: ”) আর normalisation লিখে রাখুন। দুই index-এ একই হাতে লেখা প্রশ্নের set চালিয়ে মান তুলনা করুন। তারপর ধীরে ধীরে ট্রাফিক সরান, আর পুরনোটি কিছুদিন ফেরার পথ হিসেবে রাখুন।

আরও কয়েকটি ফাঁদ। IVF আর PQ-এর centroid আর codebook পুরনো ডেটা দিয়ে শেখা; নতুন model-এ এগুলোও আবার train করতে হবে। একই model-এও ডেটার ধরন বদলালে (নতুন বিষয়ের নথি) cell অসমান হয়ে যায়, তাই মাঝে মাঝে আবার train করা ভালো। নথি মুছলে vector-ও মুছতে হবে, নইলে RAG মুছে ফেলা নীতি দেখাবে। আর মূল নথি যে database-এ থাকে, সেটি আর vector store-এর মধ্যে মিল রাখা (একটি হালনাগাদ হলো, অন্যটি হলো না) সিস্টেম ডিজাইনের পুরনো সমস্যা, replication আর change data capture দিয়ে সামলানো হয়।

মূল উপলব্ধি: Model, prefix আর normalisation index-এর অংশ; বদলালে সব আবার embed করুন, পাশাপাশি বানিয়ে মেপে তারপর সরান।

প্র ০৫HNSW মেমরিতে এত দামি, তবু এত জনপ্রিয় কেন? IVFPQ কোথায় এগিয়ে?

HNSW-এর জনপ্রিয়তার কারণ ব্যবহারিক। Train লাগে না: vector একে একে যোগ করা যায়, যা নতুন নথি আসতে থাকা ব্যবস্থায় সুবিধাজনক। একটি knob (efSearch) দিয়ে খোঁজার সময়েই recall আর গতির মধ্যে সরে যাওয়া যায়। আর প্রকাশিত তুলনাগুলোয় (যেমন ann-benchmarks) উঁচু recall-এ HNSW প্রায়ই দ্রুততম দলে থাকে, যদিও ফল ডেটাভেদে বদলায়। Malkov আর Yashunin-এর প্রবন্ধ ২০১৬ সালে আসার পর বহু library আর database এটি গ্রহণ করে; লেখার সময়ে (নভেম্বর ২০২৬) pgvector-এর নথিতে HNSW আর IVFFlat দুটি index-ই আছে, পরিবর্তন হতে পারে।

দাম হলো মেমরি আর build. প্রতিটি vector পুরো float32-এ থাকে, সঙ্গে link; এই পর্বের run-এ HNSW-এর index (১৮০.৮ MB) Flat-এর (১৫৩.৬ MB) চেয়েও বড় ছিল, আর build-এ IVFFlat-এর প্রায় দ্বিগুণ সময় লেগেছে (৭.৪ বনাম ৪.০ সেকেন্ড)। কোটি বা শত কোটি vector-এ এটি RAM-এর বিলে বড় অঙ্ক। এখানেই IVFPQ এগিয়ে: প্রতি vector ৫০-৬০ byte, তাই একই সার্ভারে বহুগুণ বেশি vector. FAISS-এর দল শত কোটি ছবির vector-এ GPU দিয়ে এই ধরনের index নিয়েই কাজ করেছে বলে তাদের প্রবন্ধে জানায়। দাম নির্ভুলতা, যা re-ranking দিয়ে অনেকটা ফেরানো যায় (আসল vector তখন disk-এ রাখা যায়)।

মাঝামাঝি পথও আছে: HNSW-এর node-এ float32-এর বদলে চাপা vector রাখা, বা IVF-এর centroid HNSW দিয়ে খোঁজা। আর বহু ক্ষেত্রে আসল প্রশ্ন index নয়, স্কেল: কয়েক লাখ vector-এ Flat বা HNSW-এর পার্থক্য ব্যবহারকারী টেরই পান না। প্রথমে মেমরির হিসাব আর latency-র লক্ষ্য লিখুন, তারপর বাছুন।

মূল উপলব্ধি: RAM-এ আঁটলে আর সহজ যোগ চাইলে HNSW; মেমরি আসল সীমা হলে IVFPQ আর re-ranking. সিদ্ধান্তের আগে byte-এর হিসাব।

অনুশীলন

খাতা-কলমে করুন, তারপর উত্তর মেলান। বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের হিসাব কাজে লাগে।

  1. মেমরি আর খরচের হিসাব: ৫০ লাখ vector, d = ৭৬৮, float32. (ক) Flat-এর মেমরি কত? (খ) HNSW, M = ৩২-এ FAISS-এর সূত্রে কত? (গ) IVFPQ, m = ৯৬, nbits = ৮, id ৮ byte ধরে কত? (ঘ) nlist = ১৬,৩৮৪ আর nprobe = ৩২-এ প্রতি প্রশ্নে মোটামুটি কতটি দূরত্ব?

    (ক) ৫ × ১০৬ × ৭৬৮ × ৪ = ১৫,৩৬০,০০০,০০০ byte = ১৫.৩৬ GB.

    (খ) প্রতি vector ৭৬৮ × ৪ + ৩২ × ২ × ৪ = ৩০৭২ + ২৫৬ = ৩৩২৮ byte; মোট ≈ ১৬.৬৪ GB.

    (গ) প্রতি vector ৯৬ + ৮ = ১০৪ byte; মোট ≈ ৫২০ MB, সঙ্গে codebook ৯৬ × ২৫৬ × ৮ × ৪ = ৭,৮৬,৪৩২ byte (৭৬৮ ÷ ৯৬ = ৮ মাত্রা প্রতি টুকরো)।

    (ঘ) ১৬,৩৮৪ + ৩২ × (৫০,০০,০০০ ÷ ১৬,৩৮৪) ≈ ১৬,৩৮৪ + ৯,৭৬৬ = প্রায় ২৬,১৫০, মোটের প্রায় ০.৫২%. খেয়াল করুন, এখানে centroid মাপাই বড় অংশ; তাই বড় nlist-এ centroid খোঁজার জন্যও HNSW ব্যবহার হয়। ৪√N ≈ ৮,৯৪৪, ১৬√N ≈ ৩৫,৭৭৮; ১৬,৩৮৪ এই সীমার ভেতরে।

  2. আউটপুট অনুমান: নিচের কোড কী ছাপবে?
    truth = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    found = [[3, 1, 0], [6, 5, 4], [0, 0, 7]]
    r = sum(len(set(f) & set(t)) / 3 for f, t in zip(found, truth)) / 3
    print(round(r, 3))
    0.667

    প্রথম প্রশ্নে মিল {1, 3}: ২/৩. দ্বিতীয়তে ক্রম উল্টো, কিন্তু set-এ ক্রম গোনা হয় না: ৩/৩. তৃতীয়তে set {0, 7}-এর মধ্যে শুধু 7 মেলে: ১/৩ (পুনরাবৃত্ত 0 একবারই গোনা)। গড় (২/৩ + ১ + ১/৩) ÷ ৩ = ২/৩ ≈ 0.667. মনে রাখুন, recall@k ক্রম দেখে না; ক্রমের জন্য MRR বা nDCG.

  3. নকশা সমালোচনা: একটি দল বাজারঘরের ৩,০০০টি help নথির জন্য একটি managed vector database-এ HNSW (M = ৬৪, efSearch = ১০) চালু করেছে। জেলা ধরে খুঁজতে তারা সেরা ১০টি এনে পরে জেলা মিলিয়ে ফেলে দেয়। ভালো ফলের প্রমাণ হিসেবে তারা দেখায় “গড় latency ২ ms”. কী কী সমস্যা দেখেন?

    (১) ৩,০০০ × d মাপের ডেটায় ANN-এর দরকারই নেই; Flat নির্ভুল আর যথেষ্ট দ্রুত, train বা টিউনিং নেই। (২) efSearch = ১০ মানে k-এর সমান প্রার্থী, recall কম হতে পারে; আর recall মাপাই হয়নি, শুধু সময়। (৩) জেলায় post-filter: ছোট জেলার নথি সেরা ১০-এ না থাকলে ফল শূন্য বা অল্প; ছোট ডেটায় আগে জেলা দিয়ে ছেঁকে brute-force-ই ঠিক। (৪) গড় latency-র বদলে p95/p99 আর recall@k একসঙ্গে দেখানো উচিত। (৫) ছোট ডেটার জন্য বাইরের সেবা মানে বাড়তি খরচ আর ডেটা বাইরে যাওয়া; প্রয়োজন সত্যিই আছে কি না আগে লেখা দরকার।

  4. সেটিং বাছাই: একটি index-এর মাপা ফল (recall@10, ms/প্রশ্ন): IVF nprobe ৪ → ০.৮৮, ০.২০; nprobe ১৬ → ০.৯৬, ০.৭; nprobe ৬৪ → ০.৯৯, ২.৬; HNSW efSearch ৩২ → ০.৯৩, ০.৩; efSearch ১২৮ → ০.৯৮, ০.৯. লক্ষ্য: recall ≥ ০.৯৫, সবচেয়ে কম সময়। কোনটি বাছবেন, আর বাছার আগে আর কী জানতে চাইবেন?

    শর্ত মানে তিনটি: nprobe ১৬ (০.৭ ms), nprobe ৬৪ (২.৬ ms), efSearch ১২৮ (০.৯ ms). সবচেয়ে দ্রুত IVF nprobe = ১৬. তবে ০.৯৬ লক্ষ্যের খুব কাছে; কয়েকশো প্রশ্নের নমুনায় এটি ±০.০১-এর মতো নড়তে পারে, তাই bootstrap CI দেখা দরকার (মেশিন লার্নিং কোর্সের পর্ব ২১)। আরও জানতে চাইবেন: দুই index-এর মেমরি আর build সময়, নতুন নথি কত ঘন ঘন যোগ হয় (IVF-এ cell পুরনো হয়, HNSW-এ যোগ সহজ), আর p99 latency, কারণ বড় cell-এ পড়া প্রশ্ন ধীর।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি IVFPQ index বানিয়ে চালু করার ধাপগুলো সাজান (▲▼ চেপে সরান)

  1. নথি embed করে normalise, model আর prefix লিখে রাখা
  2. নমুনা vector-এ k-means আর PQ codebook train
  3. সব vector যোগ: cell-এ বসানো আর code-এ চাপা
  4. আলাদা প্রশ্নে Flat-এর বিপরীতে nprobe sweep করে recall আর সময় মাপা
  5. লক্ষ্য মানা সবচেয়ে সস্তা সেটিং বেছে চালু করা

২ লাখ vector, প্রতিটি ৭৬৮-মাত্রার float32. কাঁচা vector-এর মেমরি কত?

  • ১৫৩.৬ MB
  • ৬১৪.৪ MB
  • ১.২২৯ GB
  • ৬.১৪ GB

Product quantization-এ d = ৫১২, m = ৬৪, nbits = ৮. প্রতি vector-এর code কত byte?

  • ৮
  • ৩২
  • ৬৪
  • ২০৪৮

একটি IVF index-এ nprobe ৮ থেকে ৩২ করা হলো, আর কিছু বদলায়নি। সাধারণত কী ঘটে?

  • Recall আর প্রতি প্রশ্নের সময় বাড়ে, মেমরি একই থাকে
  • মেমরি চারগুণ হয়
  • Recall কমে, সময়ও কমে
  • Index আবার train করতে হয়

HNSW index বানানো হয়ে গেছে। নিচের কোনটি index আবার না বানিয়েই বদলানো যায়?

  • M
  • efConstruction
  • vector-এর মাত্রা
  • efSearch

একটি ANN index-এর recall@10 মাপতে “সঠিক উত্তর” হিসেবে কী নেওয়া উচিত?

  • বড় অন্য একটি embedding model-এর ফল
  • একই embedding-এ exact (Flat) search-এর সেরা ১০টি
  • মানুষের বাছা প্রাসঙ্গিক নথি
  • একই index-এর আগের দিনের ফল

ANN থেকে সেরা ১০টি এনে তারপর “জেলা = সিলেট” শর্ত না মানাগুলো বাদ দেওয়া হয়। সিলেটের নথি মোটের ১%. সবচেয়ে বড় ঝুঁকি কী?

  • Index-এর মেমরি দ্বিগুণ হবে
  • বাকি ফলগুলোর score বদলে যাবে
  • অনেক প্রশ্নে ফল ১০-এর অনেক কম, এমনকি শূন্য
  • সিলেটের নথি ভুল করে বাদ পড়বে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. আগে হিসাব: Flat-এর খরচ প্রতি প্রশ্নে N × d, মেমরি N × d × ৪ byte. কয়েক লাখ পর্যন্ত Flat প্রায়ই যথেষ্ট।
  2. IVF: k-means-এর cell, প্রশ্নে কাছের nprobe-টি; nprobe বাড়লে recall আর সময় বাড়ে, মেমরি একই। সীমানার কাছের প্রতিবেশী হারায়।
  3. PQ: m টুকরো × ১ byte; ৩৮৪-মাত্রায় m = ৪৮ মানে ১৫৩৬ থেকে ৪৮ byte. নির্ভুলতা কমে, re-ranking অনেকটা ফেরায়।
  4. HNSW: স্তরের graph-এ লোভী হাঁটা; M আর efConstruction গঠনে, efSearch খোঁজায়। দ্রুত, কিন্তু মেমরি আর build দামি।
  5. Database যোগ করে ফিল্টার, যোগ-মোছা, disk, backup আর replication; index-এর সেটিং বাছুন Flat-এর বিপরীতে recall মেপে।

পরের পর্বে: পর্ব ৬৯ · RAG — chunking, retrieval, reranking আর উত্তর তৈরি. এই পর্বের index-এ খুঁজে পাওয়া নথি model-এর prompt-এ বসিয়ে বাজারঘরের প্রশ্নের উত্তর, সূত্রসহ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান