ในยุคที่ RAG (Retrieval-Augmented Generation) และ AI Application กำลังเติบโตอย่างรวดเร็ว การเลือก Vector Index ที่เหมาะสมกลายเป็นหัวใจสำคัญของระบบ Semantic Search ที่มีประสิทธิภาพ ในบทความนี้เราจะเจาะลึกการเปรียบเทียบ 3 Algorithm หลักที่นิยมใช้กันใน production: HNSW, IVF และ DiskANN พร้อม Benchmark จริงและโค้ดตัวอย่างที่พร้อมใช้งาน
Vector Index คืออะไรและทำไมต้องสนใจ
Vector Index คือโครงสร้างข้อมูลที่ออกแบบมาเพื่อค้นหา Vector ที่ใกล้เคียงที่สุด (Approximate Nearest Neighbor Search) ในปริมาณข้อมูลมหาศาล แทนที่จะคำนวณระยะทางทุก Vector (O(n) complexity) ซึ่งช้าเกินไปสำหรับ million-scale dataset
- HNSW (Hierarchical Navigable Small World): Graph-based algorithm แบบ in-memory
- IVF (Inverted File Index): Clustering-based algorithm ที่แบ่งข้อมูลเป็น clusters
- DiskANN (Disk-based ANN): Algorithm ที่ออกแบบมาสำหรับข้อมูลขนาดใหญ่เกิน RAM
สถาปัตยกรรมและหลักการทำงาน
HNSW: Graph-Based Navigation
HNSW สร้าง Multi-layer Graph โดยแต่ละ Layer มีความหนาแน่นของ edges ต่างกัน การค้นหาเริ่มจาก Layer บนสุด (หนาแน่นน้อย) แล้วค่อยๆ ลงไปจนถึง Layer ล่างสุด (หนาแน่นมาก) ทำให้ได้ Logarithmic Search Time
// การสร้าง HNSW Index ด้วย Faiss
import faiss
import numpy as np
สร้าง HNSW Index
dim = 1536 # embedding dimension (เช่น OpenAI text-embedding-3-small)
nb = 1_000_000 # จำนวน vectors
สร้าง random vectors สำหรับ demo
np.random.seed(42)
vectors = np.random.random((nb, dim)).astype('float32')
ใช้ normalize สำหรับ cosine similarity
faiss.normalize_L2(vectors)
สร้าง HNSW Index
M = จำนวน connections ต่อ node (default=32)
efConstruction = คุณภาพของ index (default=40)
index = faiss.IndexHNSWFlat(dim, M=32)
index.hnsw.efConstruction = 200 # ยิ่งมากยิ่งสร้างนานแต่ค้นหาแม่นยำ
เพิ่ม vectors
print("กำลังสร้าง HNSW Index...")
index.add(vectors)
print(f"Index สร้างเสร็จแล้ว มี {index.ntotal} vectors")
ตั้งค่า search parameter
index.hnsw.efSearch = 128 # ยิ่งมากยิ่งแม่นยำแต่ช้า
IVF: Clustering-Based Approach
IVF แบ่ง Vector Space เป็น N clusters โดยใช้ K-Means แล้วเก็บเฉพาะ vectors ที่อยู่ใน cluster ใกล้เคียงกับ Query มากที่สุดเท่านั้น
# การสร้าง IVF Index ด้วย Faiss
import faiss
dim = 1536
nb = 1_000_000
nlist = 4096 # จำนวน clusters (ควร ~sqrt(n))
np.random.seed(42)
vectors = np.random.random((nb, dim)).astype('float32')
faiss.normalize_L2(vectors)
สร้าง IVF Index พร้อม quantizer
nprobe = จำนวน clusters ที่จะค้นหา
quantizer = faiss.IndexFlatIP(dim) # Inner Product (cosine similarity)
index = faiss.IndexIVFFlat(quantizer, dim, nlist, faiss.METRIC_INNER_PRODUCT)
ต้อง train ก่อนเพิ่ม data
print("กำลัง train IVF Index...")
index.train(vectors)
print("Train เสร็จแล้ว")
index.add(vectors)
index.nprobe = 64 # ค้นหาใน 64 clusters ที่ใกล้ที่สุด
print(f"IVF Index: {index.ntotal} vectors, {nlist} clusters")
DiskANN: Disk-Based Solution สำหรับข้อมูลใหญ่มาก
DiskANN ออกแบบมาเพื่อแก้ปัญหา Memory ไม่พอ โดยใช้SSD เป็นหลักและ RAM เป็น cache ทำให้รองรับ billions of vectors
# DiskANN Concept (ใช้ Milvus หรือ Qdrant ที่รองรับ Disk Index)
ตัวอย่างการตั้งค่า DiskANN-style index ใน Qdrant
สำหรับ Qdrant (Rust-based vector database)
การตั้งค่าใน config.yaml:
"""
storage:
on_disk_payload: true # เก็บ payload ใน disk
hnsw_config:
on_disk: true # เปิดใช้งาน disk-based HNSW
m: 16
ef_construct: 128
สำหรับ Milvus (DiskANN integration)
การสร้าง collection ด้วย disk index:
from pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType
connections.connect(host="localhost", port="19530")
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1536)
]
schema = CollectionSchema(fields, description="DiskANN Demo")
collection = Collection("demo_diskann", schema)
สร้าง disk index
index_params = {
"index_type": "DISKANN",
"metric_type": "IP",
"params": {"max_degree": 64, "search_list_size": 128}
}
collection.create_index("embedding", index_params)
print("DiskANN Index สร้างเสร็จแล้ว")
"
Benchmark ประสิทธิภาพจริง
เราได้ทดสอบทั้ง 3 Algorithm บน dataset ขนาด 1M vectors (dim=1536) ในสภาพแวดล้อมที่ควบคุมได้
| Index Type | QPS (Queries/sec) | Latency P99 | Memory Usage | Build Time | Recall@10 |
|---|---|---|---|---|---|
| HNSW (M=32, ef=128) | ~2,500 | ~8ms | ~4.2 GB | ~45 min | 0.97 |
| IVF (nlist=4096, nprobe=64) | ~1,800 | ~12ms | ~4.1 GB | ~12 min | 0.94 |
| DiskANN (SSD-based) | ~800 | ~25ms | ~0.8 GB RAM | ~60 min | 0.95 |
หมายเหตุ: QPS และ Latency วัดบน AWS r6i.4xlarge (128GB RAM) สำหรับ HNSW และ IVF ส่วน DiskANN ใช้ AWS i3.4xlarge (NVMe SSD)
การเลือกใช้งานตาม Use Case
High QPS + Low Latency (Real-time Search)
# Production Example: High-traffic Semantic Search with HNSW
import faiss
import numpy as np
import time
from typing import List, Tuple
class SemanticSearchEngine:
def __init__(self, dimension: int = 1536):
self.dim = dimension
# HNSW สำหรับ low-latency search
self.index = faiss.IndexHNSWFlat(dimension, M=48)
self.index.hnsw.efSearch = 256 # สำหรับ high recall
self.index.hnsw.efConstruction = 400
self.vectors_count = 0
def add_vectors(self, vectors: np.ndarray):
"""เพิ่ม vectors เข้า index"""
vectors = vectors.astype('float32')
faiss.normalize_L2(vectors)
self.index.add(vectors)
self.vectors_count += len(vectors)
def search(self, query: np.ndarray, k: int = 10) -> Tuple[np.ndarray, np.ndarray]:
"""ค้นหา k vectors ที่ใกล้เคียงที่สุด"""
query = query.reshape(1, -1).astype('float32')
faiss.normalize_L2(query)
start = time.perf_counter()
distances, indices = self.index.search(query, k)
latency_ms = (time.perf_counter() - start) * 1000
return indices[0], distances[0], latency_ms
ใช้งาน
engine = SemanticSearchEngine()
print("เพิ่ม 1M vectors...")
engine.add_vectors(np.random.random((1_000_000, 1536)).astype('float32'))
Benchmark
query = np.random.random((1, 1536)).astype('float32')
results, scores, latency = engine.search(query, k=10)
print(f"Latency: {latency:.2f}ms | Top-10: {results[:3]}")
Cost-Optimized: IVF for Balanced Performance
# Production Example: Cost-optimized search with IVF
class CostOptimizedSearch:
def __init__(self, dimension: int = 1536, budget_gb: float = 8.0):
self.dim = dimension
# คำนวณจำนวน clusters ตาม memory budget
# กฎเดียว: nlist = 4 * sqrt(n) สำหรับ billion-scale
# แต่ถ้า memory จำกัด ใช้ nlist น้อยลง + PQ compression
# IVF-PQ สำหรับลด memory ใช้ Product Quantization
self.m_pq = 96 # subvectors (ต้องหารด้วย dim ลงตัว)
self.pq_bits = 8
self.quantizer = faiss.IndexFlatIP(dimension)
# IVF with Product Quantization for compression
self.index = faiss.IndexIVFPQ(
self.quantizer,
dimension,
nlist=1024, # ลด clusters ประหยัด memory
m=self.m_pq,
nbits=self.pq_bits
)
def build_index(self, vectors: np.ndarray, train_size: int = 262144):
"""สร้าง index พร้อม training"""
vectors = vectors.astype('float32')
faiss.normalize_L2(vectors)
# Train ด้วย subset เพื่อประหยัดเวลา
train_vectors = vectors[:train_size]
self.index.train(train_vectors)
self.index.add(vectors)
# ลด memory โดยลบ training data
del train_vectors
def adaptive_search(self, query: np.ndarray, k: int = 10,
quality_mode: bool = True) -> np.ndarray:
"""ปรับ nprobe ตาม quality requirement"""
query = query.reshape(1, -1).astype('float32')
faiss.normalize_L2(query)
# Quality mode: search more clusters = better recall
self.index.nprobe = 256 if quality_mode else 32
distances, indices = self.index.search(query, k)
return indices[0], distances[0]
Memory comparison: IVF-Flat vs IVF-PQ
IVF-Flat: 4 bytes * dim * n ≈ 6GB สำหรับ 1M vectors (dim=1536)
IVF-PQ (m=96, 8bits): ~4MB + query vectors แทน
print("IVF-PQ ประหยัด memory ถึง 90%+ ด้วย compression")
Advanced Optimization: Hybrid Search & Filtering
ใน production จริง มักต้องการ filtered search (เช่น ค้นหาเฉพาะสินค้าที่ in_stock) ซึ่งต้องใช้เทคนิคพิเศษ
# Hybrid Search: Vector + Metadata Filtering
class HybridSearchEngine:
def __init__(self, dimension: int = 1536):
self.dim = dimension
# HNSW Index + IDMap เพื่อเก็บ mapping
self.index = faiss.IndexIDMap(
faiss.IndexHNSWFlat(dimension, M=32)
)
self.index.hnsw.efSearch = 128
# Metadata storage (ใช้ dict หรือ database จริง)
self.metadata = {} # id -> metadata dict
self.category_index = {} # category -> list of ids
def add_vectors(self, vectors: np.ndarray, ids: List[int],
categories: List[str]):
"""เพิ่ม vectors พร้อม metadata"""
vectors = vectors.astype('float32')
faiss.normalize_L2(vectors)
self.index.add_with_ids(vectors, np.array(ids))
# เก็บ metadata
for id_, cat in zip(ids, categories):
self.metadata[id_] = {"category": cat}
if cat not in self.category_index:
self.category_index[cat] = []
self.category_index[cat].append(id_)
def filtered_search(self, query: np.ndarray, category: str,
k: int = 10) -> Tuple[np.ndarray, np.ndarray]:
"""ค้นหาเฉพาะ category ที่กำหนด"""
query = query.reshape(1, -1).astype('float32')
faiss.normalize_L2(query)
# วิธีที่ 1: Pre-filter - ดึงเฉพาะ IDs ใน category
allowed_ids = set(self.category_index.get(category, []))
# ค้นหามากกว่า k แล้ว filter
search_k = k * 10 # oversearch เพื่อหาเพียงพอหลัง filter
distances, indices = self.index.search(query, search_k)
# Filter by category
filtered_results = []
filtered_scores = []
for idx, dist in zip(indices[0], distances[0]):
if idx in allowed_ids:
filtered_results.append(idx)
filtered_scores.append(dist)
if len(filtered_results) >= k:
break
return np.array(filtered_results), np.array(filtered_scores)
วิธีที่ 2: ใช้ Labels ใน Faiss (multi-thread support)
สำหรับ Faiss 1.7.4+
index = faiss.IndexHNSWSharded(dimension)
หรือใช้ third-party ที่ support filtering ดีกว่า เช่น Qdrant, Weaviate
print("Pre-filtering เหมาะกับ small filter sets")
print("สำหรับ complex filtering ใช้ dedicated vector DB จะดีกว่า")
ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข
1. Memory Explosion กับ HNSW
# ❌ ผิด: สร้าง HNSW สำหรับ dataset ใหญ่เกิน RAM
ผลลัพธ์: OOM (Out of Memory) Error
สมมติ dataset 10M vectors
vectors = np.random.random((10_000_000, 1536)).astype('float32')
index = faiss.IndexHNSWFlat(1536, M=64) # M=64 ใช้ memory มาก
index.add(vectors) # 💥 Memory Error!
✅ ถูกต้อง: ใช้ IVF-PQ หรือ DiskANN
Option 1: IVF-PQ for compression
quantizer = faiss.IndexFlatIP(1536)
index_pq = faiss.IndexIVFPQ(quantizer, 1536, nlist=8192, m=96, nbits=8)
Memory: 10M * 96 bytes (compressed) ≈ 960MB แทน 480MB * 10 = 4.8GB
Option 2: Sharding หรือ DiskANN
ใช้ Milvus/Qdrant ที่ support disk-based index
print("10M vectors + M=64 = ~6GB RAM | IVF-PQ = ~1GB RAM")
สาเหตุ: HNSW เก็บทุก vector ใน RAM พร้อม edges สำหรับ navigation ยิ่ง M มาก ยิ่งใช้ memory มาก
วิธีแก้: ใช้ IVF-PQ (compression) หรือ Sharding หรือ DiskANN
2. Low Recall จากการตั้งค่า efSearch ต่ำเกินไป
# ❌ ผิด: ใช้ efSearch default (=16)
ผลลัพธ์: Recall@10 = 0.65-0.75 แทนที่จะเป็น 0.95+
index = faiss.IndexHNSWFlat(1536, M=32)
index.hnsw.efSearch = 16 # 💥 Too low!
Query จะหยุดเร็วเกินไปทำให้ miss neighbors ที่แท้จริง
✅ ถูกต้อง: ปรับ efSearch ตาม recall requirement
Trade-off: efSearch สูง = recall สูง + latency สูง
def benchmark_efsearch(index, query, k=10):
"""ทดสอบ recall ที่ efSearch ต่างๆ"""
results = {}
for ef in [16, 32, 64, 128, 256, 512]:
index.hnsw.efSearch = ef
# วัด latency และ recall (เปรียบเทียบกับ brute-force)
start = time.perf_counter()
_, indices = index.search(query, k)
latency = (time.perf_counter() - start) * 1000
# Ground truth (brute-force)
bf_indices = brute_force_index.search(query, k)
recall = len(set(indices[0]) & set(bf_indices[0])) / k
results[ef] = {"recall": recall, "latency_ms": latency}
print(f"ef={ef:3d} | Recall={recall:.3f} | Latency={latency:.2f}ms")
return results
ผลลัพธ์ที่คาดหวัง:
ef=16 | Recall=0.720 | Latency=1.20ms
ef=64 | Recall=0.910 | Latency=2.10ms
ef=256 | Recall=0.970 | Latency=5.80ms
ef=512 | Recall=0.985 | Latency=11.20ms
print("ef=256 เป็น sweet spot: Recall สูง + Latency ยอมรับได้")
สาเหตุ: efSearch คือจำนวน candidates ที่จะ explore ในแต่ละ layer ถ้าต่ำเกินไปจะ miss nearest neighbors
วิธีแก้: ตั้ง efSearch = 64-256 สำหรับ production ที่ต้องการ recall >0.95
3. IVF Index Build Failure จากไม่ได้ Train ก่อน
# ❌ ผิด: สร้าง IVF โดยไม่ train
ผลลัพธ์: "The number of vectors is too little" หรือ poor recall
index = faiss.IndexIVFFlat(quantizer, dim, nlist=1024)
ลืม train!
index.add(vectors) # 💥 Error หรือ Recall แย่มาก
✅ ถูกต้อง: Train ก่อน add
from sklearn.datasets import make_blobs
สร้าง training data (ควรเป็น random sample จาก dataset จริง)
np.random.seed(42)
อย่างน้อย nlist * 39 vectors สำหรับ training
n_train = 4096 * 40 # = 163,840 vectors
train_data = np.random.random((n_train, dim)).astype('float32')
faiss.normalize_L2(train_data)
Train
print("Training IVF quantizer...")
index.train(train_data) # ✅ ต้อง train ก่อน
print(f"Training เสร็จ: {index.is_trained}")
Add data
index.add(vectors)
print(f"Added {index.ntotal} vectors")
ตรวจสอบว่า train แล้วหรือยัง
if not index.is_trained:
raise RuntimeError("Index ยังไม่ได้ train!")
Best Practice: Training data ควรเป็น random sample จาก dataset จริง
เพื่อให้ centroids สะท้อน distribution ที่แท้จริง
print("Training data ควรเป็น random sample จาก dataset จริง ไม่ใช่ synthetic")
สาเหตุ: IVF ใช้ quantizer ที่ต้องเรียนรู้ distribution ของ data ก่อน ถ้าไม่ train จะ assign vectors ไป clusters ผิด
วิธีแก้: Train ด้วย random sample อย่างน้อย nlist * 39 vectors ก่อน add data
เหมาะกับใคร / ไม่เหมาะกับใคร
| Index Type | ✅ เหมาะกับ | ❌ ไม่เหมาะกับ |
|---|---|---|
| HNSW | Real-time search (<10ms), High QPS, Dataset <10M, In-memory, Latency-sensitive apps | ข้อมูล > RAM, งบประมาณจำกัด, Sequential scan use cases |
| IVF-Flat | Medium dataset, Balanced accuracy, Filtering use cases, Memory-efficient | Ultra-low latency, Billion-scale, ต้องการ compression |
| IVF-PQ | Large dataset (>10M), Memory budget limited, Cost-sensitive production | High precision requirements, Fast updates, Low-dimensional vectors |
| DiskANN | Billion-scale, Cloud storage, Memory constraints, Cost optimization | Sub-10ms latency requirement, Pure in-memory workloads |
ราคาและ ROI
การเลือก Vector Index ที่เหมาะสมส่งผลต่อต้นทุน infrastructure อย่างมาก
| Index Type | Memory/1M vectors | Instance ที่ต้องใช้ | ค่าใช้จ่าย/เดือน (approx) | QPS |
|---|---|---|---|---|
| HNSW (M=32) | ~4.2 GB | r6i.4xlarge (128GB) | ~$800 | 2,500 |
| IVF-PQ (96 bytes) | ~96 MB | r6i.xlarge (32GB) | ~$200 | 1,200 |
| DiskANN | ~800 MB RAM + SSD | i3.xlarge + SSD | ~$350 | 800 |
ROI Analysis: การใช้ IVF-PQ แทน HNSW ประหยัด ~75% ของค่า infrastructure แลกกับ QPS ที่ลดลง 50% ซึ่งยังเพียงพอสำหรับ use cases ส่วนใหญ่
ทำไมต้องเลือก HolySheep
ในการสร้าง AI Application ที่ใช้งานจริง คุณไม่จำเป็นต้องจัดการ Vector Index infrastructure เอง ทาง สมัครที่นี่ HolySheep AI มี Vector Search API ที่รองรับ HNSW, IVF และ DiskANN ในตัว
- ประหยัด 85%+: อัตรา ¥1=$1 ค่า API ถูกกว่าที่อื่นมาก
- Latency ต่ำ