Skip to content

Commit 4421383

Browse files
committed
Add general search benchmark script
1 parent 8e4a8e2 commit 4421383

File tree

3 files changed

+142
-1
lines changed

3 files changed

+142
-1
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,9 @@ Benchmark (Node):
294294
- Run `npm run benchmark:snapshot` to compare fresh build vs snapshot load (defaults to 1k, 10k and 30k items). Override sizes with `SIZES=5000,20000 npm run benchmark:snapshot`.
295295
- Output includes cold-start speedup ratio (build/load). Note: real-world cost in browser also includes `fetch` + `JSON.parse` time if you download the snapshot.
296296

297-
Browser smoke test (manual/optional):
297+
Search benchmark (Node):
298+
- Run `npm run benchmark:search` to measure build/search/facets timings across scenarios (empty, query-only, filters-only, query+filters, boolean filter). Defaults to sizes 1k/10k/30k; override with `SIZES=5000,20000` and repeats with `REPEAT=10`.
299+
300+
Browser smoke test (manual/optional):
298301
- Build the bundle: `npm run build`.
299302
- EITHER open `benchmarks/browser-snapshot.html` directly in a browser, OR run `npm run serve:benchmark` and open `http://localhost:4173/` (auto-loads the snapshot page). It builds once, saves a snapshot to `localStorage`, and on refresh loads from it and logs a sample search.

benchmarks/search.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import itemsjs from '../src/index.js';
2+
import { performance } from 'node:perf_hooks';
3+
4+
const defaultSizes = [1000, 10000, 30000];
5+
const sizes = process.env.SIZES
6+
? process.env.SIZES.split(',').map((v) => parseInt(v, 10)).filter(Boolean)
7+
: defaultSizes;
8+
9+
const repeats = parseInt(process.env.REPEAT || '5', 10);
10+
11+
const tagsPool = Array.from({ length: 40 }, (_, i) => `tag${i}`);
12+
const actorsPool = Array.from({ length: 30 }, (_, i) => `actor${i}`);
13+
const categories = ['catA', 'catB', 'catC', 'catD'];
14+
15+
function makeItems(count) {
16+
return Array.from({ length: count }, (_, i) => {
17+
const t1 = tagsPool[i % tagsPool.length];
18+
const t2 = tagsPool[(i * 7) % tagsPool.length];
19+
const t3 = tagsPool[(i * 11) % tagsPool.length];
20+
const actor = actorsPool[i % actorsPool.length];
21+
const category = categories[i % categories.length];
22+
const popular = i % 2 === 0;
23+
24+
return {
25+
id: `id-${i}`,
26+
name: `Item ${i} ${t1}`,
27+
tags: [t1, t2, t3],
28+
actors: [actor],
29+
category,
30+
popular,
31+
};
32+
});
33+
}
34+
35+
function average(arr) {
36+
if (!arr.length) return 0;
37+
return arr.reduce((a, b) => a + b, 0) / arr.length;
38+
}
39+
40+
function runScenario(engine, input) {
41+
const totals = [];
42+
const facets = [];
43+
const searchTimes = [];
44+
const sortingTimes = [];
45+
46+
for (let i = 0; i < repeats; i++) {
47+
const start = performance.now();
48+
const res = engine.search(input);
49+
const end = performance.now();
50+
51+
totals.push(end - start);
52+
facets.push(res.timings?.facets ?? 0);
53+
searchTimes.push(res.timings?.search ?? 0);
54+
sortingTimes.push(res.timings?.sorting ?? 0);
55+
}
56+
57+
return {
58+
totalMs: average(totals),
59+
facetsMs: average(facets),
60+
searchMs: average(searchTimes),
61+
sortingMs: average(sortingTimes),
62+
};
63+
}
64+
65+
function logResult(size, buildMs, results) {
66+
console.log(`items: ${size}`);
67+
console.log(` build (ms): ${buildMs.toFixed(1)}`);
68+
Object.entries(results).forEach(([name, data]) => {
69+
console.log(
70+
` ${name}: total=${data.totalMs.toFixed(2)}ms facets=${data.facetsMs.toFixed(
71+
2,
72+
)}ms search=${data.searchMs.toFixed(2)}ms sorting=${data.sortingMs.toFixed(2)}ms`,
73+
);
74+
});
75+
console.log('');
76+
}
77+
78+
function main() {
79+
console.log(
80+
`Search benchmark – sizes: ${sizes.join(
81+
', ',
82+
)}, repeats per scenario: ${repeats}`,
83+
);
84+
console.log(
85+
'Scenarios: empty, query-only, filters-only, query+filters, boolean filter',
86+
);
87+
console.log('');
88+
89+
sizes.forEach((size) => {
90+
const data = makeItems(size);
91+
const config = {
92+
searchableFields: ['name', 'tags', 'actors'],
93+
aggregations: {
94+
tags: { title: 'Tags', size: tagsPool.length },
95+
actors: { title: 'Actors', size: actorsPool.length },
96+
category: { title: 'Category', size: categories.length },
97+
popular: { title: 'Popular' },
98+
},
99+
};
100+
101+
const buildStart = performance.now();
102+
const engine = itemsjs(data, config);
103+
const buildEnd = performance.now();
104+
105+
const scenarios = {
106+
empty: {},
107+
query: { query: tagsPool[1] },
108+
filters: {
109+
filters: {
110+
tags: [tagsPool[2]],
111+
category: [categories[1]],
112+
},
113+
},
114+
queryAndFilters: {
115+
query: tagsPool[3],
116+
filters: {
117+
tags: [tagsPool[3]],
118+
actors: [actorsPool[2]],
119+
},
120+
},
121+
booleanFilter: {
122+
filters: {
123+
popular: [true],
124+
},
125+
},
126+
};
127+
128+
const results = {};
129+
Object.entries(scenarios).forEach(([name, input]) => {
130+
results[name] = runScenario(engine, input);
131+
});
132+
133+
logResult(size, buildEnd - buildStart, results);
134+
});
135+
}
136+
137+
main();

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"lint": "eslint \"**/*.js\" --ext js",
99
"lint:fix": "eslint \"**/*.js\" --ext js --fix",
1010
"benchmark:snapshot": "node benchmarks/snapshot.js",
11+
"benchmark:search": "node benchmarks/search.js",
1112
"serve:benchmark": "node scripts/serve-benchmark.js",
1213
"prepublishOnly": "npm run build",
1314
"build": "microbundle",

0 commit comments

Comments
 (0)