Indici di ricerca

Questa pagina descrive come aggiungere e utilizzare gli indici di ricerca. La ricerca a testo intero viene eseguita sulle voci dell'indice di ricerca.

Come utilizzare gli indici di ricerca

Puoi creare un indice di ricerca su qualsiasi colonna che vuoi rendere disponibile per le ricerche full-text. Per creare un indice di ricerca, utilizza l'istruzione DDL CREATE SEARCH INDEX. Per aggiornare un indice, utilizza l'istruzione DDL ALTER SEARCH INDEX. Spanner crea e gestisce automaticamente l'indice di ricerca, inclusa l'aggiunta e l'aggiornamento dei dati nell'indice di ricerca non appena cambiano nel database.

Partizioni dell'indice di ricerca

Un indice di ricerca può essere partizionato o non partizionato, a seconda del tipo di query che vuoi accelerare.

  • Un esempio in cui un indice partizionato è la scelta migliore è quando l'applicazione esegue query su una casella di posta. Ogni query è limitata a una casella specifica.

  • Un esempio in cui una query non partizionata è la scelta migliore è quando esiste una query in tutte le categorie di prodotti di un catalogo prodotti.

Casi d'uso dell'indice della Ricerca

Oltre alla ricerca a testo intero, gli indici di ricerca Spanner supportano quanto segue:

  • Ricerche JSON, un modo efficiente per indicizzare ed eseguire query su documenti JSON e JSONB.
  • Ricerca di sottostringhe, ovvero un tipo di query che cerca una stringa più breve (la sottostringa) all'interno di un corpo di testo più grande.
  • Combinando le condizioni su qualsiasi sottoinsieme di dati indicizzati, inclusi quelli numerici e di corrispondenza esatta, in un'unica scansione dell'indice.

Per ulteriori informazioni sui casi d'uso, vedi Ricerca e indici secondari.

Esempio di indice di ricerca

Per mostrare le funzionalità degli indici di ricerca, supponiamo che esista una tabella che memorizza informazioni sugli album musicali:

GoogleSQL

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  AlbumTitle STRING(MAX)
) PRIMARY KEY(AlbumId);

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  albumtitle character varying,
PRIMARY KEY(albumid));

Spanner dispone di diverse funzioni di tokenizzazione che creano token. Per modificare la tabella precedente in modo da consentire agli utenti di eseguire una ricerca a testo intero per trovare i titoli degli album, utilizza la funzione TOKENIZE_FULLTEXT per creare token dai titoli degli album. Poi crea una colonna che utilizza il tipo di dati TOKENLIST per contenere l'output della tokenizzazione di TOKENIZE_FULLTEXT. Per questo esempio, creiamo la colonna AlbumTitle_Tokens.

GoogleSQL

ALTER TABLE Albums
  ADD COLUMN AlbumTitle_Tokens TOKENLIST
  AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN;

PostgreSQL

ALTER TABLE albums
  ADD COLUMN albumtitle_tokens spanner.tokenlist
    GENERATED ALWAYS AS (spanner.tokenize_fulltext(albumtitle)) VIRTUAL HIDDEN;

Il seguente esempio utilizza il CREATE SEARCH INDEX DDL per creare un indice di ricerca (AlbumsIndex) sui token AlbumTitle (AlbumTitle_Tokens):

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
  ON Albums(AlbumTitle_Tokens);

PostgreSQL

Questo esempio utilizza CREATE SEARCH INDEX.

CREATE SEARCH INDEX albumsindex ON albums(albumtitle_tokens);

Dopo aver aggiunto l'indice di ricerca, utilizza le query SQL per trovare gli album che corrispondono ai criteri di ricerca. Ad esempio:

GoogleSQL

SELECT AlbumId
FROM Albums
WHERE SEARCH(AlbumTitle_Tokens, "fifth symphony")

PostgreSQL

SELECT albumid
FROM albums
WHERE spanner.search(albumtitle_tokens, 'fifth symphony')

