What this skill tests
Three objective groups: models and embeddings, intelligent search and
retrieval-augmented generation. Expect "which function", "which metric", "ANN or ENN", "which
maintenance method" and "fix this RAG procedure" items. The platform line matters more here than anywhere: vector
indexes and VECTOR_SEARCH are GA in Azure SQL and Fabric but preview in SQL Server 2025. The worked,
end-to-end build is in the Vectors, search & RAG deep dive.
Models and embeddings
An embedding is a vector — a fixed-length array of numbers — that places a piece of text (or an image) in a space where similar meanings sit close together. An embedding model produces them; a chat model generates text. A RAG solution needs both.
Evaluate external models
| Characteristic | Why it matters for a SQL workload |
|---|---|
| Modality | Text-only versus multimodal (images, documents). Product photos or scanned forms need a multimodal model. |
| Language coverage | A multilingual embedding model maps "chaqueta" and "jacket" close together; an English-only one does not. |
| Size | Larger models reason better but cost more tokens and latency; small models are usually right for embeddings and classification. |
| Dimensions | Must equal the vector(n) column size. Some models accept a dimensions parameter to shorten output (storage versus accuracy). |
| Context window and tokens | Caps how much text one embedding or one prompt can hold, and drives cost — the reason you chunk and keep RAG context compact. |
| Structured output | Models that return JSON (JSON mode or a JSON schema) can be parsed with OPENJSON instead of string handling. |
Microsoft Foundry's model catalog is where you compare capabilities, benchmarks, versions and lifecycle status, and deploy the model whose endpoint the database will call.
Create and manage external models
An external model is a database object that stores an inference endpoint's location, API format,
model name, credential and default parameters — so T-SQL can say USE MODEL EmbeddingSmall instead of
building HTTP requests.
-- Applies to: SQL Server 2025, Azure SQL Database, Azure SQL MI (Always-up-to-date), SQL database in Fabric
CREATE EXTERNAL MODEL EmbeddingSmall
AUTHORIZATION dbo
WITH (
LOCATION = 'https://contoso-ai.openai.azure.com/openai/deployments/text-embedding-3-small/embeddings?api-version=2024-02-01',
API_FORMAT = 'Azure OpenAI', -- also 'OpenAI', 'Ollama', 'ONNX Runtime'
MODEL_TYPE = EMBEDDINGS, -- the only accepted model type
MODEL = 'text-embedding-3-small',
CREDENTIAL = [https://contoso-ai.openai.azure.com/],
PARAMETERS = '{"dimensions": 1536, "sql_rest_options": {"retry_count": 3}}'
);
ALTER EXTERNAL MODEL EmbeddingSmall SET (MODEL = 'text-embedding-3-large'); -- swap the model behind the name
GRANT EXECUTE ON EXTERNAL MODEL::EmbeddingSmall TO app_writer; -- needed to use it in AI functions
SELECT name, location, api_format, model_type, model FROM sys.external_models;
DROP EXTERNAL MODEL EmbeddingSmall; - Permissions:
CREATE EXTERNAL MODELorALTER ANY EXTERNAL MODELto manage;EXECUTE ON EXTERNAL MODEL::nameto use. - Credential: a database scoped credential named after the endpoint URL —
HTTPEndpointHeaderswith an API key, orManaged Identity(preferred; see Skill 2). - HTTPS only. On SQL Server 2025 and MI, enable
external rest endpoint enabledfirst.
External models are for embeddings
MODEL_TYPE accepts only EMBEDDINGS. To call a chat model for the generation step of
RAG you use sp_invoke_external_rest_endpoint directly — with the same credential.
Choose an embedding maintenance method
Embeddings go stale the moment their source text changes. The outline lists seven ways to keep them aligned:
| Method | How it works | Choose it when |
|---|---|---|
| Table trigger | Marks rows for re-embedding (or re-embeds) inside the write transaction | Freshness must be immediate and volume is low — it adds latency and an external call risk to every write |
| Change Tracking | A scheduled job reads CHANGETABLE and re-embeds changed rows in batches | Batching is fine; you want low overhead and only need to know which rows changed |
| Change data capture | Reads before-and-after values from change tables | High volume, or you only re-embed when the text columns really changed |
| Azure Functions SQL trigger | A function receives changed rows (via Change Tracking) and calls the model outside the database | You want the work off the database, scaling independently |
| Azure Logic Apps | A workflow polls for new or modified rows and calls the embedding service | Low-code orchestration alongside other systems |
| Change event streaming | Every change is pushed to Event Hubs; consumers re-embed | Decoupled, near-real-time, possibly several consumers (preview) |
| Microsoft Foundry | Hosts, evaluates and versions the embedding model; another process detects changes | Centralized model management — paired with one of the methods above |
Identify which columns to include
- Include the columns that carry meaning a user might search for: names, titles, descriptions, categories, reviews — concatenated into one labelled string.
- Exclude keys, codes, timestamps, numbers you filter on exactly, and sensitive data (the text is sent to the model).
- Keep filters relational: price, stock, region and tenant stay as ordinary columns in the
WHEREclause, where the latest vector indexes apply them during the search. - If the meaning of an attribute changes the answer ("for children"), put it in the text; if it is a hard constraint ("in stock"), keep it as a filter.
-- One embedding per product, built from the columns that carry meaning
UPDATE p
SET p.Embedding = AI_GENERATE_EMBEDDINGS(
CONCAT(p.Name, N'. Category: ', c.Name, N'. ', p.Description)
USE MODEL EmbeddingSmall)
FROM dbo.Product AS p
JOIN dbo.Category AS c ON c.CategoryID = p.CategoryID
WHERE p.Embedding IS NULL; -- or rows flagged as changed Design and implement chunks
Long documents are split into chunks so each fits the model's input limit and each embedding is specific enough to match a question. Too large and the embedding blurs many topics; too small and chunks lose the context needed to answer. Overlap repeats the end of one chunk at the start of the next so a sentence on a boundary is not lost. Store chunks in their own table keyed by the parent row and chunk order.
-- Chunk long text and embed every chunk in one statement (compatibility level 170)
CREATE TABLE dbo.DocChunk
(
DocID int NOT NULL,
ChunkOrder bigint NOT NULL,
ChunkText nvarchar(max) NOT NULL,
Embedding vector(1536) NOT NULL,
CONSTRAINT PK_DocChunk PRIMARY KEY (DocID, ChunkOrder)
);
INSERT dbo.DocChunk (DocID, ChunkOrder, ChunkText, Embedding)
SELECT d.DocID, c.chunk_order, c.chunk,
AI_GENERATE_EMBEDDINGS(c.chunk USE MODEL EmbeddingSmall)
FROM dbo.Document AS d
CROSS APPLY AI_GENERATE_CHUNKS(
SOURCE = d.Title + N'. ' + d.Body, -- put the context the chunk needs into the text
CHUNK_TYPE = FIXED, -- the only chunk type
CHUNK_SIZE = 1000, -- characters, not tokens
OVERLAP = 10 -- percent of CHUNK_SIZE, 0 to 50
) AS c; Generate embeddings
AI_GENERATE_EMBEDDINGS(text USE MODEL name [PARAMETERS json]) calls the model and returns the vector,
which converts implicitly into a vector(n) column of the same dimension. It runs per row, so generate in
batches (an initial backfill, then the maintenance method's changed rows) and add retry_count for
throttling (HTTP 429).
Intelligent search
Full-text, semantic vector or hybrid
flowchart TB
Q["What does the user type?"] --> A{"Exact words, codes,<br/>names, phrases?"}
A -->|"yes"| FT["Full-text search<br/>CONTAINS · FREETEXTTABLE"]
A -->|"no, a meaning"| B{"Do exact terms<br/>still matter?"}
B -->|"no"| VS["Vector search<br/>VECTOR_SEARCH · VECTOR_DISTANCE"]
B -->|"yes, both"| HY["Hybrid search<br/>full-text + vector, fused with RRF"] | Full-text | Vector | Hybrid | |
|---|---|---|---|
| Matches | Words, inflections, phrases, proximity | Meaning, synonyms, paraphrases, other languages | Both |
| Misses | Synonyms ("parka" for "coat") | Exact codes, rare names, negations | Little — at the cost of two queries |
| Needs | A full-text index | Embeddings and (for scale) a vector index | Both, plus a fusion step |
| Score | RANK from CONTAINSTABLE/FREETEXTTABLE | Distance (smaller is closer) | RRF score from the two ranks |
Implement full-text search
-- Full-text index (preview in SQL database in Fabric)
CREATE FULLTEXT CATALOG ftCatalog AS DEFAULT;
CREATE FULLTEXT INDEX ON dbo.Product (Name, Description)
KEY INDEX PK_Product
WITH CHANGE_TRACKING AUTO;
-- Predicates: filter only
SELECT ProductID, Name FROM dbo.Product
WHERE CONTAINS(Description, N'"waterproof" AND NEAR((jacket, hood), 5)');
-- Rowset functions: return a RANK to order by or fuse
SELECT TOP (20) p.ProductID, p.Name, ft.[RANK]
FROM FREETEXTTABLE(dbo.Product, (Name, Description), N'light rain jacket for hiking') AS ft
JOIN dbo.Product AS p ON p.ProductID = ft.[KEY]
ORDER BY ft.[RANK] DESC; CONTAINS— precise: words, phrases, prefixes ("hik*"),NEAR, Boolean logic.FREETEXT— natural language: stems and thesaurus.CONTAINSTABLE/FREETEXTTABLEreturn[KEY]and[RANK], which you need to sort or to fuse with vector results.- The full-text index needs a unique, single-column, non-nullable key index.
Design for vector data
vector(n)storesfloat32elements, up to 1,998 dimensions;vector(n, float16)halves the storage and allows up to 3,996.- Size is roughly 4 bytes per dimension (2 for float16): a 1,536-dimension float32 vector is about 6 KB, so keep embeddings in a narrow table (or a chunk table) rather than widening a hot OLTP table.
- Vectors are exposed as JSON arrays (
'[0.1, 0.2, …]') and convert implicitly from strings andjson. - A vector index is created per column and per metric; the latest version needs at least 100 non-NULL rows, supports full DML, and blocks
TRUNCATE TABLE.
Vector types and functions for semantic search
DECLARE @a vector(3) = '[1, 2, 2]', @b vector(3) = '[2, 1, 2]';
SELECT VECTOR_DISTANCE('cosine', @a, @b) AS cosine_distance, -- 1 - cosine similarity: 0 = same direction
VECTOR_DISTANCE('euclidean', @a, @b) AS euclidean, -- straight-line distance
VECTOR_DISTANCE('dot', @a, @b) AS neg_dot, -- negative dot product: smaller = more similar
VECTOR_NORM(@a, 'norm2') AS length, -- 3
VECTOR_NORMALIZE(@a, 'norm2') AS unit_vector, -- [0.333, 0.667, 0.667]
VECTORPROPERTY(@a, 'Dimensions') AS dims, -- 3
VECTORPROPERTY(@a, 'BaseType') AS base_type; -- float32 VECTOR_DISTANCE(metric, v1, v2)- Exact distance between two vectors — the building block of ENN search and of similarity thresholds.
VECTOR_NORMALIZE(v, 'norm2')andVECTOR_NORM- Scale a vector to unit length (or measure it). With normalized vectors,
dotorders results exactly likecosine, and is cheaper. VECTORPROPERTY(v, 'Dimensions' | 'BaseType')- Inspect a vector — useful to validate that a model returned the expected size.
VECTOR_SEARCH(TABLE, COLUMN, SIMILAR_TO, METRIC)- Approximate search through a vector index, returning the table's columns plus
distance.
ANN or ENN
| ENN (exact, kNN) | ANN (approximate) | |
|---|---|---|
| How | TOP (k) … ORDER BY VECTOR_DISTANCE(…) over every row | SELECT TOP (k) WITH APPROXIMATE … FROM VECTOR_SEARCH(…) through a DiskANN index |
| Recall | 100% by definition | High, tunable by the engine; may miss a few true neighbours |
| Cost | Grows linearly with rows | Sub-linear; scales to millions of vectors |
| Choose when | Small tables, heavily filtered subsets, or building a recall baseline | Large tables where latency matters |
DECLARE @q vector(1536) = AI_GENERATE_EMBEDDINGS(N'warm jacket for winter cycling' USE MODEL EmbeddingSmall);
-- ENN / kNN: exact, scans every row — fine up to tens of thousands of rows, or as a recall baseline
SELECT TOP (10) ProductID, Name, VECTOR_DISTANCE('cosine', Embedding, @q) AS distance
FROM dbo.Product
WHERE CategoryID = 3
ORDER BY distance;
-- ANN: approximate, served by a DiskANN vector index with the same metric
CREATE VECTOR INDEX vix_Product_Embedding ON dbo.Product (Embedding)
WITH (METRIC = 'cosine', TYPE = 'DiskANN'); -- latest version needs at least 100 non-NULL rows
SELECT TOP (10) WITH APPROXIMATE p.ProductID, p.Name, r.distance
FROM VECTOR_SEARCH(TABLE = dbo.Product AS p, COLUMN = Embedding,
SIMILAR_TO = @q, METRIC = 'cosine') AS r
WHERE p.CategoryID = 3 -- iterative filtering: applied during the search
ORDER BY r.distance; -- ascending distance only VECTOR_SEARCH rules the exam can test
- Latest-version indexes need
SELECT TOP (N) WITH APPROXIMATE; the oldTOP_Nparameter is deprecated and raises error 42274 against them. ORDER BYmust be ondistance, ascending, and nothing else;WITH APPROXIMATEwithoutVECTOR_SEARCHis an error.TOPwithoutAPPROXIMATEruns an exact kNN search; with no matching index on the same column and metric the engine falls back to kNN with a warning.GROUP BY, aggregates, window functions,UNIONandDISTINCTgo in an outer query over aVECTOR_SEARCHsubquery; it cannot be used inside a view.- The
FORCE_ANN_ONLYtable hint forces the index path.
Evaluate vector index types and metrics
| Metric | Measures | Use it when |
|---|---|---|
cosine | Angle between vectors, ignoring length | The default for text embeddings; what most embedding models are trained for |
dot | Negative dot product (direction and magnitude) | Vectors are normalized (then it ranks like cosine), or the model's docs recommend it |
euclidean | Straight-line (L2) distance | Magnitude carries meaning, for example some image or sensor embeddings |
The only index type is DiskANN, a graph-based ANN index designed to work from disk with a small memory footprint. The index metric must match the metric in the query, or the index is not used.
Implement hybrid search and reciprocal rank fusion
Full-text RANK and vector distance are on different scales, so you cannot add them.
Reciprocal rank fusion uses only each item's position in each list:
score = Σ 1 / (k + rank), with k typically 60. An item ranked well by both searches rises to the
top; an item found by only one still gets partial credit.
-- Hybrid search: fuse a keyword ranking and a semantic ranking with reciprocal rank fusion
DECLARE @text nvarchar(400) = N'light rain jacket for hiking';
DECLARE @q vector(1536) = AI_GENERATE_EMBEDDINGS(@text USE MODEL EmbeddingSmall);
DECLARE @k int = 60; -- RRF constant; 60 is the conventional default
WITH keyword AS (
SELECT TOP (50) ft.[KEY] AS ProductID,
ROW_NUMBER() OVER (ORDER BY ft.[RANK] DESC) AS rnk
FROM FREETEXTTABLE(dbo.Product, (Name, Description), @text) AS ft
),
semantic AS (
SELECT ProductID, ROW_NUMBER() OVER (ORDER BY distance) AS rnk
FROM (SELECT TOP (50) WITH APPROXIMATE p.ProductID, r.distance
FROM VECTOR_SEARCH(TABLE = dbo.Product AS p, COLUMN = Embedding,
SIMILAR_TO = @q, METRIC = 'cosine') AS r
ORDER BY r.distance) AS v -- window functions need the subquery pattern
)
SELECT TOP (10) COALESCE(k.ProductID, s.ProductID) AS ProductID,
COALESCE(1.0 / (@k + k.rnk), 0) + COALESCE(1.0 / (@k + s.rnk), 0) AS rrf_score
FROM keyword AS k
FULL OUTER JOIN semantic AS s ON s.ProductID = k.ProductID
ORDER BY rrf_score DESC; Evaluate performance of vector and hybrid search
- Relevance: measure recall of ANN against an exact (ENN) baseline for a sample of queries, and judge hybrid results on a labelled set.
- Latency and cost: check the plan uses the vector index (not a kNN scan), keep vector columns narrow, and pre-filter with indexed relational columns.
- Index health:
sys.vector_indexeslists type, metric and build parameters;sys.dm_db_vector_indexesshowsgraph_catchup_pending_percent— background maintenance still to apply after heavy DML. - Model calls: embedding the question is a network call on every search — cache frequent queries and watch for HTTP 429.
Retrieval-augmented generation (RAG)
flowchart TB
subgraph R["Retrieve"]
direction LR
Q["1 · User question"] --> EMB["2 · Embed the question<br/>AI_GENERATE_EMBEDDINGS"]
EMB --> RET["3 · Find rows<br/>VECTOR_SEARCH · hybrid"]
end
subgraph A["Augment"]
direction LR
CTX["4 · Shape context<br/>FOR JSON · JSON_OBJECT"] --> PROMPT["5 · Build the prompt<br/>system + context + question"]
end
subgraph G["Generate"]
direction LR
CALL["6 · Call the chat model<br/>sp_invoke_external_rest_endpoint"] --> ANS["7 · Extract the answer<br/>JSON_VALUE $.result…"]
end
R ~~~ A
A ~~~ G Identify use cases for RAG
- Good fits: questions answered from your own, changing data — product and support assistants, policy and document Q&A, summarising a customer's history.
- Poor fits: precise aggregates and reports ("total sales by region") — write SQL; or knowledge the model already has and that never depends on your data.
- Why not fine-tuning: RAG reads current rows at question time and respects row-level security; a fine-tuned model is a snapshot.
Create a prompt and send it with sp_invoke_external_rest_endpoint
CREATE OR ALTER PROCEDURE dbo.usp_AskCatalog
@Question nvarchar(1000),
@Answer nvarchar(max) OUTPUT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @q vector(1536) = AI_GENERATE_EMBEDDINGS(@Question USE MODEL EmbeddingSmall);
-- 1. Retrieve and 2. convert structured rows to compact JSON context
DECLARE @context nvarchar(max) = (
SELECT Name, Color, ListPrice, Description
FROM (SELECT TOP (5) WITH APPROXIMATE p.Name, p.Color, p.ListPrice, p.Description, r.distance
FROM VECTOR_SEARCH(TABLE = dbo.Product AS p, COLUMN = Embedding,
SIMILAR_TO = @q, METRIC = 'cosine') AS r
WHERE p.IsActive = 1
ORDER BY r.distance) AS hits
ORDER BY distance
FOR JSON PATH);
-- 3. Build the chat request: grounding instructions + context + question
DECLARE @payload nvarchar(max) = JSON_OBJECT(
'messages': JSON_ARRAY(
JSON_OBJECT('role': 'system',
'content': 'Answer only from the products in the context. If the answer is not there, say you do not know.'),
JSON_OBJECT('role': 'user',
'content': CONCAT('Context: ', @context, ' Question: ', @Question))),
'temperature': 0.2,
'max_tokens': 400);
-- 4. Send it to the chat model (an external model cannot do this: MODEL_TYPE is EMBEDDINGS only)
DECLARE @response nvarchar(max), @rc int;
EXECUTE @rc = sp_invoke_external_rest_endpoint
@url = N'https://contoso-ai.openai.azure.com/openai/deployments/gpt-chat/chat/completions?api-version=2024-10-21',
@method = 'POST',
@payload = @payload,
@credential = [https://contoso-ai.openai.azure.com/],
@timeout = 60,
@retry_count = 3, -- retries 408, 429, 500, 502, 503, 504
@response = @response OUTPUT;
-- 5. Extract the answer; @rc is 0 for any 2xx, otherwise the HTTP status code
SET @Answer = CASE WHEN @rc = 0
THEN JSON_VALUE(@response, '$.result.choices[0].message.content')
ELSE CONCAT('Model call failed: HTTP ', @rc) END;
END; - Arguments
-
@url(HTTPS only),@method(defaultPOST),@headers(flat JSON),@payload,@credential,@timeout(1–230 s, default 30),@retry_count(0–10) and@response OUTPUT. - Return value
0for any 2xx status; otherwise the HTTP status code itself. If the call cannot be made at all, an error is raised.- Response envelope
$.response.status.http.codeand.description,$.response.headers, and the endpoint's body under$.result.- Permissions
EXECUTE ANY EXTERNAL ENDPOINTon the database, andREFERENCESon the credential.
Convert structured data to JSON for the model
FOR JSON PATHturns a result set into an array of objects; column aliases become keys (use them to give the model readable names).JSON_OBJECTandJSON_ARRAYbuild the request body;JSON_ARRAYAGGaggregates rows into an array inside a larger object.- Send only the columns the answer needs — every column is tokens, cost and a possible data leak.
Send results and extract responses
The chat completion sits at $.result.choices[0].message.content — read it with JSON_VALUE
(scalar) or JSON_QUERY (object). Check the return code before reading, handle 429 (throttled) and
401/403 (credential) explicitly, and ask for structured output when the answer must be processed further.
-- Ask for structured output, then shred it relationally
-- (add to the payload: 'response_format': JSON_OBJECT('type': 'json_object') and say "reply in JSON")
SELECT j.ProductName, j.Reason
FROM OPENJSON(JSON_VALUE(@response, '$.result.choices[0].message.content'), '$.recommendations')
WITH (ProductName nvarchar(200) '$.name',
Reason nvarchar(400) '$.reason') AS j; Ground the model and keep it honest
Tell the model to answer only from the context and to say when it does not know; keep temperature low for factual answers; and include identifiers in the context so the app can link the answer back to real rows. Retrieval runs as the calling user, so RLS and permissions decide what can ever reach the prompt.