Pencarian di Luar Kecocokan Persis

Indeks full-text dan pengenalan indeks vektor untuk pencarian yang tidak persis.

Daftar isi

Pencarian dengan kecocokan persis hanya menemukan nilai properti yang identik, sedangkan banyak kebutuhan menuntut lebih dari itu, misalnya catatan yang memuat satu kata tertentu atau dokumen yang maknanya berdekatan. Topik ini membahas dua indeks pencarian pada Neo4j 2026.10, yaitu indeks full-text untuk kata yang tidak persis melalui prosedur db.index.fulltext.queryNodes(), dan indeks vektor untuk kemiripan semantik melalui prosedur db.index.vector.queryNodes(). Seluruh contoh bekerja pada delapan node :Catatan dan enam node :Dokumen milik topik ini, sehingga dataset modul berlabel :Artikel dan :Tag tidak tersentuh. Tabel perbandingannya tersedia pada bagian akhir.

Keterbatasan pencarian kecocokan persis#

Pencarian kecocokan persis membandingkan nilai properti secara utuh, baik kesamaan nilai, rentang, maupun awalan. Perbandingan semacam ini dipercepat oleh indeks rentang, tetapi bentuk pertanyaannya tetap kaku. Kueri WHERE a.slug = 'artikel-0001' menemukan satu node karena slug-nya identik, sedangkan predikat STARTS WITH hanya melayani pencarian dari sisi awal nilai. Nilai yang hanya berbeda satu huruf tidak pernah cocok, sehingga kata yang diejanya sedikit berbeda tidak pernah ditemukan.

Operator CONTAINS melonggarkan syarat itu dengan mencocokkan substring, yaitu rangkaian karakter yang berada di tengah nilai. Pencocokannya tetap literal, karena CONTAINS membedakan huruf besar-kecil, tidak memecah nilai menjadi kata, dan menuntut rangkaian karakternya muncul utuh. Saringan ini juga tidak dilayani oleh indeks rentang, sehingga setiap node kembali diperiksa satu per satu, dan seluruh hasil diperlakukan sama tanpa peringkat relevansi. Indeks full-text dan indeks vektor mengatasi keterbatasan tersebut dari dua arah yang berbeda, yaitu kata di dalam teks dan kedekatan makna.

Indeks full-text untuk pencarian kata#

Indeks full-text menyimpan kata demi kata dari properti string, bukan nilainya secara utuh. Pemecahannya dilakukan oleh analyzer, yaitu komponen yang memecah teks menjadi token pada indeks yang dibangun di atas Lucene. Analyzer baku standard-no-stop-words menurunkan seluruh token menjadi huruf kecil, sehingga pencarian tidak peka kapitalisasi. Setiap hasil membawa skor relevansi Lucene, dan satu indeks dapat mencakup beberapa label serta properti melalui daftar pada klausa ON EACH.

Sesi cypher-shell dibuka dengan perintah berikut.

bash
# tidak-dijalankan
# membuka sesi cypher-shell pada server sandbox, kata sandi diminta interaktif
cypher-shell -a bolt://localhost:7689 -u neo4j

Pernyataan berikut membuat indeks full-text catatan_teks_m6 atas properti judul pada label :Catatan, dengan IF NOT EXISTS agar aman dijalankan ulang.

cypher
CREATE FULLTEXT INDEX catatan_teks_m6 IF NOT EXISTS
FOR (c:Catatan) ON EACH [c.judul];
CALL db.awaitIndexes(60);
SHOW INDEXES YIELD name, type, state, entityType, labelsOrTypes, properties
WHERE name = 'catatan_teks_m6'
RETURN name, type, state, entityType, labelsOrTypes, properties;
text Keluaran
name, type, state, entityType, labelsOrTypes, properties
"catatan_teks_m6", "FULLTEXT", "ONLINE", "NODE", ["Catatan"], ["judul"]

Delapan node :Catatan berisi catatan belajar basis data, dibentuk dengan MERGE pada properti id yang tetap dan SET judul, sehingga idempoten (idempotent) dan bebas duplikat meskipun dijalankan berulang kali.

