Skip to content
This repository was archived by the owner on Mar 10, 2026. It is now read-only.

Commit a04512a

Browse files
Maosaicopensearch-changeset-bot[bot]
authored andcommitted
[Explore] Improve performance with large query result (opensearch-project#11390)
* [Explore] Improve performance with large query result Signed-off-by: Joey Liu <jiyili@amazon.com> * Changeset file for PR opensearch-project#11390 created/updated * Fix tests Signed-off-by: Joey Liu <jiyili@amazon.com> --------- Signed-off-by: Joey Liu <jiyili@amazon.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Mark Boyd <mark.boyd@gsa.gov>
1 parent 3a8b1d4 commit a04512a

5 files changed

Lines changed: 51 additions & 32 deletions

File tree

changelogs/fragments/11390.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
chore:
2+
- Improve performance with large query result ([#11390](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/11390))

src/plugins/explore/public/application/utils/state_management/actions/detect_optimal_tab/detect_optimal_tab.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,53 @@ import { RootState } from '../../store';
88
import { setActiveTab } from '../../slices';
99
import { ExploreServices } from '../../../../../types';
1010
import { defaultPrepareQueryString } from '../query_actions';
11-
import { normalizeResultRows } from '../../../../../components/visualizations/utils/normalize_result_rows';
1211
import { visualizationRegistry } from '../../../../../components/visualizations/visualization_registry';
13-
import { prepareQueryForLanguage } from '../../../languages';
12+
import { FIELD_TYPE_MAP } from '../../../../../components/visualizations/constants';
13+
import { VisColumn, VisFieldType } from '../../../../../components/visualizations/types';
1414
import { Query } from '../../../../../../../data/common';
1515
import { QueryExecutionStatus } from '../../types';
1616
import { EXPLORE_LOGS_TAB_ID, EXPLORE_VISUALIZATION_TAB_ID } from '../../../../../../common';
1717

1818
/**
19-
* Determine if results can be visualized
19+
* Determine if results can be visualized.
20+
* Classifies columns from fieldSchema only — does not iterate over row data.
2021
*/
2122
const canResultsBeVisualized = (results: any): boolean => {
2223
if (!results?.hits?.hits || !results?.fieldSchema || results.hits.hits.length === 0) {
2324
return false;
2425
}
2526

26-
const rows = results.hits.hits;
27-
const fieldSchema = results.fieldSchema;
28-
const { numericalColumns, categoricalColumns, dateColumns } = normalizeResultRows(
29-
rows,
30-
fieldSchema
31-
);
32-
const matchedRule = visualizationRegistry.findBestMatch(
33-
numericalColumns,
34-
categoricalColumns,
35-
dateColumns
36-
);
27+
const rowCount = results.hits.hits.length;
28+
const numericalColumns: VisColumn[] = [];
29+
const categoricalColumns: VisColumn[] = [];
30+
const dateColumns: VisColumn[] = [];
3731

38-
return !!matchedRule;
32+
results.fieldSchema.forEach((field: { type?: string; name?: string }, index: number) => {
33+
const schema = FIELD_TYPE_MAP[field.type || ''] || VisFieldType.Unknown;
34+
const column: VisColumn = {
35+
id: index,
36+
schema,
37+
name: field.name || '',
38+
column: `field-${index}`,
39+
// Use rowCount as a conservative upper bound — rules checking uniqueValuesCount
40+
// thresholds (e.g. >= 7) will pass correctly when there are enough rows.
41+
validValuesCount: rowCount,
42+
uniqueValuesCount: rowCount,
43+
};
44+
if (schema === VisFieldType.Numerical) numericalColumns.push(column);
45+
else if (schema === VisFieldType.Categorical) categoricalColumns.push(column);
46+
else if (schema === VisFieldType.Date) dateColumns.push(column);
47+
});
48+
49+
return !!visualizationRegistry.findBestMatch(numericalColumns, categoricalColumns, dateColumns);
3950
};
4051

4152
/**
4253
* Determine the optimal tab based on results
4354
* Use visualization tab if we can find a visualization type
4455
* Otherwise, fallback to logs tab
4556
*/
46-
const determineOptimalTab = (results: any, services: ExploreServices): string => {
57+
const determineOptimalTab = (results: any, _services: ExploreServices): string => {
4758
if (canResultsBeVisualized(results)) {
4859
return EXPLORE_VISUALIZATION_TAB_ID || EXPLORE_LOGS_TAB_ID;
4960
}

src/plugins/explore/public/application/utils/state_management/store.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,25 @@ export const configurePreloadedStore = (
6565
return configureStore({
6666
reducer: rootReducer,
6767
preloadedState,
68-
middleware: (getDefaultMiddleware) =>
69-
services
70-
? getDefaultMiddleware()
68+
middleware: (getDefaultMiddleware) => {
69+
// Exclude results from dev-mode checks: the results slice holds raw OpenSearch hits which
70+
// can be very large. ImmutableStateInvariantMiddleware walks the entire state tree on every
71+
// dispatch — including stored results — making every user interaction slow when results are
72+
// loaded. Results are always fully replaced, never partially mutated, so these checks add
73+
// no safety value there.
74+
const middlewareOptions = {
75+
immutableCheck: { ignoredPaths: ['results'] },
76+
serializableCheck: { ignoredPaths: ['results'] },
77+
};
78+
return services
79+
? getDefaultMiddleware(middlewareOptions)
7180
.concat(createPersistenceMiddleware(services))
7281
.concat(createQuerySyncMiddleware(services))
7382
.concat(createTimefilterSyncMiddleware(services))
7483
.concat(createDatasetChangeMiddleware(services))
7584
.concat(createOverallStatusMiddleware())
76-
: getDefaultMiddleware(),
85+
: getDefaultMiddleware(middlewareOptions);
86+
},
7787
});
7888
};
7989

src/plugins/explore/public/components/data_table/table_cell/source_field_table_cell.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ describe('SourceFieldTableCell', () => {
6969

7070
renderInTable(defaultProps);
7171

72-
expect(mockDataset.formatHit).toHaveBeenCalledWith(mockRow);
72+
expect(mockDataset.formatHit).toHaveBeenCalledWith(mockRow, 'text');
7373
});
7474

7575
it('renders field names and values', () => {

src/plugins/explore/public/components/data_table/table_cell/source_field_table_cell.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import './source_field_table_cell.scss';
1313

1414
import React, { Fragment } from 'react';
15-
import dompurify from 'dompurify';
1615
import { IndexPattern, DataView as Dataset } from 'src/plugins/data/public';
1716
import { shortenDottedString } from '../../../helpers/shorten_dotted_string';
1817
import { OpenSearchSearchHit } from '../../../types/doc_views_types';
@@ -25,14 +24,14 @@ export interface SourceFieldTableCellProps {
2524
wrapCellText?: boolean;
2625
}
2726

28-
export const SourceFieldTableCell: React.FC<SourceFieldTableCellProps> = ({
27+
const SourceFieldTableCellComponent: React.FC<SourceFieldTableCellProps> = ({
2928
colName,
3029
dataset,
3130
row,
3231
isShortDots,
3332
wrapCellText,
3433
}) => {
35-
const formattedRow = dataset.formatHit(row);
34+
const formattedRow = dataset.formatHit(row, 'text');
3635
const metaFields = dataset.metaFields || [];
3736
const rawKeys = Object.keys(formattedRow).filter((key) => !metaFields.includes(key));
3837
const keys = isShortDots ? rawKeys.map((k) => shortenDottedString(k)) : rawKeys;
@@ -52,14 +51,9 @@ export const SourceFieldTableCell: React.FC<SourceFieldTableCellProps> = ({
5251
<span className="source__key" data-test-subj="sourceFieldKey">
5352
{key}:
5453
</span>
55-
<span
56-
className="source__value"
57-
data-test-subj="sourceFieldValue"
58-
// eslint-disable-next-line react/no-danger
59-
dangerouslySetInnerHTML={{
60-
__html: dompurify.sanitize(formattedRow[rawKeys[index]]),
61-
}}
62-
/>
54+
<span className="source__value" data-test-subj="sourceFieldValue">
55+
{formattedRow[rawKeys[index]]}
56+
</span>
6357
{index !== keys.length - 1 && ' '}
6458
</Fragment>
6559
))}
@@ -68,3 +62,5 @@ export const SourceFieldTableCell: React.FC<SourceFieldTableCellProps> = ({
6862
</td>
6963
);
7064
};
65+
66+
export const SourceFieldTableCell = React.memo(SourceFieldTableCellComponent);

0 commit comments

Comments
 (0)