Pencarian Kemiripan dalam SQL

Operator jarak pgvector, query terparameter, dan pemeriksaan penggunaan indeks dengan EXPLAIN.

Daftar isi

Pencarian kemiripan pada pgvector ditulis sebagai pernyataan SELECT biasa yang mengurutkan baris menurut jarak vektor lalu mengambil k terbaik. Topik ini membahas operator jarak yang tersedia, interpretasi skor jarak cosine, penulisan query terparameter dari kode aplikasi, dan pemeriksaan penggunaan indeks dengan EXPLAIN.

Operator jarak dan fungsi padanannya#

pgvector menyediakan enam operator jarak yang dapat digunakan di dalam pernyataan SQL, ditambah fungsi dengan nama lengkap yang menghitung ukuran yang sama. Operator beroperasi pada ekspresi vektor, misalnya kolom embedding terhadap vektor query, dan menghasilkan bilangan pecahan yang maknanya bergantung pada operatornya. Tabel berikut merangkum keenam operator beserta fungsi padanannya.

OperatorUkuran yang dihasilkanFungsi padanan
<=>jarak cosinecosine_distance()
<->jarak L2 (Euclidean)l2_distance()
<#>inner product negatifinner_product()
<+>jarak L1l1_distance()
<~>jarak Hamming (tipe bit)tidak ada
<%>jarak Jaccard (tipe bit)tidak ada

Skor <=> disebut jarak cosine, yaitu satu dikurangi cosine similarity. Hubungan keduanya ditulis sebagai berikut, dengan qq vektor query dan xx vektor yang dibandingkan:

dcos(q,x)=1−simcos(q,x) d_{\text{cos}}(q, x) = 1 - \text{sim}_{\text{cos}}(q, x)

Jarak cosine bernilai 0 apabila dua vektor searah sempurna, mendekati 1 apabila keduanya saling tegak lurus, dan mendekati 2 apabila keduanya berlawanan arah. Nilai yang lebih kecil berarti lebih mirip, sehingga pengurutannya dilakukan menaik (ASC). Apabila aplikasi memerlukan skor yang lebih besar berarti lebih mirip, konversinya berbentuk satu dikurangi jarak.

Query kNN dengan ORDER BY dan LIMIT#

Bentuk pencarian terdekat (kNN) yang lazim mengurutkan seluruh baris menurut jaraknya lalu mengambil k terbaik dengan LIMIT:

sql Contoh: lima baris terdekat menurut jarak cosine
-- tidak-dijalankan
SELECT id, embedding <=> '[3,1,2]' AS jarak
FROM items
ORDER BY embedding <=> '[3,1,2]'
LIMIT 5;

LIMIT menentukan nilai k, dan nilai yang lazim berada pada rentang 5 hingga 20 sesuai kebutuhan antarmuka yang menampilkannya. Semakin besar k, semakin banyak pekerjaan yang dilakukan indeks atau pemindaian pada setiap query. Vektor query ditulis sebagai literal '[3,1,2]' dan dikonversi ke tipe vector secara otomatis. Tanpa indeks, pernyataan ini menjalankan pemindaian eksak yang menghitung jarak untuk setiap baris tabel, sehingga hasilnya benar tetapi berbiaya sebanding dengan jumlah baris. Dengan indeks HNSW atau IVFFlat yang operator class-nya cocok, rencana eksekusi berubah menjadi pencarian aproksimatif yang jauh lebih murah pada data besar. Konversi skor kemiripan dilakukan langsung di dalam daftar kolom:

sql Contoh: mengubah jarak cosine menjadi skor kemiripan
-- tidak-dijalankan
SELECT id, 1 - (embedding <=> '[3,1,2]') AS skor
FROM items
ORDER BY embedding <=> '[3,1,2]'
LIMIT 5;

Query terparameter dari kode aplikasi#

Query yang dikirim dari aplikasi ditulis terparameter, yaitu nilai dikirim terpisah dari teks SQL melalui placeholder driver. Pada psycopg, placeholder berbentuk %s, sehingga teks SQL dan daftar parameternya diserahkan sebagai dua bagian terpisah ke cursor. Contoh berikut memperlihatkan bentuk pemanggilannya:

python Contoh: query terparameter dengan placeholder %s pada psycopg
# tidak-dijalankan
# psycopg tidak tersedia di sandbox, contoh ini memperlihatkan bentuk pemanggilannya
sql = "SELECT id FROM items WHERE kategori_id = %s ORDER BY embedding <=> %s LIMIT %s"
cur.execute(sql, (2, "[3,1,2]", 5))