cypher
UNWIND [
  {id: 'c1', judul: 'Catatan: indeks mempercepat pencarian'},
  {id: 'c2', judul: 'Catatan: normalisasi tabel relasional'},
  {id: 'c3', judul: 'Catatan: penelusuran graf dan jalur'},
  {id: 'c4', judul: 'Catatan: cadangan dan pemulihan'},
  {id: 'c5', judul: 'Catatan: transaksi dan keandalan'},
  {id: 'c6', judul: 'Catatan: model data dokumen'},
  {id: 'c7', judul: 'Catatan: indeks vektor untuk kemiripan makna'},
  {id: 'c8', judul: 'Catatan: kueri dasar basis data graf'}
] AS baris
MERGE (c:Catatan {id: baris.id})
SET c.judul = baris.judul;
MATCH (c:Catatan) RETURN count(c) AS jumlah_catatan;
MATCH (c:Catatan) RETURN c.id AS id, c.judul AS judul ORDER BY id;
text Keluaran
jumlah_catatan
8
id, judul
"c1", "Catatan: indeks mempercepat pencarian"
"c2", "Catatan: normalisasi tabel relasional"
"c3", "Catatan: penelusuran graf dan jalur"
"c4", "Catatan: cadangan dan pemulihan"
"c5", "Catatan: transaksi dan keandalan"
"c6", "Catatan: model data dokumen"
"c7", "Catatan: indeks vektor untuk kemiripan makna"
"c8", "Catatan: kueri dasar basis data graf"

Pencarian dijalankan dengan db.index.fulltext.queryNodes() yang menerima nama indeks dan string kueri, lalu mengembalikan node dan score melalui YIELD. Beberapa kata dicari secara terpisah mengikuti sintaks Lucene.

cypher
CALL db.index.fulltext.queryNodes('catatan_teks_m6', 'indeks') YIELD node, score
RETURN node.id AS id, node.judul AS judul, score
ORDER BY score DESC, node.id
LIMIT 5;
text Keluaran
id, judul, score
"c1", "Catatan: indeks mempercepat pencarian", 0.6163140535354614
"c7", "Catatan: indeks vektor untuk kemiripan makna", 0.5191078782081604

Kedua hasil sama-sama memuat satu kata indeks, tetapi skornya berbeda karena Lucene menurunkan kontribusi kata pada teks yang lebih panjang, yaitu empat token pada c1 dibandingkan enam token pada c7. Kunci tambahan node.id pada ORDER BY menjaga urutan tetap apabila dua skor sama persis.

Kueri kedua mencari kata graf dengan pola yang sama.

cypher
CALL db.index.fulltext.queryNodes('catatan_teks_m6', 'graf') YIELD node, score
RETURN node.id AS id, node.judul AS judul, score
ORDER BY score DESC, node.id
LIMIT 5;
text Keluaran
id, judul, score
"c3", "Catatan: penelusuran graf dan jalur", 0.5635499358177185
"c8", "Catatan: kueri dasar basis data graf", 0.5191078782081604

Sejak Neo4j 2026.09, cara yang disarankan untuk menanyakan indeks full-text adalah klausa SEARCH pada Cypher 25. Karena itu, prosedur db.index.fulltext.queryNodes() ditandai deprecated (tidak disarankan lagi) sejak Neo4j 2026.10, meskipun masih berjalan penuh dan tetap digunakan pada topik ini.

Indeks vektor untuk kemiripan semantik#

Embedding adalah representasi numerik sebuah objek, misalnya teks, dalam bentuk deret bilangan yang disebut vektor, dan objek yang maknanya mirip diberi vektor dengan arah yang berdekatan sehingga kedekatan makna diukur dari jarak antarvektor. Pada penerapan nyata, embedding dihasilkan oleh model embedding melalui prosedur genai.vector.encode() yang menuntut akses API eksternal di luar lingkup sandbox, yaitu lingkungan latihan yang terisolasi, sehingga topik ini menetapkan vektor empat dimensi manual dengan nilai tetap yang setiap angkanya dapat diperiksa ulang.

Enam node :Dokumen berisi judul materi belajar, dengan dimensi pertama dan kedua menonjol pada tema graf dan kueri, sedangkan dimensi ketiga dan keempat menonjol pada tema jaringan dan statistika. Pernyataan pembentuknya diawali DETACH DELETE agar aman dijalankan ulang.

