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.
- package/CLAUDE.md +79 -78
- package/README.md +93 -17
- package/UNIVERSE.md +29 -20
- package/dist/common/SearchUniverse.d.ts +28 -13
- 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 +48 -0
- package/dist/common/numeric/searchAndRank.d.ts.map +1 -0
- 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 +646 -312
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +646 -312
- 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,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/
|
|
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
|
-
│
|
|
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
|
|
390
|
-
│ ├── prepareStringForSearch.ts
|
|
391
|
-
│ ├── prepareStringForSearchNumeric.ts
|
|
392
|
-
│ ├──
|
|
393
|
-
│ ├──
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
│
|
|
631
|
-
│
|
|
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
|
-
│
|
|
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(
|
|
794
|
-
│ Syncs metadata with
|
|
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
|
-
│
|
|
808
|
+
│ Linked metadata (incremental): │
|
|
800
809
|
│ │
|
|
801
|
-
│ upsertPrimaryItem(linked,
|
|
810
|
+
│ upsertPrimaryItem(linked, item, spec) │
|
|
802
811
|
│ Adds/updates primary item in linked structure │
|
|
803
812
|
│ │
|
|
804
|
-
│ upsertLinkedItem(linked,
|
|
813
|
+
│ upsertLinkedItem(linked, item, fkGetter, spec) │
|
|
805
814
|
│ Adds/updates linked item, handles FK changes │
|
|
806
815
|
│ │
|
|
807
|
-
│ removePrimaryItem(linked,
|
|
816
|
+
│ removePrimaryItem(linked, primaryId) │
|
|
808
817
|
│ Removes primary item │
|
|
809
818
|
│ │
|
|
810
|
-
│ removeLinkedItem(linked,
|
|
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
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
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":"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":"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;AAYlB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAiCvG;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,WAAW,CAAiC;IAEpD;;;;;OAKG;IACH,kBAAkB,CAAC,CAAC,SAAS,gBAAgB,EAAE,CAAC,GAAG,OAAO,EACxD,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAM,GAClC,IAAI;IAmBP;;;;;OAKG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIpC;;;;OAIG;IACH,kBAAkB,IAAI,MAAM,EAAE;IAI9B;;;;;;OAMG;IACH,cAAc,CAAC,CAAC,SAAS,gBAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI;IA0C1E;;;;;;;OAOG;IACH,OAAO,CAAC,CAAC,SAAS,gBAAgB,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,GAAG,SAAS;IAKlF;;;;;;OAMG;IACH,QAAQ,CAAC,CAAC,SAAS,gBAAgB,EAAE,cAAc,EAAE,MAAM,GAAG,CAAC,EAAE;IAKjE;;;;;OAKG;IACH,iBAAiB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM;IAKjD;;;;;OAKG;IACH,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,qBAAqB;IAK1D;;;;;;OAMG;IACH,OAAO,CAAC,CAAC,SAAS,gBAAgB,EAAE,cAAc,EAAE,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC;IAK9E;;;;;;;OAOG;IACH,YAAY,CAAC,oBAAoB,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,EAAE;IAQ/D;;;;;;;OAOG;IACH,cAAc,CAAC,CAAC,SAAS,gBAAgB,EACvC,oBAAoB,EAAE,MAAM,EAC5B,SAAS,EAAE,EAAE,GACZ,CAAC,EAAE;IAMN;;;;;;;;;;;;OAYG;IACH,oBAAoB,CAAC,CAAC,SAAS,gBAAgB,EAC7C,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,EAAE,EACN,IAAI,EAAE,CAAC,GACN,YAAY;IA0Ff;;;;;;OAMG;IACH,yBAAyB,CAAC,CAAC,SAAS,gBAAgB,EAClD,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,CAAC,EAAE,GACT,iBAAiB;IA0BpB;;;;;;OAMG;IACH,wBAAwB,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO;IAoCjE;;;;;;OAMG;IACH,6BAA6B,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,MAAM;IAcxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyCG;IACH,cAAc,CAAC,CAAC,SAAS,gBAAgB,EACvC,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,CAAC,EAAE,EACb,OAAO,CAAC,EAAE,WAAW,GACpB,UAAU;IA4Ib;;;;;;;OAOG;IACH,OAAO,CAAC,oBAAoB;IAQ5B;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH,iBAAiB,CACf,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,MAAW,EAClB,IAAI,CAAC,EAAE,mBAAmB,GACzB,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,KAAK,CAAC;QAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QAAC,OAAO,EAAE,gBAAgB,CAAC;QAAC,SAAS,EAAE,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAA;KAAE,CAAC,CAAC;IA6FlJ;;;;;;;;OAQG;IACH,KAAK,IAAI,IAAI;CAWd"}
|