Procedura per cercare documenti per significato invece che per parole. Un modello trasforma ogni testo in una serie di numeri, Azure AI Search li conserva e trova i più vicini alla domanda. Tutto da un'applicazione console .NET, senza chiavi nel codice.

Occorre: un account Azure con una sottoscrizione attiva, .NET 10 e l'interfaccia a riga di comando di Azure sul computer, un browser. Durata: circa trenta minuti. Costo: il servizio di ricerca nel piano Free non si paga: il portale indica 0,00 USD al mese. Gli embedding si pagano a consumo sui token, e la prova ne usa pochi.

Il portale delle schermate è in inglese: i nomi dei comandi sono riportati come appaiono a schermo.

Preparare le risorse

1. Crea il servizio di ricerca

Su portal.azure.com, cerca AI Search e premi + Create. Sotto Resource group premi Create new e scrivi il nome: il gruppo si crea qui, non prima. Poi premi Change Pricing Tier e scegli Free.

Campo Valore
Resource group rg-guide-ais-01 (nuovo)
Service name ais-guide-01
Location (Europe) West Europe
Pricing tier Free

Il piano Free dà 3 indici, 50 MB di spazio e 25 MB per i vettori: ce n'è d'avanzo per una prova, ed è uno solo per sottoscrizione. Premi Review + create e poi Create.

Creazione del servizio di ricerca nel piano Free

Le regioni compaiono e spariscono da quell'elenco quando la domanda è alta. Se West Europe non c'è, scegline un'altra e tienila uguale per tutte le risorse della guida.

2. Togli le chiavi

Nel servizio appena creato apri Security + networking → Keys e scegli Role-based access control, poi conferma. Da qui in avanti il servizio accetta solo identità, non più le chiavi di amministrazione, che infatti spariscono dalla pagina.

Accesso al servizio solo tramite ruoli

3. Crea la risorsa dei modelli

Serve un modello che trasformi i testi in vettori. Si crea da riga di comando:

az cognitiveservices account create \
  --name foundry-guide-ais-01 \
  --resource-group rg-guide-ais-01 \
  --kind AIServices --sku S0 --location westeurope \
  --custom-domain foundry-guide-ais-01 --yes

--custom-domain non è facoltativo: senza, la risorsa riceve solo l'indirizzo comune della regione, l'indirizzo foundry-guide-ais-01.openai.azure.com non esiste e l'applicazione si ferma con No such host is known. Il nome del dominio deve essere unico in tutta Azure.

4. Distribuisci il modello

az cognitiveservices account deployment create \
  --name foundry-guide-ais-01 \
  --resource-group rg-guide-ais-01 \
  --deployment-name text-embedding-3-small \
  --model-name text-embedding-3-small \
  --model-version 1 --model-format OpenAI \
  --sku-capacity 1 --sku-name GlobalStandard

text-embedding-3-small restituisce 1536 numeri per ogni testo: è il numero da ripetere nella definizione dell'indice.

5. Assegna i ruoli

Senza chiavi, il permesso è un ruolo sulla risorsa. Ne servono tre:

az login
ID=$(az ad signed-in-user show --query id -o tsv)
SUB=$(az account show --query id -o tsv)
BASE="/subscriptions/$SUB/resourceGroups/rg-guide-ais-01/providers"
RICERCA="$BASE/Microsoft.Search/searchServices/ais-guide-01"
MODELLI="$BASE/Microsoft.CognitiveServices/accounts/foundry-guide-ais-01"

az role assignment create --assignee-object-id $ID --assignee-principal-type User \
  --role "Search Service Contributor" --scope $RICERCA
az role assignment create --assignee-object-id $ID --assignee-principal-type User \
  --role "Search Index Data Contributor" --scope $RICERCA
az role assignment create --assignee-object-id $ID --assignee-principal-type User \
  --role "Cognitive Services OpenAI User" --scope $MODELLI

Il primo ruolo serve a creare l'indice, il secondo a caricare e leggere i documenti, il terzo a chiamare il modello.

Scrivere l'applicazione

6. Crea il progetto

dotnet new console -n ricerca-demo
cd ricerca-demo
dotnet add package Azure.Identity --version 1.21.0
dotnet add package Azure.Search.Documents --version 12.0.0
dotnet add package OpenAI --version 2.14.0

7. Scrivi il codice

Sostituisce Program.cs. Fa tre cose: definisce l'indice, carica cinque frasi con il loro vettore, poi cerca i tre documenti più vicini a una domanda.

using Azure.Identity;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;
using Azure.Search.Documents.Models;
using OpenAI;
using OpenAI.Embeddings;
using System.ClientModel.Primitives;

#pragma warning disable OPENAI001

const string ricerca = "https://ais-guide-01.search.windows.net";
const string modelli = "https://foundry-guide-ais-01.openai.azure.com/openai/v1";
const string modello = "text-embedding-3-small";
const string indice = "documenti";

DefaultAzureCredential credenziale = new();

BearerTokenPolicy tokenPolicy = new(
    credenziale,
    "https://cognitiveservices.azure.com/.default");
EmbeddingClient embedder = new(
    modello,
    tokenPolicy,
    new OpenAIClientOptions { Endpoint = new Uri(modelli) });