cypher
MATCH (d:Dokumen) DETACH DELETE d;
UNWIND [
  {judul: 'Pengantar basis data graf', vektor: [1.0, 0.75, 0.0, 0.0]},
  {judul: 'Menulis kueri Cypher', vektor: [0.75, 1.0, 0.0, 0.0]},
  {judul: 'Desain model graf', vektor: [1.0, 0.5, 0.0, 0.0]},
  {judul: 'Algoritma penelusuran graf', vektor: [0.875, 0.625, 0.25, 0.125]},
  {judul: 'Jaringan komputer', vektor: [0.25, 0.0, 1.0, 0.125]},
  {judul: 'Statistika dasar', vektor: [0.0, 0.125, 0.0, 1.0]}
] AS baris
CREATE (d:Dokumen {judul: baris.judul})
CALL db.create.setNodeVectorProperty(d, 'embedding', vector(baris.vektor, 4, FLOAT32))
RETURN count(d) AS jumlah_dokumen;
MATCH (d:Dokumen) RETURN d.judul AS judul, d.embedding AS embedding ORDER BY judul;
text Keluaran
jumlah_dokumen
6
judul, embedding
"Algoritma penelusuran graf", [0.875, 0.625, 0.25, 0.125]
"Desain model graf", [1.0, 0.5, 0.0, 0.0]
"Jaringan komputer", [0.25, 0.0, 1.0, 0.125]
"Menulis kueri Cypher", [0.75, 1.0, 0.0, 0.0]
"Pengantar basis data graf", [1.0, 0.75, 0.0, 0.0]
"Statistika dasar", [0.0, 0.125, 0.0, 1.0]

Fungsi vector() membangun nilai VECTOR dari sebuah daftar beserta dimensi dan tipe koordinatnya, sedangkan db.create.setNodeVectorProperty() menyimpannya lebih hemat ruang daripada SET biasa. Menyimpan embedding dengan SET d.embedding = [1.0, 0.75, 0.0, 0.0] juga dilayani oleh indeks vektor, tetapi properti bertipe VECTOR ditolak oleh format penyimpanan server ini, sehingga prosedur itulah yang digunakan. Pembacaan ulang embedding mengembalikan daftar yang nilainya identik karena setiap komponennya kelipatan 0.125 yang terwakili persis dalam float32, format bilangan pecahan 32 bit.

Indeks vektor dideklarasikan dengan CREATE VECTOR INDEX atas satu properti, dengan panjang vektor dan fungsi kemiripannya ditetapkan pada OPTIONS. Nilai vector.dimensions berupa 4 mengikuti panjang vektor yang tersimpan, dan vector.similarity_function berupa cosine mengukur kedekatan arah dua vektor. Pernyataan ini idempoten karena indeks yang sudah ada tidak dibuat ulang dan tidak memicu error, sebagaimana yang terjadi saat topik ini ditulis.

cypher
CREATE VECTOR INDEX dokumen_vektor_m6 IF NOT EXISTS
FOR (d:Dokumen) ON (d.embedding)
OPTIONS {indexConfig: {`vector.dimensions`: 4, `vector.similarity_function`: 'cosine'}};
CALL db.awaitIndexes(60);
SHOW INDEXES YIELD name, type, state, entityType, labelsOrTypes, properties
WHERE name = 'dokumen_vektor_m6'
RETURN name, type, state, entityType, labelsOrTypes, properties;
text Keluaran
name, type, state, entityType, labelsOrTypes, properties
"dokumen_vektor_m6", "VECTOR", "ONLINE", "NODE", ["Dokumen"], ["embedding"]

Pencarian tetangga terdekat dijalankan dengan db.index.vector.queryNodes() yang menerima nama indeks, jumlah hasil, dan vektor kueri. Vektor kueri berikut sama dengan vektor milik dokumen “Pengantar basis data graf”, sehingga urutan hasilnya mudah dinilai.

cypher
CALL db.index.vector.queryNodes('dokumen_vektor_m6', 3, vector([1.0, 0.75, 0.0, 0.0], 4, FLOAT32)) YIELD node, score
RETURN node.judul AS judul, score
ORDER BY score DESC;
text Keluaran
judul, score
"Pengantar basis data graf", 1.0
"Desain model graf", 0.9919350147247314
"Algoritma penelusuran graf", 0.9837877750396729

