From 97bbb88510e231f080a76488a6abfda1526c7155 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 01:15:03 +0000 Subject: [PATCH 1/3] perf(search): eliminate N+1 queries during index initialization Co-authored-by: d-oit <6849456+d-oit@users.noreply.github.com> --- src/lib/search/progressive.ts | 4 +++- vitest.config.ts | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/search/progressive.ts b/src/lib/search/progressive.ts index d5d5ac93..8e4a8a09 100644 --- a/src/lib/search/progressive.ts +++ b/src/lib/search/progressive.ts @@ -82,6 +82,7 @@ export const initSearch = async () => { let totalEntitiesIndexed = 0; let hasMore = true; let fetchOffset = 0; + const entityNames = new Map(); while (hasMore) { const chunk = await repository.getAllEntities({ limit: CHUNK_SIZE, offset: fetchOffset }); @@ -94,6 +95,7 @@ export const initSearch = async () => { const originalIds: string[] = []; for (const entity of chunk) { + entityNames.set(entity.id!, entity.name); docs.push(buildEntityDoc(entity)); originalIds.push(entity.id!); @@ -126,7 +128,7 @@ export const initSearch = async () => { for (const note of allNotes) { if (note.content && note.content.trim().length > 0) { const entityName = note.entity_id - ? (await repository.getEntityById(note.entity_id))?.name + ? entityNames.get(note.entity_id) : undefined; noteDocs.push(buildNoteDoc(note, entityName)); noteIds.push(note.id); diff --git a/vitest.config.ts b/vitest.config.ts index ff94087b..42896b2e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -19,7 +19,6 @@ export default defineConfig({ 'src/features/ai/__tests__/useChat.rateLimit.test.ts', 'src/features/ai/__tests__/useRateLimiter.test.ts', 'src/features/search/__tests__/SearchPanel.createEntity.test.tsx', - 'src/lib/search/__tests__/progressive.test.ts', ], // Limit workers to prevent OOM in restricted CI environments pool: 'forks', From bb7eeee416dbd0fd5c4e7583a68fc599823445b7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 01:20:57 +0000 Subject: [PATCH 2/3] perf(search): eliminate N+1 queries during index initialization Optimized the `initSearch` function in `src/lib/search/progressive.ts` by replacing per-note asynchronous entity name lookups with a synchronous Map lookup. This eliminates N+1 database roundtrips during search indexing. Also restored the exclusion of `src/lib/search/__tests__/progressive.test.ts` in `vitest.config.ts` to maintain CI stability. Co-authored-by: d-oit <6849456+d-oit@users.noreply.github.com> --- vitest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vitest.config.ts b/vitest.config.ts index 42896b2e..ff94087b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -19,6 +19,7 @@ export default defineConfig({ 'src/features/ai/__tests__/useChat.rateLimit.test.ts', 'src/features/ai/__tests__/useRateLimiter.test.ts', 'src/features/search/__tests__/SearchPanel.createEntity.test.tsx', + 'src/lib/search/__tests__/progressive.test.ts', ], // Limit workers to prevent OOM in restricted CI environments pool: 'forks', From dc08ab0cb612ff22c0cbca2132465ff719e5d5dc Mon Sep 17 00:00:00 2001 From: d-oit Date: Fri, 3 Jul 2026 10:27:04 +0200 Subject: [PATCH 3/3] fix(search): replace non-null assertions with proper null guards in progressive.ts DeepSource flagged entity.id! and claim.id! as forbidden non-null assertions. Replace with type guards and early-continue pattern to eliminate runtime risk. - Add id guards in initSearch chunk loop - Add id guard in addEntityToIndex - Add id guard in removeEntityFromIndex claim loop - Narrow buildEntityDoc/buildClaimDoc types to require id field --- src/lib/search/progressive.ts | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/lib/search/progressive.ts b/src/lib/search/progressive.ts index 8e4a8a09..1f6841f8 100644 --- a/src/lib/search/progressive.ts +++ b/src/lib/search/progressive.ts @@ -24,16 +24,16 @@ interface SearchDocument { embedding?: number[]; } -const buildEntityDoc = (entity: Entity): SearchDocument => ({ - id: entity.id!, +const buildEntityDoc = (entity: Entity & { id: string }): SearchDocument => ({ + id: entity.id, type: 'entity', title: entity.name, content: compressText(`${entity.name} ${entity.description || ''}`), keywords: entity.type, }); -const buildClaimDoc = (claim: Claim, entityName: string, entityId: string): SearchDocument => ({ - id: claim.id!, +const buildClaimDoc = (claim: Claim & { id: string }, entityName: string, entityId: string): SearchDocument => ({ + id: claim.id, type: 'claim', title: entityName, content: compressText(claim.statement), @@ -50,15 +50,17 @@ const buildNoteDoc = (note: { id: string; content: string; entity_id?: string | const addEntityToIndex = async (entity: Entity, claims: Claim[]): Promise => { if (!oramaDb) return; + if (!entity.id) return; const entityResult = await insert(oramaDb, buildEntityDoc(entity)); - addToOramaMap(entity.id!, entityResult); + addToOramaMap(entity.id, entityResult); if (claims.length > 0) { - const claimDocs = claims.map(c => buildClaimDoc(c, entity.name, entity.id!)); + const claimsWithId = claims.filter((c): c is Claim & { id: string } => Boolean(c.id)); + const claimDocs = claimsWithId.map(c => buildClaimDoc(c, entity.name, entity.id as string)); const claimOramaIds = await insertMultiple(oramaDb, claimDocs); - for (let i = 0; i < claims.length; i++) { - addToOramaMap(claims[i].id!, claimOramaIds[i]); + for (let i = 0; i < claimsWithId.length; i++) { + addToOramaMap(claimsWithId[i].id, claimOramaIds[i]); } } }; @@ -95,14 +97,16 @@ export const initSearch = async () => { const originalIds: string[] = []; for (const entity of chunk) { - entityNames.set(entity.id!, entity.name); + if (!entity.id) continue; + entityNames.set(entity.id, entity.name); docs.push(buildEntityDoc(entity)); - originalIds.push(entity.id!); + originalIds.push(entity.id); - const claims = claimsByEntity.get(entity.id!) || []; + const claims = claimsByEntity.get(entity.id) || []; for (const claim of claims) { - docs.push(buildClaimDoc(claim, entity.name, entity.id!)); - originalIds.push(claim.id!); + if (!claim.id) continue; + docs.push(buildClaimDoc(claim, entity.name, entity.id)); + originalIds.push(claim.id); } } @@ -265,10 +269,11 @@ export const removeFromSearchIndex = async ( const claims = providedClaims ?? await repository.getClaimsByEntityId(entityId); for (const claim of claims) { - const claimOramaId = oramaIdMap.get(claim.id!); + if (!claim.id) continue; + const claimOramaId = oramaIdMap.get(claim.id); if (claimOramaId) { oramaRemovals.push(remove(oramaDb, claimOramaId)); - oramaIdMap.delete(claim.id!); + oramaIdMap.delete(claim.id); } }