import os from pathlib import Path import chromadb from chromadb.utils import embedding_functions from pypdf import PdfReader # ===================================================== # KONFIGURASI PROGRAM # ===================================================== BASE_DIR = Path(__file__).resolve().parent DB_PATH = str(BASE_DIR / "chroma_db") UKURAN_CHUNK = [500, 1000, 1500] OVERLAP_PERSEN = 0.10 MODEL_EMBEDDING = "paraphrase-multilingual-MiniLM-L12-v2" print("Memuat model embedding...") embedding = embedding_functions.SentenceTransformerEmbeddingFunction( model_name=MODEL_EMBEDDING ) client = chromadb.PersistentClient(path=DB_PATH) # ===================================================== # FUNGSI MEMBACA PDF # ===================================================== def ambil_file_pdf(): return list(BASE_DIR.glob("*.pdf")) def baca_pdf(path): reader = PdfReader(str(path)) teks = "" for halaman in reader.pages: teks += (halaman.extract_text() or "") + "\n" return teks def pilih_pdf(files): """Memilih satu PDF agar eksperimen konsisten.""" if not files: print("Belum ada file PDF!") print("Letakkan PDF di folder yang sama dengan file Python ini.") return None if len(files) == 1: return files[0] print("\nDaftar dokumen PDF:") for i, file in enumerate(files, 1): print(f"{i}. {file.name}") while True: pilihan = input("Pilih nomor PDF: ").strip() if pilihan.isdigit() and 1 <= int(pilihan) <= len(files): return files[int(pilihan) - 1] print("Pilihan tidak valid.") # ===================================================== # FUNGSI CHUNKING # ===================================================== def chunking(teks, ukuran=1000, overlap=100): if ukuran <= 0 or overlap < 0 or overlap >= ukuran: raise ValueError("Ukuran atau overlap tidak valid.") chunks = [] start = 0 while start < len(teks): akhir = min(start + ukuran, len(teks)) potongan = teks[start:akhir] if potongan.strip(): chunks.append(potongan) start += ukuran - overlap return chunks # ===================================================== # LAB 1 - EKSPERIMEN CHUNKING # ===================================================== def lab_chunking(): print("\n" + "=" * 60) print("LAB 1 - EKSPERIMEN CHUNKING") print("=" * 60) file = pilih_pdf(ambil_file_pdf()) if file is None: return teks = baca_pdf(file) print(f"\nDokumen : {file.name}") print(f"Panjang teks : {len(teks)} karakter") if not teks.strip(): print("Dokumen tidak memiliki teks yang dapat dibaca.") return for ukuran in UKURAN_CHUNK: for persentase in [0.10, 0.20]: overlap = int(ukuran * persentase) chunks = chunking(teks, ukuran, overlap) print("\n" + "-" * 55) print(f"Ukuran chunk : {ukuran}") print(f"Overlap : {overlap} ({persentase:.0%})") print(f"Jumlah chunk : {len(chunks)}") print("Tiga contoh chunk:") # Tampilkan maksimal tiga contoh chunk. # Pemilihan posisi dibuat tersebar agar tidak selalu awal dokumen. if len(chunks) <= 3: indeks = list(range(len(chunks))) else: indeks = [0, len(chunks) // 2, len(chunks) - 1] for nomor, idx in enumerate(indeks, 1): print(f"\nContoh {nomor} (Chunk {idx + 1}):") print(chunks[idx][:350]) print("\nEksperimen chunking selesai!") # ===================================================== # FUNGSI MEMBANGUN KOLEKSI # ===================================================== def bangun_koleksi(nama_koleksi, ukuran_chunk, file_pdf, reset=True): """Membangun koleksi ChromaDB dari satu dokumen PDF.""" teks = baca_pdf(file_pdf) if not teks.strip(): print("PDF kosong atau tidak dapat dibaca.") return None overlap = int(ukuran_chunk * OVERLAP_PERSEN) chunks = chunking(teks, ukuran_chunk, overlap) if reset: try: client.delete_collection(nama_koleksi) except Exception: pass collection = client.create_collection( name=nama_koleksi, embedding_function=embedding, metadata={"hnsw:space": "cosine"} ) ids = [f"chunk_{i}" for i in range(len(chunks))] metadatas = [ { "sumber": file_pdf.name, "nomor_chunk": i, "ukuran_chunk": ukuran_chunk } for i in range(len(chunks)) ] batch_size = 100 for start in range(0, len(chunks), batch_size): end = start + batch_size collection.upsert( ids=ids[start:end], documents=chunks[start:end], metadatas=metadatas[start:end] ) return collection # ===================================================== # LAB 2 - MEMBANGUN INDEKS CHROMADB # ===================================================== def lab_indexing(): print("\n" + "=" * 60) print("LAB 2 - INDEXING CHROMADB") print("=" * 60) file = pilih_pdf(ambil_file_pdf()) if file is None: return print(f"\nMembaca: {file.name}") collection = bangun_koleksi( nama_koleksi="rag_part1", ukuran_chunk=1000, file_pdf=file, reset=True ) if collection is None: return print("\nIndexing berhasil!") print("Ukuran chunk :", 1000) print("Total chunk :", collection.count()) print("Database :", DB_PATH) # ===================================================== # MENGAMBIL KOLEKSI CHROMADB # ===================================================== def ambil_koleksi(): try: return client.get_collection( name="rag_part1", embedding_function=embedding ) except Exception: print("\nDatabase belum tersedia!") print("Silakan jalankan Lab 2 terlebih dahulu.") return None # ===================================================== # FUNGSI PENCARIAN DOKUMEN # ===================================================== def cari_dokumen(pertanyaan, k=3, tampilkan=True, collection=None): if collection is None: collection = ambil_koleksi() if collection is None: return [] total = collection.count() if total == 0: print("Database masih kosong!") return [] k = min(k, total) hasil = collection.query( query_texts=[pertanyaan], n_results=k ) dokumen = hasil["documents"][0] metadata = hasil["metadatas"][0] jarak = hasil["distances"][0] if tampilkan: print("\nPertanyaan:", pertanyaan) print("\nHasil pencarian:") for i, doc in enumerate(dokumen): print("\n" + "-" * 45) print(f"HASIL KE-{i + 1}") print("-" * 45) print("Sumber:", metadata[i].get("sumber", "-")) print("Chunk:", metadata[i].get("nomor_chunk", "-")) print("Jarak:", round(jarak[i], 4)) print("Isi:") print(doc[:500]) return dokumen # ===================================================== # LAB 2 - PENCARIAN DOKUMEN # ===================================================== def lab_pencarian(): print("\n" + "=" * 60) print("LAB 2 - PENCARIAN DOKUMEN") print("=" * 60) collection = ambil_koleksi() if collection is None or collection.count() == 0: print("Silakan lakukan indexing terlebih dahulu.") return print("\nMasukkan pertanyaan untuk mencari informasi.") print("Ketik '0' untuk kembali ke menu.") while True: pertanyaan = input("\nPertanyaan: ").strip() if pertanyaan == "0": break if not pertanyaan: print("Pertanyaan tidak boleh kosong.") continue cari_dokumen(pertanyaan, k=3, collection=collection) # ===================================================== # LAB 2 - PENGUJIAN PARAFRASE # ===================================================== def lab_parafrase(): print("\n" + "=" * 60) print("LAB 2 - PENGUJIAN PARAFRASE") print("=" * 60) collection = ambil_koleksi() if collection is None or collection.count() == 0: print("Silakan lakukan indexing terlebih dahulu.") return pertanyaan_asli = input("\nPertanyaan asli: ").strip() if not pertanyaan_asli: print("Pertanyaan tidak boleh kosong.") return pertanyaan_parafrase = input("Pertanyaan parafrase: ").strip() if not pertanyaan_parafrase: print("Parafrase tidak boleh kosong.") return print("\n--- HASIL PERTANYAAN ASLI ---") cari_dokumen(pertanyaan_asli, k=3, collection=collection) print("\n--- HASIL PERTANYAAN PARAFRASE ---") cari_dokumen(pertanyaan_parafrase, k=3, collection=collection) # ===================================================== # LAB 3 - EVALUASI HIT@K # ===================================================== def buat_koleksi_evaluasi(ukuran_chunk, file_pdf): nama_koleksi = f"rag_eval_{ukuran_chunk}" print(f"\nMembangun koleksi {nama_koleksi}...") collection = bangun_koleksi( nama_koleksi=nama_koleksi, ukuran_chunk=ukuran_chunk, file_pdf=file_pdf, reset=True ) if collection is not None: print("Indexing selesai.") print("Jumlah chunk:", collection.count()) return collection def hitung_hit_at_k(collection, data_uji, k): jumlah_hit = 0 total = len(data_uji) for nomor, (pertanyaan, keyword) in enumerate(data_uji, 1): jumlah_dokumen = min(k, collection.count()) hasil = collection.query( query_texts=[pertanyaan], n_results=jumlah_dokumen ) dokumen = hasil["documents"][0] # Hit jika minimal satu dari K dokumen teratas # memuat kata/frasa kunci jawaban. ditemukan = any( keyword.casefold() in teks.casefold() for teks in dokumen ) if ditemukan: jumlah_hit += 1 status = "HIT" if ditemukan else "MISS" print(f"{nomor:02d}. {status} | {pertanyaan}") skor = jumlah_hit / total if total else 0 return jumlah_hit, total, skor def lab_evaluasi(): print("\n" + "=" * 60) print("LAB 3 - EVALUASI HIT@K") print("=" * 60) print(""" Evaluasi menggunakan: - 15 pertanyaan uji - Dua koleksi: chunk 500 dan 1000 karakter - Overlap 10% - Hit@1, Hit@3, dan Hit@5 Masukkan kata/frasa yang benar-benar terdapat pada bagian dokumen yang menjawab pertanyaan. """) file_pdf = pilih_pdf(ambil_file_pdf()) if file_pdf is None: return data_uji = [] for i in range(15): print("\n" + "-" * 50) print(f"PERTANYAAN KE-{i + 1}") pertanyaan = input("Pertanyaan: ").strip() keyword = input("Kata kunci jawaban: ").strip() if not pertanyaan or not keyword: print("Pertanyaan dan kata kunci wajib diisi.") print("Evaluasi dibatalkan. Jalankan kembali menu 5.") return data_uji.append((pertanyaan, keyword)) # Bangun dua koleksi dari PDF yang sama. koleksi_500 = buat_koleksi_evaluasi(500, file_pdf) if koleksi_500 is None: return koleksi_1000 = buat_koleksi_evaluasi(1000, file_pdf) if koleksi_1000 is None: return koleksi = { "Chunk 500": koleksi_500, "Chunk 1000": koleksi_1000 } hasil_evaluasi = {} for nama, collection in koleksi.items(): print("\n" + "=" * 60) print(f"EVALUASI {nama}") print("=" * 60) hasil_evaluasi[nama] = {} for k in [1, 3, 5]: print(f"\n--- PENGUJIAN HIT@{k} ---") hit, total, skor = hitung_hit_at_k( collection, data_uji, k ) hasil_evaluasi[nama][k] = { "hit": hit, "total": total, "skor": skor } print(f"\nHit@{k} = {hit}/{total} = {skor:.2%}") # Tampilkan perbandingan. print("\n" + "=" * 68) print("TABEL PERBANDINGAN HASIL HIT@K") print("=" * 68) print( f"{'Ukuran Chunk':<18}" f"{'Hit@1':<16}" f"{'Hit@3':<16}" f"{'Hit@5':<16}" ) print("-" * 68) for nama in koleksi: h1 = hasil_evaluasi[nama][1]["skor"] h3 = hasil_evaluasi[nama][3]["skor"] h5 = hasil_evaluasi[nama][5]["skor"] print( f"{nama:<18}" f"{h1:<16.2%}" f"{h3:<16.2%}" f"{h5:<16.2%}" ) print("-" * 68) print(""" Catatan: Hit@K adalah proporsi pertanyaan yang memiliki minimal satu dokumen relevan pada K hasil teratas. Evaluasi ini menggunakan pencocokan kata/frasa kunci sederhana, bukan penilaian relevansi manual. Periksa kembali hasil MISS dan kecocokan keyword agar kesimpulan praktikum lebih akurat. """) print("Evaluasi Hit@K selesai!") # ===================================================== # MENU UTAMA # ===================================================== def main(): while True: print("\n") print("=" * 60) print(" RAG PART 1 SYSTEM") print("=" * 60) print("1. Lab 1 - Eksperimen Chunking") print("2. Lab 2 - Indexing ChromaDB") print("3. Lab 2 - Pencarian Dokumen") print("4. Lab 2 - Pengujian Parafrase") print("5. Lab 3 - Evaluasi Hit@K") print("6. Keluar") print("=" * 60) pilihan = input("Pilih menu (1-6): ").strip() if pilihan == "1": lab_chunking() elif pilihan == "2": lab_indexing() elif pilihan == "3": lab_pencarian() elif pilihan == "4": lab_parafrase() elif pilihan == "5": lab_evaluasi() elif pilihan == "6": print("\nProgram selesai. Terima kasih!") break else: print("\nPilihan tidak valid!") # ===================================================== # MENJALANKAN PROGRAM # ===================================================== if __name__ == "__main__": main()