Pipeline di chapter sebelumnya menunjukkan dua tipe data yang bekerja secara diam-diam: tsvector menampung dokumen yang sudah dijadikan lexeme, dan tsquery menampung kueri pencarian beroperator. Keduanya bukan sekadar nama fungsi — keduanya tipe data PostgreSQL yang sungguhan, lengkap dengan bentuk tulisan teks yang bisa dibaca manusia, aturan validasinya, dan operator sendiri. Pada chapter ini kita membedah anatomi keduanya secara langsung: menulis nilai tsvector dan tsquery dari nol lewat cast literal, membaca posisi dan bobot yang tersimpan di dalamnya, lalu mencocokkan keduanya dengan operator @@. Pemahaman struktur ini menjadi modal untuk memahami fungsi konversi pada chapter berikutnya.
tsvector: Dokumen dalam Bentuk Lexeme #
Sebuah tsvector menyimpan daftar lexeme yang sudah terurut, dan untuk setiap lexeme dapat menyimpan dua hal tambahan: posisi token aslinya di dokumen (angka 1 sampai 16383) dan label bobot (A, B, C, atau D). Bentuk tulisannya ringkas: 'belajar':1,2 'php':3 dibaca sebagai “lexeme belajar berasal dari token posisi 1 dan 2, lexeme php dari token posisi 3”. Karena representasinya berupa teks, kita bisa membuat nilai tsvector langsung dengan cast:
-- tsvector tanpa posisi: hanya deretan lexeme yang sudah terurut.
SELECT 'belajar php postgresql'::tsvector AS tanpa_posisi;
-- Input tidak harus terurut; PostgreSQL mengurutkan lexeme saat menyimpan.
SELECT 'php:3 belajar:1'::tsvector AS diurutkan_otomatis;
-- Beberapa posisi untuk satu lexeme digabung, dan posisinya ikut diurutkan.
SELECT 'belajar:2,1 php:3'::tsvector AS posisi_diurutkan;
-- Lexeme yang sama ditulis dua kali digabung menjadi satu entri.
SELECT 'belajar:1 belajar:2'::tsvector AS leksim_gabung;
-- Setiap posisi boleh membawa label bobot dari A sampai D.
SELECT 'belajar:1A php:2B postgresql:3C'::tsvector AS dengan_bobot;
SELECT pg_typeof('belajar'::tsvector) AS tipe_data;Output yang Dihasilkan:
tanpa_posisi
------------------------------
'belajar' 'php' 'postgresql'
(1 row)
diurutkan_otomatis
---------------------
'belajar':1 'php':3
(1 row)
posisi_diurutkan
-----------------------
'belajar':1,2 'php':3
(1 row)
leksim_gabung
---------------
'belajar':1,2
(1 row)
dengan_bobot
---------------------------------------
'belajar':1A 'php':2B 'postgresql':3C
(1 row)
tipe_data
-----------
tsvector
(1 row)Enam hasil di atas memperlihatkan watak penyimpanan tsvector:
tanpa_posisi: input tanpa angka posisi disimpan sebagai lexeme saja, ditampilkan terurut alfabetis. PostgreSQL tidak menebak posisi — posisi hanya ada kalau Anda (atauto_tsvector) menyertakannya.diurutkan_otomatis: input'php:3 belajar:1'tetap valid; PostgreSQL mengurutkan ulang lexeme saat menyimpan sehingga hasilnya'belajar':1 'php':3.posisi_diurutkan: daftar posisi'2,1'ikut dirapikan menjadi1,2— urutan penulisan posisi tidak dianggap makna.leksim_gabung: lexeme yang sama muncul dua kali digabung jadi satu entri dengan seluruh posisinya. Sebuahtsvectortidak pernah menyimpan lexeme kembar.dengan_bobot: label bobot menempel pada posisi, bukan pada lexeme — satu lexeme pun bisa membawa bobot berbeda di posisi berbeda, misalnya'belajar':1A,2D. LabelApaling berat danDpaling ringan; bobot baru berpengaruh saat skor relevansi dihitung, topik chapter Ranking Hasil Pencarian.tipe_data: konfirmasi bahwa hasil cast memang bertipetsvector, bukan sekadar teks.
tsquery: Kueri Beroperator #
Kalau tsvector adalah dokumen, tsquery adalah pertanyaannya. Tipe ini menyimpan lexeme yang dihubungkan operator logika dan operator frasa. Daftar operatornya:
| Operator | Nama | Syarat cocok |
|---|---|---|
& | AND | semua lexeme harus ada di dokumen |
| | OR | salah satu lexeme sudah cukup |
! | NOT | lexeme tersebut tidak boleh ada |
<-> | FOLLOWED BY | kedua lexeme berdampingan pada urutan tersebut |
<N> | jarak | kedua lexeme tepat N posisi terpisah |
( ) | kurung | mengelompokkan bagian kueri |
Sama seperti tsvector, nilai tsquery dapat ditulis langsung lewat cast literal:
-- Operator AND: kedua lexeme harus ada di dokumen.
SELECT 'belajar & php'::tsquery AS operator_dan;
-- Operator OR: salah satu saja sudah cukup.
SELECT 'belajar | php'::tsquery AS operator_atau;
-- Operator NOT: lexeme tersebut justru tidak boleh ada.
SELECT '!belajar'::tsquery AS operator_bukan;
-- Frasa: "belajar" harus tepat diikuti "php" pada posisi berikutnya.
SELECT 'belajar <-> php'::tsquery AS frasa_diikuti;
-- Jarak: "php" harus berada tepat dua posisi setelah "belajar".
SELECT 'belajar <2> php'::tsquery AS frasa_jarak;
-- Kurung mengelompokkan operator; AND mengikat lebih kuat daripada OR.
SELECT 'belajar & (php | postgresql)'::tsquery AS dikelompokkan;
SELECT pg_typeof('belajar & php'::tsquery) AS tipe_data;Output yang Dihasilkan:
operator_dan
-------------------
'belajar' & 'php'
(1 row)
operator_atau
-------------------
'belajar' | 'php'
(1 row)
operator_bukan
----------------
!'belajar'
(1 row)
frasa_diikuti
---------------------
'belajar' <-> 'php'
(1 row)
frasa_jarak
---------------------
'belajar' <2> 'php'
(1 row)
dikelompokkan
--------------------------------------
'belajar' & ( 'php' | 'postgresql' )
(1 row)
tipe_data
-----------
tsquery
(1 row)Bentuk tampilannya mengikuti operator yang dipakai: 'belajar' & 'php', !'belajar', sampai 'belajar' <-> 'php'. Kurung pada dikelompokkan dipertahankan di tampilan sehingga struktur logika kueri tetap terbaca — dan seperti pada ekspresi boolean biasa, & mengikat lebih kuat daripada |, sehingga a | b & c dibaca a | (b & c). Satu aturan tegas yang perlu diingat: dua kata tidak boleh berdiri bersebelahan tanpa operator penghubung. Pelanggaran aturan ini ditolak keras oleh parser:
-- Dua kata tanpa operator penghubung tidak valid sebagai tsquery.
SELECT 'belajar php'::tsquery;Output yang Dihasilkan:
psql:tsquery-error.sql:2: ERROR: syntax error in tsquery: "belajar php"
LINE 1: SELECT 'belajar php'::tsquery;
^Error syntax error in tsquery ini akan sering Anda temui kalau teks pengguna langsung dilempar ke tsquery. Pengguna nyata memang mengetik belajar php tanpa & — dan chapter berikutnya memperkenalkan fungsi-fungsi yang mengubah teks polos seperti itu menjadi tsquery yang valid.
Operator @@ dan || #
Operator utama yang menghubungkan kedua tipe data adalah @@, yang mengembalikan true bila dokumen memenuhi syarat kueri. Operator kedua yang berguna adalah ||, yang menggabungkan dua tsvector menjadi satu dokumen:
-- Operator @@ mengevaluasi sebuah tsquery terhadap sebuah tsvector.
SELECT 'belajar php postgresql'::tsvector @@ 'php & postgresql'::tsquery AS keduanya_ada,
'belajar php postgresql'::tsvector @@ 'php & !postgresql'::tsquery AS php_tanpa_postgresql,
'belajar php postgresql'::tsvector @@ 'php & mysql'::tsquery AS bersama_mysql;
-- Frasa: posisi kedua lexeme harus berdampingan dan berurutan.
SELECT 'belajar:1 php:2 postgresql:3'::tsvector @@ 'belajar <-> php'::tsquery AS urutan_benar,
'php:1 belajar:2 postgresql:3'::tsvector @@ 'belajar <-> php'::tsquery AS urutan_terbalik,
'belajar php postgresql'::tsvector @@ 'belajar <-> php'::tsquery AS tanpa_posisi;
-- Jarak: "php" harus berada tepat dua posisi setelah "belajar".
SELECT 'belajar:1 kita:2 php:3'::tsvector @@ 'belajar <2> php'::tsquery AS jarak_tepat,
'belajar:1 php:2'::tsvector @@ 'belajar <2> php'::tsquery AS jarak_meleset;
-- Cast literal tidak melakukan normalisasi: "run" tidak menemukan "running".
SELECT 'running'::tsvector @@ 'run'::tsquery AS tanpa_normalisasi;
-- Operator || menggabungkan dua tsvector menjadi satu dokumen.
SELECT 'belajar:1'::tsvector || 'php:1'::tsvector AS dokumen_gabung;Output yang Dihasilkan:
keduanya_ada | php_tanpa_postgresql | bersama_mysql
--------------+----------------------+---------------
t | f | f
(1 row)
urutan_benar | urutan_terbalik | tanpa_posisi
--------------+-----------------+--------------
t | f | f
(1 row)
jarak_tepat | jarak_meleset
-------------+---------------
t | f
(1 row)
tanpa_normalisasi
-------------------
f
(1 row)
dokumen_gabung
---------------------
'belajar':1 'php':2
(1 row)Perhatikan tiga kelompok hasil yang paling mengajarkan:
- Baris pertama: kueri
'php & postgresql'cocok (t) karena kedua lexeme ada;'php & !postgresql'gagal karenapostgresqljustru ada;'php & mysql'gagal karenamysqltidak ada. Evaluasi@@mengikuti logika operator persis seperti tabel di atas. - Baris kedua dan ketiga: operator frasa benar-benar memeriksa posisi. Dokumen
'belajar':1 'php':2memenuhibelajar <-> php, tetapi'php':1 'belajar':2gagal karena urutannya terbalik, dan dokumen tanpa posisi sama sekali otomatis gagal — posisi adalah bahan bakar operator frasa. - Baris keempat:
'running'::tsvector @@ 'run'::tsquerymenghasilkanf. Cast literal tidak menormalkan apa pun: huruf besar tetap besar, tidak ada stemming, tidak ada pembuangan kata umum.runningdanrunadalah dua lexeme yang berbeda.
Operator || menutup demo ini dengan pola yang akan sering muncul di dunia nyata: menggabungkan tsvector dari beberapa kolom — misalnya judul dan isi artikel — menjadi satu dokumen pencarian. Perhatikan bahwa posisi php pada dokumen_gabung bergeser menjadi 2: penggabungan merentangkan ulang posisi kedua vector agar tidak bertabrakan.
Dari Literal ke Teks Nyata #
Menulis tsvector dan tsquery secara manual memang cocok untuk eksperimen, tetapi dokumen nyata berupa kalimat panjang dan kueri nyata berupa teks yang diketik pengguna. Keduanya harus melalui fungsi konversi — to_tsvector dan to_tsquery — yang menerima konfigurasi bahasa sebagai penentu perilaku normalisasinya. PostgreSQL 17 mengirimkan puluhan konfigurasi siap pakai, dari simple, english, sampai indonesian; memilih konfigurasi yang tepat untuk teks campuran Indonesia-Inggris adalah topik pembuka chapter berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
operator-match.sql
Unduh berkas-- Operator @@ mengevaluasi sebuah tsquery terhadap sebuah tsvector.
SELECT 'belajar php postgresql'::tsvector @@ 'php & postgresql'::tsquery AS keduanya_ada,
'belajar php postgresql'::tsvector @@ 'php & !postgresql'::tsquery AS php_tanpa_postgresql,
'belajar php postgresql'::tsvector @@ 'php & mysql'::tsquery AS bersama_mysql;
-- Frasa: posisi kedua lexeme harus berdampingan dan berurutan.
SELECT 'belajar:1 php:2 postgresql:3'::tsvector @@ 'belajar <-> php'::tsquery AS urutan_benar,
'php:1 belajar:2 postgresql:3'::tsvector @@ 'belajar <-> php'::tsquery AS urutan_terbalik,
'belajar php postgresql'::tsvector @@ 'belajar <-> php'::tsquery AS tanpa_posisi;
-- Jarak: "php" harus berada tepat dua posisi setelah "belajar".
SELECT 'belajar:1 kita:2 php:3'::tsvector @@ 'belajar <2> php'::tsquery AS jarak_tepat,
'belajar:1 php:2'::tsvector @@ 'belajar <2> php'::tsquery AS jarak_meleset;
-- Cast literal tidak melakukan normalisasi: "run" tidak menemukan "running".
SELECT 'running'::tsvector @@ 'run'::tsquery AS tanpa_normalisasi;
-- Operator || menggabungkan dua tsvector menjadi satu dokumen.
SELECT 'belajar:1'::tsvector || 'php:1'::tsvector AS dokumen_gabung;
tsquery-error.sql
Unduh berkas-- Dua kata tanpa operator penghubung tidak valid sebagai tsquery.
SELECT 'belajar php'::tsquery;
tsquery-literal.sql
Unduh berkas-- Operator AND: kedua lexeme harus ada di dokumen.
SELECT 'belajar & php'::tsquery AS operator_dan;
-- Operator OR: salah satu saja sudah cukup.
SELECT 'belajar | php'::tsquery AS operator_atau;
-- Operator NOT: lexeme tersebut justru tidak boleh ada.
SELECT '!belajar'::tsquery AS operator_bukan;
-- Frasa: "belajar" harus tepat diikuti "php" pada posisi berikutnya.
SELECT 'belajar <-> php'::tsquery AS frasa_diikuti;
-- Jarak: "php" harus berada tepat dua posisi setelah "belajar".
SELECT 'belajar <2> php'::tsquery AS frasa_jarak;
-- Kurung mengelompokkan operator; AND mengikat lebih kuat daripada OR.
SELECT 'belajar & (php | postgresql)'::tsquery AS dikelompokkan;
SELECT pg_typeof('belajar & php'::tsquery) AS tipe_data;
tsvector-literal.sql
Unduh berkas-- tsvector tanpa posisi: hanya deretan lexeme yang sudah terurut.
SELECT 'belajar php postgresql'::tsvector AS tanpa_posisi;
-- Input tidak harus terurut; PostgreSQL mengurutkan lexeme saat menyimpan.
SELECT 'php:3 belajar:1'::tsvector AS diurutkan_otomatis;
-- Beberapa posisi untuk satu lexeme digabung, dan posisinya ikut diurutkan.
SELECT 'belajar:2,1 php:3'::tsvector AS posisi_diurutkan;
-- Lexeme yang sama ditulis dua kali digabung menjadi satu entri.
SELECT 'belajar:1 belajar:2'::tsvector AS leksim_gabung;
-- Setiap posisi boleh membawa label bobot dari A sampai D.
SELECT 'belajar:1A php:2B postgresql:3C'::tsvector AS dengan_bobot;
SELECT pg_typeof('belajar'::tsvector) AS tipe_data;