Membangun Pencarian Semantik di Browser dengan Transformers.js dan Embedding Kalimat

Membangun Pencarian Semantik di Browser dengan Transformers.js dan Embedding Kalimat

By Reggi, 07 Jun 2026

Pernahkah kamu mengirimkan bug ini? Seorang pengguna mengetik "laptop" di bilah pencarian, tapi hasilnya nol. Padahal, database punya lusinan artikel "notebook". Kata-katanya berbeda, tapi maknanya identik. Pencarian kata kunci melihat keduanya sebagai string yang tidak berhubungan.

Ini bukan kasus langka, justru ini inti dari keterbatasan pencocokan kata kunci. Sistem ini membandingkan karakter, bukan konsep. Ia tidak tahu bahwa "membeli" dan "membeli" menjelaskan tindakan yang sama, atau bahwa "mobil" dan "kendaraan" berarti hal yang sama, atau "pengiriman" dan "pengiriman" adalah masalah yang sama yang diutarakan dengan dua cara berbeda. Untungnya, kita bisa mengatasi ini dengan Building Semantic Search with Transformers.js and Sentence Embeddings.

Pencarian semantik memperbaiki ini dengan membandingkan makna. Dengan Transformers.js, kamu bisa membangunnya sepenuhnya di browser tanpa server, tanpa kunci API, dan tanpa infrastruktur backend. Artikel ini akan memandu kamu melalui seluruh proses: bagaimana sentence embedding bekerja, cara menghasilkannya, bagaimana cosine similarity menilai relevansi, dan cara menyatukan semuanya ke dalam aplikasi pencarian basis pengetahuan yang berfungsi.

Apa Itu Embedding Kalimat Sebenarnya?

Model transformer tidak bisa memproses teks mentah. Sebelum komputasi apa pun terjadi, sebuah kalimat harus diubah menjadi angka. Embedding adalah hasil dari konversi itu, sebuah kalimat yang direpresentasikan sebagai daftar nilai floating-point yang disebut vektor.

Properti kuncinya bukan hanya kalimat menjadi angka. Sentensi dengan makna yang serupa menjadi vektor yang secara geometris berdekatan di ruang vektor yang sama.

Model yang digunakan sepanjang tutorial ini, all-MiniLM-L6-v2, memetakan setiap kalimat ke titik dalam ruang vektor 384 dimensi. Model ini disetel secara halus pada dataset SNLI dan MultiNLI khusus untuk mempelajari properti geometris ini. Kalimat seperti "pengiriman cepat" dan "pilihan pengiriman murah" akan berakhir berdekatan. Sementara itu, "perbaikan mesin mobil" akan berakhir jauh dari keduanya.

Dimensi 384 tidak mudah dibaca manusia. Kamu tidak bisa melihat dimensi 47 dan mengatakan apa yang dikodekannya. Yang penting untuk pencarian bukanlah dimensi individual, melainkan jarak antara dua vektor. Jarak pendek berarti makna serupa. Jarak besar berarti tidak terkait.

Pooling dan Normalisasi

Model transformer mentah menghasilkan satu vektor per token. Setiap kata dan subword dalam sebuah kalimat mendapatkan vektornya sendiri. Untuk pencarian semantik, kamu butuh satu vektor per kalimat.

Mean pooling menangani ini dengan merata-ratakan semua vektor token, yang diberi bobot oleh attention mask, sehingga padding token tidak berkontribusi. Normalisasi kemudian mengubah skala hasilnya menjadi panjang unit (magnitude = 1), yang menyederhanakan perhitungan kesamaan yang dibahas di bagian berikutnya.

Di Transformers.js, keduanya terjadi secara otomatis ketika kamu meneruskan pooling: 'mean' dan normalize: true ke panggilan pipeline. Tanpa opsi ini, kamu akan mendapatkan embedding tingkat token, yang berguna untuk tugas seperti pengenalan entitas bernama, tetapi tidak untuk pencarian tingkat kalimat.

Pipeline Feature-Extraction Transformers.js

Tugas feature-extraction berbeda dari setiap pipeline Transformers.js lainnya. Tugas seperti sentiment-analysis mengembalikan output yang bisa dibaca manusia: label, skor, string. feature-extraction mengembalikan representasi vektor mentah yang dihitung secara internal oleh model. Kamu bekerja satu tingkat lebih rendah, mendapatkan angka-angka yang menjadi dasar semua tugas tingkat yang lebih tinggi.

