Tutorial Semantic Search Engine dari Nol: Embeddings dan Vector Search

# Membangun Mesin Pencari Semantik dari Nol ## Daftar Isi 1. [Pendahuluan](#pendahuluan) 2. [Prasyarat](#prasyarat) 3. [Memahami Pencarian Semantik](#memahami-pencarian-semantik) 4. [Text Embedding...

By Ruby Abdullah · · tutorial
Semantic SearchEmbeddingsFAISSVector SearchSentence TransformersFastAPI

Membangun Mesin Pencari Semantik dari Nol

Daftar Isi

  • Pendahuluan
  • Prasyarat
  • Memahami Pencarian Semantik
  • Text Embedding dengan Sentence-Transformers
  • Pengindeksan Vektor dengan FAISS
  • Pengindeksan Vektor dengan Annoy
  • Membangun Pipeline Pencarian
  • Filtering dan Metadata
  • Reranking untuk Relevansi yang Lebih Baik
  • Pencarian Hybrid: Menggabungkan Semantik dan Kata Kunci
  • Membangun API dengan FastAPI
  • Metrik Evaluasi
  • Praktik Terbaik
  • Kesimpulan

  • Pendahuluan

    Mesin pencari berbasis kata kunci tradisional mencocokkan dokumen berdasarkan kecocokan kata yang tepat atau samar. Pencarian semantik melangkah lebih jauh dengan memahami makna di balik query dan dokumen. Ketika pengguna mencari "cara memperbaiki pipa bocor," mesin pencari semantik juga dapat mengembalikan hasil tentang "perbaikan saluran air" atau "solusi kebocoran pipa" -- meskipun kata-kata persis tersebut tidak ada dalam query.

    Tutorial ini memandu Anda membangun mesin pencari semantik lengkap dari nol. Anda akan mempelajari cara menghasilkan text embedding, membangun indeks vektor dengan FAISS dan Annoy, mengimplementasikan filtering dan reranking, menggabungkan pencarian semantik dan kata kunci menjadi sistem hybrid, mengekspos semuanya melalui REST API FastAPI, dan mengukur kualitas pencarian dengan metrik evaluasi standar.


    Prasyarat

    • Python 3.9 atau lebih tinggi
    • Pemahaman dasar tentang konsep machine learning
    • Keakraban dengan REST API

    pip install sentence-transformers faiss-cpu annoy numpy fastapi uvicorn rank-bm25 scikit-learn pydantic
    


    Memahami Pencarian Semantik

    Pencarian semantik bekerja dalam tiga tahap:

  • Pengindeksan: Dokumen diubah menjadi vektor embedding padat dan disimpan dalam indeks vektor.
  • Pencarian: Query pengguna diubah menjadi embedding menggunakan model yang sama.
  • Pengambilan: Indeks vektor menemukan dokumen yang embedding-nya paling dekat dengan embedding query.
  • Wawasan kuncinya adalah bahwa teks yang mirip secara semantik menghasilkan vektor yang mirip, memungkinkan pengambilan berbasis makna alih-alih pencocokan kata kunci.

    Query Pengguna: "mobil listrik terjangkau"
    

    |

    v

    [Model Embedding] -> Vektor Query [0.12, -0.45, 0.78, ...]

    |

    v

    [Indeks Vektor] -> Pencarian Tetangga Terdekat

    |

    v

    Hasil:

  • "EV ramah anggaran untuk 2025" (kemiripan: 0.92)
  • "Perbandingan kendaraan listrik murah" (kemiripan: 0.89)
  • "Panduan harga Tesla Model 3" (kemiripan: 0.84)

  • Text Embedding dengan Sentence-Transformers

    Sentence-Transformers adalah pustaka Python yang menyediakan model terlatih untuk menghasilkan text embedding berkualitas tinggi.

    Memuat dan Menggunakan Model Embedding

    from sentencetransformers import SentenceTransformer
    

    import numpy as np

    Muat model terlatih

    'all-MiniLM-L6-v2' adalah keseimbangan yang baik antara kecepatan dan kualitas

    model = SentenceTransformer('all-MiniLM-L6-v2')

    Hasilkan embedding untuk teks tunggal

    teks = "Machine learning adalah bagian dari kecerdasan buatan."

    embedding = model.encode(teks)

    print(f"Bentuk embedding: {embedding.shape}") # (384,)

    print(f"Tipe data embedding: {embedding.dtype}") # float32

    Hasilkan embedding untuk beberapa teks (batch untuk efisiensi)

    dokumen = [

    "Python adalah bahasa pemrograman serbaguna.",

    "Deep learning menggunakan jaringan saraf dengan banyak lapisan.",

    "FastAPI adalah framework web modern untuk Python.",

    "Pemrosesan bahasa alami berurusan dengan pemahaman teks.",

    "Container Docker menyederhanakan deployment aplikasi.",

    ]

    embdokumen = model.encode(dokumen, showprogressbar=True, batchsize=32)

    print(f"Bentuk embedding batch: {embdokumen.shape}") # (5, 384)

    Mengukur Kemiripan

    from sentencetransformers.util import cossim
    
    

    Bandingkan dua kalimat

    kal1 = "Kucing duduk di atas kasur."

    kal2 = "Seekor kucing beristirahat di tempat tidur."

    kal3 = "Pasar saham anjlok kemarin."

    emb1 = model.encode(kal1)

    emb2 = model.encode(kal2)

    emb3 = model.encode(kal3)

    Kemiripan cosine

    sim12 = cossim(emb1, emb2).item()

    sim13 = cossim(emb1, emb3).item()

    print(f"'{kal1}' vs '{kal2}': {sim12:.4f}") # Kemiripan tinggi (~0.7+)

    print(f"'{kal1}' vs '{kal3}': {sim13:.4f}") # Kemiripan rendah (~0.1)

    Memilih Model yang Tepat

    # Perbandingan model untuk berbagai kasus penggunaan
    

    MODEL = {

    "cepatumum": "all-MiniLM-L6-v2", # 384 dim, cepat, kualitas baik

    "kualitastinggi": "all-mpnet-base-v2", # 768 dim, lebih lambat, kualitas terbaik

    "multibahasa": "paraphrase-multilingual-MiniLM-L12-v2", # 384 dim, 50+ bahasa

    "asimetris": "msmarco-distilbert-base-v4", # 768 dim, dioptimalkan untuk pencarian

    }

    def benchmarkmodel(query: str, dokumen: list[str]):

    """Bandingkan kualitas pengambilan di berbagai model."""

    hasil = {}

    for nama, namamodel in MODEL.items():

    m = SentenceTransformer(namamodel)

    qemb = m.encode(query)

    dembs = m.encode(dokumen)

    kemiripan = cossim(qemb, dembs)[0].tolist()

    terurut = sorted(zip(dokumen, kemiripan), key=lambda x: x[1], reverse=True)

    hasil[nama] = terurut[:3]

    return hasil


    Pengindeksan Vektor dengan FAISS

    FAISS (Facebook AI Similarity Search) adalah standar industri untuk pencarian kemiripan efisien pada koleksi vektor besar.

    Membangun Indeks FAISS

    import faiss
    

    import numpy as np

    class IndeksFAISS:

    """Indeks vektor berbasis FAISS dengan dukungan berbagai tipe indeks."""

    def init(self, dimensi: int, tipeindeks: str = "flat"):

    self.dimensi = dimensi

    self.tipeindeks = tipeindeks

    self.indeks = self.buatindeks()

    self.dokumen = []

    self.metadata = []

    def buatindeks(self) -> faiss.Index:

    """Buat tipe indeks FAISS yang sesuai."""

    if self.tipeindeks == "flat":

    # Pencarian eksak (brute force) - akurasi terbaik, paling lambat untuk dataset besar

    return faiss.IndexFlatIP(self.dimensi)

    elif self.tipeindeks == "ivf":

    # Indeks file terbalik - keseimbangan kecepatan dan akurasi

    nlist = 100

    quantizer = faiss.IndexFlatIP(self.dimensi)

    indeks = faiss.IndexIVFFlat(quantizer, self.dimensi, nlist, faiss.METRICINNERPRODUCT)

    return indeks

    elif self.tipeindeks == "hnsw":

    # Hierarchical Navigable Small World - pencarian aproksimasi cepat

    indeks = faiss.IndexHNSWFlat(self.dimensi, 32)

    indeks.hnsw.efConstruction = 200

    indeks.hnsw.efSearch = 64

    return indeks

    else:

    raise ValueError(f"Tipe indeks tidak dikenal: {self.tipeindeks}")

    def tambah(self, embeddings: np.ndarray, dokumen: list[str], metadata: list[dict] = None):

    """Tambahkan dokumen dan embedding-nya ke indeks."""

    faiss.normalizeL2(embeddings)

    if self.tipeindeks == "ivf" and not self.indeks.istrained:

    self.indeks.train(embeddings)

    self.indeks.add(embeddings)

    self.dokumen.extend(dokumen)

    self.metadata.extend(metadata or [{}] len(dokumen))

    def cari(self, queryembedding: np.ndarray, k: int = 10) -> list[dict]:

    """Cari k dokumen paling mirip."""

    querynorm = queryembedding.copy().reshape(1, -1)

    faiss.normalizeL2(querynorm)

    skor, indeks = self.indeks.search(querynorm, k)

    hasil = []

    for s, idx in zip(skor[0], indeks[0]):

    if idx == -1:

    continue

    hasil.append({

    "dokumen": self.dokumen[idx],

    "skor": float(s),

    "metadata": self.metadata[idx],

    "indeks": int(idx),

    })

    return hasil

    def simpan(self, path: str):

    """Simpan indeks ke disk."""

    faiss.writeindex(self.indeks, f"{path}.faiss")

    import json

    with open(f"{path}.meta", "w") as f:

    json.dump({"dokumen": self.dokumen, "metadata": self.metadata}, f, ensureascii=False)

    def muat(self, path: str):

    """Muat indeks dari disk."""

    self.indeks = faiss.readindex(f"{path}.faiss")

    import json

    with open(f"{path}.meta", "r") as f:

    data = json.load(f)

    self.dokumen = data["dokumen"]

    self.metadata = data["metadata"]

    Penggunaan

    model = SentenceTransformer('all-MiniLM-L6-v2')

    dokumen = [

    "Python sangat bagus untuk data science dan machine learning.",

    "JavaScript adalah bahasa pemrograman web.",

    "Docker membantu kontainerisasi dan deployment.",

    "Kubernetes mengorkestrasikan beban kerja container.",

    "PostgreSQL adalah database relasional yang powerful.",

    "Redis adalah penyimpanan key-value in-memory untuk caching.",

    "Git sangat penting untuk version control.",

    "Pipeline CI/CD mengotomasi pengujian dan deployment.",

    "REST API adalah tulang punggung layanan web modern.",

    "GraphQL menyediakan bahasa query fleksibel untuk API.",

    ]

    metadata = [

    {"kategori": "bahasa", "level": "pemula"},

    {"kategori": "bahasa", "level": "pemula"},

    {"kategori": "devops", "level": "menengah"},

    {"kategori": "devops", "level": "lanjut"},

    {"kategori": "database", "level": "menengah"},

    {"kategori": "database", "level": "menengah"},

    {"kategori": "alat", "level": "pemula"},

    {"kategori": "devops", "level": "menengah"},

    {"kategori": "api", "level": "pemula"},

    {"kategori": "api", "level": "menengah"},

    ]

    embeddings = model.encode(dokumen, converttonumpy=True)

    indeks = IndeksFAISS(dimensi=384, tipeindeks="flat")

    indeks.tambah(embeddings, dokumen, metadata)

    Pencarian

    query = "Database apa yang harus saya gunakan untuk caching?"

    queryemb = model.encode(query, converttonumpy=True)

    hasil = indeks.cari(queryemb, k=3)

    for r in hasil:

    print(f" [{r['skor']:.4f}] {r['dokumen']} (kategori: {r['metadata']['kategori']})")


    Pengindeksan Vektor dengan Annoy

    Annoy (Approximate Nearest Neighbors Oh Yeah) adalah alternatif ringan FAISS, sangat baik untuk beban kerja baca-intensif dengan indeks statis.

    from annoy import AnnoyIndex
    
    

    class IndeksAnnoy:

    """Indeks vektor berbasis Annoy untuk pencarian tetangga terdekat aproksimasi cepat."""

    def init(self, dimensi: int, metrik: str = "angular", ntrees: int = 50):

    self.dimensi = dimensi

    self.metrik = metrik

    self.ntrees = ntrees

    self.indeks = AnnoyIndex(dimensi, metrik)

    self.dokumen = []

    self.metadata = []

    self.sudahdibangun = False

    def tambah(self, embeddings: np.ndarray, dokumen: list[str], metadata: list[dict] = None):

    """Tambahkan dokumen dan embedding-nya."""

    idxmulai = len(self.dokumen)

    for i, emb in enumerate(embeddings):

    self.indeks.additem(idxmulai + i, emb)

    self.dokumen.extend(dokumen)

    self.metadata.extend(metadata or [{}] len(dokumen))

    def bangun(self):

    """Bangun indeks (harus dipanggil setelah menambahkan semua item)."""

    self.indeks.build(self.ntrees)

    self.sudahdibangun = True

    def cari(self, queryembedding: np.ndarray, k: int = 10) -> list[dict]:

    """Cari k tetangga terdekat."""

    if not self.sudahdibangun:

    raise RuntimeError("Indeks harus dibangun sebelum pencarian. Panggil bangun() dulu.")

    indekslist, jarak = self.indeks.getnnsbyvector(

    queryembedding, k, includedistances=True

    )

    hasil = []

    for idx, dist in zip(indekslist, jarak):

    kemiripan = 1 - (dist * 2) / 2

    hasil.append({

    "dokumen": self.dokumen[idx],

    "skor": float(kemiripan),

    "metadata": self.metadata[idx],

    "indeks": idx,

    })

    return hasil

    Penggunaan

    indeksannoy = IndeksAnnoy(dimensi=384, ntrees=50)

    indeksannoy.tambah(embeddings, dokumen, metadata)

    indeksannoy.bangun()

    hasil = indeksannoy.cari(queryemb, k=3)

    for r in hasil:

    print(f" [{r['skor']:.4f}] {r['dokumen']}")


    Membangun Pipeline Pencarian

    Sekarang mari kita gabungkan semuanya menjadi pipeline pencarian yang kohesif.

    from dataclasses import dataclass
    

    from typing import Optional, Callable

    import time

    @dataclass

    class HasilPencarian:

    dokumen: str

    skor: float

    metadata: dict

    peringkat: int

    @dataclass

    class ResponPencarian:

    query: str

    hasil: list[HasilPencarian]

    totalhasil: int

    waktupencarianms: float

    class MesinPencarianSemantik:

    """Mesin pencari semantik lengkap dengan embedding, pengindeksan, dan pengambilan."""

    def init(self, namamodel: str = "all-MiniLM-L6-v2", tipeindeks: str = "flat"):

    self.model = SentenceTransformer(namamodel)

    self.dimensi = self.model.getsentenceembeddingdimension()

    self.indeks = IndeksFAISS(dimensi=self.dimensi, tipeindeks=tipeindeks)

    self.jumlahdokumen = 0

    def indeksdokumen(self, dokumen: list[str], metadata: list[dict] = None,

    batchsize: int = 64):

    """Indeks sekumpulan dokumen."""

    embeddings = self.model.encode(

    dokumen, converttonumpy=True,

    batchsize=batchsize, showprogressbar=True,

    )

    self.indeks.tambah(embeddings, dokumen, metadata)

    self.jumlahdokumen += len(dokumen)

    def cari(self, query: str, k: int = 10,

    filterfn: Optional[Callable[[dict], bool]] = None) -> ResponPencarian:

    """Cari dokumen yang cocok dengan query."""

    mulai = time.time()

    queryemb = self.model.encode(query, converttonumpy=True)

    ambilk = k 3 if filterfn else k

    hasilmentah = self.indeks.cari(queryemb, k=ambilk)

    if filterfn:

    hasilmentah = [r for r in hasilmentah if filterfn(r["metadata"])]

    hasilmentah = hasilmentah[:k]

    hasil = [

    HasilPencarian(

    dokumen=r["dokumen"], skor=r["skor"],

    metadata=r["metadata"], peringkat=i + 1,

    )

    for i, r in enumerate(hasilmentah)

    ]

    berlalu = (time.time() - mulai) 1000

    return ResponPencarian(

    query=query, hasil=hasil,

    totalhasil=len(hasil), waktupencarianms=round(berlalu, 2),

    )

    Bangun mesin pencari

    mesin = MesinPencarianSemantik()

    mesin.indeksdokumen(dokumen, metadata)

    Pencarian sederhana

    respon = mesin.cari("alat kontainerisasi")

    print(f"Query: {respon.query} ({respon.waktupencarianms}ms)")

    for r in respon.hasil[:3]:

    print(f" {r.peringkat}. [{r.skor:.4f}] {r.dokumen}")

    Pencarian dengan filter - hanya dokumen DevOps

    respon = mesin.cari(

    "otomasi deployment",

    filterfn=lambda m: m.get("kategori") == "devops"

    )

    print(f"\nHasil terfilter (hanya devops):")

    for r in respon.hasil:

    print(f" {r.peringkat}. [{r.skor:.4f}] {r.dokumen}")


    Filtering dan Metadata

    Filtering metadata yang efektif sangat penting untuk sistem pencarian produksi.

    class FilterMetadata:
    

    """Filter metadata yang dapat dikomposisi untuk hasil pencarian."""

    @staticmethod

    def samadengan(field: str, nilai) -> Callable[[dict], bool]:

    return lambda m: m.get(field) == nilai

    @staticmethod

    def dalamdaftar(field: str, nilailist: list) -> Callable[[dict], bool]:

    return lambda m: m.get(field) in nilailist

    @staticmethod

    def filterrentang(field: str, minval=None, maxval=None) -> Callable[[dict], bool]:

    def cek(m):

    val = m.get(field)

    if val is None:

    return False

    if minval is not None and val < minval:

    return False

    if maxval is not None and val > maxval:

    return False

    return True

    return cek

    @staticmethod

    def gabungdan(filterlist: Callable[[dict], bool]) -> Callable[[dict], bool]:

    return lambda m: all(f(m) for f in filterlist)

    @staticmethod

    def gabungatau(filterlist: Callable[[dict], bool]) -> Callable[[dict], bool]:

    return lambda m: any(f(m) for f in filterlist)

    Contoh penggunaan

    f = FilterMetadata()

    filterpemula = f.samadengan("level", "pemula")

    filterdevopsmenengah = f.gabungdan(

    f.samadengan("kategori", "devops"),

    f.dalamdaftar("level", ["menengah", "lanjut"])

    )

    respon = mesin.cari("alat terbaik untuk deployment", filterfn=filterdevopsmenengah)


    Reranking untuk Relevansi yang Lebih Baik

    Pengambilan awal cepat namun aproksimasi. Reranker dapat memperbaiki urutan hasil untuk relevansi yang lebih baik.

    from sentencetransformers import CrossEncoder
    
    

    class Reranker:

    """Reranker berbasis cross-encoder untuk meningkatkan relevansi pencarian."""

    def init(self, namamodel: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):

    self.model = CrossEncoder(namamodel)

    def rerank(self, query: str, hasil: list[HasilPencarian], topk: int = None) -> list[HasilPencarian]:

    """Rerank hasil pencarian menggunakan model cross-encoder."""

    if not hasil:

    return hasil

    pasangan = [(query, r.dokumen) for r in hasil]

    skor = self.model.predict(pasangan)

    dirankulang = []

    for result, s in zip(hasil, skor):

    dirankulang.append(HasilPencarian(

    dokumen=result.dokumen, skor=float(s),

    metadata=result.metadata, peringkat=0,

    ))

    dirankulang.sort(key=lambda r: r.skor, reverse=True)

    for i, r in enumerate(dirankulang):

    r.peringkat = i + 1

    if topk:

    dirankulang = dirankulang[:topk]

    return dirankulang

    Penggunaan

    reranker = Reranker()

    awal = mesin.cari("cara deploy aplikasi Python", k=10)

    dirankulang = reranker.rerank("cara deploy aplikasi Python", awal.hasil, topk=5)

    print("Setelah reranking:")

    for r in dirankulang:

    print(f" {r.peringkat}. [{r.skor:.4f}] {r.dokumen}")


    Pencarian Hybrid: Menggabungkan Semantik dan Kata Kunci

    Pencarian hybrid menggabungkan kekuatan pengambilan berbasis kata kunci (BM25) dengan pencarian semantik untuk hasil terbaik dari kedua pendekatan.

    from rankbm25 import BM25Okapi
    

    import re

    class MesinPencarianHybrid:

    """Menggabungkan pencarian kata kunci BM25 dengan pencarian semantik."""

    def init(self, namamodel: str = "all-MiniLM-L6-v2", alpha: float = 0.5):

    self.mesinsemantik = MesinPencarianSemantik(namamodel)

    self.alpha = alpha # Bobot semantik vs kata kunci (0=kata kunci saja, 1=semantik saja)

    self.bm25 = None

    self.doktertokenisasi = []

    self.dokumen = []

    self.metadata = []

    def tokenisasi(self, teks: str) -> list[str]:

    """Tokenisasi sederhana untuk BM25."""

    return re.findall(r'\w+', teks.lower())

    def indeksdokumen(self, dokumen: list[str], metadata: list[dict] = None):

    """Indeks dokumen untuk pencarian semantik dan kata kunci."""

    self.dokumen = dokumen

    self.metadata = metadata or [{}] len(dokumen)

    self.mesinsemantik.indeksdokumen(dokumen, metadata)

    self.doktertokenisasi = [self.tokenisasi(dok) for dok in dokumen]

    self.bm25 = BM25Okapi(self.doktertokenisasi)

    def cari(self, query: str, k: int = 10,

    filterfn: Optional[Callable[[dict], bool]] = None) -> ResponPencarian:

    """Pencarian hybrid menggabungkan hasil semantik dan kata kunci."""

    mulai = time.time()

    hasilsemantik = self.mesinsemantik.cari(query, k=k 2, filterfn=filterfn)

    querytoken = self.tokenisasi(query)

    skorbm25 = self.bm25.getscores(querytoken)

    skorsemantik = {r.dokumen: r.skor for r in hasilsemantik.hasil}

    maxbm25 = max(skorbm25) if max(skorbm25) > 0 else 1

    bm25normal = {self.dokumen[i]: s / maxbm25 for i, s in enumerate(skorbm25)}

    gabungan = {}

    semuadok = set(skorsemantik.keys()) | set(d for d, s in bm25normal.items() if s > 0)

    for dok in semuadok:

    ssem = skorsemantik.get(dok, 0)

    skw = bm25normal.get(dok, 0)

    gabungan[dok] = self.alpha ssem + (1 - self.alpha) skw

    dokterurut = sorted(gabungan.items(), key=lambda x: x[1], reverse=True)[:k]

    hasil = []

    for peringkat, (dok, skor) in enumerate(dokterurut, 1):

    idx = self.dokumen.index(dok)

    meta = self.metadata[idx]

    hasil.append(HasilPencarian(dokumen=dok, skor=skor, metadata=meta, peringkat=peringkat))

    berlalu = (time.time() - mulai) 1000

    return ResponPencarian(

    query=query, hasil=hasil,

    totalhasil=len(hasil), waktupencarianms=round(berlalu, 2),

    )

    Penggunaan

    hybrid = MesinPencarianHybrid(alpha=0.6) # 60% semantik, 40% kata kunci

    hybrid.indeksdokumen(dokumen, metadata)

    respon = hybrid.cari("performa database PostgreSQL")

    for r in respon.hasil[:5]:

    print(f" {r.peringkat}. [{r.skor:.4f}] {r.dokumen}")


    Membangun API dengan FastAPI

    from fastapi import FastAPI, HTTPException
    

    from pydantic import BaseModel, Field

    from typing import Optional

    import uvicorn

    app = FastAPI(title="API Pencarian Semantik", version="1.0.0")

    mesinpencari = MesinPencarianHybrid(alpha=0.6)

    class InputDokumen(BaseModel):

    teks: str = Field(description="Teks dokumen untuk diindeks")

    metadata: dict = Field(defaultfactory=dict, description="Metadata opsional")

    class PermintaanIndeksMassal(BaseModel):

    dokumen: list[InputDokumen]

    class PermintaanPencarian(BaseModel):

    query: str = Field(description="Query pencarian")

    k: int = Field(default=10, ge=1, le=100, description="Jumlah hasil")

    alpha: Optional[float] = Field(default=None, ge=0, le=1, description="Bobot semantik")

    kategori: Optional[str] = Field(default=None, description="Filter berdasarkan kategori")

    class ResponHasilPencarian(BaseModel):

    dokumen: str

    skor: float

    metadata: dict

    peringkat: int

    class ModelResponPencarian(BaseModel):

    query: str

    hasil: list[ResponHasilPencarian]

    totalhasil: int

    waktupencarianms: float

    @app.post("/indeks", statuscode=201)

    async def indeksdokumen(permintaan: PermintaanIndeksMassal):

    """Indeks sekumpulan dokumen."""

    tekslist = [d.teks for d in permintaan.dokumen]

    metalist = [d.metadata for d in permintaan.dokumen]

    mesinpencari.indeksdokumen(tekslist, metalist)

    return {"terindeks": len(tekslist), "total": mesinpencari.mesinsemantik.jumlahdokumen}

    @app.post("/cari", responsemodel=ModelResponPencarian)

    async def cari(permintaan: PermintaanPencarian):

    """Cari dokumen yang cocok dengan query."""

    if mesinpencari.mesinsemantik.jumlahdokumen == 0:

    raise HTTPException(statuscode=400, detail="Belum ada dokumen yang diindeks")

    filterfn = None

    if permintaan.kategori:

    filterfn = lambda m: m.get("kategori") == permintaan.kategori

    if permintaan.alpha is not None:

    mesinpencari.alpha = permintaan.alpha

    respon = mesinpencari.cari(permintaan.query, k=permintaan.k, filterfn=filterfn)

    return ModelResponPencarian(

    query=respon.query,

    hasil=[

    ResponHasilPencarian(

    dokumen=r.dokumen, skor=r.skor,

    metadata=r.metadata, peringkat=r.peringkat

    )

    for r in respon.hasil

    ],

    totalhasil=respon.totalhasil,

    waktupencarianms=respon.waktupencarianms,

    )

    @app.get("/kesehatan")

    async def cekkesehatan():

    return {"status": "sehat", "dokumenterindeks": mesinpencari.mesinsemantik.jumlahdokumen}

    if name == "main":

    uvicorn.run(app, host="0.0.0.0", port=8000)


    Metrik Evaluasi

    Mengukur kualitas pencarian sangat penting untuk iterasi dan peningkatan sistem Anda.

    from sklearn.metrics import ndcgscore
    

    import numpy as np

    class EvaluatorPencarian:

    """Evaluasi kualitas mesin pencari dengan metrik IR standar."""

    @staticmethod

    def presisipadak(relevan: set[str], diambil: list[str], k: int) -> float:

    """Proporsi dokumen yang diambil yang relevan."""

    diambilk = diambil[:k]

    relevandiambil = sum(1 for dok in diambilk if dok in relevan)

    return relevandiambil / k if k > 0 else 0.0

    @staticmethod

    def recallpadak(relevan: set[str], diambil: list[str], k: int) -> float:

    """Proporsi dokumen relevan yang berhasil diambil."""

    diambilk = set(diambil[:k])

    relevandiambil = len(relevan & diambilk)

    return relevandiambil / len(relevan) if relevan else 0.0

    @staticmethod

    def meanreciprocalrank(relevan: set[str], diambil: list[str]) -> float:

    """Reciprocal dari peringkat dokumen relevan pertama."""

    for i, dok in enumerate(diambil, 1):

    if dok in relevan:

    return 1.0 / i

    return 0.0

    @staticmethod

    def averageprecision(relevan: set[str], diambil: list[str]) -> float:

    """Rata-rata nilai presisi di setiap posisi dokumen relevan."""

    presisilist = []

    jumlahrelevan = 0

    for i, dok in enumerate(diambil, 1):

    if dok in relevan:

    jumlahrelevan += 1

    presisilist.append(jumlahrelevan / i)

    return sum(presisilist) / len(relevan) if relevan else 0.0

    def evaluasi(self, mesin, queryuji: list[dict], k: int = 10) -> dict:

    """Jalankan evaluasi penuh pada sekumpulan query uji."""

    metrik = {"presisi@k": [], "recall@k": [], "mrr": [], "map": []}

    for qu in queryuji:

    respon = mesin.cari(qu["query"], k=k)

    diambil = [r.dokumen for r in respon.hasil]

    relevan = qu["dokumenrelevan"]

    metrik["presisi@k"].append(self.presisipadak(relevan, diambil, k))

    metrik["recall@k"].append(self.recallpadak(relevan, diambil, k))

    metrik["mrr"].append(self.meanreciprocalrank(relevan, diambil))

    metrik["map"].append(self.averageprecision(relevan, diambil))

    return {

    nama: {"ratarata": np.mean(nilai), "std": np.std(nilai)}

    for nama, nilai in metrik.items()

    }

    Contoh evaluasi

    evaluator = EvaluatorPencarian()

    queryuji = [

    {

    "query": "bahasa pemrograman",

    "dokumenrelevan": {

    "Python sangat bagus untuk data science dan machine learning.",

    "JavaScript adalah bahasa pemrograman web.",

    }

    },

    {

    "query": "orkestrasikan container",

    "dokumenrelevan": {

    "Docker membantu kontainerisasi dan deployment.",

    "Kubernetes mengorkestrasikan beban kerja container.",

    }

    },

    ]

    hasil = evaluator.evaluasi(hybrid, queryuji, k=5)

    for metrik, nilai in hasil.items():

    print(f" {metrik}: {nilai['ratarata']:.4f} (+/- {nilai['std']:.4f})")


    Praktik Terbaik

  • Pilih model embedding yang tepat untuk domain Anda. Model yang di-fine-tune mengungguli model umum.
  • Normalisasi embedding sebelum pengindeksan untuk komputasi kemiripan cosine yang konsisten.
  • Gunakan encoding batch untuk pengindeksan -- jauh lebih cepat daripada encoding satu per satu.
  • Implementasikan pencarian hybrid sejak awal. Pencarian semantik murni melewatkan kecocokan kata kunci yang tepat.
  • Tambahkan reranker untuk hasil teratas. Biaya latensi kecil sangat meningkatkan relevansi.
  • Pre-filter sebelum pencarian vektor jika memungkinkan untuk mengurangi ruang pencarian.
  • Potong dokumen panjang menjadi paragraf atau kalimat untuk pengambilan yang lebih presisi.
  • Cache embedding untuk query yang sering diulang.
  • Monitor kualitas pencarian secara terus-menerus dengan metrik evaluasi dan umpan balik pengguna.
  • Gunakan indeks HNSW atau IVF untuk dataset lebih dari 100K dokumen.

  • Kesimpulan

    Membangun mesin pencari semantik melibatkan beberapa komponen yang bekerja bersama: model embedding untuk representasi teks, indeks vektor untuk pengambilan efisien, reranker untuk presisi, dan pendekatan hybrid untuk cakupan komprehensif.

    Poin-poin kunci:

    • Sentence-Transformers menyediakan model terlatih yang sangat baik untuk text embedding.
    • FAISS dan Annoy menawarkan tradeoff berbeda untuk pengindeksan vektor (akurasi vs kecepatan vs memori).
    • Pencarian hybrid yang menggabungkan BM25 dan pengambilan semantik mengungguli pendekatan mana pun secara terpisah.
    • Reranking cross-encoder secara signifikan meningkatkan relevansi hasil untuk posisi teratas.
    • Evaluasi sistematis dengan metrik IR standar sangat penting untuk mengukur dan meningkatkan kualitas.

    Dengan blok pembangun ini, Anda dapat membuat sistem pencarian yang benar-benar memahami maksud pengguna dan memberikan hasil relevan dalam skala besar.

    Artikel Terkait

    Tutorial Sentence Transformers: Embeddings, Similarity, dan Reranker

    Sentence Transformers: Embedding, Kemiripan Semantik, dan Reranker Sentence Transformers (sering disebut SBERT) adalah p...

    Tutorial FAISS: Pencarian Kemiripan Vektor dalam Skala Besar

    FAISS: Pencarian Kemiripan Vektor yang Efisien dalam Skala Besar FAISS (Facebook AI Similarity Search) adalah library C+...

    Tutorial Lengkap txtai: Database Embeddings All-in-One untuk Semantic Search dan LLM Workflows

    Tutorial Lengkap txtai: Database Embeddings All-in-One untuk Semantic Search dan LLM Workflows txtai adalah framework Py...

    DINOv2: Panduan Lengkap Vision Foundation Model dari Meta AI untuk Embedding Gambar Tanpa Label

    DINOv2: Panduan Lengkap Vision Foundation Model dari Meta AI untuk Embedding Gambar Tanpa Label Halo temen-temen, di tut...