Coerenza dei dati

Quando viene creato un indice, Spanner utilizza processi automatizzati per riempire i dati per garantire la coerenza. Quando le scritture vengono eseguite, gli indici vengono aggiornati nella stessa transazione. Spanner esegue automaticamente controlli di coerenza dei dati.

Definizioni dello schema dell'indice di ricerca

Gli indici di ricerca sono definiti su una o più colonne TOKENLIST di una tabella. Gli indici di ricerca hanno i seguenti componenti:

  • Tabella di base: la tabella Spanner che richiede l'indicizzazione.
  • Colonna TOKENLIST: una raccolta di colonne che definiscono i token che devono essere indicizzati. L'ordine di queste colonne non è importante.

Ad esempio, nella seguente istruzione, la tabella di base è Albums. Le colonne TOKENLIST sono state create il giorno AlbumTitle (AlbumTitle_Tokens) e Rating (Rating_Tokens).

GoogleSQL

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  SingerId INT64 NOT NULL,
  ReleaseTimestamp INT64 NOT NULL,
  AlbumTitle STRING(MAX),
  Rating FLOAT64,
  AlbumTitle_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN,
  Rating_Tokens TOKENLIST AS (TOKENIZE_NUMBER(Rating)) HIDDEN
) PRIMARY KEY(AlbumId);

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  singerid bigint NOT NULL,
  releasetimestamp bigint NOT NULL,
  albumtitle character varying,
  rating double precision,
  albumtitle_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.tokenize_fulltext(albumtitle)) VIRTUAL HIDDEN,
  rating_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.tokenize_fulltext(rating)) VIRTUAL HIDDEN,
PRIMARY KEY(AlbumId));

Utilizza la seguente istruzione CREATE SEARCH INDEX per creare un indice di ricerca utilizzando i token per AlbumTitle e Rating:

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens, Rating_Tokens)
PARTITION BY SingerId
ORDER BY ReleaseTimestamp DESC

PostgreSQL

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens, rating_tokens)
PARTITION BY singerid
ORDER BY releasetimestamp DESC

Gli indici di ricerca hanno le seguenti opzioni:

  • Partizioni: un gruppo facoltativo di colonne che dividono l'indice di ricerca. L'esecuzione di query su un indice partizionato è spesso molto più efficiente dell'esecuzione di query su un indice non partizionato. Per ulteriori informazioni, consulta Indici di ricerca delle partizioni.
  • Colonna dell'ordine di ordinamento: una colonna INT64 facoltativa che stabilisce l'ordine di recupero dall'indice di ricerca. Per ulteriori informazioni, consulta Ordine di ordinamento dell'indice di ricerca.
  • Interleaving: come gli indici secondari, puoi eseguire l'interleaving degli indici di ricerca. Gli indici di ricerca intercalati utilizzano meno risorse per la scrittura e l'unione con la tabella di base. Per saperne di più, consulta Indici di ricerca interlacciati.
  • Clausola Options: un elenco di coppie chiave-valore che sostituiscono le impostazioni predefinite dell'indice di ricerca.

Layout interno degli indici di ricerca

Un elemento importante della rappresentazione interna degli indici di ricerca è un docid, che funge da rappresentazione efficiente in termini di spazio di archiviazione della chiave primaria della tabella di base, che può essere di lunghezza arbitraria. Inoltre, crea l'ordine per il layout dei dati interni in base alle colonne ORDER BY fornite dall'utente dell'istruzione CREATE SEARCH INDEX. È rappresentato da uno o due numeri interi a 64 bit.

Gli indici di ricerca vengono implementati internamente come mappatura a due livelli:

  1. Token per ID documento
  2. ID documento per le chiavi primarie della tabella di base

Questo schema consente un notevole risparmio di spazio di archiviazione, in quanto Spanner non deve memorizzare la chiave primaria completa della tabella di base per ogni coppia <token, document>.

