Webanwendungen paginieren Daten häufig, wenn sie Nutzern präsentiert werden. Der Endnutzer erhält eine Seite mit Ergebnissen. Wenn er zur nächsten Seite wechselt, wird die nächste Gruppe von Ergebnissen abgerufen und angezeigt. Auf dieser Seite wird beschrieben, wie Sie Suchergebnissen bei einer Volltextsuche in Spanner eine Paginierung hinzufügen.
Paginierungsoptionen
Jede Seite einer paginierten Ergebnismenge wird durch eine unabhängige Abfrage abgerufen und Spanner speichert keinen Status zwischen den Seiten. Alles, was sich zwischen aufeinanderfolgenden Abfragen ändert, z. B. die Daten oder die Sortierreihenfolge, kann zu inkonsistenten Ergebnissen führen: Wenn die Anwendung zur nächsten Seite wechselt, können Zeilen übersprungen oder Zeilen von der vorherigen Seite wiederholt werden. Dieses Verhalten gilt für alle paginierten Abfragen in Spanner. Die von Ihnen gewählte Paginierungsmethode bestimmt jedoch, wie stark sich diese Änderungen auf die Ergebnisse auswirken.
Es gibt zwei Möglichkeiten, paginierte Abfragen in Spanner zu implementieren: schlüsselbasierte Paginierung (empfohlen) und offsetbasierte Paginierung.
Die schlüsselbasierte Paginierung ist eine Methode zum Abrufen von Suchergebnissen in kleineren, besser verwaltbaren Blöcken, die gleichzeitig konsistente Ergebnisse über Anfragen hinweg gewährleistet. Eine eindeutige Kennung (der „Schlüssel“) aus dem letzten Ergebnis einer Seite wird als Referenzpunkt verwendet, um die nächste Gruppe von Ergebnissen abzurufen.
Spanner empfiehlt im Allgemeinen die Verwendung der schlüsselbasierten Paginierung, außer bei der Paginierung von Suchergebnissen mit Ranking. Die offsetbasierte Paginierung ist zwar einfacher zu implementieren, hat aber zwei wesentliche Nachteile:
- Höhere Abfragekosten: Bei der offsetbasierten Paginierung müssen die Ergebnisse aller vorherigen Seiten abgerufen und verworfen werden, was zu höheren Kosten und einer geringeren Leistung führt.
- Inkonsistente Ergebnisse: Bei paginierten Abfragen wird jede Seite normalerweise mit einem anderen Lesestempel abgerufen. Die erste Seite könnte beispielsweise aus einer Abfrage um 13:00 Uhr und die nächste aus einer Abfrage um 13:10 Uhr stammen. Zwischen Abfragen werden alle Zeilen, die vor dem aktuellen Offset eingefügt oder gelöscht werden, verschoben, sodass Zeilen auf Seiten übersprungen oder wiederholt werden.
Bei der schlüsselbasierten Paginierung wird dagegen eine eindeutige Kennung (Schlüssel) aus dem letzten Ergebnis einer Seite verwendet, um die nächste Gruppe von Ergebnissen abzurufen. Dieser Ansatz sorgt für einen effizienten Abruf und konsistentere Ergebnisse: Die nächste Seite wird ab dem letzten Schlüssel der vorherigen Seite fortgesetzt. Daher wirken sich nur gleichzeitige Schreibvorgänge, die Zeilen über diese Grenze verschieben, auf die Ergebnisse aus. Beispielsweise wird eine neu eingefügte Zeile, die vor diesem Schlüssel sortiert wird, übersprungen, während eine zuvor zurückgegebene Zeile, die so aktualisiert wird, dass sie nach diesem Schlüssel sortiert wird, wiederholt wird.
Um die Stabilität der Seitenergebnisse zu gewährleisten, könnte die Anwendung alle Abfragen für verschiedene Seiten mit demselben Zeitstempel ausgeben. Dies kann jedoch fehlschlagen, wenn die Abfrage die Aufbewahrungsdauer für Versionen überschreitet (standardmäßig 1 Stunde). Dieser Fehler tritt beispielsweise auf, wenn version_gc eine Stunde beträgt und der Endnutzer die ersten Ergebnisse um 13:00 Uhr abgerufen und um 15:00 Uhr auf Weiter geklickt hat.
Schlüsselbasierte Paginierung verwenden
Bei der schlüsselbasierten Paginierung wird das letzte Element der vorherigen Seite gespeichert und als Ausgangspunkt für die nächste Seitenabfrage verwendet. Dazu muss die Abfrage die in der ORDER BY-Klausel angegebenen Spalten zurückgeben und die Anzahl der Zeilen mit LIMIT begrenzen.
Damit die schlüsselbasierte Paginierung funktioniert, müssen die Ergebnisse der Abfrage nach einer strengen Gesamtordnung sortiert werden. Am einfachsten ist es, eine beliebige Gesamtreihenfolge auszuwählen und dann bei Bedarf Spalten zur Auflösung von Gleichständen hinzuzufügen. In den meisten Fällen ist die Gesamtreihenfolge die Sortierreihenfolge des Suchindex und die eindeutige Kombination von Spalten der Primärschlüssel der Basistabelle.
Mit unserem Beispielschema Albums sieht die Abfrage für die erste Seite so aus:
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 ist der Tiebreaker, da ReleaseTimestamp kein Schlüssel ist. Es kann zwei verschiedene Alben mit demselben Wert für ReleaseTimestamp geben.
Um fortzufahren, führt die Anwendung dieselbe Abfrage noch einmal aus, aber mit einer WHERE-Klausel, die die Ergebnisse der vorherigen Seite einschränkt. Die zusätzliche Bedingung muss die Schlüsselrichtung (aufsteigend oder absteigend), Tiebreaker und die Reihenfolge der NULL-Werte für Spalten mit zulässigen Nullwerten berücksichtigen.
In unserem Beispiel ist AlbumId die einzige Schlüsselspalte (in aufsteigender Reihenfolge) und sie darf nicht NULL sein. Die Bedingung lautet also:
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
In diesem Beispiel werden die Suchparameter $1, $2, $3 und $4 verwendet, die an die für last_page_release_timestamp, last_page_album_id, query und page_size angegebenen Werte gebunden sind.
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 interpretiert diese Art von Bedingung als suchbar. Das bedeutet, dass Spanner den Index für Dokumente, die Sie herausfiltern, nicht liest. Diese Optimierung macht die schlüsselbasierte Paginierung viel effizienter als die offsetbasierte Paginierung.
Offsetbasierte Paginierung verwenden
Bei der offsetbasierten Paginierung werden die LIMIT- und OFFSET-Klauseln der SQL-Abfrage verwendet, um Seiten zu simulieren. Der Wert LIMIT gibt die Anzahl der Ergebnisse pro Seite an.
Der Wert OFFSET wird für die erste Seite auf null, für die zweite Seite auf die Seitengröße und für die dritte Seite auf das Doppelte der Seitengröße gesetzt.
Mit der folgenden Abfrage wird beispielsweise die dritte Seite mit einer Seitengröße von 50 abgerufen:
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;
Verwendungshinweise:
- Die
ORDER BY-Klausel wird dringend empfohlen, um eine einheitliche Reihenfolge zwischen den Seiten zu gewährleisten. - Verwenden Sie in Produktionsabfragen Abfrageparameter anstelle von Konstanten, um
LIMITundOFFSETanzugeben, damit das Abfrage-Caching effizienter wird. Weitere Informationen finden Sie unter Abfrageparameter.
Suchergebnisse mit Ranking paginieren
Cloud Spanner empfiehlt die offsetbasierte Paginierung für Abfragen mit Rangfolge, bei denen die Ergebnisse nach dem Wert der Funktion SCORE und nicht nach den Schlüsselspalten der Tabelle sortiert werden. Sie können zwar den letzten Wert der vorherigen Seite in die WHERE-Klausel für die nächste Seite aufnehmen, aber diese Bedingung ist nicht suchbar und bietet daher nicht die Kosten- und Leistungsvorteile der schlüsselbasierten Paginierung. Außerdem sind SCORE-Werte nicht über verschiedene Scorer-Versionen hinweg vergleichbar. Daher werden bei der Offset-basierten Paginierung weniger übersprungene oder wiederholte Zeilen erzeugt, wenn sich die Scorer-Version ändert.
Wenn Sie die Ergebnisse nach Rang paginieren möchten, sortieren Sie nach der Punktzahl mit dem Primärschlüssel als Tiebreaker und paginieren Sie mit LIMIT und OFFSET. Mit der folgenden Abfrage wird beispielsweise die erste Seite mit einer Seitengröße von zwei abgerufen:
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
In diesem Beispiel wird der Abfrageparameter $1 verwendet, der an den Abfragestring gebunden ist.
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;
Der Wert von SCORE hängt von der Scorer-Version ab.
Spanner verwendet die Standard-Scorer-Version der Datenbank, sofern in der Abfrage keine andere festgelegt ist. Eine neue Scorer-Version kann die Reihenfolge der Ergebnisse ändern. Wenn sich die Scorer-Version also zwischen zwei Seiten derselben paginierten Abfrage ändert, können die Seiten inkonsistent sein. Angenommen, die übereinstimmenden Alben haben die folgenden Bewertungen unter zwei Scorer-Versionen:
| AlbumId | Punktzahl mit Version 1 | Score mit Version 2 |
|---|---|---|
| 1 | 0,9 | 0,7 |
| 2 | 0,8 | 0,95 |
| 3 | 0,7 | 0,6 |
| 4 | 0,6 | 0,85 |
Bei Version 1 und einer Seitengröße von zwei werden auf der ersten Seite die Alben 1 und 2 zurückgegeben. Wenn sich die Scorer-Version vor dem Abrufen der zweiten Seite in Version 2 ändert, wird die zweite Seite mit OFFSET 2 in der Reihenfolge von Version 2 berechnet, in der die Alben die Ränge 2, 4, 1, 3 haben. Daher werden auf der zweiten Seite die Alben 1 und 3 zurückgegeben. Album 1 wird also wiederholt und Album 4 übersprungen.
Änderungen der Scorer-Version sind selten: Die Standard-Scorer-Version der Datenbank ändert sich nur, wenn Sie sie aktualisieren oder wenn Spanner die Datenbank zu einer neueren Standardversion migriert. Nur paginierte Abfragen, die zu diesem Zeitpunkt ausgeführt werden, sind davon betroffen. Für die meisten Anwendungen ist diese kurze Inkonsistenz akzeptabel. Spanner empfiehlt, die Standardversion des Scorers zu verwenden, damit Sie immer von den neuesten Verbesserungen des Scorers profitieren.
Wenn Sie die Änderung genauer steuern möchten, haben Sie zwei Möglichkeiten:
- Datenbankebene: Legen Sie die Standard-Scorer-Version der Datenbank selbst fest, wie unter Standard-Scorer-Version der Datenbank festlegen beschrieben. Wenn Sie diese Option festlegen, wird die Inkonsistenz nicht vermieden, aber Sie können auswählen, wann der Wechsel erfolgt, z. B. während einer Zeit mit geringem Traffic, oder Sie können bei der aktuellen Version bleiben, bis Sie bereit sind.
- Abfrageebene: Pinnen Sie die Scorer-Version in der Abfrage an und übernehmen Sie sie in den Paginierungsstatus, wie in den folgenden Schritten beschrieben. Bei diesem Ansatz bleiben laufende paginierte Abfragen auf der Scorer-Version, mit der sie gestartet wurden, sodass sie von der Umstellung nicht betroffen sind. Diese Abfragen unterliegen jedoch weiterhin Zeilenverschiebungen aufgrund gleichzeitiger Schreibvorgänge, wie bei jeder offsetbasierten Paginierung. Dieser Ansatz ist komplexer zu implementieren und für die meisten Anwendungen nicht erforderlich.
So pinnen Sie die Scorer-Version auf Abfrageebene an:
Legen Sie die Scorer-Version explizit im Argument
optionsder FunktionSCOREfest. Verwenden Sie für jede Seite dieselbe Abfrage, wobei der Offset für die erste Seite auf0gesetzt ist: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
In diesem Beispiel werden die Suchparameter
$1,$2,$3und$4verwendet, die an den Abfragestring, die Scorer-Optionen alsJSONB-Wert wie{"version": 2}, die Seitengröße und den Offset gebunden sind.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;Verwenden Sie für die erste Seite die Scorer-Version aus der Konfiguration Ihrer Anwendung. In der Regel ist dies derselbe Wert wie die Standard-Scorer-Version Ihrer Datenbank, sodass paginierte und andere Abfragen auf dieselbe Weise bewertet werden.
Speichern Sie die gebundene Scorer-Version zusammen mit dem Offset der nächsten Seite in Ihrem Paginierungsstatus, z. B. in einem Sitzungscookie oder im opaken Seitentoken, das von Ihrer API zurückgegeben wird.
Übergeben Sie für jede nachfolgende Seite die gespeicherte Scorer-Version in der Abfrage, anstatt die Version aus der aktuellen Konfiguration Ihrer Anwendung zu lesen. So bleiben die paginierten Ergebnisse auch bei Änderungen der Scorer-Version konsistent, unabhängig davon, ob sich die Standardeinstellung der Datenbank ändert oder Sie die Konfiguration Ihrer Anwendung aktualisieren.
Die in der Abfrage angegebene Scorer-Version hat Vorrang vor der Standard-Scorer-Version der Datenbank. Wenn Sie angepinnte Abfragen in eine neuere Scorer-Version verschieben möchten, ändern Sie den Wert in der Konfiguration Ihrer Anwendung.
Nächste Schritte
- Informationen zum Ranking von Suchergebnissen
- Informationen zum Ausführen einer Teilstringsuche
- Volltext- und Nicht-Text-Suchanfragen kombinieren
- Mehrere Spalten durchsuchen