Skip to content

Commit

Permalink
add order algorithm in search result
Browse files Browse the repository at this point in the history
  • Loading branch information
ajbura committed Feb 19, 2025
1 parent 5844209 commit f73dc05
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ export function EmoticonAutocomplete({
}, [imagePacks]);

const [result, search, resetSearch] = useAsyncSearch(searchList, getEmoticonStr, SEARCH_OPTIONS);
const autoCompleteEmoticon = (result ? result.items : recentEmoji).sort((a, b) =>
a.shortcode.localeCompare(b.shortcode)
);
const autoCompleteEmoticon = result ? result.items : recentEmoji;

useEffect(() => {
if (query.text) search(query.text);
Expand Down
56 changes: 27 additions & 29 deletions src/app/components/emoji-board/EmojiBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -471,36 +471,34 @@ export function SearchEmojiGroup({
return (
<EmojiGroup key={id} id={id} label={label}>
{tab === EmojiBoardTab.Emoji
? searchResult
.sort((a, b) => a.shortcode.localeCompare(b.shortcode))
.map((emoji) =>
'unicode' in emoji ? (
<EmojiItem
key={emoji.unicode}
label={emoji.label}
type={EmojiType.Emoji}
data={emoji.unicode}
shortcode={emoji.shortcode}
>
{emoji.unicode}
</EmojiItem>
) : (
<EmojiItem
key={emoji.shortcode}
label={emoji.body || emoji.shortcode}
type={EmojiType.CustomEmoji}
data={emoji.url}
shortcode={emoji.shortcode}
>
<img
loading="lazy"
className={css.CustomEmojiImg}
alt={emoji.body || emoji.shortcode}
src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
/>
</EmojiItem>
)
? searchResult.map((emoji) =>
'unicode' in emoji ? (
<EmojiItem
key={emoji.unicode}
label={emoji.label}
type={EmojiType.Emoji}
data={emoji.unicode}
shortcode={emoji.shortcode}
>
{emoji.unicode}
</EmojiItem>
) : (
<EmojiItem
key={emoji.shortcode}
label={emoji.body || emoji.shortcode}
type={EmojiType.CustomEmoji}
data={emoji.url}
shortcode={emoji.shortcode}
>
<img
loading="lazy"
className={css.CustomEmojiImg}
alt={emoji.body || emoji.shortcode}
src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
/>
</EmojiItem>
)
)
: searchResult.map((emoji) =>
'unicode' in emoji ? null : (
<StickerItem
Expand Down
89 changes: 79 additions & 10 deletions src/app/hooks/useAsyncSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,81 @@ export type UseAsyncSearchResult<TSearchItem extends object | string | number> =

export type SearchResetHandler = () => void;

const performMatch = (
target: string | string[],
query: string,
options?: UseAsyncSearchOptions
): string | undefined => {
if (Array.isArray(target)) {
const matchTarget = target.find((i) =>
matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions)
);
return matchTarget ? normalize(matchTarget, options?.normalizeOptions) : undefined;
}

const normalizedTargetStr = normalize(target, options?.normalizeOptions);
const matches = matchQuery(normalizedTargetStr, query, options?.matchOptions);
return matches ? normalizedTargetStr : undefined;
};

export const orderSearchItems = <TSearchItem extends object | string | number>(
query: string,
items: TSearchItem[],
getItemStr: SearchItemStrGetter<TSearchItem>,
options?: UseAsyncSearchOptions
): TSearchItem[] => {
const orderedItems: TSearchItem[] = Array.from(items);

// we will consider "_" as word boundary char.
// because in more use-cases it is used. (like: emojishortcode)
const boundaryRegex = new RegExp(`(\\b|_)${query}`);
const perfectBoundaryRegex = new RegExp(`(\\b|_)${query}(\\b|_)`);

orderedItems.sort((i1, i2) => {
const str1 = performMatch(getItemStr(i1, query), query, options);
const str2 = performMatch(getItemStr(i2, query), query, options);

if (str1 === undefined && str2 === undefined) return 0;
if (str1 === undefined) return 1;
if (str2 === undefined) return -1;

let points1 = 0;
let points2 = 0;

// short string should score more
const pointsToSmallStr = (points: number) => {
if (str1.length < str2.length) points1 += points;
else if (str2.length < str1.length) points2 += points;
};
pointsToSmallStr(1);

// closes query match should score more
const indexIn1 = str1.indexOf(query);
const indexIn2 = str2.indexOf(query);
if (indexIn1 < indexIn2) points1 += 2;
else if (indexIn2 < indexIn1) points2 += 2;
else pointsToSmallStr(2);

// query match word start on boundary should score more
const boundaryIn1 = str1.match(boundaryRegex);
const boundaryIn2 = str2.match(boundaryRegex);
if (boundaryIn1 && boundaryIn2) pointsToSmallStr(4);
else if (boundaryIn1) points1 += 4;
else if (boundaryIn2) points2 += 4;

// query match word start and end on boundary should score more
const perfectBoundaryIn1 = str1.match(perfectBoundaryRegex);
const perfectBoundaryIn2 = str2.match(perfectBoundaryRegex);
if (perfectBoundaryIn1 && perfectBoundaryIn2) pointsToSmallStr(8);
else if (perfectBoundaryIn1) points1 += 8;
else if (perfectBoundaryIn2) points2 += 8;

return points2 - points1;
});

return orderedItems;
};

export const useAsyncSearch = <TSearchItem extends object | string | number>(
list: TSearchItem[],
getItemStr: SearchItemStrGetter<TSearchItem>,
Expand All @@ -40,21 +115,15 @@ export const useAsyncSearch = <TSearchItem extends object | string | number>(

const handleMatch: MatchHandler<TSearchItem> = (item, query) => {
const itemStr = getItemStr(item, query);
if (Array.isArray(itemStr))
return !!itemStr.find((i) =>
matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions)
);
return matchQuery(
normalize(itemStr, options?.normalizeOptions),
query,
options?.matchOptions
);

const strWithMatch = performMatch(itemStr, query, options);
return typeof strWithMatch === 'string';
};

const handleResult: ResultHandler<TSearchItem> = (results, query) =>
setResult({
query,
items: [...results],
items: orderSearchItems(query, results, getItemStr, options),
});

return AsyncSearch(list, handleMatch, handleResult, options);
Expand Down

0 comments on commit f73dc05

Please sign in to comment.