Dokumen yang vektornya sama dengan vektor kueri memperoleh skor 1.0, sedangkan dua hasil lain adalah judul bertema graf, dan “Jaringan komputer” serta “Statistika dasar” tersisih karena bobotnya berada di dimensi lain. Pencarian ini bersifat perkiraan (approximate nearest neighbor) karena indeksnya menggunakan struktur HNSW (hierarchical navigable small world), sehingga peringkat pada dataset besar dapat sedikit menyimpang. Prosedur db.index.vector.queryNodes() juga ditandai deprecated sejak Neo4j 2026.04 dengan pengganti klausa SEARCH, tetapi masih berjalan penuh pada versi ini.

Memilih indeks pencarian yang tepat#

Ketiga cara mencari pada modul ini melayani pertanyaan yang berbeda, sehingga pemilihannya mengikuti bentuk pertanyaannya. Tabel berikut merangkum perbedaannya.

AspekKecocokan persisFull-textVektor
Pertanyaan yang dijawabnilai yang sama atau dalam rentangkata tertentu di dalam tekskedekatan makna antarobjek
Struktur pendukungindeks rentangindeks Lucene bertokenindeks vektor berbasis HNSW
Peringkat hasiltidak diberikanskor relevansi Luceneskor kemiripan 0.0 sampai 1.0
Kueri khasWHERE a.slug = 'artikel-0001'queryNodes('catatan_teks_m6', 'indeks')queryNodes('dokumen_vektor_m6', 3, v)

Kecocokan persis digunakan untuk nilai yang berperan sebagai kunci, misalnya slug dan pengenal, serta rentang angka dan tanggal. Indeks full-text digunakan ketika kata yang dicari berada di dalam teks dan bentuk persisnya tidak diketahui, misalnya kata kunci yang diketik oleh pengguna pada kolom pencarian. Indeks vektor digunakan ketika yang dicari adalah kedekatan makna, misalnya dokumen yang sejenis dengan dokumen lain, meskipun tidak ada satu kata pun yang sama di antara keduanya.

Kesalahan umum#

Empat kesalahan berikut sering muncul saat topik ini dipraktikkan.

  1. Menyaring teks dengan CONTAINS padahal indeks full-text sudah dibuat. Kueri tersebut tetap memindai seluruh node tanpa menggunakan indeks dan tanpa menghasilkan skor. Perbaikannya adalah memindahkan pencarian ke db.index.fulltext.queryNodes().
  2. Properti yang dicari tidak terdaftar pada ON EACH. Prosedur pencarian berjalan tanpa error, tetapi node bersangkutan tidak pernah ditemukan karena teksnya tidak masuk ke indeks. Perbaikannya adalah mencocokkan daftar properti indeks dengan properti yang dicari, lalu membuat ulang indeks apabila daftar tersebut keliru.
  3. Panjang vektor kueri berbeda dengan vector.dimensions. Kueri ditolak oleh server dengan pesan error ketidaksesuaian dimensi, misalnya vektor tiga dimensi ke indeks empat dimensi. Perbaikannya adalah menyamakan panjang vektor kueri dengan vector.dimensions pada indeks.
  4. Nama indeks yang diberikan kepada prosedur pencarian tidak sejenis. db.index.vector.queryNodes() yang diberi indeks full-text ditolak dengan pesan error karena indeksnya bukan indeks vektor. Perbaikannya adalah menggunakan nama indeks yang sejenis dengan prosedurnya.

Ringkasan#

  • Indeks full-text memecah properti string menjadi token melalui analyzer, memberi skor relevansi Lucene pada setiap hasil, dan dicari melalui db.index.fulltext.queryNodes() sehingga kata tidak persis tetap ditemukan.
  • Indeks vektor mengindeks properti embedding dan dicari melalui db.index.vector.queryNodes() untuk tetangga terdekat menurut kemiripan cosinus, dengan vektor kueri sepanjang vector.dimensions.
  • Nilai VECTOR dibangun oleh fungsi vector() dan disimpan oleh db.create.setNodeVectorProperty() yang lebih hemat ruang, sedangkan CREATE VECTOR INDEX ... IF NOT EXISTS aman dijalankan ulang karena indeks yang sudah ada dilewatkan.
  • Kecocokan persis tepat untuk nilai berkunci, full-text untuk kata kunci di dalam teks, dan vektor untuk kedekatan makna, sedangkan skor prosedur yang berbeda tidak diperbandingkan langsung.

Referensi#