cry-search 1.0.7 → 2.0.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/CLAUDE.md +73 -98
- package/README.md +39 -28
- package/UNIVERSE.md +11 -18
- package/dist/common/SearchUniverse.d.ts.map +1 -1
- package/dist/common/numeric/AugmentedMetadata.d.ts +27 -92
- package/dist/common/numeric/AugmentedMetadata.d.ts.map +1 -1
- package/dist/common/numeric/createSearchArrayMetadata.d.ts +47 -7
- package/dist/common/numeric/createSearchArrayMetadata.d.ts.map +1 -1
- package/dist/common/numeric/findInLinkedArrays.d.ts +7 -4
- package/dist/common/numeric/findInLinkedArrays.d.ts.map +1 -1
- package/dist/common/numeric/scoring.d.ts +95 -0
- package/dist/common/numeric/scoring.d.ts.map +1 -0
- package/dist/common/numeric/searchAndRank.d.ts +18 -60
- package/dist/common/numeric/searchAndRank.d.ts.map +1 -1
- package/dist/common/numeric/searchInData.d.ts +17 -9
- package/dist/common/numeric/searchInData.d.ts.map +1 -1
- package/dist/common/numeric/syncSearchArrayMetadata.d.ts +7 -50
- package/dist/common/numeric/syncSearchArrayMetadata.d.ts.map +1 -1
- package/dist/common/numeric/updateSearchLinkedMetadata.d.ts +11 -110
- package/dist/common/numeric/updateSearchLinkedMetadata.d.ts.map +1 -1
- package/dist/common/numeric/updateSearchMetadata.d.ts.map +1 -1
- package/dist/index.cjs +614 -360
- package/dist/index.d.cts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +614 -360
- package/dist/types/index.d.ts +35 -50
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/matchTokenNumeric.d.ts.map +1 -1
- package/dist/utils/prepareStringForSearchNumeric.d.ts +24 -29
- package/dist/utils/prepareStringForSearchNumeric.d.ts.map +1 -1
- package/package.json +4 -14
- package/dist/common/findInArray.d.ts +0 -37
- package/dist/common/findInArray.d.ts.map +0 -1
- package/dist/common/findInArrayReturnDataAndMeta.d.ts +0 -52
- package/dist/common/findInArrayReturnDataAndMeta.d.ts.map +0 -1
- package/dist/common/findInLinkedArrays.d.ts +0 -66
- package/dist/common/findInLinkedArrays.d.ts.map +0 -1
- package/dist/common/string/createSearchArrayMetadata.d.ts +0 -117
- package/dist/common/string/createSearchArrayMetadata.d.ts.map +0 -1
- package/dist/common/string/createSearchLinkedMetadata.d.ts +0 -36
- package/dist/common/string/createSearchLinkedMetadata.d.ts.map +0 -1
- package/dist/common/string/index.d.ts +0 -10
- package/dist/common/string/index.d.ts.map +0 -1
- package/dist/common/string/updateSearchMetadata.d.ts +0 -103
- package/dist/common/string/updateSearchMetadata.d.ts.map +0 -1
- package/dist/common/syncSearchArrayMetadata.d.ts +0 -68
- package/dist/common/syncSearchArrayMetadata.d.ts.map +0 -1
- package/dist/common/updateSearchLinkedMetadata.d.ts +0 -139
- package/dist/common/updateSearchLinkedMetadata.d.ts.map +0 -1
- package/dist/string.cjs +0 -930
- package/dist/string.d.cts +0 -26
- package/dist/string.d.ts +0 -26
- package/dist/string.d.ts.map +0 -1
- package/dist/string.js +0 -898
- package/dist/utils/matchToken.d.ts +0 -65
- package/dist/utils/matchToken.d.ts.map +0 -1
package/CLAUDE.md
CHANGED
|
@@ -19,18 +19,13 @@ Documentation is spread across multiple files that must be kept in sync:
|
|
|
19
19
|
|
|
20
20
|
## Architecture
|
|
21
21
|
|
|
22
|
-
The library has
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- Tokens stored as numeric IDs in Uint32Array
|
|
28
|
-
2. **String (legacy)** - Original string-based implementation
|
|
29
|
-
- Location: `src/common/string/`
|
|
30
|
-
- Uses string arrays for token storage
|
|
31
|
-
- Available with `*String` suffix (e.g., `createSearchArrayMetadataString`)
|
|
22
|
+
The library has a single implementation: memory-optimized using `Uint32Array` for token storage.
|
|
23
|
+
- Location: `src/common/numeric/`
|
|
24
|
+
- Global token registry (`src/utils/tokenRegistry.ts`) with array-based lookups
|
|
25
|
+
- Tokens stored as numeric IDs in `Uint32Array` per item; strings interned globally
|
|
26
|
+
- Match modes encoded in high 3 bits of each Uint32 token ID
|
|
32
27
|
|
|
33
|
-
### Token Registry
|
|
28
|
+
### Token Registry
|
|
34
29
|
|
|
35
30
|
The numeric implementation uses a global token registry (`src/utils/tokenRegistry.ts`):
|
|
36
31
|
- `tokens: string[]` - Global array of all tokens (preallocated 10k)
|
|
@@ -176,10 +171,10 @@ The numeric implementation uses a global token registry (`src/utils/tokenRegistr
|
|
|
176
171
|
|
|
177
172
|
1. Id: string
|
|
178
173
|
2. NumericToken: number (index into global tokens array)
|
|
179
|
-
3. NumericTokenSortedList: Uint32Array (sorted numeric token IDs)
|
|
180
|
-
4.
|
|
181
|
-
5.
|
|
182
|
-
6.
|
|
174
|
+
3. NumericTokenSortedList: Uint32Array (sorted ASC numeric token IDs — retrieval)
|
|
175
|
+
4. NumericTokenUnsortedList: Uint32Array (first-N tokens in SOURCE order — positional scoring)
|
|
176
|
+
5. NumericSearchMetadata: { sorted: Map<Id, NumericTokenSortedList>, unsorted: Map<Id, NumericTokenUnsortedList> }
|
|
177
|
+
6. NumericLinkedSearchMetadataWithData: { primaryMeta, linkedMeta, primaryToLinked, primaryData, linkedData }
|
|
183
178
|
7. SearchableObject: { _id: Id, _deleted?: Date, _blocked?: Date }
|
|
184
179
|
8. SearchableData<T>: Map<Id, T> where T extends SearchableObject
|
|
185
180
|
9. SearchableObjectSpec<T extends SearchableObject, C = any>:
|
|
@@ -189,6 +184,8 @@ The numeric implementation uses a global token registry (`src/utils/tokenRegistr
|
|
|
189
184
|
4. extractSearchableStringFn?: (obj: SearchableObject) => string | undefined
|
|
190
185
|
5. enrichSearchObjectFn?: (obj: SearchableObject, searchContext: C) => void
|
|
191
186
|
6. fieldMatchModes?: Partial<Record<keyof T | string, MatchMode>> - per-field match modes
|
|
187
|
+
7. keyFields?: (keyof T | string)[] - fields whose tokens populate `metadata.unsorted` (source order) for positional ranking
|
|
188
|
+
8. firstTokensN?: number (default 8) - cap on per-item `metadata.unsorted` length
|
|
192
189
|
10. MatchMode: 'start' | 'end' | 'startEnd' | 'anywhere' | 'whole'
|
|
193
190
|
1. Field token prefixes: '<' (start), '>' (end), '+' (startEnd), '*' (anywhere), '!' (whole)
|
|
194
191
|
2. Numeric: encoded in high 3 bits of Uint32 token ID
|
|
@@ -252,14 +249,20 @@ Override match behavior per token at search time. Priority: query prefix > field
|
|
|
252
249
|
2. stores tokens as Uint32Array per item
|
|
253
250
|
3. clears tokensMap after use (only global tokens[] remains)
|
|
254
251
|
4. for _deleted or _blocked nothing is added
|
|
255
|
-
3. prepareObjectForSearch(obj, spec?, tokensMap):
|
|
252
|
+
3. prepareObjectForSearch(obj, spec?, tokensMap): { sorted, firstN } | undefined
|
|
256
253
|
1. returns undefined if _deleted or _blocked
|
|
257
|
-
2.
|
|
258
|
-
3.
|
|
259
|
-
4.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
254
|
+
2. sorted: full sorted token IDs (with per-field mode bits)
|
|
255
|
+
3. firstN: first N ordered tokens from spec.keyFields (undefined if keyFields not set)
|
|
256
|
+
4. if custom extractSearchableStringFn provided, uses simple processing
|
|
257
|
+
5. otherwise processes fields individually to support per-field match modes
|
|
258
|
+
6. recursively extracts strings from nested objects/arrays
|
|
259
|
+
4. searchInData / searchInDataReturnIds(query, metadata, opts?): Id[]
|
|
260
|
+
1. returns RANKED IDs (by score DESC) when metadata.unsorted is populated
|
|
261
|
+
2. falls back to insertion order when no keyFields declared
|
|
262
|
+
5. searchInDataReturnObjects<T>(query, metadata, data, opts?): Map<Id, T>
|
|
263
|
+
1. iteration order = relevance DESC
|
|
264
|
+
6. searchInDataWithLimit(query, metadata, limit, opts?): Id[]
|
|
265
|
+
1. ranks all candidates first, then slices to limit (so top N are truly best)
|
|
263
266
|
7. updateSearchMetadata(metadata, objectId, obj, spec)
|
|
264
267
|
1. updates or deletes metadata for this object
|
|
265
268
|
2. taking into account _blocked and _deleted
|
|
@@ -277,7 +280,7 @@ Override match behavior per token at search time. Priority: query prefix > field
|
|
|
277
280
|
2. uses previousData to determine removals (no metadata.keys() iteration)
|
|
278
281
|
3. skipUpdateCheck option - skip checking for updates (even faster)
|
|
279
282
|
4. best for regular refresh cycles (e.g., every 30s from server)
|
|
280
|
-
11. AugmentedMetadata class
|
|
283
|
+
11. AugmentedMetadata class (implements NumericSearchMetadata)
|
|
281
284
|
1. constructor(baseMetadata, spec?, augmentedDataMap?)
|
|
282
285
|
1. baseMetadata: NumericSearchMetadata - base metadata to wrap (not modified)
|
|
283
286
|
2. spec?: SearchableObjectSpec - controls tokenization of augmented objects
|
|
@@ -286,8 +289,10 @@ Override match behavior per token at search time. Priority: query prefix > field
|
|
|
286
289
|
3. updateAugmentedBatch(map) - batch update (more efficient than multiple single updates)
|
|
287
290
|
4. clearAugmented(id) - remove augmentation for one item
|
|
288
291
|
5. clearAllAugmented() - remove all augmentations
|
|
289
|
-
6.
|
|
290
|
-
|
|
292
|
+
6. fields:
|
|
293
|
+
- sorted: Map subclass that lazily merges base + augmented tokens on .get()
|
|
294
|
+
- unsorted: direct reference to base.unsorted (augmentation does NOT participate in positional ranking)
|
|
295
|
+
7. sorted.set / sorted.delete / sorted.clear throw — augmentation must go through update methods
|
|
291
296
|
12. Linked metadata update functions (incremental updates to linked metadata)
|
|
292
297
|
1. upsertPrimaryItem(linked, item, spec?)
|
|
293
298
|
1. adds or updates a primary item in linked metadata
|
|
@@ -315,30 +320,39 @@ Override match behavior per token at search time. Priority: query prefix > field
|
|
|
315
320
|
5. only processes items whose foreign key matches primaryId
|
|
316
321
|
6. returns { added: number, updated: number, removed: number }
|
|
317
322
|
|
|
318
|
-
13.
|
|
323
|
+
13. findInLinkedArrays(query, linkedMeta, limit?, opts?): LinkedSearchResultWithMeta
|
|
324
|
+
1. returns RANKED results (by combined primary.unsorted + matched-linked.unsorted score)
|
|
325
|
+
2. ordering: score DESC, primary insertion-order ASC as deterministic tiebreaker
|
|
326
|
+
3. matchedIn: 'primary' | 'linked' | 'both' — where positive tokens matched
|
|
327
|
+
4. tokens that match in primary are excluded from linked matching to prevent FK redundancy
|
|
328
|
+
14. searchAndRank(query, data, metadata, options): ScoredResult<T>[]
|
|
319
329
|
1. searchAndRank = searchAndRankNumeric (alias for the numeric implementation)
|
|
320
|
-
2.
|
|
330
|
+
2. like searchInDataReturnIdsNumeric but adds:
|
|
331
|
+
- exactKeysFn boost (+100_000 when raw query equals any extracted value)
|
|
332
|
+
- locale-aware primary-text tiebreaker
|
|
333
|
+
- fallback to orderedTokenize(primaryTextFn(item)) when metadata.unsorted is empty
|
|
321
334
|
3. options: { primaryTextFn, exactKeysFn?, limit?, opts? }
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
exact equality with raw query returns score 100_000
|
|
325
|
-
4. results sorted by score desc, locale-aware primary-text asc as tiebreaker
|
|
326
|
-
5. scoring components (additive):
|
|
327
|
-
- +100_000 exact key match
|
|
328
|
-
- +50 base for any subsequence-prefix match in primary text
|
|
329
|
-
- +100 all query tokens matched (in order)
|
|
330
|
-
- +50 first matched position is 0
|
|
331
|
-
- +80 perfect prefix (positions === [0..k-1])
|
|
332
|
-
- +30 * (matchedCount / span) density
|
|
333
|
-
- -0.1 * primaryText.length length penalty
|
|
334
|
-
6. returns 1 if candidate matched only via secondary field (no primary token match)
|
|
335
|
-
14. orderedTokenize(input: string): string[]
|
|
335
|
+
4. results sorted by score desc, primaryTextFn(item) localeCompare asc as tiebreaker
|
|
336
|
+
15. orderedTokenize(input: string): string[]
|
|
336
337
|
1. prepareStringForSearch pipeline minus the trailing .sort()
|
|
337
338
|
2. required for any positional / order-aware scoring
|
|
338
339
|
3. do NOT use these tokens for candidate retrieval (sorted lists required)
|
|
339
|
-
|
|
340
|
-
1.
|
|
341
|
-
|
|
340
|
+
16. Scoring primitives (in src/common/numeric/scoring.ts, re-exported from main)
|
|
341
|
+
1. scoreOne(query, qTokens, itemTokens, exactKeys?): number
|
|
342
|
+
- feature-rich scoring: +100_000 exact, +50 base, +100 all-matched, +50 first-word,
|
|
343
|
+
+80 perfect prefix, +30*(matched/span) density, -0.1*Σitemtoken.length penalty
|
|
344
|
+
- returns 1 when no primary token matched (secondary-field-only signal)
|
|
345
|
+
- DEFAULT used internally by searchInData* and findInLinkedArrays
|
|
346
|
+
2. scoreOrderAndPresence(qTokens, itemTokens, weights?): number
|
|
347
|
+
- additive bonus: weights.ordered (default 30) per in-order match,
|
|
348
|
+
weights.presence (default 10) per out-of-order presence
|
|
349
|
+
- each query token contributes at most once
|
|
350
|
+
- "izdaja recepta":"izdaja recepta"=60, ":izdaja"=30, ":recepta izdaja"=40
|
|
351
|
+
3. scoreBag(qTokens, itemTokens, perTokenWeight=30): number
|
|
352
|
+
- ORDER-AGNOSTIC: same score regardless of query/item token order
|
|
353
|
+
- use for person names, tags, breeds where word order is conventional but not semantic
|
|
354
|
+
- "Janez Novak" vs "Novak Janez" = SAME score (invariant verified by tests)
|
|
355
|
+
- each item token consumed by at most one query token (greedy left-to-right)
|
|
342
356
|
|
|
343
357
|
## Token Registry Functions
|
|
344
358
|
|
|
@@ -352,75 +366,36 @@ Override match behavior per token at search time. Priority: query prefix > field
|
|
|
352
366
|
8. clearTokensMap(tokensMap): void
|
|
353
367
|
9. resetTokenRegistry(): void (for testing only)
|
|
354
368
|
|
|
355
|
-
## String Implementation (Legacy)
|
|
356
|
-
|
|
357
|
-
The string-based implementation is available as a separate entry point for backwards compatibility.
|
|
358
|
-
|
|
359
|
-
**Import from `cry-search/string`:**
|
|
360
|
-
```typescript
|
|
361
|
-
import {
|
|
362
|
-
createSearchArrayMetadataString,
|
|
363
|
-
updateSearchMetadataString,
|
|
364
|
-
findInArray,
|
|
365
|
-
// ... other string-based functions
|
|
366
|
-
} from 'cry-search/string';
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
**Available functions with `*String` suffix:**
|
|
370
|
-
- createSearchArrayMetadataString
|
|
371
|
-
- createSearchLinkedMetadataString
|
|
372
|
-
- updateSearchMetadataString
|
|
373
|
-
- updateSearchMetadataBatchString
|
|
374
|
-
- syncSearchArrayMetadataString
|
|
375
|
-
- syncSearchArrayMetadataStringWithPrevious
|
|
376
|
-
- upsertPrimaryItemString
|
|
377
|
-
- removePrimaryItemString
|
|
378
|
-
- upsertLinkedItemString
|
|
379
|
-
- removeLinkedItemString
|
|
380
|
-
- syncLinkedItemsForPrimaryString
|
|
381
|
-
- findInArray (uses string metadata)
|
|
382
|
-
- findInArrayReturnDataAndMeta
|
|
383
|
-
- findInLinkedArrays
|
|
384
|
-
|
|
385
|
-
**Bundle sizes:**
|
|
386
|
-
- Main bundle (`cry-search`): ~49 KB - includes only numeric implementation
|
|
387
|
-
- String bundle (`cry-search/string`): ~26 KB - includes only string implementation
|
|
388
|
-
|
|
389
369
|
## File Structure
|
|
390
370
|
|
|
391
371
|
```
|
|
392
372
|
src/
|
|
393
373
|
├── common/
|
|
394
|
-
│ ├── numeric/
|
|
374
|
+
│ ├── numeric/ # Numeric implementation (only impl)
|
|
395
375
|
│ │ ├── index.ts
|
|
396
376
|
│ │ ├── createSearchArrayMetadata.ts
|
|
397
377
|
│ │ ├── createSearchLinkedMetadata.ts
|
|
398
378
|
│ │ ├── updateSearchMetadata.ts
|
|
399
379
|
│ │ ├── updateSearchLinkedMetadata.ts
|
|
400
|
-
│ │ ├── searchInData.ts
|
|
401
|
-
│ │ ├──
|
|
380
|
+
│ │ ├── searchInData.ts # Ranked search via metadata.unsorted
|
|
381
|
+
│ │ ├── findInLinkedArrays.ts # Ranked linked search
|
|
382
|
+
│ │ ├── searchAndRank.ts # Wrapper w/ exactKeysFn boost + fallback
|
|
383
|
+
│ │ ├── scoring.ts # scoreOne, scoreOrderAndPresence, scoreBag
|
|
402
384
|
│ │ ├── syncSearchArrayMetadata.ts
|
|
403
385
|
│ │ └── AugmentedMetadata.ts
|
|
404
|
-
│
|
|
405
|
-
│ │ ├── index.ts
|
|
406
|
-
│ │ ├── createSearchArrayMetadata.ts
|
|
407
|
-
│ │ ├── createSearchLinkedMetadata.ts
|
|
408
|
-
│ │ └── updateSearchMetadata.ts
|
|
409
|
-
│ ├── findInArray.ts # String-based search
|
|
410
|
-
│ ├── findInLinkedArrays.ts # String-based linked search
|
|
411
|
-
│ ├── syncSearchArrayMetadata.ts
|
|
412
|
-
│ ├── updateSearchLinkedMetadata.ts
|
|
413
|
-
│ └── SearchUniverse.ts # Multi-collection manager (uses numeric)
|
|
386
|
+
│ └── SearchUniverse.ts # Multi-collection manager
|
|
414
387
|
├── utils/
|
|
415
|
-
│ ├── tokenRegistry.ts
|
|
416
|
-
│ ├── prepareStringForSearch.ts
|
|
417
|
-
│ ├── prepareStringForSearchNumeric.ts
|
|
418
|
-
│ ├──
|
|
419
|
-
│ ├──
|
|
420
|
-
│
|
|
388
|
+
│ ├── tokenRegistry.ts # Global tokens array
|
|
389
|
+
│ ├── prepareStringForSearch.ts # Shared query tokenization (returns sorted strings)
|
|
390
|
+
│ ├── prepareStringForSearchNumeric.ts # For metadata build (returns Uint32Array of token IDs)
|
|
391
|
+
│ ├── matchTokenNumeric.ts
|
|
392
|
+
│ ├── normalizeDates.ts
|
|
393
|
+
│ ├── preprocessString.ts
|
|
394
|
+
│ ├── sanitiseString.ts
|
|
395
|
+
│ └── tokenize.ts
|
|
421
396
|
├── types/
|
|
422
397
|
│ └── index.ts
|
|
423
|
-
└── index.ts
|
|
398
|
+
└── index.ts # Public API
|
|
424
399
|
```
|
|
425
400
|
|
|
426
401
|
## Coding rules
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A fast, memory-efficient search library for large datasets with support for toke
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Fast search** - Pre-built metadata enables instant queries on large datasets
|
|
8
|
-
- **Memory efficient** - Numeric token storage
|
|
8
|
+
- **Memory efficient** - Numeric token storage (`Uint32Array`) keeps metadata compact
|
|
9
9
|
- **Updatable** - Add, update, or remove items without rebuilding metadata
|
|
10
10
|
- **Augmented metadata** - Add local/temporary searchable data without modifying global metadata
|
|
11
11
|
- **Flexible matching** - Per-field match modes and query prefixes for prefix, suffix, anywhere, exact, and negation
|
|
@@ -32,7 +32,6 @@ A fast, memory-efficient search library for large datasets with support for toke
|
|
|
32
32
|
- [Searching](#searching)
|
|
33
33
|
- [Memory Optimization](#memory-optimization)
|
|
34
34
|
- [Query Prefixes](#query-prefixes)
|
|
35
|
-
- [Legacy String Implementation](#legacy-string-implementation)
|
|
36
35
|
- [Specification](#specification)
|
|
37
36
|
- [License](#license)
|
|
38
37
|
|
|
@@ -282,9 +281,11 @@ augmented.clearAllAugmented(); // Reset to global state
|
|
|
282
281
|
|
|
283
282
|
### Ranked Search with Positional Scoring
|
|
284
283
|
|
|
285
|
-
`
|
|
284
|
+
**Since 2.0.0** all search functions return ranked results by default. `searchInData*` and `findInLinkedArrays` automatically score candidates and sort by relevance DESC. To activate positional scoring, declare `spec.keyFields` — those fields are tokenized in source order at build time and stored in `metadata.unsorted` for fast query-time scoring.
|
|
286
285
|
|
|
287
|
-
`searchAndRank`
|
|
286
|
+
`searchAndRank` is a thin wrapper that additionally supports:
|
|
287
|
+
- **`exactKeysFn`** — +100 000 boost when raw query equals a designated field value (e.g. barcode)
|
|
288
|
+
- **`primaryTextFn`** — fallback when `keyFields` not declared, plus locale-aware tiebreaker sorting
|
|
288
289
|
|
|
289
290
|
```typescript
|
|
290
291
|
import {
|
|
@@ -308,7 +309,11 @@ const products: Product[] = [
|
|
|
308
309
|
];
|
|
309
310
|
|
|
310
311
|
const data = arrayToSearchableData(products);
|
|
311
|
-
|
|
312
|
+
// Declare keyFields so positional ranking has pre-stored ordered tokens (faster).
|
|
313
|
+
const meta = createSearchArrayMetadata(data, {
|
|
314
|
+
keyFields: ['name'],
|
|
315
|
+
firstTokensN: 8,
|
|
316
|
+
});
|
|
312
317
|
|
|
313
318
|
// Returns items sorted by relevance, NOT discovery order:
|
|
314
319
|
const results = searchAndRank("pot mat", data, meta, {
|
|
@@ -323,25 +328,45 @@ const results = searchAndRank("pot mat", data, meta, {
|
|
|
323
328
|
// Each result: { id: Id, item: T, score: number }
|
|
324
329
|
```
|
|
325
330
|
|
|
326
|
-
**
|
|
331
|
+
**Performance optimization via `keyFields`**: When the spec declares `keyFields`, the first N tokens (`firstTokensN`, default 8) of those fields are stored in `metadata.unsorted` at build time. `searchAndRank` reads those at query time instead of re-tokenizing — typically **10-50× faster scoring**. Without `keyFields`, `searchAndRank` falls back to tokenizing `primaryTextFn(item)` per candidate at query time (same correctness, slower).
|
|
332
|
+
|
|
333
|
+
**Scoring formula** (`scoreOne` is exported as a primitive — useful for composing your own scorer):
|
|
327
334
|
|
|
328
335
|
| Component | Value | When |
|
|
329
336
|
|---|---|---|
|
|
330
337
|
| Exact-key match | **+100 000** | `query === any(exactKeys)` (e.g. barcode equality) |
|
|
331
|
-
| Base | +50 | Any in-order subsequence-prefix match of query tokens in
|
|
338
|
+
| Base | +50 | Any in-order subsequence-prefix match of query tokens in itemTokens |
|
|
332
339
|
| All tokens matched | +100 | Every query token had a target |
|
|
333
340
|
| First-word match | +50 | First match at position 0 |
|
|
334
341
|
| Perfect prefix | +80 | Matches at positions `[0,1,…,k-1]` (contiguous from start) |
|
|
335
342
|
| Density | +30 × (matched / span) | Rewards contiguous matches over gapped |
|
|
336
|
-
| Length penalty | -0.1 ×
|
|
337
|
-
| Secondary-field-only | 1 | Item matched (via
|
|
343
|
+
| Length penalty | -0.1 × `Σ itemToken.length` | Weak tiebreaker — shorter wins |
|
|
344
|
+
| Secondary-field-only | 1 | Item matched (via retrieval) but no query token appears in itemTokens |
|
|
345
|
+
|
|
346
|
+
**Lower-level scoring primitives** for composing custom rankers:
|
|
347
|
+
|
|
348
|
+
| Function | Use case | Order-sensitive? |
|
|
349
|
+
|---|---|---|
|
|
350
|
+
| `scoreOne(query, qTokens, itemTokens, exactKeys?)` | Full-featured (perfect prefix, density, length penalty, exact-key boost) — default used by `searchInData*` and `searchAndRank` | yes |
|
|
351
|
+
| `scoreOrderAndPresence(qTokens, itemTokens, weights?)` | Simpler additive bonus: ordered match (+30) + presence (+10) per token. Tunable via weights | yes |
|
|
352
|
+
| `scoreBag(qTokens, itemTokens, perTokenWeight?)` | Order-agnostic: each query token present in itemTokens contributes equally. Use for **person names**, tags, pet breeds where word order is conventional but not semantic — `scoreBag(["janez","novak"], item)` === `scoreBag(["novak","janez"], item)` | no |
|
|
353
|
+
| `orderedTokenize(input)` | Tokenize without alphabetic sort (required for any positional scoring). Output is NOT compatible with `searchInData*` retrieval (which requires sorted lists from `prepareStringForSearch`) | n/a |
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { scoreBag, scoreOne, scoreOrderAndPresence, orderedTokenize, tokens, findTokenId } from 'cry-search';
|
|
338
357
|
|
|
339
|
-
|
|
358
|
+
// Combine signals per field in a custom ranker:
|
|
359
|
+
const qTokens = orderedTokenize(query);
|
|
340
360
|
|
|
341
|
-
|
|
342
|
-
|
|
361
|
+
// From metadata.unsorted (numeric IDs → strings via global tokens):
|
|
362
|
+
const unsortedIds = meta.unsorted.get(itemId);
|
|
363
|
+
const itemTokens = unsortedIds ? Array.from(unsortedIds, (id) => tokens[id]!) : [];
|
|
343
364
|
|
|
344
|
-
|
|
365
|
+
const baseScore = scoreOne(query, qTokens, itemTokens, item.barcodes);
|
|
366
|
+
const nameBoost = scoreBag(qTokens, nameTokens); // order-agnostic for "Janez Novak"
|
|
367
|
+
const titleBoost = scoreOrderAndPresence(qTokens, titleTokens); // order-sensitive for "RTG slika"
|
|
368
|
+
const total = baseScore + nameBoost + titleBoost;
|
|
369
|
+
```
|
|
345
370
|
|
|
346
371
|
## Architecture
|
|
347
372
|
|
|
@@ -372,7 +397,7 @@ Query tokens are matched against metadata using binary search. All query tokens
|
|
|
372
397
|
|
|
373
398
|
### Memory Optimization
|
|
374
399
|
|
|
375
|
-
|
|
400
|
+
Tokens are stored as `Uint32Array` indices into a global `tokens: string[]` registry. Each metadata entry is a compact sorted Uint32Array per item — tokens themselves are interned once globally.
|
|
376
401
|
|
|
377
402
|
## Query Prefixes
|
|
378
403
|
|
|
@@ -448,20 +473,6 @@ This enables matching by either part:
|
|
|
448
473
|
|
|
449
474
|
Priority: query prefix > field match mode > global SearchOpts default.
|
|
450
475
|
|
|
451
|
-
## Legacy String Implementation
|
|
452
|
-
|
|
453
|
-
A string-based implementation is available for backwards compatibility. Import from the `/string` subpath:
|
|
454
|
-
|
|
455
|
-
```typescript
|
|
456
|
-
import {
|
|
457
|
-
createSearchArrayMetadataString,
|
|
458
|
-
updateSearchMetadataString,
|
|
459
|
-
findInArray, // String-based search function
|
|
460
|
-
} from 'cry-search/string';
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
The string implementation is excluded from the main bundle. The default import (`cry-search`) only includes the modern numeric implementation, keeping bundle sizes smaller.
|
|
464
|
-
|
|
465
476
|
## Specification
|
|
466
477
|
|
|
467
478
|
See [CLAUDE.md](./CLAUDE.md) for the complete technical specification including:
|
package/UNIVERSE.md
CHANGED
|
@@ -794,36 +794,29 @@ For direct metadata manipulation without SearchUniverse:
|
|
|
794
794
|
│ │
|
|
795
795
|
│ ────────────────────────────────────────────────────────────────────── │
|
|
796
796
|
│ │
|
|
797
|
-
│
|
|
797
|
+
│ SYNC: │
|
|
798
798
|
│ │
|
|
799
|
-
│
|
|
800
|
-
│
|
|
801
|
-
│ updateSearchMetadataBatchString(metadata, batch, spec) │
|
|
802
|
-
│ removeFromSearchMetadataString(metadata, id) │
|
|
803
|
-
│ findInArray(query, data, metadata) │
|
|
804
|
-
│ │
|
|
805
|
-
│ ────────────────────────────────────────────────────────────────────── │
|
|
806
|
-
│ │
|
|
807
|
-
│ SYNC (String-based): │
|
|
808
|
-
│ │
|
|
809
|
-
│ syncSearchArrayMetadata(data, metadata, spec) │
|
|
810
|
-
│ Syncs metadata with current data state │
|
|
799
|
+
│ syncSearchArrayMetadata(metadata, newData, spec) │
|
|
800
|
+
│ Syncs metadata with new full data snapshot │
|
|
811
801
|
│ Returns: { added, updated, removed, addedIds, updatedIds, removedIds } │
|
|
812
802
|
│ │
|
|
803
|
+
│ syncSearchArrayMetadataWithPrevious(metadata, prev, newData, opts?, spec) │
|
|
804
|
+
│ Same but uses previous data to skip metadata.keys() iteration │
|
|
805
|
+
│ │
|
|
813
806
|
│ ────────────────────────────────────────────────────────────────────── │
|
|
814
807
|
│ │
|
|
815
|
-
│
|
|
808
|
+
│ Linked metadata (incremental): │
|
|
816
809
|
│ │
|
|
817
|
-
│ upsertPrimaryItem(linked,
|
|
810
|
+
│ upsertPrimaryItem(linked, item, spec) │
|
|
818
811
|
│ Adds/updates primary item in linked structure │
|
|
819
812
|
│ │
|
|
820
|
-
│ upsertLinkedItem(linked,
|
|
813
|
+
│ upsertLinkedItem(linked, item, fkGetter, spec) │
|
|
821
814
|
│ Adds/updates linked item, handles FK changes │
|
|
822
815
|
│ │
|
|
823
|
-
│ removePrimaryItem(linked,
|
|
816
|
+
│ removePrimaryItem(linked, primaryId) │
|
|
824
817
|
│ Removes primary item │
|
|
825
818
|
│ │
|
|
826
|
-
│ removeLinkedItem(linked,
|
|
819
|
+
│ removeLinkedItem(linked, linkedId, fkGetter) │
|
|
827
820
|
│ Removes linked item from structure and index │
|
|
828
821
|
│ │
|
|
829
822
|
│ syncLinkedItemsForPrimary(linked, primaryId, items, fkGetter, spec) │
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchUniverse.d.ts","sourceRoot":"","sources":["../../src/common/SearchUniverse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,EAAE,EACF,qBAAqB,EAErB,cAAc,EACd,gBAAgB,EAEhB,mBAAmB,EACnB,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,UAAU,EACV,WAAW,EACZ,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"SearchUniverse.d.ts","sourceRoot":"","sources":["../../src/common/SearchUniverse.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,111 +1,46 @@
|
|
|
1
|
-
import type { Id, NumericSearchMetadata, NumericTokenSortedList, SearchableObjectSpec } from '../../types/index.js';
|
|
1
|
+
import type { Id, NumericSearchMetadata, NumericTokenSortedList, NumericTokenUnsortedList, SearchableObjectSpec } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* additional
|
|
3
|
+
* Wraps a base {@link NumericSearchMetadata} and exposes a view that overlays
|
|
4
|
+
* additional ("augmented") tokens onto `sorted` per item, without modifying base.
|
|
5
5
|
*
|
|
6
|
-
* Use case:
|
|
7
|
-
*
|
|
6
|
+
* Use case: layer page-local or session-specific searchable data (e.g. status
|
|
7
|
+
* flags) onto a globally-built metadata snapshot.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Augmentation policy:
|
|
10
|
+
* - `sorted` (retrieval) — base tokens **merged** with augmented tokens. Both
|
|
11
|
+
* participate in `searchInData` matching.
|
|
12
|
+
* - `unsorted` (positional ranking) — **base only**. Augmented tokens are NOT
|
|
13
|
+
* included; positional scoring uses base order as authoritative.
|
|
11
14
|
*
|
|
12
15
|
* @example
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* const results = searchInDataReturnIds("aspirin uparjen", augmented);
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const aug = new AugmentedMetadata(zdravilaMeta, { searchInFields: ['status'] });
|
|
18
|
+
* aug.updateAugmented('id1', { status: 'uparjen' });
|
|
19
|
+
* const ids = searchInDataReturnIds('aspirin uparjen', aug);
|
|
20
|
+
* ```
|
|
19
21
|
*/
|
|
20
22
|
export declare class AugmentedMetadata implements NumericSearchMetadata {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
private
|
|
23
|
+
readonly sorted: Map<Id, NumericTokenSortedList>;
|
|
24
|
+
readonly unsorted: Map<Id, NumericTokenUnsortedList>;
|
|
25
|
+
private readonly augmentedTokens;
|
|
26
|
+
private readonly spec;
|
|
24
27
|
/**
|
|
25
28
|
* @param baseMetadata - Base search metadata (not modified)
|
|
26
|
-
* @param spec - Optional spec for tokenizing augmented objects
|
|
27
|
-
* @param augmentedDataMap - Optional initial
|
|
29
|
+
* @param spec - Optional spec for tokenizing augmented objects
|
|
30
|
+
* @param augmentedDataMap - Optional initial map of id → augmented object
|
|
28
31
|
*/
|
|
29
32
|
constructor(baseMetadata: NumericSearchMetadata, spec?: SearchableObjectSpec<any>, augmentedDataMap?: Map<Id, any>);
|
|
30
33
|
/**
|
|
31
|
-
* Update augmented data for a single item.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* @param id - Item ID
|
|
35
|
-
* @param augmentedData - Additional searchable object (will be tokenized using spec)
|
|
34
|
+
* Update augmented data for a single item. Pass `null`/`undefined` to clear.
|
|
35
|
+
* Augmented tokens are merged into `sorted` at lookup time; `unsorted` is
|
|
36
|
+
* unaffected.
|
|
36
37
|
*/
|
|
37
38
|
updateAugmented(id: Id, augmentedData: any): void;
|
|
38
|
-
/**
|
|
39
|
-
* Batch update augmented data for multiple items.
|
|
40
|
-
* More efficient than calling updateAugmented multiple times.
|
|
41
|
-
*
|
|
42
|
-
* @param augmentedDataMap - Map of id -> augmented object (will be tokenized using spec)
|
|
43
|
-
*/
|
|
39
|
+
/** Batch update augmented data for multiple items. */
|
|
44
40
|
updateAugmentedBatch(augmentedDataMap: Map<Id, any>): void;
|
|
45
|
-
/**
|
|
46
|
-
* Remove augmentation for a single item (reverts to base metadata only).
|
|
47
|
-
*/
|
|
41
|
+
/** Remove augmentation for a single item (reverts to base only). */
|
|
48
42
|
clearAugmented(id: Id): void;
|
|
49
|
-
/**
|
|
50
|
-
* Remove all augmentations (reverts to base metadata for all items).
|
|
51
|
-
*/
|
|
43
|
+
/** Remove all augmentations. */
|
|
52
44
|
clearAllAugmented(): void;
|
|
53
|
-
/**
|
|
54
|
-
* Get merged tokens for an item (base + augmented).
|
|
55
|
-
* Returns undefined if item doesn't exist in base metadata.
|
|
56
|
-
*/
|
|
57
|
-
get(id: Id): NumericTokenSortedList | undefined;
|
|
58
|
-
/**
|
|
59
|
-
* Check if item exists in base metadata.
|
|
60
|
-
*/
|
|
61
|
-
has(id: Id): boolean;
|
|
62
|
-
/**
|
|
63
|
-
* Get number of items in base metadata.
|
|
64
|
-
*/
|
|
65
|
-
get size(): number;
|
|
66
|
-
/**
|
|
67
|
-
* Iterate over all items (id -> merged tokens).
|
|
68
|
-
*/
|
|
69
|
-
[Symbol.iterator](): MapIterator<[Id, NumericTokenSortedList]>;
|
|
70
|
-
/**
|
|
71
|
-
* Iterate over all IDs.
|
|
72
|
-
*/
|
|
73
|
-
keys(): MapIterator<Id>;
|
|
74
|
-
/**
|
|
75
|
-
* Iterate over all merged token arrays.
|
|
76
|
-
*/
|
|
77
|
-
values(): MapIterator<NumericTokenSortedList>;
|
|
78
|
-
/**
|
|
79
|
-
* Iterate over all [id, merged tokens] entries.
|
|
80
|
-
*/
|
|
81
|
-
entries(): MapIterator<[Id, NumericTokenSortedList]>;
|
|
82
|
-
/**
|
|
83
|
-
* Execute callback for each item.
|
|
84
|
-
*/
|
|
85
|
-
forEach(callbackfn: (value: NumericTokenSortedList, key: Id, map: Map<Id, NumericTokenSortedList>) => void): void;
|
|
86
|
-
/**
|
|
87
|
-
* Not supported - this is a read-only wrapper.
|
|
88
|
-
* Use updateAugmented() or updateAugmentedBatch() instead.
|
|
89
|
-
*/
|
|
90
|
-
set(_key: Id, _value: NumericTokenSortedList): this;
|
|
91
|
-
/**
|
|
92
|
-
* Not supported - this is a read-only wrapper.
|
|
93
|
-
* Use clearAugmented() instead.
|
|
94
|
-
*/
|
|
95
|
-
delete(_key: Id): boolean;
|
|
96
|
-
/**
|
|
97
|
-
* Not supported - this is a read-only wrapper.
|
|
98
|
-
* Use clearAllAugmented() instead.
|
|
99
|
-
*/
|
|
100
|
-
clear(): void;
|
|
101
|
-
/**
|
|
102
|
-
* Symbol.toStringTag for Map interface.
|
|
103
|
-
*/
|
|
104
|
-
get [Symbol.toStringTag](): string;
|
|
105
|
-
/**
|
|
106
|
-
* Merge two sorted Uint32Arrays into a new sorted Uint32Array.
|
|
107
|
-
* Removes duplicates.
|
|
108
|
-
*/
|
|
109
|
-
private mergeSortedTokenArrays;
|
|
110
45
|
}
|
|
111
46
|
//# sourceMappingURL=AugmentedMetadata.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AugmentedMetadata.d.ts","sourceRoot":"","sources":["../../../src/common/numeric/AugmentedMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"AugmentedMetadata.d.ts","sourceRoot":"","sources":["../../../src/common/numeric/AugmentedMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,EAAE,EACF,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,EACrB,MAAM,sBAAsB,CAAA;AAqJ7B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,iBAAkB,YAAW,qBAAqB;IAC7D,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EAAE,sBAAsB,CAAC,CAAA;IAChD,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,EAAE,wBAAwB,CAAC,CAAA;IAEpD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiC;IAEjE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuC;IAE5D;;;;OAIG;gBAED,YAAY,EAAE,qBAAqB,EAEnC,IAAI,CAAC,EAAE,oBAAoB,CAAC,GAAG,CAAC,EAEhC,gBAAgB,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC;IAcjC;;;;OAIG;IAEH,eAAe,CAAC,EAAE,EAAE,EAAE,EAAE,aAAa,EAAE,GAAG,GAAG,IAAI;IAiBjD,sDAAsD;IAEtD,oBAAoB,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,GAAG,IAAI;IAmB1D,oEAAoE;IACpE,cAAc,CAAC,EAAE,EAAE,EAAE,GAAG,IAAI;IAI5B,gCAAgC;IAChC,iBAAiB,IAAI,IAAI;CAG1B"}
|