Memberi nomor halaman pada hasil penelusuran

Aplikasi web sering kali memberi nomor halaman pada data saat ditampilkan kepada pengguna. Pengguna akhir menerima satu halaman hasil, dan saat mereka membuka halaman berikutnya, batch hasil berikutnya akan diambil dan ditampilkan. Halaman ini menjelaskan cara menambahkan penomoran halaman ke hasil penelusuran saat melakukan penelusuran teks lengkap di Spanner.

Opsi penomoran halaman

Setiap halaman dari set hasil yang diberi nomor halaman diambil oleh kueri independen, dan Spanner tidak menyimpan status di antara halaman. Apa pun yang berubah di antara kueri berturut-turut, seperti data atau tata urutan, dapat membuat hasilnya tidak konsisten: saat aplikasi berpindah ke halaman berikutnya, baris dapat dilewati, atau baris dari halaman sebelumnya dapat diulang. Perilaku ini berlaku untuk semua kueri yang diberi nomor halaman di Spanner, meskipun metode penomoran halaman yang Anda pilih menentukan seberapa besar perubahan ini memengaruhi hasil.

Ada dua cara untuk menerapkan kueri yang dipaginasi di Spanner: paginasi berbasis kunci (direkomendasikan) dan paginasi berbasis offset.

Penomoran halaman berbasis kunci adalah metode untuk mengambil hasil penelusuran dalam potongan yang lebih kecil dan lebih mudah dikelola, sekaligus memastikan hasil yang konsisten di seluruh permintaan. ID unik ("kunci") dari hasil terakhir halaman digunakan sebagai titik referensi untuk mengambil kumpulan hasil berikutnya.

Spanner umumnya merekomendasikan penggunaan penomoran halaman berbasis kunci, kecuali saat memberi nomor halaman pada hasil penelusuran berperingkat. Meskipun penomoran halaman berbasis offset lebih mudah diterapkan, penomoran halaman ini memiliki dua kelemahan yang signifikan:

  • Biaya kueri yang lebih tinggi: Penomoran halaman berbasis offset harus mengambil dan menghapus hasil dari semua halaman sebelumnya, sehingga meningkatkan biaya dan menurunkan performa.
  • Hasil yang tidak konsisten: Dalam kueri yang diberi nomor halaman, setiap halaman biasanya diambil pada stempel waktu baca yang berbeda. Misalnya, halaman pertama mungkin berasal dari kueri pada pukul 13.00, dan halaman berikutnya dari kueri pada pukul 13.10. Di antara kueri, baris apa pun yang disisipkan atau dihapus sebelum offset saat ini akan menggeser baris yang mengikutinya, sehingga baris dilewati atau diulang di seluruh halaman.

Di sisi lain, penomoran halaman berbasis kunci menggunakan ID unik (kunci) dari hasil terakhir halaman untuk mengambil kumpulan hasil berikutnya. Pendekatan ini memastikan pengambilan yang efisien dan hasil yang lebih konsisten: halaman berikutnya dilanjutkan dari kunci terakhir halaman sebelumnya, sehingga hanya penulisan serentak yang memindahkan baris melintasi batas tersebut yang memengaruhi hasil. Misalnya, baris yang baru disisipkan yang diurutkan sebelum kunci tersebut akan dilewati, sedangkan baris yang ditampilkan sebelumnya yang diperbarui untuk diurutkan setelah kunci tersebut akan diulang.

Untuk memberikan stabilitas pada hasil halaman, aplikasi dapat mengeluarkan semua kueri untuk halaman yang berbeda pada stempel waktu yang sama. Namun, hal ini mungkin gagal jika kueri melebihi periode retensi versi (defaultnya adalah 1 jam). Misalnya, kegagalan ini terjadi jika version_gc adalah satu jam, dan pengguna akhir mengambil hasil pertama pada pukul 13.00 dan mengklik Berikutnya pada pukul 15.00.

