DevExtreme v26.1 is now available.

Explore our newest features/capabilities and share your thoughts with us.

Your search did not match any results.

Angular Data Grid - Semantic Search

This demo incorporates server-side semantic search into the DevExtreme DataGrid. 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.

Backend API
<dx-data-grid id="gridContainer" [dataSource]="dataSource" [showBorders]="true" [remoteOperations]="true" [height]="600" (onEditorPreparing)="onEditorPreparing($event)" > <dxo-data-grid-scrolling mode="virtual"></dxo-data-grid-scrolling> <dxo-data-grid-search-panel [visible]="true"></dxo-data-grid-search-panel> <dxo-data-grid-toolbar> <dxi-data-grid-item location="before"> <div *dxTemplate> <div [style]="{ display: 'flex', alignItems: 'center' }"> <span [style]="{ marginRight: '8px' }">Similarity Factor:</span> <dx-number-box [value]="similarityFactor" [min]="0" [max]="1" format="0.00" [step]="0.05" [showSpinButtons]="true" [inputAttr]="{ 'aria-label': 'Similarity Factor' }" (onValueChanged)="onSimilarityFactorChanged($event)" ></dx-number-box> </div> </div> </dxi-data-grid-item> <dxi-data-grid-item name="searchPanel"></dxi-data-grid-item> </dxo-data-grid-toolbar> <dxi-data-grid-column dataField="ID" [width]="50"></dxi-data-grid-column> <dxi-data-grid-column dataField="Name"></dxi-data-grid-column> <dxi-data-grid-column dataField="Description"></dxi-data-grid-column> </dx-data-grid>
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({ styleUrls: [`app/app.component.css`], selector: 'demo-app', templateUrl: `app/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<typeof setTimeout> | 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 }), ], });
// In real applications, you should not transpile code in the browser. // You can see how to create your own application with Angular and DevExtreme here: // https://js.devexpress.com/Documentation/Guide/Angular_Components/Getting_Started/Create_a_DevExtreme_Application/ const componentNames = [ 'accordion', 'action-sheet', 'autocomplete', 'bar-gauge', 'box', 'bullet', 'button-group', 'button', 'calendar', 'card-view', 'chart', 'chat', 'check-box', 'circular-gauge', 'color-box', 'context-menu', 'data-grid', 'date-box', 'date-range-box', 'defer-rendering', 'diagram', 'draggable', 'drawer', 'drop-down-box', 'drop-down-button', 'file-manager', 'file-uploader', 'filter-builder', 'form', 'funnel', 'gallery', 'gantt', 'html-editor', 'linear-gauge', 'list', 'load-indicator', 'load-panel', 'lookup', 'map', 'menu', 'multi-view', 'nested', 'number-box', 'pagination', 'pie-chart', 'pivot-grid-field-chooser', 'pivot-grid', 'polar-chart', 'popover', 'popup', 'progress-bar', 'radio-group', 'range-selector', 'range-slider', 'recurrence-editor', 'resizable', 'responsive-box', 'sankey', 'scheduler', 'scroll-view', 'select-box', 'slider', 'sortable', 'sparkline', 'speech-to-text', 'speed-dial-action', 'splitter', 'stepper', 'switch', 'tab-panel', 'tabs', 'tag-box', 'text-area', 'text-box', 'tile-view', 'toast', 'toolbar', 'tooltip', 'tree-list', 'tree-map', 'tree-view', 'validation-group', 'validation-summary', 'validator', 'vector-map', ]; window.exports = window.exports || {}; window.config = { transpiler: false, meta: { '*.ts': { loader: 'demo-ts-loader', }, 'typescript': { 'exports': 'ts', }, 'anti-forgery': { loader: 'demo-ts-loader', }, 'devextreme-aspnet-data-nojquery': { 'esModule': true, }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, '@angular/platform-browser-dynamic': { 'esModule': true, }, '@angular/platform-browser': { 'esModule': true, }, '@angular/core': { 'esModule': true, }, '@angular/common': { 'esModule': true, }, '@angular/common/http': { 'esModule': true, }, '@angular/animations': { 'esModule': true, }, '@angular/forms': { 'esModule': true, }, 'openai': { 'esModule': true, }, 'zod': { 'esModule': true, }, 'zod-to-json-schema': { 'esModule': true, }, }, paths: { 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', 'anti-forgery:': '../../../../shared/anti-forgery/', 'loaders:': '../../../../shared/loaders/', }, map: { 'anti-forgery': 'anti-forgery:fetch-override.js', 'demo-ts-loader': 'loaders:demo-ts-loader.js', 'demo-ts-shared': 'loaders:demo-ts-shared.js', 'typescript': 'npm:typescript@4.2.4/lib/typescript.js', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', /* @angular */ '@angular/compiler': 'bundles:@angular/compiler.umd.js', '@angular/platform-browser-dynamic': 'bundles:@angular/platform-browser-dynamic.umd.js', '@angular/core': 'bundles:@angular/core.umd.js', '@angular/core/primitives/signals': 'bundles:@angular/core.primitives.signals.umd.js', '@angular/core/primitives/di': 'bundles:@angular/core.primitives.di.umd.js', '@angular/common': 'bundles:@angular/common.umd.js', '@angular/common/http': 'bundles:@angular/common-http.umd.js', '@angular/platform-browser': 'bundles:@angular/platform-browser.umd.js', '@angular/platform-browser/animations': 'bundles:@angular/platform-browser.umd.js', '@angular/forms': 'bundles:@angular/forms.umd.js', /* devextreme */ 'devextreme': 'npm:devextreme@link:../../packages/devextreme/artifacts/npm/devextreme/cjs', 'devextreme-quill': 'npm:devextreme-quill@1.7.10/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.29', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.69', /* devextreme-angular umd maps */ 'devextreme-angular': 'bundles:devextreme-angular/devextreme-angular.umd.js', 'devextreme-angular/common/ai-integration': 'bundles:devextreme-angular/devextreme-angular-common-ai-integration.umd.js', 'devextreme-angular/core': 'bundles:devextreme-angular/devextreme-angular-core.umd.js', 'devextreme-angular/common/charts': 'bundles:devextreme-angular/devextreme-angular-common-charts.umd.js', 'devextreme-angular/common/core/animation': 'bundles:devextreme-angular/devextreme-angular-common-core-animation.umd.js', 'devextreme-angular/common/core/environment': 'bundles:devextreme-angular/devextreme-angular-common-core-environment.umd.js', 'devextreme-angular/common/core/events': 'bundles:devextreme-angular/devextreme-angular-common-core-events.umd.js', 'devextreme-angular/common/core/localization': 'bundles:devextreme-angular/devextreme-angular-common-core-localization.umd.js', 'devextreme-angular/common/core': 'bundles:devextreme-angular/devextreme-angular-common-core.umd.js', 'devextreme-angular/common/data/custom-store': 'bundles:devextreme-angular/devextreme-angular-common-data-custom-store.umd.js', 'devextreme-angular/common/data': 'bundles:devextreme-angular/devextreme-angular-common-data.umd.js', 'devextreme-angular/common/export/excel': 'bundles:devextreme-angular/devextreme-angular-common-export-excel.umd.js', 'devextreme-angular/common/export/pdf': 'bundles:devextreme-angular/devextreme-angular-common-export-pdf.umd.js', 'devextreme-angular/common/export': 'bundles:devextreme-angular/devextreme-angular-common-export.umd.js', 'devextreme-angular/common/grids': 'bundles:devextreme-angular/devextreme-angular-common-grids.umd.js', 'devextreme-angular/common': 'bundles:devextreme-angular/devextreme-angular-common.umd.js', 'devextreme-angular/http': 'bundles:devextreme-angular/devextreme-angular-http.umd.js', 'devextreme-angular/core/tokens': 'bundles:devextreme-angular/devextreme-angular-core-tokens.umd.js', ...componentNames.reduce((acc, name) => { acc[`devextreme-angular/ui/${name}`] = `bundles:devextreme-angular/devextreme-angular-ui-${name}.umd.js`; acc[`devextreme-angular/ui/${name}/nested`] = `bundles:devextreme-angular/devextreme-angular-ui-${name}-nested.umd.js`; return acc; }, {}), 'zod': 'externals:zod.bundle.js', 'zod-to-json-schema': 'externals:zod-to-json-schema.bundle.js', 'tslib': 'npm:tslib/tslib.js', 'rxjs': 'npm:rxjs@7.5.3/dist/bundles/rxjs.umd.js', 'rxjs/operators': 'npm:rxjs@7.5.3/dist/cjs/operators/index.js', 'devextreme-aspnet-data-nojquery': 'npm:devextreme-aspnet-data-nojquery@5.1.0/index.js', 'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js', 'luxon': 'npm:luxon@3.4.4/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign', 'inferno': 'npm:inferno@8.2.3/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@8.2.3/dist/inferno-create-element.min.js', 'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js', 'inferno-hydrate': 'npm:inferno-hydrate/dist/inferno-hydrate.min.js', 'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js', 'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js', 'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js', '@preact/signals-core': 'npm:@preact/signals-core@1.14.1/dist/signals-core.min.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', 'zone.js': 'npm:zone.js@0.16.2/bundles/zone.umd.js', }, packages: { 'app': { main: './app.component.ts', defaultExtension: 'ts', }, 'devextreme': { defaultExtension: 'js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/common/core/events/utils': { main: 'index', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, 'rxjs': { defaultExtension: 'js', }, 'rxjs/operators': { defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', 'npm:rxjs@7.5.3/package.json', 'npm:rxjs@7.5.3/operators/package.json', 'npm:devexpress-diagram@2.2.29/package.json', 'npm:devexpress-gantt@4.1.69/package.json', ], }; window.process = { env: { NODE_ENV: 'production', }, }; System.config(window.config); // eslint-disable-next-line no-console // System.import('@angular/compiler').catch(console.error.bind(console)); // eslint-disable-next-line const useTgzInCSB = ['openai']; let packagesInfo = { "zod": { "version": "3.24.4" }, "zod-to-json-schema": { "version": "3.24.6" }, "@angular/core": { "version": "22.0.2" }, "core-js": { "version": "2.6.12" }, "typescript": { "version": "6.0.3" }, "zone.js": { "version": "0.16.2" } };
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" lang="en"> <head> <title>DevExtreme Demo</title> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" /> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/26.1.3/css/dx.light.css" /> <script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/zone.js@0.16.2/bundles/zone.umd.js"></script> <script src="https://cdn.jsdelivr.net/npm/reflect-metadata@0.1.13/Reflect.js"></script> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.3/dist/system.js"></script> <script src="config.js"></script> <script> System.import("app").catch(console.error.bind(console)); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <demo-app>Loading...</demo-app> </div> </body> </html>
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Numerics.Tensors; using System.Threading.Tasks; using Azure.AI.OpenAI; using Microsoft.Extensions.AI; namespace DevExtreme.MVC.Demos.DataProviders { public class SmartFilterProvider { readonly IEmbeddingGenerator<string, Embedding<float>> Embedder; readonly static ConcurrentDictionary<string, Embedding<float>> cache = new ConcurrentDictionary<string, Embedding<float>>(StringComparer.OrdinalIgnoreCase); public SmartFilterProvider(AzureOpenAIClient openAIClient) { Embedder = openAIClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator(); } public async Task FillCacheAsync(IEnumerable<string> words) { try { var nonCachedWords = words.Where(x => !cache.ContainsKey(x)).ToArray(); if(!nonCachedWords.Any()) return; var embeddings = await Embedder.GenerateAsync(nonCachedWords); foreach(var (word, embedding) in nonCachedWords.Zip(embeddings, (word, embedding) => (word, embedding))) { cache[word] = embedding; } } catch { } } public float GetSimilarity(string text, string searchText) { var eFilter = cache[text]; var eText = cache[searchText]; var cosineSimilarity = TensorPrimitives.CosineSimilarity(eText.Vector.Span, eFilter.Vector.Span); return cosineSimilarity; } } }
using DevExtreme.AspNet.Data; using DevExtreme.AspNet.Mvc; using DevExtreme.MVC.Demos.DataProviders; using DevExtreme.MVC.Demos.Models.DataGrid; using DevExtreme.MVC.Demos.Models.SampleData; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Web.Http; namespace DevExtreme.MVC.Demos.Controllers.ApiControllers { public class DataGridSemanticSearchController : ApiController { SmartFilterProvider smartFilter; public DataGridSemanticSearchController(SmartFilterProvider smartFilterProvider) { smartFilter = smartFilterProvider; } [HttpGet] public async Task<object> Get(string searchValue, float similarityFactor, DataSourceLoadOptions loadOptions) { if(string.IsNullOrEmpty(searchValue)) { return DataSourceLoader.Load(SampleData.DictionaryItems, loadOptions); } await smartFilter.FillCacheAsync(new[] { searchValue }); var result = Search(searchValue, similarityFactor); return DataSourceLoader.Load(result, loadOptions); } IEnumerable<DictionaryItem> Search(string searchValue, float similarityFactor = 0.31f) { return SampleData.DictionaryItems .Select(item => new { Entry = item, Similarity = smartFilter.GetSimilarity(DictionaryItem.GetItemDescription(item), searchValue) }) .OrderByDescending(x => x.Similarity) .Where(x => x.Similarity > similarityFactor) .Select(x => x.Entry); } } }
using DevExtreme.MVC.Demos.DataProviders; using DevExtreme.MVC.Demos.Models.DataGrid; using DevExtreme.MVC.Demos.Models.SampleData; using Microsoft.Extensions.Hosting; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace DevExtreme.MVC.Demos.Services { public class EmbeddingsInitializerService : IHostedService { readonly SmartFilterProvider smartFilter; public EmbeddingsInitializerService(SmartFilterProvider smartFilterProvider) { smartFilter = smartFilterProvider; } public Task StartAsync(CancellationToken cancellationToken) { return smartFilter.FillCacheAsync(SampleData.DictionaryItems.Select(DictionaryItem.GetItemDescription)); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } }
namespace DevExtreme.MVC.Demos.Models.DataGrid { public class DictionaryItem { public DictionaryItem(int id, string name, string description) { ID = id; Name = name; Description = description; } public int ID { get; set; } public string Name { get; set; } public string Description { get; set; } public static string GetItemDescription(DictionaryItem item) { return $"{item.Name} - {item.Description}"; } } }

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.

This demo configures semantic filtering on the server (using AzureOpenAI embeddings). 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)
  • A similarity factor (can be modified via a custom 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.