From a7799d32b0332e9c6d9edde186522cb67d33f3f8 Mon Sep 17 00:00:00 2001 From: SiriusXT <1160925501@qq.com> Date: Tue, 1 Apr 2025 18:47:07 +0800 Subject: [PATCH] To prevent search lag when there are a large number of notes --- src/public/app/services/note_autocomplete.ts | 49 ++++++++++++++++---- src/routes/api/stats.ts | 11 ++++- src/routes/routes.ts | 1 + 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/public/app/services/note_autocomplete.ts b/src/public/app/services/note_autocomplete.ts index ca9894b1f..e879c87f1 100644 --- a/src/public/app/services/note_autocomplete.ts +++ b/src/public/app/services/note_autocomplete.ts @@ -10,6 +10,18 @@ const SELECTED_NOTE_PATH_KEY = "data-note-path"; const SELECTED_EXTERNAL_LINK_KEY = "data-external-link"; +// To prevent search lag when there are a large number of notes, set a delay based on the number of notes to avoid jitter. +const notesCount = await server.get(`stats/notesCount`); +let debounceTimeoutId: ReturnType; + +function getSearchDelay(notesCount: number): number { + const maxNotes = 20000; + const maxDelay = 1000; + const delay = Math.min(maxDelay, (notesCount / maxNotes) * maxDelay); + return delay; +} +let searchDelay = getSearchDelay(notesCount); + export interface Suggestion { noteTitle?: string; externalLink?: string; @@ -72,10 +84,9 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void const activeNoteId = appContext.tabManager.getActiveContextNoteId(); const length = term.trim().length; - let results: Suggestion[] = []; - if (length >= 3) { - results = await server.get(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`); - } + let results = await server.get(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`); + + options.fastSearch = true; if (length >= 1 && options.allowCreatingNotes) { results = [ @@ -112,6 +123,7 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void } function clearText($el: JQuery) { + searchDelay = 0; $el.setSelectedNotePath(""); $el.autocomplete("val", "").trigger("change"); } @@ -122,6 +134,7 @@ function setText($el: JQuery, text: string) { } function showRecentNotes($el: JQuery) { + searchDelay = 0; $el.setSelectedNotePath(""); $el.autocomplete("val", ""); $el.autocomplete("open"); @@ -137,11 +150,8 @@ function fullTextSearch($el: JQuery, options: Options) { options.fastSearch = false; $el.autocomplete("val", ""); $el.setSelectedNotePath(""); + searchDelay = 0; $el.autocomplete("val", searchString); - // Set a delay to avoid resetting to true before full text search (await server.get) is called. - setTimeout(() => { - options.fastSearch = true; - }, 100); } function initNoteAutocomplete($el: JQuery, options?: Options) { @@ -154,6 +164,15 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { options = options || {}; + // Used to track whether the user is performing character composition with an input method (such as Chinese Pinyin, Japanese, Korean, etc.) and to avoid triggering a search during the composition process. + let isComposingInput = false; + $el.on("compositionstart", () => { + isComposingInput = true; + }); + $el.on("compositionend", () => { + isComposingInput = false; + }); + $el.addClass("note-autocomplete-input"); const $clearTextButton = $("").addClass("input-group-text input-clearer-button bx bxs-tag-x").prop("title", t("note_autocomplete.clear-text-field")); @@ -226,7 +245,19 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { }, [ { - source: (term, cb) => autocompleteSource(term, cb, options), + source: (term, cb) => { + clearTimeout(debounceTimeoutId); + debounceTimeoutId = setTimeout(() => { + if (isComposingInput) { + return; + } + autocompleteSource(term, cb, options); + }, searchDelay); + + if (searchDelay === 0) { + searchDelay = getSearchDelay(notesCount); + } + }, displayKey: "notePathTitle", templates: { suggestion: (suggestion) => suggestion.highlightedNotePathTitle diff --git a/src/routes/api/stats.ts b/src/routes/api/stats.ts index 15e28f083..8f953bc0b 100644 --- a/src/routes/api/stats.ts +++ b/src/routes/api/stats.ts @@ -48,7 +48,16 @@ function getSubtreeSize(req: Request) { }; } +// Get the total number of notes +function getNotesCount(req: Request) { + const notesCount = sql.getRow( + `SELECT COUNT(*) AS count FROM notes WHERE isDeleted = 0;`, + ) as { count: number }; + return notesCount.count; +} + export default { getNoteSize, - getSubtreeSize + getSubtreeSize, + getNotesCount }; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index abae1acaa..4b09def4f 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -361,6 +361,7 @@ function register(app: express.Application) { apiRoute(GET, "/api/similar-notes/:noteId", similarNotesRoute.getSimilarNotes); apiRoute(GET, "/api/backend-log", backendLogRoute.getBackendLog); apiRoute(GET, "/api/stats/note-size/:noteId", statsRoute.getNoteSize); + apiRoute(GET, "/api/stats/notesCount", statsRoute.getNotesCount); apiRoute(GET, "/api/stats/subtree-size/:noteId", statsRoute.getSubtreeSize); apiRoute(PST, "/api/delete-notes-preview", notesApiRoute.getDeleteNotesPreview); route(GET, "/api/fonts", [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);