@vielzeug/scout 3.0.0 → 26.10.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/adapters.cjs +1 -1
- package/dist/adapters.cjs.map +1 -1
- package/dist/adapters.d.ts +2 -1
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +1 -12
- package/dist/adapters.js.map +1 -1
- package/dist/entries.cjs +2 -0
- package/dist/entries.cjs.map +1 -0
- package/dist/entries.d.ts +45 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +2 -0
- package/dist/entries.js.map +1 -0
- package/dist/errors.cjs +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -9
- package/dist/errors.js.map +1 -1
- package/dist/highlight.cjs +1 -1
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +1 -45
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -7
- package/dist/reactive.cjs +1 -1
- package/dist/reactive.cjs.map +1 -1
- package/dist/reactive.d.ts.map +1 -1
- package/dist/reactive.js +1 -114
- package/dist/reactive.js.map +1 -1
- package/dist/scout-index.cjs +1 -1
- package/dist/scout-index.cjs.map +1 -1
- package/dist/scout-index.d.ts +1 -1
- package/dist/scout-index.js +1 -191
- package/dist/scout-index.js.map +1 -1
- package/dist/scout.cjs +1 -1
- package/dist/scout.cjs.map +1 -1
- package/dist/scout.iife.js +1 -1
- package/dist/scout.iife.js.map +1 -1
- package/dist/scout.js +1 -1
- package/dist/scout.js.map +1 -1
- package/dist/segment.cjs +1 -1
- package/dist/segment.cjs.map +1 -1
- package/dist/segment.js +1 -12
- package/dist/segment.js.map +1 -1
- package/dist/tokenize.cjs +1 -1
- package/dist/tokenize.cjs.map +1 -1
- package/dist/tokenize.js +1 -10
- package/dist/tokenize.js.map +1 -1
- package/dist/trigram.cjs +1 -1
- package/dist/trigram.cjs.map +1 -1
- package/dist/trigram.js +1 -15
- package/dist/trigram.js.map +1 -1
- package/dist/types.d.ts +2 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -5
package/dist/reactive.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reactive.js","names":[],"sources":["../src/reactive.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"reactive.js","names":[],"sources":["../src/reactive.ts"],"sourcesContent":["import { tapper } from '@vielzeug/arsenal';\n\nimport { ScoutConfigError, ScoutDisposedError } from './errors';\nimport { createIndex, type ScoutIndex } from './scout-index';\nimport type {\n CreateSearchOptions,\n ScoutEvent,\n ScoutIndexOptions,\n SearchResult,\n SearchSnapshot,\n SearchState,\n SearchSubscribeOptions,\n} from './types';\n\nexport type ReactiveSearch<T> = SearchState<T> & { readonly index: ScoutIndex<T> };\n\nconst DEFAULT_DEBOUNCE = 200;\n\nfunction notify(listeners: Set<() => void>): void {\n for (const listener of [...listeners]) {\n try {\n listener();\n } catch (error) {\n queueMicrotask(() => {\n throw error;\n });\n }\n }\n}\n\nfunction subscribe(\n listeners: Set<() => void>,\n listener: () => void,\n options: SearchSubscribeOptions | undefined,\n disposed: boolean,\n lifetimeSignal: AbortSignal,\n): () => void {\n if (disposed || options?.signal?.aborted) return () => {};\n\n listeners.add(listener);\n const unsubscribe = (): void => {\n listeners.delete(listener);\n options?.signal?.removeEventListener('abort', unsubscribe);\n };\n options?.signal?.addEventListener('abort', unsubscribe, { once: true, signal: lifetimeSignal });\n return unsubscribe;\n}\n\nexport function createSearch<T>(index: ScoutIndex<T>, options: CreateSearchOptions = {}): SearchState<T> {\n const { debounce: debounceMs = DEFAULT_DEBOUNCE, limit, minQueryLength, threshold } = options;\n\n if (!Number.isSafeInteger(debounceMs) || debounceMs < 0) {\n throw new ScoutConfigError('debounce must be a finite non-negative integer.');\n }\n\n const search = (query: string) => index.search(query, { limit, minQueryLength, threshold });\n const createSnapshot = (\n query: string,\n isSearching: boolean,\n results: ReadonlyArray<SearchResult<T>>,\n ): SearchSnapshot<T> => Object.freeze({ isSearching, query, results: Object.freeze(results) });\n const listeners = new Set<() => void>();\n const tappers = tapper<ScoutEvent<T>>();\n const controller = new AbortController();\n let snapshot = createSnapshot('', false, search(''));\n let committedQuery = '';\n let disposed = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const publish = (next: SearchSnapshot<T>): void => {\n snapshot = next;\n notify(listeners);\n tappers.emit({ snapshot: next, type: 'state-change' });\n };\n const commit = (query: string): void => {\n committedQuery = query;\n publish(createSnapshot(query, false, search(query)));\n };\n const unsubscribeIndex = index.onMutate(() => {\n publish(createSnapshot(snapshot.query, snapshot.isSearching, search(committedQuery)));\n });\n const assertActive = (method: string): void => {\n if (disposed) throw new ScoutDisposedError(`SearchState.${method}() called after dispose()`);\n };\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n unsubscribeIndex();\n listeners.clear();\n tappers.clear();\n controller.abort();\n };\n\n return {\n clear() {\n assertActive('clear');\n if (snapshot.query === '' && !snapshot.isSearching) return;\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n commit('');\n },\n get disposalSignal() {\n return controller.signal;\n },\n dispose,\n get disposed() {\n return disposed;\n },\n getSnapshot() {\n return snapshot;\n },\n setQuery(query) {\n assertActive('setQuery');\n if (query === snapshot.query) return;\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n\n if (query === committedQuery) {\n publish(createSnapshot(query, false, snapshot.results));\n return;\n }\n\n if (debounceMs === 0) {\n commit(query);\n return;\n }\n\n publish(createSnapshot(query, true, snapshot.results));\n timer = setTimeout(() => {\n timer = undefined;\n commit(query);\n }, debounceMs);\n },\n subscribe(listener, subscribeOptions) {\n return subscribe(listeners, listener, subscribeOptions, disposed, controller.signal);\n },\n tap(handler, tapOptions) {\n if (disposed) return () => {};\n\n return tappers.tap(handler, tapOptions);\n },\n [Symbol.dispose]: dispose,\n };\n}\n\nexport function createReactiveSearch<T>(\n items: T[],\n options: ScoutIndexOptions<T> & Pick<CreateSearchOptions, 'debounce'>,\n): ReactiveSearch<T> {\n const index = createIndex(items, options);\n const state = createSearch(index, options);\n\n return {\n clear: state.clear,\n get disposalSignal() {\n return state.disposalSignal;\n },\n dispose: state.dispose,\n get disposed() {\n return state.disposed;\n },\n getSnapshot: state.getSnapshot,\n index,\n setQuery: state.setQuery,\n subscribe: state.subscribe,\n tap: state.tap,\n [Symbol.dispose]: state[Symbol.dispose],\n };\n}\n"],"mappings":"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"}
|
package/dist/scout-index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./errors.cjs"),t=require("./tokenize.cjs"),n=require("./highlight.cjs"),r=require("./trigram.cjs");function
|
|
1
|
+
const e=require("./errors.cjs"),t=require("./tokenize.cjs"),n=require("./highlight.cjs"),r=require("./trigram.cjs");function requireFiniteInteger(t,n,r){if(!Number.isFinite(t)||!Number.isInteger(t)||t<r)throw new e.ScoutConfigError(`${n} must be a finite integer greater than or equal to ${r}.`);return t}function requireFiniteNumber(t,n,r,i=1/0){if(!Number.isFinite(t)||t<r||t>i)throw new e.ScoutConfigError(`${n} must be a finite number between ${r} and ${i}.`);return t}function resolveFields(e){return e.map(e=>typeof e==`string`?{field:e,stringify:t.defaultStringify,weight:1}:{field:e.field,stringify:e.stringify??t.defaultStringify,weight:requireFiniteNumber(e.weight??1,`weight for field "${e.field}"`,Number.MIN_VALUE)})}function createIndex(i,a){if(a.fields.length===0)throw new e.ScoutConfigError(`createIndex: at least one field is required.`);let o=resolveFields(a.fields),s=o.reduce((e,t)=>Math.max(e,t.weight),1),c=requireFiniteNumber(a.threshold??.2,`threshold`,0,1),l=requireFiniteInteger(a.limit??50,`limit`,0),u=requireFiniteInteger(a.minQueryLength??3,`minQueryLength`,1),d=new Map,f=new Map,p=new Set,m=0;function notifyMutation(){m++;for(let e of p)e()}let h=null,g=null;function getQueryTrigrams(e){return e===h&&g!==null?g:(h=e,g=r.generateTrigrams(e),g)}function addFieldToIndex(e,t){for(let n of t){let t=f.get(n);t||(t=new Set,f.set(n,t)),t.add(e)}}function removeFieldFromIndex(e,t){for(let n of t){let t=f.get(n);t&&(t.delete(e),t.size===0&&f.delete(n))}}function addItem(e){let n=new Map,i=new Map;for(let{field:a,stringify:s}of o){let o=e[a],c=s(o),l=t.tokenize(c),u=l.length>=1?r.generateTrigrams(l):new Set;n.set(a,u),i.set(a,c),addFieldToIndex(e,u)}d.set(e,{trigrams:n,values:i})}function containmentScan(e){let t=new Set;for(let[n,r]of d)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function trigramCandidates(e){let t=new Set;for(let n of e){let e=f.get(n);if(e)for(let n of e)t.add(n)}return t}function scoreCandidate(e,t,n){let i=0;for(let{field:a,weight:c}of o){let o;if(t===null)o=+!!(n.values.get(a)??``).toLowerCase().includes(e);else{let e=n.trigrams.get(a);if(!e||e.size===0)continue;o=r.overlapSimilarity(t,e)}let l=c/s*o;l>i&&(i=l)}return i}function computeMatches(e,t){let r=[];for(let{field:i}of o){let a=t.get(i);if(!a)continue;let o=n.findMatchRanges(a,e);o.length>0&&r.push({field:i,ranges:o})}return r}function reindexItem(e){let n=d.get(e);if(!n)return!1;let i=!1;for(let{field:a,stringify:s}of o){let o=s(e[a]);if(o===n.values.get(a))continue;i=!0;let c=n.trigrams.get(a);c&&removeFieldFromIndex(e,c);let l=t.tokenize(o),u=l.length>=1?r.generateTrigrams(l):new Set;n.trigrams.set(a,u),n.values.set(a,o),addFieldToIndex(e,u)}return i}function removeItem(e){let t=d.get(e);if(!t)return!1;for(let n of t.trigrams.values())removeFieldFromIndex(e,n);return d.delete(e),!0}for(let e of i)d.has(e)||addItem(e);return{add(e){d.has(e)||(addItem(e),notifyMutation())},get items(){return[...d.keys()]},onMutate(e){return p.add(e),()=>{p.delete(e)}},reindex(e){reindexItem(e)&¬ifyMutation()},remove(e){removeItem(e)&¬ifyMutation()},get revision(){return m},search(e,n){let r=requireFiniteNumber(n?.threshold??c,`threshold`,0,1),i=requireFiniteInteger(n?.limit??l,`limit`,0),a=requireFiniteInteger(n?.minQueryLength??u,`minQueryLength`,1);if(!e.trim())return[...d.keys()].slice(0,i).map(e=>({item:e,matches:[],score:1}));let o=t.tokenize(e);if(!o)return[];let s=o.length<a?null:getQueryTrigrams(o),f=s===null?containmentScan(o):trigramCandidates(s),p=[];for(let e of f){let t=d.get(e);if(!t)continue;let n=scoreCandidate(o,s,t);if(n>=r){let r=computeMatches(o,t.values);p.push({item:e,matches:r,score:n})}}return p.sort((e,t)=>t.score-e.score).slice(0,i)},setItems(e){let t=new Set(e),n=[...t],r=!1;for(let e of[...d.keys()])t.has(e)||(removeItem(e),r=!0);for(let e of n)d.has(e)?reindexItem(e)&&(r=!0):(addItem(e),r=!0);let i=[...d.keys()];if(i.length!==n.length||i.some((e,t)=>e!==n[t])){let e=new Map(n.map(e=>[e,d.get(e)]));d.clear();for(let[t,n]of e)d.set(t,n);r=!0}r&¬ifyMutation()},get size(){return d.size}}}exports.createIndex=createIndex;
|
|
2
2
|
//# sourceMappingURL=scout-index.cjs.map
|
package/dist/scout-index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scout-index.cjs","names":[],"sources":["../src/scout-index.ts"],"sourcesContent":["import { ScoutConfigurationError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { generateTrigrams, overlapSimilarity } from './trigram';\nimport type { FieldDef, FieldMatch, ScoutIndexOptions, SearchConstraints, SearchResult } from './types';\n\ntype FieldConfig<T> = {\n field: keyof T & string;\n stringify: (v: unknown) => string;\n weight: number;\n};\n\ntype ItemRecord = {\n /** Per-field trigrams for scoring. */\n trigrams: Map<string, Set<string>>;\n /** Per-field original text for highlighting. */\n values: Map<string, string>;\n};\n\n/**\n * A stateful, indexed search corpus. Created via `createIndex()`.\n *\n * Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches\n * the trigram index in O(field_length) without a full rebuild.\n */\nexport interface ScoutIndex<T> {\n /** Adds `item` to the index. No-op if the item is already indexed (by reference). */\n add(item: T): void;\n /** All items currently in the index, in insertion order. */\n readonly items: readonly T[];\n /**\n * Subscribes `listener` to be called after every changed `add()` / `remove()` / `reindex()`\n * / `setItems()` operation. No-ops — e.g. removing an unindexed item or reconciling an\n * unchanged corpus — do not fire it. Each changed `setItems()` reconciliation fires once.\n * Returns an unsubscribe function.\n *\n * Framework-agnostic extension point: `createSearch()` uses this internally to keep\n * its snapshot in sync with index mutations. Most callers won't need this directly.\n */\n onMutate(listener: () => void): () => void;\n /**\n * Re-reads the item's current field values and rebuilds its index entry in-place,\n * only updating fields whose values have changed. Preserves insertion order.\n * No-op if the item is not in the index.\n */\n reindex(item: T): void;\n /**\n * Removes `item` from the index by reference equality.\n * No-op if the item is not in the index.\n */\n remove(item: T): void;\n /**\n * Monotonically increasing counter, incremented after every changed `add()` / `remove()` /\n * `reindex()` / `setItems()` operation. Use as a cache-busting token when caching search\n * results outside the index — `toSearchMatcher()` uses it for this purpose.\n */\n readonly revision: number;\n /**\n * Searches the index for `query` and returns results sorted by score descending.\n *\n * An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.\n * A `query` with no indexable content after normalization (e.g. punctuation-only) returns\n * no results. Results below `threshold` are excluded. At most `limit` results are returned.\n */\n search(query: string, options?: SearchConstraints): SearchResult<T>[];\n /**\n * Reconciles the index to `items` by reference identity. Retained items are reindexed,\n * new items are added, missing items are removed, and one mutation notification fires\n * when indexed corpus or field values change. Duplicate references collapse to one item.\n */\n setItems(items: readonly T[]): void;\n /** Number of items currently in the index. */\n readonly size: number;\n}\n\nfunction requireFiniteInteger(value: number, name: string, minimum: number): number {\n if (!Number.isFinite(value) || !Number.isInteger(value) || value < minimum) {\n throw new ScoutConfigurationError(`${name} must be a finite integer greater than or equal to ${minimum}.`);\n }\n\n return value;\n}\n\nfunction requireFiniteNumber(value: number, name: string, minimum: number, maximum = Number.POSITIVE_INFINITY): number {\n if (!Number.isFinite(value) || value < minimum || value > maximum) {\n throw new ScoutConfigurationError(`${name} must be a finite number between ${minimum} and ${maximum}.`);\n }\n\n return value;\n}\n\nfunction resolveFields<T>(defs: ReadonlyArray<FieldDef<T>>): FieldConfig<T>[] {\n return defs.map((def) => {\n if (typeof def === 'string') {\n return { field: def, stringify: defaultStringify, weight: 1 };\n }\n\n return {\n field: def.field,\n stringify: def.stringify ?? defaultStringify,\n weight: requireFiniteNumber(def.weight ?? 1, `weight for field \"${def.field}\"`, Number.MIN_VALUE),\n };\n });\n}\n\n/**\n * Builds a trigram inverted index over `items` for fast fuzzy search.\n *\n * Construction is O(corpus × field_length). Subsequent `search()` calls are\n * O(candidates) — far faster than per-query Levenshtein for large corpora.\n *\n * @example\n * ```ts\n * const index = createIndex(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * threshold: 0.3,\n * limit: 20,\n * });\n *\n * const results = index.search('alice');\n * ```\n *\n * @throws {ScoutConfigurationError} If options use an invalid field or numeric configuration.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutConfigurationError('createIndex: at least one field is required.');\n }\n\n const fields = resolveFields(options.fields);\n const maxWeight = fields.reduce((max, f) => Math.max(max, f.weight), 1);\n const defaultThreshold = requireFiniteNumber(options.threshold ?? 0.2, 'threshold', 0, 1);\n const defaultLimit = requireFiniteInteger(options.limit ?? 50, 'limit', 0);\n const defaultMinQueryLength = requireFiniteInteger(options.minQueryLength ?? 3, 'minQueryLength', 1);\n\n /** item → per-item record, preserves insertion order for `items` getter */\n const itemData = new Map<T, ItemRecord>();\n /** trigram → set of items that contain it */\n const invertedIndex = new Map<string, Set<T>>();\n const mutationListeners = new Set<() => void>();\n let revision = 0;\n\n function notifyMutation(): void {\n revision++;\n\n for (const listener of mutationListeners) listener();\n }\n\n /** Single-entry cache for the most recent normalized query's trigrams (F2). */\n let cachedNormalized: string | null = null;\n let cachedTrigrams: Set<string> | null = null;\n\n function getQueryTrigrams(normalized: string): Set<string> {\n if (normalized === cachedNormalized && cachedTrigrams !== null) return cachedTrigrams;\n\n cachedNormalized = normalized;\n cachedTrigrams = generateTrigrams(normalized);\n\n return cachedTrigrams;\n }\n\n function addFieldToIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n let bucket = invertedIndex.get(trigram);\n\n if (!bucket) {\n bucket = new Set<T>();\n invertedIndex.set(trigram, bucket);\n }\n\n bucket.add(item);\n }\n }\n\n function removeFieldFromIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n const bucket = invertedIndex.get(trigram);\n\n if (bucket) {\n bucket.delete(item);\n\n if (bucket.size === 0) invertedIndex.delete(trigram);\n }\n }\n }\n\n function addItem(item: T): void {\n const trigrams = new Map<string, Set<string>>();\n const values = new Map<string, string>();\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const text = stringify(raw);\n const normalized = tokenize(text);\n const fieldTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n trigrams.set(field, fieldTrigrams);\n values.set(field, text);\n addFieldToIndex(item, fieldTrigrams);\n }\n\n itemData.set(item, { trigrams, values });\n }\n\n /**\n * Performs a full linear scan over all items for short queries.\n * O(n × field_count) — acceptable for small corpora; consider raising\n * `minQueryLength` on large datasets to avoid triggering this path.\n */\n function containmentScan(query: string): Set<T> {\n const result = new Set<T>();\n\n for (const [item, record] of itemData) {\n for (const value of record.values.values()) {\n if (value.toLowerCase().includes(query)) {\n result.add(item);\n break;\n }\n }\n }\n\n return result;\n }\n\n function trigramCandidates(queryTrigrams: Set<string>): Set<T> {\n const candidates = new Set<T>();\n\n for (const trigram of queryTrigrams) {\n const items = invertedIndex.get(trigram);\n\n if (items) {\n for (const item of items) candidates.add(item);\n }\n }\n\n return candidates;\n }\n\n function scoreCandidate(normalized: string, queryTrigrams: Set<string> | null, record: ItemRecord): number {\n let bestScore = 0;\n\n for (const { field, weight } of fields) {\n let fieldScore: number;\n\n if (queryTrigrams === null) {\n const raw = record.values.get(field) ?? '';\n\n fieldScore = raw.toLowerCase().includes(normalized) ? 1.0 : 0;\n } else {\n const itemTrigrams = record.trigrams.get(field);\n\n if (!itemTrigrams || itemTrigrams.size === 0) continue;\n\n fieldScore = overlapSimilarity(queryTrigrams, itemTrigrams);\n }\n\n const weighted = fieldScore * (weight / maxWeight);\n\n if (weighted > bestScore) bestScore = weighted;\n }\n\n return bestScore;\n }\n\n function computeMatches(query: string, values: Map<string, string>): FieldMatch<keyof T & string>[] {\n const matches: FieldMatch<keyof T & string>[] = [];\n\n for (const { field } of fields) {\n const text = values.get(field);\n\n if (!text) continue;\n\n const ranges = findMatchRanges(text, query);\n\n if (ranges.length > 0) matches.push({ field, ranges });\n }\n\n return matches;\n }\n\n function reindexItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const newText = stringify(item[field]);\n const oldText = record.values.get(field);\n\n if (newText === oldText) continue;\n\n changed = true;\n\n const oldTrigrams = record.trigrams.get(field);\n\n if (oldTrigrams) removeFieldFromIndex(item, oldTrigrams);\n\n const normalized = tokenize(newText);\n const newTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n record.trigrams.set(field, newTrigrams);\n record.values.set(field, newText);\n addFieldToIndex(item, newTrigrams);\n }\n\n return changed;\n }\n\n function removeItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n\n return true;\n }\n\n for (const item of items) {\n if (!itemData.has(item)) addItem(item);\n }\n\n const index: ScoutIndex<T> = {\n add(item: T): void {\n if (itemData.has(item)) return;\n\n addItem(item);\n notifyMutation();\n },\n\n get items(): readonly T[] {\n return [...itemData.keys()];\n },\n\n onMutate(listener: () => void): () => void {\n mutationListeners.add(listener);\n\n return () => {\n mutationListeners.delete(listener);\n };\n },\n\n reindex(item: T): void {\n if (reindexItem(item)) notifyMutation();\n },\n\n remove(item: T): void {\n if (removeItem(item)) notifyMutation();\n },\n\n get revision(): number {\n return revision;\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = requireFiniteNumber(options?.threshold ?? defaultThreshold, 'threshold', 0, 1);\n const limit = requireFiniteInteger(options?.limit ?? defaultLimit, 'limit', 0);\n const minQueryLength = requireFiniteInteger(\n options?.minQueryLength ?? defaultMinQueryLength,\n 'minQueryLength',\n 1,\n );\n\n if (!query.trim()) {\n return [...itemData.keys()].slice(0, limit).map((item) => ({ item, matches: [], score: 1 }));\n }\n\n const normalized = tokenize(query);\n\n // Query had no indexable content (e.g. punctuation-only) — no match, not \"match all\".\n if (!normalized) return [];\n\n const isShort = normalized.length < minQueryLength;\n const queryTrigrams = isShort ? null : getQueryTrigrams(normalized);\n const candidates = queryTrigrams === null ? containmentScan(normalized) : trigramCandidates(queryTrigrams);\n const results: SearchResult<T>[] = [];\n\n for (const item of candidates) {\n const record = itemData.get(item);\n\n if (!record) continue;\n\n const score = scoreCandidate(normalized, queryTrigrams, record);\n\n if (score >= threshold) {\n const matches = computeMatches(normalized, record.values);\n\n results.push({ item, matches, score });\n }\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n },\n\n setItems(items: readonly T[]): void {\n const incoming = new Set(items);\n const next = [...incoming];\n let changed = false;\n\n for (const item of [...itemData.keys()]) {\n if (incoming.has(item)) continue;\n\n removeItem(item);\n changed = true;\n }\n\n for (const item of next) {\n if (!itemData.has(item)) {\n addItem(item);\n changed = true;\n } else if (reindexItem(item)) {\n changed = true;\n }\n }\n\n const current = [...itemData.keys()];\n const orderChanged = current.length !== next.length || current.some((item, index) => item !== next[index]);\n\n if (orderChanged) {\n const records = new Map(next.map((item) => [item, itemData.get(item)!]));\n\n itemData.clear();\n\n for (const [item, record] of records) itemData.set(item, record);\n\n changed = true;\n }\n\n if (changed) notifyMutation();\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n\n return index;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scout-index.cjs","names":[],"sources":["../src/scout-index.ts"],"sourcesContent":["import { ScoutConfigError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { generateTrigrams, overlapSimilarity } from './trigram';\nimport type { FieldDef, FieldMatch, ScoutIndexOptions, SearchConstraints, SearchResult } from './types';\n\ntype FieldConfig<T> = {\n field: keyof T & string;\n stringify: (v: unknown) => string;\n weight: number;\n};\n\ntype ItemRecord = {\n /** Per-field trigrams for scoring. */\n trigrams: Map<string, Set<string>>;\n /** Per-field original text for highlighting. */\n values: Map<string, string>;\n};\n\n/**\n * A stateful, indexed search corpus. Created via `createIndex()`.\n *\n * Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches\n * the trigram index in O(field_length) without a full rebuild.\n */\nexport interface ScoutIndex<T> {\n /** Adds `item` to the index. No-op if the item is already indexed (by reference). */\n add(item: T): void;\n /** All items currently in the index, in insertion order. */\n readonly items: readonly T[];\n /**\n * Subscribes `listener` to be called after every changed `add()` / `remove()` / `reindex()`\n * / `setItems()` operation. No-ops — e.g. removing an unindexed item or reconciling an\n * unchanged corpus — do not fire it. Each changed `setItems()` reconciliation fires once.\n * Returns an unsubscribe function.\n *\n * Framework-agnostic extension point: `createSearch()` uses this internally to keep\n * its snapshot in sync with index mutations. Most callers won't need this directly.\n */\n onMutate(listener: () => void): () => void;\n /**\n * Re-reads the item's current field values and rebuilds its index entry in-place,\n * only updating fields whose values have changed. Preserves insertion order.\n * No-op if the item is not in the index.\n */\n reindex(item: T): void;\n /**\n * Removes `item` from the index by reference equality.\n * No-op if the item is not in the index.\n */\n remove(item: T): void;\n /**\n * Monotonically increasing counter, incremented after every changed `add()` / `remove()` /\n * `reindex()` / `setItems()` operation. Use as a cache-busting token when caching search\n * results outside the index — `toSearchMatcher()` uses it for this purpose.\n */\n readonly revision: number;\n /**\n * Searches the index for `query` and returns results sorted by score descending.\n *\n * An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.\n * A `query` with no indexable content after normalization (e.g. punctuation-only) returns\n * no results. Results below `threshold` are excluded. At most `limit` results are returned.\n */\n search(query: string, options?: SearchConstraints): SearchResult<T>[];\n /**\n * Reconciles the index to `items` by reference identity. Retained items are reindexed,\n * new items are added, missing items are removed, and one mutation notification fires\n * when indexed corpus or field values change. Duplicate references collapse to one item.\n */\n setItems(items: readonly T[]): void;\n /** Number of items currently in the index. */\n readonly size: number;\n}\n\nfunction requireFiniteInteger(value: number, name: string, minimum: number): number {\n if (!Number.isFinite(value) || !Number.isInteger(value) || value < minimum) {\n throw new ScoutConfigError(`${name} must be a finite integer greater than or equal to ${minimum}.`);\n }\n\n return value;\n}\n\nfunction requireFiniteNumber(value: number, name: string, minimum: number, maximum = Number.POSITIVE_INFINITY): number {\n if (!Number.isFinite(value) || value < minimum || value > maximum) {\n throw new ScoutConfigError(`${name} must be a finite number between ${minimum} and ${maximum}.`);\n }\n\n return value;\n}\n\nfunction resolveFields<T>(defs: ReadonlyArray<FieldDef<T>>): FieldConfig<T>[] {\n return defs.map((def) => {\n if (typeof def === 'string') {\n return { field: def, stringify: defaultStringify, weight: 1 };\n }\n\n return {\n field: def.field,\n stringify: def.stringify ?? defaultStringify,\n weight: requireFiniteNumber(def.weight ?? 1, `weight for field \"${def.field}\"`, Number.MIN_VALUE),\n };\n });\n}\n\n/**\n * Builds a trigram inverted index over `items` for fast fuzzy search.\n *\n * Construction is O(corpus × field_length). Subsequent `search()` calls are\n * O(candidates) — far faster than per-query Levenshtein for large corpora.\n *\n * @example\n * ```ts\n * const index = createIndex(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * threshold: 0.3,\n * limit: 20,\n * });\n *\n * const results = index.search('alice');\n * ```\n *\n * @throws {ScoutConfigError} If options use an invalid field or numeric configuration.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutConfigError('createIndex: at least one field is required.');\n }\n\n const fields = resolveFields(options.fields);\n const maxWeight = fields.reduce((max, f) => Math.max(max, f.weight), 1);\n const defaultThreshold = requireFiniteNumber(options.threshold ?? 0.2, 'threshold', 0, 1);\n const defaultLimit = requireFiniteInteger(options.limit ?? 50, 'limit', 0);\n const defaultMinQueryLength = requireFiniteInteger(options.minQueryLength ?? 3, 'minQueryLength', 1);\n\n /** item → per-item record, preserves insertion order for `items` getter */\n const itemData = new Map<T, ItemRecord>();\n /** trigram → set of items that contain it */\n const invertedIndex = new Map<string, Set<T>>();\n const mutationListeners = new Set<() => void>();\n let revision = 0;\n\n function notifyMutation(): void {\n revision++;\n\n for (const listener of mutationListeners) listener();\n }\n\n /** Single-entry cache for the most recent normalized query's trigrams (F2). */\n let cachedNormalized: string | null = null;\n let cachedTrigrams: Set<string> | null = null;\n\n function getQueryTrigrams(normalized: string): Set<string> {\n if (normalized === cachedNormalized && cachedTrigrams !== null) return cachedTrigrams;\n\n cachedNormalized = normalized;\n cachedTrigrams = generateTrigrams(normalized);\n\n return cachedTrigrams;\n }\n\n function addFieldToIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n let bucket = invertedIndex.get(trigram);\n\n if (!bucket) {\n bucket = new Set<T>();\n invertedIndex.set(trigram, bucket);\n }\n\n bucket.add(item);\n }\n }\n\n function removeFieldFromIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n const bucket = invertedIndex.get(trigram);\n\n if (bucket) {\n bucket.delete(item);\n\n if (bucket.size === 0) invertedIndex.delete(trigram);\n }\n }\n }\n\n function addItem(item: T): void {\n const trigrams = new Map<string, Set<string>>();\n const values = new Map<string, string>();\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const text = stringify(raw);\n const normalized = tokenize(text);\n const fieldTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n trigrams.set(field, fieldTrigrams);\n values.set(field, text);\n addFieldToIndex(item, fieldTrigrams);\n }\n\n itemData.set(item, { trigrams, values });\n }\n\n /**\n * Performs a full linear scan over all items for short queries.\n * O(n × field_count) — acceptable for small corpora; consider raising\n * `minQueryLength` on large datasets to avoid triggering this path.\n */\n function containmentScan(query: string): Set<T> {\n const result = new Set<T>();\n\n for (const [item, record] of itemData) {\n for (const value of record.values.values()) {\n if (value.toLowerCase().includes(query)) {\n result.add(item);\n break;\n }\n }\n }\n\n return result;\n }\n\n function trigramCandidates(queryTrigrams: Set<string>): Set<T> {\n const candidates = new Set<T>();\n\n for (const trigram of queryTrigrams) {\n const items = invertedIndex.get(trigram);\n\n if (items) {\n for (const item of items) candidates.add(item);\n }\n }\n\n return candidates;\n }\n\n function scoreCandidate(normalized: string, queryTrigrams: Set<string> | null, record: ItemRecord): number {\n let bestScore = 0;\n\n for (const { field, weight } of fields) {\n let fieldScore: number;\n\n if (queryTrigrams === null) {\n const raw = record.values.get(field) ?? '';\n\n fieldScore = raw.toLowerCase().includes(normalized) ? 1.0 : 0;\n } else {\n const itemTrigrams = record.trigrams.get(field);\n\n if (!itemTrigrams || itemTrigrams.size === 0) continue;\n\n fieldScore = overlapSimilarity(queryTrigrams, itemTrigrams);\n }\n\n const weighted = fieldScore * (weight / maxWeight);\n\n if (weighted > bestScore) bestScore = weighted;\n }\n\n return bestScore;\n }\n\n function computeMatches(query: string, values: Map<string, string>): FieldMatch<keyof T & string>[] {\n const matches: FieldMatch<keyof T & string>[] = [];\n\n for (const { field } of fields) {\n const text = values.get(field);\n\n if (!text) continue;\n\n const ranges = findMatchRanges(text, query);\n\n if (ranges.length > 0) matches.push({ field, ranges });\n }\n\n return matches;\n }\n\n function reindexItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const newText = stringify(item[field]);\n const oldText = record.values.get(field);\n\n if (newText === oldText) continue;\n\n changed = true;\n\n const oldTrigrams = record.trigrams.get(field);\n\n if (oldTrigrams) removeFieldFromIndex(item, oldTrigrams);\n\n const normalized = tokenize(newText);\n const newTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n record.trigrams.set(field, newTrigrams);\n record.values.set(field, newText);\n addFieldToIndex(item, newTrigrams);\n }\n\n return changed;\n }\n\n function removeItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n\n return true;\n }\n\n for (const item of items) {\n if (!itemData.has(item)) addItem(item);\n }\n\n const index: ScoutIndex<T> = {\n add(item: T): void {\n if (itemData.has(item)) return;\n\n addItem(item);\n notifyMutation();\n },\n\n get items(): readonly T[] {\n return [...itemData.keys()];\n },\n\n onMutate(listener: () => void): () => void {\n mutationListeners.add(listener);\n\n return () => {\n mutationListeners.delete(listener);\n };\n },\n\n reindex(item: T): void {\n if (reindexItem(item)) notifyMutation();\n },\n\n remove(item: T): void {\n if (removeItem(item)) notifyMutation();\n },\n\n get revision(): number {\n return revision;\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = requireFiniteNumber(options?.threshold ?? defaultThreshold, 'threshold', 0, 1);\n const limit = requireFiniteInteger(options?.limit ?? defaultLimit, 'limit', 0);\n const minQueryLength = requireFiniteInteger(\n options?.minQueryLength ?? defaultMinQueryLength,\n 'minQueryLength',\n 1,\n );\n\n if (!query.trim()) {\n return [...itemData.keys()].slice(0, limit).map((item) => ({ item, matches: [], score: 1 }));\n }\n\n const normalized = tokenize(query);\n\n // Query had no indexable content (e.g. punctuation-only) — no match, not \"match all\".\n if (!normalized) return [];\n\n const isShort = normalized.length < minQueryLength;\n const queryTrigrams = isShort ? null : getQueryTrigrams(normalized);\n const candidates = queryTrigrams === null ? containmentScan(normalized) : trigramCandidates(queryTrigrams);\n const results: SearchResult<T>[] = [];\n\n for (const item of candidates) {\n const record = itemData.get(item);\n\n if (!record) continue;\n\n const score = scoreCandidate(normalized, queryTrigrams, record);\n\n if (score >= threshold) {\n const matches = computeMatches(normalized, record.values);\n\n results.push({ item, matches, score });\n }\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n },\n\n setItems(items: readonly T[]): void {\n const incoming = new Set(items);\n const next = [...incoming];\n let changed = false;\n\n for (const item of [...itemData.keys()]) {\n if (incoming.has(item)) continue;\n\n removeItem(item);\n changed = true;\n }\n\n for (const item of next) {\n if (!itemData.has(item)) {\n addItem(item);\n changed = true;\n } else if (reindexItem(item)) {\n changed = true;\n }\n }\n\n const current = [...itemData.keys()];\n const orderChanged = current.length !== next.length || current.some((item, index) => item !== next[index]);\n\n if (orderChanged) {\n const records = new Map(next.map((item) => [item, itemData.get(item)!]));\n\n itemData.clear();\n\n for (const [item, record] of records) itemData.set(item, record);\n\n changed = true;\n }\n\n if (changed) notifyMutation();\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n\n return index;\n}\n"],"mappings":"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"}
|
package/dist/scout-index.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export interface ScoutIndex<T> {
|
|
|
71
71
|
* const results = index.search('alice');
|
|
72
72
|
* ```
|
|
73
73
|
*
|
|
74
|
-
* @throws {
|
|
74
|
+
* @throws {ScoutConfigError} If options use an invalid field or numeric configuration.
|
|
75
75
|
*/
|
|
76
76
|
export declare function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T>;
|
|
77
77
|
//# sourceMappingURL=scout-index.d.ts.map
|
package/dist/scout-index.js
CHANGED
|
@@ -1,192 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { defaultStringify as t, tokenize as n } from "./tokenize.js";
|
|
3
|
-
import { findMatchRanges as r } from "./highlight.js";
|
|
4
|
-
import { generateTrigrams as i, overlapSimilarity as a } from "./trigram.js";
|
|
5
|
-
//#region src/scout-index.ts
|
|
6
|
-
function o(t, n, r) {
|
|
7
|
-
if (!Number.isFinite(t) || !Number.isInteger(t) || t < r) throw new e(`${n} must be a finite integer greater than or equal to ${r}.`);
|
|
8
|
-
return t;
|
|
9
|
-
}
|
|
10
|
-
function s(t, n, r, i = Infinity) {
|
|
11
|
-
if (!Number.isFinite(t) || t < r || t > i) throw new e(`${n} must be a finite number between ${r} and ${i}.`);
|
|
12
|
-
return t;
|
|
13
|
-
}
|
|
14
|
-
function c(e) {
|
|
15
|
-
return e.map((e) => typeof e == "string" ? {
|
|
16
|
-
field: e,
|
|
17
|
-
stringify: t,
|
|
18
|
-
weight: 1
|
|
19
|
-
} : {
|
|
20
|
-
field: e.field,
|
|
21
|
-
stringify: e.stringify ?? t,
|
|
22
|
-
weight: s(e.weight ?? 1, `weight for field "${e.field}"`, Number.MIN_VALUE)
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
function l(t, l) {
|
|
26
|
-
if (l.fields.length === 0) throw new e("createIndex: at least one field is required.");
|
|
27
|
-
let u = c(l.fields), d = u.reduce((e, t) => Math.max(e, t.weight), 1), f = s(l.threshold ?? .2, "threshold", 0, 1), p = o(l.limit ?? 50, "limit", 0), m = o(l.minQueryLength ?? 3, "minQueryLength", 1), h = /* @__PURE__ */ new Map(), g = /* @__PURE__ */ new Map(), _ = /* @__PURE__ */ new Set(), v = 0;
|
|
28
|
-
function y() {
|
|
29
|
-
v++;
|
|
30
|
-
for (let e of _) e();
|
|
31
|
-
}
|
|
32
|
-
let b = null, x = null;
|
|
33
|
-
function S(e) {
|
|
34
|
-
return e === b && x !== null ? x : (b = e, x = i(e), x);
|
|
35
|
-
}
|
|
36
|
-
function C(e, t) {
|
|
37
|
-
for (let n of t) {
|
|
38
|
-
let t = g.get(n);
|
|
39
|
-
t || (t = /* @__PURE__ */ new Set(), g.set(n, t)), t.add(e);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
function w(e, t) {
|
|
43
|
-
for (let n of t) {
|
|
44
|
-
let t = g.get(n);
|
|
45
|
-
t && (t.delete(e), t.size === 0 && g.delete(n));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function T(e) {
|
|
49
|
-
let t = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map();
|
|
50
|
-
for (let { field: a, stringify: o } of u) {
|
|
51
|
-
let s = e[a], c = o(s), l = n(c), u = l.length >= 1 ? i(l) : /* @__PURE__ */ new Set();
|
|
52
|
-
t.set(a, u), r.set(a, c), C(e, u);
|
|
53
|
-
}
|
|
54
|
-
h.set(e, {
|
|
55
|
-
trigrams: t,
|
|
56
|
-
values: r
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
function E(e) {
|
|
60
|
-
let t = /* @__PURE__ */ new Set();
|
|
61
|
-
for (let [n, r] of h) for (let i of r.values.values()) if (i.toLowerCase().includes(e)) {
|
|
62
|
-
t.add(n);
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
return t;
|
|
66
|
-
}
|
|
67
|
-
function D(e) {
|
|
68
|
-
let t = /* @__PURE__ */ new Set();
|
|
69
|
-
for (let n of e) {
|
|
70
|
-
let e = g.get(n);
|
|
71
|
-
if (e) for (let n of e) t.add(n);
|
|
72
|
-
}
|
|
73
|
-
return t;
|
|
74
|
-
}
|
|
75
|
-
function O(e, t, n) {
|
|
76
|
-
let r = 0;
|
|
77
|
-
for (let { field: i, weight: o } of u) {
|
|
78
|
-
let s;
|
|
79
|
-
if (t === null) s = +!!(n.values.get(i) ?? "").toLowerCase().includes(e);
|
|
80
|
-
else {
|
|
81
|
-
let e = n.trigrams.get(i);
|
|
82
|
-
if (!e || e.size === 0) continue;
|
|
83
|
-
s = a(t, e);
|
|
84
|
-
}
|
|
85
|
-
let c = o / d * s;
|
|
86
|
-
c > r && (r = c);
|
|
87
|
-
}
|
|
88
|
-
return r;
|
|
89
|
-
}
|
|
90
|
-
function k(e, t) {
|
|
91
|
-
let n = [];
|
|
92
|
-
for (let { field: i } of u) {
|
|
93
|
-
let a = t.get(i);
|
|
94
|
-
if (!a) continue;
|
|
95
|
-
let o = r(a, e);
|
|
96
|
-
o.length > 0 && n.push({
|
|
97
|
-
field: i,
|
|
98
|
-
ranges: o
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return n;
|
|
102
|
-
}
|
|
103
|
-
function A(e) {
|
|
104
|
-
let t = h.get(e);
|
|
105
|
-
if (!t) return !1;
|
|
106
|
-
let r = !1;
|
|
107
|
-
for (let { field: a, stringify: o } of u) {
|
|
108
|
-
let s = o(e[a]);
|
|
109
|
-
if (s === t.values.get(a)) continue;
|
|
110
|
-
r = !0;
|
|
111
|
-
let c = t.trigrams.get(a);
|
|
112
|
-
c && w(e, c);
|
|
113
|
-
let l = n(s), u = l.length >= 1 ? i(l) : /* @__PURE__ */ new Set();
|
|
114
|
-
t.trigrams.set(a, u), t.values.set(a, s), C(e, u);
|
|
115
|
-
}
|
|
116
|
-
return r;
|
|
117
|
-
}
|
|
118
|
-
function j(e) {
|
|
119
|
-
let t = h.get(e);
|
|
120
|
-
if (!t) return !1;
|
|
121
|
-
for (let n of t.trigrams.values()) w(e, n);
|
|
122
|
-
return h.delete(e), !0;
|
|
123
|
-
}
|
|
124
|
-
for (let e of t) h.has(e) || T(e);
|
|
125
|
-
return {
|
|
126
|
-
add(e) {
|
|
127
|
-
h.has(e) || (T(e), y());
|
|
128
|
-
},
|
|
129
|
-
get items() {
|
|
130
|
-
return [...h.keys()];
|
|
131
|
-
},
|
|
132
|
-
onMutate(e) {
|
|
133
|
-
return _.add(e), () => {
|
|
134
|
-
_.delete(e);
|
|
135
|
-
};
|
|
136
|
-
},
|
|
137
|
-
reindex(e) {
|
|
138
|
-
A(e) && y();
|
|
139
|
-
},
|
|
140
|
-
remove(e) {
|
|
141
|
-
j(e) && y();
|
|
142
|
-
},
|
|
143
|
-
get revision() {
|
|
144
|
-
return v;
|
|
145
|
-
},
|
|
146
|
-
search(e, t) {
|
|
147
|
-
let r = s(t?.threshold ?? f, "threshold", 0, 1), i = o(t?.limit ?? p, "limit", 0), a = o(t?.minQueryLength ?? m, "minQueryLength", 1);
|
|
148
|
-
if (!e.trim()) return [...h.keys()].slice(0, i).map((e) => ({
|
|
149
|
-
item: e,
|
|
150
|
-
matches: [],
|
|
151
|
-
score: 1
|
|
152
|
-
}));
|
|
153
|
-
let c = n(e);
|
|
154
|
-
if (!c) return [];
|
|
155
|
-
let l = c.length < a ? null : S(c), u = l === null ? E(c) : D(l), d = [];
|
|
156
|
-
for (let e of u) {
|
|
157
|
-
let t = h.get(e);
|
|
158
|
-
if (!t) continue;
|
|
159
|
-
let n = O(c, l, t);
|
|
160
|
-
if (n >= r) {
|
|
161
|
-
let r = k(c, t.values);
|
|
162
|
-
d.push({
|
|
163
|
-
item: e,
|
|
164
|
-
matches: r,
|
|
165
|
-
score: n
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return d.sort((e, t) => t.score - e.score).slice(0, i);
|
|
170
|
-
},
|
|
171
|
-
setItems(e) {
|
|
172
|
-
let t = new Set(e), n = [...t], r = !1;
|
|
173
|
-
for (let e of [...h.keys()]) t.has(e) || (j(e), r = !0);
|
|
174
|
-
for (let e of n) h.has(e) ? A(e) && (r = !0) : (T(e), r = !0);
|
|
175
|
-
let i = [...h.keys()];
|
|
176
|
-
if (i.length !== n.length || i.some((e, t) => e !== n[t])) {
|
|
177
|
-
let e = new Map(n.map((e) => [e, h.get(e)]));
|
|
178
|
-
h.clear();
|
|
179
|
-
for (let [t, n] of e) h.set(t, n);
|
|
180
|
-
r = !0;
|
|
181
|
-
}
|
|
182
|
-
r && y();
|
|
183
|
-
},
|
|
184
|
-
get size() {
|
|
185
|
-
return h.size;
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
//#endregion
|
|
190
|
-
export { l as createIndex };
|
|
191
|
-
|
|
1
|
+
import{ScoutConfigError as e}from"./errors.js";import{defaultStringify as t,tokenize as n}from"./tokenize.js";import{findMatchRanges as r}from"./highlight.js";import{generateTrigrams as i,overlapSimilarity as a}from"./trigram.js";function requireFiniteInteger(t,n,r){if(!Number.isFinite(t)||!Number.isInteger(t)||t<r)throw new e(`${n} must be a finite integer greater than or equal to ${r}.`);return t}function requireFiniteNumber(t,n,r,i=1/0){if(!Number.isFinite(t)||t<r||t>i)throw new e(`${n} must be a finite number between ${r} and ${i}.`);return t}function resolveFields(e){return e.map(e=>typeof e==`string`?{field:e,stringify:t,weight:1}:{field:e.field,stringify:e.stringify??t,weight:requireFiniteNumber(e.weight??1,`weight for field "${e.field}"`,Number.MIN_VALUE)})}function createIndex(t,o){if(o.fields.length===0)throw new e(`createIndex: at least one field is required.`);let s=resolveFields(o.fields),c=s.reduce((e,t)=>Math.max(e,t.weight),1),l=requireFiniteNumber(o.threshold??.2,`threshold`,0,1),u=requireFiniteInteger(o.limit??50,`limit`,0),d=requireFiniteInteger(o.minQueryLength??3,`minQueryLength`,1),f=new Map,p=new Map,m=new Set,h=0;function notifyMutation(){h++;for(let e of m)e()}let g=null,_=null;function getQueryTrigrams(e){return e===g&&_!==null?_:(g=e,_=i(e),_)}function addFieldToIndex(e,t){for(let n of t){let t=p.get(n);t||(t=new Set,p.set(n,t)),t.add(e)}}function removeFieldFromIndex(e,t){for(let n of t){let t=p.get(n);t&&(t.delete(e),t.size===0&&p.delete(n))}}function addItem(e){let t=new Map,r=new Map;for(let{field:a,stringify:o}of s){let s=e[a],c=o(s),l=n(c),u=l.length>=1?i(l):new Set;t.set(a,u),r.set(a,c),addFieldToIndex(e,u)}f.set(e,{trigrams:t,values:r})}function containmentScan(e){let t=new Set;for(let[n,r]of f)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function trigramCandidates(e){let t=new Set;for(let n of e){let e=p.get(n);if(e)for(let n of e)t.add(n)}return t}function scoreCandidate(e,t,n){let r=0;for(let{field:i,weight:o}of s){let s;if(t===null)s=+!!(n.values.get(i)??``).toLowerCase().includes(e);else{let e=n.trigrams.get(i);if(!e||e.size===0)continue;s=a(t,e)}let l=o/c*s;l>r&&(r=l)}return r}function computeMatches(e,t){let n=[];for(let{field:i}of s){let a=t.get(i);if(!a)continue;let o=r(a,e);o.length>0&&n.push({field:i,ranges:o})}return n}function reindexItem(e){let t=f.get(e);if(!t)return!1;let r=!1;for(let{field:a,stringify:o}of s){let s=o(e[a]);if(s===t.values.get(a))continue;r=!0;let c=t.trigrams.get(a);c&&removeFieldFromIndex(e,c);let l=n(s),u=l.length>=1?i(l):new Set;t.trigrams.set(a,u),t.values.set(a,s),addFieldToIndex(e,u)}return r}function removeItem(e){let t=f.get(e);if(!t)return!1;for(let n of t.trigrams.values())removeFieldFromIndex(e,n);return f.delete(e),!0}for(let e of t)f.has(e)||addItem(e);return{add(e){f.has(e)||(addItem(e),notifyMutation())},get items(){return[...f.keys()]},onMutate(e){return m.add(e),()=>{m.delete(e)}},reindex(e){reindexItem(e)&¬ifyMutation()},remove(e){removeItem(e)&¬ifyMutation()},get revision(){return h},search(e,t){let r=requireFiniteNumber(t?.threshold??l,`threshold`,0,1),i=requireFiniteInteger(t?.limit??u,`limit`,0),a=requireFiniteInteger(t?.minQueryLength??d,`minQueryLength`,1);if(!e.trim())return[...f.keys()].slice(0,i).map(e=>({item:e,matches:[],score:1}));let o=n(e);if(!o)return[];let s=o.length<a?null:getQueryTrigrams(o),c=s===null?containmentScan(o):trigramCandidates(s),p=[];for(let e of c){let t=f.get(e);if(!t)continue;let n=scoreCandidate(o,s,t);if(n>=r){let r=computeMatches(o,t.values);p.push({item:e,matches:r,score:n})}}return p.sort((e,t)=>t.score-e.score).slice(0,i)},setItems(e){let t=new Set(e),n=[...t],r=!1;for(let e of[...f.keys()])t.has(e)||(removeItem(e),r=!0);for(let e of n)f.has(e)?reindexItem(e)&&(r=!0):(addItem(e),r=!0);let i=[...f.keys()];if(i.length!==n.length||i.some((e,t)=>e!==n[t])){let e=new Map(n.map(e=>[e,f.get(e)]));f.clear();for(let[t,n]of e)f.set(t,n);r=!0}r&¬ifyMutation()},get size(){return f.size}}}export{createIndex};
|
|
192
2
|
//# sourceMappingURL=scout-index.js.map
|
package/dist/scout-index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scout-index.js","names":[],"sources":["../src/scout-index.ts"],"sourcesContent":["import { ScoutConfigurationError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { generateTrigrams, overlapSimilarity } from './trigram';\nimport type { FieldDef, FieldMatch, ScoutIndexOptions, SearchConstraints, SearchResult } from './types';\n\ntype FieldConfig<T> = {\n field: keyof T & string;\n stringify: (v: unknown) => string;\n weight: number;\n};\n\ntype ItemRecord = {\n /** Per-field trigrams for scoring. */\n trigrams: Map<string, Set<string>>;\n /** Per-field original text for highlighting. */\n values: Map<string, string>;\n};\n\n/**\n * A stateful, indexed search corpus. Created via `createIndex()`.\n *\n * Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches\n * the trigram index in O(field_length) without a full rebuild.\n */\nexport interface ScoutIndex<T> {\n /** Adds `item` to the index. No-op if the item is already indexed (by reference). */\n add(item: T): void;\n /** All items currently in the index, in insertion order. */\n readonly items: readonly T[];\n /**\n * Subscribes `listener` to be called after every changed `add()` / `remove()` / `reindex()`\n * / `setItems()` operation. No-ops — e.g. removing an unindexed item or reconciling an\n * unchanged corpus — do not fire it. Each changed `setItems()` reconciliation fires once.\n * Returns an unsubscribe function.\n *\n * Framework-agnostic extension point: `createSearch()` uses this internally to keep\n * its snapshot in sync with index mutations. Most callers won't need this directly.\n */\n onMutate(listener: () => void): () => void;\n /**\n * Re-reads the item's current field values and rebuilds its index entry in-place,\n * only updating fields whose values have changed. Preserves insertion order.\n * No-op if the item is not in the index.\n */\n reindex(item: T): void;\n /**\n * Removes `item` from the index by reference equality.\n * No-op if the item is not in the index.\n */\n remove(item: T): void;\n /**\n * Monotonically increasing counter, incremented after every changed `add()` / `remove()` /\n * `reindex()` / `setItems()` operation. Use as a cache-busting token when caching search\n * results outside the index — `toSearchMatcher()` uses it for this purpose.\n */\n readonly revision: number;\n /**\n * Searches the index for `query` and returns results sorted by score descending.\n *\n * An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.\n * A `query` with no indexable content after normalization (e.g. punctuation-only) returns\n * no results. Results below `threshold` are excluded. At most `limit` results are returned.\n */\n search(query: string, options?: SearchConstraints): SearchResult<T>[];\n /**\n * Reconciles the index to `items` by reference identity. Retained items are reindexed,\n * new items are added, missing items are removed, and one mutation notification fires\n * when indexed corpus or field values change. Duplicate references collapse to one item.\n */\n setItems(items: readonly T[]): void;\n /** Number of items currently in the index. */\n readonly size: number;\n}\n\nfunction requireFiniteInteger(value: number, name: string, minimum: number): number {\n if (!Number.isFinite(value) || !Number.isInteger(value) || value < minimum) {\n throw new ScoutConfigurationError(`${name} must be a finite integer greater than or equal to ${minimum}.`);\n }\n\n return value;\n}\n\nfunction requireFiniteNumber(value: number, name: string, minimum: number, maximum = Number.POSITIVE_INFINITY): number {\n if (!Number.isFinite(value) || value < minimum || value > maximum) {\n throw new ScoutConfigurationError(`${name} must be a finite number between ${minimum} and ${maximum}.`);\n }\n\n return value;\n}\n\nfunction resolveFields<T>(defs: ReadonlyArray<FieldDef<T>>): FieldConfig<T>[] {\n return defs.map((def) => {\n if (typeof def === 'string') {\n return { field: def, stringify: defaultStringify, weight: 1 };\n }\n\n return {\n field: def.field,\n stringify: def.stringify ?? defaultStringify,\n weight: requireFiniteNumber(def.weight ?? 1, `weight for field \"${def.field}\"`, Number.MIN_VALUE),\n };\n });\n}\n\n/**\n * Builds a trigram inverted index over `items` for fast fuzzy search.\n *\n * Construction is O(corpus × field_length). Subsequent `search()` calls are\n * O(candidates) — far faster than per-query Levenshtein for large corpora.\n *\n * @example\n * ```ts\n * const index = createIndex(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * threshold: 0.3,\n * limit: 20,\n * });\n *\n * const results = index.search('alice');\n * ```\n *\n * @throws {ScoutConfigurationError} If options use an invalid field or numeric configuration.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutConfigurationError('createIndex: at least one field is required.');\n }\n\n const fields = resolveFields(options.fields);\n const maxWeight = fields.reduce((max, f) => Math.max(max, f.weight), 1);\n const defaultThreshold = requireFiniteNumber(options.threshold ?? 0.2, 'threshold', 0, 1);\n const defaultLimit = requireFiniteInteger(options.limit ?? 50, 'limit', 0);\n const defaultMinQueryLength = requireFiniteInteger(options.minQueryLength ?? 3, 'minQueryLength', 1);\n\n /** item → per-item record, preserves insertion order for `items` getter */\n const itemData = new Map<T, ItemRecord>();\n /** trigram → set of items that contain it */\n const invertedIndex = new Map<string, Set<T>>();\n const mutationListeners = new Set<() => void>();\n let revision = 0;\n\n function notifyMutation(): void {\n revision++;\n\n for (const listener of mutationListeners) listener();\n }\n\n /** Single-entry cache for the most recent normalized query's trigrams (F2). */\n let cachedNormalized: string | null = null;\n let cachedTrigrams: Set<string> | null = null;\n\n function getQueryTrigrams(normalized: string): Set<string> {\n if (normalized === cachedNormalized && cachedTrigrams !== null) return cachedTrigrams;\n\n cachedNormalized = normalized;\n cachedTrigrams = generateTrigrams(normalized);\n\n return cachedTrigrams;\n }\n\n function addFieldToIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n let bucket = invertedIndex.get(trigram);\n\n if (!bucket) {\n bucket = new Set<T>();\n invertedIndex.set(trigram, bucket);\n }\n\n bucket.add(item);\n }\n }\n\n function removeFieldFromIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n const bucket = invertedIndex.get(trigram);\n\n if (bucket) {\n bucket.delete(item);\n\n if (bucket.size === 0) invertedIndex.delete(trigram);\n }\n }\n }\n\n function addItem(item: T): void {\n const trigrams = new Map<string, Set<string>>();\n const values = new Map<string, string>();\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const text = stringify(raw);\n const normalized = tokenize(text);\n const fieldTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n trigrams.set(field, fieldTrigrams);\n values.set(field, text);\n addFieldToIndex(item, fieldTrigrams);\n }\n\n itemData.set(item, { trigrams, values });\n }\n\n /**\n * Performs a full linear scan over all items for short queries.\n * O(n × field_count) — acceptable for small corpora; consider raising\n * `minQueryLength` on large datasets to avoid triggering this path.\n */\n function containmentScan(query: string): Set<T> {\n const result = new Set<T>();\n\n for (const [item, record] of itemData) {\n for (const value of record.values.values()) {\n if (value.toLowerCase().includes(query)) {\n result.add(item);\n break;\n }\n }\n }\n\n return result;\n }\n\n function trigramCandidates(queryTrigrams: Set<string>): Set<T> {\n const candidates = new Set<T>();\n\n for (const trigram of queryTrigrams) {\n const items = invertedIndex.get(trigram);\n\n if (items) {\n for (const item of items) candidates.add(item);\n }\n }\n\n return candidates;\n }\n\n function scoreCandidate(normalized: string, queryTrigrams: Set<string> | null, record: ItemRecord): number {\n let bestScore = 0;\n\n for (const { field, weight } of fields) {\n let fieldScore: number;\n\n if (queryTrigrams === null) {\n const raw = record.values.get(field) ?? '';\n\n fieldScore = raw.toLowerCase().includes(normalized) ? 1.0 : 0;\n } else {\n const itemTrigrams = record.trigrams.get(field);\n\n if (!itemTrigrams || itemTrigrams.size === 0) continue;\n\n fieldScore = overlapSimilarity(queryTrigrams, itemTrigrams);\n }\n\n const weighted = fieldScore * (weight / maxWeight);\n\n if (weighted > bestScore) bestScore = weighted;\n }\n\n return bestScore;\n }\n\n function computeMatches(query: string, values: Map<string, string>): FieldMatch<keyof T & string>[] {\n const matches: FieldMatch<keyof T & string>[] = [];\n\n for (const { field } of fields) {\n const text = values.get(field);\n\n if (!text) continue;\n\n const ranges = findMatchRanges(text, query);\n\n if (ranges.length > 0) matches.push({ field, ranges });\n }\n\n return matches;\n }\n\n function reindexItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const newText = stringify(item[field]);\n const oldText = record.values.get(field);\n\n if (newText === oldText) continue;\n\n changed = true;\n\n const oldTrigrams = record.trigrams.get(field);\n\n if (oldTrigrams) removeFieldFromIndex(item, oldTrigrams);\n\n const normalized = tokenize(newText);\n const newTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n record.trigrams.set(field, newTrigrams);\n record.values.set(field, newText);\n addFieldToIndex(item, newTrigrams);\n }\n\n return changed;\n }\n\n function removeItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n\n return true;\n }\n\n for (const item of items) {\n if (!itemData.has(item)) addItem(item);\n }\n\n const index: ScoutIndex<T> = {\n add(item: T): void {\n if (itemData.has(item)) return;\n\n addItem(item);\n notifyMutation();\n },\n\n get items(): readonly T[] {\n return [...itemData.keys()];\n },\n\n onMutate(listener: () => void): () => void {\n mutationListeners.add(listener);\n\n return () => {\n mutationListeners.delete(listener);\n };\n },\n\n reindex(item: T): void {\n if (reindexItem(item)) notifyMutation();\n },\n\n remove(item: T): void {\n if (removeItem(item)) notifyMutation();\n },\n\n get revision(): number {\n return revision;\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = requireFiniteNumber(options?.threshold ?? defaultThreshold, 'threshold', 0, 1);\n const limit = requireFiniteInteger(options?.limit ?? defaultLimit, 'limit', 0);\n const minQueryLength = requireFiniteInteger(\n options?.minQueryLength ?? defaultMinQueryLength,\n 'minQueryLength',\n 1,\n );\n\n if (!query.trim()) {\n return [...itemData.keys()].slice(0, limit).map((item) => ({ item, matches: [], score: 1 }));\n }\n\n const normalized = tokenize(query);\n\n // Query had no indexable content (e.g. punctuation-only) — no match, not \"match all\".\n if (!normalized) return [];\n\n const isShort = normalized.length < minQueryLength;\n const queryTrigrams = isShort ? null : getQueryTrigrams(normalized);\n const candidates = queryTrigrams === null ? containmentScan(normalized) : trigramCandidates(queryTrigrams);\n const results: SearchResult<T>[] = [];\n\n for (const item of candidates) {\n const record = itemData.get(item);\n\n if (!record) continue;\n\n const score = scoreCandidate(normalized, queryTrigrams, record);\n\n if (score >= threshold) {\n const matches = computeMatches(normalized, record.values);\n\n results.push({ item, matches, score });\n }\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n },\n\n setItems(items: readonly T[]): void {\n const incoming = new Set(items);\n const next = [...incoming];\n let changed = false;\n\n for (const item of [...itemData.keys()]) {\n if (incoming.has(item)) continue;\n\n removeItem(item);\n changed = true;\n }\n\n for (const item of next) {\n if (!itemData.has(item)) {\n addItem(item);\n changed = true;\n } else if (reindexItem(item)) {\n changed = true;\n }\n }\n\n const current = [...itemData.keys()];\n const orderChanged = current.length !== next.length || current.some((item, index) => item !== next[index]);\n\n if (orderChanged) {\n const records = new Map(next.map((item) => [item, itemData.get(item)!]));\n\n itemData.clear();\n\n for (const [item, record] of records) itemData.set(item, record);\n\n changed = true;\n }\n\n if (changed) notifyMutation();\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n\n return index;\n}\n"],"mappings":";;;;;AA2EA,SAAS,EAAqB,GAAe,GAAc,GAAyB;CAClF,IAAI,CAAC,OAAO,SAAS,CAAK,KAAK,CAAC,OAAO,UAAU,CAAK,KAAK,IAAQ,GACjE,MAAM,IAAI,EAAwB,GAAG,EAAK,qDAAqD,EAAQ,EAAE;CAG3G,OAAO;AACT;AAEA,SAAS,EAAoB,GAAe,GAAc,GAAiB,IAAU,UAAkC;CACrH,IAAI,CAAC,OAAO,SAAS,CAAK,KAAK,IAAQ,KAAW,IAAQ,GACxD,MAAM,IAAI,EAAwB,GAAG,EAAK,mCAAmC,EAAQ,OAAO,EAAQ,EAAE;CAGxG,OAAO;AACT;AAEA,SAAS,EAAiB,GAAoD;CAC5E,OAAO,EAAK,KAAK,MACX,OAAO,KAAQ,WACV;EAAE,OAAO;EAAK,WAAW;EAAkB,QAAQ;CAAE,IAGvD;EACL,OAAO,EAAI;EACX,WAAW,EAAI,aAAa;EAC5B,QAAQ,EAAoB,EAAI,UAAU,GAAG,qBAAqB,EAAI,MAAM,IAAI,OAAO,SAAS;CAClG,CACD;AACH;AAqBA,SAAgB,EAAe,GAAY,GAA8C;CACvF,IAAI,EAAQ,OAAO,WAAW,GAC5B,MAAM,IAAI,EAAwB,8CAA8C;CAGlF,IAAM,IAAS,EAAc,EAAQ,MAAM,GACrC,IAAY,EAAO,QAAQ,GAAK,MAAM,KAAK,IAAI,GAAK,EAAE,MAAM,GAAG,CAAC,GAChE,IAAmB,EAAoB,EAAQ,aAAa,IAAK,aAAa,GAAG,CAAC,GAClF,IAAe,EAAqB,EAAQ,SAAS,IAAI,SAAS,CAAC,GACnE,IAAwB,EAAqB,EAAQ,kBAAkB,GAAG,kBAAkB,CAAC,GAG7F,oBAAW,IAAI,IAAmB,GAElC,oBAAgB,IAAI,IAAoB,GACxC,oBAAoB,IAAI,IAAgB,GAC1C,IAAW;CAEf,SAAS,IAAuB;EAC9B;EAEA,KAAK,IAAM,KAAY,GAAmB,EAAS;CACrD;CAGA,IAAI,IAAkC,MAClC,IAAqC;CAEzC,SAAS,EAAiB,GAAiC;EAMzD,OALI,MAAe,KAAoB,MAAmB,OAAa,KAEvE,IAAmB,GACnB,IAAiB,EAAiB,CAAU,GAErC;CACT;CAEA,SAAS,EAAgB,GAAS,GAAkC;EAClE,KAAK,IAAM,KAAW,GAAe;GACnC,IAAI,IAAS,EAAc,IAAI,CAAO;GAOtC,AALK,MACH,oBAAS,IAAI,IAAO,GACpB,EAAc,IAAI,GAAS,CAAM,IAGnC,EAAO,IAAI,CAAI;EACjB;CACF;CAEA,SAAS,EAAqB,GAAS,GAAkC;EACvE,KAAK,IAAM,KAAW,GAAe;GACnC,IAAM,IAAS,EAAc,IAAI,CAAO;GAExC,AAAI,MACF,EAAO,OAAO,CAAI,GAEd,EAAO,SAAS,KAAG,EAAc,OAAO,CAAO;EAEvD;CACF;CAEA,SAAS,EAAQ,GAAe;EAC9B,IAAM,oBAAW,IAAI,IAAyB,GACxC,oBAAS,IAAI,IAAoB;EAEvC,KAAK,IAAM,EAAE,UAAO,kBAAe,GAAQ;GACzC,IAAM,IAAM,EAAK,IACX,IAAO,EAAU,CAAG,GACpB,IAAa,EAAS,CAAI,GAC1B,IAAgB,EAAW,UAAU,IAAI,EAAiB,CAAU,oBAAI,IAAI,IAAY;GAI9F,AAFA,EAAS,IAAI,GAAO,CAAa,GACjC,EAAO,IAAI,GAAO,CAAI,GACtB,EAAgB,GAAM,CAAa;EACrC;EAEA,EAAS,IAAI,GAAM;GAAE;GAAU;EAAO,CAAC;CACzC;CAOA,SAAS,EAAgB,GAAuB;EAC9C,IAAM,oBAAS,IAAI,IAAO;EAE1B,KAAK,IAAM,CAAC,GAAM,MAAW,GAC3B,KAAK,IAAM,KAAS,EAAO,OAAO,OAAO,GACvC,IAAI,EAAM,YAAY,CAAC,CAAC,SAAS,CAAK,GAAG;GACvC,EAAO,IAAI,CAAI;GACf;EACF;EAIJ,OAAO;CACT;CAEA,SAAS,EAAkB,GAAoC;EAC7D,IAAM,oBAAa,IAAI,IAAO;EAE9B,KAAK,IAAM,KAAW,GAAe;GACnC,IAAM,IAAQ,EAAc,IAAI,CAAO;GAEvC,IAAI,GACF,KAAK,IAAM,KAAQ,GAAO,EAAW,IAAI,CAAI;EAEjD;EAEA,OAAO;CACT;CAEA,SAAS,EAAe,GAAoB,GAAmC,GAA4B;EACzG,IAAI,IAAY;EAEhB,KAAK,IAAM,EAAE,UAAO,eAAY,GAAQ;GACtC,IAAI;GAEJ,IAAI,MAAkB,MAGpB,IAAA,IAFY,EAAO,OAAO,IAAI,CAAK,KAAK,GAAA,CAEvB,YAAY,CAAC,CAAC,SAAS,CAAU;QAC7C;IACL,IAAM,IAAe,EAAO,SAAS,IAAI,CAAK;IAE9C,IAAI,CAAC,KAAgB,EAAa,SAAS,GAAG;IAE9C,IAAa,EAAkB,GAAe,CAAY;GAC5D;GAEA,IAAM,IAAyB,IAAS,IAAvB;GAEjB,AAAI,IAAW,MAAW,IAAY;EACxC;EAEA,OAAO;CACT;CAEA,SAAS,EAAe,GAAe,GAA6D;EAClG,IAAM,IAA0C,CAAC;EAEjD,KAAK,IAAM,EAAE,cAAW,GAAQ;GAC9B,IAAM,IAAO,EAAO,IAAI,CAAK;GAE7B,IAAI,CAAC,GAAM;GAEX,IAAM,IAAS,EAAgB,GAAM,CAAK;GAE1C,AAAI,EAAO,SAAS,KAAG,EAAQ,KAAK;IAAE;IAAO;GAAO,CAAC;EACvD;EAEA,OAAO;CACT;CAEA,SAAS,EAAY,GAAkB;EACrC,IAAM,IAAS,EAAS,IAAI,CAAI;EAEhC,IAAI,CAAC,GAAQ,OAAO;EAEpB,IAAI,IAAU;EAEd,KAAK,IAAM,EAAE,UAAO,kBAAe,GAAQ;GACzC,IAAM,IAAU,EAAU,EAAK,EAAM;GAGrC,IAAI,MAFY,EAAO,OAAO,IAAI,CAElB,GAAS;GAEzB,IAAU;GAEV,IAAM,IAAc,EAAO,SAAS,IAAI,CAAK;GAE7C,AAAI,KAAa,EAAqB,GAAM,CAAW;GAEvD,IAAM,IAAa,EAAS,CAAO,GAC7B,IAAc,EAAW,UAAU,IAAI,EAAiB,CAAU,oBAAI,IAAI,IAAY;GAI5F,AAFA,EAAO,SAAS,IAAI,GAAO,CAAW,GACtC,EAAO,OAAO,IAAI,GAAO,CAAO,GAChC,EAAgB,GAAM,CAAW;EACnC;EAEA,OAAO;CACT;CAEA,SAAS,EAAW,GAAkB;EACpC,IAAM,IAAS,EAAS,IAAI,CAAI;EAEhC,IAAI,CAAC,GAAQ,OAAO;EAEpB,KAAK,IAAM,KAAiB,EAAO,SAAS,OAAO,GACjD,EAAqB,GAAM,CAAa;EAK1C,OAFA,EAAS,OAAO,CAAI,GAEb;CACT;CAEA,KAAK,IAAM,KAAQ,GACjB,AAAK,EAAS,IAAI,CAAI,KAAG,EAAQ,CAAI;CAqHvC,OAAO;EAjHL,IAAI,GAAe;GACb,EAAS,IAAI,CAAI,MAErB,EAAQ,CAAI,GACZ,EAAe;EACjB;EAEA,IAAI,QAAsB;GACxB,OAAO,CAAC,GAAG,EAAS,KAAK,CAAC;EAC5B;EAEA,SAAS,GAAkC;GAGzC,OAFA,EAAkB,IAAI,CAAQ,SAEjB;IACX,EAAkB,OAAO,CAAQ;GACnC;EACF;EAEA,QAAQ,GAAe;GACrB,AAAI,EAAY,CAAI,KAAG,EAAe;EACxC;EAEA,OAAO,GAAe;GACpB,AAAI,EAAW,CAAI,KAAG,EAAe;EACvC;EAEA,IAAI,WAAmB;GACrB,OAAO;EACT;EAEA,OAAO,GAAe,GAAgD;GACpE,IAAM,IAAY,EAAoB,GAAS,aAAa,GAAkB,aAAa,GAAG,CAAC,GACzF,IAAQ,EAAqB,GAAS,SAAS,GAAc,SAAS,CAAC,GACvE,IAAiB,EACrB,GAAS,kBAAkB,GAC3B,kBACA,CACF;GAEA,IAAI,CAAC,EAAM,KAAK,GACd,OAAO,CAAC,GAAG,EAAS,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,CAAK,CAAC,CAAC,KAAK,OAAU;IAAE;IAAM,SAAS,CAAC;IAAG,OAAO;GAAE,EAAE;GAG7F,IAAM,IAAa,EAAS,CAAK;GAGjC,IAAI,CAAC,GAAY,OAAO,CAAC;GAGzB,IAAM,IADU,EAAW,SAAS,IACJ,OAAO,EAAiB,CAAU,GAC5D,IAAa,MAAkB,OAAO,EAAgB,CAAU,IAAI,EAAkB,CAAa,GACnG,IAA6B,CAAC;GAEpC,KAAK,IAAM,KAAQ,GAAY;IAC7B,IAAM,IAAS,EAAS,IAAI,CAAI;IAEhC,IAAI,CAAC,GAAQ;IAEb,IAAM,IAAQ,EAAe,GAAY,GAAe,CAAM;IAE9D,IAAI,KAAS,GAAW;KACtB,IAAM,IAAU,EAAe,GAAY,EAAO,MAAM;KAExD,EAAQ,KAAK;MAAE;MAAM;MAAS;KAAM,CAAC;IACvC;GACF;GAEA,OAAO,EAAQ,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,MAAM,GAAG,CAAK;EACjE;EAEA,SAAS,GAA2B;GAClC,IAAM,IAAW,IAAI,IAAI,CAAK,GACxB,IAAO,CAAC,GAAG,CAAQ,GACrB,IAAU;GAEd,KAAK,IAAM,KAAQ,CAAC,GAAG,EAAS,KAAK,CAAC,GAChC,EAAS,IAAI,CAAI,MAErB,EAAW,CAAI,GACf,IAAU;GAGZ,KAAK,IAAM,KAAQ,GACjB,AAAK,EAAS,IAAI,CAAI,IAGX,EAAY,CAAI,MACzB,IAAU,OAHV,EAAQ,CAAI,GACZ,IAAU;GAMd,IAAM,IAAU,CAAC,GAAG,EAAS,KAAK,CAAC;GAGnC,IAFqB,EAAQ,WAAW,EAAK,UAAU,EAAQ,MAAM,GAAM,MAAU,MAAS,EAAK,EAAM,GAEvF;IAChB,IAAM,IAAU,IAAI,IAAI,EAAK,KAAK,MAAS,CAAC,GAAM,EAAS,IAAI,CAAI,CAAE,CAAC,CAAC;IAEvE,EAAS,MAAM;IAEf,KAAK,IAAM,CAAC,GAAM,MAAW,GAAS,EAAS,IAAI,GAAM,CAAM;IAE/D,IAAU;GACZ;GAEA,AAAI,KAAS,EAAe;EAC9B;EAEA,IAAI,OAAe;GACjB,OAAO,EAAS;EAClB;CAGK;AACT"}
|
|
1
|
+
{"version":3,"file":"scout-index.js","names":[],"sources":["../src/scout-index.ts"],"sourcesContent":["import { ScoutConfigError } from './errors';\nimport { findMatchRanges } from './highlight';\nimport { defaultStringify, tokenize } from './tokenize';\nimport { generateTrigrams, overlapSimilarity } from './trigram';\nimport type { FieldDef, FieldMatch, ScoutIndexOptions, SearchConstraints, SearchResult } from './types';\n\ntype FieldConfig<T> = {\n field: keyof T & string;\n stringify: (v: unknown) => string;\n weight: number;\n};\n\ntype ItemRecord = {\n /** Per-field trigrams for scoring. */\n trigrams: Map<string, Set<string>>;\n /** Per-field original text for highlighting. */\n values: Map<string, string>;\n};\n\n/**\n * A stateful, indexed search corpus. Created via `createIndex()`.\n *\n * Supports incremental `add()`, `remove()`, and `reindex()` operations — each patches\n * the trigram index in O(field_length) without a full rebuild.\n */\nexport interface ScoutIndex<T> {\n /** Adds `item` to the index. No-op if the item is already indexed (by reference). */\n add(item: T): void;\n /** All items currently in the index, in insertion order. */\n readonly items: readonly T[];\n /**\n * Subscribes `listener` to be called after every changed `add()` / `remove()` / `reindex()`\n * / `setItems()` operation. No-ops — e.g. removing an unindexed item or reconciling an\n * unchanged corpus — do not fire it. Each changed `setItems()` reconciliation fires once.\n * Returns an unsubscribe function.\n *\n * Framework-agnostic extension point: `createSearch()` uses this internally to keep\n * its snapshot in sync with index mutations. Most callers won't need this directly.\n */\n onMutate(listener: () => void): () => void;\n /**\n * Re-reads the item's current field values and rebuilds its index entry in-place,\n * only updating fields whose values have changed. Preserves insertion order.\n * No-op if the item is not in the index.\n */\n reindex(item: T): void;\n /**\n * Removes `item` from the index by reference equality.\n * No-op if the item is not in the index.\n */\n remove(item: T): void;\n /**\n * Monotonically increasing counter, incremented after every changed `add()` / `remove()` /\n * `reindex()` / `setItems()` operation. Use as a cache-busting token when caching search\n * results outside the index — `toSearchMatcher()` uses it for this purpose.\n */\n readonly revision: number;\n /**\n * Searches the index for `query` and returns results sorted by score descending.\n *\n * An empty (or whitespace-only) `query` returns all indexed items with `score = 1`.\n * A `query` with no indexable content after normalization (e.g. punctuation-only) returns\n * no results. Results below `threshold` are excluded. At most `limit` results are returned.\n */\n search(query: string, options?: SearchConstraints): SearchResult<T>[];\n /**\n * Reconciles the index to `items` by reference identity. Retained items are reindexed,\n * new items are added, missing items are removed, and one mutation notification fires\n * when indexed corpus or field values change. Duplicate references collapse to one item.\n */\n setItems(items: readonly T[]): void;\n /** Number of items currently in the index. */\n readonly size: number;\n}\n\nfunction requireFiniteInteger(value: number, name: string, minimum: number): number {\n if (!Number.isFinite(value) || !Number.isInteger(value) || value < minimum) {\n throw new ScoutConfigError(`${name} must be a finite integer greater than or equal to ${minimum}.`);\n }\n\n return value;\n}\n\nfunction requireFiniteNumber(value: number, name: string, minimum: number, maximum = Number.POSITIVE_INFINITY): number {\n if (!Number.isFinite(value) || value < minimum || value > maximum) {\n throw new ScoutConfigError(`${name} must be a finite number between ${minimum} and ${maximum}.`);\n }\n\n return value;\n}\n\nfunction resolveFields<T>(defs: ReadonlyArray<FieldDef<T>>): FieldConfig<T>[] {\n return defs.map((def) => {\n if (typeof def === 'string') {\n return { field: def, stringify: defaultStringify, weight: 1 };\n }\n\n return {\n field: def.field,\n stringify: def.stringify ?? defaultStringify,\n weight: requireFiniteNumber(def.weight ?? 1, `weight for field \"${def.field}\"`, Number.MIN_VALUE),\n };\n });\n}\n\n/**\n * Builds a trigram inverted index over `items` for fast fuzzy search.\n *\n * Construction is O(corpus × field_length). Subsequent `search()` calls are\n * O(candidates) — far faster than per-query Levenshtein for large corpora.\n *\n * @example\n * ```ts\n * const index = createIndex(users, {\n * fields: [{ field: 'name', weight: 2 }, 'email'],\n * threshold: 0.3,\n * limit: 20,\n * });\n *\n * const results = index.search('alice');\n * ```\n *\n * @throws {ScoutConfigError} If options use an invalid field or numeric configuration.\n */\nexport function createIndex<T>(items: T[], options: ScoutIndexOptions<T>): ScoutIndex<T> {\n if (options.fields.length === 0) {\n throw new ScoutConfigError('createIndex: at least one field is required.');\n }\n\n const fields = resolveFields(options.fields);\n const maxWeight = fields.reduce((max, f) => Math.max(max, f.weight), 1);\n const defaultThreshold = requireFiniteNumber(options.threshold ?? 0.2, 'threshold', 0, 1);\n const defaultLimit = requireFiniteInteger(options.limit ?? 50, 'limit', 0);\n const defaultMinQueryLength = requireFiniteInteger(options.minQueryLength ?? 3, 'minQueryLength', 1);\n\n /** item → per-item record, preserves insertion order for `items` getter */\n const itemData = new Map<T, ItemRecord>();\n /** trigram → set of items that contain it */\n const invertedIndex = new Map<string, Set<T>>();\n const mutationListeners = new Set<() => void>();\n let revision = 0;\n\n function notifyMutation(): void {\n revision++;\n\n for (const listener of mutationListeners) listener();\n }\n\n /** Single-entry cache for the most recent normalized query's trigrams (F2). */\n let cachedNormalized: string | null = null;\n let cachedTrigrams: Set<string> | null = null;\n\n function getQueryTrigrams(normalized: string): Set<string> {\n if (normalized === cachedNormalized && cachedTrigrams !== null) return cachedTrigrams;\n\n cachedNormalized = normalized;\n cachedTrigrams = generateTrigrams(normalized);\n\n return cachedTrigrams;\n }\n\n function addFieldToIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n let bucket = invertedIndex.get(trigram);\n\n if (!bucket) {\n bucket = new Set<T>();\n invertedIndex.set(trigram, bucket);\n }\n\n bucket.add(item);\n }\n }\n\n function removeFieldFromIndex(item: T, fieldTrigrams: Set<string>): void {\n for (const trigram of fieldTrigrams) {\n const bucket = invertedIndex.get(trigram);\n\n if (bucket) {\n bucket.delete(item);\n\n if (bucket.size === 0) invertedIndex.delete(trigram);\n }\n }\n }\n\n function addItem(item: T): void {\n const trigrams = new Map<string, Set<string>>();\n const values = new Map<string, string>();\n\n for (const { field, stringify } of fields) {\n const raw = item[field];\n const text = stringify(raw);\n const normalized = tokenize(text);\n const fieldTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n trigrams.set(field, fieldTrigrams);\n values.set(field, text);\n addFieldToIndex(item, fieldTrigrams);\n }\n\n itemData.set(item, { trigrams, values });\n }\n\n /**\n * Performs a full linear scan over all items for short queries.\n * O(n × field_count) — acceptable for small corpora; consider raising\n * `minQueryLength` on large datasets to avoid triggering this path.\n */\n function containmentScan(query: string): Set<T> {\n const result = new Set<T>();\n\n for (const [item, record] of itemData) {\n for (const value of record.values.values()) {\n if (value.toLowerCase().includes(query)) {\n result.add(item);\n break;\n }\n }\n }\n\n return result;\n }\n\n function trigramCandidates(queryTrigrams: Set<string>): Set<T> {\n const candidates = new Set<T>();\n\n for (const trigram of queryTrigrams) {\n const items = invertedIndex.get(trigram);\n\n if (items) {\n for (const item of items) candidates.add(item);\n }\n }\n\n return candidates;\n }\n\n function scoreCandidate(normalized: string, queryTrigrams: Set<string> | null, record: ItemRecord): number {\n let bestScore = 0;\n\n for (const { field, weight } of fields) {\n let fieldScore: number;\n\n if (queryTrigrams === null) {\n const raw = record.values.get(field) ?? '';\n\n fieldScore = raw.toLowerCase().includes(normalized) ? 1.0 : 0;\n } else {\n const itemTrigrams = record.trigrams.get(field);\n\n if (!itemTrigrams || itemTrigrams.size === 0) continue;\n\n fieldScore = overlapSimilarity(queryTrigrams, itemTrigrams);\n }\n\n const weighted = fieldScore * (weight / maxWeight);\n\n if (weighted > bestScore) bestScore = weighted;\n }\n\n return bestScore;\n }\n\n function computeMatches(query: string, values: Map<string, string>): FieldMatch<keyof T & string>[] {\n const matches: FieldMatch<keyof T & string>[] = [];\n\n for (const { field } of fields) {\n const text = values.get(field);\n\n if (!text) continue;\n\n const ranges = findMatchRanges(text, query);\n\n if (ranges.length > 0) matches.push({ field, ranges });\n }\n\n return matches;\n }\n\n function reindexItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n let changed = false;\n\n for (const { field, stringify } of fields) {\n const newText = stringify(item[field]);\n const oldText = record.values.get(field);\n\n if (newText === oldText) continue;\n\n changed = true;\n\n const oldTrigrams = record.trigrams.get(field);\n\n if (oldTrigrams) removeFieldFromIndex(item, oldTrigrams);\n\n const normalized = tokenize(newText);\n const newTrigrams = normalized.length >= 1 ? generateTrigrams(normalized) : new Set<string>();\n\n record.trigrams.set(field, newTrigrams);\n record.values.set(field, newText);\n addFieldToIndex(item, newTrigrams);\n }\n\n return changed;\n }\n\n function removeItem(item: T): boolean {\n const record = itemData.get(item);\n\n if (!record) return false;\n\n for (const fieldTrigrams of record.trigrams.values()) {\n removeFieldFromIndex(item, fieldTrigrams);\n }\n\n itemData.delete(item);\n\n return true;\n }\n\n for (const item of items) {\n if (!itemData.has(item)) addItem(item);\n }\n\n const index: ScoutIndex<T> = {\n add(item: T): void {\n if (itemData.has(item)) return;\n\n addItem(item);\n notifyMutation();\n },\n\n get items(): readonly T[] {\n return [...itemData.keys()];\n },\n\n onMutate(listener: () => void): () => void {\n mutationListeners.add(listener);\n\n return () => {\n mutationListeners.delete(listener);\n };\n },\n\n reindex(item: T): void {\n if (reindexItem(item)) notifyMutation();\n },\n\n remove(item: T): void {\n if (removeItem(item)) notifyMutation();\n },\n\n get revision(): number {\n return revision;\n },\n\n search(query: string, options?: SearchConstraints): SearchResult<T>[] {\n const threshold = requireFiniteNumber(options?.threshold ?? defaultThreshold, 'threshold', 0, 1);\n const limit = requireFiniteInteger(options?.limit ?? defaultLimit, 'limit', 0);\n const minQueryLength = requireFiniteInteger(\n options?.minQueryLength ?? defaultMinQueryLength,\n 'minQueryLength',\n 1,\n );\n\n if (!query.trim()) {\n return [...itemData.keys()].slice(0, limit).map((item) => ({ item, matches: [], score: 1 }));\n }\n\n const normalized = tokenize(query);\n\n // Query had no indexable content (e.g. punctuation-only) — no match, not \"match all\".\n if (!normalized) return [];\n\n const isShort = normalized.length < minQueryLength;\n const queryTrigrams = isShort ? null : getQueryTrigrams(normalized);\n const candidates = queryTrigrams === null ? containmentScan(normalized) : trigramCandidates(queryTrigrams);\n const results: SearchResult<T>[] = [];\n\n for (const item of candidates) {\n const record = itemData.get(item);\n\n if (!record) continue;\n\n const score = scoreCandidate(normalized, queryTrigrams, record);\n\n if (score >= threshold) {\n const matches = computeMatches(normalized, record.values);\n\n results.push({ item, matches, score });\n }\n }\n\n return results.sort((a, b) => b.score - a.score).slice(0, limit);\n },\n\n setItems(items: readonly T[]): void {\n const incoming = new Set(items);\n const next = [...incoming];\n let changed = false;\n\n for (const item of [...itemData.keys()]) {\n if (incoming.has(item)) continue;\n\n removeItem(item);\n changed = true;\n }\n\n for (const item of next) {\n if (!itemData.has(item)) {\n addItem(item);\n changed = true;\n } else if (reindexItem(item)) {\n changed = true;\n }\n }\n\n const current = [...itemData.keys()];\n const orderChanged = current.length !== next.length || current.some((item, index) => item !== next[index]);\n\n if (orderChanged) {\n const records = new Map(next.map((item) => [item, itemData.get(item)!]));\n\n itemData.clear();\n\n for (const [item, record] of records) itemData.set(item, record);\n\n changed = true;\n }\n\n if (changed) notifyMutation();\n },\n\n get size(): number {\n return itemData.size;\n },\n };\n\n return index;\n}\n"],"mappings":"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"}
|
package/dist/scout.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@vielzeug/arsenal");function toSearchMatcher(e,t){let n,r=-1,i=new Set;return(a,o)=>((o!==n||e.revision!==r)&&(n=o,r=e.revision,i=new Set(e.search(o,t).map(e=>e.item))),i.has(a))}function toFilterPredicate(e,t,n){let r=new Set(e.search(t,n).map(e=>e.item));return e=>r.has(e)}var t={Exact:0,FuzzyOnly:5,LabelContains:2,LabelStart:1,MetaContains:3,TextContains:4};function rankEntries(e,n,r={}){let i=n.trim().toLowerCase();if(!i)return[...e];let a=r.fuzzyIds??new Set,o=[];for(let n of e){let e=n.label.toLowerCase(),r=e===i?t.Exact:e.startsWith(i)?t.LabelStart:e.includes(i)?t.LabelContains:n.meta.toLowerCase().includes(i)?t.MetaContains:n.text.toLowerCase().includes(i)?t.TextContains:a.has(n.id)?t.FuzzyOnly:null;r!==null&&o.push([r,n])}return o.sort((e,t)=>e[0]-t[0]||e[1].label.localeCompare(t[1].label)||e[1].text.localeCompare(t[1].text)).map(([,e])=>e)}function dedupeEntries(e){let t=new Set;return e.filter(e=>{let n=`${e.label}\u0000${e.text}`.toLowerCase();return!t.has(n)&&(t.add(n),!0)})}function splitPattern(e,t){if(!t)return[{text:e}];let n=[],r=0;for(let i of e.matchAll(t)){if(i[0].length===0)continue;let t=i.index??0;t>r&&n.push({text:e.slice(r,t)}),n.push({matched:!0,text:i[0]}),r=t+i[0].length}return r<e.length&&n.push({text:e.slice(r)}),n.length?n:[{text:e}]}var n=/[.*+?^${}()|[\]\\]/g,escapeRegExp=e=>e.replace(n,`\\$&`),ScoutError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},ScoutDisposedError=class extends ScoutError{},ScoutConfigError=class extends ScoutError{};function tokenize(e){return e.toLowerCase().replace(/[^\p{L}\p{N}\s']/gu,` `).trim().replace(/\s+/g,` `)}function defaultStringify(e){return e==null?``:typeof e==`string`?e:typeof e==`number`||typeof e==`boolean`?String(e):Array.isArray(e)?e.filter(e=>typeof e==`string`||typeof e==`number`||typeof e==`boolean`).join(` `):``}function findMatchRanges(e,t){let n=e.toLowerCase(),r=tokenize(t).split(` `).filter(Boolean),i=[];for(let e of r){let t=0;for(;t<n.length;){let r=n.indexOf(e,t);if(r===-1)break;i.push([r,r+e.length]),t=r+1}}i.sort((e,t)=>e[0]-t[0]);let a=[];for(let e of i){let t=a[a.length-1];t&&e[0]<=t[1]?t[1]=Math.max(t[1],e[1]):a.push([e[0],e[1]])}return a}function highlight(e,t){if(!e)return[];if(!t.length)return[{highlighted:!1,text:e}];let n=[],r=0;for(let[i,a]of t)i>r&&n.push({highlighted:!1,text:e.slice(r,i)}),a>i&&n.push({highlighted:!0,text:e.slice(i,a)}),r=a;return r<e.length&&n.push({highlighted:!1,text:e.slice(r)}),n}function highlightField(e,t,n){return highlight(n,e.matches.find(e=>e.field===t)?.ranges??[])}function generateTrigrams(e){let t=new Set,n=` ${e} `;for(let e=0;e<n.length-2;e++)t.add(n.slice(e,e+3));return t}function overlapSimilarity(e,t){if(e.size===0||t.size===0)return 0;let n=0;for(let r of e)t.has(r)&&n++;return n<2&&n<e.size?0:n/Math.min(e.size,t.size)}function requireFiniteInteger(e,t,n){if(!Number.isFinite(e)||!Number.isInteger(e)||e<n)throw new ScoutConfigError(`${t} must be a finite integer greater than or equal to ${n}.`);return e}function requireFiniteNumber(e,t,n,r=1/0){if(!Number.isFinite(e)||e<n||e>r)throw new ScoutConfigError(`${t} must be a finite number between ${n} and ${r}.`);return e}function resolveFields(e){return e.map(e=>typeof e==`string`?{field:e,stringify:defaultStringify,weight:1}:{field:e.field,stringify:e.stringify??defaultStringify,weight:requireFiniteNumber(e.weight??1,`weight for field "${e.field}"`,Number.MIN_VALUE)})}function createIndex(e,t){if(t.fields.length===0)throw new ScoutConfigError(`createIndex: at least one field is required.`);let n=resolveFields(t.fields),r=n.reduce((e,t)=>Math.max(e,t.weight),1),i=requireFiniteNumber(t.threshold??.2,`threshold`,0,1),a=requireFiniteInteger(t.limit??50,`limit`,0),o=requireFiniteInteger(t.minQueryLength??3,`minQueryLength`,1),s=new Map,c=new Map,l=new Set,u=0;function notifyMutation(){u++;for(let e of l)e()}let d=null,f=null;function getQueryTrigrams(e){return e===d&&f!==null?f:(d=e,f=generateTrigrams(e),f)}function addFieldToIndex(e,t){for(let n of t){let t=c.get(n);t||(t=new Set,c.set(n,t)),t.add(e)}}function removeFieldFromIndex(e,t){for(let n of t){let t=c.get(n);t&&(t.delete(e),t.size===0&&c.delete(n))}}function addItem(e){let t=new Map,r=new Map;for(let{field:i,stringify:a}of n){let n=e[i],o=a(n),s=tokenize(o),c=s.length>=1?generateTrigrams(s):new Set;t.set(i,c),r.set(i,o),addFieldToIndex(e,c)}s.set(e,{trigrams:t,values:r})}function containmentScan(e){let t=new Set;for(let[n,r]of s)for(let i of r.values.values())if(i.toLowerCase().includes(e)){t.add(n);break}return t}function trigramCandidates(e){let t=new Set;for(let n of e){let e=c.get(n);if(e)for(let n of e)t.add(n)}return t}function scoreCandidate(e,t,i){let a=0;for(let{field:o,weight:s}of n){let n;if(t===null)n=+!!(i.values.get(o)??``).toLowerCase().includes(e);else{let e=i.trigrams.get(o);if(!e||e.size===0)continue;n=overlapSimilarity(t,e)}let c=s/r*n;c>a&&(a=c)}return a}function computeMatches(e,t){let r=[];for(let{field:i}of n){let n=t.get(i);if(!n)continue;let a=findMatchRanges(n,e);a.length>0&&r.push({field:i,ranges:a})}return r}function reindexItem(e){let t=s.get(e);if(!t)return!1;let r=!1;for(let{field:i,stringify:a}of n){let n=a(e[i]);if(n===t.values.get(i))continue;r=!0;let o=t.trigrams.get(i);o&&removeFieldFromIndex(e,o);let s=tokenize(n),c=s.length>=1?generateTrigrams(s):new Set;t.trigrams.set(i,c),t.values.set(i,n),addFieldToIndex(e,c)}return r}function removeItem(e){let t=s.get(e);if(!t)return!1;for(let n of t.trigrams.values())removeFieldFromIndex(e,n);return s.delete(e),!0}for(let t of e)s.has(t)||addItem(t);return{add(e){s.has(e)||(addItem(e),notifyMutation())},get items(){return[...s.keys()]},onMutate(e){return l.add(e),()=>{l.delete(e)}},reindex(e){reindexItem(e)&¬ifyMutation()},remove(e){removeItem(e)&¬ifyMutation()},get revision(){return u},search(e,t){let n=requireFiniteNumber(t?.threshold??i,`threshold`,0,1),r=requireFiniteInteger(t?.limit??a,`limit`,0),c=requireFiniteInteger(t?.minQueryLength??o,`minQueryLength`,1);if(!e.trim())return[...s.keys()].slice(0,r).map(e=>({item:e,matches:[],score:1}));let l=tokenize(e);if(!l)return[];let u=l.length<c?null:getQueryTrigrams(l),d=u===null?containmentScan(l):trigramCandidates(u),f=[];for(let e of d){let t=s.get(e);if(!t)continue;let r=scoreCandidate(l,u,t);if(r>=n){let n=computeMatches(l,t.values);f.push({item:e,matches:n,score:r})}}return f.sort((e,t)=>t.score-e.score).slice(0,r)},setItems(e){let t=new Set(e),n=[...t],r=!1;for(let e of[...s.keys()])t.has(e)||(removeItem(e),r=!0);for(let e of n)s.has(e)?reindexItem(e)&&(r=!0):(addItem(e),r=!0);let i=[...s.keys()];if(i.length!==n.length||i.some((e,t)=>e!==n[t])){let e=new Map(n.map(e=>[e,s.get(e)]));s.clear();for(let[t,n]of e)s.set(t,n);r=!0}r&¬ifyMutation()},get size(){return s.size}}}var r=200;function notify(e){for(let t of[...e])try{t()}catch(e){queueMicrotask(()=>{throw e})}}function subscribe(e,t,n,r,i){if(r||n?.signal?.aborted)return()=>{};e.add(t);let unsubscribe=()=>{e.delete(t),n?.signal?.removeEventListener(`abort`,unsubscribe)};return n?.signal?.addEventListener(`abort`,unsubscribe,{once:!0,signal:i}),unsubscribe}function createSearch(t,n={}){let{debounce:i=r,limit:a,minQueryLength:o,threshold:s}=n;if(!Number.isSafeInteger(i)||i<0)throw new ScoutConfigError(`debounce must be a finite non-negative integer.`);let search=e=>t.search(e,{limit:a,minQueryLength:o,threshold:s}),createSnapshot=(e,t,n)=>Object.freeze({isSearching:t,query:e,results:Object.freeze(n)}),c=new Set,l=(0,e.tapper)(),u=new AbortController,d=createSnapshot(``,!1,search(``)),f=``,p=!1,m,publish=e=>{d=e,notify(c),l.emit({snapshot:e,type:`state-change`})},commit=e=>{f=e,publish(createSnapshot(e,!1,search(e)))},h=t.onMutate(()=>{publish(createSnapshot(d.query,d.isSearching,search(f)))}),assertActive=e=>{if(p)throw new ScoutDisposedError(`SearchState.${e}() called after dispose()`)},dispose=()=>{p||(p=!0,m!==void 0&&clearTimeout(m),m=void 0,h(),c.clear(),l.clear(),u.abort())};return{clear(){assertActive(`clear`),(d.query!==``||d.isSearching)&&(m!==void 0&&clearTimeout(m),m=void 0,commit(``))},get disposalSignal(){return u.signal},dispose,get disposed(){return p},getSnapshot(){return d},setQuery(e){if(assertActive(`setQuery`),e!==d.query){if(m!==void 0&&clearTimeout(m),m=void 0,e===f){publish(createSnapshot(e,!1,d.results));return}if(i===0){commit(e);return}publish(createSnapshot(e,!0,d.results)),m=setTimeout(()=>{m=void 0,commit(e)},i)}},subscribe(e,t){return subscribe(c,e,t,p,u.signal)},tap(e,t){return p?()=>{}:l.tap(e,t)},[Symbol.dispose]:dispose}}function createReactiveSearch(e,t){let n=createIndex(e,t),r=createSearch(n,t);return{clear:r.clear,get disposalSignal(){return r.disposalSignal},dispose:r.dispose,get disposed(){return r.disposed},getSnapshot:r.getSnapshot,index:n,setQuery:r.setQuery,subscribe:r.subscribe,tap:r.tap,[Symbol.dispose]:r[Symbol.dispose]}}function segmentWords(e){let t=getSegmenter();return t?[...t.segment(e)].filter(e=>e.isWordLike).map(e=>e.segment).join(` `):e}var i;function getSegmenter(){return i===void 0&&(i=typeof Intl>`u`||typeof Intl.Segmenter!=`function`?null:new Intl.Segmenter(void 0,{granularity:`word`})),i}exports.ScoutConfigError=ScoutConfigError,exports.ScoutDisposedError=ScoutDisposedError,exports.ScoutError=ScoutError,exports.createIndex=createIndex,exports.createReactiveSearch=createReactiveSearch,exports.createSearch=createSearch,exports.dedupeEntries=dedupeEntries,exports.escapeRegExp=escapeRegExp,exports.findMatchRanges=findMatchRanges,exports.highlight=highlight,exports.highlightField=highlightField,exports.rankEntries=rankEntries,exports.segmentWords=segmentWords,exports.splitPattern=splitPattern,exports.toFilterPredicate=toFilterPredicate,exports.toSearchMatcher=toSearchMatcher;
|
|
2
2
|
//# sourceMappingURL=scout.cjs.map
|