Menggunakan penomoran halaman berbasis kunci

Penomoran halaman berbasis kunci mengingat item terakhir dari halaman sebelumnya dan menggunakannya sebagai titik awal untuk kueri halaman berikutnya. Untuk melakukannya, kueri harus menampilkan kolom yang ditentukan dalam klausa ORDER BY dan membatasi jumlah baris menggunakan LIMIT.

Agar penomoran halaman berbasis kunci berfungsi, kueri harus mengurutkan hasil berdasarkan urutan total yang ketat. Cara termudah untuk mendapatkannya adalah dengan memilih total order, lalu menambahkan kolom penentu, jika diperlukan. Pada umumnya, urutan total adalah tata urutan indeks penelusuran dan kombinasi kolom yang unik adalah kunci utama tabel dasar.

Dengan menggunakan contoh skema Albums, untuk halaman pertama, kuerinya akan terlihat seperti berikut:

GoogleSQL

SELECT AlbumId, ReleaseTimestamp
FROM Albums
WHERE SEARCH(AlbumTitle_Tokens, "fifth symphony")
ORDER BY ReleaseTimestamp DESC, AlbumId
LIMIT 10;

PostgreSQL

SELECT albumid, releasetimestamp
FROM albums
WHERE spanner.search(albumtitle_tokens, 'fifth symphony')
ORDER BY releasetimestamp DESC, albumid
LIMIT 10;

AlbumId adalah penentu karena ReleaseTimestamp bukan kunci. Mungkin ada dua album berbeda dengan nilai yang sama untuk ReleaseTimestamp.

Untuk melanjutkan, aplikasi menjalankan kueri yang sama lagi, tetapi dengan klausa WHERE yang membatasi hasil dari halaman sebelumnya. Kondisi tambahan perlu memperhitungkan arah kunci (menaik versus menurun), tie breaker, dan urutan nilai NULL untuk kolom nullable.

Dalam contoh kita, AlbumId adalah satu-satunya kolom kunci (dalam urutan menaik) dan tidak boleh NULL, sehingga kondisinya adalah sebagai berikut:

GoogleSQL

SELECT AlbumId, ReleaseTimestamp
FROM Albums
WHERE (ReleaseTimestamp < @last_page_release_timestamp
    OR (ReleaseTimestamp = @last_page_release_timestamp
        AND AlbumId > @last_page_album_id))
    AND SEARCH(AlbumTitle_Tokens, @p)
ORDER BY ReleaseTimestamp DESC, AlbumId ASC
LIMIT @page_size;

PostgreSQL

Contoh ini menggunakan parameter kueri $1, $2, $3, dan $4 yang terikat ke nilai yang ditentukan untuk last_page_release_timestamp, last_page_album_id, query, dan page_size.

SELECT albumid, releasetimestamp
FROM albums
WHERE (releasetimestamp < $1
    OR (releasetimestamp = $1
        AND albumid > $2))
    AND spanner.search(albumtitle_tokens, $3)
ORDER BY releasetimestamp DESC, albumid ASC
LIMIT $4;

Spanner menafsirkan jenis kondisi ini sebagai dapat dicari. Artinya, Spanner tidak membaca indeks untuk dokumen yang Anda filter. Pengoptimalan inilah yang membuat penomoran halaman berbasis kunci jauh lebih efisien daripada penomoran halaman berbasis offset.

Menggunakan penomoran halaman berbasis offset

Penomoran halaman berbasis offset memanfaatkan klausa LIMIT dan OFFSET kueri SQL untuk menyimulasikan halaman. Nilai LIMIT menunjukkan jumlah hasil per halaman. Nilai OFFSET ditetapkan ke nol untuk halaman pertama, ukuran halaman untuk halaman kedua, dan dua kali ukuran halaman untuk halaman ketiga.

Misalnya, kueri berikut mengambil halaman ketiga, dengan ukuran halaman 50:

GoogleSQL

