From 79b4390911dfe0d6114c00c62510ea5e6c9497c7 Mon Sep 17 00:00:00 2001
From: Vyacheslav Tumanov <me@slavatumanov.me>
Date: Sun, 10 Mar 2024 20:45:56 +0500
Subject: [PATCH] 5 results per class in search (#4914)

Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>
---
 packages/core/src/storage.ts        |  1 +
 packages/presentation/src/search.ts | 36 +++++++++++++++++++----------
 server/core/src/mapper.ts           |  3 ++-
 3 files changed, 27 insertions(+), 13 deletions(-)

diff --git a/packages/core/src/storage.ts b/packages/core/src/storage.ts
index 98cfdab27d..df77cc2cbb 100644
--- a/packages/core/src/storage.ts
+++ b/packages/core/src/storage.ts
@@ -230,6 +230,7 @@ export interface SearchResultDoc {
   iconProps?: Record<string, string>
   shortTitle?: string
   title?: string
+  score?: number
   doc: Pick<Doc, '_id' | '_class'>
 }
 
diff --git a/packages/presentation/src/search.ts b/packages/presentation/src/search.ts
index 269f0a5847..fe62e690ee 100644
--- a/packages/presentation/src/search.ts
+++ b/packages/presentation/src/search.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 //
 
-import type { Class, Ref, Doc, SearchResultDoc, TxOperations } from '@hcengineering/core'
+import type { Class, Ref, Doc, SearchResultDoc, TxOperations, SearchResult } from '@hcengineering/core'
 import { type ObjectSearchCategory } from './types'
 import plugin from './plugin'
 import { getClient } from './utils'
@@ -67,18 +67,27 @@ async function doFulltextSearch (
   query: string,
   categories: ObjectSearchCategory[]
 ): Promise<SearchSection[]> {
-  const result = await client.searchFulltext(
-    {
-      query: `${query}*`,
-      classes
-    },
-    {
-      limit: 10
+  let result: SearchResult | undefined
+  for (const cl of classes) {
+    const r = await client.searchFulltext(
+      {
+        query: `${query}*`,
+        classes: [cl]
+      },
+      {
+        limit: 5
+      }
+    )
+    if (result === undefined) {
+      result = r
+    } else {
+      result.docs.push(...r.docs)
+      result.total = (result?.total ?? 0) + (r.total ?? 0)
     }
-  )
+  }
 
   const itemsByClass = new Map<Ref<Class<Doc>>, SearchResultDoc[]>()
-  for (const item of result.docs) {
+  for (const item of result?.docs ?? []) {
     const list = itemsByClass.get(item.doc._class)
     if (list === undefined) {
       itemsByClass.set(item.doc._class, [item])
@@ -94,8 +103,11 @@ async function doFulltextSearch (
       sections.push({ category, items })
     }
   }
-
-  return sections
+  return sections.sort((a, b) => {
+    const maxScoreA = Math.max(...(a?.items ?? []).map((obj) => obj?.score ?? 0))
+    const maxScoreB = Math.max(...(b?.items ?? []).map((obj) => obj?.score ?? 0))
+    return maxScoreB - maxScoreA
+  })
 }
 
 const categoriesByContext = new Map<string, ObjectSearchCategory[]>()
diff --git a/server/core/src/mapper.ts b/server/core/src/mapper.ts
index 9f26cd7fd8..7b38122816 100644
--- a/server/core/src/mapper.ts
+++ b/server/core/src/mapper.ts
@@ -172,7 +172,8 @@ export function mapSearchResultDoc (hierarchy: Hierarchy, raw: IndexedDoc): Sear
     doc: {
       _id: raw.id,
       _class: raw._class
-    }
+    },
+    score: raw._score
   }
 
   const searchPresenter = findSearchPresenter(hierarchy, doc.doc._class)