mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-22 22:49:45 +01:00
core: reuse fuzzy search logic
This commit is contained in:
@@ -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<T> = {
|
||||
sorted: (limit?: number) => Promise<VirtualizedGrouping<T>>;
|
||||
@@ -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<keyof T, number>,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
private toSearchResults<T extends Item>(
|
||||
|
||||
@@ -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" }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,38 +18,58 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { match, surround } from "fuzzyjs";
|
||||
import { clone } from "./clone";
|
||||
|
||||
export function fuzzy<T>(
|
||||
export function fuzzy<T extends { id: string }>(
|
||||
query: string,
|
||||
items: T[],
|
||||
key: keyof T,
|
||||
opts?: {
|
||||
fields: Partial<Record<keyof T, number>>,
|
||||
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<keyof T, string>];
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user