cry-search 1.0.6 → 1.0.8

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.
Files changed (58) hide show
  1. package/CLAUDE.md +79 -78
  2. package/README.md +93 -17
  3. package/UNIVERSE.md +29 -20
  4. package/dist/common/SearchUniverse.d.ts +28 -13
  5. package/dist/common/SearchUniverse.d.ts.map +1 -1
  6. package/dist/common/numeric/AugmentedMetadata.d.ts +27 -92
  7. package/dist/common/numeric/AugmentedMetadata.d.ts.map +1 -1
  8. package/dist/common/numeric/createSearchArrayMetadata.d.ts +47 -7
  9. package/dist/common/numeric/createSearchArrayMetadata.d.ts.map +1 -1
  10. package/dist/common/numeric/findInLinkedArrays.d.ts +7 -4
  11. package/dist/common/numeric/findInLinkedArrays.d.ts.map +1 -1
  12. package/dist/common/numeric/scoring.d.ts +95 -0
  13. package/dist/common/numeric/scoring.d.ts.map +1 -0
  14. package/dist/common/numeric/searchAndRank.d.ts +48 -0
  15. package/dist/common/numeric/searchAndRank.d.ts.map +1 -0
  16. package/dist/common/numeric/searchInData.d.ts +17 -9
  17. package/dist/common/numeric/searchInData.d.ts.map +1 -1
  18. package/dist/common/numeric/syncSearchArrayMetadata.d.ts +7 -50
  19. package/dist/common/numeric/syncSearchArrayMetadata.d.ts.map +1 -1
  20. package/dist/common/numeric/updateSearchLinkedMetadata.d.ts +11 -110
  21. package/dist/common/numeric/updateSearchLinkedMetadata.d.ts.map +1 -1
  22. package/dist/common/numeric/updateSearchMetadata.d.ts.map +1 -1
  23. package/dist/index.cjs +646 -312
  24. package/dist/index.d.cts +3 -3
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +646 -312
  28. package/dist/types/index.d.ts +35 -50
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/dist/utils/matchTokenNumeric.d.ts.map +1 -1
  31. package/dist/utils/prepareStringForSearchNumeric.d.ts +24 -29
  32. package/dist/utils/prepareStringForSearchNumeric.d.ts.map +1 -1
  33. package/package.json +4 -14
  34. package/dist/common/findInArray.d.ts +0 -37
  35. package/dist/common/findInArray.d.ts.map +0 -1
  36. package/dist/common/findInArrayReturnDataAndMeta.d.ts +0 -52
  37. package/dist/common/findInArrayReturnDataAndMeta.d.ts.map +0 -1
  38. package/dist/common/findInLinkedArrays.d.ts +0 -66
  39. package/dist/common/findInLinkedArrays.d.ts.map +0 -1
  40. package/dist/common/string/createSearchArrayMetadata.d.ts +0 -117
  41. package/dist/common/string/createSearchArrayMetadata.d.ts.map +0 -1
  42. package/dist/common/string/createSearchLinkedMetadata.d.ts +0 -36
  43. package/dist/common/string/createSearchLinkedMetadata.d.ts.map +0 -1
  44. package/dist/common/string/index.d.ts +0 -10
  45. package/dist/common/string/index.d.ts.map +0 -1
  46. package/dist/common/string/updateSearchMetadata.d.ts +0 -103
  47. package/dist/common/string/updateSearchMetadata.d.ts.map +0 -1
  48. package/dist/common/syncSearchArrayMetadata.d.ts +0 -68
  49. package/dist/common/syncSearchArrayMetadata.d.ts.map +0 -1
  50. package/dist/common/updateSearchLinkedMetadata.d.ts +0 -139
  51. package/dist/common/updateSearchLinkedMetadata.d.ts.map +0 -1
  52. package/dist/string.cjs +0 -930
  53. package/dist/string.d.cts +0 -26
  54. package/dist/string.d.ts +0 -26
  55. package/dist/string.d.ts.map +0 -1
  56. package/dist/string.js +0 -898
  57. package/dist/utils/matchToken.d.ts +0 -65
  58. 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 two implementations:
