diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/Angular/app/app.component.html b/apps/demos/Demos/DataGrid/SemanticSearch/Angular/app/app.component.html new file mode 100644 index 000000000000..e6096d1bdcc6 --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/Angular/app/app.component.html @@ -0,0 +1,34 @@ + + + + + +
+
+ Similarity Factor: + +
+
+
+ +
+ + + +
diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/Angular/app/app.component.ts b/apps/demos/Demos/DataGrid/SemanticSearch/Angular/app/app.component.ts new file mode 100644 index 000000000000..af8b03f0710f --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/Angular/app/app.component.ts @@ -0,0 +1,77 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core'; +import { DxDataGridModule, type DxDataGridTypes } from 'devextreme-angular/ui/data-grid'; +import { DxNumberBoxModule, type DxNumberBoxTypes } from 'devextreme-angular/ui/number-box'; +import { DataSource } from 'devextreme-angular/common/data'; +import * as AspNetData from 'devextreme-aspnet-data-nojquery'; + +if (!/localhost/.test(document.location.host)) { + enableProdMode(); +} + +let modulePrefix = ''; +// @ts-ignore +if (window && window.config?.packageConfigPaths) { + modulePrefix = '/app'; +} + +const url = 'https://js.devexpress.com/Demos/NetCore/api/DataGridSemanticSearch'; + +@Component({ + selector: 'demo-app', + templateUrl: `.${modulePrefix}/app.component.html`, + imports: [ + DxDataGridModule, + DxNumberBoxModule, + ], +}) +export class AppComponent { + searchValue = ''; + + similarityFactor = 0.31; + + dataSource: DataSource; + + constructor() { + this.dataSource = new DataSource({ + store: AspNetData.createStore({ + key: 'ID', + loadUrl: `${url}/Get`, + loadParams: { + searchValue: () => this.searchValue, + similarityFactor: () => this.similarityFactor, + }, + onBeforeSend: (method, ajaxOptions) => { + ajaxOptions.xhrFields = { withCredentials: true }; + }, + }), + }); + } + + onSimilarityFactorChanged(e: DxNumberBoxTypes.ValueChangedEvent): void { + this.similarityFactor = e.value; + if (this.searchValue !== '') { + this.dataSource.reload(); + } + } + + onEditorPreparing(e: DxDataGridTypes.EditorPreparingEvent): void { + if (e.parentType === 'searchPanel') { + let searchTimeout: ReturnType | undefined; + e.editorOptions.onValueChanged = (args: { value: string }) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + this.searchValue = args.value; + this.dataSource.reload(); + }, e.updateValueTimeout); + }; + e.editorOptions.placeholder = 'Try: clothing'; + } + } +} + +bootstrapApplication(AppComponent, { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }), + ], +}); diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/Angular/index.html b/apps/demos/Demos/DataGrid/SemanticSearch/Angular/index.html new file mode 100644 index 000000000000..1ab1fb54a1df --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/Angular/index.html @@ -0,0 +1,26 @@ + + + + DevExtreme Demo + + + + + + + + + + + + + + + +
+ Loading... +
+ + diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/React/App.tsx b/apps/demos/Demos/DataGrid/SemanticSearch/React/App.tsx new file mode 100644 index 000000000000..21ab47f66901 --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/React/App.tsx @@ -0,0 +1,84 @@ +import React, { useCallback, useRef, useMemo } from 'react'; +import DataGrid, { Column, Scrolling, SearchPanel, Toolbar, Item, type DataGridTypes } from 'devextreme-react/data-grid'; +import NumberBox, { type NumberBoxTypes } from 'devextreme-react/number-box'; +import { DataSource } from 'devextreme-react/common/data'; +import { createStore } from 'devextreme-aspnet-data-nojquery'; + +const url = 'https://js.devexpress.com/Demos/NetCore/api/DataGridSemanticSearch'; +const numberBoxAttr = { 'aria-label': 'Similarity Factor' }; + +const App = () => { + const searchValueRef = useRef(''); + const similarityFactorRef = useRef(0.31); + + const dataSource = useMemo(() => new DataSource({ + store: createStore({ + key: 'ID', + loadUrl: `${url}/Get`, + loadParams: { + searchValue: () => searchValueRef.current, + similarityFactor: () => similarityFactorRef.current, + }, + onBeforeSend(method, ajaxOptions) { + ajaxOptions.xhrFields = { withCredentials: true }; + }, + }), + }), []); + + const onSimilarityFactorChanged = useCallback(({ value }: NumberBoxTypes.ValueChangedEvent) => { + similarityFactorRef.current = value; + if (searchValueRef.current !== '') { + dataSource.reload(); + } + }, [dataSource]); + + const onEditorPreparing = useCallback((e: DataGridTypes.EditorPreparingEvent) => { + if (e.parentType === 'searchPanel') { + let searchTimeout: ReturnType | undefined; + e.editorOptions.onValueChanged = (args: { value: string }) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + searchValueRef.current = args.value; + e.component.getDataSource().reload(); + }, e.updateValueTimeout); + }; + e.editorOptions.placeholder = 'Try: clothing'; + } + }, []); + + return ( + + + + + +
+ Similarity Factor: + +
+
+ +
+ + + +
+ ); +}; + +export default App; diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/React/index.html b/apps/demos/Demos/DataGrid/SemanticSearch/React/index.html new file mode 100644 index 000000000000..cfb9b3aa6769 --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/React/index.html @@ -0,0 +1,23 @@ + + + + DevExtreme Demo + + + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/React/index.tsx b/apps/demos/Demos/DataGrid/SemanticSearch/React/index.tsx new file mode 100644 index 000000000000..8acbec4b6179 --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/React/index.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './App.tsx'; + +ReactDOM.render( + , + document.getElementById('app'), +); diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/ReactJs/App.js b/apps/demos/Demos/DataGrid/SemanticSearch/ReactJs/App.js new file mode 100644 index 000000000000..80e0bc6e4f21 --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/ReactJs/App.js @@ -0,0 +1,101 @@ +import React, { useCallback, useRef, useMemo } from 'react'; +import DataGrid, { + Column, + Scrolling, + SearchPanel, + Toolbar, + Item, +} from 'devextreme-react/data-grid'; +import NumberBox from 'devextreme-react/number-box'; +import { DataSource } from 'devextreme-react/common/data'; +import { createStore } from 'devextreme-aspnet-data-nojquery'; + +const url = 'https://js.devexpress.com/Demos/NetCore/api/DataGridSemanticSearch'; +const numberBoxAttr = { 'aria-label': 'Similarity Factor' }; +const App = () => { + const searchValueRef = useRef(''); + const similarityFactorRef = useRef(0.31); + const dataSource = useMemo( + () => + new DataSource({ + store: createStore({ + key: 'ID', + loadUrl: `${url}/Get`, + loadParams: { + searchValue: () => searchValueRef.current, + similarityFactor: () => similarityFactorRef.current, + }, + onBeforeSend(method, ajaxOptions) { + ajaxOptions.xhrFields = { withCredentials: true }; + }, + }), + }), + [], + ); + const onSimilarityFactorChanged = useCallback( + ({ value }) => { + similarityFactorRef.current = value; + if (searchValueRef.current !== '') { + dataSource.reload(); + } + }, + [dataSource], + ); + const onEditorPreparing = useCallback((e) => { + if (e.parentType === 'searchPanel') { + let searchTimeout; + e.editorOptions.onValueChanged = (args) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + searchValueRef.current = args.value; + e.component.getDataSource().reload(); + }, e.updateValueTimeout); + }; + e.editorOptions.placeholder = 'Try: clothing'; + } + }, []); + return ( + + + + + +
+ Similarity Factor: + +
+
+ +
+ + + +
+ ); +}; +export default App; diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/ReactJs/index.html b/apps/demos/Demos/DataGrid/SemanticSearch/ReactJs/index.html new file mode 100644 index 000000000000..d6d83f735a76 --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/ReactJs/index.html @@ -0,0 +1,39 @@ + + + + DevExtreme Demo + + + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/ReactJs/index.js b/apps/demos/Demos/DataGrid/SemanticSearch/ReactJs/index.js new file mode 100644 index 000000000000..b853e0be8242 --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/ReactJs/index.js @@ -0,0 +1,5 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App.js'; + +ReactDOM.render(, document.getElementById('app')); diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/Vue/App.vue b/apps/demos/Demos/DataGrid/SemanticSearch/Vue/App.vue new file mode 100644 index 000000000000..795b3932ebbe --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/Vue/App.vue @@ -0,0 +1,99 @@ + + diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/Vue/index.html b/apps/demos/Demos/DataGrid/SemanticSearch/Vue/index.html new file mode 100644 index 000000000000..b21c5e373a7a --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/Vue/index.html @@ -0,0 +1,29 @@ + + + + DevExtreme Demo + + + + + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/Vue/index.ts b/apps/demos/Demos/DataGrid/SemanticSearch/Vue/index.ts new file mode 100644 index 000000000000..684d04215d72 --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/Vue/index.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +createApp(App).mount('#app'); diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/description.md b/apps/demos/Demos/DataGrid/SemanticSearch/description.md new file mode 100644 index 000000000000..d94a4bea43a9 --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/description.md @@ -0,0 +1,19 @@ +This demo incorporates server-side semantic search into the [DevExtreme DataGrid](/Documentation/Guide/UI_Components/DataGrid/Overview/). Semantic search finds results based on meaning rather than exact wording, understanding the context and intent behind a question or phrase. This allows your DevExtreme-powered app to deliver more relevant answers by connecting related concepts, even if exact words differ. + +To review benefits of this feature, search for dictionary entries and their descriptions and use synonyms or generic descriptions instead of exact search strings (such as "clothing" instead of a specific product name). You can fine-tune search results: use the Similarity Factor editor to change the search precision. + + +[note] + +AI services used for this demo have been rate and data limited. As such, you may experience performance-related delays when exploring the capabilities of the DataGrid AI Assistant. + +When connected to your own AI model/service without rate and data limits, semantic search will perform seamlessly, without artificial delays. Note that DevExtreme does not offer an AI REST API and does not ship any built-in LLMs/SLMs. + +[/note] + +This demo configures semantic filtering on the server (using [AzureOpenAI embeddings](https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/embeddings?tabs=csharp)). You can incorporate server-side semantic search using built-in DataGrid visual elements. In this demo, each request returns filtered data using two parameters: + +- A search value (entered in the built-in [search panel](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/searchPanel/)) +- A similarity factor (can be modified via a custom [toolbar](/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/toolbar/) item) + +After you enter a query in the search panel, the DevExtreme DataGrid passes the new search value to the backend and loads the filtered data set. diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/jQuery/index.html b/apps/demos/Demos/DataGrid/SemanticSearch/jQuery/index.html new file mode 100644 index 000000000000..5aa7314c6053 --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/jQuery/index.html @@ -0,0 +1,19 @@ + + + + DevExtreme Demo + + + + + + + + + + +
+
+
+ + diff --git a/apps/demos/Demos/DataGrid/SemanticSearch/jQuery/index.js b/apps/demos/Demos/DataGrid/SemanticSearch/jQuery/index.js new file mode 100644 index 000000000000..6b3e8c43a585 --- /dev/null +++ b/apps/demos/Demos/DataGrid/SemanticSearch/jQuery/index.js @@ -0,0 +1,88 @@ +$(() => { + const url = 'https://js.devexpress.com/Demos/NetCore/api/DataGridSemanticSearch'; + + let searchValue = ''; + let similarityFactor = 0.31; + + const dataGrid = $('#gridContainer').dxDataGrid({ + dataSource: DevExpress.data.AspNet.createStore({ + key: 'ID', + loadUrl: `${url}/Get`, + loadParams: { + searchValue: () => searchValue, + similarityFactor: () => similarityFactor, + }, + onBeforeSend(method, ajaxOptions) { + ajaxOptions.xhrFields = { withCredentials: true }; + }, + }), + showBorders: true, + remoteOperations: true, + height: 600, + columns: [{ + dataField: 'ID', + width: 50, + }, { + dataField: 'Name', + }, { + dataField: 'Description', + }], + scrolling: { + mode: 'virtual', + }, + searchPanel: { + visible: true, + }, + toolbar: { + items: [ + { + location: 'before', + template() { + const container = $('
') + .css('display', 'flex') + .css('align-items', 'center'); + + const label = $('') + .text('Similarity Factor:') + .css('margin-right', '8px'); + + const editor = $('
') + .dxNumberBox({ + value: similarityFactor, + min: 0, + max: 1, + format: '0.00', + step: 0.05, + showSpinButtons: true, + inputAttr: { 'aria-label': 'Similarity Factor' }, + onValueChanged(e) { + similarityFactor = e.value; + if (searchValue !== '') { + dataGrid.getDataSource().reload(); + } + }, + }); + + return container.append(label, editor); + }, + }, + { + name: 'searchPanel', + }, + ], + }, + onEditorPreparing(e) { + if (e.parentType === 'searchPanel') { + let searchTimeout; + e.editorOptions.onValueChanged = (args) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + searchValue = args.value; + e.component.getDataSource().reload(); + }, e.updateValueTimeout); + }; + e.editorOptions.placeholder = 'Try: clothing'; + } + }, + }).dxDataGrid('instance'); +}); diff --git a/apps/demos/menuMeta.json b/apps/demos/menuMeta.json index 615934456789..f761c41660c8 100644 --- a/apps/demos/menuMeta.json +++ b/apps/demos/menuMeta.json @@ -37,6 +37,35 @@ "Modules": "openai" } ] + }, + { + "Name": "Semantic Search", + "Equivalents": "", + "Demos": [ + { + "Title": "Semantic Search", + "Name": "SemanticSearch", + "Widget": "DataGrid", + "Badge": "AI", + "DemoType": "Web", + "Modules": "devextreme-aspnet-data-nojquery", + "MvcAdditionalFiles": [ + "/DataProviders/SmartFilterProvider.cs", + "/Controllers/ApiControllers/DataGridSemanticSearchController.cs", + "/Services/EmbeddingsInitializerService.cs", + "/Models/DataGrid/DictionaryItem.cs", + "/Models/SampleData/DictionaryItems.cs" + ], + "BackendFiles": [ + "SmartFilterProvider.cs", + "DataGridSemanticSearchController.cs", + "EmbeddingsInitializerService.cs", + "DictionaryItem.cs" + ], + "TopLevel": true, + "Equivalents": "AI-Powered Search, Smart Search, Context-Aware Search, Natural Language Search, AI Search, Semantic Retrieval, Similarity Search" + } + ] } ] }, @@ -312,6 +341,28 @@ "/Models/SampleData/DataGridEmployees.cs" ], "DemoType": "Web" + }, + { + "Title": "Semantic Search", + "Name": "SemanticSearch", + "Widget": "DataGrid", + "Badge": "AI", + "DemoType": "Web", + "Modules": "devextreme-aspnet-data-nojquery", + "MvcAdditionalFiles": [ + "/DataProviders/SmartFilterProvider.cs", + "/Controllers/ApiControllers/DataGridSemanticSearchController.cs", + "/Services/EmbeddingsInitializerService.cs", + "/Models/DataGrid/DictionaryItem.cs", + "/Models/SampleData/DictionaryItems.cs" + ], + "BackendFiles": [ + "SmartFilterProvider.cs", + "DataGridSemanticSearchController.cs", + "EmbeddingsInitializerService.cs", + "DictionaryItem.cs" + ], + "Equivalents": "AI-Powered Search, Smart Search, Context-Aware Search, Natural Language Search, AI Search, Semantic Retrieval, Similarity Search" } ] }, diff --git a/apps/demos/testing/etalons/DataGrid-SemanticSearch (fluent.blue.light).png b/apps/demos/testing/etalons/DataGrid-SemanticSearch (fluent.blue.light).png new file mode 100644 index 000000000000..826709c5a1e1 Binary files /dev/null and b/apps/demos/testing/etalons/DataGrid-SemanticSearch (fluent.blue.light).png differ diff --git a/apps/demos/testing/etalons/DataGrid-SemanticSearch (material.blue.light).png b/apps/demos/testing/etalons/DataGrid-SemanticSearch (material.blue.light).png new file mode 100644 index 000000000000..7c2ab3efa148 Binary files /dev/null and b/apps/demos/testing/etalons/DataGrid-SemanticSearch (material.blue.light).png differ diff --git a/apps/demos/utils/server/csp-server.js b/apps/demos/utils/server/csp-server.js index c3523e7a7413..5b2243644fd6 100644 --- a/apps/demos/utils/server/csp-server.js +++ b/apps/demos/utils/server/csp-server.js @@ -91,6 +91,9 @@ const CSP_DEMO_ALLOWLIST = { 'DataGrid/RemoteVirtualScrolling': { 'img-src': ['data:'], }, + 'DataGrid/SemanticSearch': { + 'img-src': ['data:'], + }, 'DataGrid/VirtualScrolling': { 'img-src': ['data:'], },