@datakable/core 2.0.2 → 2.1.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/dist/{codes-AX8XkMNY.d.cts → codes-4sYQv_7J.d.cts} +36 -10
- package/dist/{codes-AX8XkMNY.d.ts → codes-4sYQv_7J.d.ts} +36 -10
- package/dist/column-filters.d.cts +2 -2
- package/dist/column-filters.d.ts +2 -2
- package/dist/diagnostics/build.cjs +1 -1
- package/dist/diagnostics/build.cjs.map +1 -1
- package/dist/diagnostics/build.js +2 -2
- package/dist/diagnostics/build.js.map +1 -1
- package/dist/diagnostics/console.cjs +15 -4
- package/dist/diagnostics/console.cjs.map +1 -1
- package/dist/diagnostics/console.js +15 -4
- package/dist/diagnostics/console.js.map +1 -1
- package/dist/diagnostics/prose.cjs +10 -1
- package/dist/diagnostics/prose.cjs.map +1 -1
- package/dist/diagnostics/prose.js +9 -1
- package/dist/diagnostics/prose.js.map +1 -1
- package/dist/{explain-pKAnnxEW.d.cts → explain-DmwN_RG9.d.cts} +11 -4
- package/dist/{explain-BVE3ln3G.d.ts → explain-Dr3SmMNY.d.ts} +11 -4
- package/dist/feature/install.cjs +2 -1
- package/dist/feature/install.cjs.map +1 -1
- package/dist/feature/install.js +2 -1
- package/dist/feature/install.js.map +1 -1
- package/dist/feature/slice-guard.cjs +76 -0
- package/dist/feature/slice-guard.cjs.map +1 -0
- package/dist/feature/slice-guard.js +50 -0
- package/dist/feature/slice-guard.js.map +1 -0
- package/dist/features/column-filters/codes.cjs +2 -19
- package/dist/features/column-filters/codes.cjs.map +1 -1
- package/dist/features/column-filters/codes.js +2 -19
- package/dist/features/column-filters/codes.js.map +1 -1
- package/dist/features/column-filters/column-filters.cjs +37 -22
- package/dist/features/column-filters/column-filters.cjs.map +1 -1
- package/dist/features/column-filters/column-filters.js +37 -22
- package/dist/features/column-filters/column-filters.js.map +1 -1
- package/dist/features/column-filters/match.cjs +6 -2
- package/dist/features/column-filters/match.cjs.map +1 -1
- package/dist/features/column-filters/match.js +6 -2
- package/dist/features/column-filters/match.js.map +1 -1
- package/dist/features/pagination/pagination.cjs +2 -2
- package/dist/features/pagination/pagination.cjs.map +1 -1
- package/dist/features/pagination/pagination.js +2 -2
- package/dist/features/pagination/pagination.js.map +1 -1
- package/dist/features/search/codes.cjs +18 -2
- package/dist/features/search/codes.cjs.map +1 -1
- package/dist/features/search/codes.js +16 -1
- package/dist/features/search/codes.js.map +1 -1
- package/dist/features/search/search-engine.cjs +24 -5
- package/dist/features/search/search-engine.cjs.map +1 -1
- package/dist/features/search/search-engine.js +25 -6
- package/dist/features/search/search-engine.js.map +1 -1
- package/dist/features/search/search-index.cjs +23 -44
- package/dist/features/search/search-index.cjs.map +1 -1
- package/dist/features/search/search-index.js +24 -45
- package/dist/features/search/search-index.js.map +1 -1
- package/dist/features/sorting/ranks.cjs +117 -50
- package/dist/features/sorting/ranks.cjs.map +1 -1
- package/dist/features/sorting/ranks.js +117 -50
- package/dist/features/sorting/ranks.js.map +1 -1
- package/dist/features/sorting/sorting.cjs +87 -32
- package/dist/features/sorting/sorting.cjs.map +1 -1
- package/dist/features/sorting/sorting.js +87 -32
- package/dist/features/sorting/sorting.js.map +1 -1
- package/dist/index.d.cts +26 -17
- package/dist/index.d.ts +26 -17
- package/dist/intercept/runtime.cjs +3 -2
- package/dist/intercept/runtime.cjs.map +1 -1
- package/dist/intercept/runtime.js +3 -2
- package/dist/intercept/runtime.js.map +1 -1
- package/dist/messages/index.cjs +2 -0
- package/dist/messages/index.cjs.map +1 -1
- package/dist/messages/index.js +2 -0
- package/dist/messages/index.js.map +1 -1
- package/dist/pagination.d.cts +2 -2
- package/dist/pagination.d.ts +2 -2
- package/dist/pipeline/column-cache.cjs +6 -2
- package/dist/pipeline/column-cache.cjs.map +1 -1
- package/dist/pipeline/column-cache.js +6 -2
- package/dist/pipeline/column-cache.js.map +1 -1
- package/dist/pipeline/release.cjs +34 -0
- package/dist/pipeline/release.cjs.map +1 -0
- package/dist/pipeline/release.js +8 -0
- package/dist/pipeline/release.js.map +1 -0
- package/dist/pipeline/runner.cjs +15 -1
- package/dist/pipeline/runner.cjs.map +1 -1
- package/dist/pipeline/runner.js +15 -1
- package/dist/pipeline/runner.js.map +1 -1
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/{search-async-entry-ByMJbHjc.d.cts → search-async-entry-BiemO3JT.d.cts} +2 -2
- package/dist/{search-async-entry-CFQODE08.d.ts → search-async-entry-GRj0E06n.d.ts} +2 -2
- package/dist/search-async.d.cts +3 -3
- package/dist/search-async.d.ts +3 -3
- package/dist/search.d.cts +4 -4
- package/dist/search.d.ts +4 -4
- package/dist/server.d.cts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/sorting.d.cts +2 -2
- package/dist/sorting.d.ts +2 -2
- package/dist/source/async-control.cjs +2 -1
- package/dist/source/async-control.cjs.map +1 -1
- package/dist/source/async-control.js +2 -1
- package/dist/source/async-control.js.map +1 -1
- package/dist/source/fetcher.cjs.map +1 -1
- package/dist/source/fetcher.js.map +1 -1
- package/dist/source/server.cjs +58 -5
- package/dist/source/server.cjs.map +1 -1
- package/dist/source/server.js +58 -5
- package/dist/source/server.js.map +1 -1
- package/dist/table/create-table.cjs +33 -20
- package/dist/table/create-table.cjs.map +1 -1
- package/dist/table/create-table.js +33 -20
- package/dist/table/create-table.js.map +1 -1
- package/dist/table/index.cjs +3 -0
- package/dist/table/index.cjs.map +1 -1
- package/dist/table/index.js +5 -1
- package/dist/table/index.js.map +1 -1
- package/dist/table/state-codes.cjs +72 -0
- package/dist/table/state-codes.cjs.map +1 -0
- package/dist/table/state-codes.js +45 -0
- package/dist/table/state-codes.js.map +1 -0
- package/dist/table/types.cjs.map +1 -1
- package/dist/table/types.js.map +1 -1
- package/dist/validity.d.cts +2 -2
- package/dist/validity.d.ts +2 -2
- package/dist/version.cjs +1 -1
- package/dist/version.cjs.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +3 -3
- package/dist/ingest/test-schemas.cjs +0 -73
- package/dist/ingest/test-schemas.cjs.map +0 -1
- package/dist/ingest/test-schemas.js +0 -46
- package/dist/ingest/test-schemas.js.map +0 -1
|
@@ -34,7 +34,7 @@ var import_row = require("../../pipeline/row.cjs");
|
|
|
34
34
|
var import_capability = require("../../versioning/capability.cjs");
|
|
35
35
|
var CHUNK = 4096;
|
|
36
36
|
var CELLS = 16384;
|
|
37
|
-
var hasExSearch = () => [import_ex_search.ExSearch.prototype.search, import_ex_search.ExSearch.prototype.setData, import_ex_search.
|
|
37
|
+
var hasExSearch = () => [import_ex_search.ExSearch.prototype.search, import_ex_search.ExSearch.prototype.setData, import_ex_search.ExSearch.prototype.updateConfig].every(
|
|
38
38
|
(f) => typeof f === "function"
|
|
39
39
|
);
|
|
40
40
|
function probeExSearch(emit) {
|
|
@@ -43,8 +43,8 @@ function probeExSearch(emit) {
|
|
|
43
43
|
{
|
|
44
44
|
feature: "search",
|
|
45
45
|
dependency: "ex-search",
|
|
46
|
-
capability: "ExSearch
|
|
47
|
-
supported: "^1.
|
|
46
|
+
capability: "ExSearch (fuzzyMode: 'word')",
|
|
47
|
+
supported: "^1.2.0",
|
|
48
48
|
fallback: "case-insensitive exact / prefix / substring matching (no fuzzy matching)"
|
|
49
49
|
},
|
|
50
50
|
emit
|
|
@@ -129,7 +129,7 @@ function* buildIndex(ctx, config, useExSearch) {
|
|
|
129
129
|
const fold = (v) => config.caseSensitive ? v : v.toLowerCase();
|
|
130
130
|
const foldQuery = (q) => fold(q.normalize("NFC"));
|
|
131
131
|
const at = (k, r) => fold(reads[k](r));
|
|
132
|
-
return useExSearch ? exSearchIndex(
|
|
132
|
+
return useExSearch ? exSearchIndex(keys, at, foldQuery, config) : containsIndex(keys, at, foldQuery, config);
|
|
133
133
|
}
|
|
134
134
|
function* chunked(rows, columns, matchChunk) {
|
|
135
135
|
const size = Math.max(256, Math.floor(CELLS / columns));
|
|
@@ -141,33 +141,30 @@ function* chunked(rows, columns, matchChunk) {
|
|
|
141
141
|
}
|
|
142
142
|
return rankHits(hits);
|
|
143
143
|
}
|
|
144
|
-
function projection(
|
|
145
|
-
let rows;
|
|
144
|
+
function projection(keys, at) {
|
|
145
|
+
let rows = new Uint32Array(0);
|
|
146
146
|
const target = [];
|
|
147
|
-
const length = () => rows ? rows.length : size;
|
|
148
|
-
const item = (j) => {
|
|
149
|
-
const r = rows ? rows[j] : j;
|
|
150
|
-
const o = { i: r };
|
|
151
|
-
for (let k = 0; k < keys.length; k++) o[keys[k].name] = at(k, r);
|
|
152
|
-
return o;
|
|
153
|
-
};
|
|
154
147
|
const items = new Proxy(target, {
|
|
155
148
|
get(t, p) {
|
|
156
|
-
if (p === "length") return length
|
|
157
|
-
const j = (0, import_row.arrayIndex)(p, length
|
|
158
|
-
|
|
149
|
+
if (p === "length") return rows.length;
|
|
150
|
+
const j = (0, import_row.arrayIndex)(p, rows.length);
|
|
151
|
+
if (j < 0) return t[p];
|
|
152
|
+
const r = rows[j];
|
|
153
|
+
const o = { i: r };
|
|
154
|
+
for (let k = 0; k < keys.length; k++) o[keys[k].name] = at(k, r);
|
|
155
|
+
return o;
|
|
159
156
|
},
|
|
160
|
-
has: (t, p) => (0, import_row.arrayIndex)(p, length
|
|
157
|
+
has: (t, p) => (0, import_row.arrayIndex)(p, rows.length) >= 0 || p in t
|
|
161
158
|
});
|
|
162
159
|
return {
|
|
163
160
|
items,
|
|
164
|
-
/** Shows `chunk` (row indices)
|
|
161
|
+
/** Shows `chunk` (row indices). */
|
|
165
162
|
show(chunk) {
|
|
166
163
|
rows = chunk;
|
|
167
164
|
}
|
|
168
165
|
};
|
|
169
166
|
}
|
|
170
|
-
function exSearchIndex(
|
|
167
|
+
function exSearchIndex(keys, at, fold, config) {
|
|
171
168
|
const engine = new import_ex_search.ExSearch({
|
|
172
169
|
keys,
|
|
173
170
|
...config,
|
|
@@ -176,36 +173,18 @@ function exSearchIndex(size, keys, at, fold, config) {
|
|
|
176
173
|
// stable and avoids radix's score quantisation.
|
|
177
174
|
sortAlgorithm: "tim"
|
|
178
175
|
});
|
|
179
|
-
const
|
|
180
|
-
let q = "";
|
|
181
|
-
const near = (k, r) => {
|
|
182
|
-
const v = at(k, r);
|
|
183
|
-
if (v.includes(q) || /\s/.test(q)) return v;
|
|
184
|
-
let best = "";
|
|
185
|
-
let top = 0;
|
|
186
|
-
const words = v.split(/\s+/);
|
|
187
|
-
for (const w of words[1] ? [v, ...words] : words) {
|
|
188
|
-
const s = Math.abs(w.length - q.length) > max ? 0 : import_ex_search.Scorer.fuzzy(w, q, max, true);
|
|
189
|
-
if (s > top) {
|
|
190
|
-
best = w;
|
|
191
|
-
top = s;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return best;
|
|
195
|
-
};
|
|
196
|
-
const view = projection(size, keys, near);
|
|
176
|
+
const view = projection(keys, at);
|
|
197
177
|
engine.setData(view.items);
|
|
198
178
|
return {
|
|
199
179
|
match(rows, query) {
|
|
200
|
-
const
|
|
180
|
+
const q = fold(query);
|
|
181
|
+
const fuzzyMode = /\s/.test(q) ? "value" : "word";
|
|
201
182
|
return chunked(rows, keys.length, (chunk) => {
|
|
202
|
-
|
|
183
|
+
engine.updateConfig({ fuzzyMode });
|
|
203
184
|
view.show(chunk);
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
view.show(void 0);
|
|
208
|
-
}
|
|
185
|
+
const hits = engine.search(q);
|
|
186
|
+
engine.updateConfig({});
|
|
187
|
+
return hits;
|
|
209
188
|
});
|
|
210
189
|
}
|
|
211
190
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/features/search/search-index.ts"],"sourcesContent":["import { ExSearch, Scorer } from 'ex-search'\nimport { getValue } from '../../column/get-value'\nimport '../../diagnostics/process-guard'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\nimport { nullishOf, type SlicedFormatting } from '../../pipeline/column-cache'\nimport { resolveColumns } from '../../pipeline/columns'\nimport { arrayIndex } from '../../pipeline/row'\nimport { detectCapability, hasCapability } from '../../versioning/capability'\n\n/** Matcher settings (the client options of `search()`, defaults applied). */\nexport interface MatchConfig {\n readonly threshold: number\n readonly fuzzyMaxDistance: number\n readonly caseSensitive: boolean\n}\n\n/**\n * One projected row as ex-search reads it: `i` is the row index (results map back by it, never by\n * identity: ex-search returns copies), `c0`, `c1`, … the formatted values of the searchable\n * columns. Flat keys also sidestep ex-search's top-level-key limitation for nested paths. Items\n * are built per chunk while matching and dropped after it (§9.3 memory): none is kept.\n */\nexport type Item = Record<string, string | number> & { readonly i: number }\n\n/** A match: the row index and its relevance (0–1, weight applied). */\nexport interface Hit {\n readonly i: number\n readonly score: number\n}\n\n/** The matcher of one (data, columns) pair: references to column values, no per-row copy. */\nexport interface SearchIndex {\n /**\n * A time-sliced job (§9.1) matching `query` (trimmed, non-blank) against the rows `rows` (row\n * indices), in chunks: the hits in relevance order ({@link rankHits}).\n */\n match(rows: Uint32Array, query: string): Generator<undefined, Uint32Array, undefined>\n}\n\n/** Rows per chunk between two `yield`s of a scan (one value per row). */\nconst CHUNK = 4096\n\n/**\n * Cells (rows × searchable columns) matched per chunk between two `yield`s (#40): a table with\n * many searchable columns matches fewer rows per chunk, so a chunk stays a few milliseconds.\n */\nconst CELLS = 16_384\n\ninterface Key {\n readonly name: string\n readonly weight: number\n}\n\n/** The formatted value of row `r` of one searchable column. */\ntype Read = (r: number) => string\n\n// A throw (no `ExSearch`) counts as missing (hasCapability).\nconst hasExSearch = () =>\n [ExSearch.prototype.search, ExSearch.prototype.setData, Scorer.fuzzy].every(\n (f) => typeof f === 'function',\n )\n\n/** Feature-detects ex-search once (§8.5); `false` → the contains fallback. */\nexport function probeExSearch(emit: Emit): boolean {\n // DK_DEP_INCOMPATIBLE is a development check (§8.5); the fallback is not.\n return (process.env.NODE_ENV !== \"production\")\n ? detectCapability(\n hasExSearch,\n {\n feature: 'search',\n dependency: 'ex-search',\n capability: 'ExSearch, Scorer.fuzzy',\n supported: '^1.1.1',\n fallback: 'case-insensitive exact / prefix / substring matching (no fuzzy matching)',\n },\n emit,\n )\n : hasCapability(hasExSearch)\n}\n\n/**\n * A numeric column's formatted value without cached strings (#27): what the default formatter\n * shows, `String(v)`, or `''` for a `null` / `undefined` cell (stored as `NaN`, like a real `NaN`,\n * which shows `'NaN'`).\n */\nfunction numberReader(values: Float64Array): Read {\n const nullish = nullishOf(values)\n return (r) => {\n const v = values[r] as number\n return !Number.isNaN(v) ? String(v) : nullish?.[r] ? '' : 'NaN'\n }\n}\n\n/**\n * One pass over a column's values (`get(r)` per row, time-sliced by `CHUNK` rows): their text as\n * the default formatter shows a primitive (`String(v)`, `''` when nullish), read while matching\n * (#27: no formatted copy per row). `false` at the first object or function (a `Date`, an array):\n * the column cache formats those. `undefined` when a read throws. The values are read in NFC (#30),\n * normalised here, never per keystroke: the rows whose value is not NFC (rare: NFD input from\n * macOS, Thai or Vietnamese keyboards) keep their NFC form in a sparse map. Only values with a code\n * unit from U+0300 (combining marks and up) can change.\n */\nfunction* scan(\n size: number,\n get: (r: number) => unknown,\n): Generator<undefined, Read | false | undefined, undefined> {\n const over = new Map<number, string>()\n try {\n for (let r = 0; r < size; r++) {\n const v = get(r)\n if (v !== null && (typeof v === 'object' || typeof v === 'function')) return false\n if (typeof v === 'string' && /[\\u0300-\\uffff]/.test(v)) {\n const n = v.normalize('NFC')\n if (n !== v) over.set(r, n)\n }\n if (r % CHUNK === CHUNK - 1) yield\n }\n } catch {\n return undefined\n }\n const read: Read = (r) => {\n const v = get(r)\n return v === null || v === undefined ? '' : String(v)\n }\n return over.size ? (r) => over.get(r) ?? read(r) : read\n}\n\n/**\n * Builds the matcher over the formatted values of the searchable columns (what the user sees), once\n * per data + columns, as a time-sliced job (§9.1): one column read per slice. Searchable columns are\n * those with a `search` weight > 0. When no column declares `search`, the columns of primitives\n * (strings, numbers, booleans, nullable) are searchable at weight 1, and a column of dates, arrays\n * or other objects is not (#27: it would keep a formatted string per row; it opts in with a\n * weight; `DK_SEARCH_COLUMN_SKIPPED` in development). `null` when nothing is searchable. Nothing\n * is copied per row (§9.3 memory): a key column of primitives without `format` is read from the\n * rows and stringified while matching (no values array, #27); an accessor column of primitives\n * reads the column cache's values the same way; only a column with `format` or an opted-in object\n * column (dates, arrays) keeps its formatted strings in the column cache. A key column whose read\n * throws goes through the column cache too, which reports it. The case is folded per chunk while\n * matching.\n */\nexport function* buildIndex(\n ctx: RowModelContext,\n config: MatchConfig,\n useExSearch: boolean,\n): Generator<undefined, SearchIndex | null, undefined> {\n const cols = resolveColumns(ctx.columns)\n const declared = cols.list.some((c) => (c.def as { search?: unknown }).search !== undefined)\n const data = ctx.data\n const n = data.length\n const keys: Key[] = []\n const reads: Read[] = []\n for (const col of cols.list) {\n if (cols.byId.get(col.id) !== col) continue\n const w = (col.def as { search?: unknown }).search\n const weight = declared ? (typeof w === 'number' && w > 0 ? w : 0) : 1\n if (weight === 0) continue\n const path = col.format ? undefined : col.path\n let failed = false\n const onError = () => {\n failed = true\n }\n let read = path ? yield* scan(n, (r) => getValue(data[r], path, onError)) : undefined\n if (failed || (read === false && declared)) read = undefined\n if (read === undefined) {\n const values = ctx.getColumnValues(col.id)\n if (!values) continue\n yield // reading a column is a task of its own (#40)\n const numbers = values instanceof Float64Array\n if (numbers && !col.format) read = numberReader(values)\n else {\n // Zero-config: a column of objects is not searched, `format` or not (#27).\n const own =\n numbers || (declared && col.format) ? undefined : yield* scan(n, (r) => values[r])\n if (own === false && !declared) read = false\n else if (own && !col.format) read = own\n else {\n // Time-sliced in a table run (#40): dates or a heavy `format` are not one long task.\n const sliced = (ctx as RowModelContext & SlicedFormatting).formattedSliced\n const formatted = sliced ? yield* sliced(col.id) : ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = yield* scan(n, (r) => formatted[r])\n }\n }\n }\n if (!read) {\n if ((process.env.NODE_ENV !== \"production\") && read === false) {\n ctx.emit('DK_SEARCH_COLUMN_SKIPPED', { column: col.id, index: col.index })\n }\n continue\n }\n keys.push({ name: `c${keys.length}`, weight })\n reads.push(read)\n yield\n }\n if (keys.length === 0) return null\n // Values (NFC already) are folded per chunk while matching; the query is normalised as well, so\n // NFC and NFD spellings match each other like in the `contains` filter (#30).\n const fold = (v: string) => (config.caseSensitive ? v : v.toLowerCase())\n const foldQuery = (q: string) => fold(q.normalize('NFC'))\n const at = (k: number, r: number) => fold((reads[k] as Read)(r))\n return useExSearch\n ? exSearchIndex(ctx.data.length, keys, at, foldQuery, config)\n : containsIndex(keys, at, foldQuery, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n columns: number,\n matchChunk: (chunk: Uint32Array) => Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n const size = Math.max(256, Math.floor(CELLS / columns))\n if (rows.length <= size) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += size) {\n for (const h of matchChunk(rows.subarray(k, k + size))) hits.push(h)\n yield\n }\n return rankHits(hits)\n}\n\n/**\n * The projection as ex-search reads it: an array (iteration, `length`, index reads) of `{ i, c0, … }`\n * items, each built when read and not kept. It shows every row of `data` (`size`), or one chunk of\n * rows while that chunk is matched; ex-search holds this one view (`setData` once).\n */\nfunction projection(size: number, keys: readonly Key[], at: (k: number, r: number) => string) {\n let rows: Uint32Array | undefined\n // Never written, and its own `length` stays 0: `length` is read through the trap below (setting\n // a large `length` would allocate a backing store).\n const target: Item[] = []\n const length = () => (rows ? rows.length : size)\n const item = (j: number): Item => {\n const r = rows ? (rows[j] as number) : j\n const o = { i: r } as Record<string, string | number>\n for (let k = 0; k < keys.length; k++) o[(keys[k] as Key).name] = at(k, r)\n return o as Item\n }\n const items = new Proxy(target, {\n get(t, p) {\n if (p === 'length') return length()\n const j = arrayIndex(p, length())\n return j >= 0 ? item(j) : t[p as keyof Item[]]\n },\n has: (t, p) => arrayIndex(p, length()) >= 0 || p in t,\n })\n return {\n items,\n /** Shows `chunk` (row indices), or every row again (`undefined`). */\n show(chunk: Uint32Array | undefined) {\n rows = chunk\n },\n }\n}\n\nfunction exSearchIndex(\n size: number,\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n // Values and query are folded already (buildIndex).\n const engine = new ExSearch<Item>({\n keys: keys as Key[],\n ...config,\n caseSensitive: true,\n // DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass\n // stable and avoids radix's score quantisation.\n sortAlgorithm: 'tim',\n })\n // ex-search measures the edit distance to the whole value, so a typo of one word of a multi-word\n // cell never matched (#10). For a one-word query, a value without it is shown as the best fuzzy\n // match of itself and its words, or as '' (scores 0, as the value would). A value with the query\n // stays whole for the exact / prefix / substring scores. Only strings within `max` of the query's\n // length are measured (an edit distance is at least the length difference).\n const max = config.fuzzyMaxDistance\n let q = ''\n const near = (k: number, r: number) => {\n const v = at(k, r)\n if (v.includes(q) || /\\s/.test(q)) return v\n let best = ''\n let top = 0\n const words = v.split(/\\s+/)\n for (const w of words[1] ? [v, ...words] : words) {\n const s = Math.abs(w.length - q.length) > max ? 0 : Scorer.fuzzy(w, q, max, true)\n if (s > top) {\n best = w\n top = s\n }\n }\n return best\n }\n const view = projection(size, keys, near)\n engine.setData(view.items)\n return {\n match(rows, query) {\n const folded = fold(query)\n return chunked(rows, keys.length, (chunk) => {\n // Set per chunk, like the view: another job may have matched in between.\n q = folded\n view.show(chunk)\n try {\n // Results are copies of the items (`Omit<Item, …>` drops `i` from the type, not the value).\n return engine.search(q) as unknown as Hit[]\n } finally {\n view.show(undefined)\n }\n })\n },\n }\n}\n\n/** The fallback when ex-search is missing or incompatible: exact 1 · prefix 0.8 · substring 0.65. */\nfunction containsIndex(\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n return {\n match(rows, query) {\n const q = fold(query)\n return chunked(rows, keys.length, (chunk) => {\n const hits: Hit[] = []\n for (let j = 0; j < chunk.length; j++) {\n const r = chunk[j] as number\n let best = 0\n for (let k = 0; k < keys.length; k++) {\n const v = at(k, r)\n const raw = v === q ? 1 : v.startsWith(q) ? 0.8 : v.includes(q) ? 0.65 : 0\n const w = (keys[k] as Key).weight\n if (raw * w > best) best = raw * w\n }\n if (best > 0 && best >= config.threshold) hits.push({ i: r, score: best })\n }\n return hits\n })\n },\n }\n}\n\n/**\n * Hits in relevance order (§6.3): score descending, ties by original row index. Enforced here so\n * the order never depends on the matcher's own ranking. Sorts `hits` (a fresh array) in place.\n */\nexport function rankHits(hits: Hit[]): Uint32Array {\n return Uint32Array.from(\n hits.sort((a, b) => b.score - a.score || a.i - b.i),\n (h) => h.i,\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/features/search/search-index.ts"],"sourcesContent":["import { ExSearch } from 'ex-search'\nimport { getValue } from '../../column/get-value'\nimport '../../diagnostics/process-guard'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\nimport { nullishOf, type SlicedFormatting } from '../../pipeline/column-cache'\nimport { resolveColumns } from '../../pipeline/columns'\nimport { arrayIndex } from '../../pipeline/row'\nimport { detectCapability, hasCapability } from '../../versioning/capability'\n\n/** Matcher settings (the client options of `search()`, defaults applied). */\nexport interface MatchConfig {\n readonly threshold: number\n readonly fuzzyMaxDistance: number\n readonly caseSensitive: boolean\n}\n\n/**\n * One projected row as ex-search reads it: `i` is the row index (results map back by it, never by\n * identity: ex-search returns copies), `c0`, `c1`, … the formatted values of the searchable\n * columns. Flat keys also sidestep ex-search's top-level-key limitation for nested paths. Items\n * are built per chunk while matching and dropped after it (§9.3 memory): none is kept.\n */\nexport type Item = Record<string, string | number> & { readonly i: number }\n\n/** A match: the row index and its relevance (0–1, weight applied). */\nexport interface Hit {\n readonly i: number\n readonly score: number\n}\n\n/** The matcher of one (data, columns) pair: references to column values, no per-row copy. */\nexport interface SearchIndex {\n /**\n * A time-sliced job (§9.1) matching `query` (trimmed, non-blank) against the rows `rows` (row\n * indices), in chunks: the hits in relevance order ({@link rankHits}).\n */\n match(rows: Uint32Array, query: string): Generator<undefined, Uint32Array, undefined>\n}\n\n/** Rows per chunk between two `yield`s of a scan (one value per row). */\nconst CHUNK = 4096\n\n/**\n * Cells (rows × searchable columns) matched per chunk between two `yield`s (#40): a table with\n * many searchable columns matches fewer rows per chunk, so a chunk stays a few milliseconds.\n */\nconst CELLS = 16_384\n\ninterface Key {\n readonly name: string\n readonly weight: number\n}\n\n/** The formatted value of row `r` of one searchable column. */\ntype Read = (r: number) => string\n\n// A throw (no `ExSearch`) counts as missing (hasCapability).\nconst hasExSearch = () =>\n [ExSearch.prototype.search, ExSearch.prototype.setData, ExSearch.prototype.updateConfig].every(\n (f) => typeof f === 'function',\n )\n\n/** Feature-detects ex-search once (§8.5); `false` → the contains fallback. */\nexport function probeExSearch(emit: Emit): boolean {\n // DK_DEP_INCOMPATIBLE is a development check (§8.5); the fallback is not.\n return (process.env.NODE_ENV !== \"production\")\n ? detectCapability(\n hasExSearch,\n {\n feature: 'search',\n dependency: 'ex-search',\n capability: \"ExSearch (fuzzyMode: 'word')\",\n supported: '^1.2.0',\n fallback: 'case-insensitive exact / prefix / substring matching (no fuzzy matching)',\n },\n emit,\n )\n : hasCapability(hasExSearch)\n}\n\n/**\n * A numeric column's formatted value without cached strings (#27): what the default formatter\n * shows, `String(v)`, or `''` for a `null` / `undefined` cell (stored as `NaN`, like a real `NaN`,\n * which shows `'NaN'`).\n */\nfunction numberReader(values: Float64Array): Read {\n const nullish = nullishOf(values)\n return (r) => {\n const v = values[r] as number\n return !Number.isNaN(v) ? String(v) : nullish?.[r] ? '' : 'NaN'\n }\n}\n\n/**\n * One pass over a column's values (`get(r)` per row, time-sliced by `CHUNK` rows): their text as\n * the default formatter shows a primitive (`String(v)`, `''` when nullish), read while matching\n * (#27: no formatted copy per row). `false` at the first object or function (a `Date`, an array):\n * the column cache formats those. `undefined` when a read throws. The values are read in NFC (#30),\n * normalised here, never per keystroke: the rows whose value is not NFC (rare: NFD input from\n * macOS, Thai or Vietnamese keyboards) keep their NFC form in a sparse map. Only values with a code\n * unit from U+0300 (combining marks and up) can change.\n */\nfunction* scan(\n size: number,\n get: (r: number) => unknown,\n): Generator<undefined, Read | false | undefined, undefined> {\n const over = new Map<number, string>()\n try {\n for (let r = 0; r < size; r++) {\n const v = get(r)\n if (v !== null && (typeof v === 'object' || typeof v === 'function')) return false\n if (typeof v === 'string' && /[\\u0300-\\uffff]/.test(v)) {\n const n = v.normalize('NFC')\n if (n !== v) over.set(r, n)\n }\n if (r % CHUNK === CHUNK - 1) yield\n }\n } catch {\n return undefined\n }\n const read: Read = (r) => {\n const v = get(r)\n return v === null || v === undefined ? '' : String(v)\n }\n return over.size ? (r) => over.get(r) ?? read(r) : read\n}\n\n/**\n * Builds the matcher over the formatted values of the searchable columns (what the user sees), once\n * per data + columns, as a time-sliced job (§9.1): one column read per slice. Searchable columns are\n * those with a `search` weight > 0. When no column declares `search`, the columns of primitives\n * (strings, numbers, booleans, nullable) are searchable at weight 1, and a column of dates, arrays\n * or other objects is not (#27: it would keep a formatted string per row; it opts in with a\n * weight; `DK_SEARCH_COLUMN_SKIPPED` in development). `null` when nothing is searchable. Nothing\n * is copied per row (§9.3 memory): a key column of primitives without `format` is read from the\n * rows and stringified while matching (no values array, #27); an accessor column of primitives\n * reads the column cache's values the same way; only a column with `format` or an opted-in object\n * column (dates, arrays) keeps its formatted strings in the column cache. A key column whose read\n * throws goes through the column cache too, which reports it. The case is folded per chunk while\n * matching.\n */\nexport function* buildIndex(\n ctx: RowModelContext,\n config: MatchConfig,\n useExSearch: boolean,\n): Generator<undefined, SearchIndex | null, undefined> {\n const cols = resolveColumns(ctx.columns)\n const declared = cols.list.some((c) => (c.def as { search?: unknown }).search !== undefined)\n const data = ctx.data\n const n = data.length\n const keys: Key[] = []\n const reads: Read[] = []\n for (const col of cols.list) {\n if (cols.byId.get(col.id) !== col) continue\n const w = (col.def as { search?: unknown }).search\n const weight = declared ? (typeof w === 'number' && w > 0 ? w : 0) : 1\n if (weight === 0) continue\n const path = col.format ? undefined : col.path\n let failed = false\n const onError = () => {\n failed = true\n }\n let read = path ? yield* scan(n, (r) => getValue(data[r], path, onError)) : undefined\n if (failed || (read === false && declared)) read = undefined\n if (read === undefined) {\n const values = ctx.getColumnValues(col.id)\n if (!values) continue\n yield // reading a column is a task of its own (#40)\n const numbers = values instanceof Float64Array\n if (numbers && !col.format) read = numberReader(values)\n else {\n // Zero-config: a column of objects is not searched, `format` or not (#27).\n const own =\n numbers || (declared && col.format) ? undefined : yield* scan(n, (r) => values[r])\n if (own === false && !declared) read = false\n else if (own && !col.format) read = own\n else {\n // Time-sliced in a table run (#40): dates or a heavy `format` are not one long task.\n const sliced = (ctx as RowModelContext & SlicedFormatting).formattedSliced\n const formatted = sliced ? yield* sliced(col.id) : ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = yield* scan(n, (r) => formatted[r])\n }\n }\n }\n if (!read) {\n if ((process.env.NODE_ENV !== \"production\") && read === false) {\n ctx.emit('DK_SEARCH_COLUMN_SKIPPED', { column: col.id, index: col.index })\n }\n continue\n }\n keys.push({ name: `c${keys.length}`, weight })\n reads.push(read)\n yield\n }\n if (keys.length === 0) return null\n // Values (NFC already) are folded per chunk while matching; the query is normalised as well, so\n // NFC and NFD spellings match each other like in the `contains` filter (#30).\n const fold = (v: string) => (config.caseSensitive ? v : v.toLowerCase())\n const foldQuery = (q: string) => fold(q.normalize('NFC'))\n const at = (k: number, r: number) => fold((reads[k] as Read)(r))\n return useExSearch\n ? exSearchIndex(keys, at, foldQuery, config)\n : containsIndex(keys, at, foldQuery, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n columns: number,\n matchChunk: (chunk: Uint32Array) => Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n const size = Math.max(256, Math.floor(CELLS / columns))\n if (rows.length <= size) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += size) {\n for (const h of matchChunk(rows.subarray(k, k + size))) hits.push(h)\n yield\n }\n return rankHits(hits)\n}\n\n/**\n * The projection as ex-search reads it: an array (`length`, index reads) of `{ i, c0, … }` items,\n * each built when read and not kept. It shows the chunk of rows being matched; ex-search holds\n * this one view (`setData` once).\n */\nfunction projection(keys: readonly Key[], at: (k: number, r: number) => string) {\n let rows: Uint32Array = new Uint32Array(0)\n // Never written, and its own `length` stays 0: `length` is read through the trap below (setting\n // a large `length` would allocate a backing store).\n const target: Item[] = []\n const items = new Proxy(target, {\n get(t, p) {\n if (p === 'length') return rows.length\n const j = arrayIndex(p, rows.length)\n if (j < 0) return t[p as keyof Item[]]\n const r = rows[j] as number\n const o = { i: r } as Record<string, string | number>\n for (let k = 0; k < keys.length; k++) o[(keys[k] as Key).name] = at(k, r)\n return o\n },\n has: (t, p) => arrayIndex(p, rows.length) >= 0 || p in t,\n })\n return {\n items,\n /** Shows `chunk` (row indices). */\n show(chunk: Uint32Array) {\n rows = chunk\n },\n }\n}\n\nfunction exSearchIndex(\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n // Values and query are folded already (buildIndex).\n const engine = new ExSearch<Item>({\n keys: keys as Key[],\n ...config,\n caseSensitive: true,\n // DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass\n // stable and avoids radix's score quantisation.\n sortAlgorithm: 'tim',\n })\n // A one-word query is measured against the whole value and each of its words, the best wins: a\n // typo of one word of a multi-word cell matches (#10). ex-search's `fuzzyMode: 'word'` does it\n // with its bounded matcher (#106). A query with whitespace is measured against whole values only,\n // as before (a single word is never that query), and is decided once per query, not per row (#96).\n // ex-search caches the words per position of its data: the view shows another chunk each time,\n // and a cache kept over every row would hold a word list per row and column (tens of MB at 100k\n // rows, §9.3), so it is dropped after each chunk (`updateConfig`) and values are split per chunk.\n const view = projection(keys, at)\n engine.setData(view.items)\n return {\n match(rows, query) {\n const q = fold(query)\n const fuzzyMode = /\\s/.test(q) ? 'value' : 'word'\n return chunked(rows, keys.length, (chunk) => {\n // Set per chunk, like the view: another job may have matched in between.\n engine.updateConfig({ fuzzyMode })\n view.show(chunk)\n // Results are copies of the items (`Omit<Item, …>` drops `i` from the type, not the value).\n const hits = engine.search(q) as unknown as Hit[]\n engine.updateConfig({})\n return hits\n })\n },\n }\n}\n\n/** The fallback when ex-search is missing or incompatible: exact 1 · prefix 0.8 · substring 0.65. */\nfunction containsIndex(\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n return {\n match(rows, query) {\n const q = fold(query)\n return chunked(rows, keys.length, (chunk) => {\n const hits: Hit[] = []\n for (let j = 0; j < chunk.length; j++) {\n const r = chunk[j] as number\n let best = 0\n for (let k = 0; k < keys.length; k++) {\n const v = at(k, r)\n const raw = v === q ? 1 : v.startsWith(q) ? 0.8 : v.includes(q) ? 0.65 : 0\n const w = (keys[k] as Key).weight\n if (raw * w > best) best = raw * w\n }\n if (best > 0 && best >= config.threshold) hits.push({ i: r, score: best })\n }\n return hits\n })\n },\n }\n}\n\n/**\n * Hits in relevance order (§6.3): score descending, ties by original row index. Enforced here so\n * the order never depends on the matcher's own ranking. Sorts `hits` (a fresh array) in place.\n */\nexport function rankHits(hits: Hit[]): Uint32Array {\n return Uint32Array.from(\n hits.sort((a, b) => b.score - a.score || a.i - b.i),\n (h) => h.i,\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/features/search/search-index.ts
|
|
2
|
-
import { ExSearch
|
|
2
|
+
import { ExSearch } from "ex-search";
|
|
3
3
|
import { getValue } from "../../column/get-value.js";
|
|
4
4
|
import "../../diagnostics/process-guard.js";
|
|
5
5
|
import { nullishOf } from "../../pipeline/column-cache.js";
|
|
@@ -8,7 +8,7 @@ import { arrayIndex } from "../../pipeline/row.js";
|
|
|
8
8
|
import { detectCapability, hasCapability } from "../../versioning/capability.js";
|
|
9
9
|
var CHUNK = 4096;
|
|
10
10
|
var CELLS = 16384;
|
|
11
|
-
var hasExSearch = () => [ExSearch.prototype.search, ExSearch.prototype.setData,
|
|
11
|
+
var hasExSearch = () => [ExSearch.prototype.search, ExSearch.prototype.setData, ExSearch.prototype.updateConfig].every(
|
|
12
12
|
(f) => typeof f === "function"
|
|
13
13
|
);
|
|
14
14
|
function probeExSearch(emit) {
|
|
@@ -17,8 +17,8 @@ function probeExSearch(emit) {
|
|
|
17
17
|
{
|
|
18
18
|
feature: "search",
|
|
19
19
|
dependency: "ex-search",
|
|
20
|
-
capability: "ExSearch
|
|
21
|
-
supported: "^1.
|
|
20
|
+
capability: "ExSearch (fuzzyMode: 'word')",
|
|
21
|
+
supported: "^1.2.0",
|
|
22
22
|
fallback: "case-insensitive exact / prefix / substring matching (no fuzzy matching)"
|
|
23
23
|
},
|
|
24
24
|
emit
|
|
@@ -103,7 +103,7 @@ function* buildIndex(ctx, config, useExSearch) {
|
|
|
103
103
|
const fold = (v) => config.caseSensitive ? v : v.toLowerCase();
|
|
104
104
|
const foldQuery = (q) => fold(q.normalize("NFC"));
|
|
105
105
|
const at = (k, r) => fold(reads[k](r));
|
|
106
|
-
return useExSearch ? exSearchIndex(
|
|
106
|
+
return useExSearch ? exSearchIndex(keys, at, foldQuery, config) : containsIndex(keys, at, foldQuery, config);
|
|
107
107
|
}
|
|
108
108
|
function* chunked(rows, columns, matchChunk) {
|
|
109
109
|
const size = Math.max(256, Math.floor(CELLS / columns));
|
|
@@ -115,33 +115,30 @@ function* chunked(rows, columns, matchChunk) {
|
|
|
115
115
|
}
|
|
116
116
|
return rankHits(hits);
|
|
117
117
|
}
|
|
118
|
-
function projection(
|
|
119
|
-
let rows;
|
|
118
|
+
function projection(keys, at) {
|
|
119
|
+
let rows = new Uint32Array(0);
|
|
120
120
|
const target = [];
|
|
121
|
-
const length = () => rows ? rows.length : size;
|
|
122
|
-
const item = (j) => {
|
|
123
|
-
const r = rows ? rows[j] : j;
|
|
124
|
-
const o = { i: r };
|
|
125
|
-
for (let k = 0; k < keys.length; k++) o[keys[k].name] = at(k, r);
|
|
126
|
-
return o;
|
|
127
|
-
};
|
|
128
121
|
const items = new Proxy(target, {
|
|
129
122
|
get(t, p) {
|
|
130
|
-
if (p === "length") return length
|
|
131
|
-
const j = arrayIndex(p, length
|
|
132
|
-
|
|
123
|
+
if (p === "length") return rows.length;
|
|
124
|
+
const j = arrayIndex(p, rows.length);
|
|
125
|
+
if (j < 0) return t[p];
|
|
126
|
+
const r = rows[j];
|
|
127
|
+
const o = { i: r };
|
|
128
|
+
for (let k = 0; k < keys.length; k++) o[keys[k].name] = at(k, r);
|
|
129
|
+
return o;
|
|
133
130
|
},
|
|
134
|
-
has: (t, p) => arrayIndex(p, length
|
|
131
|
+
has: (t, p) => arrayIndex(p, rows.length) >= 0 || p in t
|
|
135
132
|
});
|
|
136
133
|
return {
|
|
137
134
|
items,
|
|
138
|
-
/** Shows `chunk` (row indices)
|
|
135
|
+
/** Shows `chunk` (row indices). */
|
|
139
136
|
show(chunk) {
|
|
140
137
|
rows = chunk;
|
|
141
138
|
}
|
|
142
139
|
};
|
|
143
140
|
}
|
|
144
|
-
function exSearchIndex(
|
|
141
|
+
function exSearchIndex(keys, at, fold, config) {
|
|
145
142
|
const engine = new ExSearch({
|
|
146
143
|
keys,
|
|
147
144
|
...config,
|
|
@@ -150,36 +147,18 @@ function exSearchIndex(size, keys, at, fold, config) {
|
|
|
150
147
|
// stable and avoids radix's score quantisation.
|
|
151
148
|
sortAlgorithm: "tim"
|
|
152
149
|
});
|
|
153
|
-
const
|
|
154
|
-
let q = "";
|
|
155
|
-
const near = (k, r) => {
|
|
156
|
-
const v = at(k, r);
|
|
157
|
-
if (v.includes(q) || /\s/.test(q)) return v;
|
|
158
|
-
let best = "";
|
|
159
|
-
let top = 0;
|
|
160
|
-
const words = v.split(/\s+/);
|
|
161
|
-
for (const w of words[1] ? [v, ...words] : words) {
|
|
162
|
-
const s = Math.abs(w.length - q.length) > max ? 0 : Scorer.fuzzy(w, q, max, true);
|
|
163
|
-
if (s > top) {
|
|
164
|
-
best = w;
|
|
165
|
-
top = s;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return best;
|
|
169
|
-
};
|
|
170
|
-
const view = projection(size, keys, near);
|
|
150
|
+
const view = projection(keys, at);
|
|
171
151
|
engine.setData(view.items);
|
|
172
152
|
return {
|
|
173
153
|
match(rows, query) {
|
|
174
|
-
const
|
|
154
|
+
const q = fold(query);
|
|
155
|
+
const fuzzyMode = /\s/.test(q) ? "value" : "word";
|
|
175
156
|
return chunked(rows, keys.length, (chunk) => {
|
|
176
|
-
|
|
157
|
+
engine.updateConfig({ fuzzyMode });
|
|
177
158
|
view.show(chunk);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
view.show(void 0);
|
|
182
|
-
}
|
|
159
|
+
const hits = engine.search(q);
|
|
160
|
+
engine.updateConfig({});
|
|
161
|
+
return hits;
|
|
183
162
|
});
|
|
184
163
|
}
|
|
185
164
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/features/search/search-index.ts"],"sourcesContent":["import { ExSearch, Scorer } from 'ex-search'\nimport { getValue } from '../../column/get-value'\nimport '../../diagnostics/process-guard'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\nimport { nullishOf, type SlicedFormatting } from '../../pipeline/column-cache'\nimport { resolveColumns } from '../../pipeline/columns'\nimport { arrayIndex } from '../../pipeline/row'\nimport { detectCapability, hasCapability } from '../../versioning/capability'\n\n/** Matcher settings (the client options of `search()`, defaults applied). */\nexport interface MatchConfig {\n readonly threshold: number\n readonly fuzzyMaxDistance: number\n readonly caseSensitive: boolean\n}\n\n/**\n * One projected row as ex-search reads it: `i` is the row index (results map back by it, never by\n * identity: ex-search returns copies), `c0`, `c1`, … the formatted values of the searchable\n * columns. Flat keys also sidestep ex-search's top-level-key limitation for nested paths. Items\n * are built per chunk while matching and dropped after it (§9.3 memory): none is kept.\n */\nexport type Item = Record<string, string | number> & { readonly i: number }\n\n/** A match: the row index and its relevance (0–1, weight applied). */\nexport interface Hit {\n readonly i: number\n readonly score: number\n}\n\n/** The matcher of one (data, columns) pair: references to column values, no per-row copy. */\nexport interface SearchIndex {\n /**\n * A time-sliced job (§9.1) matching `query` (trimmed, non-blank) against the rows `rows` (row\n * indices), in chunks: the hits in relevance order ({@link rankHits}).\n */\n match(rows: Uint32Array, query: string): Generator<undefined, Uint32Array, undefined>\n}\n\n/** Rows per chunk between two `yield`s of a scan (one value per row). */\nconst CHUNK = 4096\n\n/**\n * Cells (rows × searchable columns) matched per chunk between two `yield`s (#40): a table with\n * many searchable columns matches fewer rows per chunk, so a chunk stays a few milliseconds.\n */\nconst CELLS = 16_384\n\ninterface Key {\n readonly name: string\n readonly weight: number\n}\n\n/** The formatted value of row `r` of one searchable column. */\ntype Read = (r: number) => string\n\n// A throw (no `ExSearch`) counts as missing (hasCapability).\nconst hasExSearch = () =>\n [ExSearch.prototype.search, ExSearch.prototype.setData, Scorer.fuzzy].every(\n (f) => typeof f === 'function',\n )\n\n/** Feature-detects ex-search once (§8.5); `false` → the contains fallback. */\nexport function probeExSearch(emit: Emit): boolean {\n // DK_DEP_INCOMPATIBLE is a development check (§8.5); the fallback is not.\n return (process.env.NODE_ENV !== \"production\")\n ? detectCapability(\n hasExSearch,\n {\n feature: 'search',\n dependency: 'ex-search',\n capability: 'ExSearch, Scorer.fuzzy',\n supported: '^1.1.1',\n fallback: 'case-insensitive exact / prefix / substring matching (no fuzzy matching)',\n },\n emit,\n )\n : hasCapability(hasExSearch)\n}\n\n/**\n * A numeric column's formatted value without cached strings (#27): what the default formatter\n * shows, `String(v)`, or `''` for a `null` / `undefined` cell (stored as `NaN`, like a real `NaN`,\n * which shows `'NaN'`).\n */\nfunction numberReader(values: Float64Array): Read {\n const nullish = nullishOf(values)\n return (r) => {\n const v = values[r] as number\n return !Number.isNaN(v) ? String(v) : nullish?.[r] ? '' : 'NaN'\n }\n}\n\n/**\n * One pass over a column's values (`get(r)` per row, time-sliced by `CHUNK` rows): their text as\n * the default formatter shows a primitive (`String(v)`, `''` when nullish), read while matching\n * (#27: no formatted copy per row). `false` at the first object or function (a `Date`, an array):\n * the column cache formats those. `undefined` when a read throws. The values are read in NFC (#30),\n * normalised here, never per keystroke: the rows whose value is not NFC (rare: NFD input from\n * macOS, Thai or Vietnamese keyboards) keep their NFC form in a sparse map. Only values with a code\n * unit from U+0300 (combining marks and up) can change.\n */\nfunction* scan(\n size: number,\n get: (r: number) => unknown,\n): Generator<undefined, Read | false | undefined, undefined> {\n const over = new Map<number, string>()\n try {\n for (let r = 0; r < size; r++) {\n const v = get(r)\n if (v !== null && (typeof v === 'object' || typeof v === 'function')) return false\n if (typeof v === 'string' && /[\\u0300-\\uffff]/.test(v)) {\n const n = v.normalize('NFC')\n if (n !== v) over.set(r, n)\n }\n if (r % CHUNK === CHUNK - 1) yield\n }\n } catch {\n return undefined\n }\n const read: Read = (r) => {\n const v = get(r)\n return v === null || v === undefined ? '' : String(v)\n }\n return over.size ? (r) => over.get(r) ?? read(r) : read\n}\n\n/**\n * Builds the matcher over the formatted values of the searchable columns (what the user sees), once\n * per data + columns, as a time-sliced job (§9.1): one column read per slice. Searchable columns are\n * those with a `search` weight > 0. When no column declares `search`, the columns of primitives\n * (strings, numbers, booleans, nullable) are searchable at weight 1, and a column of dates, arrays\n * or other objects is not (#27: it would keep a formatted string per row; it opts in with a\n * weight; `DK_SEARCH_COLUMN_SKIPPED` in development). `null` when nothing is searchable. Nothing\n * is copied per row (§9.3 memory): a key column of primitives without `format` is read from the\n * rows and stringified while matching (no values array, #27); an accessor column of primitives\n * reads the column cache's values the same way; only a column with `format` or an opted-in object\n * column (dates, arrays) keeps its formatted strings in the column cache. A key column whose read\n * throws goes through the column cache too, which reports it. The case is folded per chunk while\n * matching.\n */\nexport function* buildIndex(\n ctx: RowModelContext,\n config: MatchConfig,\n useExSearch: boolean,\n): Generator<undefined, SearchIndex | null, undefined> {\n const cols = resolveColumns(ctx.columns)\n const declared = cols.list.some((c) => (c.def as { search?: unknown }).search !== undefined)\n const data = ctx.data\n const n = data.length\n const keys: Key[] = []\n const reads: Read[] = []\n for (const col of cols.list) {\n if (cols.byId.get(col.id) !== col) continue\n const w = (col.def as { search?: unknown }).search\n const weight = declared ? (typeof w === 'number' && w > 0 ? w : 0) : 1\n if (weight === 0) continue\n const path = col.format ? undefined : col.path\n let failed = false\n const onError = () => {\n failed = true\n }\n let read = path ? yield* scan(n, (r) => getValue(data[r], path, onError)) : undefined\n if (failed || (read === false && declared)) read = undefined\n if (read === undefined) {\n const values = ctx.getColumnValues(col.id)\n if (!values) continue\n yield // reading a column is a task of its own (#40)\n const numbers = values instanceof Float64Array\n if (numbers && !col.format) read = numberReader(values)\n else {\n // Zero-config: a column of objects is not searched, `format` or not (#27).\n const own =\n numbers || (declared && col.format) ? undefined : yield* scan(n, (r) => values[r])\n if (own === false && !declared) read = false\n else if (own && !col.format) read = own\n else {\n // Time-sliced in a table run (#40): dates or a heavy `format` are not one long task.\n const sliced = (ctx as RowModelContext & SlicedFormatting).formattedSliced\n const formatted = sliced ? yield* sliced(col.id) : ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = yield* scan(n, (r) => formatted[r])\n }\n }\n }\n if (!read) {\n if ((process.env.NODE_ENV !== \"production\") && read === false) {\n ctx.emit('DK_SEARCH_COLUMN_SKIPPED', { column: col.id, index: col.index })\n }\n continue\n }\n keys.push({ name: `c${keys.length}`, weight })\n reads.push(read)\n yield\n }\n if (keys.length === 0) return null\n // Values (NFC already) are folded per chunk while matching; the query is normalised as well, so\n // NFC and NFD spellings match each other like in the `contains` filter (#30).\n const fold = (v: string) => (config.caseSensitive ? v : v.toLowerCase())\n const foldQuery = (q: string) => fold(q.normalize('NFC'))\n const at = (k: number, r: number) => fold((reads[k] as Read)(r))\n return useExSearch\n ? exSearchIndex(ctx.data.length, keys, at, foldQuery, config)\n : containsIndex(keys, at, foldQuery, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n columns: number,\n matchChunk: (chunk: Uint32Array) => Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n const size = Math.max(256, Math.floor(CELLS / columns))\n if (rows.length <= size) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += size) {\n for (const h of matchChunk(rows.subarray(k, k + size))) hits.push(h)\n yield\n }\n return rankHits(hits)\n}\n\n/**\n * The projection as ex-search reads it: an array (iteration, `length`, index reads) of `{ i, c0, … }`\n * items, each built when read and not kept. It shows every row of `data` (`size`), or one chunk of\n * rows while that chunk is matched; ex-search holds this one view (`setData` once).\n */\nfunction projection(size: number, keys: readonly Key[], at: (k: number, r: number) => string) {\n let rows: Uint32Array | undefined\n // Never written, and its own `length` stays 0: `length` is read through the trap below (setting\n // a large `length` would allocate a backing store).\n const target: Item[] = []\n const length = () => (rows ? rows.length : size)\n const item = (j: number): Item => {\n const r = rows ? (rows[j] as number) : j\n const o = { i: r } as Record<string, string | number>\n for (let k = 0; k < keys.length; k++) o[(keys[k] as Key).name] = at(k, r)\n return o as Item\n }\n const items = new Proxy(target, {\n get(t, p) {\n if (p === 'length') return length()\n const j = arrayIndex(p, length())\n return j >= 0 ? item(j) : t[p as keyof Item[]]\n },\n has: (t, p) => arrayIndex(p, length()) >= 0 || p in t,\n })\n return {\n items,\n /** Shows `chunk` (row indices), or every row again (`undefined`). */\n show(chunk: Uint32Array | undefined) {\n rows = chunk\n },\n }\n}\n\nfunction exSearchIndex(\n size: number,\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n // Values and query are folded already (buildIndex).\n const engine = new ExSearch<Item>({\n keys: keys as Key[],\n ...config,\n caseSensitive: true,\n // DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass\n // stable and avoids radix's score quantisation.\n sortAlgorithm: 'tim',\n })\n // ex-search measures the edit distance to the whole value, so a typo of one word of a multi-word\n // cell never matched (#10). For a one-word query, a value without it is shown as the best fuzzy\n // match of itself and its words, or as '' (scores 0, as the value would). A value with the query\n // stays whole for the exact / prefix / substring scores. Only strings within `max` of the query's\n // length are measured (an edit distance is at least the length difference).\n const max = config.fuzzyMaxDistance\n let q = ''\n const near = (k: number, r: number) => {\n const v = at(k, r)\n if (v.includes(q) || /\\s/.test(q)) return v\n let best = ''\n let top = 0\n const words = v.split(/\\s+/)\n for (const w of words[1] ? [v, ...words] : words) {\n const s = Math.abs(w.length - q.length) > max ? 0 : Scorer.fuzzy(w, q, max, true)\n if (s > top) {\n best = w\n top = s\n }\n }\n return best\n }\n const view = projection(size, keys, near)\n engine.setData(view.items)\n return {\n match(rows, query) {\n const folded = fold(query)\n return chunked(rows, keys.length, (chunk) => {\n // Set per chunk, like the view: another job may have matched in between.\n q = folded\n view.show(chunk)\n try {\n // Results are copies of the items (`Omit<Item, …>` drops `i` from the type, not the value).\n return engine.search(q) as unknown as Hit[]\n } finally {\n view.show(undefined)\n }\n })\n },\n }\n}\n\n/** The fallback when ex-search is missing or incompatible: exact 1 · prefix 0.8 · substring 0.65. */\nfunction containsIndex(\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n return {\n match(rows, query) {\n const q = fold(query)\n return chunked(rows, keys.length, (chunk) => {\n const hits: Hit[] = []\n for (let j = 0; j < chunk.length; j++) {\n const r = chunk[j] as number\n let best = 0\n for (let k = 0; k < keys.length; k++) {\n const v = at(k, r)\n const raw = v === q ? 1 : v.startsWith(q) ? 0.8 : v.includes(q) ? 0.65 : 0\n const w = (keys[k] as Key).weight\n if (raw * w > best) best = raw * w\n }\n if (best > 0 && best >= config.threshold) hits.push({ i: r, score: best })\n }\n return hits\n })\n },\n }\n}\n\n/**\n * Hits in relevance order (§6.3): score descending, ties by original row index. Enforced here so\n * the order never depends on the matcher's own ranking. Sorts `hits` (a fresh array) in place.\n */\nexport function rankHits(hits: Hit[]): Uint32Array {\n return Uint32Array.from(\n hits.sort((a, b) => b.score - a.score || a.i - b.i),\n (h) => h.i,\n )\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/features/search/search-index.ts"],"sourcesContent":["import { ExSearch } from 'ex-search'\nimport { getValue } from '../../column/get-value'\nimport '../../diagnostics/process-guard'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\nimport { nullishOf, type SlicedFormatting } from '../../pipeline/column-cache'\nimport { resolveColumns } from '../../pipeline/columns'\nimport { arrayIndex } from '../../pipeline/row'\nimport { detectCapability, hasCapability } from '../../versioning/capability'\n\n/** Matcher settings (the client options of `search()`, defaults applied). */\nexport interface MatchConfig {\n readonly threshold: number\n readonly fuzzyMaxDistance: number\n readonly caseSensitive: boolean\n}\n\n/**\n * One projected row as ex-search reads it: `i` is the row index (results map back by it, never by\n * identity: ex-search returns copies), `c0`, `c1`, … the formatted values of the searchable\n * columns. Flat keys also sidestep ex-search's top-level-key limitation for nested paths. Items\n * are built per chunk while matching and dropped after it (§9.3 memory): none is kept.\n */\nexport type Item = Record<string, string | number> & { readonly i: number }\n\n/** A match: the row index and its relevance (0–1, weight applied). */\nexport interface Hit {\n readonly i: number\n readonly score: number\n}\n\n/** The matcher of one (data, columns) pair: references to column values, no per-row copy. */\nexport interface SearchIndex {\n /**\n * A time-sliced job (§9.1) matching `query` (trimmed, non-blank) against the rows `rows` (row\n * indices), in chunks: the hits in relevance order ({@link rankHits}).\n */\n match(rows: Uint32Array, query: string): Generator<undefined, Uint32Array, undefined>\n}\n\n/** Rows per chunk between two `yield`s of a scan (one value per row). */\nconst CHUNK = 4096\n\n/**\n * Cells (rows × searchable columns) matched per chunk between two `yield`s (#40): a table with\n * many searchable columns matches fewer rows per chunk, so a chunk stays a few milliseconds.\n */\nconst CELLS = 16_384\n\ninterface Key {\n readonly name: string\n readonly weight: number\n}\n\n/** The formatted value of row `r` of one searchable column. */\ntype Read = (r: number) => string\n\n// A throw (no `ExSearch`) counts as missing (hasCapability).\nconst hasExSearch = () =>\n [ExSearch.prototype.search, ExSearch.prototype.setData, ExSearch.prototype.updateConfig].every(\n (f) => typeof f === 'function',\n )\n\n/** Feature-detects ex-search once (§8.5); `false` → the contains fallback. */\nexport function probeExSearch(emit: Emit): boolean {\n // DK_DEP_INCOMPATIBLE is a development check (§8.5); the fallback is not.\n return (process.env.NODE_ENV !== \"production\")\n ? detectCapability(\n hasExSearch,\n {\n feature: 'search',\n dependency: 'ex-search',\n capability: \"ExSearch (fuzzyMode: 'word')\",\n supported: '^1.2.0',\n fallback: 'case-insensitive exact / prefix / substring matching (no fuzzy matching)',\n },\n emit,\n )\n : hasCapability(hasExSearch)\n}\n\n/**\n * A numeric column's formatted value without cached strings (#27): what the default formatter\n * shows, `String(v)`, or `''` for a `null` / `undefined` cell (stored as `NaN`, like a real `NaN`,\n * which shows `'NaN'`).\n */\nfunction numberReader(values: Float64Array): Read {\n const nullish = nullishOf(values)\n return (r) => {\n const v = values[r] as number\n return !Number.isNaN(v) ? String(v) : nullish?.[r] ? '' : 'NaN'\n }\n}\n\n/**\n * One pass over a column's values (`get(r)` per row, time-sliced by `CHUNK` rows): their text as\n * the default formatter shows a primitive (`String(v)`, `''` when nullish), read while matching\n * (#27: no formatted copy per row). `false` at the first object or function (a `Date`, an array):\n * the column cache formats those. `undefined` when a read throws. The values are read in NFC (#30),\n * normalised here, never per keystroke: the rows whose value is not NFC (rare: NFD input from\n * macOS, Thai or Vietnamese keyboards) keep their NFC form in a sparse map. Only values with a code\n * unit from U+0300 (combining marks and up) can change.\n */\nfunction* scan(\n size: number,\n get: (r: number) => unknown,\n): Generator<undefined, Read | false | undefined, undefined> {\n const over = new Map<number, string>()\n try {\n for (let r = 0; r < size; r++) {\n const v = get(r)\n if (v !== null && (typeof v === 'object' || typeof v === 'function')) return false\n if (typeof v === 'string' && /[\\u0300-\\uffff]/.test(v)) {\n const n = v.normalize('NFC')\n if (n !== v) over.set(r, n)\n }\n if (r % CHUNK === CHUNK - 1) yield\n }\n } catch {\n return undefined\n }\n const read: Read = (r) => {\n const v = get(r)\n return v === null || v === undefined ? '' : String(v)\n }\n return over.size ? (r) => over.get(r) ?? read(r) : read\n}\n\n/**\n * Builds the matcher over the formatted values of the searchable columns (what the user sees), once\n * per data + columns, as a time-sliced job (§9.1): one column read per slice. Searchable columns are\n * those with a `search` weight > 0. When no column declares `search`, the columns of primitives\n * (strings, numbers, booleans, nullable) are searchable at weight 1, and a column of dates, arrays\n * or other objects is not (#27: it would keep a formatted string per row; it opts in with a\n * weight; `DK_SEARCH_COLUMN_SKIPPED` in development). `null` when nothing is searchable. Nothing\n * is copied per row (§9.3 memory): a key column of primitives without `format` is read from the\n * rows and stringified while matching (no values array, #27); an accessor column of primitives\n * reads the column cache's values the same way; only a column with `format` or an opted-in object\n * column (dates, arrays) keeps its formatted strings in the column cache. A key column whose read\n * throws goes through the column cache too, which reports it. The case is folded per chunk while\n * matching.\n */\nexport function* buildIndex(\n ctx: RowModelContext,\n config: MatchConfig,\n useExSearch: boolean,\n): Generator<undefined, SearchIndex | null, undefined> {\n const cols = resolveColumns(ctx.columns)\n const declared = cols.list.some((c) => (c.def as { search?: unknown }).search !== undefined)\n const data = ctx.data\n const n = data.length\n const keys: Key[] = []\n const reads: Read[] = []\n for (const col of cols.list) {\n if (cols.byId.get(col.id) !== col) continue\n const w = (col.def as { search?: unknown }).search\n const weight = declared ? (typeof w === 'number' && w > 0 ? w : 0) : 1\n if (weight === 0) continue\n const path = col.format ? undefined : col.path\n let failed = false\n const onError = () => {\n failed = true\n }\n let read = path ? yield* scan(n, (r) => getValue(data[r], path, onError)) : undefined\n if (failed || (read === false && declared)) read = undefined\n if (read === undefined) {\n const values = ctx.getColumnValues(col.id)\n if (!values) continue\n yield // reading a column is a task of its own (#40)\n const numbers = values instanceof Float64Array\n if (numbers && !col.format) read = numberReader(values)\n else {\n // Zero-config: a column of objects is not searched, `format` or not (#27).\n const own =\n numbers || (declared && col.format) ? undefined : yield* scan(n, (r) => values[r])\n if (own === false && !declared) read = false\n else if (own && !col.format) read = own\n else {\n // Time-sliced in a table run (#40): dates or a heavy `format` are not one long task.\n const sliced = (ctx as RowModelContext & SlicedFormatting).formattedSliced\n const formatted = sliced ? yield* sliced(col.id) : ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = yield* scan(n, (r) => formatted[r])\n }\n }\n }\n if (!read) {\n if ((process.env.NODE_ENV !== \"production\") && read === false) {\n ctx.emit('DK_SEARCH_COLUMN_SKIPPED', { column: col.id, index: col.index })\n }\n continue\n }\n keys.push({ name: `c${keys.length}`, weight })\n reads.push(read)\n yield\n }\n if (keys.length === 0) return null\n // Values (NFC already) are folded per chunk while matching; the query is normalised as well, so\n // NFC and NFD spellings match each other like in the `contains` filter (#30).\n const fold = (v: string) => (config.caseSensitive ? v : v.toLowerCase())\n const foldQuery = (q: string) => fold(q.normalize('NFC'))\n const at = (k: number, r: number) => fold((reads[k] as Read)(r))\n return useExSearch\n ? exSearchIndex(keys, at, foldQuery, config)\n : containsIndex(keys, at, foldQuery, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n columns: number,\n matchChunk: (chunk: Uint32Array) => Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n const size = Math.max(256, Math.floor(CELLS / columns))\n if (rows.length <= size) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += size) {\n for (const h of matchChunk(rows.subarray(k, k + size))) hits.push(h)\n yield\n }\n return rankHits(hits)\n}\n\n/**\n * The projection as ex-search reads it: an array (`length`, index reads) of `{ i, c0, … }` items,\n * each built when read and not kept. It shows the chunk of rows being matched; ex-search holds\n * this one view (`setData` once).\n */\nfunction projection(keys: readonly Key[], at: (k: number, r: number) => string) {\n let rows: Uint32Array = new Uint32Array(0)\n // Never written, and its own `length` stays 0: `length` is read through the trap below (setting\n // a large `length` would allocate a backing store).\n const target: Item[] = []\n const items = new Proxy(target, {\n get(t, p) {\n if (p === 'length') return rows.length\n const j = arrayIndex(p, rows.length)\n if (j < 0) return t[p as keyof Item[]]\n const r = rows[j] as number\n const o = { i: r } as Record<string, string | number>\n for (let k = 0; k < keys.length; k++) o[(keys[k] as Key).name] = at(k, r)\n return o\n },\n has: (t, p) => arrayIndex(p, rows.length) >= 0 || p in t,\n })\n return {\n items,\n /** Shows `chunk` (row indices). */\n show(chunk: Uint32Array) {\n rows = chunk\n },\n }\n}\n\nfunction exSearchIndex(\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n // Values and query are folded already (buildIndex).\n const engine = new ExSearch<Item>({\n keys: keys as Key[],\n ...config,\n caseSensitive: true,\n // DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass\n // stable and avoids radix's score quantisation.\n sortAlgorithm: 'tim',\n })\n // A one-word query is measured against the whole value and each of its words, the best wins: a\n // typo of one word of a multi-word cell matches (#10). ex-search's `fuzzyMode: 'word'` does it\n // with its bounded matcher (#106). A query with whitespace is measured against whole values only,\n // as before (a single word is never that query), and is decided once per query, not per row (#96).\n // ex-search caches the words per position of its data: the view shows another chunk each time,\n // and a cache kept over every row would hold a word list per row and column (tens of MB at 100k\n // rows, §9.3), so it is dropped after each chunk (`updateConfig`) and values are split per chunk.\n const view = projection(keys, at)\n engine.setData(view.items)\n return {\n match(rows, query) {\n const q = fold(query)\n const fuzzyMode = /\\s/.test(q) ? 'value' : 'word'\n return chunked(rows, keys.length, (chunk) => {\n // Set per chunk, like the view: another job may have matched in between.\n engine.updateConfig({ fuzzyMode })\n view.show(chunk)\n // Results are copies of the items (`Omit<Item, …>` drops `i` from the type, not the value).\n const hits = engine.search(q) as unknown as Hit[]\n engine.updateConfig({})\n return hits\n })\n },\n }\n}\n\n/** The fallback when ex-search is missing or incompatible: exact 1 · prefix 0.8 · substring 0.65. */\nfunction containsIndex(\n keys: readonly Key[],\n at: (k: number, r: number) => string,\n fold: (v: string) => string,\n config: MatchConfig,\n): SearchIndex {\n return {\n match(rows, query) {\n const q = fold(query)\n return chunked(rows, keys.length, (chunk) => {\n const hits: Hit[] = []\n for (let j = 0; j < chunk.length; j++) {\n const r = chunk[j] as number\n let best = 0\n for (let k = 0; k < keys.length; k++) {\n const v = at(k, r)\n const raw = v === q ? 1 : v.startsWith(q) ? 0.8 : v.includes(q) ? 0.65 : 0\n const w = (keys[k] as Key).weight\n if (raw * w > best) best = raw * w\n }\n if (best > 0 && best >= config.threshold) hits.push({ i: r, score: best })\n }\n return hits\n })\n },\n }\n}\n\n/**\n * Hits in relevance order (§6.3): score descending, ties by original row index. Enforced here so\n * the order never depends on the matcher's own ranking. Sorts `hits` (a fresh array) in place.\n */\nexport function rankHits(hits: Hit[]): Uint32Array {\n return Uint32Array.from(\n hits.sort((a, b) => b.score - a.score || a.i - b.i),\n (h) => h.i,\n )\n}\n"],"mappings":";AAAA,SAAS,gBAAgB;AACzB,SAAS,gBAAgB;AACzB,OAAO;AAGP,SAAS,iBAAwC;AACjD,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB,qBAAqB;AAiChD,IAAM,QAAQ;AAMd,IAAM,QAAQ;AAWd,IAAM,cAAc,MAClB,CAAC,SAAS,UAAU,QAAQ,SAAS,UAAU,SAAS,SAAS,UAAU,YAAY,EAAE;AAAA,EACvF,CAAC,MAAM,OAAO,MAAM;AACtB;AAGK,SAAS,cAAc,MAAqB;AAEjD,SAAQ,QAAQ,IAAI,aAAa,eAC7B;AAAA,IACE;AAAA,IACA;AAAA,MACE,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,EACF,IACA,cAAc,WAAW;AAC/B;AAOA,SAAS,aAAa,QAA4B;AAChD,QAAM,UAAU,UAAU,MAAM;AAChC,SAAO,CAAC,MAAM;AACZ,UAAM,IAAI,OAAO,CAAC;AAClB,WAAO,CAAC,OAAO,MAAM,CAAC,IAAI,OAAO,CAAC,IAAI,UAAU,CAAC,IAAI,KAAK;AAAA,EAC5D;AACF;AAWA,UAAU,KACR,MACA,KAC2D;AAC3D,QAAM,OAAO,oBAAI,IAAoB;AACrC,MAAI;AACF,aAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC7B,YAAM,IAAI,IAAI,CAAC;AACf,UAAI,MAAM,SAAS,OAAO,MAAM,YAAY,OAAO,MAAM,YAAa,QAAO;AAC7E,UAAI,OAAO,MAAM,YAAY,kBAAkB,KAAK,CAAC,GAAG;AACtD,cAAM,IAAI,EAAE,UAAU,KAAK;AAC3B,YAAI,MAAM,EAAG,MAAK,IAAI,GAAG,CAAC;AAAA,MAC5B;AACA,UAAI,IAAI,UAAU,QAAQ,EAAG;AAAA,IAC/B;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,OAAa,CAAC,MAAM;AACxB,UAAM,IAAI,IAAI,CAAC;AACf,WAAO,MAAM,QAAQ,MAAM,SAAY,KAAK,OAAO,CAAC;AAAA,EACtD;AACA,SAAO,KAAK,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI;AACrD;AAgBO,UAAU,WACf,KACA,QACA,aACqD;AACrD,QAAM,OAAO,eAAe,IAAI,OAAO;AACvC,QAAM,WAAW,KAAK,KAAK,KAAK,CAAC,MAAO,EAAE,IAA6B,WAAW,MAAS;AAC3F,QAAM,OAAO,IAAI;AACjB,QAAM,IAAI,KAAK;AACf,QAAM,OAAc,CAAC;AACrB,QAAM,QAAgB,CAAC;AACvB,aAAW,OAAO,KAAK,MAAM;AAC3B,QAAI,KAAK,KAAK,IAAI,IAAI,EAAE,MAAM,IAAK;AACnC,UAAM,IAAK,IAAI,IAA6B;AAC5C,UAAM,SAAS,WAAY,OAAO,MAAM,YAAY,IAAI,IAAI,IAAI,IAAK;AACrE,QAAI,WAAW,EAAG;AAClB,UAAM,OAAO,IAAI,SAAS,SAAY,IAAI;AAC1C,QAAI,SAAS;AACb,UAAM,UAAU,MAAM;AACpB,eAAS;AAAA,IACX;AACA,QAAI,OAAO,OAAO,OAAO,KAAK,GAAG,CAAC,MAAM,SAAS,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,IAAI;AAC5E,QAAI,UAAW,SAAS,SAAS,SAAW,QAAO;AACnD,QAAI,SAAS,QAAW;AACtB,YAAM,SAAS,IAAI,gBAAgB,IAAI,EAAE;AACzC,UAAI,CAAC,OAAQ;AACb;AACA,YAAM,UAAU,kBAAkB;AAClC,UAAI,WAAW,CAAC,IAAI,OAAQ,QAAO,aAAa,MAAM;AAAA,WACjD;AAEH,cAAM,MACJ,WAAY,YAAY,IAAI,SAAU,SAAY,OAAO,KAAK,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;AACnF,YAAI,QAAQ,SAAS,CAAC,SAAU,QAAO;AAAA,iBAC9B,OAAO,CAAC,IAAI,OAAQ,QAAO;AAAA,aAC/B;AAEH,gBAAM,SAAU,IAA2C;AAC3D,gBAAM,YAAY,SAAS,OAAO,OAAO,IAAI,EAAE,IAAI,IAAI,mBAAmB,IAAI,EAAE;AAChF,cAAI,CAAC,UAAW;AAChB,iBAAO,OAAO,KAAK,GAAG,CAAC,MAAM,UAAU,CAAC,CAAC;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,MAAM;AACT,UAAK,QAAQ,IAAI,aAAa,gBAAiB,SAAS,OAAO;AAC7D,YAAI,KAAK,4BAA4B,EAAE,QAAQ,IAAI,IAAI,OAAO,IAAI,MAAM,CAAC;AAAA,MAC3E;AACA;AAAA,IACF;AACA,SAAK,KAAK,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,OAAO,CAAC;AAC7C,UAAM,KAAK,IAAI;AACf;AAAA,EACF;AACA,MAAI,KAAK,WAAW,EAAG,QAAO;AAG9B,QAAM,OAAO,CAAC,MAAe,OAAO,gBAAgB,IAAI,EAAE,YAAY;AACtE,QAAM,YAAY,CAAC,MAAc,KAAK,EAAE,UAAU,KAAK,CAAC;AACxD,QAAM,KAAK,CAAC,GAAW,MAAc,KAAM,MAAM,CAAC,EAAW,CAAC,CAAC;AAC/D,SAAO,cACH,cAAc,MAAM,IAAI,WAAW,MAAM,IACzC,cAAc,MAAM,IAAI,WAAW,MAAM;AAC/C;AAGA,UAAU,QACR,MACA,SACA,YAC8C;AAC9C,QAAM,OAAO,KAAK,IAAI,KAAK,KAAK,MAAM,QAAQ,OAAO,CAAC;AACtD,MAAI,KAAK,UAAU,KAAM,QAAO,SAAS,WAAW,IAAI,CAAC;AACzD,QAAM,OAAc,CAAC;AACrB,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,MAAM;AAC1C,eAAW,KAAK,WAAW,KAAK,SAAS,GAAG,IAAI,IAAI,CAAC,EAAG,MAAK,KAAK,CAAC;AACnE;AAAA,EACF;AACA,SAAO,SAAS,IAAI;AACtB;AAOA,SAAS,WAAW,MAAsB,IAAsC;AAC9E,MAAI,OAAoB,IAAI,YAAY,CAAC;AAGzC,QAAM,SAAiB,CAAC;AACxB,QAAM,QAAQ,IAAI,MAAM,QAAQ;AAAA,IAC9B,IAAI,GAAG,GAAG;AACR,UAAI,MAAM,SAAU,QAAO,KAAK;AAChC,YAAM,IAAI,WAAW,GAAG,KAAK,MAAM;AACnC,UAAI,IAAI,EAAG,QAAO,EAAE,CAAiB;AACrC,YAAM,IAAI,KAAK,CAAC;AAChB,YAAM,IAAI,EAAE,GAAG,EAAE;AACjB,eAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,IAAK,GAAG,KAAK,CAAC,EAAU,IAAI,IAAI,GAAG,GAAG,CAAC;AACxE,aAAO;AAAA,IACT;AAAA,IACA,KAAK,CAAC,GAAG,MAAM,WAAW,GAAG,KAAK,MAAM,KAAK,KAAK,KAAK;AAAA,EACzD,CAAC;AACD,SAAO;AAAA,IACL;AAAA;AAAA,IAEA,KAAK,OAAoB;AACvB,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,SAAS,cACP,MACA,IACA,MACA,QACa;AAEb,QAAM,SAAS,IAAI,SAAe;AAAA,IAChC;AAAA,IACA,GAAG;AAAA,IACH,eAAe;AAAA;AAAA;AAAA,IAGf,eAAe;AAAA,EACjB,CAAC;AAQD,QAAM,OAAO,WAAW,MAAM,EAAE;AAChC,SAAO,QAAQ,KAAK,KAAK;AACzB,SAAO;AAAA,IACL,MAAM,MAAM,OAAO;AACjB,YAAM,IAAI,KAAK,KAAK;AACpB,YAAM,YAAY,KAAK,KAAK,CAAC,IAAI,UAAU;AAC3C,aAAO,QAAQ,MAAM,KAAK,QAAQ,CAAC,UAAU;AAE3C,eAAO,aAAa,EAAE,UAAU,CAAC;AACjC,aAAK,KAAK,KAAK;AAEf,cAAM,OAAO,OAAO,OAAO,CAAC;AAC5B,eAAO,aAAa,CAAC,CAAC;AACtB,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAGA,SAAS,cACP,MACA,IACA,MACA,QACa;AACb,SAAO;AAAA,IACL,MAAM,MAAM,OAAO;AACjB,YAAM,IAAI,KAAK,KAAK;AACpB,aAAO,QAAQ,MAAM,KAAK,QAAQ,CAAC,UAAU;AAC3C,cAAM,OAAc,CAAC;AACrB,iBAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,gBAAM,IAAI,MAAM,CAAC;AACjB,cAAI,OAAO;AACX,mBAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,kBAAM,IAAI,GAAG,GAAG,CAAC;AACjB,kBAAM,MAAM,MAAM,IAAI,IAAI,EAAE,WAAW,CAAC,IAAI,MAAM,EAAE,SAAS,CAAC,IAAI,OAAO;AACzE,kBAAM,IAAK,KAAK,CAAC,EAAU;AAC3B,gBAAI,MAAM,IAAI,KAAM,QAAO,MAAM;AAAA,UACnC;AACA,cAAI,OAAO,KAAK,QAAQ,OAAO,UAAW,MAAK,KAAK,EAAE,GAAG,GAAG,OAAO,KAAK,CAAC;AAAA,QAC3E;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAMO,SAAS,SAAS,MAA0B;AACjD,SAAO,YAAY;AAAA,IACjB,KAAK,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AAAA,IAClD,CAAC,MAAM,EAAE;AAAA,EACX;AACF;","names":[]}
|
|
@@ -25,85 +25,152 @@ __export(ranks_exports, {
|
|
|
25
25
|
module.exports = __toCommonJS(ranks_exports);
|
|
26
26
|
var import_compare = require("./compare.cjs");
|
|
27
27
|
var CHUNK = 4096;
|
|
28
|
-
var RUN =
|
|
29
|
-
function* sortPositions(n, cmp) {
|
|
28
|
+
var RUN = 1024;
|
|
29
|
+
function* sortPositions(n, cmp, run) {
|
|
30
30
|
let a = new Uint32Array(n);
|
|
31
|
+
let ea = new Uint8Array(n);
|
|
31
32
|
for (let i = 0; i < n; i++) a[i] = i;
|
|
32
|
-
for (let lo = 0; lo < n; lo +=
|
|
33
|
-
|
|
33
|
+
for (let lo = 0; lo < n; lo += run) {
|
|
34
|
+
const hi = Math.min(lo + run, n);
|
|
35
|
+
let zero = false;
|
|
36
|
+
a.subarray(lo, hi).sort((x, y) => {
|
|
37
|
+
const c = cmp(x, y);
|
|
38
|
+
if (c === 0) zero = true;
|
|
39
|
+
return c;
|
|
40
|
+
});
|
|
41
|
+
if (zero)
|
|
42
|
+
for (let k = lo + 1; k < hi; k++) ea[k] = cmp(a[k - 1], a[k]) ? 0 : 1;
|
|
34
43
|
yield;
|
|
35
44
|
}
|
|
36
45
|
let b = new Uint32Array(n);
|
|
46
|
+
let eb = new Uint8Array(n);
|
|
37
47
|
let ops = 0;
|
|
38
|
-
for (let width =
|
|
48
|
+
for (let width = run; width < n; width *= 2) {
|
|
39
49
|
for (let lo = 0; lo < n; lo += 2 * width) {
|
|
40
50
|
const mid = Math.min(lo + width, n);
|
|
41
51
|
const hi = Math.min(lo + 2 * width, n);
|
|
42
52
|
let i = lo;
|
|
43
53
|
let j = mid;
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
j
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
let prev = -1;
|
|
55
|
+
let same = 0;
|
|
56
|
+
for (let k = lo; k < hi; k++) {
|
|
57
|
+
let c = 1;
|
|
58
|
+
let src;
|
|
59
|
+
if (i < mid && j < hi) {
|
|
60
|
+
c = cmp(a[j], a[i]);
|
|
61
|
+
src = c < 0 ? 1 : 0;
|
|
62
|
+
if (++ops === CHUNK) {
|
|
63
|
+
ops = 0;
|
|
64
|
+
yield;
|
|
65
|
+
}
|
|
66
|
+
} else src = i < mid ? 0 : 1;
|
|
67
|
+
const p = src ? j++ : i++;
|
|
68
|
+
b[k] = a[p];
|
|
69
|
+
eb[k] = prev === src ? ea[p] : prev < 0 ? 0 : same;
|
|
70
|
+
same = src === 0 && c === 0 ? 1 : 0;
|
|
71
|
+
prev = src;
|
|
59
72
|
}
|
|
60
|
-
while (i < mid) b[k++] = a[i++];
|
|
61
|
-
while (j < hi) b[k++] = a[j++];
|
|
62
73
|
}
|
|
63
74
|
const t = a;
|
|
64
75
|
a = b;
|
|
65
76
|
b = t;
|
|
77
|
+
const e = ea;
|
|
78
|
+
ea = eb;
|
|
79
|
+
eb = e;
|
|
66
80
|
}
|
|
67
|
-
return a;
|
|
81
|
+
return { order: a, eq: ea };
|
|
68
82
|
}
|
|
69
|
-
function* collationRanks(values, collator) {
|
|
83
|
+
function* collationRanks(values, collator, prev = { values: [], ranks: [], reps: [] }) {
|
|
70
84
|
const n = values.length;
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
85
|
+
const { values: was, ranks: wasRank, reps } = prev;
|
|
86
|
+
const m = reps.length;
|
|
87
|
+
const out = new Float64Array(n);
|
|
88
|
+
const used = new Uint8Array(m);
|
|
89
|
+
const freshIds = /* @__PURE__ */ new Map();
|
|
90
|
+
const fresh = [];
|
|
91
|
+
const gap = [];
|
|
92
|
+
let rowOf;
|
|
93
|
+
let shift = 0;
|
|
94
|
+
let ops = 0;
|
|
74
95
|
for (let r = 0; r < n; r++) {
|
|
75
96
|
const v = values[r];
|
|
76
97
|
if (typeof v === "string") {
|
|
77
|
-
let
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
let k;
|
|
99
|
+
const q = r + shift;
|
|
100
|
+
if (was[q] === v) k = wasRank[q];
|
|
101
|
+
else {
|
|
102
|
+
const id = freshIds.get(v);
|
|
103
|
+
if (id !== void 0) k = m + id;
|
|
104
|
+
else {
|
|
105
|
+
let lo = 0;
|
|
106
|
+
let hi = m;
|
|
107
|
+
while (lo < hi) {
|
|
108
|
+
const mid = lo + hi >>> 1;
|
|
109
|
+
const c = collator.compare(v, reps[mid]);
|
|
110
|
+
ops++;
|
|
111
|
+
if (c === 0) {
|
|
112
|
+
k = mid;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
if (c < 0) hi = mid;
|
|
116
|
+
else lo = mid + 1;
|
|
117
|
+
}
|
|
118
|
+
if (k === void 0) {
|
|
119
|
+
k = m + fresh.length;
|
|
120
|
+
freshIds.set(v, fresh.length);
|
|
121
|
+
fresh.push(v);
|
|
122
|
+
gap.push(lo - 0.5);
|
|
123
|
+
} else {
|
|
124
|
+
if (!rowOf) {
|
|
125
|
+
rowOf = new Int32Array(m);
|
|
126
|
+
for (let p = 0; p < was.length; p++) {
|
|
127
|
+
const x = wasRank[p];
|
|
128
|
+
if (!Number.isNaN(x)) rowOf[x] = p;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
shift = rowOf[k] - r;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
82
134
|
}
|
|
83
|
-
|
|
84
|
-
|
|
135
|
+
if (k < m) used[k] = 1;
|
|
136
|
+
out[r] = k;
|
|
137
|
+
} else if ((0, import_compare.typeRank)(v) === import_compare.RANK.missing) out[r] = Number.NaN;
|
|
85
138
|
else return null;
|
|
86
|
-
if (
|
|
139
|
+
if (++ops >= CHUNK) {
|
|
140
|
+
ops = 0;
|
|
141
|
+
yield;
|
|
142
|
+
}
|
|
87
143
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
144
|
+
if (!fresh.length && !used.includes(0)) return { ranks: out, state: { values, ranks: out, reps } };
|
|
145
|
+
const f = fresh.length;
|
|
146
|
+
const { order, eq } = yield* sortPositions(
|
|
147
|
+
f,
|
|
148
|
+
// By gap; two new strings in the same gap (cold: all of them) go to the collator.
|
|
149
|
+
(a, b) => gap[a] - gap[b] || collator.compare(fresh[a], fresh[b]),
|
|
150
|
+
// Comparing numbers is far cheaper: longer native runs, fewer merge passes.
|
|
151
|
+
m ? 8 * RUN : RUN
|
|
91
152
|
);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
153
|
+
const remap = new Float64Array(m + f);
|
|
154
|
+
const kept = [];
|
|
155
|
+
let rank = -1;
|
|
156
|
+
let j = 0;
|
|
157
|
+
for (let g = 0; g <= m; g++) {
|
|
158
|
+
while (j < f && gap[order[j]] < g) {
|
|
159
|
+
const id = order[j];
|
|
160
|
+
if (!eq[j]) kept[++rank] = fresh[id];
|
|
161
|
+
remap[m + id] = rank;
|
|
162
|
+
j++;
|
|
163
|
+
}
|
|
164
|
+
if (g < m && used[g]) {
|
|
165
|
+
kept[++rank] = reps[g];
|
|
166
|
+
remap[g] = rank;
|
|
167
|
+
}
|
|
100
168
|
}
|
|
101
|
-
const out = new Float64Array(n);
|
|
102
169
|
for (let r = 0; r < n; r++) {
|
|
103
|
-
const
|
|
104
|
-
out[r] =
|
|
170
|
+
const k = out[r];
|
|
171
|
+
if (!Number.isNaN(k)) out[r] = remap[k];
|
|
105
172
|
}
|
|
106
|
-
return out;
|
|
173
|
+
return { ranks: out, state: { values, ranks: out, reps: kept } };
|
|
107
174
|
}
|
|
108
175
|
// Annotate the CommonJS export names for ESM import in node:
|
|
109
176
|
0 && (module.exports = {
|