mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-10 15:42:01 +03:00
perf(search): optimize case-insensitive string search
Replace `.split("\n")` and `.toLowerCase()` loop with a highly optimized
`RegExp` iteration using `indexOf` for fast line number counting.
This significantly reduces memory allocations and CPU cycles when searching
large numbers of lines in the workspace.
Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
parent
868a34497b
commit
e87bc6de1d
2 changed files with 110 additions and 16 deletions
45
benchmark/search_benchmark.ts
Normal file
45
benchmark/search_benchmark.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { searchAllSheets } from "../src/ui/search.js";
|
||||
import { SumEngine } from "../src/engine/index.js";
|
||||
import { Workspace } from "../src/workspace.js";
|
||||
import Decimal from "decimal.js";
|
||||
import { performance } from "perf_hooks";
|
||||
|
||||
function generateDummyDoc(id: string, title: string, numLines: number): any {
|
||||
const lines = [];
|
||||
for (let i = 0; i < numLines; i++) {
|
||||
lines.push(`Line ${i}: this is some dummy text to search. The quick brown fox jumps over the lazy dog.`);
|
||||
}
|
||||
lines.push(`Line target: looking for SPECIFIC_WORD here.`);
|
||||
for (let i = 0; i < numLines; i++) {
|
||||
lines.push(`Line ${i + numLines + 1}: more random text. A journey of a thousand miles begins with a single step.`);
|
||||
}
|
||||
return { id, title, text: lines.join("\n") };
|
||||
}
|
||||
|
||||
const docs = [];
|
||||
for (let i = 0; i < 500; i++) { // Increase size to 1M lines
|
||||
docs.push(generateDummyDoc(`doc_${i}`, `Document ${i}`, 1000)); // ~2000 lines per doc, 500 docs = 1,000,000 lines
|
||||
}
|
||||
|
||||
const engine = new SumEngine();
|
||||
const workspace = new Workspace(engine);
|
||||
|
||||
const deps = {
|
||||
engine,
|
||||
workspace,
|
||||
docs: () => docs,
|
||||
t: (key: string) => key,
|
||||
onOpen: () => {}
|
||||
};
|
||||
|
||||
console.log(`Starting benchmark with ${docs.length} documents...`);
|
||||
|
||||
const start1 = performance.now();
|
||||
const res1 = searchAllSheets(deps, "specific_word");
|
||||
const end1 = performance.now();
|
||||
console.log(`Baseline Search (specific_word): ${(end1 - start1).toFixed(2)}ms, Hits: ${res1.length}`);
|
||||
|
||||
const start2 = performance.now();
|
||||
const res2 = searchAllSheets(deps, "not_found_word_that_is_very_long");
|
||||
const end2 = performance.now();
|
||||
console.log(`Baseline Search (not_found_word): ${(end2 - start2).toFixed(2)}ms, Hits: ${res2.length}`);
|
||||
|
|
@ -30,9 +30,13 @@ export interface SearchController {
|
|||
open(): void;
|
||||
}
|
||||
|
||||
const $ = <T extends HTMLElement>(sel: string): T => document.querySelector(sel) as T;
|
||||
const $ = <T extends HTMLElement>(sel: string): T =>
|
||||
document.querySelector(sel) as T;
|
||||
|
||||
export function parseResultQuery(engine: SumEngine, q: string): { op: string; threshold: Value } | null {
|
||||
export function parseResultQuery(
|
||||
engine: SumEngine,
|
||||
q: string,
|
||||
): { op: string; threshold: Value } | null {
|
||||
const m = /^(>=|<=|>|<|=|~)\s*(.+)$/.exec(q.trim());
|
||||
if (!m) return null;
|
||||
const v = engine.evaluateExpression(m[2].trim());
|
||||
|
|
@ -57,30 +61,73 @@ export function searchAllSheets(deps: SearchDeps, query: string): SearchHit[] {
|
|||
const v = r.value.value;
|
||||
let match = false;
|
||||
switch (rq.op) {
|
||||
case ">": match = v.gt(th); break;
|
||||
case ">=": match = v.gte(th); break;
|
||||
case "<": match = v.lt(th); break;
|
||||
case "<=": match = v.lte(th); break;
|
||||
case "=": match = v.eq(th); break;
|
||||
case "~": match = !th.isZero() && v.minus(th).abs().div(th.abs()).lte(0.01); break;
|
||||
case ">":
|
||||
match = v.gt(th);
|
||||
break;
|
||||
case ">=":
|
||||
match = v.gte(th);
|
||||
break;
|
||||
case "<":
|
||||
match = v.lt(th);
|
||||
break;
|
||||
case "<=":
|
||||
match = v.lte(th);
|
||||
break;
|
||||
case "=":
|
||||
match = v.eq(th);
|
||||
break;
|
||||
case "~":
|
||||
match = !th.isZero() && v.minus(th).abs().div(th.abs()).lte(0.01);
|
||||
break;
|
||||
}
|
||||
if (match) {
|
||||
hits.push({ docId: doc.id, docTitle: doc.title, line: i + 1, text: lines[i] ?? "", result: r.text ?? undefined });
|
||||
hits.push({
|
||||
docId: doc.id,
|
||||
docTitle: doc.title,
|
||||
line: i + 1,
|
||||
text: lines[i] ?? "",
|
||||
result: r.text ?? undefined,
|
||||
});
|
||||
if (hits.length >= 200) return hits;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
const ql = q.toLowerCase();
|
||||
const hits: SearchHit[] = [];
|
||||
const escapedQ = q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp(escapedQ, "ig");
|
||||
|
||||
for (const doc of docs) {
|
||||
const lines = doc.text.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].toLowerCase().includes(ql)) {
|
||||
hits.push({ docId: doc.id, docTitle: doc.title, line: i + 1, text: lines[i] });
|
||||
if (hits.length >= 200) return hits;
|
||||
const text = doc.text;
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
let lineNo = 1;
|
||||
|
||||
regex.lastIndex = 0;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
let nIdx = text.indexOf("\n", lastIndex);
|
||||
while (nIdx !== -1 && nIdx < match.index) {
|
||||
lineNo++;
|
||||
nIdx = text.indexOf("\n", nIdx + 1);
|
||||
}
|
||||
|
||||
let lineStart = text.lastIndexOf("\n", match.index);
|
||||
lineStart = lineStart === -1 ? 0 : lineStart + 1;
|
||||
let lineEnd = text.indexOf("\n", match.index);
|
||||
if (lineEnd === -1) lineEnd = text.length;
|
||||
|
||||
hits.push({
|
||||
docId: doc.id,
|
||||
docTitle: doc.title,
|
||||
line: lineNo,
|
||||
text: text.substring(lineStart, lineEnd),
|
||||
});
|
||||
if (hits.length >= 200) return hits;
|
||||
|
||||
regex.lastIndex = lineEnd;
|
||||
lastIndex = lineEnd;
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
|
|
@ -113,7 +160,9 @@ export function initSearch(deps: SearchDeps): SearchController {
|
|||
docEl.textContent = hit.docTitle;
|
||||
const lineEl = document.createElement("div");
|
||||
lineEl.className = "line";
|
||||
lineEl.textContent = hit.result ? `${hit.text} = ${hit.result}` : hit.text;
|
||||
lineEl.textContent = hit.result
|
||||
? `${hit.text} = ${hit.result}`
|
||||
: hit.text;
|
||||
item.append(docEl, lineEl);
|
||||
item.addEventListener("click", () => {
|
||||
deps.onOpen(hit.docId, hit.line);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue