@datakable/core 1.0.0 → 1.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/README.md +4 -2
- package/dist/catalog/codes.cjs +12 -8
- package/dist/catalog/codes.cjs.map +1 -1
- package/dist/catalog/codes.js +12 -8
- package/dist/catalog/codes.js.map +1 -1
- package/dist/catalog/slots.cjs +10 -6
- package/dist/catalog/slots.cjs.map +1 -1
- package/dist/catalog/slots.js +10 -6
- package/dist/catalog/slots.js.map +1 -1
- package/dist/{codes-BKdS9EiO.d.cts → codes-B9bCK73-.d.cts} +46 -8
- package/dist/{codes-BKdS9EiO.d.ts → codes-B9bCK73-.d.ts} +46 -8
- package/dist/column/column-def.cjs.map +1 -1
- package/dist/column/get-value.cjs +6 -1
- package/dist/column/get-value.cjs.map +1 -1
- package/dist/column/get-value.js +6 -1
- package/dist/column/get-value.js.map +1 -1
- package/dist/column-filters.d.cts +2 -2
- package/dist/column-filters.d.ts +2 -2
- package/dist/diagnostics/build.cjs +10 -4
- package/dist/diagnostics/build.cjs.map +1 -1
- package/dist/diagnostics/build.js +8 -3
- package/dist/diagnostics/build.js.map +1 -1
- package/dist/diagnostics/catalog.cjs +11 -5
- package/dist/diagnostics/catalog.cjs.map +1 -1
- package/dist/diagnostics/catalog.js +10 -5
- package/dist/diagnostics/catalog.js.map +1 -1
- package/dist/diagnostics/codes.cjs +12 -8
- package/dist/diagnostics/codes.cjs.map +1 -1
- package/dist/diagnostics/codes.js +12 -8
- package/dist/diagnostics/codes.js.map +1 -1
- package/dist/diagnostics/console.cjs +13 -6
- package/dist/diagnostics/console.cjs.map +1 -1
- package/dist/diagnostics/console.js +13 -6
- package/dist/diagnostics/console.js.map +1 -1
- package/dist/diagnostics/env.cjs +1 -0
- package/dist/diagnostics/env.cjs.map +1 -1
- package/dist/diagnostics/env.js +1 -0
- package/dist/diagnostics/env.js.map +1 -1
- package/dist/diagnostics/explain-text.cjs +2 -1
- package/dist/diagnostics/explain-text.cjs.map +1 -1
- package/dist/diagnostics/explain-text.js +2 -1
- package/dist/diagnostics/explain-text.js.map +1 -1
- package/dist/diagnostics/explain.cjs +3 -2
- package/dist/diagnostics/explain.cjs.map +1 -1
- package/dist/diagnostics/explain.js +3 -2
- package/dist/diagnostics/explain.js.map +1 -1
- package/dist/diagnostics/hub.cjs +6 -5
- package/dist/diagnostics/hub.cjs.map +1 -1
- package/dist/diagnostics/hub.js +7 -6
- package/dist/diagnostics/hub.js.map +1 -1
- package/dist/diagnostics/process-guard.cjs +26 -0
- package/dist/diagnostics/process-guard.cjs.map +1 -0
- package/dist/diagnostics/process-guard.js +9 -0
- package/dist/diagnostics/process-guard.js.map +1 -0
- package/dist/diagnostics/prose.cjs +120 -0
- package/dist/diagnostics/prose.cjs.map +1 -0
- package/dist/diagnostics/prose.js +93 -0
- package/dist/diagnostics/prose.js.map +1 -0
- package/dist/diagnostics/redact.cjs +7 -4
- package/dist/diagnostics/redact.cjs.map +1 -1
- package/dist/diagnostics/redact.js +6 -4
- package/dist/diagnostics/redact.js.map +1 -1
- package/dist/diagnostics/trace-store.cjs +6 -0
- package/dist/diagnostics/trace-store.cjs.map +1 -1
- package/dist/diagnostics/trace-store.js +4 -0
- package/dist/diagnostics/trace-store.js.map +1 -1
- package/dist/diagnostics/tracer.cjs +74 -13
- package/dist/diagnostics/tracer.cjs.map +1 -1
- package/dist/diagnostics/tracer.js +75 -14
- package/dist/diagnostics/tracer.js.map +1 -1
- package/dist/diagnostics/types.cjs.map +1 -1
- package/dist/{explain-aagaYHcy.d.cts → explain-Q8Cg0cZy.d.cts} +13 -4
- package/dist/{explain-DcUZfFh5.d.ts → explain-i0OATi_W.d.ts} +13 -4
- package/dist/feature/codes.cjs +1 -0
- package/dist/feature/codes.cjs.map +1 -1
- package/dist/feature/codes.js +1 -0
- package/dist/feature/codes.js.map +1 -1
- package/dist/feature/define.cjs +3 -1
- package/dist/feature/define.cjs.map +1 -1
- package/dist/feature/define.js +3 -1
- package/dist/feature/define.js.map +1 -1
- package/dist/feature/install.cjs +4 -3
- package/dist/feature/install.cjs.map +1 -1
- package/dist/feature/install.js +4 -3
- package/dist/feature/install.js.map +1 -1
- package/dist/feature/runtime.cjs.map +1 -1
- package/dist/feature/runtime.js.map +1 -1
- package/dist/features/column-filters/codes.cjs +1 -0
- package/dist/features/column-filters/codes.cjs.map +1 -1
- package/dist/features/column-filters/codes.js +1 -0
- package/dist/features/column-filters/codes.js.map +1 -1
- package/dist/features/column-filters/column-filters.cjs +4 -2
- package/dist/features/column-filters/column-filters.cjs.map +1 -1
- package/dist/features/column-filters/column-filters.js +4 -2
- package/dist/features/column-filters/column-filters.js.map +1 -1
- package/dist/features/column-filters/match.cjs +5 -5
- package/dist/features/column-filters/match.cjs.map +1 -1
- package/dist/features/column-filters/match.js +5 -5
- package/dist/features/column-filters/match.js.map +1 -1
- package/dist/features/pagination/codes.cjs +6 -3
- package/dist/features/pagination/codes.cjs.map +1 -1
- package/dist/features/pagination/codes.js +6 -3
- package/dist/features/pagination/codes.js.map +1 -1
- package/dist/features/pagination/pagination.cjs +19 -14
- package/dist/features/pagination/pagination.cjs.map +1 -1
- package/dist/features/pagination/pagination.js +19 -14
- package/dist/features/pagination/pagination.js.map +1 -1
- package/dist/features/search/codes.cjs +1 -0
- package/dist/features/search/codes.cjs.map +1 -1
- package/dist/features/search/codes.js +1 -0
- package/dist/features/search/codes.js.map +1 -1
- package/dist/features/search/search-index.cjs +105 -48
- package/dist/features/search/search-index.cjs.map +1 -1
- package/dist/features/search/search-index.js +106 -49
- package/dist/features/search/search-index.js.map +1 -1
- package/dist/features/search/search.cjs +82 -24
- package/dist/features/search/search.cjs.map +1 -1
- package/dist/features/search/search.js +82 -24
- package/dist/features/search/search.js.map +1 -1
- package/dist/features/search/skipped-codes.cjs +45 -0
- package/dist/features/search/skipped-codes.cjs.map +1 -0
- package/dist/features/search/skipped-codes.js +20 -0
- package/dist/features/search/skipped-codes.js.map +1 -0
- package/dist/features/sorting/codes.cjs +1 -0
- package/dist/features/sorting/codes.cjs.map +1 -1
- package/dist/features/sorting/codes.js +1 -0
- package/dist/features/sorting/codes.js.map +1 -1
- package/dist/features/sorting/sorting.cjs +19 -23
- package/dist/features/sorting/sorting.cjs.map +1 -1
- package/dist/features/sorting/sorting.js +19 -23
- package/dist/features/sorting/sorting.js.map +1 -1
- package/dist/features/sorting/strategy.cjs +4 -4
- package/dist/features/sorting/strategy.cjs.map +1 -1
- package/dist/features/sorting/strategy.js +4 -4
- package/dist/features/sorting/strategy.js.map +1 -1
- package/dist/features/validity/codes.cjs +1 -0
- package/dist/features/validity/codes.cjs.map +1 -1
- package/dist/features/validity/codes.js +1 -0
- package/dist/features/validity/codes.js.map +1 -1
- package/dist/features/validity/validity.cjs +3 -3
- package/dist/features/validity/validity.cjs.map +1 -1
- package/dist/features/validity/validity.js +3 -3
- package/dist/features/validity/validity.js.map +1 -1
- package/dist/index.d.cts +16 -11
- package/dist/index.d.ts +16 -11
- package/dist/ingest/data-check-codes.cjs +4 -2
- package/dist/ingest/data-check-codes.cjs.map +1 -1
- package/dist/ingest/data-check-codes.js +4 -2
- package/dist/ingest/data-check-codes.js.map +1 -1
- package/dist/ingest/data-checks.cjs +1 -1
- package/dist/ingest/data-checks.cjs.map +1 -1
- package/dist/ingest/data-checks.js +1 -1
- package/dist/ingest/data-checks.js.map +1 -1
- package/dist/ingest/row-key-codes.cjs +1 -0
- package/dist/ingest/row-key-codes.cjs.map +1 -1
- package/dist/ingest/row-key-codes.js +1 -0
- package/dist/ingest/row-key-codes.js.map +1 -1
- package/dist/ingest/row-key.cjs +23 -20
- package/dist/ingest/row-key.cjs.map +1 -1
- package/dist/ingest/row-key.js +23 -20
- package/dist/ingest/row-key.js.map +1 -1
- package/dist/ingest/schema-codes.cjs +1 -0
- package/dist/ingest/schema-codes.cjs.map +1 -1
- package/dist/ingest/schema-codes.js +1 -0
- package/dist/ingest/schema-codes.js.map +1 -1
- package/dist/ingest/unique-fields.cjs +2 -2
- package/dist/ingest/unique-fields.cjs.map +1 -1
- package/dist/ingest/unique-fields.js +2 -2
- package/dist/ingest/unique-fields.js.map +1 -1
- package/dist/intercept/boundary.cjs +4 -2
- package/dist/intercept/boundary.cjs.map +1 -1
- package/dist/intercept/boundary.js +4 -2
- package/dist/intercept/boundary.js.map +1 -1
- package/dist/intercept/cell-batch.cjs +7 -4
- package/dist/intercept/cell-batch.cjs.map +1 -1
- package/dist/intercept/cell-batch.js +7 -4
- package/dist/intercept/cell-batch.js.map +1 -1
- package/dist/intercept/codes.cjs +1 -0
- package/dist/intercept/codes.cjs.map +1 -1
- package/dist/intercept/codes.js +1 -0
- package/dist/intercept/codes.js.map +1 -1
- package/dist/intercept/fallbacks.cjs +12 -20
- package/dist/intercept/fallbacks.cjs.map +1 -1
- package/dist/intercept/fallbacks.js +12 -20
- package/dist/intercept/fallbacks.js.map +1 -1
- package/dist/intercept/runtime.cjs +7 -9
- package/dist/intercept/runtime.cjs.map +1 -1
- package/dist/intercept/runtime.js +7 -9
- package/dist/intercept/runtime.js.map +1 -1
- package/dist/messages/index.cjs +4 -1
- package/dist/messages/index.cjs.map +1 -1
- package/dist/messages/index.js +4 -1
- 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/codes.cjs +1 -0
- package/dist/pipeline/codes.cjs.map +1 -1
- package/dist/pipeline/codes.js +1 -0
- package/dist/pipeline/codes.js.map +1 -1
- package/dist/pipeline/column-cache.cjs +94 -76
- package/dist/pipeline/column-cache.cjs.map +1 -1
- package/dist/pipeline/column-cache.js +92 -75
- package/dist/pipeline/column-cache.js.map +1 -1
- package/dist/pipeline/dev-checks.cjs +1 -0
- package/dist/pipeline/dev-checks.cjs.map +1 -1
- package/dist/pipeline/dev-checks.js +1 -0
- package/dist/pipeline/dev-checks.js.map +1 -1
- package/dist/pipeline/env-codes.cjs +3 -2
- package/dist/pipeline/env-codes.cjs.map +1 -1
- package/dist/pipeline/env-codes.js +3 -2
- package/dist/pipeline/env-codes.js.map +1 -1
- package/dist/pipeline/perf-codes.cjs +1 -0
- package/dist/pipeline/perf-codes.cjs.map +1 -1
- package/dist/pipeline/perf-codes.js +1 -0
- package/dist/pipeline/perf-codes.js.map +1 -1
- package/dist/pipeline/row.cjs +8 -7
- package/dist/pipeline/row.cjs.map +1 -1
- package/dist/pipeline/row.js +8 -7
- package/dist/pipeline/row.js.map +1 -1
- package/dist/pipeline/runner.cjs +17 -9
- package/dist/pipeline/runner.cjs.map +1 -1
- package/dist/pipeline/runner.js +17 -9
- package/dist/pipeline/runner.js.map +1 -1
- package/dist/pipeline/slice.cjs +3 -1
- package/dist/pipeline/slice.cjs.map +1 -1
- package/dist/pipeline/slice.js +3 -1
- package/dist/pipeline/slice.js.map +1 -1
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/{search-async-entry-C3mXxLP8.d.cts → search-async-entry-CuLYPfY_.d.cts} +2 -2
- package/dist/{search-async-entry-BmQYT120.d.ts → search-async-entry-Cx7xpuVZ.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 +8 -6
- package/dist/search.d.ts +8 -6
- package/dist/server.d.cts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/sorting.d.cts +6 -3
- package/dist/sorting.d.ts +6 -3
- package/dist/source/codes.cjs +1 -0
- package/dist/source/codes.cjs.map +1 -1
- package/dist/source/codes.js +1 -0
- package/dist/source/codes.js.map +1 -1
- package/dist/source/fetch-codes.cjs +1 -0
- package/dist/source/fetch-codes.cjs.map +1 -1
- package/dist/source/fetch-codes.js +1 -0
- package/dist/source/fetch-codes.js.map +1 -1
- package/dist/table/codes.cjs +1 -0
- package/dist/table/codes.cjs.map +1 -1
- package/dist/table/codes.js +1 -0
- package/dist/table/codes.js.map +1 -1
- package/dist/table/create-table.cjs +39 -9
- package/dist/table/create-table.cjs.map +1 -1
- package/dist/table/create-table.js +39 -9
- package/dist/table/create-table.js.map +1 -1
- package/dist/table/data-mutation.cjs +1 -0
- package/dist/table/data-mutation.cjs.map +1 -1
- package/dist/table/data-mutation.js +1 -0
- package/dist/table/data-mutation.js.map +1 -1
- package/dist/table/internals.cjs.map +1 -1
- package/dist/table/internals.js.map +1 -1
- package/dist/table/ssr-codes.cjs +1 -0
- package/dist/table/ssr-codes.cjs.map +1 -1
- package/dist/table/ssr-codes.js +1 -0
- package/dist/table/ssr-codes.js.map +1 -1
- package/dist/table/ssr.cjs +4 -1
- package/dist/table/ssr.cjs.map +1 -1
- package/dist/table/ssr.js +4 -1
- package/dist/table/ssr.js.map +1 -1
- package/dist/table/state-feedback.cjs +19 -0
- package/dist/table/state-feedback.cjs.map +1 -1
- package/dist/table/state-feedback.js +19 -0
- package/dist/table/state-feedback.js.map +1 -1
- 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/dist/versioning/codes.cjs +18 -0
- package/dist/versioning/codes.cjs.map +1 -1
- package/dist/versioning/codes.js +18 -0
- package/dist/versioning/codes.js.map +1 -1
- package/dist/versioning/registry.cjs +4 -2
- package/dist/versioning/registry.cjs.map +1 -1
- package/dist/versioning/registry.js +4 -2
- package/dist/versioning/registry.js.map +1 -1
- package/package.json +39 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/features/search/search-index.ts"],"sourcesContent":["import { ExSearch } from 'ex-search'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\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 the matching job. */\nconst CHUNK = 4096\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\nconst hasExSearch = () =>\n typeof ExSearch === 'function' &&\n typeof ExSearch.prototype.search === 'function' &&\n typeof ExSearch.prototype.setData === 'function'\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',\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 the cached strings: `String(v)` is what the default\n * formatter shows for a number. Only when no value is `NaN` — a `NaN` cell is `null` / `undefined`\n * (shown as `''`) or a real `NaN` (shown as `'NaN'`), which only the formatted cache tells apart.\n */\nfunction numberReader(values: Float64Array): Read | undefined {\n for (let r = 0; r < values.length; r++) if (Number.isNaN(values[r])) return undefined\n return (r) => String(values[r])\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`, every column is searchable at\n * weight 1. `null` when nothing is searchable. The values are read from the column cache, never\n * copied (§9.3 memory): a numeric column without `format` is stringified while matching, and the\n * case is folded per chunk while 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 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 let read: Read | undefined\n if (!col.format) {\n const values = ctx.getColumnValues(col.id)\n if (values instanceof Float64Array) read = numberReader(values)\n }\n if (!read) {\n const formatted = ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = (r) => formatted[r] ?? ''\n }\n keys.push({ name: `c${keys.length}`, weight })\n reads.push(read)\n yield\n }\n if (keys.length === 0) return null\n const fold = !config.caseSensitive\n const at = (k: number, r: number) => {\n const v = (reads[k] as Read)(r)\n return fold ? v.toLowerCase() : v\n }\n return useExSearch\n ? exSearchIndex(ctx.data.length, keys, at, config)\n : containsIndex(keys, at, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n matchChunk: (chunk: Uint32Array) => readonly Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n if (rows.length <= CHUNK) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += CHUNK) {\n for (const h of matchChunk(rows.subarray(k, k + CHUNK))) 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, receiver) {\n if (p === 'length') return length()\n const j = arrayIndex(p, length())\n return j >= 0 ? item(j) : Reflect.get(t, p, receiver)\n },\n has: (t, p) => arrayIndex(p, length()) >= 0 || Reflect.has(t, p),\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 config: MatchConfig,\n): SearchIndex {\n // Values are folded per chunk when not case-sensitive (buildIndex), and so is the query below.\n const engine = new ExSearch<Item>({\n keys: keys.map((k) => ({ name: k.name, weight: k.weight })),\n threshold: config.threshold,\n fuzzyMaxDistance: config.fuzzyMaxDistance,\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 const view = projection(size, keys, at)\n engine.setData(view.items)\n return {\n match(rows, query) {\n const q = config.caseSensitive ? query : query.toLowerCase()\n return chunked(rows, (chunk) => {\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 readonly 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 config: MatchConfig,\n): SearchIndex {\n return {\n match(rows, query) {\n // Values are folded per chunk (buildIndex).\n const q = config.caseSensitive ? query : query.toLowerCase()\n return chunked(rows, (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.\n */\nexport function rankHits(hits: readonly Hit[]): Uint32Array {\n const n = hits.length\n const index = new Uint32Array(n)\n const score = new Float64Array(n)\n const order = new Uint32Array(n)\n for (let k = 0; k < n; k++) {\n const h = hits[k] as Hit\n index[k] = h.i\n score[k] = h.score\n order[k] = k\n }\n order.sort(\n (a, b) =>\n (score[b] as number) - (score[a] as number) || (index[a] as number) - (index[b] as number),\n )\n const out = new Uint32Array(n)\n for (let k = 0; k < n; k++) out[k] = index[order[k] as number] as number\n return out\n}\n"],"mappings":"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","names":[]}
|
|
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,17 +1,23 @@
|
|
|
1
1
|
// src/features/search/search-index.ts
|
|
2
|
-
import { ExSearch } from "ex-search";
|
|
2
|
+
import { ExSearch, Scorer } from "ex-search";
|
|
3
|
+
import { getValue } from "../../column/get-value.js";
|
|
4
|
+
import "../../diagnostics/process-guard.js";
|
|
5
|
+
import { nullishOf } from "../../pipeline/column-cache.js";
|
|
3
6
|
import { resolveColumns } from "../../pipeline/columns.js";
|
|
4
7
|
import { arrayIndex } from "../../pipeline/row.js";
|
|
5
8
|
import { detectCapability, hasCapability } from "../../versioning/capability.js";
|
|
6
9
|
var CHUNK = 4096;
|
|
7
|
-
var
|
|
10
|
+
var CELLS = 16384;
|
|
11
|
+
var hasExSearch = () => [ExSearch.prototype.search, ExSearch.prototype.setData, Scorer.fuzzy].every(
|
|
12
|
+
(f) => typeof f === "function"
|
|
13
|
+
);
|
|
8
14
|
function probeExSearch(emit) {
|
|
9
15
|
return process.env.NODE_ENV !== "production" ? detectCapability(
|
|
10
16
|
hasExSearch,
|
|
11
17
|
{
|
|
12
18
|
feature: "search",
|
|
13
19
|
dependency: "ex-search",
|
|
14
|
-
capability: "ExSearch",
|
|
20
|
+
capability: "ExSearch, Scorer.fuzzy",
|
|
15
21
|
supported: "^1.1.1",
|
|
16
22
|
fallback: "case-insensitive exact / prefix / substring matching (no fuzzy matching)"
|
|
17
23
|
},
|
|
@@ -19,12 +25,38 @@ function probeExSearch(emit) {
|
|
|
19
25
|
) : hasCapability(hasExSearch);
|
|
20
26
|
}
|
|
21
27
|
function numberReader(values) {
|
|
22
|
-
|
|
23
|
-
return (r) =>
|
|
28
|
+
const nullish = nullishOf(values);
|
|
29
|
+
return (r) => {
|
|
30
|
+
const v = values[r];
|
|
31
|
+
return !Number.isNaN(v) ? String(v) : nullish?.[r] ? "" : "NaN";
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function* scan(size, get) {
|
|
35
|
+
const over = /* @__PURE__ */ new Map();
|
|
36
|
+
try {
|
|
37
|
+
for (let r = 0; r < size; r++) {
|
|
38
|
+
const v = get(r);
|
|
39
|
+
if (v !== null && (typeof v === "object" || typeof v === "function")) return false;
|
|
40
|
+
if (typeof v === "string" && /[\u0300-\uffff]/.test(v)) {
|
|
41
|
+
const n = v.normalize("NFC");
|
|
42
|
+
if (n !== v) over.set(r, n);
|
|
43
|
+
}
|
|
44
|
+
if (r % CHUNK === CHUNK - 1) yield;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
const read = (r) => {
|
|
50
|
+
const v = get(r);
|
|
51
|
+
return v === null || v === void 0 ? "" : String(v);
|
|
52
|
+
};
|
|
53
|
+
return over.size ? (r) => over.get(r) ?? read(r) : read;
|
|
24
54
|
}
|
|
25
55
|
function* buildIndex(ctx, config, useExSearch) {
|
|
26
56
|
const cols = resolveColumns(ctx.columns);
|
|
27
57
|
const declared = cols.list.some((c) => c.def.search !== void 0);
|
|
58
|
+
const data = ctx.data;
|
|
59
|
+
const n = data.length;
|
|
28
60
|
const keys = [];
|
|
29
61
|
const reads = [];
|
|
30
62
|
for (const col of cols.list) {
|
|
@@ -32,33 +64,53 @@ function* buildIndex(ctx, config, useExSearch) {
|
|
|
32
64
|
const w = col.def.search;
|
|
33
65
|
const weight = declared ? typeof w === "number" && w > 0 ? w : 0 : 1;
|
|
34
66
|
if (weight === 0) continue;
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
const path = col.format ? void 0 : col.path;
|
|
68
|
+
let failed = false;
|
|
69
|
+
const onError = () => {
|
|
70
|
+
failed = true;
|
|
71
|
+
};
|
|
72
|
+
let read = path ? yield* scan(n, (r) => getValue(data[r], path, onError)) : void 0;
|
|
73
|
+
if (failed || read === false && declared) read = void 0;
|
|
74
|
+
if (read === void 0) {
|
|
37
75
|
const values = ctx.getColumnValues(col.id);
|
|
38
|
-
if (values
|
|
76
|
+
if (!values) continue;
|
|
77
|
+
yield;
|
|
78
|
+
const numbers = values instanceof Float64Array;
|
|
79
|
+
if (numbers && !col.format) read = numberReader(values);
|
|
80
|
+
else {
|
|
81
|
+
const own = numbers || declared && col.format ? void 0 : yield* scan(n, (r) => values[r]);
|
|
82
|
+
if (own === false && !declared) read = false;
|
|
83
|
+
else if (own && !col.format) read = own;
|
|
84
|
+
else {
|
|
85
|
+
const sliced = ctx.formattedSliced;
|
|
86
|
+
const formatted = sliced ? yield* sliced(col.id) : ctx.getFormattedValues(col.id);
|
|
87
|
+
if (!formatted) continue;
|
|
88
|
+
read = yield* scan(n, (r) => formatted[r]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
39
91
|
}
|
|
40
92
|
if (!read) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
93
|
+
if (process.env.NODE_ENV !== "production" && read === false) {
|
|
94
|
+
ctx.emit("DK_SEARCH_COLUMN_SKIPPED", { column: col.id, index: col.index });
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
44
97
|
}
|
|
45
98
|
keys.push({ name: `c${keys.length}`, weight });
|
|
46
99
|
reads.push(read);
|
|
47
100
|
yield;
|
|
48
101
|
}
|
|
49
102
|
if (keys.length === 0) return null;
|
|
50
|
-
const fold =
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
};
|
|
55
|
-
return useExSearch ? exSearchIndex(ctx.data.length, keys, at, config) : containsIndex(keys, at, config);
|
|
103
|
+
const fold = (v) => config.caseSensitive ? v : v.toLowerCase();
|
|
104
|
+
const foldQuery = (q) => fold(q.normalize("NFC"));
|
|
105
|
+
const at = (k, r) => fold(reads[k](r));
|
|
106
|
+
return useExSearch ? exSearchIndex(ctx.data.length, keys, at, foldQuery, config) : containsIndex(keys, at, foldQuery, config);
|
|
56
107
|
}
|
|
57
|
-
function* chunked(rows, matchChunk) {
|
|
58
|
-
|
|
108
|
+
function* chunked(rows, columns, matchChunk) {
|
|
109
|
+
const size = Math.max(256, Math.floor(CELLS / columns));
|
|
110
|
+
if (rows.length <= size) return rankHits(matchChunk(rows));
|
|
59
111
|
const hits = [];
|
|
60
|
-
for (let k = 0; k < rows.length; k +=
|
|
61
|
-
for (const h of matchChunk(rows.subarray(k, k +
|
|
112
|
+
for (let k = 0; k < rows.length; k += size) {
|
|
113
|
+
for (const h of matchChunk(rows.subarray(k, k + size))) hits.push(h);
|
|
62
114
|
yield;
|
|
63
115
|
}
|
|
64
116
|
return rankHits(hits);
|
|
@@ -74,12 +126,12 @@ function projection(size, keys, at) {
|
|
|
74
126
|
return o;
|
|
75
127
|
};
|
|
76
128
|
const items = new Proxy(target, {
|
|
77
|
-
get(t, p
|
|
129
|
+
get(t, p) {
|
|
78
130
|
if (p === "length") return length();
|
|
79
131
|
const j = arrayIndex(p, length());
|
|
80
|
-
return j >= 0 ? item(j) :
|
|
132
|
+
return j >= 0 ? item(j) : t[p];
|
|
81
133
|
},
|
|
82
|
-
has: (t, p) => arrayIndex(p, length()) >= 0 ||
|
|
134
|
+
has: (t, p) => arrayIndex(p, length()) >= 0 || p in t
|
|
83
135
|
});
|
|
84
136
|
return {
|
|
85
137
|
items,
|
|
@@ -89,22 +141,39 @@ function projection(size, keys, at) {
|
|
|
89
141
|
}
|
|
90
142
|
};
|
|
91
143
|
}
|
|
92
|
-
function exSearchIndex(size, keys, at, config) {
|
|
144
|
+
function exSearchIndex(size, keys, at, fold, config) {
|
|
93
145
|
const engine = new ExSearch({
|
|
94
|
-
keys
|
|
95
|
-
|
|
96
|
-
fuzzyMaxDistance: config.fuzzyMaxDistance,
|
|
146
|
+
keys,
|
|
147
|
+
...config,
|
|
97
148
|
caseSensitive: true,
|
|
98
149
|
// DataKable re-ranks with exact scores and an index tie-break; tim keeps ex-search's own pass
|
|
99
150
|
// stable and avoids radix's score quantisation.
|
|
100
151
|
sortAlgorithm: "tim"
|
|
101
152
|
});
|
|
102
|
-
const
|
|
153
|
+
const max = config.fuzzyMaxDistance;
|
|
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);
|
|
103
171
|
engine.setData(view.items);
|
|
104
172
|
return {
|
|
105
173
|
match(rows, query) {
|
|
106
|
-
const
|
|
107
|
-
return chunked(rows, (chunk) => {
|
|
174
|
+
const folded = fold(query);
|
|
175
|
+
return chunked(rows, keys.length, (chunk) => {
|
|
176
|
+
q = folded;
|
|
108
177
|
view.show(chunk);
|
|
109
178
|
try {
|
|
110
179
|
return engine.search(q);
|
|
@@ -115,11 +184,11 @@ function exSearchIndex(size, keys, at, config) {
|
|
|
115
184
|
}
|
|
116
185
|
};
|
|
117
186
|
}
|
|
118
|
-
function containsIndex(keys, at, config) {
|
|
187
|
+
function containsIndex(keys, at, fold, config) {
|
|
119
188
|
return {
|
|
120
189
|
match(rows, query) {
|
|
121
|
-
const q =
|
|
122
|
-
return chunked(rows, (chunk) => {
|
|
190
|
+
const q = fold(query);
|
|
191
|
+
return chunked(rows, keys.length, (chunk) => {
|
|
123
192
|
const hits = [];
|
|
124
193
|
for (let j = 0; j < chunk.length; j++) {
|
|
125
194
|
const r = chunk[j];
|
|
@@ -138,22 +207,10 @@ function containsIndex(keys, at, config) {
|
|
|
138
207
|
};
|
|
139
208
|
}
|
|
140
209
|
function rankHits(hits) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const order = new Uint32Array(n);
|
|
145
|
-
for (let k = 0; k < n; k++) {
|
|
146
|
-
const h = hits[k];
|
|
147
|
-
index[k] = h.i;
|
|
148
|
-
score[k] = h.score;
|
|
149
|
-
order[k] = k;
|
|
150
|
-
}
|
|
151
|
-
order.sort(
|
|
152
|
-
(a, b) => score[b] - score[a] || index[a] - index[b]
|
|
210
|
+
return Uint32Array.from(
|
|
211
|
+
hits.sort((a, b) => b.score - a.score || a.i - b.i),
|
|
212
|
+
(h) => h.i
|
|
153
213
|
);
|
|
154
|
-
const out = new Uint32Array(n);
|
|
155
|
-
for (let k = 0; k < n; k++) out[k] = index[order[k]];
|
|
156
|
-
return out;
|
|
157
214
|
}
|
|
158
215
|
export {
|
|
159
216
|
buildIndex,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/features/search/search-index.ts"],"sourcesContent":["import { ExSearch } from 'ex-search'\nimport type { Emit } from '../../diagnostics/types'\nimport type { RowModelContext } from '../../feature/types'\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 the matching job. */\nconst CHUNK = 4096\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\nconst hasExSearch = () =>\n typeof ExSearch === 'function' &&\n typeof ExSearch.prototype.search === 'function' &&\n typeof ExSearch.prototype.setData === 'function'\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',\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 the cached strings: `String(v)` is what the default\n * formatter shows for a number. Only when no value is `NaN` — a `NaN` cell is `null` / `undefined`\n * (shown as `''`) or a real `NaN` (shown as `'NaN'`), which only the formatted cache tells apart.\n */\nfunction numberReader(values: Float64Array): Read | undefined {\n for (let r = 0; r < values.length; r++) if (Number.isNaN(values[r])) return undefined\n return (r) => String(values[r])\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`, every column is searchable at\n * weight 1. `null` when nothing is searchable. The values are read from the column cache, never\n * copied (§9.3 memory): a numeric column without `format` is stringified while matching, and the\n * case is folded per chunk while 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 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 let read: Read | undefined\n if (!col.format) {\n const values = ctx.getColumnValues(col.id)\n if (values instanceof Float64Array) read = numberReader(values)\n }\n if (!read) {\n const formatted = ctx.getFormattedValues(col.id)\n if (!formatted) continue\n read = (r) => formatted[r] ?? ''\n }\n keys.push({ name: `c${keys.length}`, weight })\n reads.push(read)\n yield\n }\n if (keys.length === 0) return null\n const fold = !config.caseSensitive\n const at = (k: number, r: number) => {\n const v = (reads[k] as Read)(r)\n return fold ? v.toLowerCase() : v\n }\n return useExSearch\n ? exSearchIndex(ctx.data.length, keys, at, config)\n : containsIndex(keys, at, config)\n}\n\n/** Matches `rows` chunk by chunk (`score` decides alone, so chunks match like the whole). */\nfunction* chunked(\n rows: Uint32Array,\n matchChunk: (chunk: Uint32Array) => readonly Hit[],\n): Generator<undefined, Uint32Array, undefined> {\n if (rows.length <= CHUNK) return rankHits(matchChunk(rows))\n const hits: Hit[] = []\n for (let k = 0; k < rows.length; k += CHUNK) {\n for (const h of matchChunk(rows.subarray(k, k + CHUNK))) 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, receiver) {\n if (p === 'length') return length()\n const j = arrayIndex(p, length())\n return j >= 0 ? item(j) : Reflect.get(t, p, receiver)\n },\n has: (t, p) => arrayIndex(p, length()) >= 0 || Reflect.has(t, p),\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 config: MatchConfig,\n): SearchIndex {\n // Values are folded per chunk when not case-sensitive (buildIndex), and so is the query below.\n const engine = new ExSearch<Item>({\n keys: keys.map((k) => ({ name: k.name, weight: k.weight })),\n threshold: config.threshold,\n fuzzyMaxDistance: config.fuzzyMaxDistance,\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 const view = projection(size, keys, at)\n engine.setData(view.items)\n return {\n match(rows, query) {\n const q = config.caseSensitive ? query : query.toLowerCase()\n return chunked(rows, (chunk) => {\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 readonly 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 config: MatchConfig,\n): SearchIndex {\n return {\n match(rows, query) {\n // Values are folded per chunk (buildIndex).\n const q = config.caseSensitive ? query : query.toLowerCase()\n return chunked(rows, (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.\n */\nexport function rankHits(hits: readonly Hit[]): Uint32Array {\n const n = hits.length\n const index = new Uint32Array(n)\n const score = new Float64Array(n)\n const order = new Uint32Array(n)\n for (let k = 0; k < n; k++) {\n const h = hits[k] as Hit\n index[k] = h.i\n score[k] = h.score\n order[k] = k\n }\n order.sort(\n (a, b) =>\n (score[b] as number) - (score[a] as number) || (index[a] as number) - (index[b] as number),\n )\n const out = new Uint32Array(n)\n for (let k = 0; k < n; k++) out[k] = index[order[k] as number] as number\n return out\n}\n"],"mappings":"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","names":[]}
|
|
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":[]}
|
|
@@ -23,19 +23,22 @@ __export(search_exports, {
|
|
|
23
23
|
search: () => search
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(search_exports);
|
|
26
|
+
var import_debounce = require("kk-debounce/debounce");
|
|
26
27
|
var import_debounceSignal = require("kk-debounce/debounceSignal");
|
|
27
28
|
var import_throttle = require("kk-debounce/throttle");
|
|
28
29
|
var import_codes = require("../../catalog/codes.cjs");
|
|
29
30
|
var import_options = require("../../catalog/options.cjs");
|
|
30
31
|
var import_build = require("../../diagnostics/build.cjs");
|
|
31
32
|
var import_catalog = require("../../diagnostics/catalog.cjs");
|
|
33
|
+
var import_process_guard = require("../../diagnostics/process-guard.cjs");
|
|
32
34
|
var import_define = require("../../feature/define.cjs");
|
|
33
35
|
var import_define2 = require("../../intercept/define.cjs");
|
|
34
36
|
var import_slice = require("../../pipeline/slice.cjs");
|
|
35
37
|
var import_mode = require("../../source/mode.cjs");
|
|
36
38
|
var import_search_fn_brand = require("./search-fn-brand.cjs");
|
|
37
39
|
var import_search_index = require("./search-index.cjs");
|
|
38
|
-
|
|
40
|
+
var import_skipped_codes = require("./skipped-codes.cjs");
|
|
41
|
+
(0, import_catalog.keepDiagnostics)(import_codes.optionDiagnostics, import_skipped_codes.skippedColumnDiagnostics);
|
|
39
42
|
var DEFAULT_WAIT = 300;
|
|
40
43
|
var isDuration = (v) => {
|
|
41
44
|
if (typeof v === "number") return Number.isFinite(v) && v >= 0;
|
|
@@ -90,7 +93,7 @@ var searchOptions = {
|
|
|
90
93
|
}
|
|
91
94
|
};
|
|
92
95
|
var settledSuperseded = () => Promise.resolve(import_search_fn_brand.SUPERSEDED);
|
|
93
|
-
var toText = (q) =>
|
|
96
|
+
var toText = (q) => q == null ? "" : String(q);
|
|
94
97
|
var search = (opts = {}) => {
|
|
95
98
|
const config = {
|
|
96
99
|
threshold: opts.threshold ?? 0.3,
|
|
@@ -115,13 +118,17 @@ var search = (opts = {}) => {
|
|
|
115
118
|
(0, import_build.throwConfigError)("DK_INVALID_OPTION", {
|
|
116
119
|
path: "searchFn",
|
|
117
120
|
feature: "search",
|
|
118
|
-
expected: "withSearchFn
|
|
119
|
-
|
|
121
|
+
expected: "wrapped|withSearchFn|searchFn|search-async",
|
|
122
|
+
// prose key (diagnostics/prose.ts)
|
|
123
|
+
received: typeof opts.searchFn,
|
|
120
124
|
suggestions: [],
|
|
121
125
|
phase: "init"
|
|
122
126
|
});
|
|
123
127
|
}
|
|
124
128
|
const instances = /* @__PURE__ */ new WeakMap();
|
|
129
|
+
const resumes = /* @__PURE__ */ new WeakMap();
|
|
130
|
+
const ran = /* @__PURE__ */ new WeakMap();
|
|
131
|
+
let waited = false;
|
|
125
132
|
function indexFor(ctx) {
|
|
126
133
|
let byColumns = indexes.get(ctx.data);
|
|
127
134
|
if (!byColumns) {
|
|
@@ -129,7 +136,7 @@ var search = (opts = {}) => {
|
|
|
129
136
|
indexes.set(ctx.data, byColumns);
|
|
130
137
|
}
|
|
131
138
|
let key = byColumns.get(ctx.columns);
|
|
132
|
-
if (key
|
|
139
|
+
if (!key) {
|
|
133
140
|
key = {};
|
|
134
141
|
byColumns.set(ctx.columns, key);
|
|
135
142
|
}
|
|
@@ -148,7 +155,10 @@ var search = (opts = {}) => {
|
|
|
148
155
|
}
|
|
149
156
|
return (0, import_slice.sliced)(ctx, matching2.key, () => index.match(rows, query));
|
|
150
157
|
}
|
|
151
|
-
const standIn = (indices) =>
|
|
158
|
+
const standIn = (indices) => {
|
|
159
|
+
waited = true;
|
|
160
|
+
return shown && shown.input === indices ? shown.output : indices;
|
|
161
|
+
};
|
|
152
162
|
function step(indices, state, ctx) {
|
|
153
163
|
const query = typeof state?.query === "string" ? state.query.trim() : "";
|
|
154
164
|
if (query === "") return indices;
|
|
@@ -161,7 +171,7 @@ var search = (opts = {}) => {
|
|
|
161
171
|
ctx.emit("DK_OPTION_NO_EFFECT", {
|
|
162
172
|
path: "query",
|
|
163
173
|
feature: "search",
|
|
164
|
-
reason: "
|
|
174
|
+
reason: "not-searchable",
|
|
165
175
|
once: false
|
|
166
176
|
});
|
|
167
177
|
}
|
|
@@ -190,14 +200,27 @@ var search = (opts = {}) => {
|
|
|
190
200
|
interceptors: [
|
|
191
201
|
(0, import_define2.defineInterceptor)({
|
|
192
202
|
name: "search",
|
|
193
|
-
on:
|
|
203
|
+
on: ["table:destroy", "pipeline:run", "table:init", "state:dispatch", "table:setOptions"],
|
|
194
204
|
intercept(ctx, next) {
|
|
195
205
|
const table = ctx.table;
|
|
196
206
|
if (ctx.op === "table:destroy") {
|
|
197
207
|
instances.get(table)?.();
|
|
198
208
|
return next();
|
|
199
209
|
}
|
|
200
|
-
|
|
210
|
+
if (ctx.op !== "pipeline:run") {
|
|
211
|
+
const out = next();
|
|
212
|
+
resumes.get(table)?.();
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
const prev = waited;
|
|
216
|
+
waited = false;
|
|
217
|
+
try {
|
|
218
|
+
return engine ? engine.run(table, next) : next();
|
|
219
|
+
} finally {
|
|
220
|
+
const w = waited;
|
|
221
|
+
waited = prev;
|
|
222
|
+
ran.get(table)?.(w);
|
|
223
|
+
}
|
|
201
224
|
}
|
|
202
225
|
})
|
|
203
226
|
],
|
|
@@ -206,11 +229,12 @@ var search = (opts = {}) => {
|
|
|
206
229
|
const write = (input2, query, status, error) => {
|
|
207
230
|
const s = ctx.getState();
|
|
208
231
|
if (status !== "error" && s?.input === input2 && s.query === query && s.status === status && !("error" in s)) {
|
|
209
|
-
return;
|
|
232
|
+
return false;
|
|
210
233
|
}
|
|
211
234
|
ctx.setState(
|
|
212
235
|
status === "error" ? { input: input2, query, status, error } : { input: input2, query, status }
|
|
213
236
|
);
|
|
237
|
+
return ctx.getState() !== s;
|
|
214
238
|
};
|
|
215
239
|
const applied = () => ctx.getState()?.query ?? "";
|
|
216
240
|
const input = () => ctx.getState()?.input ?? "";
|
|
@@ -218,11 +242,6 @@ var search = (opts = {}) => {
|
|
|
218
242
|
const waitFor = (query) => new Promise((resolve) => {
|
|
219
243
|
waiting.push({ query, resolve });
|
|
220
244
|
});
|
|
221
|
-
const settleWaiting = (query, outcome) => {
|
|
222
|
-
const list = waiting;
|
|
223
|
-
waiting = [];
|
|
224
|
-
for (const w of list) w.resolve(w.query === query ? outcome : import_search_fn_brand.SUPERSEDED);
|
|
225
|
-
};
|
|
226
245
|
const replaceWaiting = (query) => {
|
|
227
246
|
if (waiting.length === 0) return;
|
|
228
247
|
const keep = [];
|
|
@@ -238,6 +257,22 @@ var search = (opts = {}) => {
|
|
|
238
257
|
waiting = [];
|
|
239
258
|
return keep;
|
|
240
259
|
};
|
|
260
|
+
let held = [];
|
|
261
|
+
let busy = false;
|
|
262
|
+
const release = (outcome) => {
|
|
263
|
+
const list = held;
|
|
264
|
+
held = [];
|
|
265
|
+
for (const w of list) w.resolve(outcome);
|
|
266
|
+
};
|
|
267
|
+
const supersede = () => {
|
|
268
|
+
held = [...waiting, ...held];
|
|
269
|
+
waiting = [];
|
|
270
|
+
release(import_search_fn_brand.SUPERSEDED);
|
|
271
|
+
};
|
|
272
|
+
ran.set(table, (w) => {
|
|
273
|
+
busy = w;
|
|
274
|
+
if (!w) release(import_search_fn_brand.APPLIED);
|
|
275
|
+
});
|
|
241
276
|
const session = engine?.session({
|
|
242
277
|
ctx,
|
|
243
278
|
write,
|
|
@@ -245,10 +280,13 @@ var search = (opts = {}) => {
|
|
|
245
280
|
input,
|
|
246
281
|
claim
|
|
247
282
|
});
|
|
283
|
+
let scheduled = false;
|
|
248
284
|
const applyQuery = (query, inp = input()) => {
|
|
285
|
+
scheduled = false;
|
|
249
286
|
if (session?.apply(query, inp)) return;
|
|
250
|
-
|
|
251
|
-
|
|
287
|
+
if (held[0]?.query !== query) release(import_search_fn_brand.SUPERSEDED);
|
|
288
|
+
held.push(...claim(query));
|
|
289
|
+
if (!write(inp, query, "idle") && !busy) release(import_search_fn_brand.APPLIED);
|
|
252
290
|
};
|
|
253
291
|
let calling = false;
|
|
254
292
|
let appliedNow;
|
|
@@ -258,10 +296,27 @@ var search = (opts = {}) => {
|
|
|
258
296
|
};
|
|
259
297
|
const timer = strategy === "debounce" ? (0, import_debounceSignal.debouncedSignal)(applied, apply, wait, { behavior: typing.behavior ?? "trailing" }) : strategy === "throttle" ? (0, import_throttle.throttle)(apply, wait) : void 0;
|
|
260
298
|
const resting = () => session?.busy() ? "searching" : "idle";
|
|
261
|
-
|
|
299
|
+
resumes.set(table, () => {
|
|
300
|
+
const s = ctx.getState();
|
|
301
|
+
const pending = s?.status === "pending";
|
|
302
|
+
if (pending ? !scheduled : s?.status === "searching" && !session?.busy()) {
|
|
303
|
+
const inp = toText(s.input);
|
|
304
|
+
write(inp, pending && strategy !== "enter" ? target(inp) : toText(s.query), resting());
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
const drop = (0, import_debounce.debounce)(() => {
|
|
308
|
+
const list = waiting;
|
|
309
|
+
waiting = [];
|
|
310
|
+
for (const w of list) w.resolve(import_search_fn_brand.SUPERSEDED);
|
|
311
|
+
}, wait);
|
|
312
|
+
const stop = () => {
|
|
262
313
|
timer?.cancel();
|
|
314
|
+
drop.cancel();
|
|
315
|
+
};
|
|
316
|
+
instances.set(table, () => {
|
|
317
|
+
stop();
|
|
263
318
|
session?.destroy();
|
|
264
|
-
|
|
319
|
+
supersede();
|
|
265
320
|
});
|
|
266
321
|
const commit = (next) => {
|
|
267
322
|
const current = applied();
|
|
@@ -273,7 +328,7 @@ var search = (opts = {}) => {
|
|
|
273
328
|
return p;
|
|
274
329
|
}
|
|
275
330
|
if (!timer || query === current) {
|
|
276
|
-
|
|
331
|
+
stop();
|
|
277
332
|
applyQuery(query, next);
|
|
278
333
|
return p;
|
|
279
334
|
}
|
|
@@ -290,13 +345,15 @@ var search = (opts = {}) => {
|
|
|
290
345
|
}
|
|
291
346
|
if (strategy === "debounce" && typing.behavior === "leading") {
|
|
292
347
|
write(next, current, resting());
|
|
348
|
+
drop();
|
|
293
349
|
return p;
|
|
294
350
|
}
|
|
351
|
+
scheduled = true;
|
|
295
352
|
write(next, current, "pending");
|
|
296
353
|
return p;
|
|
297
354
|
};
|
|
298
355
|
const applyNow = (next) => {
|
|
299
|
-
|
|
356
|
+
stop();
|
|
300
357
|
const query = target(next);
|
|
301
358
|
replaceWaiting(query);
|
|
302
359
|
const p = waitFor(query);
|
|
@@ -307,12 +364,13 @@ var search = (opts = {}) => {
|
|
|
307
364
|
setSearch: (query) => commit(toText(query)),
|
|
308
365
|
flushSearch: () => applyNow(input()),
|
|
309
366
|
cancelSearch: () => {
|
|
310
|
-
|
|
367
|
+
scheduled = false;
|
|
368
|
+
stop();
|
|
311
369
|
session?.abort();
|
|
312
|
-
|
|
370
|
+
supersede();
|
|
313
371
|
const s = ctx.getState();
|
|
314
372
|
if (s?.status === "pending" || s?.status === "searching") {
|
|
315
|
-
ctx.setState({
|
|
373
|
+
ctx.setState({ ...s, status: "idle" });
|
|
316
374
|
}
|
|
317
375
|
},
|
|
318
376
|
clearSearch: () => {
|