Impagina i risultati di ricerca

Le applicazioni web spesso impaginano i dati quando vengono presentati agli utenti. L'utente finale riceve una pagina di risultati e, quando passa alla pagina successiva, viene recuperato e presentato il gruppo di risultati successivo. Questa pagina descrive come aggiungere l'impaginazione ai risultati di ricerca quando esegui una ricerca a testo intero in Spanner.

Opzioni di impaginazione

Ogni pagina di un set di risultati impaginati viene recuperata da una query indipendente e Spanner non mantiene lo stato tra le pagine. Qualsiasi elemento che cambia tra query consecutive, ad esempio i dati o l'ordinamento, può rendere i risultati incoerenti: quando l'applicazione passa alla pagina successiva, le righe possono essere saltate o le righe della pagina precedente possono essere ripetute. Questo comportamento si applica a tutte le query impaginate in Spanner, anche se il metodo di impaginazione scelto determina l'impatto di queste modifiche sui risultati.

Esistono due modi per implementare le query impaginate in Spanner: impaginazione basata su chiavi (consigliata) e impaginazione basata su offset.

La paginazione basata su chiavi è un metodo per recuperare i risultati di ricerca in blocchi più piccoli e gestibili, garantendo al contempo risultati coerenti tra le richieste. Un identificatore univoco (la "chiave") dell'ultimo risultato di una pagina viene utilizzato come punto di riferimento per recuperare l'insieme di risultati successivo.

In genere, Spanner consiglia di utilizzare l'impaginazione basata su chiavi, tranne quando si impaginano i risultati di ricerca classificati. Sebbene la paginazione basata su offset sia più facile da implementare, presenta due svantaggi significativi:

  • Costo delle query più elevato: l'impaginazione basata su offset deve recuperare e ignorare i risultati di tutte le pagine precedenti, il che comporta un aumento dei costi e una riduzione delle prestazioni.
  • Risultati incoerenti: nelle query impaginate, ogni pagina viene in genere recuperata con un timestamp di lettura diverso. Ad esempio, la prima pagina potrebbe provenire da una query eseguita alle 13:00 e la successiva da una query eseguita alle 13:10. Tra le query, qualsiasi riga inserita o eliminata prima dell'offset corrente sposta le righe che la seguono, pertanto le righe vengono saltate o ripetute nelle pagine.

La paginazione basata su chiavi, invece, utilizza un identificatore univoco (chiave) dell'ultimo risultato di una pagina per recuperare l'insieme di risultati successivo. Questo approccio garantisce un recupero efficiente e risultati più coerenti: la pagina successiva riprende dall'ultima chiave della pagina precedente, quindi solo le scritture simultanee che spostano le righe oltre questo limite influiscono sui risultati. Ad esempio, una riga appena inserita che viene ordinata prima di quella chiave viene saltata, mentre una riga restituita in precedenza che viene aggiornata per essere ordinata dopo quella chiave viene ripetuta.

Per garantire la stabilità dei risultati delle pagine, l'applicazione potrebbe emettere tutte le query per pagine diverse con lo stesso timestamp. Tuttavia, questa operazione potrebbe non riuscire se la query supera il periodo di conservazione della versione (il valore predefinito è 1 ora). Ad esempio, questo errore si verifica se version_gc è un'ora e l'utente finale ha recuperato i primi risultati alle 13:00 e ha fatto clic su Avanti alle 15:00.

Utilizza l'impaginazione basata su chiavi

La paginazione basata su chiavi memorizza l'ultimo elemento della pagina precedente e lo utilizza come punto di partenza per la query della pagina successiva. Per farlo, la query deve restituire le colonne specificate nella clausola ORDER BY e limitare il numero di righe utilizzando LIMIT.

Affinché l'impaginazione basata su chiavi funzioni, la query deve ordinare i risultati in base a un ordine totale rigoroso. Il modo più semplice per ottenerne uno è scegliere un ordine totale qualsiasi e poi aggiungere colonne di spareggio, se necessario. Nella maggior parte dei casi, l'ordinamento totale è l'ordinamento dell'indice della Ricerca e la combinazione univoca di colonne è la chiave primaria della tabella di base.

Utilizzando il nostro schema di esempio Albums, per la prima pagina, la query è simile alla seguente:

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 è il criterio di spareggio, poiché ReleaseTimestamp non è una chiave. Potrebbero esserci due album diversi con lo stesso valore per ReleaseTimestamp.

Per riprendere, l'applicazione esegue nuovamente la stessa query, ma con una clausola WHERE che limita i risultati della pagina precedente. La condizione aggiuntiva deve tenere conto della direzione della chiave (ordine crescente o decrescente), dei criteri di spareggio e dell'ordine dei valori NULL per le colonne che ammettono valori nulli.

Nel nostro esempio, AlbumId è l'unica colonna chiave (in ordine crescente) e non può essere NULL, quindi la condizione è la seguente:

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

Questo esempio utilizza parametri di ricerca $1, $2, $3 e $4, che sono associati ai valori specificati rispettivamente per last_page_release_timestamp, last_page_album_id, query e 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 interpreta questo tipo di condizione come ricercabile. Ciò significa che Spanner non legge l'indice per i documenti che stai filtrando. Questa ottimizzazione rende l'impaginazione basata su chiavi molto più efficiente dell'impaginazione basata su offset.

Utilizza l'impaginazione basata sull'offset

La paginazione basata su offset sfrutta le clausole LIMIT e OFFSET della query SQL per simulare le pagine. Il valore LIMIT indica il numero di risultati per pagina. Il valore OFFSET è impostato su zero per la prima pagina, sulle dimensioni della pagina per la seconda pagina e sul doppio delle dimensioni della pagina per la terza pagina.

