List yang dikerjakan dengan benar

collect, UNWIND, predikat list, dan koleksi jalur dalam Cypher.

Daftar isi

Pada Cypher, sekelompok nilai dapat disimpan dalam satu urutan tunggal yang disebut list, misalnya kumpulan nama teman dari satu orang atau deretan node yang dilalui sebuah jalur. Topik ini membahas pengolahan list secara lengkap, mulai dari pengumpulan baris hasil kueri menjadi list dengan collect, pembongkaran list menjadi baris dengan UNWIND, penelaahan isi list dengan predikat any(), all(), none(), dan single(), hingga fungsi head(), last(), tail(), reverse(), dan range(). Bagian akhir membahas koleksi jalur, yaitu nodes() dan relationships(), beserta gabungannya dengan UNWIND untuk operasi berulang. Seluruh contoh dieksekusi pada dataset jaringan sosial modul ini, sehingga setiap keluaran dapat direproduksi ulang.

Dataset jaringan sosial#

Seluruh contoh pada modul ini menggunakan dataset jaringan sosial yang sama, yaitu tujuh node berlabel :Person dengan properti name dan kota, ditambah dua node :Hobi bernama Fotografi dan Bersepeda. Relasi :KENAL menghubungkan orang-orang tersebut dalam satu rantai utama beserta beberapa cabang, sedangkan relasi :PUNYA_HOBI hanya dimiliki Hana dan Fajar. Ketimpangan hobi inilah yang penting bagi topik ini, karena pengelompokan atas data yang tidak lengkap memunculkan nilai null secara alami dan perilaku list terhadap null perlu diamati langsung. Jaringan yang kecil juga membuat setiap list hasil pengelompokan mudah diperiksa kebenarannya secara manual, sehingga sifat-sifat list tidak sekadar diyakini tetapi terlihat pada keluarannya. Skrip setup.cypher berikut membangun dataset menggunakan MERGE, sehingga aman dijalankan berulang tanpa menduplikasi data.

cypher setup.cypher
// Dataset modul Pola kueri: jaringan sosial dengan relasi KENAL dan hobi.
// Skrip ini idempoten, sehingga aman dijalankan lebih dari satu kali.

// Tujuh orang beserta kota asalnya
MERGE (p:Person {name: 'Eko'}) SET p.kota = 'Bandung';
MERGE (p:Person {name: 'Fajar'}) SET p.kota = 'Bandung';
MERGE (p:Person {name: 'Gita'}) SET p.kota = 'Semarang';
MERGE (p:Person {name: 'Hana'}) SET p.kota = 'Surabaya';
MERGE (p:Person {name: 'Iwan'}) SET p.kota = 'Semarang';
MERGE (p:Person {name: 'Jihan'}) SET p.kota = 'Bandung';
MERGE (p:Person {name: 'Rara'}) SET p.kota = 'Yogyakarta';

// Dua hobi
MERGE (h:Hobi {name: 'Fotografi'});
MERGE (h:Hobi {name: 'Bersepeda'});

