To prevent search lag when there are a large number of notes

This commit is contained in:
SiriusXT 2025-04-01 18:47:07 +08:00
parent 8c6dbb4446
commit a7799d32b0
3 changed files with 51 additions and 10 deletions

View File

@ -10,6 +10,18 @@ const SELECTED_NOTE_PATH_KEY = "data-note-path";
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link"; 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<number>(`stats/notesCount`);
let debounceTimeoutId: ReturnType<typeof setTimeout>;
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 { export interface Suggestion {
noteTitle?: string; noteTitle?: string;
externalLink?: string; externalLink?: string;
@ -72,10 +84,9 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void
const activeNoteId = appContext.tabManager.getActiveContextNoteId(); const activeNoteId = appContext.tabManager.getActiveContextNoteId();
const length = term.trim().length; const length = term.trim().length;
let results: Suggestion[] = []; let results = await server.get<Suggestion[]>(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`);
if (length >= 3) {
results = await server.get<Suggestion[]>(`autocomplete?query=${encodeURIComponent(term)}&activeNoteId=${activeNoteId}&fastSearch=${fastSearch}`); options.fastSearch = true;
}
if (length >= 1 && options.allowCreatingNotes) { if (length >= 1 && options.allowCreatingNotes) {
results = [ results = [
@ -112,6 +123,7 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void
} }
function clearText($el: JQuery<HTMLElement>) { function clearText($el: JQuery<HTMLElement>) {
searchDelay = 0;
$el.setSelectedNotePath(""); $el.setSelectedNotePath("");
$el.autocomplete("val", "").trigger("change"); $el.autocomplete("val", "").trigger("change");
} }
@ -122,6 +134,7 @@ function setText($el: JQuery<HTMLElement>, text: string) {
} }
function showRecentNotes($el: JQuery<HTMLElement>) { function showRecentNotes($el: JQuery<HTMLElement>) {
searchDelay = 0;
$el.setSelectedNotePath(""); $el.setSelectedNotePath("");
$el.autocomplete("val", ""); $el.autocomplete("val", "");
$el.autocomplete("open"); $el.autocomplete("open");
@ -137,11 +150,8 @@ function fullTextSearch($el: JQuery<HTMLElement>, options: Options) {
options.fastSearch = false; options.fastSearch = false;
$el.autocomplete("val", ""); $el.autocomplete("val", "");
$el.setSelectedNotePath(""); $el.setSelectedNotePath("");
searchDelay = 0;
$el.autocomplete("val", searchString); $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<HTMLElement>, options?: Options) { function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
@ -154,6 +164,15 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
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"); $el.addClass("note-autocomplete-input");
const $clearTextButton = $("<a>").addClass("input-group-text input-clearer-button bx bxs-tag-x").prop("title", t("note_autocomplete.clear-text-field")); const $clearTextButton = $("<a>").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<HTMLElement>, 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", displayKey: "notePathTitle",
templates: { templates: {
suggestion: (suggestion) => suggestion.highlightedNotePathTitle suggestion: (suggestion) => suggestion.highlightedNotePathTitle

View File

@ -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 { export default {
getNoteSize, getNoteSize,
getSubtreeSize getSubtreeSize,
getNotesCount
}; };

View File

@ -361,6 +361,7 @@ function register(app: express.Application) {
apiRoute(GET, "/api/similar-notes/:noteId", similarNotesRoute.getSimilarNotes); apiRoute(GET, "/api/similar-notes/:noteId", similarNotesRoute.getSimilarNotes);
apiRoute(GET, "/api/backend-log", backendLogRoute.getBackendLog); apiRoute(GET, "/api/backend-log", backendLogRoute.getBackendLog);
apiRoute(GET, "/api/stats/note-size/:noteId", statsRoute.getNoteSize); 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(GET, "/api/stats/subtree-size/:noteId", statsRoute.getSubtreeSize);
apiRoute(PST, "/api/delete-notes-preview", notesApiRoute.getDeleteNotesPreview); apiRoute(PST, "/api/delete-notes-preview", notesApiRoute.getDeleteNotesPreview);
route(GET, "/api/fonts", [auth.checkApiAuthOrElectron], fontsRoute.getFontCss); route(GET, "/api/fonts", [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);