L'AI dove vivono i dati aziendali (parte 2): Generare embeddings con Azure OpenAI e T-SQL

Punto di partenza

Nel primo articolo di questa serie dedicata all’applicazione dell’AI sui dati aziendali, abbiamo preparato il terreno per uno scenario di ricerca semantica con SQL Server 2025 o Azure SQL. Usando il database StackOverflowMini, abbiamo creato lo schema ai_demo e la tabella ai_demo.PostSearchDocuments, pensata per contenere documenti testuali costruiti a partire da domande, risposte, tag e metadati della tabella dbo.Posts.

La colonna centrale di questa struttura è DocumentText, che contiene il testo da trasformare in embedding. Accanto a questo campo abbiamo definito la colonna Embedding di tipo VECTOR, che ospiterà la rappresentazione vettoriale del testo generata dal modello text-embedding-ada-002.

Abbiamo inoltre abilitato l’opzione “external rest endpoint enabled” a livello di istanza SQL, necessaria per consentire a SQL Server di invocare endpoint REST esterni.

In questo secondo articolo useremo questa base per configurare il collegamento verso Azure OpenAI, creare un external model e generare gli embeddings direttamente da T-SQL.

Prerequisito: Il modello di embedding

Per generare embeddings da SQL Server è necessario avere a disposizione un modello di embedding invocabile tramite endpoint. Il modello può essere esposto da Azure OpenAI, da un altro servizio cloud oppure anche da un ambiente locale, purché sia raggiungibile e utilizzabile dall’engine di SQL Server secondo le modalità supportate.

Quando si lavora con dati aziendali, soprattutto se contengono informazioni riservate, dati personali, documentazione interna o contenuti soggetti a privacy policy aziendali, è importante valutare con attenzione dove viene eseguito il modello di embedding. In questi scenari, l’utilizzo di modelli locali o ospitati in ambienti controllati dall’azienda è da preferire e, in alcuni contesti, può essere un requisito obbligatorio perché consente di mantenere un maggiore controllo sul trattamento delle informazioni.

In questo articolo useremo Azure OpenAI per semplicità e per la coerenza con l’ecosistema Azure. Il modello scelto è text-embedding-ada-002, che produce embeddings con 1536 dimensioni, coerenti con la colonna Embedding VECTOR(1536) definita nella tabella ai_demo.PostSearchDocuments.

Il deployment del modello viene effettuato da Azure AI Foundry e richiede una risorsa Azure OpenAI attiva, un deployment del modello di embedding e le informazioni necessarie per invocare l’endpoint. Non entreremo nel dettaglio della configurazione del servizio Azure OpenAI, perché dipende dall’ambiente Azure utilizzato. Per la procedura completa di creazione e utilizzo di un modello di embedding con Azure OpenAI, si può fare riferimento alla documentazione ufficiale Microsoft: Learn how to generate embeddings with Azure OpenAI

Creazione dell’external model

SQL Server 2025 introduce una nuova sintassi per gestire le definizioni dei modelli di intelligenza artificiale direttamente nel database. L’istruzione CREATE EXTERNAL MODEL consente di creare un oggetto di tipo EXTERNAL MODEL contenente l’indirizzo del modello LLM, il metodo di autenticazione, il tipo di modello e il suo nome specifico. L’external model è la porta di accesso al modello di intelligenza artificiale che si desidera utilizzare in modo centralizzato e riutilizzabile.

In questo caso vogliamo registrare nel database un modello di tipo embeddings basato sulla distribuzione del modello text-embedding-ada-002. Prima di creare l’external model, però, dobbiamo predisporre una SCOPED CREDENTIAL, a livello di database, che SQL Server userà per autenticarsi verso l’endpoint esterno.

La scoped credential contiene un secret, il database deve quindi avere una master key. In un database che non dispone già di una master key, possiamo crearla con il seguente comando T-SQL.

USE [StackOverflowMini];
GO

-- https://learn.microsoft.com/sql/t-sql/statements/create-master-key-transact-sql
CREATE MASTER KEY ENCRYPTION BY PASSWORD = '<STRONG_DATABASE_MASTER_KEY_PASSWORD>';
GO

Il placeholder <STRONG_DATABASE_MASTER_KEY_PASSWORD> deve essere sostituito con una password robusta, gestita secondo le policy di sicurezza definite dall’azienda.

A questo punto possiamo creare una database scoped credential. In questo esempio usiamo una scoped credential chiamata AzureOpenAI_Embedding_Credential, mentre il secret contiene la chiave API usata per invocare il servizio.