jsx
import { pipeline } from '@xenova/transformers'; const extractor = await pipeline( 'feature-extraction', 'Xenova/all-MiniLM-L6-v2', { progress_callback: console.log } ); // downloads and initializes the model on first run // (browser caches it after that, so subsequent page loads are instant) const output = await extractor( 'Ini adalah kalimat contoh.', { pooling: 'mean', normalize: true } ); // You then call the extractor with a string and the two options that give you a single, normalized sentence vector const embedding = output.data; // .data converts it to a plain JavaScript array of 384 numbers you can work with directly

Memahami Output Tensor

Objek Tensor yang dikembalikan oleh feature-extraction memiliki tiga bidang yang perlu diketahui:

  • dims: [1, 384] untuk satu kalimat, [10, 384] untuk sepuluh kalimat dalam sebuah batch. Dimensi kedua selalu 384 untuk model ini.
  • type: 'float32', artinya setiap dari 384 nilai adalah angka floating-point 32-bit.
  • data: Berisi semua angka dalam urutan row-major. Untuk batch 3 kalimat, ini adalah array datar berisi 3 × 384 = 1.152 angka.

Kamu bisa menggunakan output.tolist() untuk mengonversi tensor menjadi array JavaScript bersarang, satu array dalam untuk setiap kalimat. output[0].data memberikan vektor untuk kalimat pertama sebagai array biasa berisi 384 angka.

Batching: Memproses Banyak Kalimat Sekaligus

Meneruskan array string ke extractor memproses semuanya dalam satu panggilan model. Ini secara signifikan lebih cepat daripada memanggil pipeline sekali per kalimat, karena transformer memproses semua input secara paralel dalam satu forward pass.

jsx
const outputs = await extractor( [ 'Saya ingin membeli laptop baru.', 'Di mana saya bisa mendapatkan mobil?', 'Pengiriman paket saya tertunda.', 'Bagaimana cara saya melakukan pembelian online?' ], { pooling: 'mean', normalize: true } ); // Instead of four separate extractor calls, one call handles all four sentences simultaneously // The transformer architecture is optimized for batched input, so the time it takes to embed 10 sentences together // is much closer to embedding 1 sentence than to embedding 10 individually

Batching adalah keputusan kinerja paling penting dalam sistem pencarian semantik. Saat mengindeks kumpulan 50 dokumen, satu panggilan batch jauh lebih cepat daripada 50 panggilan individual. Perbedaan ini akan berlipat ganda seiring bertambahnya ukuran kumpulan dokumen kamu.

Cosine Similarity: Matematika di Balik Pencarian

Setelah kamu memiliki vektor untuk dokumen dan vektor untuk query pencarian, kamu membutuhkan cara untuk mengukur seberapa mirip dua vektor tersebut. Itulah yang dilakukan oleh cosine similarity.

Cosine similarity mengukur sudut antara dua vektor. Skor 1.0 berarti vektor-vektor menunjuk ke arah yang sama (makna identik). Skor 0 berarti tidak berhubungan sama sekali. Karena kita menggunakan normalize: true saat menghasilkan embedding, kedua vektor sudah memiliki panjang unit (magnitude = 1), yang menyederhanakan rumus secara signifikan:

Cukup jumlahkan hasil perkalian elemen-demi-elemen dari kedua vektor. Angka itu adalah cosine similarity. Untuk sentence embedding dengan mean pooling dan normalisasi, skor praktis jatuh kira-kira dalam rentang ini:

Rentang SkorMakna Kesamaan
0.8 - 1.0Makna identik
0.6 - 0.8Sangat mirip
0.4 - 0.6Terkait
0.2 - 0.4Cukup terkait
0.0 - 0.2Tidak terkait

Berikut adalah implementasinya:

