diff --git a/.Jules/bolt.md b/.Jules/bolt.md index 9bb054fe..89e61cda 100644 --- a/.Jules/bolt.md +++ b/.Jules/bolt.md @@ -1,3 +1,6 @@ ## 2025-06-27 - [Map Initialization Overhead] **Learning:** Initializing Maps with `new Map(array.map(...))` creates unnecessary intermediate arrays, consuming memory and triggering garbage collection overhead, especially noticeable when dealing with many nodes. **Action:** Use a `for...of` loop to directly `map.set()` elements rather than creating an intermediate array of tuples, especially in frequently executed or rendering paths. +## 2024-05-18 - Optimize string search logic for node properties +**Learning:** Checking multiple fields for string match across a large React array by looping over elements and doing repeated `toLowerCase()` for each element creates significant garbage collection pressure and is a major bottleneck on large datasets. +**Action:** When implementing high-frequency search or filtering operations across large collections of ERD nodes in the frontend, prefer direct string concatenation to consolidate all searchable fields into a single pre-lowercased string per node. Using substring matching on this single string prevents severe garbage collection pressure compared to array methods (`flatMap`, `join`) and avoids the overhead of iteratively lowercasing individual properties. diff --git a/frontend/src/erd/search.ts b/frontend/src/erd/search.ts index 7e169562..6b854c72 100644 --- a/frontend/src/erd/search.ts +++ b/frontend/src/erd/search.ts @@ -2,32 +2,41 @@ import type { Node } from "@xyflow/react"; import type { TableNodeData } from "./convert"; -function fieldIncludes(value: string | null | undefined, term: string): boolean { - return Boolean(value && value.toLocaleLowerCase().includes(term)); -} - -function nodeIncludesTerm(node: Node, term: string): boolean { - if (fieldIncludes(node.data.title, term)) return true; - if (fieldIncludes(node.data.comment, term)) return true; - - for (const column of node.data.columns) { - if (fieldIncludes(column.column_name, term)) return true; - if (fieldIncludes(column.data_type, term)) return true; - if (fieldIncludes(column.column_comment, term)) return true; +// ⚡ Bolt: Consolidate searchable text into a single string per node to avoid redundant +// .toLocaleLowerCase() calls and enable fast substring matching via indexOf. +function nodeSearchText(node: Node): string { + let text = ""; + if (node.data.title) text += node.data.title + " "; + if (node.data.comment) text += node.data.comment + " "; + for (let i = 0; i < node.data.columns.length; i++) { + const col = node.data.columns[i]; + if (col.column_name) text += col.column_name + " "; + if (col.data_type) text += col.data_type + " "; + if (col.column_comment) text += col.column_comment + " "; } + return text.toLocaleLowerCase(); +} - return false; +function getSearchTerms(search: string): string[] { + return Array.from( + new Set(search.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean)), + ); } export function tableNodeMatchesSearch( node: Node, search: string, ): boolean { - const terms = Array.from( - new Set(search.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean)), - ); + const terms = getSearchTerms(search); if (terms.length === 0) return false; - return terms.every((term) => nodeIncludesTerm(node, term)); + + const searchText = nodeSearchText(node); + for (let i = 0; i < terms.length; i++) { + if (searchText.indexOf(terms[i]) === -1) { + return false; + } + } + return true; } export function findSearchMatchedNodeIds( @@ -35,8 +44,23 @@ export function findSearchMatchedNodeIds( search: string, ): Set { const matches = new Set(); - for (const node of nodes) { - if (tableNodeMatchesSearch(node, search)) { + + // ⚡ Bolt: Parse search terms exactly once before iterating over nodes, + // replacing O(N*M) redundant string parsing overhead with a single O(M) operation. + const terms = getSearchTerms(search); + if (terms.length === 0) return matches; + + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + const searchText = nodeSearchText(node); + let matched = true; + for (let j = 0; j < terms.length; j++) { + if (searchText.indexOf(terms[j]) === -1) { + matched = false; + break; + } + } + if (matched) { matches.add(node.id); } }