// Rantai KENAL, kedua ujung diikat variabel agar MERGE hanya menangani relasinya
MATCH (a:Person {name: 'Eko'}), (b:Person {name: 'Fajar'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Fajar'}), (b:Person {name: 'Gita'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Gita'}), (b:Person {name: 'Hana'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Eko'}), (b:Person {name: 'Iwan'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Iwan'}), (b:Person {name: 'Hana'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Gita'}), (b:Person {name: 'Jihan'}) MERGE (a)-[:KENAL]->(b);
MATCH (a:Person {name: 'Fajar'}), (b:Person {name: 'Rara'}) MERGE (a)-[:KENAL]->(b);

// Hobi hanya dimiliki sebagian orang, sehingga null muncul secara alami
MATCH (a:Person {name: 'Hana'}), (b:Hobi {name: 'Fotografi'}) MERGE (a)-[:PUNYA_HOBI]->(b);
MATCH (a:Person {name: 'Fajar'}), (b:Hobi {name: 'Bersepeda'}) MERGE (a)-[:PUNYA_HOBI]->(b);

Skrip tersebut dimuat ke server dengan cypher-shell sebagaimana perintah berikut, atau dengan membukanya langsung di Neo4j Browser.

bash
# tidak-dijalankan
# memuat dataset melalui cypher-shell, kata sandi diminta interaktif
cypher-shell -a bolt://localhost:7687 -u neo4j -f setup.cypher

Collect dan sifatnya#

Fungsi collect() adalah fungsi agregasi yang mengumpulkan nilai dari banyak baris menjadi satu list, dengan satu list untuk setiap kelompok pengelompokan. Kueri berikut mengumpulkan nama semua orang yang dikenal Eko.

cypher
MATCH (a:Person {name: 'Eko'})-[:KENAL]->(b)
RETURN a.name AS nama, collect(b.name) AS teman;
text Keluaran
nama, teman
"Eko", ["Iwan", "Fajar"]

Keluaran di atas memperlihatkan sifat pertama, yaitu urutan elemen pada list hasil collect() tidak dijamin mengikuti urutan relasi pada graf maupun urutan baris hasil pencocokan. Apabila urutan penting, list perlu diurutkan secara eksplisit, misalnya dengan fungsi coll.sort() yang mengembalikan salinan list yang telah terurut. Sifat kedua menyangkut null, karena nilai null tidak pernah masuk ke list hasil collect() dan pengumpulan atas ekspresi yang semuanya null menghasilkan list kosong, termasuk bentuk collect(null). Perilaku ini terlihat apabila pengumpulan hobi digabungkan dengan OPTIONAL MATCH, karena orang tanpa hobi tetap menghasilkan baris dengan nilai null pada variabelnya.

cypher
MATCH (p:Person)
OPTIONAL MATCH (p)-[:PUNYA_HOBI]->(h)
WITH p, collect(h.name) AS hobi
RETURN p.name AS nama, hobi
ORDER BY nama;
text Keluaran
nama, hobi
"Eko", []
"Fajar", ["Bersepeda"]
"Gita", []
"Hana", ["Fotografi"]
"Iwan", []
"Jihan", []
"Rara", []

UNWIND untuk membongkar list#

Klausa UNWIND adalah kebalikan collect(), yaitu mengubah satu list menjadi banyak baris, satu baris untuk setiap elemennya. Baris hasil pembongkaran dapat diikat ke pola MATCH berikutnya, sehingga satu kueri mengerjakan pencarian yang sama untuk setiap elemen list. Kueri berikut membongkar list dua hobi, lalu mencari pemilik setiap hobi tersebut.

cypher
UNWIND ['Fotografi', 'Bersepeda'] AS namaHobi
MATCH (h:Hobi {name: namaHobi})<-[:PUNYA_HOBI]-(p:Person)
RETURN namaHobi, collect(p.name) AS pemilik
ORDER BY namaHobi;
text Keluaran
namaHobi, pemilik
"Bersepeda", ["Fajar"]
"Fotografi", ["Hana"]

Perilaku yang paling sering terlewat adalah list kosong, karena UNWIND atas list kosong menghasilkan nol baris, bukan satu baris berisi nilai kosong. Dokumentasi resmi menyatakan UNWIND [] mengurangi jumlah baris menjadi nol, sehingga seluruh klausa setelahnya tidak pernah berjalan. Kueri berikut membuktikannya dengan menghitung node :Person setelah membongkar list kosong.

cypher
UNWIND [] AS x
MATCH (p:Person)
RETURN count(p) AS banyakPerson;
text Keluaran
banyakPerson
0

Gabungan keduanya membentuk pola yang sering digunakan, yaitu collect() untuk merapatkan banyak baris menjadi satu baris per kelompok, lalu UNWIND untuk membuka kembali list tersebut bagi operasi yang berulang.

cypher
MATCH (p:Person)-[:KENAL]->(b)
WITH p, collect(b.name) AS teman
UNWIND teman AS t
RETURN p.name AS nama, t AS teman
ORDER BY nama, teman;
text Keluaran
nama, teman
"Eko", "Fajar"
"Eko", "Iwan"
"Fajar", "Gita"
"Fajar", "Rara"
"Gita", "Hana"
"Gita", "Jihan"
"Iwan", "Hana"

Predikat list#

Predikat list (list predicate) menilai sebuah kondisi atas elemen-elemen list dan mengembalikan satu nilai kebenaran. Cypher menyediakan empat predikat, yaitu any() untuk minimal satu elemen yang memenuhi kondisi, all() untuk semua elemen, none() untuk tidak ada elemen yang memenuhi kondisi, dan single() untuk tepat satu elemen. Keempatnya ditulis dengan bentuk variabel IN list WHERE kondisi, dan variabel di dalamnya hanya berlaku pada predikat itu. Kueri berikut menguji keberadaan kota Semarang pada list kota asal teman setiap orang, dengan coll.sort() menjaga keluaran tetap terurut.

cypher
MATCH (p:Person)-[:KENAL]->(b)
WITH p, coll.sort(collect(b.kota)) AS kotaTeman
RETURN p.name AS nama, kotaTeman,
       any(k IN kotaTeman WHERE k = 'Semarang') AS adaSemarang,
       all(k IN kotaTeman WHERE k = 'Semarang') AS semuaSemarang,
       none(k IN kotaTeman WHERE k = 'Semarang') AS tanpaSemarang,
       single(k IN kotaTeman WHERE k = 'Semarang') AS tunggalSemarang
ORDER BY nama;
text Keluaran
nama, kotaTeman, adaSemarang, semuaSemarang, tanpaSemarang, tunggalSemarang
"Eko", ["Bandung", "Semarang"], TRUE, FALSE, FALSE, TRUE
"Fajar", ["Semarang", "Yogyakarta"], TRUE, FALSE, FALSE, TRUE
"Gita", ["Bandung", "Surabaya"], FALSE, FALSE, TRUE, FALSE
"Iwan", ["Surabaya"], FALSE, FALSE, TRUE, FALSE

Eko dan Fajar masing-masing memiliki tepat satu teman dari Semarang, sehingga tunggalSemarang bernilai TRUE dan semuaSemarang bernilai FALSE karena teman lainnya berasal dari kota berbeda. Gita dan Iwan tidak memiliki teman dari Semarang, sehingga tanpaSemarang bernilai TRUE. Satu perilaku yang perlu diingat adalah penilaian atas list kosong, karena pada list kosong all() dan none() mengembalikan TRUE, sedangkan any() dan single() mengembalikan FALSE.

Fungsi list#

Fungsi head() mengembalikan elemen pertama sebuah list, last() mengembalikan elemen terakhir, dan tail() mengembalikan seluruh elemen kecuali yang pertama. Selain melalui fungsi, elemen tunggal diakses dengan indeks di dalam kurung siku, dan indeks negatif dihitung dari ujung belakang.

cypher
WITH ['Bandung', 'Semarang', 'Surabaya'] AS daftar
RETURN head(daftar) AS pertama,
       last(daftar) AS terakhir,
       daftar[0] AS indeksPertama,
       daftar[-1] AS indeksTerakhir,
       tail(daftar) AS sisanya;
text Keluaran
pertama, terakhir, indeksPertama, indeksTerakhir, sisanya
"Bandung", "Surabaya", "Bandung", "Surabaya", ["Semarang", "Surabaya"]

Irisan list ditulis dengan rentang di dalam kurung siku, misalnya daftar[0..2]. Batas awalnya inklusif dan batas akhirnya eksklusif, sehingga irisan tersebut hanya memuat elemen pada indeks 0 dan 1. Pada versi Cypher yang digunakan di jalur ini, fungsi slice() tidak tersedia lagi, sehingga operator kurung siku merupakan satu-satunya cara mengambil irisan. Fungsi reverse() membalik urutan elemen, sedangkan range() membentuk list deret angka dengan kedua batas inklusif, dan bentuk tiga argumennya menetapkan langkah loncatan. List buatan range() lazim menjadi bahan UNWIND untuk perulangan dengan jumlah yang diketahui. Adapun tail() atas list satu elemen maupun list kosong menghasilkan list kosong, bukan null.

cypher
WITH ['Bandung', 'Semarang', 'Surabaya'] AS daftar
RETURN reverse(daftar) AS dibalik,
       daftar[0..2] AS irisan,
       range(1, 5) AS rentang,
       range(0, 10, 2) AS loncatDua;
text Keluaran
dibalik, irisan, rentang, loncatDua
["Surabaya", "Semarang", "Bandung"], ["Bandung", "Semarang"], [1, 2, 3, 4, 5], [0, 2, 4, 6, 8, 10]

Panjang sebuah list diukur dengan size(), yang juga berlaku untuk list buatan range().

cypher
RETURN size(range(1, 100)) AS banyakAngka, size([]) AS listKosong;
text Keluaran
banyakAngka, listKosong
100, 0

Koleksi jalur: nodes() dan relationships()#

Jalur yang diikat ke sebuah variabel menyimpan lintasan lengkap, dan dua fungsi mengubah jalur itu menjadi list, yaitu nodes() yang mengembalikan list node serta relationships() yang mengembalikan list relasi. Keduanya sering digabungkan dengan list comprehension, yaitu ekspresi [n IN nodes(p) | n.name] yang membentuk list baru dari elemen list yang sudah ada. Kueri berikut menelusuri seluruh jalur tiga lompatan KENAL dari Eko, lalu menampilkan urutan node dan tipe relasinya.

cypher
MATCH p = (a:Person {name: 'Eko'})-[:KENAL*3]->(b:Person)
RETURN b.name AS tujuan,
       [n IN nodes(p) | n.name] AS urutanNode,
       [r IN relationships(p) | type(r)] AS tipeRelasi
ORDER BY tujuan;
text Keluaran
tujuan, urutanNode, tipeRelasi
"Hana", ["Eko", "Fajar", "Gita", "Hana"], ["KENAL", "KENAL", "KENAL"]
"Jihan", ["Eko", "Fajar", "Gita", "Jihan"], ["KENAL", "KENAL", "KENAL"]

List hasil nodes() dibongkar dengan UNWIND seperti list lainnya, sehingga satu operasi dapat dijalankan berulang untuk setiap node pada jalur. Kueri berikut mengumpulkan semua orang yang bersinggungan dengan jalur-jalur tersebut.

cypher
MATCH p = (a:Person {name: 'Eko'})-[:KENAL*3]->(b:Person)
UNWIND nodes(p) AS n
RETURN DISTINCT n.name AS dikunjungi
ORDER BY dikunjungi;
text Keluaran
dikunjungi
"Eko"
"Fajar"
"Gita"
"Hana"
"Jihan"

DISTINCT diperlukan karena Eko, Fajar, dan Gita masing-masing muncul pada dua jalur berbeda, sehingga tanpa penyaringan ketiganya terduplikasi pada hasil. Tanpa pembongkaran, informasi jalur hanya tersimpan pada dua baris hasil, sedangkan pembongkaran membuka setiap node untuk perhitungan lanjutan seperti penghitungan dan pengurutan. Panjang list jalur juga dapat diukur langsung dengan size(nodes(p)) tanpa membongkarnya terlebih dahulu.

Kesalahan umum#

Kesalahan pertama adalah mengandalkan urutan elemen pada list hasil collect(). Urutan tersebut tidak dijamin oleh bahasanya, sehingga logika yang bergantung pada elemen pertama dapat berperilaku berbeda antareksekusi. Perbaikannya adalah mengurutkan list secara eksplisit dengan coll.sort(), atau menyaring elemen dengan predikat alih-alih mengambil elemen berdasarkan posisinya.

Kesalahan kedua adalah menganggap UNWIND atas list kosong menghasilkan satu baris. Perilaku sebenarnya adalah nol baris, sehingga bagian kueri setelah UNWIND menghilang tanpa pesan error dan hasil akhirnya kosong. Kebiasaan yang aman adalah menghitung size(list) terlebih dahulu, atau merancang kueri agar kekosongan memang diterima sebagai jawaban yang sah.

Kesalahan ketiga adalah membaca indeks di luar batas list dan mengharapkan error. Akses seperti daftar[5] pada list tiga elemen mengembalikan null tanpa pesan apa pun, dan nilai null itu menyebar ke seluruh ekspresi yang menggunakannya sehingga hasil akhir terlihat kosong tanpa penjelasan. Panjang list sebaiknya diperiksa dengan size() sebelum elemen pada posisi tertentu diakses.

Ringkasan#

  • collect() mengumpulkan baris menjadi list per kelompok, melewatkan nilai null, dan tidak menjamin urutan elemennya.
  • UNWIND membongkar list menjadi baris, dengan list kosong menghasilkan nol baris sehingga kueri setelahnya tidak pernah berjalan.
  • Predikat any(), all(), none(), dan single() menilai kondisi atas elemen list, dan pada list kosong all() serta none() mengembalikan TRUE.
  • head(), last(), tail(), reverse(), range(), dan operator irisan memanipulasi list, sedangkan nodes() dan relationships() mengubah jalur menjadi list yang dapat dibongkar dengan UNWIND.

Referensi#