Nilai vektor query dikirim sebagai teks '[3,1,2]' dan dikonversi ke tipe vector oleh Postgres, sedangkan nilai filter dan LIMIT ikut terparameter. Pembagian ini menutup celah SQL injection karena masukan pengguna tidak pernah disambungkan ke dalam teks SQL. Kebiasaan yang sama diterapkan pada pernyataan lain yang menyertakan nilai masukan, sehingga seluruh lapisan akses data mengikuti satu pola yang sama. Pola itu juga berlaku pada driver lain dengan bentuk placeholder yang berbeda, misalnya $1 dan $2 pada klien yang mengikuti gaya protokol PostgreSQL.

Memeriksa penggunaan indeks dengan EXPLAIN#

Keyakinan bahwa indeks digunakan perlu diverifikasi dengan EXPLAIN ANALYZE, yaitu perintah Postgres yang menampilkan rencana eksekusi sekaligus menjalankan pernyataannya untuk mencatat waktu dan jumlah baris nyata. Contoh pemanggilannya berbentuk berikut:

sql Contoh: memeriksa rencana eksekusi pencarian terdekat
-- tidak-dijalankan
EXPLAIN ANALYZE
SELECT id FROM items
ORDER BY embedding <=> '[3,1,2]'
LIMIT 5;
text Ilustrasi hasil EXPLAIN ANALYZE dengan indeks HNSW
Limit  (cost=0.12..4.23 rows=5 width=16) (actual time=0.290..0.362 rows=5 loops=1)
  ->  Index Scan using idx_items_hnsw on items  (actual time=0.288..0.357 rows=5 loops=1)
        Order By: (embedding <=> '[3,1,2]'::vector)
Planning Time: 0.116 ms
Execution Time: 0.394 ms

Bentuk dan angka keluaran EXPLAIN berbeda antar versi Postgres, ukuran data, dan kehadiran indeks, sehingga bagian yang digunakan hanya bentuk nodenya. Node Index Scan using idx_items_hnsw menunjukkan indeks HNSW digunakan untuk menjawab pengurutan jaraknya. Apabila node yang muncul berupa Seq Scan, seluruh tabel dipindai, dan penyebab yang lazim berupa indeks yang belum dibuat, operator yang tidak cocok dengan operator class indeks, atau data yang masih kecil sehingga pemindaian berurutan dinilai planner lebih murah.

Perbandingan sebelum dan sesudah indeks dibuat memberi gambaran biaya yang nyata pada data masing-masing. Keluaran ANALYZE mencatat waktu eksekusi nyata per node, sehingga perbedaan latensi antara pemindaian eksak dan pencarian terindeks terlihat langsung. Pengukuran ini sebaiknya diulang pada ukuran data yang menyerupai produksi, karena planner dapat mengubah pilihannya mengikuti statistik tabel yang dimilikinya.

Kesalahan umum#

  • Mengurutkan jarak dengan DESC mengembalikan baris yang paling jauh terlebih dahulu, bukan yang paling mirip. Nilai jarak yang kecil berarti dekat, sehingga pencarian terdekat selalu diurutkan menaik. Perbaikannya, gunakan urutan menaik dan batasi dengan LIMIT.
  • Menafsirkan kolom jarak cosine sebagai skor kemiripan membuat laporan aplikasi terbalik maknanya, karena nilai 0 berarti paling mirip pada jarak, sedangkan nilai 1 berarti paling mirip pada skor. Perbaikannya, tetapkan bentuk yang digunakan aplikasi, lalu konversi dengan satu dikurangi jarak hanya di titik yang menampilkan skor.
  • Menyusun SQL dengan penyambungan string membuat query rentan SQL injection dan mempersulit penggunaan rencana eksekusi. Perbaikannya, gunakan placeholder driver untuk seluruh nilai masukan.
  • Membaca keluaran EXPLAIN tanpa opsi ANALYZE membuat perkiraan biaya disangka sebagai waktu nyata. Nilai cost pada rencana merupakan perkiraan planner, sedangkan waktu eksekusi nyata hanya muncul dengan ANALYZE. Perbaikannya, gunakan EXPLAIN ANALYZE untuk membandingkan latensi.

Ringkasan#

  • Operator jarak pgvector mencakup <=> cosine, <-> L2, <#> inner product negatif, <+> L1, <~> Hamming, dan <%> Jaccard, dengan fungsi padanan seperti cosine_distance() dan l2_distance().
  • Pencarian kNN berbentuk ORDER BY jarak diikuti LIMIT k, dan jarak cosine bernilai satu dikurangi cosine similarity sehingga urutannya menaik.
  • Query dari aplikasi ditulis terparameter melalui placeholder driver agar nilai tidak pernah disambungkan ke teks SQL.
  • EXPLAIN ANALYZE memperlihatkan node rencana eksekusi, dan kehadiran Index Scan menandakan indeks benar-benar digunakan.

Referensi#