Mengokohkannya

Indeks, rencana terverifikasi PROFILE, dan alur penyegaran data untuk proyek yang siap digunakan.

Daftar isi

Topik ini mengokohkan mesin rekomendasi yang berjalan di atas roster pembelajaran modul melalui tiga langkah, yaitu skema penjangkar identitas data, verifikasi rencana eksekusi, dan alur penyegaran data harian. Constraint keunikan pada properti nama dan indeks pada properti judul diperiksa keberadaannya, lalu kueri PROFILE membuktikan bahwa pencarian jangkar menembus indeks unik dengan akses basis data yang kecil. Batch penyegaran dijalankan sebagai upsert yang idempoten, kemudian pemeriksaan kesehatan memastikan tidak ada relasi yang kehilangan ujungnya. Setelah topik ini, mesin rekomendasi bertumpu pada data yang dijangkarkan oleh skema, rencana eksekusinya terukur, dan pembaruannya terkendali.

Constraint keunikan dan indeks#

Dua pernyataan skema berikut menjangkarkan identitas data rekomendasi. Constraint keunikan (uniqueness constraint) pengguna_nama_m8 melarang dua node :Pengguna menyimpan nilai nama yang sama, sehingga batch impor tidak dapat menciptakan pengguna kembar. Indeks kursus_judul_m8 berjenis range (rentang) atas properti judul dan mempercepat pencarian kursus menurut judulnya. Akhiran _m8 pada kedua nama menandai kepemilikan modul ini pada instance yang digunakan bersama oleh beberapa dataset.

Kedua pernyataan tersebut menggunakan klausa IF NOT EXISTS, sehingga eksekusi ulangnya aman: pernyataan diabaikan apabila skema yang setara sudah ada. Pada instance latihan, keduanya sudah terpasang sejak persiapan modul, sehingga eksekusi ulang dua pernyataan berikut tidak menambah apa pun dan tidak menghasilkan baris keluaran apa pun.

cypher
CREATE CONSTRAINT pengguna_nama_m8 IF NOT EXISTS
FOR (p:Pengguna) REQUIRE p.nama IS UNIQUE;
CREATE INDEX kursus_judul_m8 IF NOT EXISTS
FOR (k:Kursus) ON (k.judul);

Keberadaan keduanya diperiksa dengan SHOW yang saringannya membatasi keluaran pada dua nama tersebut, karena instance yang sama juga memuat indeks milik dataset lain.

cypher
SHOW CONSTRAINTS WHERE name IN ['pengguna_nama_m8', 'kursus_judul_m8'];
text Keluaran
id, name, type, entityType, labelsOrTypes, properties, enforcedLabel, ownedIndex, propertyType
8, "pengguna_nama_m8", "NODE_PROPERTY_UNIQUENESS", "NODE", ["Pengguna"], ["nama"], NULL, "pengguna_nama_m8", NULL
cypher
SHOW INDEXES WHERE name IN ['pengguna_nama_m8', 'kursus_judul_m8'];
text Keluaran
id, name, state, populationPercent, type, entityType, labelsOrTypes, properties, indexProvider, owningConstraint, lastRead, readCount
9, "kursus_judul_m8", "ONLINE", 100.0, "RANGE", "NODE", ["Kursus"], ["judul"], "range-1.0", NULL, 2026-11-04T13:07:51.026Z, 161
7, "pengguna_nama_m8", "ONLINE", 100.0, "RANGE", "NODE", ["Pengguna"], ["nama"], "range-1.0", "pengguna_nama_m8", 2026-11-04T13:07:57.813Z, 313

Keluaran SHOW CONSTRAINTS memuat satu baris dengan tipe NODE_PROPERTY_UNIQUENESS atas label Pengguna dan properti nama. Keluaran SHOW INDEXES memuat dua baris yang keduanya berstatus ONLINE dan berjenis RANGE. Baris kursus_judul_m8 berdiri sendiri, sedangkan baris pengguna_nama_m8 menunjukkan kolom owningConstraint yang terisi, artinya indeks itu milik constraint keunikannya. Struktur seperti ini membuat indeks tambahan atas nama tidak diperlukan.

Verifikasi rencana eksekusi dengan PROFILE#

