diff --git a/src/backend/model/database/SearchManager.ts b/src/backend/model/database/SearchManager.ts index b4e87388..217ec6f6 100644 --- a/src/backend/model/database/SearchManager.ts +++ b/src/backend/model/database/SearchManager.ts @@ -85,7 +85,7 @@ export class SearchManager { .where('photo.metadata.keywords LIKE :text COLLATE ' + SQL_COLLATE, { text: '%' + text + '%', }) - .limit(Config.Search.AutoComplete.ItemsPerCategory.keyword*2) + .limit(Config.Search.AutoComplete.ItemsPerCategory.keyword) .getRawMany() ) .map( @@ -120,7 +120,7 @@ export class SearchManager { text: '%' + text + '%', }) .limit( - Config.Search.AutoComplete.ItemsPerCategory.person*2 + Config.Search.AutoComplete.ItemsPerCategory.person ) .orderBy('person.count', 'DESC') .getRawMany() @@ -161,7 +161,7 @@ export class SearchManager { .groupBy( 'photo.metadata.positionData.country, photo.metadata.positionData.state, photo.metadata.positionData.city' ) - .limit(Config.Search.AutoComplete.ItemsPerCategory.position*2) + .limit(Config.Search.AutoComplete.ItemsPerCategory.position ) .getRawMany() ) .filter((pm): boolean => !!pm) @@ -199,7 +199,7 @@ export class SearchManager { text: '%' + text + '%', }) .limit( - Config.Search.AutoComplete.ItemsPerCategory.file_name*2 + Config.Search.AutoComplete.ItemsPerCategory.fileName ) .getRawMany() ).map((r) => r.name), @@ -223,7 +223,7 @@ export class SearchManager { {text: '%' + text + '%'} ) .limit( - Config.Search.AutoComplete.ItemsPerCategory.caption*2 + Config.Search.AutoComplete.ItemsPerCategory.caption ) .getRawMany() ).map((r) => r.caption), @@ -246,7 +246,7 @@ export class SearchManager { text: '%' + text + '%', }) .limit( - Config.Search.AutoComplete.ItemsPerCategory.directory*2 + Config.Search.AutoComplete.ItemsPerCategory.directory ) .getRawMany() ).map((r) => r.name), @@ -255,22 +255,23 @@ export class SearchManager { ); } - let result: AutoCompleteItem[]; + const result: AutoCompleteItem[] = []; - // if not enough items are available, load more from one category - if ( - [].concat(...partialResult).length < - Config.Search.AutoComplete.ItemsPerCategory.maxItems - ) { - result = [].concat(...partialResult); - } else { - result = [].concat( - ...partialResult.map((l) => - l.slice(0, (Config.Search.AutoComplete.ItemsPerCategory as any)[SearchQueryTypes[l[0].type]]) - ) - ); + while (result.length < Config.Search.AutoComplete.ItemsPerCategory.maxItems) { + let adding = false; + for (let i = 0; i < partialResult.length; ++i) { + if (partialResult[i].length <= 0) { + continue; + } + result.push(partialResult[i].pop()); + adding = true; + } + if (!adding) { + break; + } } + return SearchManager.autoCompleteItemsUnique(result); } @@ -720,7 +721,7 @@ export class SearchManager { const relationBottom = tq.negate ? '<=' : '>'; switch (tq.frequency) { case DatePatternFrequency.every_year: - if(tq.daysLength >= 365){ + if (tq.daysLength >= 365) { return q; } q.where( @@ -729,7 +730,7 @@ export class SearchManager { textParam); break; case DatePatternFrequency.every_month: - if(tq.daysLength >=31){ + if (tq.daysLength >= 31) { return q; } q.where( @@ -738,7 +739,7 @@ export class SearchManager { textParam); break; case DatePatternFrequency.every_week: - if(tq.daysLength >= 7){ + if (tq.daysLength >= 7) { return q; } q.where( diff --git a/src/common/config/public/ClientConfig.ts b/src/common/config/public/ClientConfig.ts index 78826f16..963db026 100644 --- a/src/common/config/public/ClientConfig.ts +++ b/src/common/config/public/ClientConfig.ts @@ -80,7 +80,7 @@ export class AutoCompleteItemsPerCategoryConfig { }, description: $localize`Maximum number autocomplete items shown per photo category.` }) - file_name: number = 2; + fileName: number = 2; @ConfigProperty({ type: 'unsignedInt', diff --git a/src/frontend/app/ui/gallery/search/autocomplete.service.ts b/src/frontend/app/ui/gallery/search/autocomplete.service.ts index 2dcd8104..8f5316d4 100644 --- a/src/frontend/app/ui/gallery/search/autocomplete.service.ts +++ b/src/frontend/app/ui/gallery/search/autocomplete.service.ts @@ -244,27 +244,43 @@ export class AutoCompleteService { items: RenderableAutoCompleteItem[] ): RenderableAutoCompleteItem[] { const textLC = text.toLowerCase(); + // Source: https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex + const isStartRgx = new RegExp('(\\s|^)' + textLC.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); return items.sort((a, b) => { const aLC = a.text.toLowerCase(); const bLC = b.text.toLowerCase(); - // prioritize persons higher - if (a.type !== b.type) { - if (a.type === SearchQueryTypes.person) { - return -1; - } else if (b.type === SearchQueryTypes.person) { - return 1; - } - } - if ( - (aLC.startsWith(textLC) && bLC.startsWith(textLC)) || - (!aLC.startsWith(textLC) && !bLC.startsWith(textLC)) - ) { + const basicCompare = () => { + // prioritize persons higher + if (a.type !== b.type) { + if (a.type === SearchQueryTypes.person) { + return -1; + } else if (b.type === SearchQueryTypes.person) { + return 1; + } + } return aLC.localeCompare(bLC); + }; + + // both starts with the searched string + if (aLC.startsWith(textLC) && bLC.startsWith(textLC)) { + return basicCompare(); + + // none starts with the searched string + } else if (!aLC.startsWith(textLC) && !bLC.startsWith(textLC)) { + + if ((isStartRgx.test(aLC) && isStartRgx.test(bLC)) || + (!isStartRgx.test(aLC) && !isStartRgx.test(bLC))) { + return basicCompare(); + } else if (isStartRgx.test(aLC)) { + return -1; + } + return 1; + // one of them starts with the searched string } else if (aLC.startsWith(textLC)) { return -1; } - return 1; + return basicCompare(); }); }