Vector search for NoSQL accounts (#1843)

* Add container vector policy and indexing policy support

* Add vector search capability

* hide vector settings for shared throughput DB

* update package-lock

* fix pipeline

* remove comments

* Address comments

* Address comments
This commit is contained in:
sunghyunkang1111 2024-05-20 13:30:30 -05:00 committed by GitHub
parent 4da3363cf7
commit ceeead8458
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 297 additions and 36 deletions

8
package-lock.json generated
View File

@ -10,7 +10,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos": "4.0.1-beta.3",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.5.2", "@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.1.1", "@azure/ms-rest-nodeauth": "3.1.1",
@ -362,9 +362,9 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
}, },
"node_modules/@azure/cosmos": { "node_modules/@azure/cosmos": {
"version": "4.0.1-beta.2", "version": "4.0.1-beta.3",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.1-beta.2.tgz", "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.1-beta.3.tgz",
"integrity": "sha512-iuqg/QwLQlxgRi4pnXU8JUYv+f24wkRvJ9ZZI4/sYk+DxSgkuQ194Cc2IpckpeO8z7ZpcBkVQFa82wcZVVZ8Zg==", "integrity": "sha512-CpRGt+S5jnvtGUi4TmlS79YvxpbNc8/5/QHgIvvQ9D2ZFUqO0MjbMCU3lVZV2NAJT02BsbLfRAFe+FPn8nMRQw==",
"dependencies": { "dependencies": {
"@azure/abort-controller": "^1.0.0", "@azure/abort-controller": "^1.0.0",
"@azure/core-auth": "^1.3.0", "@azure/core-auth": "^1.3.0",

View File

@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/arm-cosmosdb": "9.1.0", "@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.0.1-beta.2", "@azure/cosmos": "4.0.1-beta.3",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.5.2", "@azure/identity": "1.5.2",
"@azure/ms-rest-nodeauth": "3.1.1", "@azure/ms-rest-nodeauth": "3.1.1",

View File

@ -88,6 +88,7 @@ export class CapabilityNames {
public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics"; public static readonly EnableStorageAnalytics: string = "EnableStorageAnalytics";
public static readonly EnableMongo: string = "EnableMongo"; public static readonly EnableMongo: string = "EnableMongo";
public static readonly EnableServerless: string = "EnableServerless"; public static readonly EnableServerless: string = "EnableServerless";
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
} }
export enum CapacityMode { export enum CapacityMode {

View File

@ -2,6 +2,7 @@
exports[`getCommonQueryOptions builds the correct default options objects 1`] = ` exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
Object { Object {
"disableNonStreamingOrderByQuery": true,
"enableScanInQuery": true, "enableScanInQuery": true,
"forceQueryPlan": true, "forceQueryPlan": true,
"maxDegreeOfParallelism": 0, "maxDegreeOfParallelism": 0,
@ -12,6 +13,7 @@ Object {
exports[`getCommonQueryOptions reads from localStorage 1`] = ` exports[`getCommonQueryOptions reads from localStorage 1`] = `
Object { Object {
"disableNonStreamingOrderByQuery": true,
"enableScanInQuery": true, "enableScanInQuery": true,
"forceQueryPlan": true, "forceQueryPlan": true,
"maxDegreeOfParallelism": 17, "maxDegreeOfParallelism": 17,

View File

@ -6,13 +6,13 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils"; import { getCollectionName } from "../../Utils/APITypeUtils";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { createUpdateCassandraTable } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; import { createUpdateGremlinGraph } from "../../Utils/arm/generatedClients/cosmos/gremlinResources";
import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources"; import { createUpdateMongoDBCollection } from "../../Utils/arm/generatedClients/cosmos/mongoDBResources";
import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; import { createUpdateSqlContainer } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources"; import { createUpdateTable } from "../../Utils/arm/generatedClients/cosmos/tableResources";
import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types"; import * as ARMTypes from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils"; import { handleError } from "../ErrorHandlingUtils";
import { createMongoCollectionWithProxy } from "../MongoProxyClient"; import { createMongoCollectionWithProxy } from "../MongoProxyClient";
@ -96,6 +96,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr
if (params.uniqueKeyPolicy) { if (params.uniqueKeyPolicy) {
resource.uniqueKeyPolicy = params.uniqueKeyPolicy; resource.uniqueKeyPolicy = params.uniqueKeyPolicy;
} }
if (params.vectorEmbeddingPolicy) {
resource.vectorEmbeddingPolicy = params.vectorEmbeddingPolicy;
}
const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = { const rpPayload: ARMTypes.SqlDatabaseCreateUpdateParameters = {
properties: { properties: {

View File

@ -1,4 +1,5 @@
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Queries } from "../Constants"; import { Queries } from "../Constants";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
@ -26,5 +27,6 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) || (storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Queries.itemsPerPage; Queries.itemsPerPage;
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism); options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
return options; return options;
}; };

View File

@ -158,6 +158,7 @@ export interface Collection extends Resource {
changeFeedPolicy?: ChangeFeedPolicy; changeFeedPolicy?: ChangeFeedPolicy;
analyticalStorageTtl?: number; analyticalStorageTtl?: number;
geospatialConfig?: GeospatialConfig; geospatialConfig?: GeospatialConfig;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
schema?: ISchema; schema?: ISchema;
requestSchema?: () => void; requestSchema?: () => void;
computedProperties?: ComputedProperties; computedProperties?: ComputedProperties;
@ -195,8 +196,14 @@ export interface IndexingPolicy {
indexingMode: "consistent" | "lazy" | "none"; indexingMode: "consistent" | "lazy" | "none";
includedPaths: any; includedPaths: any;
excludedPaths: any; excludedPaths: any;
compositeIndexes?: any; compositeIndexes?: any[];
spatialIndexes?: any; spatialIndexes?: any[];
vectorIndexes?: VectorIndex[];
}
export interface VectorIndex {
path: string;
type: "flat" | "diskANN" | "quantizedFlat";
} }
export interface ComputedProperty { export interface ComputedProperty {
@ -334,6 +341,18 @@ export interface CreateCollectionParams {
partitionKey?: PartitionKey; partitionKey?: PartitionKey;
uniqueKeyPolicy?: UniqueKeyPolicy; uniqueKeyPolicy?: UniqueKeyPolicy;
createMongoWildcardIndex?: boolean; createMongoWildcardIndex?: boolean;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
}
export interface VectorEmbeddingPolicy {
vectorEmbeddings: VectorEmbedding[];
}
export interface VectorEmbedding {
dataType: "float16" | "float32" | "uint8" | "int8";
dimensions: number;
distanceFunction: "euclidean" | "cosine" | "dotproduct";
path: string;
} }
export interface ReadDatabaseOfferParams { export interface ReadDatabaseOfferParams {

View File

@ -26,8 +26,8 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
this.setState({ isExpanded: !this.state.isExpanded }); this.setState({ isExpanded: !this.state.isExpanded });
}; };
public componentDidUpdate(): void { public componentDidUpdate(_prevProps: CollapsibleSectionProps, prevState: CollapsibleSectionState): void {
if (this.state.isExpanded && this.props.onExpand) { if (!prevState.isExpanded && this.state.isExpanded && this.props.onExpand) {
this.props.onExpand(); this.props.onExpand();
} }
} }
@ -43,7 +43,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
return ( return (
<> <>
<Stack <Stack
className="collapsibleSection" className={"collapsibleSection"}
horizontal horizontal
verticalAlign="center" verticalAlign="center"
tokens={accordionStackTokens} tokens={accordionStackTokens}

View File

@ -20,7 +20,10 @@ export interface EditorReactProps {
lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"]; lineDecorationsWidth?: monaco.editor.IEditorOptions["lineDecorationsWidth"];
minimap?: monaco.editor.IEditorOptions["minimap"]; minimap?: monaco.editor.IEditorOptions["minimap"];
scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"]; scrollBeyondLastLine?: monaco.editor.IEditorOptions["scrollBeyondLastLine"];
fontSize?: monaco.editor.IEditorOptions["fontSize"];
monacoContainerStyles?: React.CSSProperties; monacoContainerStyles?: React.CSSProperties;
className?: string;
spinnerClassName?: string;
} }
export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> { export class EditorReact extends React.Component<EditorReactProps, EditorReactStates> {
@ -75,9 +78,11 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<React.Fragment> <React.Fragment>
{!this.state.showEditor && <Spinner size={SpinnerSize.large} className="spinner" />} {!this.state.showEditor && (
<Spinner size={SpinnerSize.large} className={this.props.spinnerClassName || "spinner"} />
)}
<div <div
className="jsonEditor" className={this.props.className || "jsonEditor"}
style={this.props.monacoContainerStyles} style={this.props.monacoContainerStyles}
ref={(elt: HTMLElement) => this.setRef(elt)} ref={(elt: HTMLElement) => this.setRef(elt)}
/> />
@ -119,7 +124,7 @@ export class EditorReact extends React.Component<EditorReactProps, EditorReactSt
value: this.props.content, value: this.props.content,
readOnly: this.props.isReadOnly, readOnly: this.props.isReadOnly,
ariaLabel: this.props.ariaLabel, ariaLabel: this.props.ariaLabel,
fontSize: 12, fontSize: this.props.fontSize || 12,
automaticLayout: true, automaticLayout: true,
theme: this.props.theme, theme: this.props.theme,
wordWrap: this.props.wordWrap || "off", wordWrap: this.props.wordWrap || "off",

View File

@ -25,7 +25,14 @@
font-family: @DataExplorerFont; font-family: @DataExplorerFont;
} }
.settingsV2IndexingPolicyEditor { .settingsV2Editor {
width: 100%; width: 100%;
height: 60vh; height: 60vh;
} }
.settingsV2EditorSpinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@ -3,7 +3,12 @@ import {
ComputedPropertiesComponent, ComputedPropertiesComponent,
ComputedPropertiesComponentProps, ComputedPropertiesComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent"; } from "Explorer/Controls/Settings/SettingsSubComponents/ComputedPropertiesComponent";
import {
ContainerVectorPolicyComponent,
ContainerVectorPolicyComponentProps,
} from "Explorer/Controls/Settings/SettingsSubComponents/ContainerVectorPolicyComponent";
import { useDatabases } from "Explorer/useDatabases"; import { useDatabases } from "Explorer/useDatabases";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import { isRunningOnPublicCloud } from "Utils/CloudUtils";
import * as React from "react"; import * as React from "react";
import DiscardIcon from "../../../../images/discard.svg"; import DiscardIcon from "../../../../images/discard.svg";
@ -144,6 +149,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private shouldShowComputedPropertiesEditor: boolean; private shouldShowComputedPropertiesEditor: boolean;
private shouldShowIndexingPolicyEditor: boolean; private shouldShowIndexingPolicyEditor: boolean;
private shouldShowPartitionKeyEditor: boolean; private shouldShowPartitionKeyEditor: boolean;
private isVectorSearchEnabled: boolean;
private totalThroughputUsed: number; private totalThroughputUsed: number;
public mongoDBCollectionResource: MongoDBCollectionResource; public mongoDBCollectionResource: MongoDBCollectionResource;
@ -158,6 +164,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL"; this.shouldShowComputedPropertiesEditor = userContext.apiType === "SQL";
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo"; this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud(); this.shouldShowPartitionKeyEditor = userContext.apiType === "SQL" && isRunningOnPublicCloud();
this.isVectorSearchEnabled = isVectorSearchEnabled() && !hasDatabaseSharedThroughput(this.collection);
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy; this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
@ -1097,6 +1104,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
indexTransformationProgress: this.state.indexTransformationProgress, indexTransformationProgress: this.state.indexTransformationProgress,
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress, refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange, onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange,
isVectorSearchEnabled: this.isVectorSearchEnabled,
}; };
const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = { const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = {
@ -1143,6 +1151,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
explorer: this.props.settingsTab.getContainer(), explorer: this.props.settingsTab.getContainer(),
}; };
const containerVectorPolicyProps: ContainerVectorPolicyComponentProps = {
vectorEmbeddingPolicy: this.collection.rawDataModel?.vectorEmbeddingPolicy,
};
const tabs: SettingsV2TabInfo[] = []; const tabs: SettingsV2TabInfo[] = [];
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) { if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
tabs.push({ tabs.push({
@ -1156,6 +1168,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
content: <SubSettingsComponent {...subSettingsComponentProps} />, content: <SubSettingsComponent {...subSettingsComponentProps} />,
}); });
if (this.isVectorSearchEnabled) {
tabs.push({
tab: SettingsV2TabTypes.ContainerVectorPolicyTab,
content: <ContainerVectorPolicyComponent {...containerVectorPolicyProps} />,
});
}
if (this.shouldShowIndexingPolicyEditor) { if (this.shouldShowIndexingPolicyEditor) {
tabs.push({ tabs.push({
tab: SettingsV2TabTypes.IndexingPolicyTab, tab: SettingsV2TabTypes.IndexingPolicyTab,

View File

@ -121,7 +121,7 @@ export class ComputedPropertiesComponent extends React.Component<
</Link> </Link>
&#160; about how to define computed properties and how to use them. &#160; about how to define computed properties and how to use them.
</Text> </Text>
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.computedPropertiesDiv}></div> <div className="settingsV2Editor" tabIndex={0} ref={this.computedPropertiesDiv}></div>
</Stack> </Stack>
); );
} }

View File

@ -0,0 +1,30 @@
import { Stack } from "@fluentui/react";
import { VectorEmbeddingPolicy } from "Contracts/DataModels";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { titleAndInputStackProps } from "Explorer/Controls/Settings/SettingsRenderUtils";
import React from "react";
export interface ContainerVectorPolicyComponentProps {
vectorEmbeddingPolicy: VectorEmbeddingPolicy;
}
export const ContainerVectorPolicyComponent: React.FC<ContainerVectorPolicyComponentProps> = ({
vectorEmbeddingPolicy,
}) => {
return (
<Stack {...titleAndInputStackProps} styles={{ root: { position: "relative" } }}>
<EditorReact
language={"json"}
content={JSON.stringify(vectorEmbeddingPolicy, null, 4)}
isReadOnly={true}
wordWrap={"on"}
ariaLabel={"Container vector policy"}
lineNumbers={"on"}
scrollBeyondLastLine={false}
className={"settingsV2Editor"}
spinnerClassName={"settingsV2EditorSpinner"}
fontSize={14}
/>
</Stack>
);
};

View File

@ -16,6 +16,7 @@ export interface IndexingPolicyComponentProps {
logIndexingPolicySuccessMessage: () => void; logIndexingPolicySuccessMessage: () => void;
indexTransformationProgress: number; indexTransformationProgress: number;
refreshIndexTransformationProgress: () => Promise<void>; refreshIndexTransformationProgress: () => Promise<void>;
isVectorSearchEnabled?: boolean;
onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void; onIndexingPolicyDirtyChange: (isIndexingPolicyDirty: boolean) => void;
} }
@ -119,10 +120,15 @@ export class IndexingPolicyComponent extends React.Component<
indexTransformationProgress={this.props.indexTransformationProgress} indexTransformationProgress={this.props.indexTransformationProgress}
refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress} refreshIndexTransformationProgress={this.props.refreshIndexTransformationProgress}
/> />
{this.props.isVectorSearchEnabled && (
<MessageBar messageBarType={MessageBarType.severeWarning}>
Container vector policies and vector indexes are not modifiable after container creation
</MessageBar>
)}
{isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && ( {isDirty(this.props.indexingPolicyContent, this.props.indexingPolicyContentBaseline) && (
<MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar> <MessageBar messageBarType={MessageBarType.warning}>{unsavedEditorWarningMessage("indexPolicy")}</MessageBar>
)} )}
<div className="settingsV2IndexingPolicyEditor" tabIndex={0} ref={this.indexingPolicyDiv}></div> <div className="settingsV2Editor" tabIndex={0} ref={this.indexingPolicyDiv}></div>
</Stack> </Stack>
); );
} }

View File

@ -29,7 +29,7 @@ exports[`ComputedPropertiesComponent renders 1`] = `
  about how to define computed properties and how to use them.   about how to define computed properties and how to use them.
</Text> </Text>
<div <div
className="settingsV2IndexingPolicyEditor" className="settingsV2Editor"
tabIndex={0} tabIndex={0}
/> />
</Stack> </Stack>

View File

@ -12,7 +12,7 @@ exports[`IndexingPolicyComponent renders 1`] = `
refreshIndexTransformationProgress={[Function]} refreshIndexTransformationProgress={[Function]}
/> />
<div <div
className="settingsV2IndexingPolicyEditor" className="settingsV2Editor"
tabIndex={0} tabIndex={0}
/> />
</Stack> </Stack>

View File

@ -47,6 +47,7 @@ export enum SettingsV2TabTypes {
IndexingPolicyTab, IndexingPolicyTab,
PartitionKeyTab, PartitionKeyTab,
ComputedPropertiesTab, ComputedPropertiesTab,
ContainerVectorPolicyTab,
} }
export interface IsComponentDirtyResult { export interface IsComponentDirtyResult {
@ -152,6 +153,8 @@ export const getTabTitle = (tab: SettingsV2TabTypes): string => {
return "Partition Keys (preview)"; return "Partition Keys (preview)";
case SettingsV2TabTypes.ComputedPropertiesTab: case SettingsV2TabTypes.ComputedPropertiesTab:
return "Computed Properties"; return "Computed Properties";
case SettingsV2TabTypes.ContainerVectorPolicyTab:
return "Container Vector Policy (preview)";
default: default:
throw new Error(`Unknown tab ${tab}`); throw new Error(`Unknown tab ${tab}`);
} }

View File

@ -196,6 +196,7 @@ exports[`SettingsComponent renders 1`] = `
"indexingMode": "consistent", "indexingMode": "consistent",
} }
} }
isVectorSearchEnabled={false}
logIndexingPolicySuccessMessage={[Function]} logIndexingPolicySuccessMessage={[Function]}
onIndexingPolicyContentChange={[Function]} onIndexingPolicyContentChange={[Function]}
onIndexingPolicyDirtyChange={[Function]} onIndexingPolicyDirtyChange={[Function]}

View File

@ -21,6 +21,7 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils";
import { configContext, Platform } from "ConfigContext"; import { configContext, Platform } from "ConfigContext";
import * as DataModels from "Contracts/DataModels"; import * as DataModels from "Contracts/DataModels";
import { SubscriptionType } from "Contracts/SubscriptionType"; import { SubscriptionType } from "Contracts/SubscriptionType";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { useSidePanel } from "hooks/useSidePanel"; import { useSidePanel } from "hooks/useSidePanel";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react"; import React from "react";
@ -29,7 +30,7 @@ import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "Shared/Telemetry/TelemetryProcessor";
import { userContext } from "UserContext"; import { userContext } from "UserContext";
import { getCollectionName } from "Utils/APITypeUtils"; import { getCollectionName } from "Utils/APITypeUtils";
import { isCapabilityEnabled, isServerlessAccount } from "Utils/CapabilityUtils"; import { isCapabilityEnabled, isServerlessAccount, isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { getUpsellMessage } from "Utils/PricingUtils"; import { getUpsellMessage } from "Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
@ -81,6 +82,26 @@ export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
excludedPaths: [], excludedPaths: [],
}; };
const DefaultDatabaseVectorIndex: DataModels.IndexingPolicy = {
indexingMode: "consistent",
automatic: true,
includedPaths: [
{
path: "/*",
},
],
excludedPaths: [
{
path: '/"_etag"/?',
},
],
vectorIndexes: [],
};
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
vectorEmbeddings: [],
};
export interface AddCollectionPanelState { export interface AddCollectionPanelState {
createNewDatabase: boolean; createNewDatabase: boolean;
newDatabaseId: string; newDatabaseId: string;
@ -101,6 +122,8 @@ export interface AddCollectionPanelState {
isExecuting: boolean; isExecuting: boolean;
isThroughputCapExceeded: boolean; isThroughputCapExceeded: boolean;
teachingBubbleStep: number; teachingBubbleStep: number;
vectorIndexingPolicy: string;
vectorEmbeddingPolicy: string;
} }
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> { export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
@ -136,6 +159,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
isExecuting: false, isExecuting: false,
isThroughputCapExceeded: false, isThroughputCapExceeded: false,
teachingBubbleStep: 0, teachingBubbleStep: 0,
vectorIndexingPolicy: JSON.stringify(DefaultDatabaseVectorIndex, null, 2),
vectorEmbeddingPolicy: JSON.stringify(DefaultVectorEmbeddingPolicy, null, 2),
}; };
} }
@ -145,11 +170,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
} }
componentDidUpdate(_prevProps: AddCollectionPanelProps, prevState: AddCollectionPanelState): void {
if (this.state.errorMessage && this.state.errorMessage !== prevState.errorMessage) {
this.scrollToSection("panelContainer");
}
}
render(): JSX.Element { render(): JSX.Element {
const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated(); const isFirstResourceCreated = useDatabases.getState().isFirstResourceCreated();
return ( return (
<form className="panelFormWrapper" onSubmit={this.submit.bind(this)}> <form className="panelFormWrapper" onSubmit={this.submit.bind(this)} id="panelContainer">
{this.state.errorMessage && ( {this.state.errorMessage && (
<PanelInfoErrorComponent <PanelInfoErrorComponent
message={this.state.errorMessage} message={this.state.errorMessage}
@ -864,17 +895,76 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)} )}
</Stack> </Stack>
)} )}
{this.shouldShowVectorSearchParameters() && (
<Stack>
<CollapsibleSectionComponent
title="Indexing Policy"
isExpandedByDefault={false}
onExpand={() => {
this.scrollToSection("collapsibleVectorPolicySectionContent");
}}
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Link href="https://aka.ms/CosmosDBVectorSetup" target="_blank">
Learn more
</Link>
<EditorReact
language={"json"}
content={this.state.vectorIndexingPolicy}
isReadOnly={false}
wordWrap={"on"}
ariaLabel={"Editing indexing policy"}
lineNumbers={"on"}
scrollBeyondLastLine={false}
spinnerClassName="panelSectionSpinner"
monacoContainerStyles={{
minHeight: 200,
}}
onContentChanged={(newIndexingPolicy: string) => this.setVectorIndexingPolicy(newIndexingPolicy)}
/>
</Stack>
</CollapsibleSectionComponent>
<CollapsibleSectionComponent
title="Container Vector Policy"
isExpandedByDefault={false}
onExpand={() => {
this.scrollToSection("collapsibleVectorPolicySectionContent");
}}
>
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
<Link href="https://aka.ms/CosmosDBVectorSetup" target="_blank">
Learn more
</Link>
<EditorReact
language={"json"}
content={this.state.vectorEmbeddingPolicy}
isReadOnly={false}
wordWrap={"on"}
ariaLabel={"Editing container vector policy"}
lineNumbers={"on"}
scrollBeyondLastLine={false}
spinnerClassName="panelSectionSpinner"
monacoContainerStyles={{
minHeight: 200,
}}
onContentChanged={(newVectorEmbeddingPolicy: string) =>
this.setVectorEmbeddingPolicy(newVectorEmbeddingPolicy)
}
/>
</Stack>
</CollapsibleSectionComponent>
</Stack>
)}
{userContext.apiType !== "Tables" && ( {userContext.apiType !== "Tables" && (
<CollapsibleSectionComponent <CollapsibleSectionComponent
title="Advanced" title="Advanced"
isExpandedByDefault={false} isExpandedByDefault={false}
onExpand={() => { onExpand={() => {
TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection); TelemetryProcessor.traceOpen(Action.ExpandAddCollectionPaneAdvancedSection);
this.scrollToAdvancedSection(); this.scrollToSection("collapsibleAdvancedSectionContent");
}} }}
> >
<Stack className="panelGroupSpacing" id="collapsibleSectionContent"> <Stack className="panelGroupSpacing" id="collapsibleAdvancedSectionContent">
{isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && ( {isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && (
<Stack className="panelGroupSpacing"> <Stack className="panelGroupSpacing">
<Stack horizontal> <Stack horizontal>
@ -1070,6 +1160,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
} }
private setVectorEmbeddingPolicy(vectorEmbeddingPolicy: string): void {
this.setState({
vectorEmbeddingPolicy,
});
}
private setVectorIndexingPolicy(vectorIndexingPolicy: string): void {
this.setState({
vectorIndexingPolicy,
});
}
private isSelectedDatabaseSharedThroughput(): boolean { private isSelectedDatabaseSharedThroughput(): boolean {
if (!this.state.selectedDatabaseId) { if (!this.state.selectedDatabaseId) {
return false; return false;
@ -1209,6 +1311,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
); );
} }
private shouldShowVectorSearchParameters() {
return isVectorSearchEnabled() && this.shouldShowCollectionThroughputInput();
}
private parseUniqueKeys(): DataModels.UniqueKeyPolicy { private parseUniqueKeys(): DataModels.UniqueKeyPolicy {
if (this.state.uniqueKeys?.length === 0) { if (this.state.uniqueKeys?.length === 0) {
return undefined; return undefined;
@ -1265,6 +1371,22 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false; return false;
} }
if (this.shouldShowVectorSearchParameters()) {
try {
JSON.parse(this.state.vectorIndexingPolicy) as DataModels.IndexingPolicy;
} catch (e) {
this.setState({ errorMessage: "Invalid JSON format for indexingPolicy" });
return false;
}
try {
JSON.parse(this.state.vectorEmbeddingPolicy) as DataModels.VectorEmbeddingPolicy;
} catch (e) {
this.setState({ errorMessage: "Invalid JSON format for vectorEmbeddingPolicy" });
return false;
}
}
return true; return true;
} }
@ -1287,8 +1409,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return Constants.AnalyticalStorageTtl.Disabled; return Constants.AnalyticalStorageTtl.Disabled;
} }
private scrollToAdvancedSection(): void { private scrollToSection(id: string): void {
document.getElementById("collapsibleSectionContent")?.scrollIntoView(); document.getElementById(id)?.scrollIntoView();
} }
private getSampleDBName(): string { private getSampleDBName(): string {
@ -1340,10 +1462,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
: undefined; : undefined;
const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing let indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
? AllPropertiesIndexed ? AllPropertiesIndexed
: SharedDatabaseDefault; : SharedDatabaseDefault;
let vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
if (this.shouldShowVectorSearchParameters()) {
indexingPolicy = JSON.parse(this.state.vectorIndexingPolicy);
vectorEmbeddingPolicy = JSON.parse(this.state.vectorEmbeddingPolicy);
}
const telemetryData = { const telemetryData = {
database: { database: {
id: databaseId, id: databaseId,
@ -1402,6 +1531,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
partitionKey, partitionKey,
uniqueKeyPolicy, uniqueKeyPolicy,
createMongoWildcardIndex: this.state.createMongoWildCardIndex, createMongoWildcardIndex: this.state.createMongoWildCardIndex,
vectorEmbeddingPolicy,
}; };
this.setState({ isExecuting: true }); this.setState({ isExecuting: true });

View File

@ -48,6 +48,13 @@
font-size: @mediumFontSize; font-size: @mediumFontSize;
padding: 0 @LargeSpace 0 @SmallSpace; padding: 0 @LargeSpace 0 @SmallSpace;
} }
.panelSectionSpinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
} }
} }

View File

@ -3,6 +3,7 @@
exports[`AddCollectionPanel should render Default properly 1`] = ` exports[`AddCollectionPanel should render Default properly 1`] = `
<form <form
className="panelFormWrapper" className="panelFormWrapper"
id="panelContainer"
onSubmit={[Function]} onSubmit={[Function]}
> >
<div <div
@ -433,7 +434,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
> >
<Stack <Stack
className="panelGroupSpacing" className="panelGroupSpacing"
id="collapsibleSectionContent" id="collapsibleAdvancedSectionContent"
> >
<Stack <Stack
className="panelGroupSpacing" className="panelGroupSpacing"

View File

@ -16,3 +16,7 @@ export const isServerlessAccount = (): boolean => {
isCapabilityEnabled(Constants.CapabilityNames.EnableServerless) isCapabilityEnabled(Constants.CapabilityNames.EnableServerless)
); );
}; };
export const isVectorSearchEnabled = (): boolean => {
return userContext.apiType === "SQL" && isCapabilityEnabled(Constants.CapabilityNames.EnableNoSQLVectorSearch);
};