23
- 1. **Numeric (default)** - Memory-optimized using Uint32Array for token storage
24
- - Location: `src/common/numeric/`
25
- - ~45% less memory than string implementation
26
- - Global token registry with array-based lookups
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 (Numeric Implementation)
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. NumericSearchMetadata: Map<Id, NumericTokenSortedList> (default SearchMetadata)
181
- 5. StringSearchMetadata: Map<Id, string[]> (legacy, string-based)
182
- 6. LinkedSearchMetadata: { primaryMeta, linkedMeta, primaryToLinked: Map<Id, Id[]> }
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): NumericTokenSortedList | undefined
252
+ 3. prepareObjectForSearch(obj, spec?, tokensMap): { sorted, firstN } | undefined
256
253
  1. returns undefined if _deleted or _blocked
257
- 2. if custom extractSearchableStringFn provided, uses simple processing
258
- 3. otherwise processes fields individually to support per-field match modes
259
- 4. recursively extracts strings from nested objects/arrays
260
- 4. searchInData / searchInDataReturnIds(query: string, metadata): Id[]
261
- 5. searchInDataReturnObjects<T>(query, metadata, data): Map<Id, T>
262
- 6. searchInDataWithLimit(query, metadata, limit): Id[]
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. implements NumericSearchMetadata interface (get, has, size, keys, values, entries, forEach)
290
- 7. Map mutation methods (set, delete, clear) throw errors - use update methods instead
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,6 +320,40 @@ 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
 
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>[]
329
+ 1. searchAndRank = searchAndRankNumeric (alias for the numeric implementation)
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
334
+ 3. options: { primaryTextFn, exactKeysFn?, limit?, opts? }
335
+ 4. results sorted by score desc, primaryTextFn(item) localeCompare asc as tiebreaker
336
+ 15. orderedTokenize(input: string): string[]
337
+ 1. prepareStringForSearch pipeline minus the trailing .sort()
338
+ 2. required for any positional / order-aware scoring
339
+ 3. do NOT use these tokens for candidate retrieval (sorted lists required)
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)
356
+
318
357
  ## Token Registry Functions
319
358
 
320
359
  1. tokens: string[] - global array (read-only)
@@ -327,74 +366,36 @@ Override match behavior per token at search time. Priority: query prefix > field
327
366
  8. clearTokensMap(tokensMap): void
328
367
  9. resetTokenRegistry(): void (for testing only)
329
368
 
330
- ## String Implementation (Legacy)
331
-
332
- The string-based implementation is available as a separate entry point for backwards compatibility.
333
-
334
- **Import from `cry-search/string`:**
335
- ```typescript
336
- import {
337
- createSearchArrayMetadataString,
338
- updateSearchMetadataString,
339
- findInArray,
340
- // ... other string-based functions
341
- } from 'cry-search/string';
342
- ```
343
-
344
- **Available functions with `*String` suffix:**
345
- - createSearchArrayMetadataString
346
- - createSearchLinkedMetadataString
347
- - updateSearchMetadataString
348
- - updateSearchMetadataBatchString
349
- - syncSearchArrayMetadataString
350
- - syncSearchArrayMetadataStringWithPrevious
351
- - upsertPrimaryItemString
352
- - removePrimaryItemString
353
- - upsertLinkedItemString
354
- - removeLinkedItemString
355
- - syncLinkedItemsForPrimaryString
356
- - findInArray (uses string metadata)
357
- - findInArrayReturnDataAndMeta
358
- - findInLinkedArrays
359
-
360
- **Bundle sizes:**
361
- - Main bundle (`cry-search`): ~49 KB - includes only numeric implementation
362
- - String bundle (`cry-search/string`): ~26 KB - includes only string implementation
363
-
364
369
  ## File Structure
365
370
 
