diff --git a/CHANGELOG.md b/CHANGELOG.md index 43afd88fe7..7ebf2493a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#3738](https://github.com/plotly/dash/pull/3738) Add missing `stacklevel=2` to `warnings.warn()` calls so warnings report the caller's location instead of internal Dash source lines - [#3740](https://github.com/plotly/dash/pull/3740) Fix cannot tab into dropdowns in Safari - [#2462](https://github.com/plotly/dash/issues/2462) Allow `MATCH` in `Input`/`State` when the callback's `Output` has no wildcards (fixed-id Output, no Output, or `ALL`-only wildcard Output). `ALLSMALLER` still requires a corresponding `MATCH` in an Output. +- [#3768](https://github.com/plotly/dash/pull/3768) Improved `Dropdown` search performance for large options lists ## [4.1.0] - 2026-03-23 diff --git a/components/dash-core-components/src/utils/dropdownSearch.ts b/components/dash-core-components/src/utils/dropdownSearch.ts index 7b88014a4f..079bba0290 100644 --- a/components/dash-core-components/src/utils/dropdownSearch.ts +++ b/components/dash-core-components/src/utils/dropdownSearch.ts @@ -23,6 +23,7 @@ export interface SanitizedOptions { options: DetailedOption[]; indexes: string[]; valueSet: Set; + search: Search; } // Single-pass sanitization via sanitizeOptions, plus detection of @@ -55,38 +56,48 @@ export function sanitizeDropdownOptions( indexes.push('search'); } - return {options: sanitized, indexes, valueSet}; + // Build the search index ONCE during sanitization + const search = new Search('value'); + search.searchIndex = new UnorderedSearchIndex(); + search.indexStrategy = new AllSubstringsIndexStrategy(); + search.tokenizer = TOKENIZER; + + indexes.forEach(index => { + search.addIndex(index); + }); + + if (sanitized.length > 0) { + search.addDocuments(sanitized); + } + + return { + options: sanitized, + indexes, + valueSet, + search, + }; } export function filterOptions( options: SanitizedOptions, searchValue?: string, - search_order?: 'index' | 'original' + searchOrder?: string ): DetailedOption[] { if (!searchValue) { return options.options; } - const search = new Search('value'); - search.searchIndex = new UnorderedSearchIndex(); - search.indexStrategy = new AllSubstringsIndexStrategy(); - search.tokenizer = TOKENIZER; - - options.indexes.forEach(index => { - search.addIndex(index); - }); + const results = + (options.search.search(searchValue) as DetailedOption[]) || []; - if (options.options.length > 0) { - search.addDocuments(options.options); - } + // Preserve original option order + if (searchOrder === 'original') { + const resultSet = new Set(results.map(option => option.value)); - const searchResults = - (search.search(searchValue) as DetailedOption[]) || []; - - if (search_order === 'original') { - const resultSet = new Set(searchResults); - return options.options.filter(option => resultSet.has(option)); + return options.options.filter(option => + resultSet.has(option.value) + ); } - return searchResults; + return results; }