Esistono due tipi di indici fisici che implementano i due livelli di mapping:

  1. Un indice secondario che mappa le chiavi di partizione e un docid alla chiave primaria della tabella di base. Nell'esempio della sezione precedente, {SingerId, ReleaseTimestamp, uid} viene mappato a {AlbumId}. L'indice secondario memorizza anche tutte le colonne specificate nella clausola STORING di CREATE SEARCH INDEX.
  2. Un indice di token che mappa i token ai docid, in modo simile agli indici invertiti nella letteratura sul recupero delle informazioni. Spanner gestisce un indice dei token separato per ogni TOKENLIST dell'indice di ricerca. A livello logico, gli indici dei token gestiscono elenchi di docid per ogni token all'interno di ogni partizione (noti nel recupero delle informazioni come elenchi di posting). Gli elenchi sono ordinati in base ai token per un rapido recupero e, all'interno degli elenchi, viene utilizzato docid per l'ordinamento. Gli indici dei singoli token sono un dettaglio di implementazione non esposto tramite le API Spanner.

Spanner supporta le seguenti quattro opzioni per docid.

Indice di ricerca Docid Comportamento
La clausola ORDER BY viene omessa per l'indice di ricerca {uid} Spanner aggiunge un valore univoco nascosto (UID) per identificare ogni riga.
ORDER BY column {column, uid} Spanner aggiunge la colonna UID come criterio di spareggio tra le righe con gli stessi valori column all'interno di una partizione.

Note sull'utilizzo:

  • La colonna UID interno non viene esposta tramite l'API Spanner.
  • Negli indici in cui non viene aggiunto l'UID, le transazioni che aggiungono una riga con un ordine di partizione e ordinamento già esistente non vanno a buon fine.

Ad esempio, considera i seguenti dati:

AlbumId SingerId ReleaseTimestamp SongTitle
a1 1 997 Beautiful days
a2 1 743 Occhi bellissimi

Supponendo che la colonna di preselezione sia in ordine crescente, il contenuto dell'indice dei token partizionato per SingerId partiziona il contenuto dell'indice dei token nel seguente modo:

SingerId _token ReleaseTimestamp uid
1 bellissimo 743 uid1
1 bellissimo 997 uid2
1 giorni 743 uid1
1 occhi 997 uid2

Sharding dell'indice di ricerca

Quando Spanner divide una tabella, distribuisce i dati dell'indice di ricerca in modo che tutti i token in una determinata riga della tabella di base si trovino nella stessa divisione. In altre parole, l'indice di ricerca è suddiviso in segmenti di documenti. Questa strategia di partizionamento ha implicazioni significative per il rendimento:

  1. Il numero di server con cui comunica ogni transazione rimane costante, indipendentemente dal numero di token o dal numero di colonne TOKENLIST indicizzate.
  2. Le query di ricerca che coinvolgono più espressioni condizionali vengono eseguite in modo indipendente su ogni suddivisione, evitando il sovraccarico delle prestazioni associato a un join distribuito.

Gli indici di ricerca hanno due modalità di distribuzione:

  • Partizionamento orizzontale uniforme (impostazione predefinita). Nel partizionamento uniforme, i dati indicizzati per ogni riga della tabella di base vengono assegnati in modo casuale a una suddivisione dell'indice di una partizione.
  • Sharding dell'ordine di ordinamento. Nel partizionamento in base all'ordine di ordinamento, i dati di ogni riga della tabella di base vengono assegnati a una suddivisione dell'indice di una partizione in base alle colonne ORDER BY (ovvero le colonne di preordinamento). Ad esempio, nel caso di un ordinamento decrescente, tutte le righe con i valori di ordinamento più grandi vengono visualizzate nella prima suddivisione dell'indice di una partizione, mentre il gruppo successivo di valori di ordinamento più grandi viene visualizzato nella suddivisione successiva.

