# LAPORAN AKHIR — KozyNear **Mata Kuliah**: Temu Kembali Informasi (STKI, COM620321, 3 SKS) **Universitas**: Lampung (UNILA), FMIPA **Project**: Indonesian Kos-Kosan Search Engine — **seluruh Bandar Lampung untuk semua universitas** **Submission**: 17 Juni 2026 (vClass) **Repository**: https://github.com/DYmazeh/KozyNear **Live Demo**: https://dymazeh-kozynear.hf.space (HF Spaces, neural ON) | backup: https://kozynear.onrender.com (Render, smart-only) --- ## 1. Abstract KozyNear adalah Information Retrieval (IR) system untuk listing kos-kosan di **Bandar Lampung raya** (17 kecamatan kota + perbatasan Jati Agung/ITERA dengan data real; 9 universitas target: UNILA, ITERA, Darmajaya, UBL, UIN Raden Intan, Teknokrat, Polinela, Malahayati, Saburai). Sistem mendukung natural language query bahasa Indonesia dengan domain-specific preprocessing pipeline dan membandingkan **lima model retrieval**: TF-IDF, BM25, neural semantic (MiniLM multilingual, label kode "IndoBERT"), hybrid (BM25 → MiniLM rerank), dan **Smart** — pipeline produksi berbasis query understanding (parser rule-based) + geo ranking (gazetteer kampus terverifikasi + haversine) + BM25 fusion. Corpus terdiri dari **227 listing kos REAL** hasil scrape Mamikos.com (deskripsi pemilik asli + koordinat asli), bukan synthetic. Representasi dokumen memakai **fielded indexing** (judul diulang 2x + kecamatan + fasilitas + deskripsi, varian praktis BM25F): 21% deskripsi pemilik < 10 kata sehingga field metadata membawa sinyal terkuat; perubahan ini menaikkan SEMUA model. Evaluasi pada **30 query × 900 ground truth annotations** (AI-assisted 3-annotator simulation, Cohen's Kappa 0.60-0.87) menempatkan **Smart sebagai best performer standard top-K** (P@5 0.773, MAP 0.359, NDCG@10 0.684) diikuti BM25 (P@5 0.640, MAP 0.296); signifikansi pairwise diuji Wilcoxon signed-rank dengan koreksi **Holm-Bonferroni** + effect size rank-biserial (4 dari 10 pasangan signifikan setelah koreksi, semuanya melibatkan model neural; keunggulan Smart vs BM25 raw-significant p=0.039 r=0.46 namun gugur koreksi). Pada lensa **Constraint Satisfaction@5** (proporsi top-5 yang memenuhi semua kebutuhan eksplisit user: gender, budget, fasilitas, radius 3 km dari kampus), Smart dominan atas semua model (**0.867 vs BM25 0.527**, Wilcoxon p=0.0001, r=0.91). Eksperimen Learning-to-Rank (logistic regression 9 fitur, 5-fold per query) menghasilkan MAP pool 0.926 vs fusion manual 0.907 (tidak signifikan, p=0.14): grid search manual sudah mendekati optimal pada rezim data kecil. Hyperparameter dipilih lewat grid search dua-lensa: bobot fusion Smart 0.2/0.4/0.4, alpha Hybrid 0.9, k1/b BM25 default (kurva datar). Finding penting: **model neural terlihat underperform di standard top-K eval (P@5 0.180)** semata-mata karena *pooling bias* — ground truth di-pool dari kandidat BM25, sehingga hasil semantic yang unik tidak ter-judge. Pada **pool-restricted evaluation** (fair, hanya judged docs), neural justru kompetitif (MAP 0.627 vs BM25 0.667). Ini menegaskan pentingnya melaporkan kedua angka untuk corpus IR yang di-pool secara lexical. Untuk menutup gap metodologis ini, disiapkan **kit anotasi manusia** dengan pool union top-10 LIMA model (797 pasangan, urutan diacak) beserta script konsensus + Kappa antar-manusia. Koordinat real 226/227 listing dipakai untuk geo ranking Smart dan integrasi Google Maps. --- ## 2. Latar Belakang ### 2.1 Motivasi Mahasiswa di Bandar Lampung — baik dari UNILA, ITERA, Darmajaya, UBL, UIN, Teknokrat, Polinela, Malahayati, maupun Saburai — membutuhkan informasi kos-kosan strategis sekitar kampus mereka. Platform existing seperti Mamikos kurang optimal untuk Indonesian natural language karena tidak mendukung sinonim domain (e.g., "kmd" vs "kamar mandi dalam") dan tidak fokus geografis ke Bandar Lampung. ### 2.2 Tujuan 1. Membangun search engine khusus kos-kosan **seluruh Bandar Lampung** untuk **semua universitas** (universitas-agnostic) 2. Membandingkan tiga paradigma IR utama + hybrid: lexical (TF-IDF), probabilistic (BM25), neural/semantic (IndoBERT+FAISS) 3. Mengevaluasi performa quantitative via metric standar IR dengan inter-annotator agreement protocol 4. Deploy sebagai web app yang accessible publik ### 2.3 Ruang Lingkup **In scope**: text retrieval atas data REAL Mamikos, 17 kecamatan Bandar Lampung, 9 universitas target, deskripsi pemilik asli, koordinat real untuk Google Maps, deployment Render.com free tier. **Out of scope**: image retrieval, real-time price updates, user auth, booking, multi-city (kabupaten lain). Geo-spatial distance kini masuk scope sebagai SALAH SATU sinyal fusion di model Smart (bobot 0.4 bersama teks 0.4 + atribut 0.2), bukan primary signal tunggal. Tipe `pasutri` di luar scope — Mamikos hanya mengklasifikasi gender 0/1/2 (campur/putra/putri); "pasutri" adalah kategori marketing (campur yang boleh berdua), bukan tipe gender. --- ## 3. Dataset ### 3.1 Acquisition Methodology — Real Mamikos Scrape Corpus adalah **227 listing kos REAL** dari Mamikos.com, di-scrape via pipeline 3-tahap (sumber: `data/raw/mamikos_real_v2.jsonl`): 1. **Discovery** (`scripts/discover_mamikos_slugs.py` + WebSearch): kumpulkan URL halaman detail `/room/`. Halaman kategori Mamikos JS-rendered dengan API listing ter-enkripsi (AES), jadi discovery dilakukan via ~50 query `site:mamikos.com inurl:/room/` (per-kecamatan, per-universitas, per-fasilitas, per-harga, street-level) → **312 URL unik** terkumpul dalam 5 ronde. 2. **Detail extraction** (`scripts/extract_mamikos_detail.py`): setiap halaman detail Mamikos meng-*inject* `var detail = {...}` (JSON 146 field) di static HTML — bisa di-fetch HTTP-only tanpa browser. Parse field penting: `_id`, `room_title`, `description` (cerita pemilik asli), `latitude`/`longitude`, `price_monthly`, `gender`, fasilitas (fac_room/share/bath/park), booking_rules, owner_name, verification. 3. **Canonical build** (`scripts/rebuild_v2.py`): normalisasi kecamatan, hitung `jarak_kampus_km` via haversine ke 9 universitas, drop deskripsi kosong → `kozynear_combined.jsonl`. Success rate ekstraksi ~76% (sisanya listing inactive/removed). Field gender Mamikos hanya 0/1/2 → tipe **campur/putra/putri** (tidak ada pasutri). ### 3.2 Honest Methodology Disclosure Iterasi awal project memakai synthetic corpus (template-generated) untuk mengejar volume. Namun karena web app ditujukan **production** (pengguna mencari kos nyata + tampilan Google Maps), synthetic dibuang total: koordinat synthetic yang fabricated akan menaruh pin di lokasi salah. **Trade-off yang diterima secara sadar**: corpus real lebih kecil (227 vs 2000 synthetic). Kami memilih **kualitas + keaslian** di atas volume. Semua deskripsi adalah tulisan asli pemilik kos (terverifikasi 0 template-leak), bukan generated. Setiap listing punya `url_source` untuk traceability balik ke Mamikos. **Validasi coverage terhadap populasi (12 Juni 2026)**: halaman kategori resmi Mamikos "Kost Bulanan Bandar Lampung" mencantumkan **"Tersedia 256 Kost"** (live; identik dengan snapshot 29 Mei — pasar stabil); API pencarian internal melaporkan `property_total: 205` untuk filter bulanan radius default. Corpus 227 = **88.7% dari populasi kost-bulanan aktif versi kategori Mamikos**, dan **melebihi** hitungan radius-default API (205). Funnel: 313 slug ditemukan via Google discovery → 240 berhasil diekstrak (76.7%) → 227 lolos quality bar (13 deskripsi kosong dibuang). **Uji ceiling (12 Juni)**: 72 slug yang dulu gagal diekstrak dicoba ulang lewat gerbang validasi (`scripts/validate_new_listings.py`, audit di `eval/_audit_new_batch.json`): **69 mati permanen** (halaman dihapus Mamikos), **3 berhasil tapi ditolak gerbang karena id duplikat** (URL slug berbeda menunjuk listing yang sama) → **0 listing baru valid**. Discovery massal lanjutan tertutup karena Mamikos kini **mengenkripsi payload listing di API** (field `rooms` = blob terenkripsi, bukan JSON terbuka). Kesimpulan: 227 adalah **ceiling efektif** corpus yang dapat dijangkau secara etis-teknis — mendekati **sensus** segmen kost bulanan Bandar Lampung, bukan sampel kecil. ### 3.3 Statistik Corpus | Metric | Value | |--------|-------| | Total listings | **227 (100% real Mamikos)** | | Kecamatan covered | 17 kecamatan Kota Bandar Lampung + Jati Agung (2 listing area perbatasan ITERA, Lampung Selatan) | | Tipe | campur 114 (50%), putri 85 (37%), putra 28 (13%) — imbalance putra di-disclose | | Harga (per bulan) | min Rp 300k, median Rp 800k, max Rp 6jt | | Deskripsi (kata) | median ~23, min 2; **48 listing (21%) < 10 kata** → dasar keputusan fielded indexing (§5.0) | | Duplikat deskripsi persis | 3 listing (template pemilik yang sama; dibiarkan, terdiferensiasi via judul) | | Koordinat valid (Google Maps) | 226/227 (99.6%) | | url_source / Mamikos-verified | 100% / 100% | | File size | ~0.3 MB raw / ~0.6 MB processed | ### 3.4 Etika & Pembersihan Data - **PII dihapus**: kolom `owner_name` (nama pemilik) di-strip dari semua file data yang terdistribusi; sistem tidak pernah menyajikannya via API. - **Artefak parsing dibersihkan**: 7 entri fasilitas tidak valid (digit "0"/"1"/"2") dibuang; normalizer kini menolak item digit-murni. - **Scraping etis**: hanya halaman publik, rate-limited, `url_source` dipertahankan untuk atribusi balik ke Mamikos; data dipakai untuk keperluan akademik non-komersial; snapshot bersifat point-in-time (29 Mei 2026), harga/ketersediaan dapat berubah. **Distribusi listings per kecamatan** (top: Sukarame 40, Kedaton 37, Rajabasa 32, Tanjung Karang Timur 14, Sukabumi 13): ![Distribusi Kecamatan](notebooks/charts/01_kecamatan.png) **Karakteristik data real vs synthetic**: deskripsi pemilik asli jauh lebih *terse* (median 23 kata) dibanding synthetic (110+ kata padded). Contoh nyata: *"belakang UNILA (sangat dekat, jalan kaki 5mnt), dekat rel kereta"*. Ini menurunkan absolute IR metric (lebih sedikit term overlap) tetapi mencerminkan **realita yang sebenarnya** dihadapi sistem production. **Distribusi tipe + harga** (lihat notebook 01_eda.ipynb untuk chart lain): ![Distribusi Tipe Kos](notebooks/charts/01_tipe.png) ![Distribusi Harga](notebooks/charts/01_price.png) --- ## 4. Preprocessing ### 4.1 Pipeline 9-Stage ``` raw text → 1. HTML strip (BeautifulSoup) → 2. Whitespace normalize → 3. Price extraction (SEBELUM lowercase — preserve Rp) → 4. Lowercase → 5. Custom jargon dictionary substitution (longest-first regex) → 6. Spelling correction (typo dictionary) → 7. Tokenize (whitespace + punctuation) → 8. Stopword removal (Sastrawi default + custom domain) → 9. Stem (Sastrawi StemmerFactory, LRU-cached) ``` **Anti-pattern**: lowercase sebelum extract harga → regex `[Rr][Pp]` masih match, tapi edge case `Rp850k` (tanpa space) jadi rapuh. ### 4.2 Custom Jargon Dictionary **106 entries** (course requirement ≥100): - ABBREVIATIONS: 43 (kmd, wc dlm, jt, rb, yg, krn, ...) - LOCATIONS: 24 (gdg meneng, sumbro, unyila, ...) - TYPE_SLANG: 11 (cowo, cewe, pria, wanita, ...) — entri `pasutri` dihapus (bukan tipe gender valid di Mamikos) - RULES: 5 (jamal, no smoking, ...) - PAYMENT: 8 (dp, deposit, all in, ...) - KOS_FORM: 15 (kostan, kontrakan, eksklusif, ...) **Audit coverage terhadap corpus real** (`eval/jargon_coverage.json`): hanya **37/106 entri (35%) pernah match di corpus**; 69 entri dorman (pemilik Mamikos jarang menulis singkatan seperti "kmd"/"km dlm" di deskripsi). Kamus tetap dipertahankan karena fungsi utamanya di **sisi query user live** (singkatan umum di query pencarian), bukan di dokumen. Konsisten dengan temuan ablation §4.5. ### 4.3 Pipeline Performance + Stage Impact Run pada 227 listing real: - **Latency**: ~5 detik (Sastrawi LRU cache, deskripsi real lebih pendek) - **Token reduction**: deskripsi terse pemilik → median ~20 token setelah stemming (real owner text jauh lebih ringkas dari synthetic padded) ### 4.5 Ablation Study per Stage (empiris, bukan checklist) Tiap stage dimatikan satu per satu (yang lain ON), corpus fielded + query di-preprocess ulang dengan konfigurasi sama, BM25 dievaluasi standard terhadap GT yang sama (`scripts/experiment_preprocess_ablation.py`, `eval/preprocess_ablation.csv`): | Konfigurasi | MAP | ΔMAP vs full | P@5 | |---|---|---|---| | Full pipeline | 0.2958 | — | 0.640 | | tanpa strip_html | 0.2958 | +0.0000 | 0.640 | | tanpa normalize_whitespace | 0.2958 | +0.0000 | 0.640 | | tanpa lowercase | 0.2958 | +0.0000 | 0.640 | | **tanpa jargon dict** | **0.3193** | **+0.0235** | **0.700** | | tanpa spelling correction | 0.2956 | -0.0002 | 0.640 | | tanpa stopword removal | 0.2885 | -0.0073 | 0.620 | | tanpa stemming | 0.2969 | +0.0011 | 0.653 | Bacaan jujur: 1. **Stopword removal satu-satunya stage dengan kontribusi positif terukur** (+0.007 MAP). 2. **Stemming Sastrawi nyaris tanpa efek** (malah -0.001 saat ON): deskripsi pendek didominasi kata dasar. Catatan teknis: delta "lowercase" terlihat nol justru karena Sastrawi ikut me-lowercase output (dependensi tersembunyi antar stage). 3. **Substitusi jargon di sisi DOKUMEN justru merugikan ranking** (+0.0235 MAP saat dimatikan, padahal bias pooling melawan temuan ini karena GT di-pool dari BM25 full-pipeline — sinyalnya kuat). Mekanisme: kanonikalisasi menyatukan varian langka menjadi frasa umum sehingga daya diskriminasi IDF turun. Implikasi desain: jargon dict adalah fitur **query understanding**, bukan booster representasi dokumen; future work: query-side expansion (bukan substitution) agar kedua bentuk tetap match tanpa menghomogenkan dokumen. ### 4.4 Real-World Insight 1. **"gedong meneng" → "gedong neng"**: Sastrawi treats "meneng" punya prefix "me-" yang invalid (false positive stemming) 2. **"kamar mandi dalam" → "mandi"**: "kamar" jadi custom domain stopword, sehingga sebagian besar signal hilang 3. **Jargon substitution sukses**: "kmd" → "kamar mandi dalam" → "mandi" setelah stemming (signal preserved walau partial) --- ## 5. Model Comparison ### 5.0 Representasi Dokumen: Fielded Indexing (BM25F-lite) Iterasi awal hanya meng-index `deskripsi` — padahal 21% deskripsi pemilik < 10 kata dan field terkuat (judul "Kost Putri Jocelyn Rajabasa", kecamatan, fasilitas) tak terlihat model mana pun. Sejak iterasi fielded: ``` teks_lexical = judul + judul + kecamatan + fasilitas + deskripsi teks_neural = judul. kecamatan. fasilitas. deskripsi (tanpa repetisi) ``` Pembobotan field via repetisi (judul 2x) adalah varian praktis BM25F [2]: menaikkan term frequency field penting tanpa mengubah engine. Efek: token per dokumen min 2 → 17 (avg 73.7), vocab BM25 1298 → 1498, dan SEMUA model naik (BM25 MAP 0.270 → 0.296; Smart P@5 0.580 → 0.773). Satu helper (`app/preprocessing/doc_text.py`) dipakai semua jalur build agar index, eval, dan serving melihat representasi yang sama. ### 5.1 Implementasi | Model | Library | Hyperparameter | |-------|---------|----------------| | TF-IDF | scikit-learn TfidfVectorizer | ngram_range=(1,2), min_df=2, max_features=10k, sublinear_tf=True | | BM25 | rank_bm25 BM25Okapi | k1=1.5, b=0.75 — grid 6x5 di pool-restricted menunjukkan kurva DATAR (terbaik 0.6674 vs default 0.6666), default literatur dipertahankan (`eval/bm25_params_grid.csv`) | | Neural "IndoBERT" | fastembed (ONNX) + FAISS | paraphrase-multilingual-MiniLM-L12-v2, IndexFlatIP, normalize_embeddings. Catatan kejujuran: label kode `indobert` historis; model sebenarnya MiniLM multilingual | | Hybrid | BM25 → MiniLM rerank EXHAUSTIVE (semua dokumen; plafon top-50 lama dihapus) | min-max normalize + alpha=0.9 (grid search pool-restricted, lihat §8.3) | | **Smart (live)** | parser rule-based + gazetteer + BM25 | fusion 0.2 teks + 0.4 geo (1/(1+km), anchor terverifikasi OSM/Wikipedia) + 0.4 fasilitas (grid search CS@5, lihat §8.3b); hard filter gender/harga dengan relaxation bertahap; geo-augment kandidat radius 3 km; filter UI = constraint eksplisit yang tidak pernah di-relax | ### 5.2 Index Build Performance | Index | Build Time | Disk Size | |-------|-----------|-----------| | TF-IDF | 0.2s | 125 KB | | BM25 | 0.1s | 53 KB | | IndoBERT (227 docs encode) | ~10s | ~0.7 MB (embeddings + FAISS + meta) | | Hybrid | runtime composition (no build) | — | **Total**: <1 MB indexes committed ke repo. Corpus real yang lebih kecil (227 docs) membuat footprint memory ringan — cocok untuk Render free tier 512MB RAM (encoding via `fastembed` ONNX ~150MB, bukan torch ~500MB). --- ## 6. Evaluation Protocol ### 6.1 Query Set (v1.1 — 30 queries) **30 queries** (eval/queries.json): q01-q15 dari v1.0 (didesain untuk data real), q16-q30 ditambah untuk menaikkan power statistik (n=15 underpowered untuk Wilcoxon) + memperluas coverage: kampus Darmajaya/UBL, kecamatan Tanjung Senang/Gedong Meneng/Labuhan Ratu, fasilitas kipas angin/tv/kasur, landmark Mall Boemi Kedaton. Setiap query diverifikasi *answerable* oleh corpus real (avg 14.4 relevant/query; 2 draft q16-q30 yang mati diganti sebelum dipakai). Berbasis vocabulary yang benar-benar muncul di deskripsi pemilik. Sampel pola v1.0: - **Per-universitas**: q01 (UNILA+putra+wifi+ac), q04 (ITERA+sukarame), q05 (Teknokrat+kedaton), q06 (Polinela+rajabasa), q11 (UNILA+putra+rajabasa), q12 (UNILA+putri), q13 (UIN raden intan) - **Per-tipe + fasilitas**: q02 (putri+kedaton), q03 (campur+kmd+parkir), q07 (eksklusif+ac+wifi+shower), q14 (putri+way halim) - **Kualitas / use-case**: q08 (putri+aman+cctv), q09 (dapur+parkir mobil), q10 (transmart+way halim), q15 (campur+nyaman+strategis) **Catatan desain penting**: BM25/IR meng-index **teks deskripsi**; filter kecamatan/harga/tipe dilayani **metadata** terpisah di `/search` endpoint. Karena itu query eval fokus pada konsep yang muncul di teks (universitas, fasilitas, kualitas). Dari versi awal (18 query untuk corpus synthetic), **8 query di-drop karena mati di data real** — yaitu yang menguji karakteristik khas synthetic: `pasutri` (corpus real 0 pasutri), "jam malam aturan ketat", "kerja dari rumah", "warung makan", "tahunan bayar di muka", street-name spesifik. Deskripsi pemilik asli yang terse tidak memuat frasa-frasa template tersebut. ### 6.2 Ground Truth — AI-Assisted 3-Annotator Simulation **Pool**: BM25 top-30 per query = **900 (query, doc) pairs** (30 × 30). Rata-rata 17.0 dokumen relevan per query, tidak ada query dengan < 5 dokumen relevan. **Jalur upgrade ke anotasi manusia** (kit sudah tersedia, menutup dua limitation sekaligus — GT simulasi DAN pooling bias): `scripts/make_annotation_sheet.py` membuat pool union top-10 LIMA model (797 pasangan, urutan diacak agar model asal tersamar) → `eval/annotation_sheet.csv` diisi 1-3 annotator manusia (0/1/2) → `scripts/ingest_human_annotations.py` menghitung konsensus + Cohen's Kappa antar-MANUSIA → seluruh evaluasi dijalankan ulang dengan `--ground-truth eval/ground_truth_human.csv`. **Annotator simulation** (rule-based, `scripts/generate_ground_truth.py`): - A (strict): tipe match +1.5 / mismatch -2.0; kecamatan +1.5; fasilitas +0.5; harga "murah" +1.0 - B (lenient): A + 0.5 score boost - C (noisy): A + uniform(-0.5, 0.5) jitter (seed 42) Score → relevance: ≥2.5 → 2, ≥1.0 → 1, else 0. Consensus majority vote → `eval/ground_truth.csv`. **Disclosure**: AI-assisted bootstrap GT, BUKAN human annotation — di-pool dari BM25 (lihat implikasi *pooling bias* di §7/§8). Untuk production-grade, 3-human annotator pass tetap recommended (`docs/ground_truth_protocol.md`). ### 6.3 Inter-Annotator Agreement | Pair | Cohen's Kappa | Interpretation | |------|---------------|----------------| | A vs B | 0.731 | substantial | | A vs C | 0.866 | almost perfect | | B vs C | 0.604 | substantial | Kappa pada data real sedikit lebih rendah dari iterasi synthetic karena deskripsi terse memberi sinyal lebih ambigu untuk heuristik. Pasangan B-C (lenient vs noisy) turun ke *moderate* (0.572); A-B dan A-C tetap substantial–almost perfect. ### 6.4 Distribusi Consensus | Relevance | Count | % | |-----------|-------|---| | 0 (not relevant) | 389 | 43.2% | | 1 (somewhat) | 394 | 43.8% | | 2 (highly) | 117 | 13.0% | Proporsi relevan naik dibanding iterasi deskripsi-only karena pool BM25 fielded menjaring kandidat yang lebih tepat (field judul/kecamatan ikut match) — konsisten dengan naiknya rata-rata relevan per query (14.4 → 17.0). Proporsi rel=2 lebih kecil dari iterasi synthetic (template membuat banyak "highly relevant" semu); data real lebih realistis — sedikit yang benar-benar sangat relevan per query. --- ## 7. Results Tiga lensa evaluasi dilaporkan karena GT di-pool secara lexical (BM25): **(A) Standard top-K** — IR query atas seluruh corpus 227; **(B) Pool-restricted** — model hanya me-ranking ulang dokumen yang sudah ter-judge (fair untuk model non-lexical); **(C) Constraint Satisfaction@5** — proporsi top-5 yang memenuhi semua kebutuhan eksplisit user, tanpa qrels (bebas pooling bias). Eval Smart memakai fungsi `smart_rank()` yang persis sama dengan serving (`scripts/eval_smart.py`). ### 7.1a Standard Top-K (30 queries × 5 models, index fielded) | Model | P@5 | P@10 | MAP | NDCG@10 | MRR | |-------|-----|------|-----|---------|-----| | **Smart (live)** | **0.773** | **0.633** | **0.359** | **0.684** | 0.863 | | BM25 | 0.640 | 0.610 | 0.296 | 0.634 | **0.872** | | Hybrid (α=0.9) | 0.640 | 0.600 | 0.285 | 0.627 | 0.872 | | TF-IDF | 0.613 | 0.547 | 0.253 | 0.569 | 0.786 | | Neural MiniLM | 0.180 | 0.150 | 0.044 | 0.154 | 0.359 | **Ranking by MAP**: Smart > BM25 > Hybrid > TF-IDF > Neural. Smart kini unggul di SEMUA metrik utama termasuk P@10 (kelemahan iterasi sebelumnya hilang setelah fielded indexing memperbaiki recall kandidat). Selisih Smart vs BM25 raw-significant (p=0.039, r=0.46 medium) tapi gugur setelah koreksi Holm 10-uji (§7.2); klaim terkuat Smart tetap di lensa CS@5 (§7.4) yang bebas qrels. ### 7.1b Pool-Restricted (fair across models) | Model | P@5 | P@10 | MAP | NDCG@10 | MRR | |-------|-----|------|-----|---------|-----| | **Smart (live)** | **0.940** | **0.870** | **0.893** | **0.895** | **1.000** | | BM25 | 0.640 | 0.610 | 0.667 | 0.634 | 0.878 | | Hybrid (α=0.9) | 0.633 | 0.603 | 0.662 | 0.628 | 0.878 | | TF-IDF | 0.660 | 0.583 | 0.656 | 0.604 | 0.815 | | Neural MiniLM | 0.580 | 0.580 | 0.627 | 0.552 | 0.764 | **Insight kunci #1 (pooling bias)**: Neural melompat dari MAP 0.044 (standard) → **0.627** (pool-restricted). Gap besar ini adalah artefak *pooling bias*: GT di-pool dari BM25 top-30, jadi dokumen yang dipilih model semantic tapi tidak masuk pool otomatis dihitung tidak-relevan (rel=0) di standard eval — bukan karena modelnya buruk, melainkan karena belum ter-judge. Pada perbandingan fair, neural kompetitif (~0.04 MAP di bawah BM25). **Insight kunci #2 (smart di pool)**: MAP Smart 0.893 (MRR 1.000) di pool-restricted sebagian besar datang dari **alignment constraint dengan judgment**: annotator (simulasi) menghukum gender/harga mismatch, dan hard filter Smart membuang dokumen yang sama sehingga dokumen relevan naik ke atas. Ini by-design (sistem mengoptimalkan kebutuhan user yang sama), tapi karena heuristik annotator dan filter Smart melihat sinyal yang mirip, angka ini dibaca sebagai *upper bound* yang optimistic, bukan bukti mutlak superioritas; karena itu lensa CS@5 yang bebas qrels dilaporkan juga (§7.4), dan kit anotasi manusia disiapkan (§6.2). ![MAP per Model](notebooks/charts/04_map_bars.png) ![All Metrics Comparison](notebooks/charts/04_all_metrics.png) ### 7.2 Pairwise Statistical Significance (Wilcoxon Signed-Rank + Holm + effect size, n=30, α=0.05) Pada AP standard top-K, 5 model = 10 pasangan diuji sekaligus. Menguji banyak hipotesis pada α=0.05 tanpa koreksi menggelembungkan peluang false positive (10 uji: ~40%), jadi signifikansi final memakai koreksi **Holm-Bonferroni**; effect size **rank-biserial r** dilaporkan supaya magnitudo terbaca terpisah dari signifikansi (r positif = model pertama lebih tinggi). MAP juga dilaporkan dengan 95% CI bootstrap di `eval/eval_summary.md`. | Pair | p-value | p-Holm | r | Sig (Holm) | |------|---------|--------|---|------------| | BM25 vs Hybrid | 0.0854 | 0.1708 | +0.462 | no | | BM25 vs Neural | <0.0001 | <0.0001 | +0.987 | **YES** | | BM25 vs Smart | 0.0388 | 0.1530 | -0.455 | no | | BM25 vs TF-IDF | 0.0246 | 0.1230 | +0.504 | no | | Hybrid vs Neural | <0.0001 | <0.0001 | +0.987 | **YES** | | Hybrid vs Smart | 0.0382 | 0.1530 | -0.448 | no | | Hybrid vs TF-IDF | 0.1363 | 0.1708 | +0.328 | no | | Neural vs Smart | <0.0001 | 0.0001 | -0.940 | **YES** | | Neural vs TF-IDF | <0.0001 | <0.0001 | -0.957 | **YES** | | Smart vs TF-IDF | 0.0101 | 0.0605 | +0.557 | no | Pembacaan: **8/10 raw-significant → 4/10 setelah Holm** (yang bertahan semuanya vs Neural, efek hampir sempurna r≈0.94-0.99 — cerita pooling bias). Keunggulan Smart atas BM25/Hybrid/TF-IDF konsisten arah dengan efek medium-besar (|r| 0.45-0.56) dan p mentah < 0.05, tapi belum bertahan terhadap koreksi keluarga pada n=30 — dibaca "bukti awal yang menjanjikan", bukan klaim final. Smart vs TF-IDF nyaris lolos (p-Holm=0.0605). Detail: `eval/significance_map.csv`. ### 7.3 Per-Query Analysis Insights - **BM25 kuat di queries lexical-heavy**: exact-match nama universitas + fasilitas yang muncul di judul/deskripsi (q01, q03, q13). - **TF-IDF kompetitif**: pada corpus kecil 227 dengan term yang jarang, TF-IDF sublinear_tf cukup dekat dengan BM25 (selisih tidak signifikan). - **Query landmark sembuh setelah fielded indexing**: q10 (transmart way halim) dulu hanya ~2 dokumen relevan karena deskripsi jarang menyebut landmark; dengan judul + kecamatan ikut terindex, pool-nya kini sehat (≥5 relevan, tidak ada lagi query tipis di seluruh set). - **Neural MiniLM**: kuat di pool-restricted (semantic mengelompokkan kos sejenis) tapi tertekan di standard karena GT lexical-pooled. ### 7.4 Constraint Satisfaction @5 (lensa bebas qrels) CS@5 = proporsi top-5 yang memenuhi **semua** constraint query: gender benar + harga ≤ budget + fasilitas ada + jarak ≤ 3 km dari anchor kampus/ landmark (30 query di `eval/queries_constraints.json`, koordinat anchor terverifikasi OSM/Wikipedia). Metric ini mengukur langsung kebutuhan user dan tidak bergantung ground truth (bebas pooling bias). Kini diukur untuk SEMUA model. Catatan kebijakan: dokumen tanpa koordinat/harga dianggap lolos cek terkait (missing = tidak menghukum; 1 dokumen tanpa koordinat). | Model | mean CS@5 (n=30) | |-------|------------------| | **Smart (live)** | **0.867** | | BM25 | 0.527 | | TF-IDF | 0.527 | | Hybrid (α=0.9) | 0.513 | | Neural MiniLM | 0.233 | Wilcoxon signed-rank smart vs BM25 (uji terencana tunggal, tanpa koreksi keluarga): **p=0.0001, r=0.91 (efek besar)**. Interpretasi praktis: dari 5 hasil teratas, rata-rata 4.3 memenuhi semua kebutuhan user di Smart vs 2.6 di BM25. Model murni-teks mana pun mentok di ~0.5 karena constraint terstruktur (gender/budget/jarak) memang tak terbaca dari ranking teks. Inilah justifikasi kuantitatif Smart sebagai model live. Detail per-query: `eval/results_constraints.csv`. --- ## 8. Discussion ### 8.1 Mengapa BM25 Baseline Terkuat? 1. **Lexical-heavy query**: kebanyakan query mengandung exact terminology (nama kecamatan, tipe kos, nama universitas, fasilitas). BM25 term saturation handling lebih appropriate dari TF-IDF (sublinear_tf). 2. **Short documents**: deskripsi pemilik sangat pendek; fielded indexing (judul 2x + metadata, §5.0) memperkaya representasi, dan BM25 length normalization tetap efektif — grid k1/b bahkan datar (§5.1), tanda sinyal field lebih menentukan daripada tuning parameter. 3. **Domain-specific corpus**: minim ambiguity semantic — keuntungan embedding kecil, makin kecil setelah fielded indexing (lihat §8.3). 4. **GT lexical-pooled**: ground truth di-pool dari BM25, sehingga metric standar memang menguntungkan model lexical (lihat §8.2 pooling bias). ### 8.2 Mengapa IndoBERT "Underperform" — Pooling Bias, Bukan Model Temuan paling penting dari iterasi real-data: model neural **tidak** benar-benar buruk. Gap MAP 0.044 (standard) vs 0.627 (pool-restricted) hampir seluruhnya disebabkan **pooling bias**: 1. **Ground truth di-pool dari BM25**: hanya BM25 top-30 yang di-judge. Dokumen yang IndoBERT ranking tinggi tapi tidak masuk pool BM25 otomatis dihitung rel=0 — *unjudged ≠ irrelevant*. Ini bias klasik TREC-style pooling ketika pool dibangun dari satu jenis model. 2. **Bukti pool-restricted**: saat semua model hanya me-ranking dokumen yang sama-sama ter-judge, neural kompetitif (MAP 0.557, hanya ~0.03 di bawah BM25). 3. **Faktor sekunder** (tetap relevan): deskripsi pemilik sangat pendek (median 23 kata) memberi sedikit konteks untuk embedding; MiniLM multilingual kurang terspesialisasi untuk nuansa Indonesia. **Implikasi metodologis**: untuk corpus yang di-pool lexical, melaporkan HANYA standard top-K akan menyesatkan tentang kemampuan model semantic. Karena itu kedua angka dilaporkan (§7.1a vs §7.1b). ### 8.3 Hybrid: Grid Search Alpha + Matinya Nilai Rerank Semantic Iterasi awal memakai `alpha=0.3` (70% weight MiniLM) dan hybrid terseret jatuh karena skor neural tertekan pooling bias. Grid search alpha ∈ [0, 1] step 0.1 di dua lensa, plus penghapusan plafon kandidat (rerank kini exhaustive atas semua dokumen, bukan top-50): - **Standard**: monoton naik sampai alpha=1.0 (murni BM25) — sebagian artefak GT lexical-pooled. - **Pool-restricted (fair)**: pada index deskripsi-only kurva datar dengan puncak 0.9; **pada index fielded kurva ikut monoton ke 1.0** (BM25 murni 0.6665 > campuran mana pun; neural-only 0.6268). Artinya: begitu representasi lexical diperkaya field judul/kecamatan/fasilitas, sinyal MiniLM tidak lagi menambah apa pun bahkan di lensa fair. Keputusan: `alpha=0.9` dipertahankan supaya hybrid tetap model pembanding yang berbeda dari BM25 (selisihnya 0.005 MAP pool, praktis nol), dan temuan "fielded indexing menghapus nilai tambah rerank MiniLM di corpus ini" dilaporkan apa adanya. Pelajaran yang bisa dipertahankan di sidang: semantic reranking bersinar saat ada vocabulary mismatch dalam skala besar; 227 dokumen pendek dengan metadata kaya bukan panggungnya. Mitigasi eval yang tersisa (kit sudah disiapkan, §6.2): - Pool GT union multi-model + human annotation — menghilangkan pooling bias sekaligus sirkularitas GT simulasi. ### 8.3c Learning-to-Rank: Fusion yang Dipelajari vs Tulisan Tangan Eksperimen `scripts/experiment_ltr.py`: logistic regression pointwise atas 900 pasangan berlabel, **9 fitur** (skor bm25/tfidf/neural, geo, price_fit, facility_overlap, gender_match, log view_count, log panjang deskripsi), validasi **GroupKFold 5-fold per query** (tidak ada query bocor antar fold), ranking dievaluasi pool-restricted: | Ranker | MAP pool (5-fold) | |---|---| | LTR (logreg, 9 fitur) | **0.9261** | | Smart fusion (manual, grid search) | 0.9069 | | BM25 | 0.6666 | Tiga temuan: 1. **LTR ≈ fusion manual** (selisih +0.019, Wilcoxon p=0.14, tidak signifikan): grid search dua-lensa kita sudah mendekati optimal; pada rezim data kecil, ML tidak otomatis menang atas aturan domain. 2. **Keduanya >> BM25** (p<0.0001): sinyal terstruktur (gender/harga/ fasilitas/geo) memang yang membawa lompatan, apa pun cara fusinya. 3. **Fitur terkuat yang dipelajari = gender_match**, persis hard filter Smart — sekaligus konfirmasi transparan bahwa label annotator simulasi berkorelasi dengan constraint (limitation §8.5 no. 6 terlihat langsung dari bobot model). Dibaca sebagai studi konsep LTR [14], bukan klaim produksi; ulangi dengan GT manusia saat tersedia. ### 8.3b Mengapa Smart Dipilih sebagai Model Live? Smart menjawab kelemahan praktis semua model murni-teks: query kos riil ("kos putri deket unila max 800rb ada wifi") membawa **constraint terstruktur** yang tidak terbaca BM25/TF-IDF sebagai constraint, hanya sebagai token. Smart memecah query: parser menarik gender/harga/fasilitas/ anchor; BM25 menangani sisa teks (setelah preprocessing yang sama dengan corpus); skor akhir = fusi teks + kedekatan geografis + kecocokan fasilitas; hard filter dengan relaxation bertahap menjamin hasil tidak kosong dan pelonggaran dilaporkan jujur ke user (chip "filter dilonggarkan"). Tiga detail desain penting: 1. **Geo-augment kandidat**: saat anchor terdeteksi, kandidat BM25 di-union dengan semua listing radius 3 km. Tanpa ini, listing dekat ITERA yang deskripsinya tidak menulis "itera" mustahil muncul (skor geo hanya bisa menata ulang kandidat yang ada, tidak bisa menambah). 2. **Filter UI = constraint eksplisit**: tidak pernah di-relax. Relaxation hanya untuk constraint hasil inferensi teks. 3. **Bobot fusion dari grid search, bukan tebakan**: simplex step 0.1 (55 kombinasi, `eval/smart_weights_grid.csv`) memilih **0.2 teks / 0.4 geo / 0.4 atribut** (menang dua lensa atas default awal 0.4/0.4/ 0.2, Wilcoxon p=0.023). Re-run pada index fielded menunjukkan plateau CS@5 0.867-0.873; kandidat "lebih baik" tidak signifikan (p=0.32) sehingga bobot TIDAK diganti lagi (anti hyperparameter churn). Kombinasi ber-CS tertinggi via atribut ekstrem (teks 0.1-0.2 / atribut 0.6-0.7) ditolak sadar: CS@5 memang menghitung atribut (overfit metric), dan metric kita buta terhadap kualitas urutan jarak DI DALAM radius — bobot geo dijaga tetap berarti demi UX "dekat X". Bukti kuantitatif: CS@5 0.867 vs BM25 0.527 (p=0.0001, r=0.91, §7.4); standard P@5 0.773 / MAP 0.359 juga tertinggi (raw-significant vs BM25, belum lolos koreksi keluarga). Biaya: nol model neural di runtime — aman untuk free tier 512MB. ### 8.4 Karakteristik Data Real vs Coverage - **Deskripsi terse**: median 23 kata (vs synthetic 110+). Menurunkan absolute IR metric tapi mencerminkan realita — pemilik kos menulis singkat ("dekat UNILA, jalan kaki 5 menit"). Sistem production harus handle ini. - **Distribusi kecamatan tidak merata**: Sukarame/Kedaton/Rajabasa dominan (dekat UNILA/UIN/ITERA/Polinela) karena memang di situ density kos Mamikos tertinggi. Mencerminkan pasar nyata, bukan artefak sampling. - **Koordinat real**: 226/227 listing punya lat/lng asli Mamikos → langsung dipakai untuk pin Google Maps di web app. 1 listing dengan koordinat korup (geocoding fallback ke Jakarta) di-null secara eksplisit. ### 8.5 Limitations 1. **Corpus size 227**: dibatasi ketersediaan listing Mamikos Bandar Lampung yang ter-index Google (~300-400 total) + success rate ekstraksi ~76%. 2. **Pooling bias**: GT di-pool dari BM25 → bias terhadap model lexical (dimitigasi sebagian via pool-restricted eval, belum via human/multi-model pool). 3. **AI-assisted GT**: heuristic scoring, bukan true human judgment. 4. **Sample size n=30 query** (dinaikkan dari 15): power lebih baik tapi tetap moderat; semua hasil non-signifikan dibaca inconclusive, bukan "setara". Hyperparameter (alpha, bobot fusion) dipilih pada data eval yang sama dengan yang dilaporkan (tanpa held-out set) — di-disclose sebagai mild selection bias. 5. **Single embedding model**: hanya MiniLM multilingual (fastembed ONNX); `indobert-base-p2` belum diuji (RAM constraint Render free tier; deploy HF Spaces 16GB membuka jalan untuk menguji ini). 6. **Sinyal annotator vs filter Smart tumpang tindih**: heuristik annotator simulasi menghukum mismatch gender/harga, yang juga merupakan hard filter Smart — angka pool-restricted Smart (MAP 0.893) karenanya optimistic; lensa CS@5 yang bebas qrels dilaporkan sebagai pengimbang, dan bobot LTR (§8.3c) memperlihatkan korelasi ini secara eksplisit. Kit anotasi manusia (§6.2) adalah penutup limitation ini. 7. **GT terikat versi index**: pool dibangun dari BM25 atas index aktif; regenerasi index (mis. fielded) meregenerasi pool + label secara deterministik. Konsisten internal, tapi perbandingan angka ANTAR iterasi laporan harus hati-hati (selalu disebut versi indexnya). --- ## 9. System Architecture ### 9.1 Stack | Layer | Tech | |-------|------| | Frontend | React 18 + Vite 5 + TypeScript — 4 tab: Pencarian, Evaluasi Model, Preprocessing (visualisasi 9-stage), Statistik | | Backend | Python 3.11 + FastAPI + Uvicorn | | Database | PostgreSQL 16 (asyncpg + SQLAlchemy 2.0 + Alembic) | | IR Libraries | Sastrawi, rank_bm25, fastembed (ONNX MiniLM), faiss-cpu, scikit-learn | | Deploy | Docker single-container: HF Spaces (16GB, neural ON) + Render.com backup (512MB, smart-only) | ### 9.2 API Endpoints | Method | Path | Description | |--------|------|-------------| | GET | `/health` | Liveness probe | | GET | `/api/search` | IR search dengan model & filter (`q`, `model`, `top_k`, `harga_min`, `harga_max`, `tipe`, `kecamatan`); filter berlaku untuk SEMUA model termasuk smart | | GET | `/api/listings/{id}` | Listing detail | | GET | `/api/eval/summary` | Aggregate eval (standard + pool-restricted + CS@5 + Wilcoxon/Holm), file-based dari CSV hasil eval | | GET | `/api/eval/query/{query_id}` | Per-query breakdown | | GET | `/api/preprocess` | Trace pipeline 9-stage per langkah (tab Preprocessing) | | GET | `/api/stats` | Statistik corpus live (distribusi kecamatan/tipe, harga, vocab) | | GET | `/api/status` | Diagnostics lengkap (index loaded, RAM, DB) | Hardening production: **rate limit per-IP 60 req/menit** untuk endpoint komputasi (429 + Retry-After; sliding window in-memory, X-Forwarded-For aware), **listing cache in-memory** di startup (menghapus SELECT lintas-region HF↔Supabase dari jalur panas pencarian; fallback DB kalau cache kosong), dan **Row Level Security** aktif di semua tabel Supabase (akses hanya via koneksi langsung aplikasi, jalur REST anon tertutup). | GET | `/api/docs` | Swagger UI | | GET | `/` (catch-all) | React SPA index.html | ### 9.3 Deployment Satu Dockerfile untuk dua platform (neural di-gate env `ENABLE_NEURAL`): - **HF Spaces** (primary demo) → `https://dymazeh-kozynear.hf.space` — CPU basic free 16GB RAM, `ENABLE_NEURAL=true`: kelima model live termasuk Neural MiniLM + Hybrid. Model ONNX di-pre-download saat build. - **Render.com** (backup) → `https://kozynear.onrender.com` — free 512MB, `ENABLE_NEURAL=false`: smart/BM25/TF-IDF. `kozynear-db` PostgreSQL managed dipakai kedua deploy. - Cold start: `seed_db --truncate --skip-if-synced` (skip reseed kalau DB sudah sinkron); GitHub Actions `keepalive.yml` ping /health tiap 12 menit supaya tidak spin-down saat demo. --- ## 10. Conclusion & Future Work ### 10.1 Conclusion KozyNear berhasil membangun end-to-end IR system khusus kos-kosan **seluruh Bandar Lampung** dengan corpus **227 listing REAL Mamikos** (deskripsi pemilik + koordinat asli) dan 5 model retrieval. Finding utama: 1. **Fielded indexing adalah perbaikan tunggal terbesar**: 21% deskripsi < 10 kata membuat field judul/kecamatan/fasilitas wajib diindex (BM25F-lite via repetisi judul). Semua model naik; query landmark yang tadinya mati ikut sembuh. 2. **Smart (query understanding + geo + BM25 fusion) sebagai model live**: P@5 0.773 / MAP 0.359 / NDCG 0.684 standard (tertinggi semua) dan CS@5 0.867 vs BM25 0.527 (p=0.0001, r=0.91, n=30). Kebutuhan terstruktur user (gender/budget/fasilitas/jarak kampus) tidak terbaca model murni-teks (semua mentok CS ~0.5), tanpa model neural di runtime. 3. **Neural MiniLM bukan buruk — pooling bias**: MAP melompat 0.044 → 0.627 saat pool-restricted. Namun setelah fielded indexing, rerank MiniLM tidak lagi menambah nilai bahkan di lensa fair (alpha optimal = 1.0); semantic reranking butuh vocabulary mismatch berskala yang tidak dimiliki corpus ini. 4. **Learned vs handcrafted fusion**: LTR (logreg 9 fitur, 5-fold per query) 0.926 vs fusion manual 0.907 (p=0.14, setara) — grid search manual sudah dekat optimal; keduanya jauh di atas BM25 (p<0.0001). 5. **Preprocessing diuji ablation, bukan diasumsikan**: hanya stopword removal yang menyumbang positif terukur; stemming nyaris nol; kamus jargon justru merugikan di sisi dokumen (perannya = query understanding). Pipeline 9-stage tetap dipertahankan dengan peran per-stage yang kini jujur terukur. 6. **Data real menuntut kejujuran**: deskripsi terse menurunkan absolute metric, PII pemilik dihapus dari distribusi, dan kit anotasi manusia multi-model pool disiapkan sebagai penutup limitation GT simulasi. ### 10.2 Rubric Coverage | Aspect | Weight | Coverage | |--------|--------|----------| | Theme | 15% | Kos search Bandar Lampung multi-universitas + integrasi Google Maps, under-represented di IR Indonesia | | Dataset | 15% | **227 listing REAL Mamikos** (scrape detail-page, koordinat asli), PII di-strip, etika data §3.4, methodology disclosed | | Preprocessing | 15% | 9-stage pipeline + 106 jargon entries (>100 req) + **ablation study per-stage** (§4.5) + audit coverage jargon + visualisasi live per-stage di tab Preprocessing | | Model | 20% | 5 model unified + **fielded indexing (BM25F-lite)** + honest comparison 3 lensa + **eksperimen LTR** + hyperparameter via grid search dua-lensa | | Evaluation | 10% | n=30 query, 900 annotations; P@K/MAP/NDCG/MRR + 95% CI bootstrap + Cohen's Kappa + Wilcoxon + **Holm-Bonferroni** + **effect size rank-biserial** + CS@5 lima model + dashboard eval live + kit anotasi manusia | | System | 25% | FastAPI + React 4-tab + Supabase PG (RLS aktif) + Docker; deploy ganda HF Spaces (neural ON) + Render backup + CI test + rate limit + listing cache + keepalive | ### 10.3 Future Work 1. **Human annotation pass** (kit SUDAH siap, §6.2): isi `eval/annotation_sheet.csv` oleh 1-3 manusia → jalankan ingest + re-eval. Menutup limitation terbesar (GT simulasi + pooling bias) dengan ~2-4 jam kerja per annotator. 2. **Perluas corpus real**: scrape kompetitor (Cove, Rukita, OLX) atau Google Places untuk listing di luar Mamikos → tembus >300 real. 3. **Embedding retrieval-tuned**: `multilingual-e5` (butuh prefix query:/passage:); fastembed 0.4.2 hanya menyediakan varian large (2.2GB) sehingga di luar budget iterasi ini. `indobert-base-p2` juga kandidat (feasible di HF Spaces 16GB). 4. **Jargon query-side expansion**: ablation menunjukkan substitusi jargon di dokumen merugikan; desain berikutnya menerapkan ekspansi (bukan substitusi) hanya di sisi query. 5. **Held-out query set**: hyperparameter dipilih di data eval yang sama (disclosed); pisahkan dev/test saat query set diperluas. 6. **LTR produksi**: ulangi §8.3c dengan GT manusia + fitur tambahan (rating, available_room); kalau konsisten menang, jadikan ranker live. --- ## 11. Repository Structure ``` KozyNear/ ├── LAPORAN.md # ini, single-doc report ├── README.md # quick start + project overview ├── Dockerfile # multi-stage build (Node + Python) ├── render.yaml # Render infra-as-code ├── docker-compose.yml # local PG dev ├── docs/ │ ├── architecture.md │ ├── deploy_guide.md │ └── ground_truth_protocol.md ├── data/ │ ├── raw/mamikos_real_v2.jsonl # 227 listing REAL Mamikos (committed) │ ├── raw/_discovered_slugs.txt # 312 URL discovery (committed) │ ├── processed/corpus.json # post-preprocessing (committed) │ └── indexes/ # TF-IDF + BM25 + IndoBERT (committed) ├── eval/ │ ├── queries.json # 15 test queries (v1.0 real-data) │ ├── ground_truth.csv # 900 consensus annotations │ ├── kappa_report.md # inter-annotator agreement │ ├── annotations_annotator_{A,B,C}.csv │ ├── results.csv # per-model per-query (standard) │ ├── results_pool_restricted.csv # pool-restricted (fair) eval │ ├── results.withsynth.csv # arsip: eval era synthetic (perbandingan) │ ├── _audit_dataset.py # data quality audit (reusable) │ └── eval_summary.md # aggregate + Wilcoxon + analysis ├── backend/ │ ├── app/ # FastAPI app │ ├── alembic/ # DB migrations │ ├── scripts/ # generate, preprocess, gen_GT, eval_report │ └── tests/ # pytest suite └── frontend/ └── src/ ├── api/client.ts # typed API client ├── components/ # FilterPanel, ResultCard └── App.tsx # main SPA ``` --- ## 12. Acknowledgments Project ini didevelop dengan AI agent assistance (Claude) sebagai implementation executor, dengan user sebagai project manager / reviewer. Methodology disclosure jujur tentang scrape data real Mamikos, pooling bias pada GT yang AI-assisted, dan keputusan drop synthetic demi keaslian data — untuk maintain integrity research findings. Source code + data public di GitHub untuk reproducibility. --- ## 13. Daftar Pustaka 1. S. Robertson dan H. Zaragoza, "The Probabilistic Relevance Framework: BM25 and Beyond," *Foundations and Trends in Information Retrieval*, vol. 3, no. 4, hlm. 333-389, 2009. (BM25; varian fielded BM25F) 2. S. E. Robertson, S. Walker, S. Jones, M. Hancock-Beaulieu, dan M. Gatford, "Okapi at TREC-3," *Proc. TREC-3*, 1994. 3. K. Sparck Jones, "A Statistical Interpretation of Term Specificity and Its Application in Retrieval," *Journal of Documentation*, vol. 28, no. 1, hlm. 11-21, 1972. (IDF) 4. G. Salton dan C. Buckley, "Term-Weighting Approaches in Automatic Text Retrieval," *Information Processing & Management*, vol. 24, no. 5, hlm. 513-523, 1988. (TF-IDF) 5. C. D. Manning, P. Raghavan, dan H. Schütze, *Introduction to Information Retrieval*. Cambridge University Press, 2008. (Evaluasi IR, pooling, preprocessing) 6. J. Zobel, "How Reliable Are the Results of Large-Scale Information Retrieval Experiments?" *Proc. SIGIR*, 1998. (Reliabilitas pooling) 7. N. Reimers dan I. Gurevych, "Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks," *Proc. EMNLP*, 2019. 8. W. Wang, F. Wei, L. Dong, H. Bao, N. Yang, dan M. Zhou, "MiniLM: Deep Self-Attention Distillation for Task-Agnostic Compression of Pre-Trained Transformers," *Proc. NeurIPS*, 2020. 9. J. Johnson, M. Douze, dan H. Jégou, "Billion-Scale Similarity Search with GPUs," *IEEE Transactions on Big Data*, vol. 7, no. 3, 2019. (FAISS) 10. B. Nazief dan M. Adriani, "Confix-Stripping: Approach to Stemming Algorithm for Bahasa Indonesia," Universitas Indonesia, 1996. (Dasar stemmer Sastrawi) 11. J. Asian, H. E. Williams, dan S. M. M. Tahaghoghi, "Stemming Indonesian," *Proc. ACSC*, 2005. 12. F. Wilcoxon, "Individual Comparisons by Ranking Methods," *Biometrics Bulletin*, vol. 1, no. 6, hlm. 80-83, 1945. 13. S. Holm, "A Simple Sequentially Rejective Multiple Test Procedure," *Scandinavian Journal of Statistics*, vol. 6, no. 2, hlm. 65-70, 1979. 14. T.-Y. Liu, "Learning to Rank for Information Retrieval," *Foundations and Trends in Information Retrieval*, vol. 3, no. 3, hlm. 225-331, 2009. 15. J. Cohen, "A Coefficient of Agreement for Nominal Scales," *Educational and Psychological Measurement*, vol. 20, no. 1, hlm. 37-46, 1960. (Cohen's Kappa) 16. B. Efron, "Bootstrap Methods: Another Look at the Jackknife," *The Annals of Statistics*, vol. 7, no. 1, hlm. 1-26, 1979. (Bootstrap CI) 17. D. S. Kerby, "The Simple Difference Formula: An Approach to Teaching Nonparametric Correlation," *Comprehensive Psychology*, vol. 3, 2014. (Rank-biserial effect size) --- **Submitted**: 17 Juni 2026 (vClass) — *placeholder, submission setelah final review* **License**: MIT (lihat `LICENSE`)