Add the vector policy source embedding controls

This commit is contained in:
Sung-Hyun Kang
2026-06-16 11:03:20 -05:00
parent 74b6a92219
commit c6139c0cd1
11 changed files with 493 additions and 59 deletions
+12
View File
@@ -66,6 +66,8 @@ export interface DatabaseAccountExtendedProperties {
enablePriorityBasedExecution?: boolean;
vcoreMongoEndpoint?: string;
enableAllVersionsAndDeletesChangeFeed?: boolean;
enableFullFidelityChangeFeed?: boolean;
enableEmbeddingGenerator?: boolean;
}
export interface DatabaseAccountResponseLocation {
@@ -414,6 +416,7 @@ export interface AccountOverride {
capacityMode?: CapacityMode;
enableFreeTier?: boolean;
enableAnalyticalStorage?: boolean;
enableEmbeddingGenerator?: boolean;
}
export interface CreateDatabaseParams {
@@ -458,6 +461,15 @@ export interface VectorEmbedding {
dimensions: number;
distanceFunction: "euclidean" | "cosine" | "dotproduct";
path: string;
embeddingSource?: VectorEmbeddingSource;
}
export interface VectorEmbeddingSource {
sourcePaths: string[];
deploymentName: string;
modelName: string;
endpoint: string;
authType: "Entra";
}
export interface FullTextPolicy {
@@ -13,6 +13,7 @@ export interface CollapsibleSectionProps {
onDelete?: () => void;
disabled?: boolean;
disableDelete?: boolean;
dataTest?: string;
}
export interface CollapsibleSectionState {
@@ -57,6 +58,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
tabIndex={0}
role="button"
aria-expanded={this.state.isExpanded}
data-test={this.props.dataTest}
>
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
<Label styles={{ root: { color: "var(--colorNeutralForeground1)" } }}>{this.props.title}</Label>
@@ -2,8 +2,14 @@ import "@testing-library/jest-dom";
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import React from "react";
import * as CapabilityUtils from "Utils/CapabilityUtils";
import { VectorEmbeddingPoliciesComponent } from "./VectorEmbeddingPoliciesComponent";
// The embedding-source UI is gated behind the integrated-embedding capability.
// Default the mock to `true` for the existing suites so they exercise the full UI;
// the dedicated suite at the bottom of this file flips it to `false` to verify gating.
jest.spyOn(CapabilityUtils, "isIntegratedEmbeddingEnabled").mockReturnValue(true);
const mockVectorEmbedding: VectorEmbedding[] = [
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
];
@@ -81,3 +87,197 @@ describe("AddVectorEmbeddingPolicyForm", () => {
});
});
});
describe("VectorEmbeddingPoliciesComponent - embedding source", () => {
const newEmbedding: VectorEmbedding[] = [
{ path: "/vector2", dataType: "float32", distanceFunction: "cosine", dimensions: 1536 },
];
let onChange: jest.Mock;
let view: RenderResult;
beforeEach(() => {
onChange = jest.fn();
view = render(
<VectorEmbeddingPoliciesComponent
vectorEmbeddingsBaseline={[]}
vectorEmbeddings={newEmbedding}
vectorIndexes={[]}
onVectorEmbeddingChange={onChange}
/>,
);
});
const expandSection = () => {
const header = screen.getByRole("button", { name: /Embedding source/ });
fireEvent.click(header);
};
test("renders the embedding source accordion collapsed by default with no errors", () => {
expect(screen.getByText("Embedding source (Preview)")).toBeInTheDocument();
expect(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1")).toBeNull();
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
expect(lastCall[2]).toBe(true);
expect(lastCall[0][0].embeddingSource).toBeUndefined();
});
test("expanding the accordion reveals all source fields without errors when empty", () => {
expandSection();
expect(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1")).not.toBeNull();
expect(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1")).not.toBeNull();
expect(view.container.querySelector("#vector-policy-embeddingSource-modelName-1")).not.toBeNull();
expect(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1")).not.toBeNull();
expect(view.container.querySelector("#vector-policy-embeddingSource-authType-1")).not.toBeNull();
expect(screen.queryByText("At least one source path is required")).toBeNull();
expect(screen.queryByText("Deployment name is required")).toBeNull();
});
test("typing in one field surfaces required errors for the others", async () => {
expandSection();
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1"), {
target: { value: "my-deployment" },
});
await waitFor(() => {
expect(screen.getByText("At least one source path is required")).toBeInTheDocument();
expect(screen.getByText("Model name is required")).toBeInTheDocument();
expect(screen.getByText("Endpoint is required")).toBeInTheDocument();
});
const last = onChange.mock.calls[onChange.mock.calls.length - 1];
expect(last[2]).toBe(false);
});
test("invalid endpoint shows the https:// error", async () => {
expandSection();
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
target: { value: "not-a-url" },
});
await waitFor(() => expect(screen.getByText("Endpoint must be a valid https:// URL")).toBeInTheDocument());
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
target: { value: "http://insecure.example.com" },
});
await waitFor(() => expect(screen.getByText("Endpoint must be a valid https:// URL")).toBeInTheDocument());
});
test("valid input propagates an embeddingSource with parsed sourcePaths", async () => {
expandSection();
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1"), {
target: { value: "/description, title" },
});
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1"), {
target: { value: "my-deployment" },
});
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-modelName-1"), {
target: { value: "text-embedding-3-small" },
});
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
target: { value: "https://my-foundry.openai.azure.com" },
});
await waitFor(() => {
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
const [embeddings, , valid] = lastCall;
expect(valid).toBe(true);
expect(embeddings[0].embeddingSource).toEqual({
sourcePaths: ["/description", "/title"],
deploymentName: "my-deployment",
modelName: "text-embedding-3-small",
endpoint: "https://my-foundry.openai.azure.com",
authType: "Entra",
});
});
});
test("clearing all fields drops embeddingSource from the emitted embedding", async () => {
expandSection();
const sourcePaths = view.container.querySelector(
"#vector-policy-embeddingSource-sourcePaths-1",
) as HTMLInputElement;
const deploymentName = view.container.querySelector(
"#vector-policy-embeddingSource-deploymentName-1",
) as HTMLInputElement;
const modelName = view.container.querySelector("#vector-policy-embeddingSource-modelName-1") as HTMLInputElement;
const endpoint = view.container.querySelector("#vector-policy-embeddingSource-endpoint-1") as HTMLInputElement;
fireEvent.change(sourcePaths, { target: { value: "/description" } });
fireEvent.change(deploymentName, { target: { value: "d" } });
fireEvent.change(modelName, { target: { value: "m" } });
fireEvent.change(endpoint, { target: { value: "https://x.example.com" } });
await waitFor(() => {
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
expect(lastCall[2]).toBe(true);
expect(lastCall[0][0].embeddingSource).toBeDefined();
});
fireEvent.change(sourcePaths, { target: { value: "" } });
fireEvent.change(deploymentName, { target: { value: "" } });
fireEvent.change(modelName, { target: { value: "" } });
fireEvent.change(endpoint, { target: { value: "" } });
await waitFor(() => {
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
const [embeddings, , valid] = lastCall;
expect(valid).toBe(true);
expect(embeddings[0].embeddingSource).toBeUndefined();
});
});
test("does not loop onVectorEmbeddingChange once all fields become valid (regression)", async () => {
// Regression for an infinite render loop where the child rebuilt a fresh
// VectorEmbeddingSource literal on every render, defeating the parent's
// reference-equality dedupe and re-triggering child useEffect via the
// unstable onChange prop. Before the fix, this scenario hung the test
// runner (Jest would time out after several minutes).
expandSection();
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1"), {
target: { value: "/title, /description" },
});
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1"), {
target: { value: "text-embedding-ada-002" },
});
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-modelName-1"), {
target: { value: "text-embedding-ada-002" },
});
fireEvent.change(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1"), {
target: { value: "https://my-foundry.openai.azure.com" },
});
await waitFor(() => {
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1];
expect(lastCall[2]).toBe(true);
expect(lastCall[0][0].embeddingSource).toBeDefined();
});
const stable = onChange.mock.calls.length;
await new Promise((resolve) => setTimeout(resolve, 100));
expect(onChange.mock.calls.length).toBe(stable);
});
});
describe("VectorEmbeddingPoliciesComponent - embedding source gating", () => {
const newEmbedding: VectorEmbedding[] = [
{ path: "/vector3", dataType: "float32", distanceFunction: "cosine", dimensions: 1536 },
];
test("hides the embedding source accordion when the integrated-embedding capability is missing", () => {
const isEnabledSpy = jest.spyOn(CapabilityUtils, "isIntegratedEmbeddingEnabled").mockReturnValue(false);
try {
const view = render(
<VectorEmbeddingPoliciesComponent
vectorEmbeddingsBaseline={[]}
vectorEmbeddings={newEmbedding}
vectorIndexes={[]}
onVectorEmbeddingChange={jest.fn()}
/>,
);
expect(screen.queryByText("Embedding source (Preview)")).toBeNull();
expect(view.container.querySelector("#vector-policy-embeddingSource-sourcePaths-1")).toBeNull();
expect(view.container.querySelector("#vector-policy-embeddingSource-deploymentName-1")).toBeNull();
expect(view.container.querySelector("#vector-policy-embeddingSource-modelName-1")).toBeNull();
expect(view.container.querySelector("#vector-policy-embeddingSource-endpoint-1")).toBeNull();
expect(view.container.querySelector("#vector-policy-embeddingSource-authType-1")).toBeNull();
} finally {
isEnabledSpy.mockReturnValue(true);
}
});
});
@@ -1,17 +1,8 @@
import {
DefaultButton,
Dropdown,
IDropdownOption,
IStyleFunctionOrObject,
ITextFieldStyleProps,
ITextFieldStyles,
Label,
Stack,
TextField,
} from "@fluentui/react";
import { DefaultButton, Dropdown, IDropdownOption, Label, Stack, TextField } from "@fluentui/react";
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
import { VectorEmbedding, VectorEmbeddingSource, VectorIndex } from "Contracts/DataModels";
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { VectorEmbeddingSourceComponent } from "Explorer/Controls/VectorSearch/VectorEmbeddingSourceComponent";
import {
getDataTypeOptions,
getDistanceFunctionOptions,
@@ -19,8 +10,15 @@ import {
getQuantizerTypeOptions,
supportsQuantization,
} from "Explorer/Controls/VectorSearch/VectorSearchUtils";
import { dropdownStyles, labelStyles, textFieldStyles } from "Explorer/Controls/VectorSearch/vectorSearchStyles";
import { Keys, t } from "Localization";
import React, { FunctionComponent, useState } from "react";
import React, { FunctionComponent, useCallback, useState } from "react";
import { isIntegratedEmbeddingEnabled } from "Utils/CapabilityUtils";
const generatePolicyId = (): string =>
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: `vep-${Date.now()}-${Math.random().toString(36).slice(2)}`;
export interface IVectorEmbeddingPoliciesComponentProps {
vectorEmbeddingsBaseline: VectorEmbedding[];
@@ -37,6 +35,7 @@ export interface IVectorEmbeddingPoliciesComponentProps {
}
export interface VectorEmbeddingPolicyData {
id: string;
path: string;
dataType: VectorEmbedding["dataType"];
distanceFunction: VectorEmbedding["distanceFunction"];
@@ -50,44 +49,12 @@ export interface VectorEmbeddingPolicyData {
quantizationByteSize?: number;
quantizationByteSizeError?: string;
quantizerType?: VectorIndex["quantizerType"];
embeddingSource?: VectorEmbeddingSource;
embeddingSourceValid: boolean;
}
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
const labelStyles = {
root: {
fontSize: 12,
color: "var(--colorNeutralForeground1)",
},
};
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
fieldGroup: {
height: 27,
},
field: {
fontSize: 12,
padding: "0 8px",
backgroundColor: "var(--colorNeutralBackground1)",
color: "var(--colorNeutralForeground1)",
},
};
const dropdownStyles = {
title: {
height: 27,
lineHeight: "24px",
fontSize: 12,
},
dropdown: {
height: 27,
lineHeight: "24px",
},
dropdownItem: {
fontSize: 12,
},
};
export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddingPoliciesComponentProps> = ({
vectorEmbeddingsBaseline,
vectorEmbeddings,
@@ -163,6 +130,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
const supportsQuantizer = supportsQuantization(matchingType);
mergedData.push({
...embedding,
id: generatePolicyId(),
indexType: matchingType || "none",
indexingSearchListSize: matchingIndex?.indexingSearchListSize || undefined,
quantizationByteSize: matchingIndex?.quantizationByteSize || undefined,
@@ -170,6 +138,8 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
vectorIndexShardKey: matchingIndex?.vectorIndexShardKey || undefined,
pathError: onVectorEmbeddingPathError(embedding.path),
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
embeddingSource: embedding.embeddingSource,
embeddingSourceValid: true,
});
});
@@ -193,12 +163,18 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
}, [discardChanges]);
const propagateData = () => {
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
path: policy.path,
dataType: policy.dataType,
dimensions: policy.dimensions,
distanceFunction: policy.distanceFunction,
}));
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => {
const base: VectorEmbedding = {
path: policy.path,
dataType: policy.dataType,
dimensions: policy.dimensions,
distanceFunction: policy.distanceFunction,
};
if (policy.embeddingSource) {
base.embeddingSource = policy.embeddingSource;
}
return base;
});
const vectorIndexes: VectorIndex[] = vectorEmbeddingPolicyData
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
.map(
@@ -215,7 +191,8 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
}) as VectorIndex,
);
const validationPassed = vectorEmbeddingPolicyData.every(
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
(policy: VectorEmbeddingPolicyData) =>
policy.pathError === "" && policy.dimensionsError === "" && policy.embeddingSourceValid,
);
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexes, validationPassed);
@@ -306,10 +283,29 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
setVectorEmbeddingPolicyData(vectorEmbeddings);
};
const onEmbeddingSourceChange = useCallback(
(index: number, embeddingSource: VectorEmbeddingSource | undefined, isValid: boolean): void => {
setVectorEmbeddingPolicyData((prev) => {
const current = prev[index];
if (!current) {
return prev;
}
if (current.embeddingSource === embeddingSource && current.embeddingSourceValid === isValid) {
return prev;
}
const next = [...prev];
next[index] = { ...current, embeddingSource, embeddingSourceValid: isValid };
return next;
});
},
[],
);
const onAdd = () => {
setVectorEmbeddingPolicyData([
...vectorEmbeddingPolicyData,
{
id: generatePolicyId(),
path: "",
dataType: "float32",
distanceFunction: "euclidean",
@@ -317,6 +313,8 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
indexType: "none",
pathError: onVectorEmbeddingPathError(""),
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
embeddingSource: undefined,
embeddingSourceValid: true,
},
]);
};
@@ -340,12 +338,13 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
<CollapsibleSectionComponent
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
key={index}
key={vectorEmbeddingPolicy.id}
isExpandedByDefault={true}
title={t(Keys.controls.vectorEmbeddingPolicies.vectorEmbeddingTitle, { index: index + 1 })}
showDelete={true}
onDelete={() => onDelete(index)}
disableDelete={false}
dataTest={`VectorEmbedding/Section/${index + 1}`}
>
<Stack horizontal tokens={{ childrenGap: 4 }}>
<Stack
@@ -365,6 +364,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
<TextField
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
id={`vector-policy-path-${index + 1}`}
data-test={`VectorEmbedding/Path/${index + 1}`}
required={true}
placeholder="/vector1"
styles={textFieldStyles}
@@ -410,6 +410,7 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
<TextField
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
id={`vector-policy-dimension-${index + 1}`}
data-test={`VectorEmbedding/Dimensions/${index + 1}`}
required={true}
styles={textFieldStyles}
value={String(vectorEmbeddingPolicy.dimensions || 0)}
@@ -525,11 +526,25 @@ export const VectorEmbeddingPoliciesComponent: FunctionComponent<IVectorEmbeddin
</Stack>
</Stack>
)}
{isIntegratedEmbeddingEnabled() && (
<VectorEmbeddingSourceComponent
index={index}
disabled={isExistingPolicy(vectorEmbeddingPolicy)}
initialEmbeddingSource={vectorEmbeddingPolicy.embeddingSource}
discardChanges={discardChanges}
onChange={(source, isValid) => onEmbeddingSourceChange(index, source, isValid)}
/>
)}
</Stack>
</Stack>
</CollapsibleSectionComponent>
))}
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
<DefaultButton
id={`add-vector-policy`}
data-test="VectorEmbedding/AddButton"
styles={{ root: { maxWidth: 170, fontSize: 12 } }}
onClick={onAdd}
>
{t(Keys.controls.vectorEmbeddingPolicies.addVectorEmbedding)}
</DefaultButton>
</Stack>
@@ -1,14 +1,16 @@
import { IDropdownOption } from "@fluentui/react";
import { VectorIndex } from "Contracts/DataModels";
import { VectorEmbeddingSource, VectorIndex } from "Contracts/DataModels";
import { Keys, t } from "Localization";
const dataTypes = ["float32", "uint8", "int8", "float16"];
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
const authTypes: VectorEmbeddingSource["authType"][] = ["Entra"];
export const getDataTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(dataTypes);
export const getDistanceFunctionOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(distanceFunctions);
export const getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
export const getAuthTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(authTypes);
export const getQuantizerTypeOptions = (): IDropdownOption[] => [
{ key: "product", text: "Product" },
{ key: "spherical", text: `Spherical (${t(Keys.common.preview)})` },
@@ -17,6 +19,28 @@ export const getQuantizerTypeOptions = (): IDropdownOption[] => [
export const supportsQuantization = (indexType: VectorIndex["type"] | "none" | undefined): boolean =>
indexType === "quantizedFlat" || indexType === "diskANN";
// Parses a comma-separated path list, trims whitespace, drops blanks, and
// prefixes a leading "/" on each entry if it isn't there already.
export const parseSourcePaths = (raw: string): string[] => {
if (!raw) {
return [];
}
return raw
.split(",")
.map((p) => p.trim())
.filter((p) => p.length > 0)
.map((p) => (p.startsWith("/") ? p : `/${p}`));
};
export const isValidHttpsUrl = (value: string): boolean => {
try {
const url = new URL(value);
return url.protocol === "https:";
} catch {
return false;
}
};
function createDropdownOptionsFromLiterals<T extends string>(literals: T[]): IDropdownOption[] {
return literals.map((value) => ({
key: value,
@@ -838,6 +838,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
scrollToSection("collapsibleVectorPolicySectionContent");
}}
tooltipContent={ContainerVectorPolicyTooltipContent()}
dataTest="ContainerVectorPolicy/Section"
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Stack styles={{ root: { paddingLeft: 40 } }}>
+17 -2
View File
@@ -994,12 +994,27 @@
"indexingSearchListSize": "Indexing search list size",
"vectorIndexShardKey": "Vector index shard key",
"addVectorEmbedding": "Add vector embedding",
"embeddingSourceSection": "Embedding source",
"embeddingSourceTooltip": "Configure Cosmos DB to auto-generate the vector from one or more source paths using a Microsoft Foundry deployment. Leave all fields empty to skip auto-generation.",
"sourcePaths": "Source paths",
"sourcePathsPlaceholder": "/description, /title",
"deploymentName": "Deployment name",
"modelName": "Model name",
"endpoint": "Endpoint",
"endpointPlaceholder": "https://<your-foundry-endpoint>",
"authType": "Authentication type",
"pathEmptyError": "Path should not be empty",
"pathDuplicateError": "Path is already defined",
"dimensionRangeError": "Dimension must be greater than 0 and less than or equal 4096",
"dimensionFlatIndexError": "Maximum allowed dimension for flat index is 505",
"quantizationByteSizeRangeError": "Quantization byte size must be greater than 0 and less than or equal to 512",
"indexingSearchListSizeRangeError": "Indexing search list size must be greater than or equal to 25 and less than or equal to 500"
"indexingSearchListSizeRangeError": "Indexing search list size must be greater than or equal to 25 and less than or equal to 500",
"sourcePathsRequiredError": "At least one source path is required",
"sourcePathDuplicateError": "Source paths must be unique",
"deploymentNameRequiredError": "Deployment name is required",
"modelNameRequiredError": "Model name is required",
"endpointRequiredError": "Endpoint is required",
"endpointInvalidError": "Endpoint must be a valid https:// URL"
}
},
"containerCopy": {
@@ -1173,4 +1188,4 @@
}
}
}
}
}
+11
View File
@@ -34,3 +34,14 @@ export const isFullTextSearchPreviewFeaturesEnabled = (targetAccountOverride?: A
isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLFullTextSearchPreviewFeatures, targetAccountOverride)
);
};
// `enableEmbeddingGenerator` is a top-level boolean on the database account
// (e.g. properties.enableEmbeddingGenerator), not an entry in
// properties.capabilities[]. It's the Foundry integrated embedding generator
// flag that gates the `embeddingSource` block inside a Container Vector Policy.
export const isIntegratedEmbeddingEnabled = (targetAccountOverride?: AccountOverride): boolean => {
const { databaseAccount } = userContext;
const enableEmbeddingGenerator =
targetAccountOverride?.enableEmbeddingGenerator ?? databaseAccount?.properties?.enableEmbeddingGenerator;
return isVectorSearchEnabled(targetAccountOverride) && enableEmbeddingGenerator === true;
};
@@ -1412,6 +1412,27 @@ export interface VectorEmbedding {
/* The number of dimensions in the vector. */
dimensions: number;
/* Optional configuration for Cosmos DB integrated embeddings (server-side
embedding generation via Microsoft Foundry). When set, Cosmos DB
auto-populates the vector field from the listed source paths.
TODO: This field was added manually pending an upstream Swagger update;
re-applying `npm run generateARMClients` will overwrite it. */
embeddingSource?: VectorEmbeddingSource;
}
/* Configuration for Cosmos DB integrated embeddings. */
export interface VectorEmbeddingSource {
/* Source field paths used as input for embedding generation. */
sourcePaths: string[];
/* The Foundry deployment name. */
deploymentName: string;
/* The Foundry model name. */
modelName: string;
/* The Foundry endpoint URL. */
endpoint: string;
/* Authentication type used by Cosmos DB to call Foundry. */
authType: "Entra";
}
/* Represents the full text index path. */
+78
View File
@@ -50,3 +50,81 @@ test("SQL database and container CRUD", async ({ page }) => {
await expect(databaseNode.element).not.toBeAttached();
});
test("SQL container with vector embedding policy and embedding source", async ({ page }) => {
const databaseId = generateUniqueName("vdb");
const containerId = "testvectorcontainer";
const explorer = await DataExplorer.open(page, TestAccount.SQL);
// Open the New Container panel and check if the embedding source capability is available
// before proceeding. We must skip before whilePanelOpen to avoid a timeout on panel close.
const newContainerButton = await explorer.globalCommandButton("New Container");
await newContainerButton.click();
const panel = explorer.panel("New Container");
await panel.waitFor();
// Expand vector policy section and add a vector embedding to check for the embedding source accordion
await panel.getByTestId("ContainerVectorPolicy/Section").click();
await panel.getByTestId("VectorEmbedding/AddButton").click();
const embeddingSourceSection = panel.getByTestId("VectorEmbeddingSource/Section/1");
if ((await embeddingSourceSection.count()) === 0) {
// Close the panel before skipping
await panel.getByRole("button", { name: "Close New Container" }).click();
await panel.waitFor({ state: "detached" });
test.skip(true, "Test account does not have the integrated embedding capability.");
}
await panel.getByPlaceholder("Type a new database id").fill(databaseId);
await panel.getByRole("textbox", { name: "Container id, Example Container1" }).fill(containerId);
await panel.getByRole("textbox", { name: "Partition key" }).fill("/pk");
await panel.getByTestId("autoscaleRUInput").fill(TEST_AUTOSCALE_THROUGHPUT_RU.toString());
await panel.getByTestId("VectorEmbedding/Path/1").fill("/embedding");
await panel.getByTestId("VectorEmbedding/Dimensions/1").fill("1536");
// Expand embedding source section and fill fields
await embeddingSourceSection.click();
await panel.getByTestId("VectorEmbeddingSource/SourcePaths/1").fill("/description");
await panel.getByTestId("VectorEmbeddingSource/DeploymentName/1").fill("text-embedding-3-small");
await panel.getByTestId("VectorEmbeddingSource/ModelName/1").fill("text-embedding-3-small");
await panel
.getByTestId("VectorEmbeddingSource/Endpoint/1")
.fill("https://e2e-embedding.cognitiveservices.azure.com/");
const okButton = panel.getByTestId("Panel/OkButton");
await okButton.click();
await panel.waitFor({ state: "detached", timeout: 5 * 60 * 1000 });
const databaseNode = await explorer.waitForNode(databaseId);
const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
// Cleanup
await containerNode.openContextMenu();
await containerNode.contextMenuItem("Delete Container").click();
await explorer.whilePanelOpen(
"Delete Container",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the container id" }).fill(containerId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(containerNode.element).not.toBeAttached();
await databaseNode.openContextMenu();
await databaseNode.contextMenuItem("Delete Database").click();
await explorer.whilePanelOpen(
"Delete Database",
async (panel, okButton) => {
await panel.getByRole("textbox", { name: "Confirm by typing the database id" }).fill(databaseId);
await okButton.click();
},
{ closeTimeout: 5 * 60 * 1000 },
);
await expect(databaseNode.element).not.toBeAttached();
});
@@ -190,4 +190,59 @@ test.describe("Vector Policy under Scale & Settings", () => {
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
await expect(saveButton).toBeDisabled();
});
test("Add embedding source to vector embedding policy", async () => {
const addButton = explorer.frame.getByTestId("VectorEmbedding/AddButton");
await addButton.click();
const embeddingSourceSection = explorer.frame.getByTestId("VectorEmbeddingSource/Section/1");
if ((await embeddingSourceSection.count()) === 0) {
test.skip(true, "Test account does not have the integrated embedding capability.");
}
await explorer.frame.getByTestId("VectorEmbedding/Path/1").fill("/embedding");
await explorer.frame.getByTestId("VectorEmbedding/Dimensions/1").fill("1536");
// Expand embedding source section and fill fields
await embeddingSourceSection.click();
await explorer.frame.getByTestId("VectorEmbeddingSource/SourcePaths/1").fill("/description");
await explorer.frame.getByTestId("VectorEmbeddingSource/DeploymentName/1").fill("text-embedding-3-small");
await explorer.frame.getByTestId("VectorEmbeddingSource/ModelName/1").fill("text-embedding-3-small");
await explorer.frame
.getByTestId("VectorEmbeddingSource/Endpoint/1")
.fill("https://e2e-embedding.cognitiveservices.azure.com/");
// Save changes
const saveButton = explorer.commandBarButton(CommandBarButton.Save);
await expect(saveButton).toBeEnabled();
await saveButton.click();
await expect(explorer.getConsoleHeaderStatus()).toContainText(
`Successfully updated container ${context.container.id}`,
{ timeout: 2 * ONE_MINUTE_MS },
);
});
test("Existing embedding source fields are disabled after save", async () => {
// Ensure a policy with embedding source exists (from previous test or create one)
const sourcePathsInput = explorer.frame.getByTestId("VectorEmbeddingSource/SourcePaths/1");
if ((await sourcePathsInput.count()) === 0) {
test.skip(true, "No embedding source present; previous test may have been skipped.");
}
await expect(sourcePathsInput).toBeDisabled();
await expect(sourcePathsInput).toHaveValue("/description");
const deploymentInput = explorer.frame.getByTestId("VectorEmbeddingSource/DeploymentName/1");
await expect(deploymentInput).toBeDisabled();
await expect(deploymentInput).toHaveValue("text-embedding-3-small");
const modelInput = explorer.frame.getByTestId("VectorEmbeddingSource/ModelName/1");
await expect(modelInput).toBeDisabled();
await expect(modelInput).toHaveValue("text-embedding-3-small");
const endpointInput = explorer.frame.getByTestId("VectorEmbeddingSource/Endpoint/1");
await expect(endpointInput).toBeDisabled();
await expect(endpointInput).toHaveValue("https://e2e-embedding.cognitiveservices.azure.com/");
});
});