View File

@ -9,7 +9,7 @@
import { configContext } from "../../../../ConfigContext"; import { configContext } from "../../../../ConfigContext";
import { armRequest } from "../../request"; import { armRequest } from "../../request";
import * as Types from "./types"; import * as Types from "./types";
const apiVersion = "2024-02-15-preview"; const apiVersion = "2024-05-15-preview";
/* Lists the SQL databases under an existing Azure Cosmos DB database account. */ /* Lists the SQL databases under an existing Azure Cosmos DB database account. */
export async function listSqlDatabases( export async function listSqlDatabases(

View File

@ -1235,6 +1235,9 @@ export interface SqlDatabaseResource {
export interface SqlContainerResource { export interface SqlContainerResource {
/* Name of the Cosmos DB SQL container */ /* Name of the Cosmos DB SQL container */
id: string; id: string;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
/* The configuration of the indexing policy. By default, the indexing is automatic for all document paths within the container */ /* The configuration of the indexing policy. By default, the indexing is automatic for all document paths within the container */
indexingPolicy?: IndexingPolicy; indexingPolicy?: IndexingPolicy;
@ -1267,6 +1270,17 @@ export interface SqlContainerResource {
computedProperties?: ComputedProperty[]; computedProperties?: ComputedProperty[];
} }
export interface VectorEmbeddingPolicy {
vectorEmbeddings: VectorEmbedding[];
}
export interface VectorEmbedding {
path?: string;
dataType?: string;
dimensions?: number;
distanceFunction?: string;
}
/* Cosmos DB indexing policy */ /* Cosmos DB indexing policy */
export interface IndexingPolicy { export interface IndexingPolicy {
/* Indicates if the indexing policy is automatic */ /* Indicates if the indexing policy is automatic */
@ -1285,6 +1299,13 @@ export interface IndexingPolicy {
/* List of spatial specifics */ /* List of spatial specifics */
spatialIndexes?: SpatialSpec[]; spatialIndexes?: SpatialSpec[];
vectorIndexes?: VectorIndex[];
}
export interface VectorIndex {
path?: string;
type?: string;
} }
/* undocumented */ /* undocumented */