ในยุคที่ 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: 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 TypeQPS (Queries/sec)Latency P99Memory UsageBuild TimeRecall@10
HNSW (M=32, ef=128)~2,500~8ms~4.2 GB~45 min0.97
IVF (nlist=4096, nprobe=64)~1,800~12ms~4.1 GB~12 min0.94
DiskANN (SSD-based)~800~25ms~0.8 GB RAM~60 min0.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✅ เหมาะกับ❌ ไม่เหมาะกับ
HNSWReal-time search (<10ms), High QPS, Dataset <10M, In-memory, Latency-sensitive appsข้อมูล > RAM, งบประมาณจำกัด, Sequential scan use cases
IVF-FlatMedium dataset, Balanced accuracy, Filtering use cases, Memory-efficientUltra-low latency, Billion-scale, ต้องการ compression
IVF-PQLarge dataset (>10M), Memory budget limited, Cost-sensitive productionHigh precision requirements, Fast updates, Low-dimensional vectors
DiskANNBillion-scale, Cloud storage, Memory constraints, Cost optimizationSub-10ms latency requirement, Pure in-memory workloads

ราคาและ ROI

การเลือก Vector Index ที่เหมาะสมส่งผลต่อต้นทุน infrastructure อย่างมาก

Index TypeMemory/1M vectorsInstance ที่ต้องใช้ค่าใช้จ่าย/เดือน (approx)QPS
HNSW (M=32)~4.2 GBr6i.4xlarge (128GB)~$8002,500
IVF-PQ (96 bytes)~96 MBr6i.xlarge (32GB)~$2001,200
DiskANN~800 MB RAM + SSDi3.xlarge + SSD~$350800

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 ในตัว