Skip to content
Open
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
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@ Optimized metric route processing to O(N) by creating a mapping of routes direct
## 2024-07-13 - [Optimize Export Dictionary FK lookups]
**Learning:** Found O(N * C * E) performance bottleneck in ERD export dictionaries due to repeated array searching with `edges.some()` inside a nested loop over nodes and columns.
**Action:** Replace repeated linear array scans for edges by precomputing O(1) Set lookups of foreign key column handles per node before looping.

## 2024-08-25 - [Search Performance] 노드 텍스트 통합 및 WeakMap 캐싱을 통한 ERD 검색 최적화
**Learning:** 다수의 문자열 필드를 개별적으로 `.toLocaleLowerCase()` 호출하여 검색하면, 검색어 입력마다 수많은 문자열 객체가 생성되어 GC 부하와 프레임 드랍이 발생합니다. V8의 단일 문자열 탐색이 JS 루프보다 훨씬 빠릅니다.
**Action:** 노드의 모든 검색 대상 텍스트를 하나의 문자열로 결합한 뒤 소문자로 변환하고, 이를 안정적인 `node.data` 객체를 키로 하는 `WeakMap`에 캐싱하여 O(1) 조회로 최적화해야 합니다.
30 changes: 18 additions & 12 deletions frontend/src/erd/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@ 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));
}
const nodeSearchTextCache = new WeakMap<TableNodeData, string>();

function getNodeSearchText(node: Node<TableNodeData>): string {
let text = nodeSearchTextCache.get(node.data);
if (text !== undefined) return text;

function nodeIncludesTerm(node: Node<TableNodeData>, term: string): boolean {
if (fieldIncludes(node.data.title, term)) return true;
if (fieldIncludes(node.data.comment, term)) return true;
const parts: string[] = [];
if (node.data.title) parts.push(node.data.title);
if (node.data.comment) parts.push(node.data.comment);

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;
for (const col of node.data.columns) {
if (col.column_name) parts.push(col.column_name);
if (col.data_type) parts.push(col.data_type);
if (col.column_comment) parts.push(col.column_comment);
}

return false;
text = parts.join(" ").toLocaleLowerCase();
nodeSearchTextCache.set(node.data, text);
return text;
}
Comment on lines +5 to 24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Search-text cache cannot go stale

nodeSearchTextCache is keyed on node.data. Every content edit in App.tsx builds a new data object via ...node.data spread, so the key changes whenever searchable text changes. Drag updates keep the same reference, yielding a correct cache hit.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


export function tableNodeMatchesSearch(
Expand All @@ -29,7 +33,9 @@ export function tableNodeMatchesSearch(
new Set(search.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean)),
);
if (terms.length === 0) return false;
return terms.every((term) => nodeIncludesTerm(node, term));

const nodeText = getNodeSearchText(node);
return terms.every((term) => nodeText.includes(term));
Comment on lines 33 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Combined-haystack search matches per-field results

Search now joins all node fields with a space and calls .includes instead of testing fields individually. Since terms are split on \s+ and the join separator is a space, no term can span a field boundary, so results stay identical to the old per-field logic.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}

export function findSearchMatchedNodeIds(
Expand Down
Loading