USE [StackOverflowMini];
GO

CREATE DATABASE SCOPED CREDENTIAL [AzureOpenAI_Embedding_Credential]
WITH
  IDENTITY = 'HTTPEndpointHeaders'
  ,SECRET = '{"api-key":"<AZURE_OPENAI_API_KEY>"}';
GO

Nel codice T-SQL precedente <AZURE_OPENAI_API_KEY> rappresenta la chiave di accesso al servizio. Questo valore non deve essere pubblicato in chiaro, salvato in repository pubblici o incluso in materiale dimostrativo, perché rappresenta il secret per accedere alla distribuzione del modello. In un ambiente reale, valori come questo devono essere gestiti secondo le policy di sicurezza dell’organizzazione.

Ora possiamo creare l’external model. Il parametro LOCATION punta all’endpoint della distribuzione del modello di embedding, API_FORMAT indica il formato dell’API utilizzata, MODEL_TYPE = EMBEDDINGS specifica lo scopo del modello e MODEL indica il modello LLM usato.

USE [StackOverflowMini];
GO

CREATE EXTERNAL MODEL [AzureOpenAI_text_embedding_ada_002]
WITH
(
  LOCATION = 'https://<AZURE_OPENAI_ENDPOINT>/openai/deployments/<AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME>/embeddings?api-version=2024-02-01'
  ,API_FORMAT = 'Azure OpenAI'
  ,MODEL_TYPE = EMBEDDINGS
  ,MODEL = 'text-embedding-ada-002'
  ,CREDENTIAL = [AzureOpenAI_Embedding_Credential]
);
GO

Il placeholder <AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME> deve essere sostituito con il nome del deployment creato in Azure AI Foundry. Per verificare la definizione dell’external model appena creato, possiamo interrogare la vista di sistema sys.external_models.

USE [StackOverflowMini];
GO

SELECT
  EM.[name] AS ExternalModelName
  ,EM.model_type_desc AS ModelTypeDescription
  ,EM.[model] AS ModelName
  ,EM.[api_format] AS ApiFormat
  ,EM.[location] AS Location
  ,DSC.[name] AS CredentialName
FROM
  sys.external_models AS EM
LEFT JOIN
  sys.database_scoped_credentials AS DSC ON DSC.credential_id = EM.credential_id
WHERE
  EM.[name] = N'AzureOpenAI_text_embedding_ada_002';
GO

Ora possiamo eseguire un primo test minimale usando la funzione AI_GENERATE_EMBEDDINGS su una delle frasi più cercate in rete “Log file is full in SQL Server”.

USE [StackOverflowMini];
GO

SELECT
  AI_GENERATE_EMBEDDINGS(N'Log file is full in SQL Server' USE MODEL [AzureOpenAI_text_embedding_ada_002]);
GO

Se la configurazione è corretta, SQL Server invoca l’external model e restituisce la rappresentazione vettoriale del testo passato alla funzione. Nel passaggio successivo useremo la stessa funzione per generare gli embeddings dei documenti presenti nella tabella ai_demo.PostSearchDocuments.

Preparazione dei post per gli embeddings

Prima di generare gli embeddings dobbiamo popolare la tabella ai_demo.PostSearchDocuments con le domande e le risposte che vogliamo rendere ricercabili semanticamente.

La sorgente è la tabella dbo.Posts, in questo esempio consideriamo solo domande con risposta accettata e tag sql-server, in modo da costruire un insieme di testi coerente. Per ogni domanda selezionata recuperiamo anche la risposta accettata. Il testo finale non contiene soltanto il testo della domanda o della risposta, ma un contesto più completo composto da titolo, tag, punteggio della domanda, punteggio della risposta accettata, numero di visualizzazioni, corpo della domanda e corpo della risposta accettata.

Questo contenuto viene concatenato nella colonna DocumentText, che sarà poi passata alla funzione AI_GENERATE_EMBEDDINGS. La scelta di costruire un testo più ricco aiuta il modello a rappresentare meglio il significato del testo, perché include sia il problema descritto nella domanda sia la soluzione proposta nella risposta accettata.