SELECT AlbumId
FROM Albums
WHERE SEARCH(AlbumTitle_Tokens, "fifth symphony")
ORDER BY ReleaseTimestamp DESC, AlbumId
LIMIT 50 OFFSET 100;

PostgreSQL

SELECT albumid
FROM albums
WHERE spanner.search(albumtitle_tokens, 'fifth symphony')
ORDER BY releasetimestamp DESC, albumid
LIMIT 50 OFFSET 100;

Catatan Penggunaan:

  • Klausa ORDER BY sangat direkomendasikan untuk memastikan pengurutan yang konsisten di antara halaman.
  • Dalam kueri produksi, gunakan parameter kueri, bukan konstanta, untuk menentukan LIMIT dan OFFSET agar caching kueri lebih efisien. Untuk mengetahui informasi selengkapnya, lihat Parameter kueri.

Memberi nomor halaman pada hasil penelusuran yang diberi peringkat

Spanner merekomendasikan penomoran halaman berbasis offset untuk kueri berperingkat, yang mengurutkan hasil berdasarkan nilai fungsi SCORE, bukan berdasarkan kolom kunci tabel. Meskipun Anda dapat menyertakan skor terakhir halaman sebelumnya dalam klausa WHERE untuk halaman berikutnya, kondisi tersebut tidak dapat dicari sehingga tidak memberikan manfaat biaya dan performa dari penomoran halaman berbasis kunci. Selain itu, nilai SCORE tidak dapat dibandingkan di seluruh versi pemberi skor, sehingga penomoran halaman berbasis offset menghasilkan lebih sedikit baris yang dilewati atau diulang saat versi pemberi skor berubah.

Untuk memberi nomor halaman pada hasil yang diberi peringkat, urutkan berdasarkan skor dengan kunci utama sebagai penentu, dan halaman dengan LIMIT dan OFFSET. Misalnya, kueri berikut mengambil halaman pertama dengan ukuran halaman dua:

GoogleSQL

SELECT
  AlbumId,
  SCORE(AlbumTitle_Tokens, @p) AS Score
FROM Albums
WHERE SEARCH(AlbumTitle_Tokens, @p)
ORDER BY Score DESC, AlbumId
LIMIT 2 OFFSET 0;

PostgreSQL

Contoh ini menggunakan parameter kueri $1, yang terikat ke string kueri.

SELECT
  albumid,
  spanner.score(albumtitle_tokens, $1) AS score
FROM albums
WHERE spanner.search(albumtitle_tokens, $1)
ORDER BY score DESC, albumid
LIMIT 2 OFFSET 0;

Nilai SCORE bergantung pada versi pemberi skor. Spanner menggunakan versi pemberi skor default database, kecuali jika kueri menetapkannya. Versi pencetak skor baru dapat mengubah urutan hasil, sehingga jika versi pencetak skor berubah di antara dua halaman dari kueri yang sama yang diberi nomor halaman, halaman tersebut dapat menjadi tidak konsisten. Misalnya, album yang cocok memiliki skor berikut dalam dua versi pemberi skor:

AlbumId Skor dengan versi 1 Skor dengan versi 2
1 0,9 0,7
2 0,8 0,95
3 0,7 0,6
4 0,6 0,85

Dengan versi 1 dan ukuran halaman dua, halaman pertama menampilkan album 1 dan 2. Jika versi pemberi skor berubah menjadi versi 2 sebelum halaman kedua diambil, halaman kedua akan dihitung dengan OFFSET 2 pada urutan versi 2, yang albumnya diberi peringkat 2, 4, 1, 3. Akibatnya, halaman kedua menampilkan album 1 dan 3, sehingga album 1 diulang dan album 4 dilewati.

