Skip to content

Commit 437808b

Browse files
authored
Benchmark search (#150)
* Add general search benchmark script * Add detailed benchmark docs and facet-heavy search benchmark
1 parent 8e4a8e2 commit 437808b

4 files changed

Lines changed: 195 additions & 7 deletions

File tree

README.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -290,10 +290,5 @@ APIs:
290290

291291
Snapshots are optional; if you don’t provide them, itemsjs rebuilds indexes as before.
292292

293-
Benchmark (Node):
294-
- 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`.
295-
- 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.
296-
297-
Browser smoke test (manual/optional):
298-
- Build the bundle: `npm run build`.
299-
- 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.
293+
Benchmarks and browser smoke test:
294+
- See `docs/benchmarks.md` for snapshot/search benchmarks and the optional browser smoke test.

benchmarks/search.js

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

docs/benchmarks.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Benchmarks
2+
3+
This folder contains small, reproducible benchmarks for ItemsJS. They are optional and not part of the published package.
4+
5+
## Snapshot benchmark
6+
7+
Script: `npm run benchmark:snapshot`
8+
9+
- Compares fresh index build vs loading from snapshot.
10+
- Defaults: sizes `1000,10000,30000`. Override with `SIZES=5000,20000 npm run benchmark:snapshot`.
11+
- Output includes cold-start speedup (build/load) and snapshot size.
12+
- Note: In the browser, total cost also includes `fetch + JSON.parse` if you download the snapshot.
13+
14+
## Search benchmark
15+
16+
Script: `npm run benchmark:search`
17+
18+
- Measures build/search/facets timings across scenarios: empty, query-only, filters-only, query+filters, boolean filter.
19+
- Defaults: sizes `1000,10000,30000`, repeats per scenario `5`.
20+
- Override: `SIZES=5000,20000`, `REPEAT=10`.
21+
- Dataset per size: 40 tags, 30 actors, 4 categories, boolean `popular`; each item has 3 tags, 1 actor, 1 category. Facets: tags, actors, category, popular. Searchable fields: name (boosted), tags, actors.
22+
- Stress facet-heavy setups with `EXTRA_FACETS=1000` (each with 3 values) to see scaling for many facets.
23+
24+
## Browser smoke test
25+
26+
- Build: `npm run build`.
27+
- Run: `npm run serve:benchmark` and open `http://localhost:4173/` (serves `benchmarks/browser-snapshot.html`), or open the HTML directly.
28+
- First load builds and stores a snapshot in `localStorage`; subsequent loads use the snapshot and log a sample search. Green message = OK; red/error or stuck on “Loading…” → check console.

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)