Merge pull request #3106 from joenunezb/optimize/improve-search perf(search): improve fuzzy character search performance by ~13x (4.5s → 350ms)

c3c16ea0d61802f610f7164a7cf004d9ecc49e8a

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +118 -75Showing whitespace changes
public/script.js+4 -3
@@ -1621,7 +1621,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
16211621 subEntities = filterByTagState(entities, { subForEntity: entity });
16221622 if (doFilter) {
16231623 // sub entities filter "hacked" because folder filter should not be applied there, so even in "only folders" mode characters show up
16241624 subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false, tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED }, clearFuzzySearchCaches: false });
16251625 }
16261626 if (doSort) {
16271627 sortEntitiesList(subEntities);
@@ -1634,11 +1634,11 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
16341634 // Second run filters, hiding whatever should be filtered later
16351635 if (doFilter) {
16361636 const beforeFinalEntities = filterByTagState(entities, { globalDisplayFilters: true });
16371637 entities = entitiesFilter.applyFilters(beforeFinalEntities, { clearFuzzySearchCaches: false });
16381638
16391639 // Magic for folder filter. If that one is enabled, and no folders are display anymore, we remove that filter to actually show the characters.
16401640 if (isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED) && entities.filter(x => x.type == 'tag').length == 0) {
16411641 entities = entitiesFilter.applyFilters(beforeFinalEntities, { tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED }, clearFuzzySearchCaches: false });
16421642 }
16431643 }
16441644
@@ -1654,6 +1654,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
16541654 if (doSort) {
16551655 sortEntitiesList(entities);
16561656 }
1657+ entitiesFilter.clearFuzzySearchCaches();
16571658 return entities;
16581659}
16591660
public/scripts/filters.js+45 -6
@@ -56,6 +56,19 @@ export function isFilterState(a, b) {
5656}
5757
5858/**
59+ * The fuzzy search categories
60+ * @type {{ characters: string, worldInfo: string, personas: string, tags: string, groups: string }}
61+ */
62+export const fuzzySearchCategories = Object.freeze({
63+ characters: 'characters',
64+ worldInfo: 'worldInfo',
65+ personas: 'personas',
66+ tags: 'tags',
67+ groups: 'groups',
68+});
69+
70+
71+/**
5972 * Helper class for filtering data.
6073 * @example
6174 * const filterHelper = new FilterHelper(() => console.log('data changed'));
@@ -73,12 +86,25 @@ export class FilterHelper {
7386 scoreCache;
7487
7588 /**
89+ * Cache for fuzzy search results per category.
90+ * @type {Object.<string, { resultMap: Map<string, any> }>}
91+ */
92+ fuzzySearchCaches;
93+
94+ /**
7695 * Creates a new FilterHelper
7796 * @param {Function} onDataChanged Callback to trigger when the filter data changes
7897 */
7998 constructor(onDataChanged) {
8099 this.onDataChanged = onDataChanged;
81100 this.scoreCache = new Map();
101+ this.fuzzySearchCaches = {
102+ [fuzzySearchCategories.characters]: { resultMap: new Map() },
103+ [fuzzySearchCategories.worldInfo]: { resultMap: new Map() },
104+ [fuzzySearchCategories.personas]: { resultMap: new Map() },
105+ [fuzzySearchCategories.tags]: { resultMap: new Map() },
106+ [fuzzySearchCategories.groups]: { resultMap: new Map() },
107+ };
82108 }
83109
84110 /**
@@ -151,7 +177,7 @@ export class FilterHelper {
151177 return data;
152178 }
153179
154180 const fuzzySearchResults = fuzzySearchWorldInfo(data, term, this.fuzzySearchCaches);
155181 this.cacheScores(FILTER_TYPES.WORLD_INFO_SEARCH, new Map(fuzzySearchResults.map(i => [i.item?.uid, i.score])));
156182
157183 const filteredData = data.filter(entity => fuzzySearchResults.find(x => x.item === entity));
@@ -170,7 +196,7 @@ export class FilterHelper {
170196 return data;
171197 }
172198
173199 const fuzzySearchResults = fuzzySearchPersonas(data, term, this.fuzzySearchCaches);
174200 this.cacheScores(FILTER_TYPES.PERSONA_SEARCH, new Map(fuzzySearchResults.map(i => [i.item.key, i.score])));
175201
176202 const filteredData = data.filter(name => fuzzySearchResults.find(x => x.item.key === name));
@@ -289,9 +315,9 @@ export class FilterHelper {
289315
290316 // Save fuzzy search results and scores if enabled
291317 if (power_user.fuzzy_search) {
292318 const fuzzySearchCharactersResults = fuzzySearchCharacters(searchValue, this.fuzzySearchCaches);
293319 const fuzzySearchGroupsResults = fuzzySearchGroups(searchValue, this.fuzzySearchCaches);
294320 const fuzzySearchTagsResult = fuzzySearchTags(searchValue, this.fuzzySearchCaches);
295321 this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchCharactersResults.map(i => [`character.${i.refIndex}`, i.score])));
296322 this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchGroupsResults.map(i => [`group.${i.item.id}`, i.score])));
297323 this.cacheScores(FILTER_TYPES.SEARCH, new Map(fuzzySearchTagsResult.map(i => [`tag.${i.item.id}`, i.score])));
@@ -343,11 +369,14 @@ export class FilterHelper {
343369 * @param {object} options - Optional call parameters
344370 * @param {boolean} [options.clearScoreCache=true] - Whether the score cache should be cleared.
345371 * @param {Object.<FilterType, any>} [options.tempOverrides={}] - Temporarily override specific filters for this filter application
372+ * @param {boolean} [options.clearFuzzySearchCaches=true] - Whether the fuzzy search caches should be cleared.
346373 * @returns {any[]} The filtered data.
347374 */
348375 applyFilters(data, { clearScoreCache = true, tempOverrides = {}, clearFuzzySearchCaches = true } = {}) {
349376 if (clearScoreCache) this.clearScoreCache();
350377
378+ if (clearFuzzySearchCaches) this.clearFuzzySearchCaches();
379+
351380 // Save original filter states
352381 const originalStates = {};
353382 for (const key in tempOverrides) {
@@ -411,4 +440,14 @@ export class FilterHelper {
411440 this.scoreCache = new Map();
412441 }
413442 }
443+
444+ /**
445+ * Clears fuzzy search caches
446+ */
447+ clearFuzzySearchCaches() {
448+ for (const cache of Object.values(this.fuzzySearchCaches)) {
449+ cache.resultMap.clear();
450+ }
451+ console.log('All fuzzy search caches cleared');
452+ }
414453}
public/scripts/power-user.js+69 -66
@@ -53,6 +53,7 @@ import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandE
5353import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
5454import { POPUP_TYPE, callGenericPopup } from './popup.js';
5555import { loadSystemPrompts } from './sysprompt.js';
56+import { fuzzySearchCategories } from './filters.js';
5657
5758export {
5859 loadPowerUserSettings,
@@ -1829,15 +1830,51 @@ async function loadContextSettings() {
18291830 });
18301831}
18311832
1833+
18321834/**
18331835 * FuzzyCommon searchfunction charactersto byperform afuzzy search termwith optional caching
1836+ * @param {string} type - Type of search from fuzzySearchCategories
1837+ * @param {any[]} data - Data array to search in
1838+ * @param {Array<{name: string, weight: number, getFn?: Function}>} keys - Fuse.js keys configuration
18341839 * @param {string} searchValue - The search term
1840+ * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
18351841 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
18361842 */
1837-export function fuzzySearchCharacters(searchValue) {
1843+function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
1844+ // Check cache if provided
1845+ if (fuzzySearchCaches) {
1846+ const cache = fuzzySearchCaches[type];
1847+ if (cache?.resultMap.has(searchValue)) {
1848+ return cache.resultMap.get(searchValue);
1849+ }
1850+ }
1851+
18381852 // @ts-ignore
18391853 const fuse = new Fuse(charactersdata, {
18401854 keys: [keys,
1855+ includeScore: true,
1856+ ignoreLocation: true,
1857+ useExtendedSearch: true,
1858+ threshold: 0.2,
1859+ });
1860+
1861+ const results = fuse.search(searchValue);
1862+
1863+ // Store in cache if provided
1864+ if (fuzzySearchCaches) {
1865+ fuzzySearchCaches[type].resultMap.set(searchValue, results);
1866+ }
1867+ return results;
1868+}
1869+
1870+/**
1871+ * Fuzzy search characters by a search term
1872+ * @param {string} searchValue - The search term
1873+ * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
1874+ * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
1875+ */
1876+export function fuzzySearchCharacters(searchValue, fuzzySearchCaches = null) {
1877+ const keys = [
18411878 { name: 'data.name', weight: 20 },
18421879 { name: '#tags', weight: 10, getFn: (character) => getTagsList(character.avatar).map(x => x.name).join('||') },
18431880 { name: 'data.description', weight: 3 },
@@ -1849,28 +1886,20 @@ export function fuzzySearchCharacters(searchValue) {
18491886 { name: 'data.creator', weight: 1 },
18501887 { name: 'data.tags', weight: 1 },
18511888 { name: 'data.alternate_greetings', weight: 1 },
1852- ],
1889+ ];
1853- includeScore: true,
1854- ignoreLocation: true,
1855- useExtendedSearch: true,
1856- threshold: 0.2,
1857- });
18581890
1859- const results = fuse.search(searchValue);
1891+ return performFuzzySearch(fuzzySearchCategories.characters, characters, keys, searchValue, fuzzySearchCaches);
1860- console.debug('Characters fuzzy search results for ' + searchValue, results);
1861- return results;
18621892}
18631893
18641894/**
18651895 * Fuzzy search world info entries by a search term
18661896 * @param {*[]} data - WI items data array
18671897 * @param {string} searchValue - The search term
1898+ * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
18681899 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
18691900 */
18701901export function fuzzySearchWorldInfo(data, searchValue, fuzzySearchCaches = null) {
1871- // @ts-ignore
1902+ const keys = [
1872- const fuse = new Fuse(data, {
1873- keys: [
18741903 { name: 'key', weight: 20 },
18751904 { name: 'group', weight: 15 },
18761905 { name: 'comment', weight: 10 },
@@ -1878,88 +1907,62 @@ export function fuzzySearchWorldInfo(data, searchValue) {
18781907 { name: 'content', weight: 3 },
18791908 { name: 'uid', weight: 1 },
18801909 { name: 'automationId', weight: 1 },
1881- ],
1910+ ];
1882- includeScore: true,
1883- ignoreLocation: true,
1884- useExtendedSearch: true,
1885- threshold: 0.2,
1886- });
18871911
1888- const results = fuse.search(searchValue);
1912+ return performFuzzySearch(fuzzySearchCategories.worldInfo, data, keys, searchValue, fuzzySearchCaches);
1889- console.debug('World Info fuzzy search results for ' + searchValue, results);
1890- return results;
18911913}
18921914
18931915/**
18941916 * Fuzzy search persona entries by a search term
18951917 * @param {*[]} data - persona data array
18961918 * @param {string} searchValue - The search term
1919+ * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
18971920 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
18981921 */
18991922export function fuzzySearchPersonas(data, searchValue, fuzzySearchCaches = null) {
1900- data = data.map(x => ({ key: x, name: power_user.personas[x] ?? '', description: power_user.persona_descriptions[x]?.description ?? '' }));
1923+ const mappedData = data.map(x => ({
1901- // @ts-ignore
1924+ key: x,
1902- const fuse = new Fuse(data, {
1925+ name: power_user.personas[x] ?? '',
1903- keys: [
1926+ description: power_user.persona_descriptions[x]?.description ?? ''
1927+ }));
1928+
1929+ const keys = [
19041930 { name: 'name', weight: 20 },
19051931 { name: 'description', weight: 3 },
1906- ],
1932+ ];
1907- includeScore: true,
1908- ignoreLocation: true,
1909- useExtendedSearch: true,
1910- threshold: 0.2,
1911- });
19121933
1913- const results = fuse.search(searchValue);
1934+ return performFuzzySearch(fuzzySearchCategories.personas, mappedData, keys, searchValue, fuzzySearchCaches);
1914- console.debug('Personas fuzzy search results for ' + searchValue, results);
1915- return results;
19161935}
19171936
19181937/**
19191938 * Fuzzy search tags by a search term
19201939 * @param {string} searchValue - The search term
1940+ * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
19211941 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
19221942 */
19231943export function fuzzySearchTags(searchValue, fuzzySearchCaches = null) {
1924- // @ts-ignore
1944+ const keys = [
1925- const fuse = new Fuse(tags, {
1926- keys: [
19271945 { name: 'name', weight: 1 },
1928- ],
1946+ ];
1929- includeScore: true,
1930- ignoreLocation: true,
1931- useExtendedSearch: true,
1932- threshold: 0.2,
1933- });
19341947
1935- const results = fuse.search(searchValue);
1948+ return performFuzzySearch(fuzzySearchCategories.tags, tags, keys, searchValue, fuzzySearchCaches);
1936- console.debug('Tags fuzzy search results for ' + searchValue, results);
1937- return results;
19381949}
19391950
19401951/**
19411952 * Fuzzy search groups by a search term
19421953 * @param {string} searchValue - The search term
1954+ * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
19431955 * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
19441956 */
19451957export function fuzzySearchGroups(searchValue, fuzzySearchCaches = null) {
1946- // @ts-ignore
1958+ const keys = [
1947- const fuse = new Fuse(groups, {
1948- keys: [
19491959 { name: 'name', weight: 20 },
19501960 { name: 'members', weight: 15 },
19511961 { name: '#tags', weight: 10, getFn: (group) => getTagsList(group.id).map(x => x.name).join('||') },
19521962 { name: 'id', weight: 1 },
1953- ],
1963+ ];
1954- includeScore: true,
1955- ignoreLocation: true,
1956- useExtendedSearch: true,
1957- threshold: 0.2,
1958- });
19591964
1960- const results = fuse.search(searchValue);
1965+ return performFuzzySearch(fuzzySearchCategories.groups, groups, keys, searchValue, fuzzySearchCaches);
1961- console.debug('Groups fuzzy search results for ' + searchValue, results);
1962- return results;
19631966}
19641967
19651968/**