Skip to content
Merged
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
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +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.

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.
164 changes: 164 additions & 0 deletions benchmarks/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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 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}`);
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,
...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;
}

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(
` 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(
` ${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' },
},
};

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();

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();
28 changes: 28 additions & 0 deletions docs/benchmarks.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down