L'AI dove vivono i dati aziendali (parte 5): VECTOR_DISTANCE vs VECTOR_SEARCH, come cambia l'execution plan?
Introduzione
Una delle innovazioni più recenti e con più hype nel mondo SQL Server è la ricerca semantica, resa possibile grazie al nuovo tipo di dato Vector e a funzioni specifiche come VECTOR_DISTANCE e VECTOR_SEARCH.
Recentemente stavo lavorando a un progetto mettendo in pratica la ricerca semantica e mi sono trovato a cercare di capire le differenze tra le due funzioni, concentrandomi soprattutto sulle performance, scoprendo che ci sono differenze palesi ed altre meno evidenti che influenzano parecchio il modo in cui l’optimizer costruisce il piano di esecuzione.
In questa serie abbiamo già parlato, e andremo ancora più nel dettaglio in post futuri, di VECTOR_DISTANCE e VECTOR_SEARCH, in questo post invece ci concentreremo su dettagli che si rischia di trascurare quando ci si avvicina a questo mondo.
VECTOR_DISTANCE vs VECTOR_SEARCH
Prima di tutto, entrambe le funzioni servono a valutare la distanza tra un vettore e un altro.
Il caso d’uso reale può essere quello di convertire una stringa di linguaggio naturale in input in un vettore, consentendoci di cercare righe semanticamente simili in una tabella dove abbiamo memorizzato la versione vettoriale delle righe (il vettore può essere creato utilizzando solo le colonne rilevanti per la ricerca). In scenari reali le tabelle possono essere anche di grandi dimensioni e le performance possono diventare un fattore importante.
Per migliorare le performance di ricerca su una colonna vettoriale, è possibile creare un indice ANN (Approximate Nearest Neighbours).
CREATE VECTOR INDEX vi_mytable_myvectorcolumn
ON dbo.mytable(myvectorcolumn)
WITH ( METRIC = 'cosine' );
GO
L’indicizzazione di un vettore ci permette di mettere in luce la prima differenza degna di nota tra le due funzioni, ossia, la capacità di VECTOR_SEARCH di eseguire una seek sull’indice, e l’incapacità di VECTOR_DISTANCE di farlo.
In altre parole, indipendentemente dal fatto che la vostra colonna vettoriale sia indicizzata o meno, una query con VECTOR_DISTANCE eseguirà comunque un table scan.
Sentendo questo, potreste essere tentati di considerare VECTOR_DISTANCE come una funzione “di seconda categoria” rispetto a VECTOR_SEARCH, ma in realtà le funzioni sono disegnate per uno scopo diverso, e lo raggiungono in modi diversi.
La prima restituisce un risultato esatto, dando in output la lista esatta e completa di tutti i vettori che soddisfano il criterio di similarità (distanza) richiesto. Per ottenere una corrispondenza esatta e completa l’unico modo è scansionare ogni vettore e valutare la distanza con l’input riga per riga su tutta la tabella.
D’altra parte, VECTOR_SEARCH può usare un indice ANN per effettuare una seek, approssimando i vettori che potrebbero essere vicini all’input. In altre parole, otteniamo un guadagno di performance, ma accettiamo un’approssimazione (non un’accuratezza del 100%).
VECTOR_DISTANCE e l’Index Scan
Nell’esempio seguente, stiamo interrogando una tabella con un indice ANN sulla colonna vettoriale usando VECTOR_DISTANCE, e possiamo verificare nel piano di esecuzione che l’indice viene ignorato e che l’optimizer esegue un index scan.
SELECT TOP(10) *
FROM dbo.mytable
WHERE vector_distance('cosine', @input, myvectorcolumn) < 0.2;

Come forse già sapete, anche nel caso di “normali” indici clustered e nonclustered, avere un indice non significa automaticamente una seek, ma quando si parla di semantic search le regole sono ancora più stringenti, esiste infatti un elenco specifico di condizioni che devono essere soddisfatte per sfruttare l’indice ANN ed eseguire seek veloci.
- la query deve usare VECTOR_SEARCH
- la query deve specificare TOP (N) WITH APPROXIMATE
- la query deve specificare ORDER BY distance
Per un elenco completo e dettagliato, fate riferimento alla documentazione ufficiale, leggetela con attenzione perché, decisamente, non è banale.
Se tutti i requisiti sono soddisfatti, una query con VECTOR_SEARCH eseguirà una seek sull’indice prima di restituire un risultato.

WITH APPROXIMATE
Come cambia il piano se ometto WITH APPROXIMATE? Sostanzialmente è come se stessimo dicendo al motore: “voglio un risultato esatto”.
Di conseguenza, l’optimizer passa a una scansione completa della colonna vettoriale per ottenere tutte le righe corrispondenti, anche se stiamo utilizzando VECTOR_SEARCH.

Fin qui, tutto chiaro. Ma se guardate più da vicino il vector index seek, noterete qualcosa di particolare: cambiare il valore di TOP influisce direttamente sull’operazione di index seek.
Se ricordate, il normale ordine di esecuzione di SQL è:
FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, TOP
Quindi ci si aspetterebbe che TOP si limiti a ridurre il numero di righe da proiettare, una volta che sono già state recuperate dal FROM, ma nel caso di vector index, non è così.
Il parametro di TOP viene passato all’Index Seek per recuperare un numero specifico di “vicini del vettore” (neighbours).

TOP e Over-fetching
In realtà, il valore specificato in TOP ha un duplice ruolo, perché guida l’Index Seek, che effettuerà un “over-fetch” di righe in base al numero richiesto, e poi, come al solito, guida la proiezione delle prime X righe dal dataset recuperato.
Per esempio, se vengono richieste 5 righe, l’Index Seek potrebbe recuperarne 10.
Incrementando a 15 righe, il seek recupererà 30 righe.

Perché questo comportamento?
Senza entrare troppo nei dettagli di funzionamento interno dell’ANN, l’over-fetching seguito da un re-rank (Sort + Top N Sort che si vede nel piano) è ciò che avvicina la ricerca approssimativa al risultato reale, restando comunque più economico rispetto a una full scan.
In altre parole, è un modo per ridurre il margine di errore.
D’altra parte, in caso di scan, TOP avrà il comportamento consueto, applicandosi subito dopo l’ORDER BY.

Conclusione
La cosa importante da tenere a mente è che VECTOR_DISTANCE e VECTOR_SEARCH non sono semplicemente due modi diversi per ottenere lo stesso risultato. VECTOR_DISTANCE è orientato ai risultati esatti, mentre VECTOR_SEARCH è progettato per sfruttare gli indici ANN e migliorare le performance accettando un’approssimazione. Spetta a noi valutare se la nostra applicazione può accettare un certo margine di errore oppure necessitiamo di risultati esatti. Bisogna poi ricordare che, quando si utilizzano queste funzioni, altri operatori possono modificare il loro comportamento in base alla circostanza, per esempio, TOP potrebbe non effettuare solo un’operazione di “proiezione finale”, ma arrivare a influenzare direttamente il vector index seek e la quantità di dati che vengono recuperati (guidando direttamente l’over-fetching). Capire queste “sottili” differenze può fare una grande differenza quando si analizzano i piani di esecuzione e si cerca di ottimizzare le ricerche semantiche sui vettori.
Disclaimer contenuto generato da IA
Questo footer-disclaimer indica al lettore se questo post contiene contenuto generato dall’IA. In questo post l’IA è stata usata per:
- Formattare il testo (grassetto, evidenziazioni, ecc.)
- Ottenere alcuni spunti sugli algoritmi ANN
- Generare l’immagine di copertina