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:

text
         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:

  1. tanpa_posisi: input tanpa angka posisi disimpan sebagai lexeme saja, ditampilkan terurut alfabetis. PostgreSQL tidak menebak posisi — posisi hanya ada kalau Anda (atau to_tsvector) menyertakannya.
  2. diurutkan_otomatis: input 'php:3 belajar:1' tetap valid; PostgreSQL mengurutkan ulang lexeme saat menyimpan sehingga hasilnya 'belajar':1 'php':3.
  3. posisi_diurutkan: daftar posisi '2,1' ikut dirapikan menjadi 1,2 — urutan penulisan posisi tidak dianggap makna.
  4. leksim_gabung: lexeme yang sama muncul dua kali digabung jadi satu entri dengan seluruh posisinya. Sebuah tsvector tidak pernah menyimpan lexeme kembar.
  5. dengan_bobot: label bobot menempel pada posisi, bukan pada lexeme — satu lexeme pun bisa membawa bobot berbeda di posisi berbeda, misalnya 'belajar':1A,2D. Label A paling berat dan D paling ringan; bobot baru berpengaruh saat skor relevansi dihitung, topik chapter Ranking Hasil Pencarian.
  6. tipe_data: konfirmasi bahwa hasil cast memang bertipe tsvector, 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:

OperatorNamaSyarat cocok
&ANDsemua lexeme harus ada di dokumen
|ORsalah satu lexeme sudah cukup
!NOTlexeme tersebut tidak boleh ada
<->FOLLOWED BYkedua lexeme berdampingan pada urutan tersebut
<N>jarakkedua lexeme tepat N posisi terpisah
( )kurungmengelompokkan 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:

text
   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:

text
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:

text
 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:

  1. Baris pertama: kueri 'php & postgresql' cocok (t) karena kedua lexeme ada; 'php & !postgresql' gagal karena postgresql justru ada; 'php & mysql' gagal karena mysql tidak ada. Evaluasi @@ mengikuti logika operator persis seperti tabel di atas.
  2. Baris kedua dan ketiga: operator frasa benar-benar memeriksa posisi. Dokumen 'belajar':1 'php':2 memenuhi belajar <-> php, tetapi 'php':1 'belajar':2 gagal karena urutannya terbalik, dan dokumen tanpa posisi sama sekali otomatis gagal — posisi adalah bahan bakar operator frasa.
  3. Baris keempat: 'running'::tsvector @@ 'run'::tsquery menghasilkan f. Cast literal tidak menormalkan apa pun: huruf besar tetap besar, tidak ada stemming, tidak ada pembuangan kata umum. running dan run adalah 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
sql
-- 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
sql
-- Dua kata tanpa operator penghubung tidak valid sebagai tsquery.
SELECT 'belajar php'::tsquery;

tsquery-literal.sql

Unduh berkas
sql
-- 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
sql
-- 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;