|
1 | 1 | import sql from "../services/sql.js"; |
| 2 | +import log from "../services/log.js"; |
| 3 | +import { formatSize } from "../services/utils.js"; |
2 | 4 | import NoteSet from "../services/search/note_set.js"; |
3 | 5 | import NotFoundError from "../errors/not_found_error.js"; |
4 | 6 | import type BOption from "./entities/boption.js"; |
@@ -31,9 +33,22 @@ export default class Becca { |
31 | 33 |
|
32 | 34 | allNoteSetCache: NoteSet | null; |
33 | 35 |
|
| 36 | + /** |
| 37 | + * Pre-built parallel arrays for fast flat text scanning in search. |
| 38 | + * Avoids per-note property access overhead when iterating 50K+ notes. |
| 39 | + * Supports incremental updates: when individual notes change, only their |
| 40 | + * entries are rebuilt rather than the entire index. |
| 41 | + */ |
| 42 | + flatTextIndex: { notes: BNote[], flatTexts: string[], noteIdToIdx: Map<string, number> } | null; |
| 43 | + |
| 44 | + /** NoteIds whose flat text needs to be recomputed in the index. */ |
| 45 | + dirtyFlatTextNoteIds: Set<string>; |
| 46 | + |
34 | 47 | constructor() { |
35 | | - this.reset(); |
| 48 | + this.dirtyFlatTextNoteIds = new Set(); |
36 | 49 | this.allNoteSetCache = null; |
| 50 | + this.flatTextIndex = null; |
| 51 | + this.reset(); |
37 | 52 | } |
38 | 53 |
|
39 | 54 | reset() { |
@@ -242,6 +257,67 @@ export default class Becca { |
242 | 257 | /** Should be called when the set of all non-skeleton notes changes (added/removed) */ |
243 | 258 | dirtyNoteSetCache() { |
244 | 259 | this.allNoteSetCache = null; |
| 260 | + // Full rebuild needed since the note set itself changed |
| 261 | + this.flatTextIndex = null; |
| 262 | + this.dirtyFlatTextNoteIds.clear(); |
| 263 | + } |
| 264 | + |
| 265 | + /** Mark a single note's flat text as needing recomputation in the index. */ |
| 266 | + dirtyNoteFlatText(noteId: string) { |
| 267 | + if (this.flatTextIndex) { |
| 268 | + // Index exists — schedule an incremental update |
| 269 | + this.dirtyFlatTextNoteIds.add(noteId); |
| 270 | + } |
| 271 | + // If flatTextIndex is null, full rebuild will happen on next access anyway |
| 272 | + } |
| 273 | + |
| 274 | + /** |
| 275 | + * Returns pre-built parallel arrays of notes and their flat texts for fast scanning. |
| 276 | + * The flat texts are already normalized (lowercase, diacritics removed). |
| 277 | + * Supports incremental updates: when individual notes are dirtied, only their |
| 278 | + * entries are recomputed rather than rebuilding the entire index. |
| 279 | + */ |
| 280 | + getFlatTextIndex(): { notes: BNote[], flatTexts: string[], noteIdToIdx: Map<string, number> } { |
| 281 | + if (!this.flatTextIndex) { |
| 282 | + // Measure heap before building |
| 283 | + const heapBefore = process.memoryUsage().heapUsed; |
| 284 | + |
| 285 | + const allNoteSet = this.getAllNoteSet(); |
| 286 | + const notes: BNote[] = []; |
| 287 | + const flatTexts: string[] = []; |
| 288 | + const noteIdToIdx = new Map<string, number>(); |
| 289 | + |
| 290 | + for (const note of allNoteSet.notes) { |
| 291 | + noteIdToIdx.set(note.noteId, notes.length); |
| 292 | + notes.push(note); |
| 293 | + flatTexts.push(note.getFlatText()); |
| 294 | + } |
| 295 | + |
| 296 | + this.flatTextIndex = { notes, flatTexts, noteIdToIdx }; |
| 297 | + this.dirtyFlatTextNoteIds.clear(); |
| 298 | + |
| 299 | + // Measure heap after building and log |
| 300 | + const heapAfter = process.memoryUsage().heapUsed; |
| 301 | + const heapDelta = heapAfter - heapBefore; |
| 302 | + log.info(`Flat text search index built: ${notes.length} notes, ${formatSize(heapDelta)}`); |
| 303 | + } else if (this.dirtyFlatTextNoteIds.size > 0) { |
| 304 | + // Incremental update: only recompute flat texts for dirtied notes |
| 305 | + const { flatTexts, noteIdToIdx } = this.flatTextIndex; |
| 306 | + |
| 307 | + for (const noteId of this.dirtyFlatTextNoteIds) { |
| 308 | + const idx = noteIdToIdx.get(noteId); |
| 309 | + if (idx !== undefined) { |
| 310 | + const note = this.notes[noteId]; |
| 311 | + if (note) { |
| 312 | + flatTexts[idx] = note.getFlatText(); |
| 313 | + } |
| 314 | + } |
| 315 | + } |
| 316 | + |
| 317 | + this.dirtyFlatTextNoteIds.clear(); |
| 318 | + } |
| 319 | + |
| 320 | + return this.flatTextIndex; |
245 | 321 | } |
246 | 322 |
|
247 | 323 | getAllNoteSet() { |
|
0 commit comments