Rencana eksekusi (query plan) adalah rangkaian operator yang dijalankan server untuk menjawab satu kueri. Awalan PROFILE membuat setiap operator melaporkan jumlah baris yang dihasilkan dan akses basis datanya (db hits), sehingga klaim tentang indeks dapat diverifikasi, bukan diduga. Kueri jangkar berikut mencari Hesti sebagai titik masuk rekomendasi, lalu menelusuri kursus yang diikutinya.

cypher
PROFILE MATCH (p:Pengguna {nama: 'Hesti'})-[:MENGIKUTI]->(k:Kursus)
RETURN k.judul AS judul;
text Keluaran
+----------------------------+
| judul                      |
+----------------------------+
| "Rekayasa Perangkat Lunak" |
| "Pemrograman Python"       |
| "Basis Data Graf"          |
| "Pengantar Algoritma"      |
+----------------------------+

+-------------------------------------------------------------------------------------------------+
| Plan      | Statement   | Version | Planner | Runtime   | Time | DbHits | Rows | Memory (Bytes) |
+-------------------------------------------------------------------------------------------------+
| "PROFILE" | "READ_ONLY" | "25"    | "COST"  | "SLOTTED" | 5    | 15     | 4    | 64             |
+-------------------------------------------------------------------------------------------------+


Cypher 25

Planner COST

Runtime SLOTTED

Runtime version 2026.10.0

+----------------------+----+----------------------------------------------------+----------------+------+---------+----------------+------------------------+---------------------+
| Operator             | Id | Details                                            | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Indexes Used        |
+----------------------+----+----------------------------------------------------+----------------+------+---------+----------------+------------------------+---------------------+
| +ProduceResults      |  0 | judul                                              |              0 |    4 |       0 |              0 |                    0/0 |                     |
| |                    +----+----------------------------------------------------+----------------+------+---------+----------------+------------------------+---------------------+
| +Projection          |  1 | k.judul AS judul                                   |              0 |    4 |       4 |                |                    0/0 |                     |
| |                    +----+----------------------------------------------------+----------------+------+---------+----------------+------------------------+---------------------+
| +Filter              |  2 | k:Kursus                                           |              0 |    4 |       4 |                |                    0/0 |                     |
| |                    +----+----------------------------------------------------+----------------+------+---------+----------------+------------------------+---------------------+
| +Expand(All)         |  3 | (p)-[:MENGIKUTI]->(k)                              |              0 |    4 |       5 |                |                    0/0 |                     |
| |                    +----+----------------------------------------------------+----------------+------+---------+----------------+------------------------+---------------------+
| +NodeUniqueIndexSeek |  4 | UNIQUE p:Pengguna(nama) WHERE nama = $autostring_0 |              0 |    1 |       2 |                |                    0/0 | pengguna_nama_m8: 1 |
+----------------------+----+----------------------------------------------------+----------------+------+---------+----------------+------------------------+---------------------+

Total database accesses: 15, total allocated memory: 64

4 rows
ready to start consuming query after 2 ms, results consumed after another 3 ms

Baris paling bawah pada tabel rencana memuat operator NodeUniqueIndexSeek dengan detail UNIQUE p:Pengguna(nama) WHERE nama = $autostring_0, kolom Indexes Used terisi pengguna_nama_m8, satu baris dihasilkan, dan dua akses basis data. Operator itulah bukti bahwa pencarian jangkar menembus indeks unik, bukan menyapu seluruh node berlabel Pengguna. Empat kursus Hesti dijaring oleh Expand(All) dengan lima akses basis data, sedangkan Filter, Projection, dan ProduceResults melengkapi sisanya. Total akses basis data kueri ini 15, dan biaya pencarian jangkarnya tetap konstan meskipun banyaknya pengguna pada instance bertambah.

Nilai Time pada ringkasan dan durasi pada baris akhir berubah pada setiap eksekusi, sehingga keduanya tidak dijadikan acuan. Ukuran yang stabil adalah kolom Rows, DB Hits, dan baris Total database accesses. Pada sesi interaktif cypher-shell, tabel rencana tercetak secara default, sedangkan eksekusi melalui pipa stdin memerlukan opsi --format verbose sebagaimana keluaran di atas.

Alur penyegaran data harian#

