Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Jules/bolt.md
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 43 additions & 19 deletions frontend/src/erd/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,65 @@ 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<TableNodeData>, 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<TableNodeData>): 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<TableNodeData>,
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(
nodes: Array<Node<TableNodeData>>,
search: string,
): Set<string> {
const matches = new Set<string>();
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);
}
}
Expand Down
Loading