366
371
  ```
367
372
  src/
368
373
  ├── common/
369
- │ ├── numeric/ # Default numeric implementation
374
+ │ ├── numeric/ # Numeric implementation (only impl)
370
375
  │ │ ├── index.ts
371
376
  │ │ ├── createSearchArrayMetadata.ts
372
377
  │ │ ├── createSearchLinkedMetadata.ts
373
378
  │ │ ├── updateSearchMetadata.ts
374
379
  │ │ ├── updateSearchLinkedMetadata.ts
375
- │ │ ├── searchInData.ts
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
376
384
  │ │ ├── syncSearchArrayMetadata.ts
377
385
  │ │ └── AugmentedMetadata.ts
378
- │ ├── string/ # Legacy string implementation
379
- │ │ ├── index.ts
380
- │ │ ├── createSearchArrayMetadata.ts
381
- │ │ ├── createSearchLinkedMetadata.ts
382
- │ │ └── updateSearchMetadata.ts
383
- │ ├── findInArray.ts # String-based search
384
- │ ├── findInLinkedArrays.ts # String-based linked search
385
- │ ├── syncSearchArrayMetadata.ts
386
- │ ├── updateSearchLinkedMetadata.ts
387
- │ └── SearchUniverse.ts # Multi-collection manager (uses numeric)
386
+ │ └── SearchUniverse.ts # Multi-collection manager
388
387
  ├── utils/
389
- │ ├── tokenRegistry.ts # Global tokens array
390
- │ ├── prepareStringForSearch.ts
391
- │ ├── prepareStringForSearchNumeric.ts
392
- │ ├── matchToken.ts # String-based matching
393
- │ ├── matchTokenNumeric.ts # Numeric matching
394
- │ └── ...
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
395
396
  ├── types/
396
397
  │ └── index.ts
397
- └── index.ts # Main exports (numeric as default)
398
+ └── index.ts # Public API
398
399
  ```
399
400
 
400
401
  ## Coding rules
package/README.md CHANGED
@@ -5,12 +5,13 @@ 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 uses ~45% less memory than string-based approaches
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
12
12
  - **Search prefixes** - use `-word`, `--word`, or `~word` to exclude, `..word` for suffix, `=word` for exact match
13
13
  - **Automatic normalization** - Handles diacritics, case, dates, and number formatting
14
+ - **Ranked search** - `searchAndRank` returns results scored by positional relevance (token order, contiguity, perfect-prefix, length)
14
15
  - **[Universal search](./UNIVERSE.md)** - Search and update across linked collections (e.g., customers with their pets)
15
16
 
16
17
  ## Table of Contents
