diff --git a/apps/server/src/services/search/expressions/note_content_fulltext.ts b/apps/server/src/services/search/expressions/note_content_fulltext.ts index 5d95c3538..8a64f001c 100644 --- a/apps/server/src/services/search/expressions/note_content_fulltext.ts +++ b/apps/server/src/services/search/expressions/note_content_fulltext.ts @@ -78,8 +78,13 @@ class NoteContentFulltextExp extends Expression { const resultNoteSet = new NoteSet(); + // Skip FTS5 for empty token searches - traditional search is more efficient + // Empty tokens means we're returning all notes (no filtering), which FTS5 doesn't optimize + if (this.tokens.length === 0) { + // Fall through to traditional search below + } // Try to use FTS5 if available for better performance - if (ftsSearchService.checkFTS5Availability() && this.canUseFTS5()) { + else if (ftsSearchService.checkFTS5Availability() && this.canUseFTS5()) { try { // Check if we need to search protected notes const searchProtected = protectedSessionService.isProtectedSessionAvailable(); diff --git a/apps/server/src/services/search/fts_search.ts b/apps/server/src/services/search/fts_search.ts index 1541bdd4b..033dcebb9 100644 --- a/apps/server/src/services/search/fts_search.ts +++ b/apps/server/src/services/search/fts_search.ts @@ -225,6 +225,40 @@ class FTSSearchService { throw new FTSNotAvailableError(); } + // Handle empty tokens efficiently - return all notes without running diagnostics + if (tokens.length === 0) { + // Empty query means return all indexed notes (optionally filtered by noteIds) + log.info('[FTS-OPTIMIZATION] Empty token array - returning all indexed notes without diagnostics'); + + const results: FTSSearchResult[] = []; + let query: string; + const params: any[] = []; + + if (noteIds && noteIds.size > 0) { + const nonProtectedNoteIds = this.filterNonProtectedNoteIds(noteIds); + if (nonProtectedNoteIds.length === 0) { + return []; // No non-protected notes to search + } + query = `SELECT noteId, title FROM notes_fts WHERE noteId IN (${nonProtectedNoteIds.map(() => '?').join(',')})`; + params.push(...nonProtectedNoteIds); + } else { + // Return all indexed notes + query = `SELECT noteId, title FROM notes_fts`; + } + + for (const row of sql.iterateRows<{ noteId: string; title: string }>(query, params)) { + results.push({ + noteId: row.noteId, + title: row.title, + score: 0, // No ranking for empty query + snippet: undefined + }); + } + + log.info(`[FTS-OPTIMIZATION] Empty token search returned ${results.length} results`); + return results; + } + // Normalize tokens to lowercase for case-insensitive search const normalizedTokens = tokens.map(t => t.toLowerCase()); @@ -458,6 +492,40 @@ class FTSSearchService { throw new FTSNotAvailableError(); } + // Handle empty tokens efficiently - return all notes without MATCH query + if (tokens.length === 0) { + log.info('[FTS-OPTIMIZATION] Empty token array in searchSync - returning all indexed notes'); + + // Reuse the empty token logic from searchWithLike + const results: FTSSearchResult[] = []; + let query: string; + const params: any[] = []; + + if (noteIds && noteIds.size > 0) { + const nonProtectedNoteIds = this.filterNonProtectedNoteIds(noteIds); + if (nonProtectedNoteIds.length === 0) { + return []; // No non-protected notes to search + } + query = `SELECT noteId, title FROM notes_fts WHERE noteId IN (${nonProtectedNoteIds.map(() => '?').join(',')})`; + params.push(...nonProtectedNoteIds); + } else { + // Return all indexed notes + query = `SELECT noteId, title FROM notes_fts`; + } + + for (const row of sql.iterateRows<{ noteId: string; title: string }>(query, params)) { + results.push({ + noteId: row.noteId, + title: row.title, + score: 0, // No ranking for empty query + snippet: undefined + }); + } + + log.info(`[FTS-OPTIMIZATION] Empty token search returned ${results.length} results`); + return results; + } + const { limit = FTS_CONFIG.DEFAULT_LIMIT, offset = 0,