@arraypress/search 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/filter/address.d.ts +6 -0
- package/src/filter/address.js +48 -0
- package/src/filter/selection.d.ts +28 -0
- package/src/filter/selection.js +135 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ All notable changes to `@arraypress/search` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] — 2026-11-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **A filter panel's logic:** `facetTable` (each record's values per facet, worked out once), `narrow` (any picked value within a facet, every facet with a pick), `facetCounts` (each value's live count against the other facets' picks, so it is what ticking it would show), `toggle`, `pickCount`, `picks`, `sameSelection`, and `readSelection` / `writeSelection` for the selection in a page's address. From the Logomarks filter panel. `facets()` is unchanged: it counts values without a selection.
|
|
13
|
+
|
|
8
14
|
## [0.1.0] — 2026-11-05
|
|
9
15
|
|
|
10
16
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @arraypress/search
|
|
2
2
|
|
|
3
|
-
Ranked in-memory search with synonyms and facets. Field weights, synonym phrases (up to three words read as one), plural folding, word-start matching for what is typed and whole-word matching for synonyms, exact filters, stable tie-breaks, and a compact prebuilt index you build once and load in the browser. Plus rule-based tagging: weighted word rules with caps, hand picks and a review.
|
|
3
|
+
Ranked in-memory search with synonyms and facets. Field weights, synonym phrases (up to three words read as one), plural folding, word-start matching for what is typed and whole-word matching for synonyms, exact filters, a filter panel's facet logic (live counts, the selection in the address), stable tie-breaks, and a compact prebuilt index you build once and load in the browser. Plus rule-based tagging: weighted word rules with caps, hand picks and a review.
|
|
4
4
|
|
|
5
5
|
No dependencies. Runs in the browser and in Node. About 2.2 KB minified and gzipped; the rules are 1.1 KB more.
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arraypress/search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Ranked in-memory search with synonyms and facets: field weights, synonym phrases, plural folding, word-start and whole-word matching, exact filters, stable tie-breaks and a compact prebuilt JSON index. Plus rule-based tagging (weighted word rules, caps, picks and a review). No dependencies; browser and Node.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Selection } from './selection.js';
|
|
2
|
+
|
|
3
|
+
/** Writes the selection into a query (one comma-joined parameter per picked facet), keeping other parameters. */
|
|
4
|
+
export function writeSelection<F extends string>(params: URLSearchParams, ids: readonly F[], sel: Selection<F>, keys?: Partial<Record<F, string>>): URLSearchParams;
|
|
5
|
+
/** The selection a query asks for; unknown values and repeats dropped. */
|
|
6
|
+
export function readSelection<F extends string>(params: URLSearchParams, ids: readonly F[], known?: Partial<Record<F, ReadonlySet<string>>>, keys?: Partial<Record<F, string>>): Selection<F>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arraypress/search/filter/address
|
|
3
|
+
*
|
|
4
|
+
* A selection in a page's address: one query parameter per facet with a pick,
|
|
5
|
+
* its values joined by commas (`?style=iso,track&mood=calm`), and back.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Writes the selection into a query: one parameter per facet with a pick (its key from `keys`, else its id), the
|
|
10
|
+
* values joined by commas, in facet order. Other parameters are kept; a facet without a pick is removed.
|
|
11
|
+
*
|
|
12
|
+
* @param {URLSearchParams} params The query, changed in place.
|
|
13
|
+
* @param {readonly string[]} ids The facets, in order.
|
|
14
|
+
* @param {import('./selection.js').Selection} sel The selection.
|
|
15
|
+
* @param {Record<string, string>} [keys] A parameter name per facet, where it isn't the facet's id.
|
|
16
|
+
* @returns {URLSearchParams} The same query.
|
|
17
|
+
*/
|
|
18
|
+
export function writeSelection(params, ids, sel, keys = {}) {
|
|
19
|
+
for (const f of ids) {
|
|
20
|
+
const key = keys[f] ?? f;
|
|
21
|
+
const vals = sel[f];
|
|
22
|
+
if (vals?.length) params.set(key, vals.join(','));
|
|
23
|
+
else params.delete(key);
|
|
24
|
+
}
|
|
25
|
+
return params;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The selection a query asks for. Values a facet doesn't take (when `known` is given) are dropped, and repeats.
|
|
30
|
+
*
|
|
31
|
+
* @param {URLSearchParams} params The query.
|
|
32
|
+
* @param {readonly string[]} ids The facets.
|
|
33
|
+
* @param {Record<string, ReadonlySet<string>>} [known] The values each facet takes (a table's `known`).
|
|
34
|
+
* @param {Record<string, string>} [keys] A parameter name per facet, where it isn't the facet's id.
|
|
35
|
+
* @returns {import('./selection.js').Selection} The selection.
|
|
36
|
+
*/
|
|
37
|
+
export function readSelection(params, ids, known, keys = {}) {
|
|
38
|
+
/** @type {Record<string, string[]>} */
|
|
39
|
+
const sel = {};
|
|
40
|
+
for (const f of ids) {
|
|
41
|
+
const raw = params.get(keys[f] ?? f);
|
|
42
|
+
if (!raw) continue;
|
|
43
|
+
const ok = known?.[f];
|
|
44
|
+
const vals = [...new Set(raw.split(',').map((v) => v.trim()))].filter((v) => v && (!ok || ok.has(v)));
|
|
45
|
+
if (vals.length) sel[f] = vals;
|
|
46
|
+
}
|
|
47
|
+
return sel;
|
|
48
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The values picked in each facet; an absent or empty list means any. */
|
|
2
|
+
export type Selection<F extends string = string> = Partial<Record<F, readonly string[]>>;
|
|
3
|
+
|
|
4
|
+
/** Each record's values per facet: `values[k][i]` are record i's values in facet `ids[k]`. */
|
|
5
|
+
export interface FacetTable<F extends string = string> {
|
|
6
|
+
ids: readonly F[];
|
|
7
|
+
values: (readonly string[])[][];
|
|
8
|
+
/** Every value each facet takes, for checking a selection read from an address. */
|
|
9
|
+
known: Record<F, ReadonlySet<string>>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Live counts: per facet, each value's count among the records that pass every other facet. */
|
|
13
|
+
export type FacetCounts<F extends string = string> = Record<F, Map<string, number>>;
|
|
14
|
+
|
|
15
|
+
/** Each record's values in each facet, as a table. */
|
|
16
|
+
export function facetTable<T, F extends string>(records: readonly T[], valuesOf: Record<F, (record: T) => readonly string[]>): FacetTable<F>;
|
|
17
|
+
/** The records (indices, in the order given) that pass every facet with a pick. */
|
|
18
|
+
export function narrow<F extends string>(table: FacetTable<F>, candidates: Iterable<number>, sel: Selection<F>): number[];
|
|
19
|
+
/** Each facet's value counts among the candidates, each counted against the other facets' picks. */
|
|
20
|
+
export function facetCounts<F extends string>(table: FacetTable<F>, candidates: Iterable<number>, sel: Selection<F>): FacetCounts<F>;
|
|
21
|
+
/** The selection with one value ticked, unticked or (with `on`) forced. */
|
|
22
|
+
export function toggle<F extends string>(sel: Selection<F>, facet: F, value: string, on?: boolean): Selection<F>;
|
|
23
|
+
/** How many values are picked across every facet. */
|
|
24
|
+
export function pickCount(sel: Selection): number;
|
|
25
|
+
/** Every pick as [facet, value], in facet order then pick order. */
|
|
26
|
+
export function picks<F extends string>(ids: readonly F[], sel: Selection<F>): (readonly [F, string])[];
|
|
27
|
+
/** Whether two selections pick the same values, in any order. */
|
|
28
|
+
export function sameSelection(a: Selection, b: Selection): boolean;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arraypress/search/filter/selection
|
|
3
|
+
*
|
|
4
|
+
* A filter panel's logic over facets: each record's values per facet worked
|
|
5
|
+
* out once (a table), the records that pass a selection (any picked value
|
|
6
|
+
* within a facet, every facet with a pick), each value's live count (counted
|
|
7
|
+
* against the other facets' picks, so it is what ticking it would show), and
|
|
8
|
+
* small helpers for changing and comparing selections. Generic over facet ids.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Each record's values in each facet, as a table.
|
|
13
|
+
*
|
|
14
|
+
* @template T, F
|
|
15
|
+
* @param {readonly T[]} records The records.
|
|
16
|
+
* @param {Record<F & string, (record: T) => readonly string[]>} valuesOf Per facet, a record's values in it (none for none).
|
|
17
|
+
* @returns {import('./selection.js').FacetTable<F & string>} The table.
|
|
18
|
+
*/
|
|
19
|
+
export function facetTable(records, valuesOf) {
|
|
20
|
+
const ids = /** @type {(F & string)[]} */ (Object.keys(valuesOf));
|
|
21
|
+
const values = ids.map((f) => records.map((r) => valuesOf[f](r)));
|
|
22
|
+
const known = /** @type {Record<F & string, ReadonlySet<string>>} */ (Object.fromEntries(ids.map((f, k) => [f, new Set(values[k].flat())])));
|
|
23
|
+
return { ids, values, known };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The facets with a pick, as [table column, picked values].
|
|
28
|
+
*
|
|
29
|
+
* @param {import('./selection.js').FacetTable} table The table.
|
|
30
|
+
* @param {import('./selection.js').Selection} sel The selection.
|
|
31
|
+
* @returns {(readonly [number, Set<string>])[]} The picks.
|
|
32
|
+
*/
|
|
33
|
+
const picked = (table, sel) => table.ids.flatMap((f, k) => (sel[f]?.length ? [/** @type {const} */ ([k, new Set(sel[f])])] : []));
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether a record has any of the picked values.
|
|
37
|
+
*
|
|
38
|
+
* @param {readonly string[]} have The record's values.
|
|
39
|
+
* @param {ReadonlySet<string>} want The picked values.
|
|
40
|
+
* @returns {boolean} True when one matches.
|
|
41
|
+
*/
|
|
42
|
+
const hasAny = (have, want) => have.some((v) => want.has(v));
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The records (indices, in the order given) that pass every facet with a pick: any of its picked values.
|
|
46
|
+
*
|
|
47
|
+
* @param {import('./selection.js').FacetTable} table The table.
|
|
48
|
+
* @param {Iterable<number>} candidates The records to consider (a search's hits, or all).
|
|
49
|
+
* @param {import('./selection.js').Selection} sel The selection.
|
|
50
|
+
* @returns {number[]} The indices that pass.
|
|
51
|
+
*/
|
|
52
|
+
export function narrow(table, candidates, sel) {
|
|
53
|
+
const picks = picked(table, sel);
|
|
54
|
+
/** @type {number[]} */
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const i of candidates) if (picks.every(([k, want]) => hasAny(table.values[k][i], want))) out.push(i);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Each facet's value counts among the candidates: a value's count is how many candidates have it and pass every
|
|
62
|
+
* OTHER facet, so it is how many records ticking it would add (or, in a facet with no pick, show).
|
|
63
|
+
*
|
|
64
|
+
* @param {import('./selection.js').FacetTable} table The table.
|
|
65
|
+
* @param {Iterable<number>} candidates The records to count (a search's hits, or all).
|
|
66
|
+
* @param {import('./selection.js').Selection} sel The selection.
|
|
67
|
+
* @returns {Record<string, Map<string, number>>} Per facet, each value's count.
|
|
68
|
+
*/
|
|
69
|
+
export function facetCounts(table, candidates, sel) {
|
|
70
|
+
const picks = picked(table, sel);
|
|
71
|
+
const counts = table.ids.map(() => /** @type {Map<string, number>} */ (new Map()));
|
|
72
|
+
/** Counts record i's values in facet column k. */
|
|
73
|
+
const add = (/** @type {number} */ k, /** @type {number} */ i) => {
|
|
74
|
+
for (const v of table.values[k][i]) counts[k].set(v, (counts[k].get(v) ?? 0) + 1);
|
|
75
|
+
};
|
|
76
|
+
for (const i of candidates) {
|
|
77
|
+
// the facets it fails: none, it counts everywhere; one, it counts only there (ticking more of that facet adds it)
|
|
78
|
+
let failed = -1;
|
|
79
|
+
let fails = 0;
|
|
80
|
+
for (const [k, want] of picks)
|
|
81
|
+
if (!hasAny(table.values[k][i], want)) {
|
|
82
|
+
failed = k;
|
|
83
|
+
if (++fails > 1) break;
|
|
84
|
+
}
|
|
85
|
+
if (fails === 0) table.ids.forEach((_, k) => add(k, i));
|
|
86
|
+
else if (fails === 1) add(failed, i);
|
|
87
|
+
}
|
|
88
|
+
return Object.fromEntries(table.ids.map((f, k) => [f, counts[k]]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The selection with one value ticked or unticked.
|
|
93
|
+
*
|
|
94
|
+
* @param {import('./selection.js').Selection} sel The selection.
|
|
95
|
+
* @param {string} facet The facet.
|
|
96
|
+
* @param {string} value The value.
|
|
97
|
+
* @param {boolean} [on] Force it on or off; toggles when absent.
|
|
98
|
+
* @returns {import('./selection.js').Selection} A new selection.
|
|
99
|
+
*/
|
|
100
|
+
export function toggle(sel, facet, value, on) {
|
|
101
|
+
const have = sel[facet] ?? [];
|
|
102
|
+
const now = on ?? !have.includes(value);
|
|
103
|
+
const next = now ? (have.includes(value) ? have : [...have, value]) : have.filter((v) => v !== value);
|
|
104
|
+
return { ...sel, [facet]: next };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* How many values are picked across every facet.
|
|
109
|
+
*
|
|
110
|
+
* @param {import('./selection.js').Selection} sel The selection.
|
|
111
|
+
* @returns {number} The count.
|
|
112
|
+
*/
|
|
113
|
+
export const pickCount = (sel) => Object.values(sel).reduce((n, v) => n + (v?.length ?? 0), 0);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Every pick as [facet, value], in facet order then pick order (a row of chips).
|
|
117
|
+
*
|
|
118
|
+
* @param {readonly string[]} ids The facets, in order.
|
|
119
|
+
* @param {import('./selection.js').Selection} sel The selection.
|
|
120
|
+
* @returns {(readonly [string, string])[]} The picks.
|
|
121
|
+
*/
|
|
122
|
+
export const picks = (ids, sel) => ids.flatMap((f) => (sel[f] ?? []).map((v) => /** @type {const} */ ([f, v])));
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Whether two selections pick the same values, in any order (an empty list is no pick).
|
|
126
|
+
*
|
|
127
|
+
* @param {import('./selection.js').Selection} a One selection.
|
|
128
|
+
* @param {import('./selection.js').Selection} b The other.
|
|
129
|
+
* @returns {boolean} True when they match.
|
|
130
|
+
*/
|
|
131
|
+
export function sameSelection(a, b) {
|
|
132
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
133
|
+
for (const k of keys) if ([...(a[k] ?? [])].sort().join(',') !== [...(b[k] ?? [])].sort().join(',')) return false;
|
|
134
|
+
return true;
|
|
135
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export { pack, unpack, PACK_VERSION } from './pack/index.js';
|
|
|
5
5
|
export { facets } from './filter/facets.js';
|
|
6
6
|
export type { Filter, FilterValue } from './filter/filter.js';
|
|
7
7
|
export { filterOf } from './filter/filter.js';
|
|
8
|
+
export { facetTable, narrow, facetCounts, toggle, pickCount, picks, sameSelection } from './filter/selection.js';
|
|
9
|
+
export type { Selection, FacetTable, FacetCounts } from './filter/selection.js';
|
|
10
|
+
export { readSelection, writeSelection } from './filter/address.js';
|
|
8
11
|
export type { Tie } from './filter/tie.js';
|
|
9
12
|
export type { Term, TermOptions } from './query/terms.js';
|
|
10
13
|
export { termsOf, synonymMap } from './query/terms.js';
|
package/src/index.js
CHANGED
|
@@ -11,5 +11,7 @@ export { createSearch } from './search.js';
|
|
|
11
11
|
export { pack, unpack, PACK_VERSION } from './pack/index.js';
|
|
12
12
|
export { facets } from './filter/facets.js';
|
|
13
13
|
export { filterOf } from './filter/filter.js';
|
|
14
|
+
export { facetTable, narrow, facetCounts, toggle, pickCount, picks, sameSelection } from './filter/selection.js';
|
|
15
|
+
export { readSelection, writeSelection } from './filter/address.js';
|
|
14
16
|
export { termsOf, synonymMap } from './query/terms.js';
|
|
15
17
|
export { singular } from './query/plural.js';
|