mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
* Accept gallery ids in findGalleries * Add gallery select component * Add and fix image gallery filter * Show gallery path as alias
345 lines
7.8 KiB
TypeScript
345 lines
7.8 KiB
TypeScript
interface ISortable {
|
|
id: string;
|
|
}
|
|
|
|
// sortByRelevance is a function that sorts an array of objects by relevance to a query string.
|
|
// It uses the following priorities:
|
|
// 1. Exact matches
|
|
// 2. Starts with
|
|
// 3. Word matches
|
|
// 4. Word starts with
|
|
// 5. Includes
|
|
// If aliases are provided, they are also checked in the same order, but with lower priority than
|
|
// the name of the object.
|
|
export function sortByRelevance<T extends ISortable>(
|
|
query: string,
|
|
value: T[],
|
|
getName: (o: T) => string,
|
|
getAliases?: (o: T) => string[] | undefined
|
|
) {
|
|
if (!query) {
|
|
return value;
|
|
}
|
|
|
|
query = query.toLowerCase();
|
|
|
|
interface ICacheEntry {
|
|
aliases?: string[];
|
|
aliasMatch?: boolean;
|
|
aliasStartsWith?: boolean;
|
|
wordIndex?: number;
|
|
wordStartsWithIndex?: number;
|
|
aliasWordIndex?: number;
|
|
aliasWordStartsWithIndex?: number;
|
|
aliasIncludesIndex?: number;
|
|
}
|
|
|
|
const cache: Record<string, ICacheEntry> = {};
|
|
|
|
function setCache(tag: T, partial: Partial<ICacheEntry>) {
|
|
cache[tag.id] = {
|
|
...cache[tag.id],
|
|
...partial,
|
|
};
|
|
}
|
|
|
|
function getObjectAliases(o: T) {
|
|
const cached = cache[o.id]?.aliases;
|
|
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
if (!getAliases) {
|
|
return [];
|
|
}
|
|
|
|
const aliases = getAliases(o)?.map((a) => a.toLowerCase()) ?? [];
|
|
setCache(o, { aliases });
|
|
|
|
return aliases;
|
|
}
|
|
|
|
function aliasMatches(o: T) {
|
|
const cached = cache[o.id]?.aliasMatch;
|
|
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
const aliases = getObjectAliases(o);
|
|
const aliasMatch = aliases.some((a) => a === query);
|
|
setCache(o, { aliasMatch });
|
|
|
|
return aliasMatch;
|
|
}
|
|
|
|
function aliasStartsWith(o: T) {
|
|
const cached = cache[o.id]?.aliasStartsWith;
|
|
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
const aliases = getObjectAliases(o);
|
|
const startsWith = aliases.some((a) => a.startsWith(query));
|
|
setCache(o, { aliasStartsWith: startsWith });
|
|
|
|
return startsWith;
|
|
}
|
|
|
|
function getWords(o: T) {
|
|
return getName(o).toLowerCase().split(" ");
|
|
}
|
|
|
|
function getAliasWords(tag: T) {
|
|
const aliases = getObjectAliases(tag);
|
|
return aliases.map((a) => a.split(" ")).flat();
|
|
}
|
|
|
|
function getWordIndex(o: T) {
|
|
const cached = cache[o.id]?.wordIndex;
|
|
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
const words = getWords(o);
|
|
const wordIndex = words.findIndex((w) => w === query);
|
|
setCache(o, { wordIndex });
|
|
|
|
return wordIndex;
|
|
}
|
|
|
|
function getAliasWordIndex(o: T) {
|
|
const cached = cache[o.id]?.aliasWordIndex;
|
|
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
const aliasWords = getAliasWords(o);
|
|
const aliasWordIndex = aliasWords.findIndex((w) => w === query);
|
|
setCache(o, { aliasWordIndex });
|
|
|
|
return aliasWordIndex;
|
|
}
|
|
|
|
function getWordStartsWithIndex(o: T) {
|
|
const cached = cache[o.id]?.wordStartsWithIndex;
|
|
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
const words = getWords(o);
|
|
const wordStartsWithIndex = words.findIndex((w) => w.startsWith(query));
|
|
setCache(o, { wordStartsWithIndex });
|
|
|
|
return wordStartsWithIndex;
|
|
}
|
|
|
|
function getAliasWordStartsWithIndex(o: T) {
|
|
const cached = cache[o.id]?.aliasWordStartsWithIndex;
|
|
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
const aliasWords = getAliasWords(o);
|
|
const aliasWordStartsWithIndex = aliasWords.findIndex((w) =>
|
|
w.startsWith(query)
|
|
);
|
|
setCache(o, { aliasWordStartsWithIndex });
|
|
|
|
return aliasWordStartsWithIndex;
|
|
}
|
|
|
|
function getAliasIncludesIndex(o: T) {
|
|
const cached = cache[o.id]?.aliasIncludesIndex;
|
|
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
const aliases = getObjectAliases(o);
|
|
const aliasIncludesIndex = aliases.findIndex((a) => a.includes(query));
|
|
setCache(o, { aliasIncludesIndex });
|
|
|
|
return aliasIncludesIndex;
|
|
}
|
|
|
|
function compare(a: T, b: T) {
|
|
const aName = getName(a).toLowerCase();
|
|
const bName = getName(b).toLowerCase();
|
|
|
|
const aAlias = aliasMatches(a);
|
|
const bAlias = aliasMatches(b);
|
|
|
|
// exact matches first
|
|
if (aName === query && bName !== query) {
|
|
return -1;
|
|
}
|
|
|
|
if (aName !== query && bName === query) {
|
|
return 1;
|
|
}
|
|
|
|
if (aAlias && !bAlias) {
|
|
return -1;
|
|
}
|
|
|
|
if (!aAlias && bAlias) {
|
|
return 1;
|
|
}
|
|
|
|
// then starts with
|
|
if (aName.startsWith(query) && !bName.startsWith(query)) {
|
|
return -1;
|
|
}
|
|
|
|
if (!aName.startsWith(query) && bName.startsWith(query)) {
|
|
return 1;
|
|
}
|
|
|
|
const aAliasStartsWith = aliasStartsWith(a);
|
|
const bAliasStartsWith = aliasStartsWith(b);
|
|
|
|
if (aAliasStartsWith && !bAliasStartsWith) {
|
|
return -1;
|
|
}
|
|
|
|
if (!aAliasStartsWith && bAliasStartsWith) {
|
|
return 1;
|
|
}
|
|
|
|
// only check words if the query is a single word
|
|
if (!query.includes(" ")) {
|
|
// word matches
|
|
{
|
|
const aWord = getWordIndex(a);
|
|
const bWord = getWordIndex(b);
|
|
|
|
if (aWord !== -1 && bWord === -1) {
|
|
return -1;
|
|
}
|
|
|
|
if (aWord === -1 && bWord !== -1) {
|
|
return 1;
|
|
}
|
|
|
|
if (aWord !== -1 && bWord !== -1) {
|
|
if (aWord === bWord) {
|
|
return aName.localeCompare(bName);
|
|
}
|
|
|
|
return aWord - bWord;
|
|
}
|
|
|
|
const aAliasWord = getAliasWordIndex(a);
|
|
const bAliasWord = getAliasWordIndex(b);
|
|
|
|
if (aAliasWord !== -1 && bAliasWord === -1) {
|
|
return -1;
|
|
}
|
|
|
|
if (aAliasWord === -1 && bAliasWord !== -1) {
|
|
return 1;
|
|
}
|
|
|
|
if (aAliasWord !== -1 && bAliasWord !== -1) {
|
|
if (aAliasWord === bAliasWord) {
|
|
return aName.localeCompare(bName);
|
|
}
|
|
|
|
return aAliasWord - bAliasWord;
|
|
}
|
|
}
|
|
|
|
// then start of word
|
|
{
|
|
const aWord = getWordStartsWithIndex(a);
|
|
const bWord = getWordStartsWithIndex(b);
|
|
|
|
if (aWord !== -1 && bWord === -1) {
|
|
return -1;
|
|
}
|
|
|
|
if (aWord === -1 && bWord !== -1) {
|
|
return 1;
|
|
}
|
|
|
|
if (aWord !== -1 && bWord !== -1) {
|
|
if (aWord === bWord) {
|
|
return aName.localeCompare(bName);
|
|
}
|
|
|
|
return aWord - bWord;
|
|
}
|
|
|
|
const aAliasWord = getAliasWordStartsWithIndex(a);
|
|
const bAliasWord = getAliasWordStartsWithIndex(b);
|
|
|
|
if (aAliasWord !== -1 && bAliasWord === -1) {
|
|
return -1;
|
|
}
|
|
|
|
if (aAliasWord === -1 && bAliasWord !== -1) {
|
|
return 1;
|
|
}
|
|
|
|
if (aAliasWord !== -1 && bAliasWord !== -1) {
|
|
if (aAliasWord === bAliasWord) {
|
|
return aName.localeCompare(bName);
|
|
}
|
|
|
|
return aAliasWord - bAliasWord;
|
|
}
|
|
}
|
|
}
|
|
|
|
// then contains
|
|
// performance of this is presumably fast enough to not require caching
|
|
const aNameIncludeIndex = aName.indexOf(query);
|
|
const bNameIncludeIndex = bName.indexOf(query);
|
|
|
|
if (aNameIncludeIndex !== -1 && bNameIncludeIndex === -1) {
|
|
return -1;
|
|
}
|
|
|
|
if (aNameIncludeIndex === -1 && bNameIncludeIndex !== -1) {
|
|
return 1;
|
|
}
|
|
|
|
if (aNameIncludeIndex !== -1 && bNameIncludeIndex !== -1) {
|
|
if (aNameIncludeIndex === bNameIncludeIndex) {
|
|
return aName.localeCompare(bName);
|
|
}
|
|
|
|
return aNameIncludeIndex - bNameIncludeIndex;
|
|
}
|
|
|
|
const aAliasIncludes = getAliasIncludesIndex(a);
|
|
const bAliasIncludes = getAliasIncludesIndex(b);
|
|
|
|
if (aAliasIncludes !== -1 && bAliasIncludes === -1) {
|
|
return -1;
|
|
}
|
|
|
|
if (aAliasIncludes === -1 && bAliasIncludes !== -1) {
|
|
return 1;
|
|
}
|
|
|
|
if (aAliasIncludes !== -1 && bAliasIncludes !== -1) {
|
|
if (aAliasIncludes === bAliasIncludes) {
|
|
return aName.localeCompare(bName);
|
|
}
|
|
|
|
return aAliasIncludes - bAliasIncludes;
|
|
}
|
|
|
|
return aName.localeCompare(bName);
|
|
}
|
|
|
|
return value.slice().sort(compare);
|
|
}
|