From 4421383fa1779b06ce81c27a441f2feeabd293d3 Mon Sep 17 00:00:00 2001 From: Mateusz Rzepa Date: Sat, 22 Nov 2025 12:46:37 +0100 Subject: [PATCH 1/2] Add general search benchmark script --- README.md | 5 +- benchmarks/search.js | 137 +++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 benchmarks/search.js diff --git a/README.md b/README.md index dfed572..b3e4f31 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,9 @@ Benchmark (Node): - 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`. - 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. - Browser smoke test (manual/optional): +Search benchmark (Node): +- 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`. + +Browser smoke test (manual/optional): - Build the bundle: `npm run build`. - 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. diff --git a/benchmarks/search.js b/benchmarks/search.js new file mode 100644 index 0000000..c0dd2ff --- /dev/null +++ b/benchmarks/search.js @@ -0,0 +1,137 @@ +import itemsjs from '../src/index.js'; +import { performance } from 'node:perf_hooks'; + +const defaultSizes = [1000, 10000, 30000]; +const sizes = process.env.SIZES + ? process.env.SIZES.split(',').map((v) => parseInt(v, 10)).filter(Boolean) + : defaultSizes; + +const repeats = parseInt(process.env.REPEAT || '5', 10); + +const tagsPool = Array.from({ length: 40 }, (_, i) => `tag${i}`); +const actorsPool = Array.from({ length: 30 }, (_, i) => `actor${i}`); +const categories = ['catA', 'catB', 'catC', 'catD']; + +function makeItems(count) { + return Array.from({ length: count }, (_, i) => { + const t1 = tagsPool[i % tagsPool.length]; + const t2 = tagsPool[(i * 7) % tagsPool.length]; + const t3 = tagsPool[(i * 11) % tagsPool.length]; + const actor = actorsPool[i % actorsPool.length]; + const category = categories[i % categories.length]; + const popular = i % 2 === 0; + + return { + id: `id-${i}`, + name: `Item ${i} ${t1}`, + tags: [t1, t2, t3], + actors: [actor], + category, + popular, + }; + }); +} + +function average(arr) { + if (!arr.length) return 0; + return arr.reduce((a, b) => a + b, 0) / arr.length; +} + +function runScenario(engine, input) { + const totals = []; + const facets = []; + const searchTimes = []; + const sortingTimes = []; + + for (let i = 0; i < repeats; i++) { + const start = performance.now(); + const res = engine.search(input); + const end = performance.now(); + + totals.push(end - start); + facets.push(res.timings?.facets ?? 0); + searchTimes.push(res.timings?.search ?? 0); + sortingTimes.push(res.timings?.sorting ?? 0); + } + + return { + totalMs: average(totals), + facetsMs: average(facets), + searchMs: average(searchTimes), + sortingMs: average(sortingTimes), + }; +} + +function logResult(size, buildMs, results) { + console.log(`items: ${size}`); + console.log(` build (ms): ${buildMs.toFixed(1)}`); + Object.entries(results).forEach(([name, data]) => { + console.log( + ` ${name}: total=${data.totalMs.toFixed(2)}ms facets=${data.facetsMs.toFixed( + 2, + )}ms search=${data.searchMs.toFixed(2)}ms sorting=${data.sortingMs.toFixed(2)}ms`, + ); + }); + console.log(''); +} + +function main() { + console.log( + `Search benchmark – sizes: ${sizes.join( + ', ', + )}, repeats per scenario: ${repeats}`, + ); + console.log( + 'Scenarios: empty, query-only, filters-only, query+filters, boolean filter', + ); + console.log(''); + + sizes.forEach((size) => { + const data = makeItems(size); + const config = { + searchableFields: ['name', 'tags', 'actors'], + aggregations: { + tags: { title: 'Tags', size: tagsPool.length }, + actors: { title: 'Actors', size: actorsPool.length }, + category: { title: 'Category', size: categories.length }, + popular: { title: 'Popular' }, + }, + }; + + const buildStart = performance.now(); + const engine = itemsjs(data, config); + const buildEnd = performance.now(); + + const scenarios = { + empty: {}, + query: { query: tagsPool[1] }, + filters: { + filters: { + tags: [tagsPool[2]], + category: [categories[1]], + }, + }, + queryAndFilters: { + query: tagsPool[3], + filters: { + tags: [tagsPool[3]], + actors: [actorsPool[2]], + }, + }, + booleanFilter: { + filters: { + popular: [true], + }, + }, + }; + + const results = {}; + Object.entries(scenarios).forEach(([name, input]) => { + results[name] = runScenario(engine, input); + }); + + logResult(size, buildEnd - buildStart, results); + }); +} + +main(); diff --git a/package.json b/package.json index ecbdcfb..b0a0ce0 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "lint": "eslint \"**/*.js\" --ext js", "lint:fix": "eslint \"**/*.js\" --ext js --fix", "benchmark:snapshot": "node benchmarks/snapshot.js", + "benchmark:search": "node benchmarks/search.js", "serve:benchmark": "node scripts/serve-benchmark.js", "prepublishOnly": "npm run build", "build": "microbundle", From 9bd43561c87366b8b463e9413e8bafeb91b7ee3c Mon Sep 17 00:00:00 2001 From: Mateusz Rzepa Date: Sat, 22 Nov 2025 12:54:49 +0100 Subject: [PATCH 2/2] Add detailed benchmark docs and facet-heavy search benchmark --- README.md | 12 ++---------- benchmarks/search.js | 27 +++++++++++++++++++++++++++ docs/benchmarks.md | 28 ++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 docs/benchmarks.md diff --git a/README.md b/README.md index b3e4f31..57c0120 100644 --- a/README.md +++ b/README.md @@ -290,13 +290,5 @@ APIs: Snapshots are optional; if you don’t provide them, itemsjs rebuilds indexes as before. -Benchmark (Node): -- 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`. -- 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. - -Search benchmark (Node): -- 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`. - -Browser smoke test (manual/optional): -- Build the bundle: `npm run build`. -- 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 and browser smoke test: +- See `docs/benchmarks.md` for snapshot/search benchmarks and the optional browser smoke test. diff --git a/benchmarks/search.js b/benchmarks/search.js index c0dd2ff..dd27540 100644 --- a/benchmarks/search.js +++ b/benchmarks/search.js @@ -7,6 +7,8 @@ const sizes = process.env.SIZES : defaultSizes; const repeats = parseInt(process.env.REPEAT || '5', 10); +const extraFacetsCount = parseInt(process.env.EXTRA_FACETS || '0', 10); +const extraFacetValues = ['a', 'b', 'c']; const tagsPool = Array.from({ length: 40 }, (_, i) => `tag${i}`); const actorsPool = Array.from({ length: 30 }, (_, i) => `actor${i}`); @@ -28,10 +30,20 @@ function makeItems(count) { actors: [actor], category, popular, + ...makeExtraFacets(i), }; }); } +function makeExtraFacets(index) { + const result = {}; + for (let j = 0; j < extraFacetsCount; j++) { + const val = extraFacetValues[(index + j) % extraFacetValues.length]; + result[`facet_${j}`] = val; + } + return result; +} + function average(arr) { if (!arr.length) return 0; return arr.reduce((a, b) => a + b, 0) / arr.length; @@ -64,6 +76,15 @@ function runScenario(engine, input) { function logResult(size, buildMs, results) { console.log(`items: ${size}`); + console.log( + ` facets: tags(${tagsPool.length}), actors(${actorsPool.length}), category(${categories.length}), popular(boolean)`, + ); + if (extraFacetsCount > 0) { + console.log(` extra facets: ${extraFacetsCount} (3 values each)`); + } + console.log( + ' fields: name (boosted), tags, actors; each item has 3 tags, 1 actor, 1 category, boolean popular', + ); console.log(` build (ms): ${buildMs.toFixed(1)}`); Object.entries(results).forEach(([name, data]) => { console.log( @@ -98,6 +119,12 @@ function main() { }, }; + if (extraFacetsCount > 0) { + for (let i = 0; i < extraFacetsCount; i++) { + config.aggregations[`facet_${i}`] = { title: `Facet ${i}` }; + } + } + const buildStart = performance.now(); const engine = itemsjs(data, config); const buildEnd = performance.now(); diff --git a/docs/benchmarks.md b/docs/benchmarks.md new file mode 100644 index 0000000..63f5d4b --- /dev/null +++ b/docs/benchmarks.md @@ -0,0 +1,28 @@ +# Benchmarks + +This folder contains small, reproducible benchmarks for ItemsJS. They are optional and not part of the published package. + +## Snapshot benchmark + +Script: `npm run benchmark:snapshot` + +- Compares fresh index build vs loading from snapshot. +- Defaults: sizes `1000,10000,30000`. Override with `SIZES=5000,20000 npm run benchmark:snapshot`. +- Output includes cold-start speedup (build/load) and snapshot size. +- Note: In the browser, total cost also includes `fetch + JSON.parse` if you download the snapshot. + +## Search benchmark + +Script: `npm run benchmark:search` + +- Measures build/search/facets timings across scenarios: empty, query-only, filters-only, query+filters, boolean filter. +- Defaults: sizes `1000,10000,30000`, repeats per scenario `5`. +- Override: `SIZES=5000,20000`, `REPEAT=10`. +- 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. +- Stress facet-heavy setups with `EXTRA_FACETS=1000` (each with 3 values) to see scaling for many facets. + +## Browser smoke test + +- Build: `npm run build`. +- Run: `npm run serve:benchmark` and open `http://localhost:4173/` (serves `benchmarks/browser-snapshot.html`), or open the HTML directly. +- 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.