diff --git a/packages/core/src/api/lookup.ts b/packages/core/src/api/lookup.ts index c3b847c33..af96abdaa 100644 --- a/packages/core/src/api/lookup.ts +++ b/packages/core/src/api/lookup.ts @@ -35,6 +35,7 @@ import { logger } from "../logger.js"; import { rebuildSearchIndex } from "../database/fts.js"; import { transformQuery } from "../utils/query-transformer.js"; import { getSortSelectors } from "../utils/grouping.js"; +import { fuzzy } from "../utils/fuzzy.js"; type SearchResults = { sorted: (limit?: number) => Promise>; @@ -210,63 +211,17 @@ export default class Lookup { suffix?: string; } = {} ) { - const results: Map< - string, - { - item: T; - score: number; - } - > = new Map(); const columns = fields.map((f) => f.column); const items = await selector.fields(columns).items(); - for (const item of items) { - if (options.limit && results.size >= options.limit) break; - - for (const field of fields) { - if (field.ignore) continue; - - const result = match(query, `${item[field.name]}`); - if (!result.match) continue; - - const oldMatch = results.get(item.id); - if (options.suffix && options.prefix) { - item[field.name] = surround(`${item[field.name]}`, { - suffix: options.suffix, - prefix: options.prefix, - result - }) as T[keyof T]; - } - if (oldMatch) { - oldMatch.score += result.score * (field.weight || 1); - } else { - results.set(item.id, { - item, - score: result.score * (field.weight || 1) - }); - } - } - } - selector.fields([]); - - if (results.size === 0) return []; - - const sorted = Array.from(results.entries()); - - if (!options.sortOptions) - // || sortOptions.sortBy === "relevance") - sorted.sort( - // sortOptions?.sortDirection === "desc" - // ? (a, b) => a[1] - b[1] - // : - (a, b) => b[1].score - a[1].score - ); - - return sorted.map((item) => ({ - id: item[0], - score: item[1].score, - item: item[1].item - })); + return fuzzy( + query, + items, + Object.fromEntries( + fields.filter((f) => !f.ignore).map((f) => [f.name, f.weight || 1]) + ) as Record, + options + ); } private toSearchResults( diff --git a/packages/core/src/utils/__tests__/fuzzy.test.ts b/packages/core/src/utils/__tests__/fuzzy.test.ts index 569a4e780..bf7a6470f 100644 --- a/packages/core/src/utils/__tests__/fuzzy.test.ts +++ b/packages/core/src/utils/__tests__/fuzzy.test.ts @@ -24,52 +24,69 @@ describe("lookup.fuzzy", () => { test("should sort items by score", () => { const items = [ { + id: "1", title: "system" }, { + id: "2", title: "hello" }, { + id: "3", title: "items" } ]; const query = "ems"; - expect(fuzzy(query, items, "title")).toStrictEqual([items[2]]); + expect(fuzzy(query, items, { title: 1 })).toStrictEqual([items[2]]); }); describe("opts.prefix", () => { test("should prefix matched field with provided value when given", () => { const items = [ { + id: "1", title: "hello" }, { + id: "2", title: "world" } ]; const query = "d"; expect( - fuzzy(query, items, "title", { - prefix: "prefix-" - }) - ).toStrictEqual([{ title: "worlprefix-d" }]); + fuzzy( + query, + items, + { title: 1 }, + { + prefix: "prefix-" + } + ) + ).toStrictEqual([{ id: "2", title: "worlprefix-d" }]); }); }); describe("opt.suffix", () => { test("should suffix matched field with provided value when given", () => { const items = [ { + id: "1", title: "hello" }, { + id: "2", title: "world" } ]; const query = "llo"; expect( - fuzzy(query, items, "title", { - suffix: "-suffix" - }) - ).toStrictEqual([{ title: "hello-suffix" }]); + fuzzy( + query, + items, + { title: 1 }, + { + suffix: "-suffix" + } + ) + ).toStrictEqual([{ id: "1", title: "hello-suffix" }]); }); }); }); diff --git a/packages/core/src/utils/fuzzy.ts b/packages/core/src/utils/fuzzy.ts index 89c4e78f8..075f4228f 100644 --- a/packages/core/src/utils/fuzzy.ts +++ b/packages/core/src/utils/fuzzy.ts @@ -18,38 +18,58 @@ along with this program. If not, see . */ import { match, surround } from "fuzzyjs"; +import { clone } from "./clone"; -export function fuzzy( +export function fuzzy( query: string, items: T[], - key: keyof T, - opts?: { + fields: Partial>, + options: { + limit?: number; prefix?: string; suffix?: string; - } + } = {} ): T[] { - if (query === "") return items; - - const fuzzied: [T, number][] = []; + const results: Map< + string, + { + item: T; + score: number; + } + > = new Map(); for (const item of items) { - const result = match(query, `${item[key]}`); - if (!result.match) continue; + if (options.limit && results.size >= options.limit) break; - if (opts?.prefix || opts?.suffix) { - fuzzied.push([ - { - ...item, - [key]: surround(`${item[key]}`, { - result: result, - prefix: opts?.prefix, - suffix: opts?.suffix - }) - }, - result.score - ]); - } else fuzzied.push([item, result.score]); + for (const field in fields) { + const result = match(query, `${item[field]}`); + if (!result.match) continue; + + const oldMatch = results.get(item.id); + const clonedItem = oldMatch?.item || clone(item); + + if (options.suffix || options.prefix) { + clonedItem[field] = surround(`${clonedItem[field]}`, { + suffix: options.suffix, + prefix: options.prefix, + result + }) as T[Extract]; + } + if (oldMatch) { + oldMatch.score += result.score * (fields[field] || 1); + } else { + results.set(item.id, { + item: clonedItem, + score: result.score * (fields[field] || 1) + }); + } + } } - return fuzzied.sort((a, b) => b[1] - a[1]).map((f) => f[0]); + if (results.size === 0) return []; + + const sorted = Array.from(results.entries()); + sorted.sort((a, b) => b[1].score - a[1].score); + + return sorted.map((item) => item[1].item); }