Pencarian pakai LIKE '%kata%' memang enak ditulis: satu baris, langsung jalan. Tapi begitu tabel kamu berisi puluhan ribu baris, dia mulai tersedak karena PostgreSQL mesti scan semua baris satu per satu. Lebih parah lagi, LIKE nggak paham konsep kata — 'database' dan 'Database' dianggap dua hal berbeda, nggak ada skor relevansi, dan pengguna yang typo sedikit saja langsung dapat hasil kosong. PostgreSQL sebenarnya sudah menyimpan mesin full-text search bawaan: tsvector, tsquery, dan index GIN. Tiga komponen ini menyelesaikan masalah di atas tanpa perlu menjalankan Elasticsearch atau Meilisearch terpisah.
Siapkan Database dan Data Uji
Kita pakai PostgreSQL lewat Docker supaya bisa langsung dicoba tanpa instalasi manual. Kalau belum kenal Docker, ada panduan lengkapnya di Belajar Docker untuk Pemula. Jalankan container berikut:
docker run --name artikel-db \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=artikel_db \
-p 5432:5432 -d postgres:16
docker exec -it artikel-db psql -U postgres -d artikel_db
Setelah masuk ke psql, buat tabel dan isi beberapa baris data uji. Kalau koneksi Python ke PostgreSQL masih baru buat kamu, sempatkan baca Belajar PostgreSQL dengan Python dulu.
CREATE TABLE artikel (
id SERIAL PRIMARY KEY,
judul TEXT NOT NULL,
isi TEXT NOT NULL
);
INSERT INTO artikel (judul, isi) VALUES
('Belajar PostgreSQL dari Nol',
'PostgreSQL adalah database relasional open source yang kuat dan andal.'),
('Membuat REST API dengan FastAPI',
'FastAPI memudahkan pembuatan REST API dengan validasi otomatis.'),
('Full-Text Search di PostgreSQL',
'PostgreSQL punya mesin pencarian teks bawaan bernama full-text search.'),
('Pencarian Cepat dengan Index GIN',
'Index GIN mempercepat pencarian kata di dalam dokumen teks yang panjang.');
tsvector dan tsquery: Inti Full-Text Search
Dua fungsi ini jantungnya full-text search. to_tsvector() mengubah teks menjadi tsvector, yaitu representasi kata yang sudah dipecah, dinormalisasi ke huruf kecil, dan diberi posisi. to_tsquery() mengubah string pencarian menjadi tsquery yang siap dicocokkan. Lihat bedanya dengan konfigurasi 'english', yang sekalian menghapus stopword dan menyingkat kata ke bentuk dasar:
SELECT to_tsvector('english', 'The running dogs are running faster');
-- 'dog':3 'fast':6 'run':2,5
SELECT to_tsquery('english', 'running & dog');
-- 'run' & 'dog'
Operator @@ yang menjodohkan keduanya. Query berikut mencari baris yang isinya mengandung kata database, tanpa peduli besar-kecil huruf:
SELECT judul
FROM artikel
WHERE to_tsvector('simple', isi) @@ to_tsquery('simple', 'database');
-- Belajar PostgreSQL dari Nol
-- Full-Text Search di PostgreSQL
Ranking dengan ts_rank
Hasil pencarian jauh lebih berguna kalau diurutkan berdasarkan relevansi. ts_rank() menghitung skor dari seberapa sering kata kunci muncul dan seberapa dekat posisinya. Baris yang memuat database & relasional berdekatan akan dapat skor lebih tinggi:
SELECT judul,
ts_rank(to_tsvector('simple', isi),
to_tsquery('simple', 'database & relasional')) AS rank
FROM artikel
WHERE to_tsvector('simple', isi) @@ to_tsquery('simple', 'database & relasional')
ORDER BY rank DESC;
Supaya Cepat: Index GIN
Selama to_tsvector(isi) dihitung ulang setiap query jalan, PostgreSQL tetap harus membaca semua baris. Solusinya: simpan hasil tsvector ke kolom yang digenerate otomatis, lalu pasang index GIN di atasnya. Dengan begitu nilainya dihitung sekali saat baris ditulis, bukan setiap kali dicari.
Kolom GENERATED ALWAYS AS ... STORED otomatis ikut ter-update setiap kali isi berubah. Kamu nggak perlu repot bikin trigger untuk menjaga index tetap sinkron.
ALTER TABLE artikel
ADD COLUMN isi_tsv tsvector
GENERATED ALWAYS AS (to_tsvector('simple', isi)) STORED;
CREATE INDEX artikel_isi_tsv_idx ON artikel USING GIN (isi_tsv);
Query-nya sekarang tinggal memakai kolom itu, dan PostgreSQL memakai index untuk mencari tanpa scan penuh:
SELECT judul
FROM artikel
WHERE isi_tsv @@ to_tsquery('simple', 'database');
Pencarian Ramah Pengguna dengan websearch_to_tsquery
Input dari kotak pencarian biasanya kalimat bebas, bukan operator & atau |. websearch_to_tsquery() meniru sintaks mesin pencari: kata dipisah spasi diartikan AND, frasa dalam tanda kutip dicari berurutan, dan kata dengan tanda minus dikecualikan.
Untuk input pengguna, hampir selalu pakai websearch_to_tsquery(), bukan to_tsquery(). to_tsquery() menuntut sintaks khusus dan bisa error kalau pengguna mengetik kata biasa yang mengandung karakter tak dikenal.
SELECT websearch_to_tsquery('simple', 'database relasional');
-- 'database' & 'relasional'
SELECT websearch_to_tsquery('simple', '"index gin"');
-- 'index' <-> 'gin'
Highlight Hasil dengan ts_headline
Setelah dapat hasil yang relevan, tunjukkan di mana kata kuncinya muncul. ts_headline() mengembalikan cuplikan teks dengan kata yang cocok dibungkus tag <b>:
SELECT judul,
ts_headline('simple', isi, to_tsquery('simple', 'database')) AS cuplikan
FROM artikel
WHERE isi_tsv @@ to_tsquery('simple', 'database');
-- Belajar PostgreSQL dari Nol
-- PostgreSQL adalah <b>database</b> relasional open source yang kuat...
Catatan untuk Teks Bahasa Indonesia
Konfigurasi 'english' bagus untuk teks berbahasa Inggris karena punya daftar stopword dan stemmer. PostgreSQL tidak mengirim konfigurasi bahasa Indonesia, jadi untuk konten berbahasa Indonesia pakailah 'simple', yang hanya menurunkan huruf besar tanpa stemming. Konsekuensinya, belajar dan mempelajari dianggap dua kata berbeda.
Karena 'simple' tidak melakukan stemming, pencarian “belajar” tidak akan menemukan “mempelajari”. Kalau butuh pencocokan sebagian kata (prefix) atau toleransi typo, kombinasikan dengan ekstensi pg_trgm dan index GIN bertipe gin_trgm_ops.
Integrasi dengan Python (psycopg)
Terakhir, rakit semuanya di aplikasi Python. Contoh ini memakai psycopg versi 3, install dengan pip install "psycopg[binary]", lalu jalankan pencarian dari input pengguna. Perhatikan parameter %s yang mencegah SQL injection — bukan menyisipkan string secara langsung.
import psycopg
conn = psycopg.connect(
"postgresql://postgres:postgres@localhost:5432/artikel_db"
)
kata_kunci = input("Cari: ")
with conn.cursor() as cur:
cur.execute(
"""
SELECT judul,
ts_rank(isi_tsv, query) AS rank,
ts_headline('simple', isi, query) AS cuplikan
FROM artikel,
websearch_to_tsquery('simple', %s) AS query
WHERE isi_tsv @@ query
ORDER BY rank DESC
LIMIT 10
""",
(kata_kunci,),
)
for judul, rank, cuplikan in cur.fetchall():
print(f"{judul} (skor: {rank:.4f})")
print(f" {cuplikan}\n")
Langkah Berikutnya
Mesin full-text ini menangani pencarian kata kunci dengan sangat baik, tapi nggak paham maksud di balik kalimat. Kalau kamu butuh pencarian berdasarkan makna, gabungkan dengan vector search — panduannya ada di Vector Search di PostgreSQL dengan pgvector. Kombinasi full-text (kata kunci eksak) dan vector (kemiripan makna) adalah resep pencarian yang dipakai banyak aplikasi produksi. Mulai dari skema tabel di atas, pasang pg_trgm, lalu ukur beda kecepatan LIKE versus index GIN dengan EXPLAIN ANALYZE di data kamu sendiri.