Queste modalità di partizionamento comportano un compromesso tra i rischi di hotspot e il costo della query:

  • Gli indici di ricerca uniformi con partizionamento sono consigliati quando i pattern di lettura o scrittura nell'indice di ricerca potrebbero portare a hotspot. Lo sharding uniforme mitiga gli hotspot distribuendo il carico di lettura e scrittura in modo uniforme tra le suddivisioni, ma ciò potrebbe aumentare l'utilizzo delle risorse durante l'esecuzione delle query come compromesso. Negli indici di ricerca con partizionamento uniforme, le query devono leggere tutte le suddivisioni all'interno di una partizione a causa della distribuzione casuale dei dati. Quando accede a indici con partizionamento uniforme, Spanner legge tutte le suddivisioni in parallelo per ridurre la latenza complessiva delle query.
  • Gli indici di ricerca suddivisi in base all'ordine di ordinamento sono preferibili quando i pattern di lettura o scrittura non causano probabilmente hotspot. Questo approccio può ridurre il costo delle query il cui ORDER BY corrisponde al ORDER BY dell'indice e specifica un LIMIT relativamente basso. Quando esegue queste query, Spanner legge in modo incrementale a partire dalle prime suddivisioni di una partizione e la query può essere completata senza leggere tutte le suddivisioni quando LIMIT può essere soddisfatta in anticipo.
  • La modalità di partizionamento di un indice di ricerca viene configurata utilizzando la clausola OPTIONS.

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens, Rating_Tokens)
PARTITION BY SingerId
ORDER BY ReleaseTimestamp DESC
OPTIONS (sort_order_sharding = true);

PostgreSQL

La modalità di partizionamento di un indice di ricerca viene configurata utilizzando la clausola WITH.

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens, rating_tokens)
PARTITION BY singerid
ORDER BY releasetimestamp DESC
WITH (sort_order_sharding = true);

Quando sort_order_sharding=false è impostato o non specificato, l'indice di ricerca viene creato utilizzando lo sharding uniforme.

Indici di ricerca intercalati

Come gli indici secondari, puoi intercalare gli indici di ricerca in una tabella padre della tabella di base. Il motivo principale per utilizzare gli indici di ricerca interleaved è quello di collocare i dati della tabella di base insieme ai dati dell'indice per le partizioni di piccole dimensioni. Questa collocazione opportunistica presenta i seguenti vantaggi:

  • Le operazioni di scrittura non richiedono un commit in due fasi.
  • I back-join dell'indice di ricerca con la tabella di base non vengono distribuiti.

Gli indici di ricerca intercalati presentano le seguenti limitazioni:

  1. Solo gli indici con partizionamento <x0A>dell'ordine di ordinamento <x0A>possono essere intercalati.
  2. Gli indici di ricerca possono essere intercalati solo nelle tabelle di primo livello (non nelle tabelle secondarie).
  3. Come per le tabelle interleaved e gli indici secondari, rendi la chiave della tabella principale un prefisso delle colonne PARTITION BY nell'indice di ricerca interleaved.

Definisci un indice di ricerca intercalato

Il seguente esempio mostra come definire un indice di ricerca intercalato:

GoogleSQL

CREATE TABLE Singers (
  SingerId INT64 NOT NULL
) PRIMARY KEY(SingerId);

CREATE TABLE Albums (
  SingerId INT64 NOT NULL,
  AlbumId STRING(MAX) NOT NULL,
  AlbumTitle STRING(MAX),
  AlbumTitle_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN
) PRIMARY KEY(SingerId, AlbumId),
INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens)
PARTITION BY SingerId,
INTERLEAVE IN Singers
OPTIONS (sort_order_sharding = true);

PostgreSQL

CREATE TABLE singers(
  singerid bigint NOT NULL
PRIMARY KEY(singerid));

CREATE TABLE albums(
  singerid bigint NOT NULL,
  albumid character varying NOT NULL,
  albumtitle character varying,
  albumtitle_tokens spanner.tokenlist
  GENERATED ALWAYS
AS (
  spanner.tokenize_fulltext(albumtitle)
) VIRTUAL HIDDEN,
  PRIMARY KEY(singerid, albumid)),
