Kotak pencarian hampir selalu hadir di aplikasi nyata: pembaca blog mengetik kata yang baru saja diingatnya, calon pembeli katalog toko mengetik nama produk secara asal. Di section sebelumnya kita memakai LIKE untuk mencocokkan pola karakter — dan untuk kebutuhan semacam itu, LIKE sering terasa terlalu sempit atau terlalu longgar. Chapter ini memperkenalkan pendekatan yang dipakai mesin pencari: full-text search, di mana PostgreSQL tidak lagi membandingkan pola karakter mentah, melainkan membandingkan kata yang telah dinormalkan ke bentuk akarnya. Kita akan melihat tiga titik kegagalan LIKE, memahami pipeline di balik full-text search, dan memposisikannya terhadap mesin pencari khusus seperti Elasticsearch.
Keterbatasan LIKE untuk Mencari Kata #
LIKE dan ILIKE membandingkan rangkaian karakter, bukan kata. Untuk fitur pencarian, perbedaan ini menimbulkan tiga masalah yang saling berkaitan:
- Bentuk kata tidak dikenali. Pola
'%belajar%'melewatkan artikel yang memuatPelajari,mengajar, ataumempelajari— kata-kata yang tulisannya berbeda tetapi berakar pada makna yang sama. - Pola yang dilonggarkan berbahaya. Mengubah pola menjadi
'%bel%'agar “lebih tangkap” justru memancing kata tak relevan sepertibeliau, tanpa menyelesaikan masalah pertama. - Tidak ada ukuran relevansi. Hasil
LIKEdatang dalam urutan fisik tabel; tidak ada skor yang membedakan artikel yang sangat membahas topik dari artikel yang hanya menyebutnya sekilas.
Sebagai bahan uji, kita memakai tabel artikel berisi delapan artikel blog teknis — kalimat Indonesia yang dicampur istilah Inggris seperti PHP, Docker, dan running, persis seperti konten website sungguhan:
-- Tabel artikel: korpus kecil untuk demo full-text search.
CREATE TABLE artikel (
id integer PRIMARY KEY,
judul text NOT NULL,
isi text NOT NULL
);
INSERT INTO artikel (id, judul, isi) VALUES
(1, 'Belajar PHP untuk Pemula',
'PHP adalah bahasa pemrograman server side yang populer. Di kelas pemula ini kita belajar variabel, function, dan array. Setiap materi disertai contoh kode yang bisa dijalankan langsung.'),
(2, 'Migrasi Database dari MySQL ke PostgreSQL',
'Migrasi skema dari MySQL ke PostgreSQL butuh persiapan. Pelajari perbedaan tipe data, konfigurasi transaction, dan fitur window function terlebih dahulu. Uji coba di server staging lebih dulu.'),
(3, 'Tips Menulis Query SQL yang Cepat',
'Query yang lambat biasanya bermula dari index yang kurang tepat. Belajar membaca execution plan, hindari SELECT *, dan ukur setiap perubahan. Profiling adalah kunci tuning database.'),
(4, 'Mengajar Data Science untuk Anak SMA',
'Program mengajar data science di sekolah menekankan praktik. Siswa mengolah data penjualan dengan spreadsheet lalu naik kelas ke SQL. Para guru mempelajari statistik dasar lebih dulu, lalu mengajarkan visualisasi data.'),
(5, 'Memilih Database untuk Aplikasi Startup',
'Setiap database punya kekuatan sendiri. PostgreSQL unggul di fitur JSONB dan full text search, sementara MySQL familiar di hosting murah. Sesuaikan pilihan dengan kebutuhan tim, bukan tren.'),
(6, 'Belajar DevOps: Docker dan CI/CD',
'Modul belajar DevOps membahas container Docker, pipeline CI/CD, dan monitoring. Pipeline berjalan otomatis setiap push, dengan jobs running in parallel untuk mempercepat feedback. Semua lab berjalan di virtual machine.'),
(7, 'Wawancara dengan Alumni Bootcamp',
'Beliau kini bekerja sebagai backend engineer di sebuah startup logistik. Cerita alumni bootcamp ini menyoroti disiplin latihan, portofolio yang rapi, dan kebiasaan menulis dokumentasi. Rekruter menilai konsistensi lebih dari sertifikat.'),
(8, 'Panduan Full Text Search di PostgreSQL',
'Full text search mencocokkan akar kata, bukan pola karakter. Katalog toko online yang menulis sepatu untuk berlari tetap ditemukan pencari sepatu lari. Kita akan belajar tsvector dan tsquery langkah demi langkah.');Output yang Dihasilkan:
CREATE TABLE
INSERT 0 8Sekarang kita uji dua pola ILIKE yang mewakili dua strategi pencarian kata. Strategi pertama memakai kata lengkap; strategi kedua sengaja memakai potongan kata agar lebih toleran terhadap variasi:
-- Percobaan 1: pola ketat, hanya menangkap kata "belajar" secara harfiah.
SELECT id, judul FROM artikel
WHERE judul || ' ' || isi ILIKE '%belajar%'
ORDER BY id;
-- Percobaan 2: pola dilonggarkan agar menangkap bentuk kata lain.
SELECT id, judul FROM artikel
WHERE judul || ' ' || isi ILIKE '%bel%'
ORDER BY id;Output yang Dihasilkan:
id | judul
----+----------------------------------------
1 | Belajar PHP untuk Pemula
3 | Tips Menulis Query SQL yang Cepat
6 | Belajar DevOps: Docker dan CI/CD
8 | Panduan Full Text Search di PostgreSQL
(4 rows)
id | judul
----+----------------------------------------
1 | Belajar PHP untuk Pemula
3 | Tips Menulis Query SQL yang Cepat
6 | Belajar DevOps: Docker dan CI/CD
7 | Wawancara dengan Alumni Bootcamp
8 | Panduan Full Text Search di PostgreSQL
(5 rows)Mari kita bedah dua hasil tersebut:
- Pola ketat
'%belajar%'menjaring empat artikel: id 1, 3, 6, dan 8. Dua artikel yang justru membahas proses belajar lolos dari jaring: id 2 memuat kataPelajari, sedangkan id 4 memuatmengajar,mempelajari, danmengajarkan. KataBelajardi judul id 3 hanya lolos karenaILIKEmengabaikan besar kecil huruf. - Pola longgar
'%bel%'hanya menambah satu baris: id 7, yang lolos karena kataBeliaumengandung potonganbel. Artikel wawancara alumni tersebut bukan materi belajar, jadi kecocokannya palsu — dan yang lebih mengganggu, id 2 serta id 4 masih tetap lolos. Pola karakter tidak punya cara mengetahui bahwabeliaudanbelajaradalah dua kata yang berbeda maknanya.
Dari String ke Lexeme #
Full-text search menyelesaikan masalah di atas dengan mengubah cara teks direpresentasikan sebelum dicocokkan. Tiga istilah menjadi fondasinya:
- Token: potongan teks hasil pemenggalan parser — kata, angka, URL, hingga tanda baca, masing-masing dikelompokkan menurut tipenya.
- Lexeme: bentuk kamus dari sebuah token; hasil normalisasi seperti penyeragaman huruf kecil dan pereduksian ke akar kata.
- Stemming: proses mereduksi bentuk terinfleksi ke akarnya —
runningmenjadirun,mengajardanbelajarsama-sama jatuh keajar.
Pipeline ini berjalan di dua sisi. Di sisi dokumen, seluruh teks kolom diubah menjadi kumpulan lexeme berikut posisinya. Di sisi pencari, kata kunci yang diketik pengguna mengalami normalisasi yang sama sehingga kedua sisi berbicara dalam “bahasa” yang identik. Kecocokan tidak lagi soal rangkaian karakter, melainkan soal lexeme yang sama — dan karena setiap lexeme tahu posisinya, PostgreSQL bisa menghitung seberapa relevan sebuah dokumen terhadap kueri.
Pipeline Full-Text Search #
Diagram berikut merangkum alur lengkapnya dari teks mentah sampai hasil terurut:
Dokumen lewat to_tsvector, kata kunci lewat to_tsquery, dan operator @@ memutuskan apakah keduanya cocok. Kita lihat pipeline itu bekerja — mulai dari contoh kecil berbahasa Inggris, lalu tabel artikel kita:
-- Tahap 1 dan 2 untuk teks Inggris: dokumen dan kueri menjadi lexeme.
SELECT to_tsvector('english', 'The cats are running') AS dokumen_inggris,
to_tsquery('english', 'cat & run') AS kueri_inggris;
-- Tahap 3: operator @@ membandingkan keduanya.
SELECT to_tsvector('english', 'The cats are running') @@ to_tsquery('english', 'cat & run') AS cocok;
-- Bahasa Indonesia: konfigurasi 'indonesian' mereduksi bentuk kata ke akar yang sama.
SELECT to_tsvector('indonesian', judul || ' ' || isi) AS dokumen
FROM artikel
WHERE id = 4;
SELECT to_tsquery('indonesian', 'belajar') AS kueri;
-- Pencarian pada seluruh tabel: kata kunci "belajar" menemukan bentuk-bentuk saudaranya.
SELECT id, judul FROM artikel
WHERE to_tsvector('indonesian', judul || ' ' || isi) @@ to_tsquery('indonesian', 'belajar')
ORDER BY id;Output yang Dihasilkan:
dokumen_inggris | kueri_inggris
-----------------+---------------
'cat':2 'run':4 | 'cat' & 'run'
(1 row)
cocok
-------
t
(1 row)
dokumen
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
'ajar':1,8,28,34 'anak':5 'dasar':30 'data':2,9,17,36 'dengan':19 'di':11 'dulu':32 'ekan':13 'guru':27 'jual':18 'ke':24 'kelas':23 'lalu':21,33 'lebih':31 'mengo':16 'naik':22 'para':26 'praktik':14 'program':7 'science':3,10 'seko':12 'siswa':15 'sma':6 'spreadsheet':20 'sql':25 'statistik':29 'untuk':4 'visualisasi':35
(1 row)
kueri
--------
'ajar'
(1 row)
id | judul
----+-------------------------------------------
1 | Belajar PHP untuk Pemula
2 | Migrasi Database dari MySQL ke PostgreSQL
3 | Tips Menulis Query SQL yang Cepat
4 | Mengajar Data Science untuk Anak SMA
6 | Belajar DevOps: Docker dan CI/CD
8 | Panduan Full Text Search di PostgreSQL
(6 rows)Empat hasil tersebut adalah inti konsep chapter ini:
- Baris pertama:
to_tsvector('english', 'The cats are running')menghasilkan'cat':2 'run':4. KataThedanaredibuang sebagai stopword bahasa Inggris,catsdireduksi menjadicat, danrunningmenjadirun. Angka di belakang titik dua adalah posisi token asli — kucing ada di kata kedua, berlari di kata keempat. - Baris kedua: operator
@@mengembalikantkarena lexemecatdanrunyang dicari kueri memang ada di dokumen. Perhatikan bahwa kueri memakai bentukrundancatyang tidak pernah muncul utuh di teks asli — pencocokan terjadi di tingkat akar kata. - Baris ketiga: dokumen artikel id 4 memuat
'ajar':1,8,28,34. Empat posisi itu berasal dari kata-kata yang tulisannya berbeda-beda:Mengajardi judul (posisi 1), lalumengajar(8),mempelajari(28), danmengajarkan(34) di isi. Konfigurasiindonesianjuga menghasilkan reduksi yang kurang rapi pada beberapa kata —sekolahmenjadiseko,menekankanmenjadiekan— perilaku yang akan kita bahas jujur di chapter konfigurasi bahasa. - Baris keempat dan kelima:
to_tsquery('indonesian', 'belajar')menghasilkan lexeme'ajar'— sama dengan yang tersimpan di dokumen. Karena kedua sisi sudah dinormalkan, kueribelajarotomatis menemukan artikel berisimengajar.
Hasil akhirnya: enam artikel relevan (id 1, 2, 3, 4, 6, 8) — termasuk dua artikel yang lolos dari kedua pola ILIKE, dan tanpa artikel wawancara id 7 yang menipu pola '%bel%'.
Mesin Pencari di Dalam Database #
Membangun fitur pencarian sering dianggap identik dengan menginstal mesin pencari khusus seperti Elasticsearch. PostgreSQL menawarkan posisi tengah yang layak dipertimbangkan sebelum melangkah ke sana:
- Tanpa infrastruktur baru. Pipeline di atas berjalan di server PostgreSQL yang sudah Anda kelola; tidak ada cluster pencari terpisah yang harus dimonitor dan disinkronkan.
- Konsisten dengan transaksi. Hasil pencarian mengikuti aturan MVCC: baris yang baru di-
INSERTdalam transaksi Anda langsung bisa dicari di sesi yang sama, dan sesi lain belum melihatnya sampai transaksi di-commit. - Terintegrasi penuh dengan SQL. Kueri pencarian bisa di-
JOINdengan tabel lain, difilter tanggal, dibatasiLIMIT, dan diurutkan kolom apa pun — semuanya dalam satu query.
Batasannya juga jujur diakui: untuk korpus ratusan juta dokumen, pencarian faceted, atau aggregasi log skala besar, mesin pencari khusus tetap unggul. Untuk blog, dokumentasi produk, katalog toko, dan fitur “cari” pada umumnya aplikasi berbasis database, full-text search PostgreSQL lebih dari cukup — apalagi setelah kolom tsvector diindeks dengan GIN, topik chapter GIN Index untuk Pencarian
.
Sampai di sini, dua tipe data baru sudah berkali-kali muncul: tsvector untuk dokumen dan tsquery untuk kueri. Chapter berikutnya, tsvector dan tsquery, membedah struktur internal keduanya — bentuk tulisannya, posisi dan bobot pada tsvector, serta operator logika pada tsquery — sebelum kita melanjutkan ke fungsi konversinya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- Tabel artikel: korpus kecil untuk demo full-text search.
CREATE TABLE artikel (
id integer PRIMARY KEY,
judul text NOT NULL,
isi text NOT NULL
);
INSERT INTO artikel (id, judul, isi) VALUES
(1, 'Belajar PHP untuk Pemula',
'PHP adalah bahasa pemrograman server side yang populer. Di kelas pemula ini kita belajar variabel, function, dan array. Setiap materi disertai contoh kode yang bisa dijalankan langsung.'),
(2, 'Migrasi Database dari MySQL ke PostgreSQL',
'Migrasi skema dari MySQL ke PostgreSQL butuh persiapan. Pelajari perbedaan tipe data, konfigurasi transaction, dan fitur window function terlebih dahulu. Uji coba di server staging lebih dulu.'),
(3, 'Tips Menulis Query SQL yang Cepat',
'Query yang lambat biasanya bermula dari index yang kurang tepat. Belajar membaca execution plan, hindari SELECT *, dan ukur setiap perubahan. Profiling adalah kunci tuning database.'),
(4, 'Mengajar Data Science untuk Anak SMA',
'Program mengajar data science di sekolah menekankan praktik. Siswa mengolah data penjualan dengan spreadsheet lalu naik kelas ke SQL. Para guru mempelajari statistik dasar lebih dulu, lalu mengajarkan visualisasi data.'),
(5, 'Memilih Database untuk Aplikasi Startup',
'Setiap database punya kekuatan sendiri. PostgreSQL unggul di fitur JSONB dan full text search, sementara MySQL familiar di hosting murah. Sesuaikan pilihan dengan kebutuhan tim, bukan tren.'),
(6, 'Belajar DevOps: Docker dan CI/CD',
'Modul belajar DevOps membahas container Docker, pipeline CI/CD, dan monitoring. Pipeline berjalan otomatis setiap push, dengan jobs running in parallel untuk mempercepat feedback. Semua lab berjalan di virtual machine.'),
(7, 'Wawancara dengan Alumni Bootcamp',
'Beliau kini bekerja sebagai backend engineer di sebuah startup logistik. Cerita alumni bootcamp ini menyoroti disiplin latihan, portofolio yang rapi, dan kebiasaan menulis dokumentasi. Rekruter menilai konsistensi lebih dari sertifikat.'),
(8, 'Panduan Full Text Search di PostgreSQL',
'Full text search mencocokkan akar kata, bukan pola karakter. Katalog toko online yang menulis sepatu untuk berlari tetap ditemukan pencari sepatu lari. Kita akan belajar tsvector dan tsquery langkah demi langkah.');
fts-teaser.sql
Unduh berkas-- Tahap 1 dan 2 untuk teks Inggris: dokumen dan kueri menjadi lexeme.
SELECT to_tsvector('english', 'The cats are running') AS dokumen_inggris,
to_tsquery('english', 'cat & run') AS kueri_inggris;
-- Tahap 3: operator @@ membandingkan keduanya.
SELECT to_tsvector('english', 'The cats are running') @@ to_tsquery('english', 'cat & run') AS cocok;
-- Bahasa Indonesia: konfigurasi 'indonesian' mereduksi bentuk kata ke akar yang sama.
SELECT to_tsvector('indonesian', judul || ' ' || isi) AS dokumen
FROM artikel
WHERE id = 4;
SELECT to_tsquery('indonesian', 'belajar') AS kueri;
-- Pencarian pada seluruh tabel: kata kunci "belajar" menemukan bentuk-bentuk saudaranya.
SELECT id, judul FROM artikel
WHERE to_tsvector('indonesian', judul || ' ' || isi) @@ to_tsquery('indonesian', 'belajar')
ORDER BY id;
like-gagal.sql
Unduh berkas-- Percobaan 1: pola ketat, hanya menangkap kata "belajar" secara harfiah.
SELECT id, judul FROM artikel
WHERE judul || ' ' || isi ILIKE '%belajar%'
ORDER BY id;
-- Percobaan 2: pola dilonggarkan agar menangkap bentuk kata lain.
SELECT id, judul FROM artikel
WHERE judul || ' ' || isi ILIKE '%bel%'
ORDER BY id;