mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-02-26 05:58:01 +00:00
Add form and validation for vector search (#1856)
* Add form and validation for vector search * Add form and validation for vector search * Add unit tests and merge forms
This commit is contained in:
parent
06d4829422
commit
c680481fe0
@ -209,6 +209,7 @@
|
|||||||
"test": "rimraf coverage && jest",
|
"test": "rimraf coverage && jest",
|
||||||
"test:debug": "jest --runInBand",
|
"test:debug": "jest --runInBand",
|
||||||
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
|
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
|
||||||
|
"test:file": "jest --coverage=false",
|
||||||
"watch": "npm run start",
|
"watch": "npm run start",
|
||||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
||||||
"build:ase": "gulp build:ase",
|
"build:ase": "gulp build:ase",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Icon, Label, Stack } from "@fluentui/react";
|
import { DirectionalHint, Icon, Label, Stack, TooltipHost } from "@fluentui/react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||||
@ -8,6 +8,7 @@ export interface CollapsibleSectionProps {
|
|||||||
isExpandedByDefault: boolean;
|
isExpandedByDefault: boolean;
|
||||||
onExpand?: () => void;
|
onExpand?: () => void;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
|
tooltipContent?: string | JSX.Element | JSX.Element[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollapsibleSectionState {
|
export interface CollapsibleSectionState {
|
||||||
@ -55,6 +56,19 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
|||||||
>
|
>
|
||||||
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
||||||
<Label>{this.props.title}</Label>
|
<Label>{this.props.title}</Label>
|
||||||
|
{this.props.tooltipContent && (
|
||||||
|
<TooltipHost
|
||||||
|
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||||
|
content={this.props.tooltipContent}
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
marginLeft: "0 !important",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||||
|
</TooltipHost>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
{this.state.isExpanded && this.props.children}
|
{this.state.isExpanded && this.props.children}
|
||||||
</>
|
</>
|
||||||
|
@ -21,7 +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 { AddVectorEmbeddingPolicyForm } from "Explorer/Panes/VectorSearchPanel/AddVectorEmbeddingPolicyForm";
|
||||||
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";
|
||||||
@ -82,22 +82,6 @@ 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 = {
|
export const DefaultVectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy = {
|
||||||
vectorEmbeddings: [],
|
vectorEmbeddings: [],
|
||||||
};
|
};
|
||||||
@ -122,8 +106,9 @@ export interface AddCollectionPanelState {
|
|||||||
isExecuting: boolean;
|
isExecuting: boolean;
|
||||||
isThroughputCapExceeded: boolean;
|
isThroughputCapExceeded: boolean;
|
||||||
teachingBubbleStep: number;
|
teachingBubbleStep: number;
|
||||||
vectorIndexingPolicy: string;
|
vectorIndexingPolicy: DataModels.VectorIndex[];
|
||||||
vectorEmbeddingPolicy: string;
|
vectorEmbeddingPolicy: DataModels.VectorEmbedding[];
|
||||||
|
vectorPolicyValidated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
export class AddCollectionPanel extends React.Component<AddCollectionPanelProps, AddCollectionPanelState> {
|
||||||
@ -159,8 +144,9 @@ 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: [],
|
||||||
vectorEmbeddingPolicy: JSON.stringify(DefaultVectorEmbeddingPolicy, null, 2),
|
vectorIndexingPolicy: [],
|
||||||
|
vectorPolicyValidated: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -896,61 +882,29 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
)}
|
)}
|
||||||
{this.shouldShowVectorSearchParameters() && (
|
{this.shouldShowVectorSearchParameters() && (
|
||||||
<Stack>
|
<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
|
<CollapsibleSectionComponent
|
||||||
title="Container Vector Policy"
|
title="Container Vector Policy"
|
||||||
isExpandedByDefault={false}
|
isExpandedByDefault={false}
|
||||||
onExpand={() => {
|
onExpand={() => {
|
||||||
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
this.scrollToSection("collapsibleVectorPolicySectionContent");
|
||||||
}}
|
}}
|
||||||
|
tooltipContent={this.getContainerVectorPolicyTooltipContent()}
|
||||||
>
|
>
|
||||||
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
<Stack id="collapsibleVectorPolicySectionContent" styles={{ root: { position: "relative" } }}>
|
||||||
<Link href="https://aka.ms/CosmosDBVectorSetup" target="_blank">
|
<Stack styles={{ root: { paddingLeft: 40 } }}>
|
||||||
Learn more
|
<AddVectorEmbeddingPolicyForm
|
||||||
</Link>
|
vectorEmbedding={this.state.vectorEmbeddingPolicy}
|
||||||
<EditorReact
|
vectorIndex={this.state.vectorIndexingPolicy}
|
||||||
language={"json"}
|
onVectorEmbeddingChange={(
|
||||||
content={this.state.vectorEmbeddingPolicy}
|
vectorEmbeddingPolicy: DataModels.VectorEmbedding[],
|
||||||
isReadOnly={false}
|
vectorIndexingPolicy: DataModels.VectorIndex[],
|
||||||
wordWrap={"on"}
|
vectorPolicyValidated: boolean,
|
||||||
ariaLabel={"Editing container vector policy"}
|
) => {
|
||||||
lineNumbers={"on"}
|
this.setState({ vectorEmbeddingPolicy, vectorIndexingPolicy, vectorPolicyValidated });
|
||||||
scrollBeyondLastLine={false}
|
|
||||||
spinnerClassName="panelSectionSpinner"
|
|
||||||
monacoContainerStyles={{
|
|
||||||
minHeight: 200,
|
|
||||||
}}
|
}}
|
||||||
onContentChanged={(newVectorEmbeddingPolicy: string) =>
|
|
||||||
this.setVectorEmbeddingPolicy(newVectorEmbeddingPolicy)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</Stack>
|
||||||
</CollapsibleSectionComponent>
|
</CollapsibleSectionComponent>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@ -1159,13 +1113,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setVectorEmbeddingPolicy(vectorEmbeddingPolicy: string): void {
|
private setVectorEmbeddingPolicy(vectorEmbeddingPolicy: DataModels.VectorEmbedding[]): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
vectorEmbeddingPolicy,
|
vectorEmbeddingPolicy,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setVectorIndexingPolicy(vectorIndexingPolicy: string): void {
|
private setVectorIndexingPolicy(vectorIndexingPolicy: DataModels.VectorIndex[]): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
vectorIndexingPolicy,
|
vectorIndexingPolicy,
|
||||||
});
|
});
|
||||||
@ -1251,6 +1205,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getContainerVectorPolicyTooltipContent(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Text variant="small">
|
||||||
|
Describe any properties in your data that contain vectors, so that they can be made available for similarity
|
||||||
|
queries.{" "}
|
||||||
|
<Link target="_blank" href="https://aka.ms/CosmosDBVectorSetup">
|
||||||
|
Learn more
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private shouldShowCollectionThroughputInput(): boolean {
|
private shouldShowCollectionThroughputInput(): boolean {
|
||||||
if (isServerlessAccount()) {
|
if (isServerlessAccount()) {
|
||||||
return false;
|
return false;
|
||||||
@ -1370,22 +1336,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldShowVectorSearchParameters()) {
|
if (this.shouldShowVectorSearchParameters() && !this.state.vectorPolicyValidated) {
|
||||||
try {
|
this.setState({ errorMessage: "Please fix errors in container vector policy" });
|
||||||
JSON.parse(this.state.vectorIndexingPolicy) as DataModels.IndexingPolicy;
|
|
||||||
} catch (e) {
|
|
||||||
this.setState({ errorMessage: "Invalid JSON format for indexingPolicy" });
|
|
||||||
return false;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1461,15 +1416,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
let indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
const indexingPolicy: DataModels.IndexingPolicy = this.state.enableIndexing
|
||||||
? AllPropertiesIndexed
|
? AllPropertiesIndexed
|
||||||
: SharedDatabaseDefault;
|
: SharedDatabaseDefault;
|
||||||
|
|
||||||
let vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
let vectorEmbeddingPolicy: DataModels.VectorEmbeddingPolicy;
|
||||||
|
|
||||||
if (this.shouldShowVectorSearchParameters()) {
|
if (this.shouldShowVectorSearchParameters()) {
|
||||||
indexingPolicy = JSON.parse(this.state.vectorIndexingPolicy);
|
indexingPolicy.vectorIndexes = this.state.vectorIndexingPolicy;
|
||||||
vectorEmbeddingPolicy = JSON.parse(this.state.vectorEmbeddingPolicy);
|
vectorEmbeddingPolicy = {
|
||||||
|
vectorEmbeddings: this.state.vectorEmbeddingPolicy,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const telemetryData = {
|
const telemetryData = {
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
|
import { RenderResult, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
|
import React from "react";
|
||||||
|
import { AddVectorEmbeddingPolicyForm } from "./AddVectorEmbeddingPolicyForm";
|
||||||
|
|
||||||
|
const mockVectorEmbedding: VectorEmbedding[] = [
|
||||||
|
{ path: "/vector1", dataType: "float32", distanceFunction: "euclidean", dimensions: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockVectorIndex: VectorIndex[] = [{ path: "/vector1", type: "flat" }];
|
||||||
|
|
||||||
|
const mockOnVectorEmbeddingChange = jest.fn();
|
||||||
|
|
||||||
|
describe("AddVectorEmbeddingPolicyForm", () => {
|
||||||
|
let component: RenderResult;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component = render(
|
||||||
|
<AddVectorEmbeddingPolicyForm
|
||||||
|
vectorEmbedding={mockVectorEmbedding}
|
||||||
|
vectorIndex={mockVectorIndex}
|
||||||
|
onVectorEmbeddingChange={mockOnVectorEmbeddingChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders correctly", () => {
|
||||||
|
expect(screen.getByText("Vector embedding 1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByPlaceholderText("/vector1")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calls onVectorEmbeddingChange on adding a new vector embedding", () => {
|
||||||
|
fireEvent.click(screen.getByText("Add vector embedding"));
|
||||||
|
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calls onDelete when delete button is clicked", async () => {
|
||||||
|
const deleteButton = component.container.querySelector("#delete-vector-policy-1");
|
||||||
|
fireEvent.click(deleteButton);
|
||||||
|
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||||
|
expect(screen.queryByText("Vector embedding 1")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calls onVectorEmbeddingPathChange on input change", () => {
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/newPath" } });
|
||||||
|
expect(mockOnVectorEmbeddingChange).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validates input correctly", async () => {
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "" } });
|
||||||
|
await waitFor(() => expect(screen.getByText("Vector embedding path should not be empty")).toBeInTheDocument(), {
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
await waitFor(
|
||||||
|
() =>
|
||||||
|
expect(
|
||||||
|
screen.getByText("Vector embedding dimension must be greater than 0 and less than or equal 4096"),
|
||||||
|
).toBeInTheDocument(),
|
||||||
|
{
|
||||||
|
timeout: 1500,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
fireEvent.change(component.container.querySelector("#vector-policy-dimension-1"), { target: { value: "4096" } });
|
||||||
|
fireEvent.change(screen.getByPlaceholderText("/vector1"), { target: { value: "/vector1" } });
|
||||||
|
await waitFor(() => expect(screen.queryByText("Vector embedding path should not be empty")).toBeNull(), {
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
await waitFor(
|
||||||
|
() => expect(screen.queryByText("Maximum allowed dimension for flat index is 505")).toBeInTheDocument(),
|
||||||
|
{
|
||||||
|
timeout: 1500,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("duplicate vector path is not allowed", async () => {
|
||||||
|
fireEvent.click(screen.getByText("Add vector embedding"));
|
||||||
|
fireEvent.change(component.container.querySelector("#vector-policy-path-2"), { target: { value: "/vector1" } });
|
||||||
|
await waitFor(() => expect(screen.queryByText("Vector embedding path is already defined")).toBeNull(), {
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,300 @@
|
|||||||
|
import {
|
||||||
|
DefaultButton,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownOption,
|
||||||
|
IStyleFunctionOrObject,
|
||||||
|
ITextFieldStyleProps,
|
||||||
|
ITextFieldStyles,
|
||||||
|
IconButton,
|
||||||
|
Label,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
} from "@fluentui/react";
|
||||||
|
import { VectorEmbedding, VectorIndex } from "Contracts/DataModels";
|
||||||
|
import { CollapsibleSectionComponent } from "Explorer/Controls/CollapsiblePanel/CollapsibleSectionComponent";
|
||||||
|
import {
|
||||||
|
getDataTypeOptions,
|
||||||
|
getDistanceFunctionOptions,
|
||||||
|
getIndexTypeOptions,
|
||||||
|
} from "Explorer/Panes/VectorSearchPanel/VectorSearchUtils";
|
||||||
|
import React, { FunctionComponent, useState } from "react";
|
||||||
|
|
||||||
|
export interface IAddVectorEmbeddingPolicyFormProps {
|
||||||
|
vectorEmbedding: VectorEmbedding[];
|
||||||
|
vectorIndex: VectorIndex[];
|
||||||
|
onVectorEmbeddingChange: (
|
||||||
|
vectorEmbeddings: VectorEmbedding[],
|
||||||
|
vectorIndexingPolicies: VectorIndex[],
|
||||||
|
validationPassed: boolean,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VectorEmbeddingPolicyData {
|
||||||
|
path: string;
|
||||||
|
dataType: VectorEmbedding["dataType"];
|
||||||
|
distanceFunction: VectorEmbedding["distanceFunction"];
|
||||||
|
dimensions: number;
|
||||||
|
indexType: VectorIndex["type"] | "none";
|
||||||
|
pathError: string;
|
||||||
|
dimensionsError: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type VectorEmbeddingPolicyProperty = "dataType" | "distanceFunction" | "indexType";
|
||||||
|
|
||||||
|
const textFieldStyles: IStyleFunctionOrObject<ITextFieldStyleProps, ITextFieldStyles> = {
|
||||||
|
fieldGroup: {
|
||||||
|
height: 27,
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
fontSize: 12,
|
||||||
|
padding: "0 8px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdownStyles = {
|
||||||
|
title: {
|
||||||
|
height: 27,
|
||||||
|
lineHeight: "24px",
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
height: 27,
|
||||||
|
lineHeight: "24px",
|
||||||
|
},
|
||||||
|
dropdownItem: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddVectorEmbeddingPolicyForm: FunctionComponent<IAddVectorEmbeddingPolicyFormProps> = ({
|
||||||
|
vectorEmbedding,
|
||||||
|
vectorIndex,
|
||||||
|
onVectorEmbeddingChange,
|
||||||
|
}): JSX.Element => {
|
||||||
|
const onVectorEmbeddingPathError = (path: string, index?: number): string => {
|
||||||
|
let error = "";
|
||||||
|
if (!path) {
|
||||||
|
error = "Vector embedding path should not be empty";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
index >= 0 &&
|
||||||
|
vectorEmbeddingPolicyData?.find(
|
||||||
|
(vectorEmbedding: VectorEmbeddingPolicyData, dataIndex: number) =>
|
||||||
|
dataIndex !== index && vectorEmbedding.path === path,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
error = "Vector embedding path is already defined";
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingDimensionError = (dimension: number, indexType: VectorIndex["type"] | "none"): string => {
|
||||||
|
let error = "";
|
||||||
|
if (dimension <= 0 || dimension > 4096) {
|
||||||
|
error = "Vector embedding dimension must be greater than 0 and less than or equal 4096";
|
||||||
|
}
|
||||||
|
if (indexType === "flat" && dimension > 505) {
|
||||||
|
error = "Maximum allowed dimension for flat index is 505";
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeData = (vectorEmbedding: VectorEmbedding[], vectorIndex: VectorIndex[]) => {
|
||||||
|
const mergedData: VectorEmbeddingPolicyData[] = [];
|
||||||
|
vectorEmbedding.forEach((embedding) => {
|
||||||
|
const matchingIndex = vectorIndex.find((index) => index.path === embedding.path);
|
||||||
|
mergedData.push({
|
||||||
|
...embedding,
|
||||||
|
indexType: matchingIndex?.type || "none",
|
||||||
|
pathError: onVectorEmbeddingPathError(embedding.path),
|
||||||
|
dimensionsError: onVectorEmbeddingDimensionError(embedding.dimensions, matchingIndex?.type || "none"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return mergedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [vectorEmbeddingPolicyData, setVectorEmbeddingPolicyData] = useState<VectorEmbeddingPolicyData[]>(
|
||||||
|
initializeData(vectorEmbedding, vectorIndex),
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
propagateData();
|
||||||
|
}, [vectorEmbeddingPolicyData]);
|
||||||
|
|
||||||
|
const propagateData = () => {
|
||||||
|
const vectorEmbeddings: VectorEmbedding[] = vectorEmbeddingPolicyData.map((policy: VectorEmbeddingPolicyData) => ({
|
||||||
|
dataType: policy.dataType,
|
||||||
|
dimensions: policy.dimensions,
|
||||||
|
distanceFunction: policy.distanceFunction,
|
||||||
|
path: policy.path,
|
||||||
|
}));
|
||||||
|
const vectorIndexingPolicies: VectorIndex[] = vectorEmbeddingPolicyData
|
||||||
|
.filter((policy: VectorEmbeddingPolicyData) => policy.indexType !== "none")
|
||||||
|
.map(
|
||||||
|
(policy) =>
|
||||||
|
({
|
||||||
|
path: policy.path,
|
||||||
|
type: policy.indexType,
|
||||||
|
}) as VectorIndex,
|
||||||
|
);
|
||||||
|
const validationPassed = vectorEmbeddingPolicyData.every(
|
||||||
|
(policy: VectorEmbeddingPolicyData) => policy.pathError === "" && policy.dimensionsError === "",
|
||||||
|
);
|
||||||
|
onVectorEmbeddingChange(vectorEmbeddings, vectorIndexingPolicies, validationPassed);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingPathChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = event.target.value.trim();
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
if (!vectorEmbeddings[index]?.path && !value.startsWith("/")) {
|
||||||
|
vectorEmbeddings[index].path = "/" + value;
|
||||||
|
} else {
|
||||||
|
vectorEmbeddings[index].path = value;
|
||||||
|
}
|
||||||
|
const error = onVectorEmbeddingPathError(value, index);
|
||||||
|
vectorEmbeddings[index].pathError = error;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingDimensionsChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = parseInt(event.target.value.trim()) || 0;
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
const vectorEmbedding = vectorEmbeddings[index];
|
||||||
|
vectorEmbeddings[index].dimensions = value;
|
||||||
|
const error = onVectorEmbeddingDimensionError(value, vectorEmbedding.indexType);
|
||||||
|
vectorEmbeddings[index].dimensionsError = error;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingIndexTypeChange = (index: number, option: IDropdownOption): void => {
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
const vectorEmbedding = vectorEmbeddings[index];
|
||||||
|
vectorEmbeddings[index].indexType = option.key as never;
|
||||||
|
const error = onVectorEmbeddingDimensionError(vectorEmbedding.dimensions, vectorEmbedding.indexType);
|
||||||
|
vectorEmbeddings[index].dimensionsError = error;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onVectorEmbeddingPolicyChange = (
|
||||||
|
index: number,
|
||||||
|
option: IDropdownOption,
|
||||||
|
property: VectorEmbeddingPolicyProperty,
|
||||||
|
): void => {
|
||||||
|
const vectorEmbeddings = [...vectorEmbeddingPolicyData];
|
||||||
|
vectorEmbeddings[index][property] = option.key as never;
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAdd = () => {
|
||||||
|
setVectorEmbeddingPolicyData([
|
||||||
|
...vectorEmbeddingPolicyData,
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
dataType: "float32",
|
||||||
|
distanceFunction: "euclidean",
|
||||||
|
dimensions: 0,
|
||||||
|
indexType: "none",
|
||||||
|
pathError: onVectorEmbeddingPathError(""),
|
||||||
|
dimensionsError: onVectorEmbeddingDimensionError(0, "none"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (index: number) => {
|
||||||
|
const vectorEmbeddings = vectorEmbeddingPolicyData.filter((_uniqueKey, j) => index !== j);
|
||||||
|
setVectorEmbeddingPolicyData(vectorEmbeddings);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 4 }}>
|
||||||
|
{vectorEmbeddingPolicyData.length > 0 &&
|
||||||
|
vectorEmbeddingPolicyData.map((vectorEmbeddingPolicy: VectorEmbeddingPolicyData, index: number) => (
|
||||||
|
<CollapsibleSectionComponent key={index} isExpandedByDefault={true} title={`Vector embedding ${index + 1}`}>
|
||||||
|
<Stack horizontal tokens={{ childrenGap: 4 }}>
|
||||||
|
<Stack
|
||||||
|
styles={{
|
||||||
|
root: {
|
||||||
|
margin: "0 0 6px 20px !important",
|
||||||
|
paddingLeft: 20,
|
||||||
|
width: "80%",
|
||||||
|
borderLeft: "1px solid",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Path</Label>
|
||||||
|
<TextField
|
||||||
|
id={`vector-policy-path-${index + 1}`}
|
||||||
|
required={true}
|
||||||
|
placeholder="/vector1"
|
||||||
|
styles={textFieldStyles}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onVectorEmbeddingPathChange(index, event)}
|
||||||
|
value={vectorEmbeddingPolicy.path || ""}
|
||||||
|
errorMessage={vectorEmbeddingPolicy.pathError}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Data type</Label>
|
||||||
|
<Dropdown
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getDataTypeOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.dataType}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingPolicyChange(index, option, "dataType")
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Distance function</Label>
|
||||||
|
<Dropdown
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getDistanceFunctionOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.distanceFunction}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingPolicyChange(index, option, "distanceFunction")
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Dimensions</Label>
|
||||||
|
<TextField
|
||||||
|
id={`vector-policy-dimension-${index + 1}`}
|
||||||
|
required={true}
|
||||||
|
styles={textFieldStyles}
|
||||||
|
value={String(vectorEmbeddingPolicy.dimensions || 0)}
|
||||||
|
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onVectorEmbeddingDimensionsChange(index, event)
|
||||||
|
}
|
||||||
|
errorMessage={vectorEmbeddingPolicy.dimensionsError}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack>
|
||||||
|
<Label styles={{ root: { fontSize: 12 } }}>Index type</Label>
|
||||||
|
<Dropdown
|
||||||
|
required={true}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
options={getIndexTypeOptions()}
|
||||||
|
selectedKey={vectorEmbeddingPolicy.indexType}
|
||||||
|
onChange={(_event: React.FormEvent<HTMLDivElement>, option: IDropdownOption) =>
|
||||||
|
onVectorEmbeddingIndexTypeChange(index, option)
|
||||||
|
}
|
||||||
|
></Dropdown>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<IconButton
|
||||||
|
id={`delete-vector-policy-${index + 1}`}
|
||||||
|
iconProps={{ iconName: "Delete" }}
|
||||||
|
style={{ height: 27, margin: "auto" }}
|
||||||
|
onClick={() => onDelete(index)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</CollapsibleSectionComponent>
|
||||||
|
))}
|
||||||
|
<DefaultButton id={`add-vector-policy`} styles={{ root: { maxWidth: 170, fontSize: 12 } }} onClick={onAdd}>
|
||||||
|
Add vector embedding
|
||||||
|
</DefaultButton>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
16
src/Explorer/Panes/VectorSearchPanel/VectorSearchUtils.ts
Normal file
16
src/Explorer/Panes/VectorSearchPanel/VectorSearchUtils.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { IDropdownOption } from "@fluentui/react";
|
||||||
|
|
||||||
|
const dataTypes = ["float32", "uint8", "int8"];
|
||||||
|
const distanceFunctions = ["euclidean", "cosine", "dotproduct"];
|
||||||
|
const indexTypes = ["none", "flat", "diskANN", "quantizedFlat"];
|
||||||
|
|
||||||
|
export const getDataTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(dataTypes);
|
||||||
|
export const getDistanceFunctionOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(distanceFunctions);
|
||||||
|
export const getIndexTypeOptions = (): IDropdownOption[] => createDropdownOptionsFromLiterals(indexTypes);
|
||||||
|
|
||||||
|
function createDropdownOptionsFromLiterals<T extends string>(literals: T[]): IDropdownOption[] {
|
||||||
|
return literals.map((value) => ({
|
||||||
|
key: value,
|
||||||
|
text: value,
|
||||||
|
}));
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user