INTERLEAVE IN PARENT singers ON DELETE CASCADE;

CREATE
  SEARCH INDEX albumsindex
ON
  albums(albumtitle_tokens)
  PARTITION BY singerid INTERLEAVE IN singers WITH(sort_order_sharding = true);

Ordinamento dell'indice di ricerca

I requisiti per la definizione dell'ordinamento dell'indice di ricerca sono diversi da quelli degli indici secondari.

Ad esempio, considera la seguente tabella:

GoogleSQL

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  ReleaseTimestamp INT64 NOT NULL,
  AlbumName STRING(MAX),
  AlbumName_Token TOKENLIST AS (TOKEN(AlbumName)) HIDDEN
) PRIMARY KEY(AlbumId);

PostgreSQL

CREATE TABLE albums (
  albumid character varying NOT NULL,
  releasetimestamp bigint NOT NULL,
  albumname character varying,
  albumname_token spanner.tokenlist
      GENERATED ALWAYS AS(spanner.token(albumname)) VIRTUAL HIDDEN,
PRIMARY KEY(albumid));

L'applicazione potrebbe definire un indice secondario per cercare informazioni utilizzando AlbumName ordinato per ReleaseTimestamp:

CREATE INDEX AlbumsSecondaryIndex ON Albums(AlbumName, ReleaseTimestamp DESC);

L'indice di ricerca equivalente è il seguente (utilizza la tokenizzazione con corrispondenza esatta, poiché gli indici secondari non supportano le ricerche full-text):

CREATE SEARCH INDEX AlbumsSearchIndex
ON Albums(AlbumName_Token)
ORDER BY ReleaseTimestamp DESC;

L'ordine di ordinamento dell'indice di ricerca deve essere conforme ai seguenti requisiti:

  1. Utilizza solo le colonne INT64 per l'ordine di ordinamento di un indice di ricerca. Le colonne con dimensioni arbitrarie utilizzano troppe risorse nell'indice di ricerca perché Spanner deve archiviare un docid accanto a ogni token. Nello specifico, la colonna dell'ordine di ordinamento non può utilizzare il tipo TIMESTAMP perché TIMESTAMP utilizza una precisione in nanosecondi che non rientra in un numero intero a 64 bit.
  2. Le colonne dell'ordine di ordinamento non devono essere NULL. Esistono due modi per soddisfare questo requisito:

    1. Dichiara la colonna dell'ordine di ordinamento come NOT NULL.
    2. Configura l'indice in modo da escludere i valori NULL.

Spesso viene utilizzato un timestamp per determinare l'ordine di ordinamento. Una pratica comune è utilizzare i microsecondi dall'epoca Unix per questi timestamp.

Le applicazioni in genere recuperano prima i dati più recenti utilizzando un indice di ricerca ordinato in ordine decrescente.

Indici di ricerca filtrati NULL

Gli indici di ricerca possono utilizzare la sintassi WHERE column_name IS NOT NULL per escludere le righe della tabella di base. Il filtro NULL può essere applicato a chiavi di partizionamento, colonne di ordinamento e colonne archiviate. Il filtro NULL sulle colonne dell'array memorizzato non è consentito.

Esempio

GoogleSQL

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens)
STORING (Genre)
WHERE Genre IS NOT NULL

PostgreSQL

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens)
INCLUDE (genre)
WHERE genre IS NOT NULL

La query deve specificare la condizione di filtro NULL (Genre IS NOT NULL per questo esempio) nella clausola WHERE. In caso contrario, lo strumento di ottimizzazione delle query non è in grado di utilizzare l'indice di ricerca. Per maggiori informazioni, consulta i requisiti delle query SQL.

Utilizza il filtro NULL su una colonna generata per escludere le righe in base a criteri arbitrari. Per ulteriori informazioni, vedi Creare un indice parziale utilizzando una colonna generata.

Passaggi successivi