mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 06:59:31 +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 { rebuildSearchIndex } from "../database/fts.js";
|
||||||
import { transformQuery } from "../utils/query-transformer.js";
|
import { transformQuery } from "../utils/query-transformer.js";
|
||||||
import { getSortSelectors } from "../utils/grouping.js";
|
import { getSortSelectors } from "../utils/grouping.js";
|
||||||
|
import { fuzzy } from "../utils/fuzzy.js";
|
||||||
|
|
||||||
type SearchResults<T> = {
|
type SearchResults<T> = {
|
||||||
sorted: (limit?: number) => Promise<VirtualizedGrouping<T>>;
|
sorted: (limit?: number) => Promise<VirtualizedGrouping<T>>;
|
||||||
@@ -210,63 +211,17 @@ export default class Lookup {
|
|||||||
suffix?: string;
|
suffix?: string;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
const results: Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
item: T;
|
|
||||||
score: number;
|
|
||||||
}
|
|
||||||
> = new Map();
|
|
||||||
const columns = fields.map((f) => f.column);
|
const columns = fields.map((f) => f.column);
|
||||||
const items = await selector.fields(columns).items();
|
const items = await selector.fields(columns).items();
|
||||||
|
|
||||||
for (const item of items) {
|
return fuzzy(
|
||||||
if (options.limit && results.size >= options.limit) break;
|
query,
|
||||||
|
items,
|
||||||
for (const field of fields) {
|
Object.fromEntries(
|
||||||
if (field.ignore) continue;
|
fields.filter((f) => !f.ignore).map((f) => [f.name, f.weight || 1])
|
||||||
|
) as Record<keyof T, number>,
|
||||||
const result = match(query, `${item[field.name]}`);
|
options
|
||||||
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
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private toSearchResults<T extends Item>(
|
private toSearchResults<T extends Item>(
|
||||||
|
|||||||
@@ -24,52 +24,69 @@ describe("lookup.fuzzy", () => {
|
|||||||
test("should sort items by score", () => {
|
test("should sort items by score", () => {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
|
id: "1",
|
||||||
title: "system"
|
title: "system"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "2",
|
||||||
title: "hello"
|
title: "hello"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "3",
|
||||||
title: "items"
|
title: "items"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const query = "ems";
|
const query = "ems";
|
||||||
expect(fuzzy(query, items, "title")).toStrictEqual([items[2]]);
|
expect(fuzzy(query, items, { title: 1 })).toStrictEqual([items[2]]);
|
||||||
});
|
});
|
||||||
describe("opts.prefix", () => {
|
describe("opts.prefix", () => {
|
||||||
test("should prefix matched field with provided value when given", () => {
|
test("should prefix matched field with provided value when given", () => {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
|
id: "1",
|
||||||
title: "hello"
|
title: "hello"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "2",
|
||||||
title: "world"
|
title: "world"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const query = "d";
|
const query = "d";
|
||||||
expect(
|
expect(
|
||||||
fuzzy(query, items, "title", {
|
fuzzy(
|
||||||
prefix: "prefix-"
|
query,
|
||||||
})
|
items,
|
||||||
).toStrictEqual([{ title: "worlprefix-d" }]);
|
{ title: 1 },
|
||||||
|
{
|
||||||
|
prefix: "prefix-"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).toStrictEqual([{ id: "2", title: "worlprefix-d" }]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("opt.suffix", () => {
|
describe("opt.suffix", () => {
|
||||||
test("should suffix matched field with provided value when given", () => {
|
test("should suffix matched field with provided value when given", () => {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
|
id: "1",
|
||||||
title: "hello"
|
title: "hello"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "2",
|
||||||
title: "world"
|
title: "world"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const query = "llo";
|
const query = "llo";
|
||||||
expect(
|
expect(
|
||||||
fuzzy(query, items, "title", {
|
fuzzy(
|
||||||
suffix: "-suffix"
|
query,
|
||||||
})
|
items,
|
||||||
).toStrictEqual([{ title: "hello-suffix" }]);
|
{ 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 { match, surround } from "fuzzyjs";
|
||||||
|
import { clone } from "./clone";
|
||||||
|
|
||||||
export function fuzzy<T>(
|
export function fuzzy<T extends { id: string }>(
|
||||||
query: string,
|
query: string,
|
||||||
items: T[],
|
items: T[],
|
||||||
key: keyof T,
|
fields: Partial<Record<keyof T, number>>,
|
||||||
opts?: {
|
options: {
|
||||||
|
limit?: number;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
}
|
} = {}
|
||||||
): T[] {
|
): T[] {
|
||||||
if (query === "") return items;
|
const results: Map<
|
||||||
|
string,
|
||||||
const fuzzied: [T, number][] = [];
|
{
|
||||||
|
item: T;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
> = new Map();
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const result = match(query, `${item[key]}`);
|
if (options.limit && results.size >= options.limit) break;
|
||||||
if (!result.match) continue;
|
|
||||||
|
|
||||||
if (opts?.prefix || opts?.suffix) {
|
for (const field in fields) {
|
||||||
fuzzied.push([
|
const result = match(query, `${item[field]}`);
|
||||||
{
|
if (!result.match) continue;
|
||||||
...item,
|
|
||||||
[key]: surround(`${item[key]}`, {
|
const oldMatch = results.get(item.id);
|
||||||
result: result,
|
const clonedItem = oldMatch?.item || clone(item);
|
||||||
prefix: opts?.prefix,
|
|
||||||
suffix: opts?.suffix
|
if (options.suffix || options.prefix) {
|
||||||
})
|
clonedItem[field] = surround(`${clonedItem[field]}`, {
|
||||||
},
|
suffix: options.suffix,
|
||||||
result.score
|
prefix: options.prefix,
|
||||||
]);
|
result
|
||||||
} else fuzzied.push([item, result.score]);
|
}) 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