Lo script limita il numero di righe a 5000. È una scelta pratica per questo articolo: generare embeddings significa invocare un modello esterno e quindi consumare tempo, risorse e, nel caso di servizi cloud, anche costi. I campi QuestionBody e AcceptedAnswerBody mantengono intenzionalmente l’HTML originale presente nel dataset Stack Overflow. In questo modo il contenuto resta aderente alla sorgente dati originale e possiamo concentrarci sull’obiettivo di questo articolo, senza introdurre una fase aggiuntiva di pulizia o normalizzazione del testo. Popoliamo ora la tabella ai_demo.PostSearchDocuments con il seguente comando di INSERT.

INSERT INTO ai_demo.PostSearchDocuments
(
  QuestionId
  ,AcceptedAnswerId
  ,Title
  ,Tags
  ,QuestionScore
  ,AcceptedAnswerScore
  ,ViewCount
  ,AnswerCount
  ,QuestionCreationDate
  ,QuestionBody
  ,AcceptedAnswerBody
  ,DocumentText
)
SELECT
  TOP (5000)
  Q.Id AS QuestionId
  ,Q.AcceptedAnswerId
  ,Q.Title
  ,Q.Tags
  ,Q.Score AS QuestionScore
  ,A.Score AS AcceptedAnswerScore
  ,Q.ViewCount
  ,Q.AnswerCount
  ,Q.CreationDate AS QuestionCreationDate
  ,LEFT(Q.Body, 4000) AS QuestionBody
  ,LEFT(A.Body, 4000) AS AcceptedAnswerBody
  ,CONCAT(
    N'Title: '
    ,Q.Title
    ,CHAR(13)
    ,CHAR(10)
    ,N'Tags: '
    ,Q.Tags
    ,CHAR(13)
    ,CHAR(10)
    ,N'Question score: '
    ,CONVERT(NVARCHAR(20), Q.Score)
    ,CHAR(13)
    ,CHAR(10)
    ,N'Accepted answer score: '
    ,CONVERT(NVARCHAR(20), A.Score)
    ,CHAR(13)
    ,CHAR(10)
    ,N'Views: '
    ,CONVERT(NVARCHAR(20), Q.ViewCount)
    ,CHAR(13)
    ,CHAR(10)
    ,N'Question: '
    ,LEFT(Q.Body, 3000)
    ,CHAR(13)
    ,CHAR(10)
    ,N'Accepted answer: '
    ,LEFT(A.Body, 3000)
  ) AS DocumentText
FROM
  dbo.Posts AS Q
JOIN
  dbo.Posts AS A ON A.Id = Q.AcceptedAnswerId
WHERE
  Q.PostTypeId = 1
  AND Q.AcceptedAnswerId IS NOT NULL
  AND Q.Tags LIKE N'%<sql-server>%'
  AND Q.Title IS NOT NULL
ORDER BY
  Q.Score DESC
  ,Q.ViewCount DESC;
GO

CREATE INDEX IX_PostSearchDocuments_QuestionScore_ViewCount ON ai_demo.PostSearchDocuments
(
  QuestionScore
  ,ViewCount
)
INCLUDE
(
  QuestionId
  ,AcceptedAnswerId
  ,Title
  ,Tags
  ,AcceptedAnswerScore
);
GO

CREATE INDEX IX_PostSearchDocuments_Tags ON ai_demo.PostSearchDocuments
(
  Tags
)
INCLUDE
(
  QuestionId
  ,Title
  ,QuestionScore
  ,AcceptedAnswerScore
);
GO

Dopo il caricamento dei documenti, creiamo due indici di supporto. Questi indici non sono indici vettoriali e non servono alla ricerca semantica vera e propria. Sono utili per interrogazioni tradizionali, filtri, ordinamenti e controlli sui dati caricati.

Generazione degli embeddings

La tabella ai_demo.PostSearchDocuments contiene ora i testi da trasformare in embeddings. Per generare la rappresentazione vettoriale useremo la funzione AI_GENERATE_EMBEDDINGS, indicando l’external model creato in precedenza.

L’aggiornamento viene eseguito a batch, con blocchi da 25 righe. Questa scelta permette di controllare meglio il processo, soprattutto quando il modello viene invocato tramite un servizio esterno. Ogni chiamata richiede tempo, può essere soggetta a limiti di throughput e, nel caso di servizi cloud, ha un costo. Lo script aggiorna solo le righe in cui la colonna Embedding è ancora NULL. In questo modo, se il processo viene interrotto, può essere rilanciato senza rielaborare i documenti già completati. La colonna EmbeddedAt viene valorizzata con la funzione SYSUTCDATETIME() per tracciare quando l’embedding è stato generato.