jsx
function cosineSimilarity(vec1, vec2) { let sum = 0; for (let i = 0; i < vec1.length; i++) { sum += vec1[i] * vec2[i]; } // The function loops through both 384-element vectors in parallel, multiplies corresponding values, and sums the results // That sum is the dot product, which equals cosine similarity when both vectors are normalized return Math.max(0, sum); // clamp at the end handles the rare case where floating-point arithmetic produces a value like -0.000000001 }

Membangun Kelas SemanticSearch

Pola untuk pencarian semantik selalu sama, terlepas dari skalanya: embed dokumen sekali saat startup, embed setiap query pada waktu pencarian, nilai setiap dokumen terhadap query, lalu urutkan berdasarkan skor.

Langkah yang mahal adalah menghasilkan vektor 384 angka untuk setiap kalimat. Menyimpan vektor tersebut di memori (caching) berarti pencarian berikutnya hanya perlu embed query, yang membutuhkan waktu milidetik.

  • indexDocuments(documents): Mengambil array objek dokumen (setiap objek minimal memiliki bidang text), embed semua teks dalam satu panggilan batch, dan menyimpan hasilnya di this.index. Ini mempertahankan metadata apa pun yang kamu teruskan, jadi tidak ada yang hilang.
  • search(query): Hanya embed query (satu panggilan inferensi, biasanya di bawah 100ms), kemudian menjalankan cosineSimilarity terhadap setiap vektor dokumen yang disimpan dalam loop JavaScript biasa. Tidak ada inferensi model lebih lanjut selama penilaian, itulah mengapa pencarian terasa instan setelah pengindeksan selesai.
  • Metode save() dan load(): Memungkinkan kamu mempertahankan indeks di antara muatan halaman, melewati langkah embedding sepenuhnya pada kunjungan berikutnya.

Demo: Pencarian Knowledge Base Berfungsi Penuh

Aplikasi di bawah ini lengkap dan mandiri. Salin ke file .html, buka di browser modern mana pun, dan itu akan berfungsi. Aplikasi ini menggunakan 12 entri FAQ dari basis pengetahuan dukungan e-commerce fiktif. Query contoh sengaja ditulis tanpa tumpang tindih kata kunci sama sekali dengan dokumen yang cocok untuk menunjukkan bahwa pencarian semantik benar-benar berfungsi.

Kamu bisa menemukan kode lengkapnya di sini: https://machinelearningmastery.com/building-semantic-search-with-transformers-js-and-sentence-embeddings/

Fungsi main() berjalan segera. Ini membuat pipeline feature-extraction dengan callback kemajuan yang memperbarui baris status selama pengunduhan model. Setelah model siap, indexDocuments menanamkan ke-12 artikel dalam satu panggilan batch dan menyimpan vektor dalam memori. Input pencarian dan tombol dinonaktifkan sampai langkah itu selesai, sehingga pengguna tidak bisa memicu pencarian di tengah pengindeksan.

Ketika pengguna mencari, search(query) hanya menanamkan query (satu panggilan inferensi, biasanya di bawah 100ms), kemudian mengulang ke-12 vektor dokumen yang di-cache, menghitung cosine similarity untuk setiapnya. Loop penilaian itu adalah aritmatika JavaScript murni tanpa melibatkan model, jadi selesai dalam waktu kurang dari satu milidetik. Hasilnya dirender diurutkan berdasarkan skor dengan badge persentase kecocokan berkode warna.

Contoh query menunjukkan kemampuan kunci. "Opsi pengiriman murah" mengembalikan "Pilihan Pengiriman Ekonomi" di bagian atas meskipun tidak berbagi kata kunci sama sekali.

Menjalankan Inferensi dalam Web Worker

Demo di atas menjalankan semua inferensi model di main browser thread. Untuk alat internal dan demo, ini tidak masalah. Untuk aplikasi produksi yang menghadap pengguna, ini tidak ideal: pemuatan model dan pembuatan embedding memblokir main thread, artinya scroll, input, dan animasi semuanya akan berhenti saat inferensi berjalan. Pada perangkat keras lama, browser mungkin menampilkan peringatan "halaman tidak responsif".

Web Workers memecahkan masalah ini dengan menjalankan JavaScript di background thread. Main thread tetap responsif sementara Worker menangani semua pekerjaan model.

  • Komunikasi main thread dan worker
    jsx
    const worker = new Worker('worker.js', { type: 'module' }); const promises = new Map(); let requestId = 0; worker.onmessage = (event) => { const { id, type, data } = event.data; if (type === 'progress') { console.log(data); // Update progress in UI } else if (type === 'result') { promises.get(id)?.resolve(data); promises.delete(id); } }; async function embedInWorker(sentences) { const id = requestId++; return new Promise((resolve, reject) => { promises.set(id, { resolve, reject }); worker.postMessage({ id, type: 'embed', data: sentences }); }); }
  • Komunikasi Worker
    jsx
    // worker.js import { pipeline } from '@xenova/transformers'; let extractor = null; self.onmessage = async (event) => { const { id, type, data } = event.data; if (type === 'embed') { if (!extractor) { extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { progress_callback: (status) => self.postMessage({ id, type: 'progress', data: status }) }); } const output = await extractor(data, { pooling: 'mean', normalize: true }); self.postMessage({ id, type: 'result', data: output.data }); } };
  • Worker menggunakan pola singleton (extractor membuat pipeline sekali dan mengembalikannya pada panggilan berikutnya) untuk menghindari pengunduhan ulang model jika beberapa pesan datang secara beruruk.
  • Bidang id pada setiap pesan adalah kunci korelasi: ketika Worker mengirim kembali result, main thread menggunakan id untuk menemukan Promise yang cocok di promises Map dan menyelesaikannya. Tanpa ini, jika dua permintaan embedding sedang dalam proses secara bersamaan, kamu tidak bisa mengetahui hasil mana milik permintaan mana.
  • promises Map tetap kecil (satu entri per permintaan yang sedang dalam proses) dan membersihkan dirinya sendiri saat respons tiba.

Mempertahankan Indeks Antar Muatan Halaman

Menghitung embedding adalah langkah yang lambat. Untuk kumpulan dokumen yang tidak berubah di antara kunjungan, kamu bisa melakukan serialize indeks ke JSON dan menyimpannya di localStorage, sehingga muatan halaman berikutnya melewati langkah embedding sepenuhnya.

localStorage menangani sekitar 5 MB, tergantung pada browser. Untuk 12 dokumen dengan vektor float 384 dimensi, indeks yang di-serialize kira-kira 200 KB, masih dalam batas. Untuk kumpulan dokumen yang lebih besar, IndexedDB tidak memiliki batasan ukuran praktis dan bekerja dengan cara yang sama dengan API yang sedikit lebih verbose.

Skala Melampaui Beberapa Ratus Dokumen

Pendekatan di atas menilai setiap dokumen per query. Itu berfungsi baik hingga beberapa ratus dokumen sebelum latensi mulai terlihat. Untuk kumpulan dokumen yang lebih besar, demo Transformers.js menjalankan instans PostgreSQL dalam browser dengan ekstensi pg_embedding untuk pencarian approximate nearest neighbor, yang secara signifikan lebih cepat daripada penilaian brute-force untuk koleksi besar, sementara semuanya tetap di sisi klien.

Memilih Model yang Tepat

Xenova/all-MiniLM-L6-v2 adalah default yang tepat untuk sebagian besar kasus penggunaan bahasa Inggris. Ini cepat, kecil, dan menghasilkan hasil yang kuat untuk pencarian semantik. Tabel di bawah ini mencakup opsi utama:

ModelDeskripsi
Xenova/all-MiniLM-L6-v2Cepat, kecil, hasil kuat untuk bahasa Inggris. Pilihan default terbaik.
Xenova/multilingual-e5-baseUntuk kasus penggunaan multibahasa (misal, basis pengetahuan Prancis, Jerman, dan Inggris secara bersamaan). Menangani query lintas bahasa.

Untuk kasus penggunaan multibahasa di mana basis pengetahuan memiliki konten dalam bahasa Prancis, Jerman, dan Inggris secara bersamaan, Xenova/multilingual-e5-base menangani query lintas bahasa. Pengguna yang mencari dalam bahasa Inggris akan memunculkan dokumen relevan yang ditulis dalam bahasa Prancis karena model memetakan makna yang setara ke vektor terdekat terlepas dari bahasanya.

Pipeline ini terdiri dari empat langkah: muat model sekali, embed kumpulan dokumen kamu dalam satu batch, embed setiap query pada waktu pencarian, nilai dengan cosine similarity, dan urutkan. Semua yang ada dalam tutorial ini berjalan dari satu import CDN tanpa server, tanpa kunci API, dan tanpa data meninggalkan perangkat pengguna.

Konsep inti yang sama, yaitu vektor, kesamaan, dan peringkat, juga merupakan dasar dari sistem rekomendasi, deteksi konten duplikat, clustering, dan retrieval-augmented generation. Masing-masing aplikasi tersebut dibangun di atas fungsi cosineSimilarity yang sama yang dibahas di sini. Mulai dengan demo basis pengetahuan, perluas kumpulan dokumen ke dokumen kamu sendiri, dan pola yang lebih canggih itu akan cepat masuk akal setelah kamu melihat dasar-dasarnya berfungsi.

Referensi

https://machinelearningmastery.com/building-semantic-search-with-transformers-js-and-sentence-embeddings/


🔥 Sedang Ramai Dibaca