Data rekomendasi berubah setiap hari, dan perubahannya dimuat sebagai batch berisi baris-baris pembaruan. Pola yang digunakan adalah upsert, yaitu pembaruan apabila data sudah ada dan penyisipan apabila belum, sehingga satu batch dapat memuat progres yang berubah sekaligus pendaftaran yang baru. Percobaan pada bagian ini berangkat dari baseline kanonis, yaitu isi roster hasil skrip persiapan, dan skrip setup.cypher berikut membangunnya ulang karena pembersihannya hanya menyentuh roster milik modul ini.

cypher setup.cypher
// Dataset roster pembelajaran untuk topik Mengokohkan mesin rekomendasi.
// Roster sama dengan dataset kanonis modul, yaitu enam pengguna, enam kursus,
// dan relasi MENGIKUTI yang membawa properti progres berupa bilangan bulat
// 0-100. Hesti dan Kartika berbagi tiga kursus.
// Instance latihan dipakai bersama oleh dataset dari beberapa topik, sehingga
// pembersihan di awal hanya menyentuh roster milik modul ini.
// Skrip ini idempoten (idempotent), artinya aman dijalankan berulang kali.

// Pembersihan terbatas pada roster modul ini
MATCH (p:Pengguna)
WHERE p.nama IN ["Hesti", "Indra", "Joko", "Kartika", "Laila", "Maya"]
DETACH DELETE p;
MATCH (k:Kursus)
WHERE k.judul IN ["Pengantar Algoritma", "Basis Data Graf", "Statistika Terapan",
                  "Pemrograman Python", "Jaringan Komputer", "Rekayasa Perangkat Lunak"]
DETACH DELETE k;

// Node pengguna: label :Pengguna dengan properti nama.
MERGE (:Pengguna {nama: "Hesti"});
MERGE (:Pengguna {nama: "Indra"});
MERGE (:Pengguna {nama: "Joko"});
MERGE (:Pengguna {nama: "Kartika"});
MERGE (:Pengguna {nama: "Laila"});
MERGE (:Pengguna {nama: "Maya"});

// Node kursus: label :Kursus dengan properti judul.
MERGE (:Kursus {judul: "Pengantar Algoritma"});
MERGE (:Kursus {judul: "Basis Data Graf"});
MERGE (:Kursus {judul: "Statistika Terapan"});
MERGE (:Kursus {judul: "Pemrograman Python"});
MERGE (:Kursus {judul: "Jaringan Komputer"});
MERGE (:Kursus {judul: "Rekayasa Perangkat Lunak"});

