diff --git a/src/main/engine/PostEngine.ts b/src/main/engine/PostEngine.ts index 86c0141..850adfb 100644 --- a/src/main/engine/PostEngine.ts +++ b/src/main/engine/PostEngine.ts @@ -1015,6 +1015,62 @@ export class PostEngine extends EventEmitter { console.log(`Rebuilt FTS index for ${allPosts.length} posts`); } + /** + * Reindex all text for full-text search. + * Runs as a background task with progress updates. + * Call this when search algorithms change or to fix search issues. + */ + async reindexText(): Promise { + const task: Task = { + id: uuidv4(), + name: 'Reindex search text', + execute: async (onProgress) => { + const client = getDatabase().getLocalClient(); + if (!client) { + throw new Error('Database client not available'); + } + + onProgress(0, 'Clearing existing search index...'); + + // Clear the entire FTS table + await client.execute('DELETE FROM posts_fts'); + + onProgress(5, 'Loading posts...'); + + const allPosts = await this.getAllPostsUnpaginated(); + const total = allPosts.length; + + if (total === 0) { + onProgress(100, 'No posts to index'); + return; + } + + onProgress(10, `Indexing ${total} posts...`); + + for (let i = 0; i < allPosts.length; i++) { + const post = allPosts[i]; + await this.updateFTSIndex(post); + + // Update progress (10% to 100%) + const progress = 10 + Math.round((i + 1) / total * 90); + if (i % 10 === 0 || i === allPosts.length - 1) { + onProgress(progress, `Indexed ${i + 1} of ${total} posts`); + } + + // Yield to event loop periodically + if (i % 20 === 0) { + await new Promise(resolve => setImmediate(resolve)); + } + } + + onProgress(100, `Reindexed ${total} posts`); + console.log(`Reindexed search text for ${total} posts`); + }, + }; + + await taskManager.runTask(task); + } + async rebuildDatabaseFromFiles(): Promise { const postsBaseDir = this.getPostsBaseDir(); const task: Task = { diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index 03089ff..da32582 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -214,6 +214,19 @@ export function registerIpcHandlers(): void { return engine.rebuildAllPostLinks(); }); + ipcMain.handle('posts:reindexText', async () => { + const projectEngine = getProjectEngine(); + const project = await projectEngine.getActiveProject(); + const engine = getPostEngine(); + if (project) { + engine.setProjectContext(project.id); + } + // Fire and forget - let it run as a background task + engine.reindexText().catch(err => { + console.error('Text reindex failed:', err); + }); + }); + // ============ Media Handlers ============ ipcMain.handle('media:import', async (_, sourcePath: string, metadata?: Partial) => { diff --git a/src/main/main.ts b/src/main/main.ts index 610518b..57adc6d 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -237,6 +237,12 @@ function createApplicationMenu(): Menu { mainWindow?.webContents.send('menu:rebuildDatabase'); }, }, + { + label: 'Reindex Search Text', + click: () => { + mainWindow?.webContents.send('menu:reindexText'); + }, + }, ], }, { diff --git a/src/main/preload.ts b/src/main/preload.ts index 147fe77..fc1bd40 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -28,6 +28,7 @@ contextBridge.exposeInMainWorld('electronAPI', { discard: (id: string) => ipcRenderer.invoke('posts:discard', id), hasPublishedVersion: (id: string) => ipcRenderer.invoke('posts:hasPublishedVersion', id), rebuildFromFiles: () => ipcRenderer.invoke('posts:rebuildFromFiles'), + reindexText: () => ipcRenderer.invoke('posts:reindexText'), search: (query: string) => ipcRenderer.invoke('posts:search', query), filter: (filter: unknown) => ipcRenderer.invoke('posts:filter', filter), getTags: () => ipcRenderer.invoke('posts:getTags'), diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 22daaab..caf0875 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -302,6 +302,13 @@ const App: React.FC = () => { }) || (() => {}) ); + unsubscribers.push( + window.electronAPI?.on('menu:reindexText', () => { + // Fire and forget - runs as a background task + window.electronAPI?.posts.reindexText(); + }) || (() => {}) + ); + return () => { unsubscribers.forEach(unsub => unsub()); }; diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index ff81688..df2b5c3 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -149,6 +149,7 @@ export interface ElectronAPI { discard: (id: string) => Promise; hasPublishedVersion: (id: string) => Promise; rebuildFromFiles: () => Promise; + reindexText: () => Promise; search: (query: string) => Promise; filter: (filter: PostFilter) => Promise; getTags: () => Promise;