Perubahan versi pencetak skor jarang terjadi: versi pencetak skor default database hanya berubah saat Anda mengupdatenya atau saat Spanner memigrasikan database ke versi default yang lebih baru, dan hanya kueri yang diberi nomor halaman yang sedang berlangsung pada saat itu yang terpengaruh. Untuk sebagian besar aplikasi, inkonsistensi singkat ini dapat diterima, dan Spanner merekomendasikan penggunaan versi pencetak skor default agar Anda selalu mendapatkan manfaat dari peningkatan pencetak skor terbaru.

Jika Anda memerlukan kontrol lebih besar atas perubahan, Anda memiliki dua opsi:

  • Tingkat database: Tetapkan sendiri versi pencetak skor default database, seperti yang dijelaskan dalam Menetapkan versi pencetak skor default database. Menyetel opsi ini tidak akan menghindari inkonsistensi, tetapi memungkinkan Anda memilih kapan peralihan terjadi—misalnya, selama periode traffic rendah—atau tetap menggunakan versi saat ini hingga Anda siap.
  • Tingkat kueri: Sematkan versi pemberi skor dalam kueri dan bawa dalam status penomoran halaman, seperti yang dijelaskan dalam langkah-langkah berikut. Pendekatan ini mempertahankan kueri yang diberi nomor halaman yang sedang berlangsung pada versi pemberi skor yang digunakan saat memulai, sehingga peralihan tidak memengaruhinya. Namun, kueri ini tetap dapat mengalami pergeseran baris karena penulisan serentak, seperti halnya penomoran halaman berbasis offset. Pendekatan ini lebih kompleks untuk diterapkan dan tidak diperlukan untuk sebagian besar aplikasi.

Untuk menyematkan versi pencetak skor di tingkat kueri, selesaikan langkah-langkah berikut:

  1. Tetapkan versi pemberi skor secara eksplisit dalam argumen options fungsi SCORE. Gunakan kueri yang sama untuk setiap halaman, dengan offset yang ditetapkan ke 0 untuk halaman pertama:

    GoogleSQL

    SELECT
      AlbumId,
      SCORE(AlbumTitle_Tokens, @p,
            options=>JSON_OBJECT('version', @score_version)) AS Score
    FROM Albums
    WHERE SEARCH(AlbumTitle_Tokens, @p)
    ORDER BY Score DESC, AlbumId
    LIMIT @page_size OFFSET @offset;
    

    PostgreSQL

    Contoh ini menggunakan parameter kueri $1, $2, $3, dan $4, yang terikat ke string kueri, opsi pemberi skor sebagai nilai JSONB seperti {"version": 2}, ukuran halaman, dan offset.

    SELECT
      albumid,
      spanner.score(albumtitle_tokens, $1, options => $2) AS score
    FROM albums
    WHERE spanner.search(albumtitle_tokens, $1)
    ORDER BY score DESC, albumid
    LIMIT $3 OFFSET $4;
    

    Untuk halaman pertama, ambil versi pemberi skor dari konfigurasi aplikasi Anda; biasanya nilainya sama dengan versi pemberi skor default database Anda, sehingga kueri yang diberi nomor halaman dan kueri lainnya diberi peringkat dengan cara yang sama.

  2. Simpan versi pemberi skor yang Anda ikat, bersama dengan offset halaman berikutnya, dalam status penomoran halaman, misalnya dalam cookie sesi atau dalam token halaman buram yang ditampilkan oleh API Anda.

  3. Untuk setiap halaman berikutnya, teruskan versi pemberi skor yang disimpan dalam kueri, bukan membaca versi dari konfigurasi aplikasi Anda saat ini. Pendekatan ini menjaga hasil yang diberi nomor halaman tetap konsisten di seluruh perubahan versi pencetak skor, baik perubahan default database maupun saat Anda memperbarui konfigurasi aplikasi.

Versi pemberi skor yang disematkan dalam kueri lebih diprioritaskan daripada versi pemberi skor default database. Untuk memindahkan kueri yang disematkan ke versi penskor yang lebih baru, ubah nilai dalam konfigurasi aplikasi Anda.

Langkah berikutnya