// Relasi MENGIKUTI berarah dari pengguna menuju kursus, satu relasi per
// pasangan pengguna dan kursus, dengan properti progres.
MATCH (p:Pengguna {nama: "Hesti"}), (k:Kursus {judul: "Pengantar Algoritma"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 80;
MATCH (p:Pengguna {nama: "Hesti"}), (k:Kursus {judul: "Basis Data Graf"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 65;
MATCH (p:Pengguna {nama: "Hesti"}), (k:Kursus {judul: "Pemrograman Python"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 90;
MATCH (p:Pengguna {nama: "Hesti"}), (k:Kursus {judul: "Rekayasa Perangkat Lunak"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 40;
MATCH (p:Pengguna {nama: "Indra"}), (k:Kursus {judul: "Pengantar Algoritma"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 75;
MATCH (p:Pengguna {nama: "Indra"}), (k:Kursus {judul: "Pemrograman Python"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 60;
MATCH (p:Pengguna {nama: "Indra"}), (k:Kursus {judul: "Statistika Terapan"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 45;
MATCH (p:Pengguna {nama: "Joko"}), (k:Kursus {judul: "Basis Data Graf"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 55;
MATCH (p:Pengguna {nama: "Joko"}), (k:Kursus {judul: "Jaringan Komputer"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 70;
MATCH (p:Pengguna {nama: "Kartika"}), (k:Kursus {judul: "Pengantar Algoritma"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 70;
MATCH (p:Pengguna {nama: "Kartika"}), (k:Kursus {judul: "Basis Data Graf"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 60;
MATCH (p:Pengguna {nama: "Kartika"}), (k:Kursus {judul: "Pemrograman Python"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 85;
MATCH (p:Pengguna {nama: "Kartika"}), (k:Kursus {judul: "Jaringan Komputer"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 50;
MATCH (p:Pengguna {nama: "Laila"}), (k:Kursus {judul: "Statistika Terapan"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 80;
MATCH (p:Pengguna {nama: "Laila"}), (k:Kursus {judul: "Pemrograman Python"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 50;
MATCH (p:Pengguna {nama: "Laila"}), (k:Kursus {judul: "Jaringan Komputer"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 35;
MATCH (p:Pengguna {nama: "Maya"}), (k:Kursus {judul: "Basis Data Graf"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 90;
MATCH (p:Pengguna {nama: "Maya"}), (k:Kursus {judul: "Statistika Terapan"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 65;
MATCH (p:Pengguna {nama: "Maya"}), (k:Kursus {judul: "Rekayasa Perangkat Lunak"})
  MERGE (p)-[r:MENGIKUTI]->(k)
  SET r.progres = 30;

Berkas penyegaran.cypher berisi satu pernyataan UNWIND yang menerima tiga baris data. Setiap baris melewati rantai yang sama, yaitu MERGE node pengguna, MERGE node kursus, MERGE relasi :MENGIKUTI di antara keduanya, lalu SET menulis ulang progresnya. Dua baris pertama memperbarui pendaftaran Hesti dan Maya yang sudah ada, sedangkan baris ketiga membawa pendaftaran baru bagi Indra pada Jaringan Komputer.

cypher penyegaran.cypher
// Batch penyegaran harian untuk dataset modul Pengerjaan proyek: rekomendasi.
// Pola upsert: MERGE menemukan pengguna, kursus, dan relasi yang sudah ada,
// lalu SET memperbarui progresnya, sehingga batch yang sama aman dimuat
// berulang kali tanpa menduplikasi data.

// Dua baris pertama memperbarui progres pendaftaran yang sudah terbentuk,
// sedangkan baris ketiga membawa pendaftaran baru bagi Indra.
UNWIND [
  ["Hesti", "Pemrograman Python", 92],
  ["Maya", "Basis Data Graf", 88],
  ["Indra", "Jaringan Komputer", 25]
] AS baris
MERGE (p:Pengguna {nama: baris[0]})
MERGE (k:Kursus {judul: baris[1]})
MERGE (p)-[r:MENGIKUTI]->(k)
SET r.progres = baris[2];
bash
# tidak-dijalankan
# membangun ulang baseline roster kanonis modul sebelum percobaan
cat setup.cypher | cypher-shell -a bolt://127.0.0.1:7687 -u neo4j
# memuat batch penyegaran beserta ringkasan eksekusinya
cat penyegaran.cypher | cypher-shell -a bolt://127.0.0.1:7687 -u neo4j --format verbose

Batch tersebut dijalankan dua kali berturut-turut pada instance latihan. Eksekusi pertama melaporkan Created 1 relationship, set 3 properties, yaitu relasi baru untuk pendaftaran Indra dan penulisan ulang tiga nilai progres. Eksekusi kedua atas berkas yang sama melaporkan Set 3 properties tanpa pembuatan relasi apa pun, sebab semua relasi yang dicari MERGE sudah ditemukan.

Nilai akhir ketiga baris diverifikasi dengan kueri berikut.

cypher
UNWIND [
  ['Hesti', 'Pemrograman Python'],
  ['Maya', 'Basis Data Graf'],
  ['Indra', 'Jaringan Komputer']
] AS baris
MATCH (p:Pengguna {nama: baris[0]})-[r:MENGIKUTI]->(k:Kursus {judul: baris[1]})
RETURN p.nama AS pengguna, k.judul AS kursus, r.progres AS progres;
text Keluaran
pengguna, kursus, progres
"Hesti", "Pemrograman Python", 92
"Maya", "Basis Data Graf", 88
"Indra", "Jaringan Komputer", 25

Progres Hesti naik dari 90 menjadi 92, progres Maya turun dari 90 menjadi 88, dan pendaftaran Indra pada Jaringan Komputer muncul dengan progres 25. Ketiga perubahan adalah isi batch yang sama, dan tidak ada pendaftaran lain yang tersentuh. Idempotensinya diperiksa dengan menghitung kursus yang diikuti Indra: baseline kanonis mencatat tiga kursus, sehingga hasil empat berarti tepat satu pendaftaran baru masuk dan eksekusi ulang tidak menduplikasi apa pun.

cypher
MATCH (p:Pengguna {nama: 'Indra'})-[r:MENGIKUTI]->(k:Kursus)
RETURN count(k) AS kursusDiikuti;
text Keluaran
kursusDiikuti
4

Pemeriksaan kesehatan pascaimpor#

Impor yang selesai belum tentu bersih, sehingga dua pemeriksaan berikut dijalankan setiap kali batch dimuat. Pemeriksaan pertama menghitung relasi :MENGIKUTI yang salah satu ujungnya tidak membawa identitas, yaitu node pengguna tanpa properti nama atau node kursus tanpa properti judul. Relasi semacam itu tidak dapat dijangkau kueri rekomendasi yang menjangkar pada nama dan judul, sehingga jumlahnya wajib nol.

cypher
MATCH (p:Pengguna)-[r:MENGIKUTI]->(k:Kursus)
WHERE p.nama IS NULL OR k.judul IS NULL
RETURN count(r) AS relasiBermasalah;
text Keluaran
relasiBermasalah
0

Pemeriksaan kedua menyasar node pengguna yang tidak membawa properti nama sama sekali. Constraint keunikan tidak membantu di sini karena aturan itu hanya mengikat node yang memiliki properti nama, dan node tanpa properti tersebut tetap lolos pembuatannya. Pemeriksaan terpisah itulah yang menutup celah tersebut.

cypher
MATCH (p:Pengguna)
WHERE p.nama IS NULL
RETURN count(p) AS penggunaTanpaNama;
text Keluaran
penggunaTanpaNama
0

Kedua keluaran bernilai nol pada dataset yang baru dimuat, sehingga seluruh relasi rekomendasi terjangkar pada identitas yang lengkap. Kedua pemeriksaan berada di sisi impor, bukan di sisi skema, karena constraint membatasi nilai yang ditulis, bukan keberadaan properti itu sendiri. Biaya eksekusinya kecil pada dataset sekecil ini dan pemeriksaan yang sama tetap bermakna ketika roster bertambah besar. Apabila salah satu angka bukan nol, node atau relasi yang teridentifikasi dihapus terlebih dahulu, penyebabnya diperbaiki pada sumber datanya, dan batch dimuat ulang dari awal.

Kesalahan umum#

Kesalahan pertama adalah membuang IF NOT EXISTS pada pernyataan skema. Pernyataan CREATE CONSTRAINT atas constraint yang sudah ada menolak eksekusinya dengan pesan 22N65: data exception - equivalent constraint already exists, sedangkan bentuk dengan IF NOT EXISTS melewatinya tanpa pesan kesalahan. Perbaikannya adalah mempertahankan klausa tersebut pada setiap skrip yang dijalankan berulang, termasuk skrip pemuatan data.

Kesalahan kedua adalah mengganti MERGE dengan CREATE pada rantai penyegaran. CREATE selalu menyisipkan relasi baru, sehingga batch harian yang dijalankan berulang menumpuk relasi :MENGIKUTI kembar antara pasangan pengguna dan kursus yang sama, dan skor konsumsi bersama ikut terhitung berlebih. Perbaikannya adalah MERGE pada ketiga tingkat, yaitu node pengguna, node kursus, dan relasinya, sebagaimana berkas penyegaran pada bagian sebelumnya.

Kesalahan ketiga adalah menjalankan SHOW CONSTRAINTS dan SHOW INDEXES tanpa penyaring. Instance latihan digunakan bersama, sehingga keluaran tanpa WHERE ikut memuat indeks pencarian token dan skema dataset lain, dan baris yang dicari mudah terlewat di antaranya. Perbaikannya adalah menyaring pada nama eksplisit dengan WHERE name IN ['pengguna_nama_m8', 'kursus_judul_m8'].

Ringkasan#

  • Constraint pengguna_nama_m8 menjaga keunikan nama pada :Pengguna dan indeks kursus_judul_m8 mempercepat pencarian judul pada :Kursus, keduanya dibuat dengan IF NOT EXISTS sehingga eksekusi ulangnya diabaikan apabila skema sudah ada.
  • Kueri PROFILE membuktikan pencarian jangkar Hesti menggunakan operator NodeUniqueIndexSeek dengan satu baris dan dua akses basis data, sedangkan total kueri hanya 15 akses.
  • Batch penyegaran bekerja sebagai upsert idempoten melalui UNWIND dan rantai MERGE, membukukan satu relasi baru pada eksekusi pertama dan tidak ada pada eksekusi berikutnya.
  • Pemeriksaan kesehatan pascaimpor menghitung relasi tanpa identitas dan pengguna tanpa nama, dan keduanya wajib bernilai nol.

Referensi#