Ad esempio, la seguente query recupera la terza pagina, con una dimensione di pagina pari a 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;

Note sull'utilizzo:

  • La clausola ORDER BY è vivamente consigliata per garantire un ordinamento coerente tra le pagine.
  • Nelle query di produzione, utilizza parametri di ricerca anziché le costanti per specificare LIMIT e OFFSET per rendere più efficiente la memorizzazione nella cache delle query. Per ulteriori informazioni, consulta Parametri di query.

Impagina i risultati di ricerca classificati

Spanner consiglia la paginazione basata su offset per le query con ranking, che ordinano i risultati in base al valore della funzione SCORE anziché in base alle colonne chiave della tabella. Sebbene tu possa includere l'ultimo punteggio della pagina precedente nella clausola WHERE per la pagina successiva, questa condizione non è ricercabile e pertanto non offre i vantaggi in termini di costi e prestazioni della paginazione basata su chiavi. Inoltre, i valori SCORE non sono confrontabili tra le versioni dello scorer, quindi l'impaginazione basata sull'offset produce meno righe saltate o ripetute quando la versione dello scorer cambia.

Per impaginare i risultati classificati, ordina per punteggio con la chiave primaria come criterio di spareggio e impagina con LIMIT e OFFSET. Ad esempio, la seguente query recupera la prima pagina con una dimensione di pagina pari a due:

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

Questo esempio utilizza il parametro di query $1, associato alla stringa di query.

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;

Il valore di SCORE dipende dalla versione dello scorer. Spanner utilizza la versione predefinita dello scorer del database, a meno che la query non ne imposti una. Una nuova versione dello scorer può modificare l'ordine dei risultati, quindi se la versione dello scorer cambia tra due pagine della stessa query impaginata, le pagine possono essere incoerenti. Ad esempio, supponiamo che gli album corrispondenti abbiano i seguenti punteggi in due versioni dello scorer:

AlbumId Punteggio con la versione 1 Score con la versione 2
1 0,9 0,7
2 0,8 0,95
3 0,7 0,6
4 0,6 0,85

Con la versione 1 e una dimensione di pagina pari a due, la prima pagina restituisce gli album 1 e 2. Se la versione dello scorer cambia alla versione 2 prima del recupero della seconda pagina, la seconda pagina viene calcolata con OFFSET 2 nell'ordine della versione 2, in cui gli album sono classificati 2, 4, 1, 3. Di conseguenza, la seconda pagina restituisce gli album 1 e 3, quindi l'album 1 viene ripetuto e l'album 4 viene saltato.

Le modifiche alla versione dello scorer sono rare: la versione predefinita dello scorer del database cambia solo quando la aggiorni o quando Spanner esegue la migrazione del database a una versione predefinita successiva e sono interessate solo le query impaginate in corso in quel momento. Per la maggior parte delle applicazioni, questa breve incoerenza è accettabile e Spanner consiglia di utilizzare la versione predefinita dello scorer in modo da trarre sempre vantaggio dai miglioramenti più recenti.

Se hai bisogno di un maggiore controllo sulla modifica, hai due opzioni:

  • Livello di database: imposta autonomamente la versione predefinita dello scorer del database, come descritto in Imposta la versione predefinita dello scorer del database. L'impostazione di questa opzione non evita l'incoerenza, ma ti consente di scegliere quando eseguire il passaggio, ad esempio durante un periodo di traffico ridotto, o di rimanere sulla versione corrente finché non è tutto pronto.
  • A livello di query: blocca la versione dello scorer nella query e inseriscila nello stato di impaginazione, come descritto nei passaggi seguenti. Questo approccio mantiene le query impaginate in corso nella versione dello scorer con cui sono state avviate, in modo che il passaggio non le influenzi. Tuttavia, queste query rimangono soggette allo spostamento delle righe a causa di scritture simultanee, come con qualsiasi impaginazione basata su offset. Questo approccio è più complesso da implementare e non è necessario per la maggior parte delle applicazioni.

Per bloccare la versione dello scorer a livello di query, completa i seguenti passaggi:

  1. Imposta esplicitamente la versione dello scorer nell'argomento options della funzione SCORE. Utilizza la stessa query per ogni pagina, con l'offset impostato su 0 per la prima pagina:

    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

    Questo esempio utilizza parametri di ricerca $1, $2, $3 e $4, che sono associati rispettivamente alla stringa di query, alle opzioni di valutazione come valore JSONB, ad esempio {"version": 2}, alla dimensione della pagina e all'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;
    

    Per la prima pagina, prendi la versione dello scorer dalla configurazione della tua applicazione; in genere è lo stesso valore della versione dello scorer predefinita del database, in modo che le query impaginate e altre query vengano classificate nello stesso modo.

  2. Memorizza la versione dello scorer che hai associato, insieme all'offset della pagina successiva, nello stato di impaginazione, ad esempio in un cookie di sessione o nel token di pagina opaco restituito dall'API.

  3. Per ogni pagina successiva, passa la versione dello scorer memorizzata nella query anziché leggere la versione dalla configurazione corrente dell'applicazione. Questo approccio mantiene i risultati impaginati coerenti tra le modifiche alla versione dello scorer, indipendentemente dal fatto che l'impostazione predefinita del database cambi o che tu aggiorni la configurazione dell'applicazione.

La versione dello scorer bloccata nella query ha la precedenza sulla versione predefinita dello scorer del database. Per spostare le query bloccate in una versione più recente dello scorer, modifica il valore nella configurazione dell'applicazione.

Passaggi successivi