add order algorithm in search result
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Wed, 19 Feb 2025 05:53:32 +0000 (11:23 +0530)
committerAjay Bura <32841439+ajbura@users.noreply.github.com>
Wed, 19 Feb 2025 05:53:32 +0000 (11:23 +0530)
src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
src/app/components/emoji-board/EmojiBoard.tsx
src/app/hooks/useAsyncSearch.ts

index 591f1bffe08bb6152b98c523be8d21cd29c8d2ea..9479a69822fb915d0e7059703d5235fd350e1f35 100644 (file)
@@ -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);
index f3bd551f5f98256946c32e827001aa42fc95e7ed..77e56a91282d65d2477b3a0d4d545ebd41aff7a4 100644 (file)
@@ -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
index 719e0d5618aae42a3b5c9cbbd08fb3fc81240c00..3fe7ee58901788ca7cec4c0b076da85ed2c2fe6d 100644 (file)
@@ -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>,
@@ -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);