DECLARE
  @BatchSize INT = 25
  ,@Rows INT = 1
  ,@TotalDocuments INT
  ,@EmbeddedDocuments INT
  ,@MissingDocuments INT
  ,@PercentComplete DECIMAL(5,2)
  ,@ProgressBar NVARCHAR(20)
  ,@ProgressMessage NVARCHAR(200);

SELECT
  @TotalDocuments = COUNT(*)
  ,@EmbeddedDocuments = SUM(CASE WHEN Embedding IS NOT NULL THEN 1 ELSE 0 END)
  ,@MissingDocuments = SUM(CASE WHEN Embedding IS NULL THEN 1 ELSE 0 END)
FROM
  [ai_demo].[PostSearchDocuments];

SET @PercentComplete = 
  CASE
    WHEN @TotalDocuments = 0 THEN 100.00
  ELSE
    CONVERT(DECIMAL(5, 2), @EmbeddedDocuments * 100.0 / @TotalDocuments)
END;

SET @ProgressBar = CONCAT(
  REPLICATE(N'#', CONVERT(INT, FLOOR(@PercentComplete / 5.0)))
  ,REPLICATE(N'.', 20 - CONVERT(INT, FLOOR(@PercentComplete / 5.0)))
);

SET @ProgressMessage = CONCAT(
  N'Embedding progress ['
  ,@ProgressBar
  ,N'] '
  ,CONVERT(NVARCHAR(20), @PercentComplete)
  ,N'% ('
  ,CONVERT(NVARCHAR(20), @EmbeddedDocuments)
  ,N'/'
  ,CONVERT(NVARCHAR(20), @TotalDocuments)
  ,N')'
);

PRINT @ProgressMessage;

WHILE @Rows > 0
BEGIN
  UPDATE
    TOP (@BatchSize) D
  SET
    D.Embedding = AI_GENERATE_EMBEDDINGS
                  (
                    D.DocumentText 
                    USE MODEL [AzureOpenAI_text_embedding_ada_002]
                  )
    ,D.EmbeddedAt = SYSUTCDATETIME()
  FROM
    [ai_demo].[PostSearchDocuments] AS D
  WHERE
    D.Embedding IS NULL;

  SET @Rows = @@ROWCOUNT;

  SELECT
    @EmbeddedDocuments = SUM(CASE WHEN Embedding IS NOT NULL THEN 1 ELSE 0 END)
    ,@MissingDocuments = SUM(CASE WHEN Embedding IS NULL THEN 1 ELSE 0 END)
  FROM
    [ai_demo].[PostSearchDocuments];

  SET @PercentComplete = CASE
    WHEN @TotalDocuments = 0 THEN 100.00
    ELSE CONVERT(DECIMAL(5,2), @EmbeddedDocuments * 100.0 / @TotalDocuments)
  END;

  SET @ProgressBar = CONCAT(
    REPLICATE(N'#', CONVERT(INT, FLOOR(@PercentComplete / 5.0)))
    ,REPLICATE(N'.', 20 - CONVERT(INT, FLOOR(@PercentComplete / 5.0)))
  );

  SET @ProgressMessage = CONCAT(
    N'Embedding progress ['
    ,@ProgressBar
    ,N'] '
    ,CONVERT(NVARCHAR(20), @PercentComplete)
    ,N'% ('
    ,CONVERT(NVARCHAR(20), @EmbeddedDocuments)
    ,N'/'
    ,CONVERT(NVARCHAR(20), @TotalDocuments)
    ,N'), batch rows: '
    ,CONVERT(NVARCHAR(20), @Rows)
  );

  PRINT @ProgressMessage;

  IF @Rows > 0
    WAITFOR DELAY '00:00:01';
END;
GO

Al termine dell’esecuzione, la colonna Embedding contiene la rappresentazione vettoriale del testo contenuto in DocumentText. Da questo momento le domande e le risposte non sono più soltanto righe relazionate tra loro, ma possono essere confrontate anche in base alla vicinanza semantica.

Conclusioni

In questo articolo abbiamo configurato l’external model verso Azure OpenAI e generato gli embeddings per i testi presenti nella tabella ai_demo.PostSearchDocuments. Il testo contenuto in DocumentText è stato trasformato in una rappresentazione vettoriale salvata nella colonna Embedding, rendendo i documenti pronti per scenari di ricerca semantica.

Nel prossimo articolo useremo questi embeddings per eseguire ricerche semantiche.