string[] testi =
[
    "Il gruppo di risorse raccoglie le risorse di una soluzione e si elimina in un colpo solo.",
    "Azure Container Apps esegue contenitori senza gestire macchine o Kubernetes.",
    "Azure AI Search indicizza documenti e li cerca per parole o per somiglianza.",
    "Azure Bastion apre il desktop di una macchina virtuale dentro il browser.",
    "Il registro container conserva le immagini da cui partono i contenitori.",
];

// 1. L'indice, con un campo vettoriale da 1536 numeri.
SearchIndexClient clientIndici = new(new Uri(ricerca), credenziale);

SearchIndex definizione = new(indice)
{
    Fields =
    {
        new SimpleField("id", SearchFieldDataType.String) { IsKey = true },
        new SearchableField("testo"),
        new SearchField("vettore", SearchFieldDataType.Collection(SearchFieldDataType.Single))
        {
            IsSearchable = true,
            VectorSearchDimensions = 1536,
            VectorSearchProfileName = "profilo",
        },
    },
    VectorSearch = new VectorSearch
    {
        Algorithms = { new HnswAlgorithmConfiguration("hnsw") },
        Profiles = { new VectorSearchProfile("profilo", "hnsw") },
    },
};

await clientIndici.CreateOrUpdateIndexAsync(definizione);
Console.WriteLine($"Indice '{indice}' pronto.");

// 2. Un vettore per ogni testo, poi il caricamento.
SearchClient clientRicerca = new(new Uri(ricerca), indice, credenziale);

List<SearchDocument> documenti = [];
for (int i = 0; i < testi.Length; i++)
{
    var vettore = await embedder.GenerateEmbeddingAsync(testi[i]);
    documenti.Add(new SearchDocument
    {
        ["id"] = (i + 1).ToString(),
        ["testo"] = testi[i],
        ["vettore"] = vettore.Value.ToFloats().ToArray(),
    });
}

await clientRicerca.UploadDocumentsAsync(documenti);
Console.WriteLine($"Caricati {documenti.Count} documenti.");

await Task.Delay(TimeSpan.FromSeconds(3));

// 3. La domanda diventa un vettore e si cercano i tre più vicini.
const string domanda = "come faccio a cancellare tutto insieme?";
var vettoreDomanda = await embedder.GenerateEmbeddingAsync(domanda);

SearchOptions opzioni = new() { Size = 3 };
opzioni.Select.Add("testo");
opzioni.VectorSearch = new VectorSearchOptions
{
    Queries =
    {
        new VectorizedQuery(vettoreDomanda.Value.ToFloats())
        {
            KNearestNeighborsCount = 3,
            Fields = { "vettore" },
        },
    },
};

Console.WriteLine($"\nDomanda: {domanda}\n");

var risultati = await clientRicerca.SearchAsync<SearchDocument>(searchText: null, opzioni);
await foreach (var r in risultati.Value.GetResultsAsync())
{
    Console.WriteLine($"{r.Score:F4}  {r.Document["testo"]}");
}

#pragma warning disable OPENAI001 serve perché il costruttore del client con l'identità è marcato sperimentale: senza, la compilazione si ferma. L'attesa di tre secondi dopo il caricamento serve perché l'indice impiega un attimo a rendere ricercabili i documenti appena arrivati.

8. Esegui

dotnet run
Indice 'documenti' pronto.
Caricati 5 documenti.

Domanda: come faccio a cancellare tutto insieme?

0,6509  Il gruppo di risorse raccoglie le risorse di una soluzione e si elimina in un colpo solo.
0,5926  Il registro container conserva le immagini da cui partono i contenitori.
0,5630  Azure Bastion apre il desktop di una macchina virtuale dentro il browser.

La frase giusta arriva prima pur non avendo nessuna parola in comune con la domanda: né «cancellare» né «insieme» compaiono nel testo trovato. È questa la differenza con la ricerca per parole.

Guardare il risultato dal portale

9. L'indice

Nel servizio di ricerca apri Search management → Indexes: c'è documenti con 5 documenti.

L'indice documenti con cinque documenti

I contatori dello spazio si aggiornano con qualche minuto di ritardo: subito dopo il caricamento segnano ancora zero.

Aprendolo, la scheda Fields mostra i tre campi: la chiave, il testo e il campo vettoriale con la sua dimensione, 1536.

I campi dell'indice, con il vettore da 1536

10. La ricerca per parole, per confronto

Nella scheda Search explorer scrivi contenitori e premi Search: tornano due documenti, quelli che contengono la parola. La ricerca per parole trova le parole, quella per vettori trova il significato.

Search explorer con due risultati per la parola contenitori

Eliminare le risorse

11. Elimina il gruppo di risorse

Apri rg-guide-ais-01: contiene il servizio di ricerca e la risorsa dei modelli.

Le due risorse dentro il gruppo

Premi Delete resource group, riscrivi il nome del gruppo e premi Delete.

Pannello di eliminazione con il nome riscritto

Conferma nella finestra che compare.

Richiesta di conferma definitiva

Eliminato il gruppo, la risorsa dei modelli resta in cancellazione soft e il suo nome non è riutilizzabile. Per liberarlo subito:

az cognitiveservices account purge \
  --name foundry-guide-ais-01 \
  --location westeurope \
  --resource-group rg-guide-ais-01

Nomi dei comandi e schermate rilevati sul portale di Azure il 4 novembre 2026, regione West Europe, con .NET 10.0.401 e l'interfaccia a riga di comando di Azure 2.91.0.