@@ -24,13 +25,13 @@ A fast, memory-efficient search library for large datasets with support for toke
24
25
  - [SearchUniverse with Linked Collections](#searchuniverse-with-linked-collections)
25
26
  - [Updating Existing Data](#updating-existing-data)
26
27
  - [Augmented Metadata - Local Search Context](#augmented-metadata---local-search-context)
28
+ - [Ranked Search with Positional Scoring](#ranked-search-with-positional-scoring)
27
29
  - [Architecture](#architecture)
28
30
  - [Text Processing Pipeline](#text-processing-pipeline)
29
31
  - [Metadata Building](#metadata-building)
30
32
  - [Searching](#searching)
31
33
  - [Memory Optimization](#memory-optimization)
32
34
  - [Query Prefixes](#query-prefixes)
33
- - [Legacy String Implementation](#legacy-string-implementation)
34
35
  - [Specification](#specification)
35
36
  - [License](#license)
36
37
 
@@ -278,6 +279,95 @@ augmented.clearAllAugmented(); // Reset to global state
278
279
  - Documents with review status or approval state
279
280
  - Items with computed scores or temporary categories
280
281
 
282
+ ### Ranked Search with Positional Scoring
283
+
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.
285
+
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
289
+
290
+ ```typescript
291
+ import {
292
+ arrayToSearchableData,
293
+ createSearchArrayMetadata,
294
+ searchAndRank,
295
+ type SearchableObject,
296
+ } from "cry-search";
297
+
298
+ interface Product extends SearchableObject {
299
+ _id: string;
300
+ name: string;
301
+ barcodes?: string[];
302
+ }
303
+
304
+ const products: Product[] = [
305
+ { _id: "1", name: "POTROŠNI MATERIAL" },
306
+ { _id: "2", name: "POTROŠNI MATERIAL-SUCRALAN 1ML" },
307
+ { _id: "3", name: "KONTROLNI PREGLED" },
308
+ { _id: "4", name: "DERMATOLOŠKI KONTROLNI PREGLED" },
309
+ ];
310
+
311
+ const data = arrayToSearchableData(products);
312
+ // Declare keyFields so positional ranking has pre-stored ordered tokens (faster).
313
+ const meta = createSearchArrayMetadata(data, {
314
+ keyFields: ['name'],
315
+ firstTokensN: 8,
316
+ });
317
+
318
+ // Returns items sorted by relevance, NOT discovery order:
319
+ const results = searchAndRank("pot mat", data, meta, {
320
+ primaryTextFn: (p) => p.name,
321
+ exactKeysFn: (p) => p.barcodes, // optional: exact-key boost (e.g. barcode)
322
+ limit: 10,
323
+ });
324
+
325
+ // results[0].item.name === "POTROŠNI MATERIAL" (perfect prefix → +80)
326
+ // results[1].item.name === "POTROŠNI MATERIAL-SUCRALAN 1ML" (longer naziv)
327
+
328
+ // Each result: { id: Id, item: T, score: number }
329
+ ```
330
+
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):
334
+
335
+ | Component | Value | When |
336
+ |---|---|---|
337
+ | Exact-key match | **+100 000** | `query === any(exactKeys)` (e.g. barcode equality) |
338
+ | Base | +50 | Any in-order subsequence-prefix match of query tokens in itemTokens |
339
+ | All tokens matched | +100 | Every query token had a target |
340
+ | First-word match | +50 | First match at position 0 |
341
+ | Perfect prefix | +80 | Matches at positions `[0,1,…,k-1]` (contiguous from start) |
342
+ | Density | +30 × (matched / span) | Rewards contiguous matches over gapped |
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';
357
+
358
+ // Combine signals per field in a custom ranker:
359
+ const qTokens = orderedTokenize(query);
360
+
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]!) : [];
364
+
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
+ ```
370
+
281
371
  ## Architecture
282
372
 
283
373
  cry-search uses a two-phase approach: **build** metadata once, then **search** instantly.
@@ -307,7 +397,7 @@ Query tokens are matched against metadata using binary search. All query tokens
307
397
 
308
398
  ### Memory Optimization
309
399
 
310
- The numeric implementation stores tokens as `Uint32Array` indices instead of string arrays, reducing memory usage by ~45% compared to the string-based implementation.
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.
311
401
 
312
402
  ## Query Prefixes
313
403
 
@@ -383,20 +473,6 @@ This enables matching by either part:
383
473
 
384
474
  Priority: query prefix > field match mode > global SearchOpts default.
385
475
 
386
- ## Legacy String Implementation
387
-
388
- A string-based implementation is available for backwards compatibility. Import from the `/string` subpath:
389
-
390
- ```typescript
391
- import {
392
- createSearchArrayMetadataString,
393
- updateSearchMetadataString,
394
- findInArray, // String-based search function
395
- } from 'cry-search/string';
396
- ```
397
-
398
- The string implementation is excluded from the main bundle. The default import (`cry-search`) only includes the modern numeric implementation, keeping bundle sizes smaller.
399
-
400
476
  ## Specification
401
477
 
402
478
  See [CLAUDE.md](./CLAUDE.md) for the complete technical specification including:
package/UNIVERSE.md CHANGED
@@ -627,8 +627,10 @@ SearchUniverse provides methods to update data and metadata after initial load.
627
627
  │ universe.syncCollection(collectionName, newItems, options?) │
628
628
  ├─────────────────────────────────────────────────────────────────────────────┤
629
629
  │ │
630
- │ Diffs a fresh full snapshot against the current collection state. │
631
- │ Use for periodic refresh cycles when you don't have a delta. │
630
+ │ ⚠️ newItems MUST be the FULL snapshot, not a partial delta. │
631
+ │ Anything currently in the collection whose _id is missing from │
632
+ │ newItems will be REMOVED (data + metadata + linked index). │
633
+ │ Passing a partial list will wipe out everything else. │
632
634
  │ │
633
635
  │ newItems = [ │
634
636
  │ { _id: "s1", name: "Krajnik", address: "Ljubljana" }, ← unchanged │
@@ -657,6 +659,20 @@ SearchUniverse provides methods to update data and metadata after initial load.
657
659
  └─────────────────────────────────────────────────────────────────────────────┘
658
660
  ```
659
661
 
662
+ #### When to use which update method
663
+
664
+ | Scenario | Method |
665
+ |---------------------------------------------------|--------------------------------------------------------------|
666
+ | Full snapshot from server (periodic refresh) | `syncCollection(name, allItems)` |
667
+ | Known delta — add/update some items | `updateSearchMetadataBatch(name, changedItems)` |
668
+ | Known IDs to remove | `removeFromSearchMetadataBatch(name, ids)` |
669
+ | Mark item as soft-deleted but keep in data | `updateSearchMetadata(name, id, { ...item, _deleted: ... })` |
670
+ | Initial load / full reset | `loadCollection(name, items)` |
671
+
672
+ **Rule of thumb:** if you don't have the complete current state of the
673
+ collection, do NOT use `syncCollection` — use the batch methods instead.
674
+ They only touch the items you pass in and leave the rest alone.
675
+
660
676
  ### Clear Universe
661
677
 
662
678
  ```
@@ -778,36 +794,29 @@ For direct metadata manipulation without SearchUniverse:
778
794
  │ │
779
795
  │ ────────────────────────────────────────────────────────────────────── │
780
796
  │ │
781
- │ STRING (Legacy) - import with *String suffix: │
782
- │ │
783
- │ createSearchArrayMetadataString(data, spec) │
784
- │ updateSearchMetadataString(metadata, id, obj, spec) │
785
- │ updateSearchMetadataBatchString(metadata, batch, spec) │
786
- │ removeFromSearchMetadataString(metadata, id) │
787
- │ findInArray(query, data, metadata) │
788
- │ │
789
- │ ────────────────────────────────────────────────────────────────────── │
790
- │ │
791
- │ SYNC (String-based): │
797
+ │ SYNC: │
792
798
  │ │
793
- │ syncSearchArrayMetadata(data, metadata, spec) │
794
- │ Syncs metadata with current data state │
799
+ │ syncSearchArrayMetadata(metadata, newData, spec) │
800
+ │ Syncs metadata with new full data snapshot │
795
801
  │ Returns: { added, updated, removed, addedIds, updatedIds, removedIds } │
796
802
  │ │
803
+ │ syncSearchArrayMetadataWithPrevious(metadata, prev, newData, opts?, spec) │
804
+ │ Same but uses previous data to skip metadata.keys() iteration │
805
+ │ │
797
806
  │ ────────────────────────────────────────────────────────────────────── │
798
807
  │ │
799
- │ For linked metadata (String-based): │
808
+ │ Linked metadata (incremental): │
800
809
  │ │
801
- │ upsertPrimaryItem(linked, id, item, spec) │
810
+ │ upsertPrimaryItem(linked, item, spec) │
802
811
  │ Adds/updates primary item in linked structure │
803
812
  │ │
804
- │ upsertLinkedItem(linked, id, item, fkGetter, spec) │
813
+ │ upsertLinkedItem(linked, item, fkGetter, spec) │
805
814
  │ Adds/updates linked item, handles FK changes │
806
815
  │ │
807
- │ removePrimaryItem(linked, id) │
816
+ │ removePrimaryItem(linked, primaryId) │
808
817
  │ Removes primary item │
809
818
  │ │
810
- │ removeLinkedItem(linked, id, primaryId) │
819
+ │ removeLinkedItem(linked, linkedId, fkGetter) │
811
820
  │ Removes linked item from structure and index │
812
821
  │ │
813
822
  │ syncLinkedItemsForPrimary(linked, primaryId, items, fkGetter, spec) │
@@ -155,30 +155,45 @@ export declare class SearchUniverse {
155
155
  */
156
156
  removeFromSearchMetadataBatch(collectionName: string, ids: Id[]): number;
157
157
  /**
158
- * Syncs a collection against a fresh snapshot of items.
159
- *
160
- * Compares the provided `newItems` to the current collection state and applies
161
- * the diff: items not in `newItems` are removed, new items are added, and existing
162
- * items are updated when their tokens change. Items marked `_deleted` or `_blocked`
163
- * are removed from metadata (and from `primaryToLinked` for linked collections),
164
- * but remain in `collection.data` for reference — same convention as
165
- * {@link updateSearchMetadata}.
166
- *
167
- * Use this for full refresh cycles (e.g. periodic snapshot from server) when you
168
- * don't have a delta. For incremental delta updates use
169
- * {@link updateSearchMetadataBatch} + {@link removeFromSearchMetadataBatch}.
158
+ * Syncs a collection against a **full snapshot** of items.
159
+ *
160
+ * **IMPORTANT:** `newItems` MUST contain the entire desired state of the
161
+ * collection, not a partial delta. Any item currently in the collection
162
+ * whose `_id` is missing from `newItems` will be **completely removed**
163
+ * (data + metadata + linked indexes). Passing a partial list will wipe
164
+ * out everything else.
165
+ *
166
+ * Compares `newItems` to the current collection state and applies the diff:
167
+ * - items absent from `newItems` → removed
168
+ * - new IDs in `newItems` → added
169
+ * - existing IDs with changed tokens → updated
170
+ * - items marked `_deleted` or `_blocked` → removed from metadata
171
+ * (kept in `collection.data` for reference — same convention as
172
+ * {@link updateSearchMetadata})
173
+ *
174
+ * **When to use which method:**
175
+ * - Full snapshot from server (periodic refresh): `syncCollection`
176
+ * - Known delta of changes: {@link updateSearchMetadataBatch}
177
+ * - Known IDs to delete: {@link removeFromSearchMetadataBatch}
178
+ *
179
+ * For partial delta updates, prefer the batch methods — they leave untouched
180
+ * items unaffected.
170
181
  *
171
182
  * @typeParam T - Type of items in the collection
172
183
  * @param collectionName - Name of the collection
173
- * @param newItems - Full new dataset (snapshot)
184
+ * @param newItems - **Full** new dataset (complete snapshot, not a delta)
174
185
  * @param options - Optional sync options (e.g. `skipUpdateCheck` to avoid re-tokenizing existing items)
175
186
  * @returns SyncResult with counts and IDs of added/updated/removed items
176
187
  *
177
188
  * @example
178
189
  * ```typescript
190
+ * // Full refresh from server — pass ALL current products
179
191
  * const fresh = await fetchAllProducts();
180
192
  * const result = universe.syncCollection('products', fresh);
181
193
  * // { added: 3, updated: 12, removed: 1, addedIds: [...], ... }
194
+ *
195
+ * // ⚠️ DO NOT do this — would delete all other products:
196
+ * // universe.syncCollection('products', [oneChangedProduct]);
182
197
  * ```
183
198
  */
184
199
  syncCollection<T extends SearchableObject>(collectionName: string, newItems: T[], options?: SyncOptions): SyncResult;
@@ -1 +1 @@
1
- {"version":3,"file":"SearchUniverse.d.ts","sourceRoot":"","sources":["../../src/common/SearchUniverse.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SearchUniverse.d.ts","sourceRoot":"","sources":["../../src/common/SearchUniverse.ts"],"names":[],"mappings":"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"}