diff --git a/apps/web/src/components/field/index.tsx b/apps/web/src/components/field/index.tsx
index 22bace8aa..b9e30d72c 100644
--- a/apps/web/src/components/field/index.tsx
+++ b/apps/web/src/components/field/index.tsx
@@ -126,13 +126,10 @@ function Field(props: FieldProps) {
sx={{
position: "absolute",
right: "2px",
- top: "2px",
- cursor: "pointer",
- bottom: "2px",
px: 1,
borderRadius: "default",
":hover": { bg: "border" },
- height: "calc(100% - 4px)"
+ height: "100%"
}}
disabled={action.disabled}
>
diff --git a/apps/web/src/components/route-container/index.tsx b/apps/web/src/components/route-container/index.tsx
index 474a8e89a..b1ef2f9fc 100644
--- a/apps/web/src/components/route-container/index.tsx
+++ b/apps/web/src/components/route-container/index.tsx
@@ -19,11 +19,13 @@ along with this program. If not, see .
import { PropsWithChildren } from "react";
import { Flex, Text } from "@theme-ui/components";
-import { ArrowLeft, Menu, Search, Plus } from "../icons";
+import { ArrowLeft, Menu, Search, Plus, Close } from "../icons";
import { useStore } from "../../stores/app-store";
+import { useStore as useSearchStore } from "../../stores/search-store";
import useMobile from "../../hooks/use-mobile";
-import { navigate } from "../../navigation";
import usePromise from "../../hooks/use-promise";
+import { debounce } from "@notesnook/common";
+import Field from "../field";
export type RouteContainerButtons = {
search?: {
@@ -64,63 +66,99 @@ function Header(props: RouteContainerProps) {
);
const toggleSideMenu = useStore((store) => store.toggleSideMenu);
const isMobile = useMobile();
+ const isSearching = useSearchStore((store) => store.isSearching);
+
+ if (isSearching)
+ return (
+
+ useSearchStore.setState({ query: e.target.value }),
+ 250
+ )}
+ action={{
+ icon: Close,
+ testId: "search-button",
+ onClick: () =>
+ useSearchStore.setState({
+ isSearching: false,
+ searchType: undefined
+ })
+ }}
+ />
+
+ );
return (
-
-
-
- {buttons?.back ? (
-
- ) : (
-
-
- {buttons?.search && (
- navigate(`/search/${type}`)}
- />
- )}
- {!isMobile && buttons?.create && (
-
- )}
-
+
+
+ {buttons?.back ? (
+
+ ) : (
+
+
+ {buttons?.search && (
+
+ useSearchStore.setState({ isSearching: true, searchType: type })
+ }
+ sx={{
+ size: 24,
+ cursor: "pointer"
+ }}
+ />
+ )}
+ {!isMobile && buttons?.create && (
+
+ )}
);
diff --git a/apps/web/src/components/search/index.jsx b/apps/web/src/components/search/index.jsx
deleted file mode 100644
index 933f09028..000000000
--- a/apps/web/src/components/search/index.jsx
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
-This file is part of the Notesnook project (https://notesnook.com/)
-
-Copyright (C) 2023 Streetwriters (Private) Limited
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-import { Search } from "../icons";
-import Field from "../field";
-import { debounce } from "@notesnook/common";
-
-function SearchBox({ onSearch }) {
- return (
- onSearch(e.target.value), 250)}
- action={{
- icon: Search,
- testId: "search-button",
- onClick: () => {
- const searchField = document.getElementById("search");
- if (searchField && searchField.value && searchField.value.length) {
- onSearch(searchField.value);
- }
- }
- }}
- />
- );
-}
-export default SearchBox;
diff --git a/apps/web/src/hooks/use-search.ts b/apps/web/src/hooks/use-search.ts
new file mode 100644
index 000000000..6c4cbfc4d
--- /dev/null
+++ b/apps/web/src/hooks/use-search.ts
@@ -0,0 +1,42 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+import { DependencyList, useEffect, useState } from "react";
+import { useStore as useSearchStore } from "../stores/search-store";
+import { VirtualizedGrouping } from "@notesnook/core";
+
+export function useSearch(
+ type: "notes" | "notebooks" | "reminders" | "trash" | "tags",
+ lookup: (query: string) => Promise> | undefined,
+ deps: DependencyList = []
+) {
+ const isSearching = useSearchStore((store) => store.isSearching);
+ const query = useSearchStore((store) => store.query);
+ const searchType = useSearchStore((store) => store.searchType);
+ const [filteredItems, setFilteredItems] = useState>();
+
+ useEffect(() => {
+ if (searchType !== type) return;
+ (async function () {
+ if (!query || !isSearching) return setFilteredItems(undefined);
+ setFilteredItems(await lookup(query));
+ })();
+ }, [isSearching, query, searchType, type, ...deps]);
+
+ return filteredItems;
+}
diff --git a/apps/web/src/navigation/routes.tsx b/apps/web/src/navigation/routes.tsx
index 1bf5efabb..c457cebdb 100644
--- a/apps/web/src/navigation/routes.tsx
+++ b/apps/web/src/navigation/routes.tsx
@@ -21,7 +21,6 @@ import { db } from "../common/db";
import AllNotes from "../views/all-notes";
import Notebooks from "../views/notebooks";
import Notes from "../views/notes";
-import Search from "../views/search";
import Tags from "../views/tags";
import Notebook from "../views/notebook";
import { navigate } from ".";
@@ -35,7 +34,7 @@ import { CREATE_BUTTON_MAP } from "../common";
type RouteResult = {
key: string;
- type: "notes" | "notebooks" | "reminders" | "trash" | "tags" | "search";
+ type: "notes" | "notebooks" | "reminders" | "trash" | "tags";
title?: string | (() => Promise);
component: React.ReactNode;
props?: any;
@@ -225,20 +224,7 @@ const routes = defineRoutes({
}
}
});
- },
- "/search/:type": ({ type }) =>
- defineRoute({
- key: "general",
- type: "search",
- title: "Search",
- component: () => ,
- buttons: {
- back: {
- title: `Go back to ${type}`,
- onClick: () => window.history.back()
- }
- }
- })
+ }
});
export default routes;
diff --git a/apps/web/src/stores/search-store.js b/apps/web/src/stores/search-store.ts
similarity index 65%
rename from apps/web/src/stores/search-store.js
rename to apps/web/src/stores/search-store.ts
index 7b500a68b..f7c18a4f5 100644
--- a/apps/web/src/stores/search-store.js
+++ b/apps/web/src/stores/search-store.ts
@@ -18,20 +18,25 @@ along with this program. If not, see .
*/
import createStore from "../common/store";
-import { db } from "../common/db";
import BaseStore from "./index";
-/**
- * @extends {BaseStore}
- */
-class SearchStore extends BaseStore {
- results = [];
+class SearchStore extends BaseStore {
+ isSearching = false;
+ query?: string;
+ searchType?: string;
+ // startSearch = () => {
+ // this.set({ isSearching: true });
+ // };
- search = async (items, query) => {
- const { type } = this.get();
- const results = await db.lookup[type](items, query);
- this.set((state) => (state.results = results));
- };
+ // endSearch = () => {
+ // this.set({ isSearching: false });
+ // };
+ // results = [];
+ // search = async (items, query) => {
+ // const { type } = this.get();
+ // const results = await db.lookup[type](items, query);
+ // this.set((state) => (state.results = results));
+ // };
}
const [useStore, store] = createStore(SearchStore);
diff --git a/apps/web/src/views/all-notes.tsx b/apps/web/src/views/all-notes.tsx
index e7e355fb2..5b7d57f00 100644
--- a/apps/web/src/views/all-notes.tsx
+++ b/apps/web/src/views/all-notes.tsx
@@ -23,28 +23,24 @@ import ListContainer from "../components/list-container";
import { hashNavigate } from "../navigation";
import useNavigate from "../hooks/use-navigate";
import Placeholder from "../components/placeholders";
+import { useSearch } from "../hooks/use-search";
+import { db } from "../common/db";
function Home() {
const notes = useStore((store) => store.notes);
const isCompact = useStore((store) => store.viewMode === "compact");
const refresh = useStore((store) => store.refresh);
const setContext = useStore((store) => store.setContext);
+ const filteredItems = useSearch("notes", (query) => {
+ if (useStore.getState().context) return;
+ return db.lookup.notes(query);
+ });
useNavigate("home", setContext);
useEffect(() => {
- (async function () {
- await refresh();
- // const note = db.notes.note("62bc3f28a1a1a10000707077").data;
- // const data = await db.content.raw(note.contentId);
- // const note2 = db.notes.note("62bc3f1ca1a1a10000707075").data;
- // const data2 = await db.content.raw(note2.contentId);
- // const data3 = { ...data, conflicted: data2 };
- // await db.content.add(data3);
- // await db.notes.add({ id: note.id, conflicted: true, resolved: false });
- // console.log(data3);
- })();
- }, [refresh]);
+ useStore.getState().refresh();
+ }, []);
if (!notes) return ;
return (
@@ -52,7 +48,7 @@ function Home() {
group="home"
compact={isCompact}
refresh={refresh}
- items={notes}
+ items={filteredItems || notes}
placeholder={}
button={{
onClick: () =>
diff --git a/apps/web/src/views/notebook.tsx b/apps/web/src/views/notebook.tsx
index 274335929..33c77c02d 100644
--- a/apps/web/src/views/notebook.tsx
+++ b/apps/web/src/views/notebook.tsx
@@ -54,6 +54,7 @@ import { NotebookContext } from "../components/list-container/types";
import { FlexScrollContainer } from "../components/scroll-container";
import { Menu } from "../hooks/use-menu";
import Config from "../utils/config";
+import { useSearch } from "../hooks/use-search";
type NotebookProps = {
rootId: string;
@@ -70,6 +71,14 @@ function Notebook(props: NotebookProps) {
const notes = useNotesStore((store) => store.contextNotes);
const refreshContext = useNotesStore((store) => store.refreshContext);
const isCompact = useNotesStore((store) => store.viewMode === "compact");
+ const filteredItems = useSearch(
+ "notes",
+ (query) => {
+ if (!context || !notes || context.type !== "notebook") return;
+ return db.lookup.notes(query, notes.ungrouped);
+ },
+ [context, notes]
+ );
useEffect(() => {
const { context, setContext } = useNotesStore.getState();
@@ -126,7 +135,7 @@ function Notebook(props: NotebookProps) {
refresh={refreshContext}
compact={isCompact}
context={context}
- items={notes}
+ items={filteredItems || notes}
placeholder={}
header={
store.refresh());
const notebooks = useStore((state) => state.notebooks);
const refresh = useStore((state) => state.refresh);
+ const filteredItems = useSearch("notebooks", (query) =>
+ db.lookup.notebooks(query)
+ );
useEffect(() => {
store.get().refresh();
@@ -38,7 +42,7 @@ function Notebooks() {
}
button={{
onClick: () => hashNavigate("/notebooks/create")
diff --git a/apps/web/src/views/notes.tsx b/apps/web/src/views/notes.tsx
index b0a0b277d..9dc5a14c8 100644
--- a/apps/web/src/views/notes.tsx
+++ b/apps/web/src/views/notes.tsx
@@ -22,6 +22,8 @@ import ListContainer from "../components/list-container";
import { useStore as useNotesStore } from "../stores/note-store";
import { hashNavigate, navigate } from "../navigation";
import Placeholder from "../components/placeholders";
+import { useSearch } from "../hooks/use-search";
+import { db } from "../common/db";
function Notes() {
const context = useNotesStore((store) => store.context);
@@ -29,6 +31,14 @@ function Notes() {
const refreshContext = useNotesStore((store) => store.refreshContext);
const type = context?.type === "favorite" ? "favorites" : "notes";
const isCompact = useNotesStore((store) => store.viewMode === "compact");
+ const filteredItems = useSearch(
+ "notes",
+ (query) => {
+ if (!context || !contextNotes) return;
+ return db.lookup.notes(query, contextNotes.ungrouped);
+ },
+ [context, contextNotes]
+ );
useEffect(() => {
if (
@@ -36,7 +46,7 @@ function Notes() {
contextNotes &&
contextNotes.ids.length <= 0
) {
- navigate("/", true);
+ navigate("/", { replace: true });
}
}, [context, contextNotes]);
@@ -47,7 +57,7 @@ function Notes() {
refresh={refreshContext}
compact={isCompact}
context={context}
- items={contextNotes}
+ items={filteredItems || contextNotes}
placeholder={
store.refresh());
const reminders = useStore((state) => state.reminders);
const refresh = useStore((state) => state.refresh);
+ const filteredItems = useSearch("reminders", (query) =>
+ db.lookup.reminders(query)
+ );
+
if (!reminders) return ;
return (
<>
}
button={{
onClick: () => hashNavigate("/reminders/create")
diff --git a/apps/web/src/views/search.jsx b/apps/web/src/views/search.jsx
deleted file mode 100644
index 6c8daca9a..000000000
--- a/apps/web/src/views/search.jsx
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
-This file is part of the Notesnook project (https://notesnook.com/)
-
-Copyright (C) 2023 Streetwriters (Private) Limited
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-*/
-
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import ListContainer from "../components/list-container";
-import { db } from "../common/db";
-import SearchBox from "../components/search";
-import { useStore as useNoteStore } from "../stores/note-store";
-import { Flex, Text } from "@theme-ui/components";
-import { showToast } from "../utils/toast";
-import { store as notebookstore } from "../stores/notebook-store";
-import { hardNavigate } from "../navigation";
-import Placeholder from "../components/placeholders";
-
-async function typeToItems(type, context) {
- switch (type) {
- case "notebook": {
- const selectedNotebook = notebookstore.get().selectedNotebook;
- if (!selectedNotebook) return ["notes", []];
- const notes = db.relations.to(selectedNotebook, "note");
- return ["notes", notes];
- }
- case "notes": {
- if (!context) return ["notes", db.notes.all];
- const notes = context.notes;
- let topicNotes = [];
-
- if (context.type === "notebook") {
- topicNotes = db.notebooks
- .notebook(context.value.id)
- .topics.all.map((topic) => {
- return db.notes.topicReferences
- .get(topic.id)
- .map((id) => db.notes.note(id)?.data);
- })
- .flat()
- .filter(
- (topicNote) =>
- notes.findIndex((note) => note?.id !== topicNote?.id) === -1
- );
- }
-
- return ["notes", [...notes, ...topicNotes]];
- }
- case "notebooks":
- return ["notebooks", db.notebooks.all];
- case "topics": {
- const selectedNotebook = notebookstore.get().selectedNotebook;
- if (!selectedNotebook) return ["topics", []];
- const topics = db.notebooks.notebook(selectedNotebook.id).topics.all;
- return ["topics", topics];
- }
- case "tags":
- return ["tags", db.tags.all];
- case "reminders":
- return ["reminders", db.reminders.all];
- case "trash":
- return ["trash", db.trash.all];
- default:
- return [];
- }
-}
-
-function Search({ type }) {
- const [searchState, setSearchState] = useState({
- isSearching: false,
- totalItems: 0
- });
- const [results, setResults] = useState([]);
- const context = useNoteStore((store) => store.context);
- const nonce = useNoteStore((store) => store.nonce);
- const cachedQuery = useRef();
-
- const onSearch = useCallback(
- async (query) => {
- if (!query) {
- setResults([]);
- return;
- }
- cachedQuery.current = query;
-
- const [lookupType, items] = await typeToItems(type, context);
-
- if (items.length <= 0) {
- showToast("error", `There are no items to search in.`);
- setResults([]);
- return;
- }
- setSearchState({ isSearching: true, totalItems: items.length });
- const results = await db.lookup[lookupType](items, query);
- setResults(results);
- setSearchState({ isSearching: false, totalItems: 0 });
- },
- [context, type]
- );
-
- const title = useMemo(() => {
- switch (type) {
- case "notes":
- if (!context) return "all notes";
- switch (context.type) {
- case "topic": {
- const notebook = db.notebooks.notebook(context.value.id);
- const topic = notebook.topics.topic(context.value.topic);
- return `notes of ${topic._topic.title} in ${notebook.title}`;
- }
- case "notebook": {
- const notebook = db.notebooks.notebook(context.value.id);
- return `notes in ${notebook.title}`;
- }
- case "tag": {
- const tag = db.tags.all.find((tag) => tag.id === context.value);
- return `notes in #${tag.title}`;
- }
- case "favorite":
- return "favorite notes";
- case "monographs":
- return "all monographs";
- case "color": {
- const color = db.colors.find(context.value);
- return `notes in color ${color.title}`;
- }
- default:
- return;
- }
- case "notebooks":
- return "all notebooks";
- case "notebook":
- case "topics": {
- const selectedNotebook = notebookstore.get().selectedNotebook;
- if (!selectedNotebook) return "";
- const notebook = db.notebooks.notebook(selectedNotebook.id);
- return `${type === "topics" ? "topics" : "notes"} in ${
- notebook.title
- } notebook`;
- }
- case "tags":
- return "all tags";
- case "reminders":
- return "all reminders";
- case "trash":
- return "all trash";
- default:
- return "";
- }
- }, [type, context]);
-
- useEffect(() => {
- (async function () {
- const [lookupType, items] = await typeToItems(type, context);
- const results = await db.lookup[lookupType](items, cachedQuery.current);
- setResults(results);
- })();
- }, [nonce, type, context]);
-
- if (!title) {
- hardNavigate("/");
- return null;
- }
-
- return (
- <>
-
- Searching {title}
-
-
- {searchState.isSearching && results?.length === 0 ? (
-
-
-
- ) : (
- (
-
- )}
- />
- )}
- >
- );
-}
-export default Search;
diff --git a/apps/web/src/views/tags.tsx b/apps/web/src/views/tags.tsx
index ff64ef3c2..5ce6fc458 100644
--- a/apps/web/src/views/tags.tsx
+++ b/apps/web/src/views/tags.tsx
@@ -21,18 +21,21 @@ import ListContainer from "../components/list-container";
import { useStore, store } from "../stores/tag-store";
import useNavigate from "../hooks/use-navigate";
import Placeholder from "../components/placeholders";
+import { useSearch } from "../hooks/use-search";
+import { db } from "../common/db";
function Tags() {
useNavigate("tags", () => store.refresh());
const tags = useStore((store) => store.tags);
const refresh = useStore((store) => store.refresh);
+ const filteredItems = useSearch("tags", (query) => db.lookup.tags(query));
if (!tags) return ;
return (
}
/>
);
diff --git a/apps/web/src/views/trash.tsx b/apps/web/src/views/trash.tsx
index 60e943b79..352b79e0a 100644
--- a/apps/web/src/views/trash.tsx
+++ b/apps/web/src/views/trash.tsx
@@ -23,12 +23,15 @@ import { useStore, store } from "../stores/trash-store";
import { showToast } from "../utils/toast";
import useNavigate from "../hooks/use-navigate";
import Placeholder from "../components/placeholders";
+import { useSearch } from "../hooks/use-search";
+import { db } from "../common/db";
function Trash() {
useNavigate("trash", store.refresh);
const items = useStore((store) => store.trash);
const refresh = useStore((store) => store.refresh);
const clearTrash = useStore((store) => store.clear);
+ const filteredItems = useSearch("trash", (query) => db.lookup.trash(query));
if (!items) return ;
return (
@@ -36,7 +39,7 @@ function Trash() {
group="trash"
refresh={refresh}
placeholder={}
- items={items}
+ items={filteredItems || items}
button={{
onClick: function () {
confirm({
diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json
index 00eb65f00..6d0d6a919 100644
--- a/packages/core/package-lock.json
+++ b/packages/core/package-lock.json
@@ -18,6 +18,7 @@
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
+ "fuzzyjs": "^5.0.1",
"html-to-text": "^9.0.5",
"htmlparser2": "^8.0.1",
"katex": "0.16.2",
@@ -2578,6 +2579,14 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/fuzzyjs": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/fuzzyjs/-/fuzzyjs-5.0.1.tgz",
+ "integrity": "sha512-/BixxKEZ0sIQkIogNBhuwX7EC1gznFoR9jhrEU0cb89HUTH+tb2u9s86Rj//xGzpQg04A1aD9KDyGE+ldxgLFQ==",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/get-func-name": {
"version": "2.0.0",
"dev": true,
diff --git a/packages/core/package.json b/packages/core/package.json
index ddeb62902..9fa471703 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -56,6 +56,7 @@
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
+ "fuzzyjs": "^5.0.1",
"html-to-text": "^9.0.5",
"htmlparser2": "^8.0.1",
"katex": "0.16.2",
diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts
index 931c6bb33..a4273cf4d 100644
--- a/packages/core/src/api/index.ts
+++ b/packages/core/src/api/index.ts
@@ -153,7 +153,7 @@ class Database {
);
};
- options?: Options;
+ options!: Options;
EventSource?: EventSourceConstructor;
eventSource?: EventSource | null;
@@ -226,6 +226,11 @@ class Database {
}
async init() {
+ if (!this.options)
+ throw new Error(
+ "options not specified. Did you forget to call db.setup()?"
+ );
+
EV.subscribeMulti(
[EVENTS.userLoggedIn, EVENTS.userFetched, EVENTS.tokenRefreshed],
this.connectSSE,
@@ -241,8 +246,7 @@ class Database {
this.disconnectSSE();
});
- if (this.options)
- this._sql = await createDatabase(this.options.sqliteOptions);
+ this._sql = await createDatabase(this.options.sqliteOptions);
await this._validate();
diff --git a/packages/core/src/api/lookup.ts b/packages/core/src/api/lookup.ts
index c5d153875..708dd4b9a 100644
--- a/packages/core/src/api/lookup.ts
+++ b/packages/core/src/api/lookup.ts
@@ -17,29 +17,25 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import uFuzzy from "@leeoniya/ufuzzy";
+import { match } from "fuzzyjs";
import Database from ".";
-import {
- Attachment,
- GroupedItems,
- Note,
- Notebook,
- Reminder,
- Tag,
- TrashItem
-} from "../types";
-import { DatabaseSchemaWithFTS, isFalse } from "../database";
-import { Kysely, sql } from "kysely";
+import { Item, TrashItem } from "../types";
+import { DatabaseSchema, DatabaseSchemaWithFTS, isFalse } from "../database";
+import { AnyColumnWithTable, Kysely, sql } from "kysely";
+import { FilteredSelector } from "../database/sql-collection";
+import { VirtualizedGrouping } from "../utils/virtualized-grouping";
+type FuzzySearchField = {
+ weight?: number;
+ name: keyof T;
+ column: AnyColumnWithTable;
+};
export default class Lookup {
constructor(private readonly db: Database) {}
- async notes(
- query: string,
- ids?: string[]
- ): Promise {
+ async notes(query: string, noteIds?: string[]) {
const db = this.db.sql() as Kysely;
- return (await db
+ const ids = await db
.with("matching", (eb) =>
eb
.selectFrom("content_fts")
@@ -54,51 +50,101 @@ export default class Lookup {
)
)
.selectFrom("notes")
- .$if(!!ids && ids.length > 0, (eb) => eb.where("id", "in", ids!))
+ .$if(!!noteIds && noteIds.length > 0, (eb) =>
+ eb.where("id", "in", noteIds!)
+ )
.where(isFalse("notes.deleted"))
.where(isFalse("notes.dateDeleted"))
.innerJoin("matching", (eb) => eb.onRef("notes.id", "==", "matching.id"))
.orderBy("matching.rank")
- .selectAll()
- .execute()) as unknown as Note & { rank: number }[];
+ .select(["notes.id"])
+ .execute();
+
+ return new VirtualizedGrouping(
+ ids.map((id) => id.id),
+ this.db.options.batchSize,
+ (ids) => this.db.notes.all.records(ids)
+ );
}
- notebooks(array: Notebook[], query: string) {
- return search(array, query, (n) => `${n.title} ${n.description}}`);
+ notebooks(query: string) {
+ return this.search(this.db.notebooks.all, query, [
+ { name: "id", column: "notebooks.id", weight: -100 },
+ { name: "title", column: "notebooks.title", weight: 10 },
+ { name: "description", column: "notebooks.description" }
+ ]);
}
- tags(array: GroupedItems, query: string) {
- return this.byTitle(array, query);
+ tags(query: string) {
+ return this.search(this.db.tags.all, query, [
+ { name: "id", column: "tags.id", weight: -100 },
+ { name: "title", column: "tags.title" }
+ ]);
}
- reminders(array: Reminder[], query: string) {
- return search(array, query, (n) => `${n.title} ${n.description || ""}`);
+ reminders(query: string) {
+ return this.search(this.db.reminders.all, query, [
+ { name: "id", column: "reminders.id", weight: -100 },
+ { name: "title", column: "reminders.title", weight: 10 },
+ { name: "description", column: "reminders.description" }
+ ]);
}
- trash(array: TrashItem[], query: string) {
- return this.byTitle(array, query);
- }
+ async trash(query: string) {
+ const items = await this.db.trash.all();
+ const records: Record = {};
+ for (const item of items) records[item.id] = item;
- attachments(array: Attachment[], query: string) {
- return search(array, query, (n) => `${n.filename} ${n.mimeType} ${n.hash}`);
- }
-
- private byTitle(array: T[], query: string) {
- return search(array, query, (n) => n.title);
- }
-}
-
-const uf = new uFuzzy();
-function search(items: T[], query: string, selector: (item: T) => string) {
- try {
- const [_idxs, _info, order] = uf.search(items.map(selector), query, true);
- if (!order) return [];
- const filtered: T[] = [];
- for (const i of order) {
- filtered.push(items[i]);
+ const results: Record = {};
+ for (const item of items) {
+ const result = match(query, item.title);
+ if (result.match) results[item.id] = result.score;
}
- return filtered;
- } catch (e) {
- return [];
+
+ const ids = Object.keys(results).sort((a, b) => results[a] - results[b]);
+ return new VirtualizedGrouping(
+ ids,
+ this.db.options.batchSize,
+ async (ids) => {
+ const items: Record = {};
+ for (const id of ids) items[id] = records[id];
+ return items;
+ }
+ );
+ }
+
+ attachments(query: string) {
+ return this.search(this.db.attachments.all, query, [
+ { name: "id", column: "attachments.id", weight: -100 },
+ { name: "filename", column: "attachments.filename", weight: 5 },
+ { name: "mimeType", column: "attachments.mimeType" },
+ { name: "hash", column: "attachments.hash" }
+ ]);
+ }
+
+ private async search(
+ selector: FilteredSelector,
+ query: string,
+ fields: FuzzySearchField[]
+ ) {
+ const results: Record = {};
+ const columns = fields.map((f) => f.column);
+ for await (const item of selector.fields(columns)) {
+ for (const field of fields) {
+ const result = match(query, `${item[field.name]}`);
+ if (result.match) {
+ const oldScore = results[item.id] || 0;
+ results[item.id] = oldScore + result.score * (field.weight || 1);
+ }
+ }
+ }
+ selector.fields([]);
+
+ const ids = Object.keys(results).sort((a, b) => results[a] - results[b]);
+ return new VirtualizedGrouping(
+ ids,
+ this.db.options.batchSize,
+ async (ids) => selector.records(ids)
+ );
}
}
diff --git a/packages/core/src/database/sql-collection.ts b/packages/core/src/database/sql-collection.ts
index 1f7a4ebf4..1ffc6d56d 100644
--- a/packages/core/src/database/sql-collection.ts
+++ b/packages/core/src/database/sql-collection.ts
@@ -347,21 +347,8 @@ export class FilteredSelector {
console.timeEnd("getting items");
console.log(items.length);
const ids = groupArray(items, options);
- return new VirtualizedGrouping(
- ids,
- this.batchSize,
- async (ids) => {
- const results = await this.filter
- .where("id", "in", ids)
- .selectAll()
- .execute();
- const items: Record = {};
- for (const item of results) {
- items[item.id] = item as T;
- }
- return items;
- }
- //(ids, items) => groupArray(ids, items, options)
+ return new VirtualizedGrouping(ids, this.batchSize, (ids) =>
+ this.records(ids)
);
}
@@ -371,17 +358,9 @@ export class FilteredSelector {
.select("id")
.execute();
const ids = items.map((item) => item.id);
- return new VirtualizedGrouping(ids, this.batchSize, async (ids) => {
- const results = await this.filter
- .where("id", "in", ids)
- .selectAll()
- .execute();
- const items: Record = {};
- for (const item of results) {
- items[item.id] = item as T;
- }
- return items;
- });
+ return new VirtualizedGrouping(ids, this.batchSize, (ids) =>
+ this.records(ids)
+ );
}
private buildSortExpression(options: SortOptions) {