Membangun Mesin Pencari Semantik dari Nol
Daftar Isi
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:
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.",
]
emb
dokumen = model.encode(dokumen, showprogressbar=True, batchsize=32)
print(f"Bentuk embedding batch: {emb
dokumen.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 presisi
padak(relevan: set[str], diambil: list[str], k: int) -> float:
"""Proporsi dokumen yang diambil yang relevan."""
diambil
k = 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
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.