cry-search 1.0.6 → 1.0.7

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 CHANGED
@@ -315,6 +315,31 @@ Override match behavior per token at search time. Priority: query prefix > field
315
315
  5. only processes items whose foreign key matches primaryId
316
316
  6. returns { added: number, updated: number, removed: number }
317
317
 
318
+ 13. searchAndRank(query, data, metadata, options): ScoredResult<T>[]
319
+ 1. searchAndRank = searchAndRankNumeric (alias for the numeric implementation)
320
+ 2. wraps searchInDataReturnIds + positional scoring over the candidate set
321
+ 3. options: { primaryTextFn, exactKeysFn?, limit?, opts? }
322
+ - primaryTextFn: (item) => string (e.g. item.naziv) — used for positional scoring
323
+ - exactKeysFn: (item) => readonly string[] | undefined (e.g. crtnaKoda)
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[]
336
+ 1. prepareStringForSearch pipeline minus the trailing .sort()
337
+ 2. required for any positional / order-aware scoring
338
+ 3. do NOT use these tokens for candidate retrieval (sorted lists required)
339
+ 15. scoreOne(item, query, qTokens, primaryTextFn, exactKeys?): number
340
+ 1. single-item scoring primitive — use to compose custom rankers
341
+ 2. callers must pre-tokenize the query with orderedTokenize for correct positional semantics
342
+
318
343
  ## Token Registry Functions
319
344
 
320
345
  1. tokens: string[] - global array (read-only)
@@ -373,6 +398,7 @@ src/
373
398
  │ │ ├── updateSearchMetadata.ts
374
399
  │ │ ├── updateSearchLinkedMetadata.ts
375
400
  │ │ ├── searchInData.ts
401
+ │ │ ├── searchAndRank.ts # Ranked search with positional scoring
376
402
  │ │ ├── syncSearchArrayMetadata.ts
377
403
  │ │ └── AugmentedMetadata.ts
378
404
  │ ├── string/ # Legacy string implementation
package/README.md CHANGED
@@ -11,6 +11,7 @@ A fast, memory-efficient search library for large datasets with support for toke
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,6 +25,7 @@ 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)
@@ -278,6 +280,69 @@ augmented.clearAllAugmented(); // Reset to global state
278
280
  - Documents with review status or approval state
279
281
  - Items with computed scores or temporary categories
280
282
 
283
+ ### Ranked Search with Positional Scoring
284
+
285
+ `searchInDataReturnIds` returns matching candidates but no ordering signal — useful when the caller needs raw IDs (e.g., for further filtering) but not when the UI needs the *most relevant* match first.
286
+
287
+ `searchAndRank` wraps candidate retrieval with positional relevance scoring. Each item gets a score that rewards in-order token matches, first-word matches, perfect-prefix matches, contiguous matches, and shorter primary text.
288
+
289
+ ```typescript
290
+ import {
291
+ arrayToSearchableData,
292
+ createSearchArrayMetadata,
293
+ searchAndRank,
294
+ type SearchableObject,
295
+ } from "cry-search";
296
+
297
+ interface Product extends SearchableObject {
298
+ _id: string;
299
+ name: string;
300
+ barcodes?: string[];
301
+ }
302
+
303
+ const products: Product[] = [
304
+ { _id: "1", name: "POTROŠNI MATERIAL" },
305
+ { _id: "2", name: "POTROŠNI MATERIAL-SUCRALAN 1ML" },
306
+ { _id: "3", name: "KONTROLNI PREGLED" },
307
+ { _id: "4", name: "DERMATOLOŠKI KONTROLNI PREGLED" },
308
+ ];
309
+
310
+ const data = arrayToSearchableData(products);
311
+ const meta = createSearchArrayMetadata(data);
312
+
313
+ // Returns items sorted by relevance, NOT discovery order:
314
+ const results = searchAndRank("pot mat", data, meta, {
315
+ primaryTextFn: (p) => p.name,
316
+ exactKeysFn: (p) => p.barcodes, // optional: exact-key boost (e.g. barcode)
317
+ limit: 10,
318
+ });
319
+
320
+ // results[0].item.name === "POTROŠNI MATERIAL" (perfect prefix → +80)
321
+ // results[1].item.name === "POTROŠNI MATERIAL-SUCRALAN 1ML" (longer naziv)
322
+
323
+ // Each result: { id: Id, item: T, score: number }
324
+ ```
325
+
326
+ **Scoring formula** (`scoreOne` is exported as a primitive — useful if you want to compose your own scoring):
327
+
328
+ | Component | Value | When |
329
+ |---|---|---|
330
+ | 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 `primaryText` |
332
+ | All tokens matched | +100 | Every query token had a target |
333
+ | First-word match | +50 | First match at position 0 |
334
+ | Perfect prefix | +80 | Matches at positions `[0,1,…,k-1]` (contiguous from start) |
335
+ | Density | +30 × (matched / span) | Rewards contiguous matches over gapped |
336
+ | Length penalty | -0.1 × `primaryText.length` | Weak tiebreaker — shorter wins |
337
+ | Secondary-field-only | 1 | Item matched (via search) but `primaryText` contains no query token |
338
+
339
+ **Lower-level primitives** are also exported, for building custom rankers:
340
+
341
+ - `orderedTokenize(input: string): string[]` — tokenize *without* alphabetic sort. `prepareStringForSearch` sorts tokens alphabetically (required for binary-search retrieval), which loses positional info. Use this for any order-aware scoring.
342
+ - `scoreOne(item, query, qTokens, primaryTextFn, exactKeys?): number` — score one item against a pre-tokenized query. Compose with your own retrieval or filtering.
343
+
344
+ **Important**: `orderedTokenize` output is NOT compatible with `searchInData...` retrieval functions — those require the sorted lists from `prepareStringForSearch`.
345
+
281
346
  ## Architecture
282
347
 
283
348
  cry-search uses a two-phase approach: **build** metadata once, then **search** instantly.
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
  ```
@@ -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":"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;AAWlB,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AA0BvG;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;IAsC1E;;;;;;;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;IAoFf;;;;;;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;IAmCjE;;;;;;OAMG;IACH,6BAA6B,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,MAAM;IAcxE;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,cAAc,CAAC,CAAC,SAAS,gBAAgB,EACvC,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,CAAC,EAAE,EACb,OAAO,CAAC,EAAE,WAAW,GACpB,UAAU;IA8Hb;;;;;;;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;CAUd"}
1
+ {"version":3,"file":"SearchUniverse.d.ts","sourceRoot":"","sources":["../../src/common/SearchUniverse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import type { Id, NumericSearchMetadata, SearchableData, SearchableObject, SearchOpts } from "../../types";
2
+ /**
3
+ * Tokenizes a string preserving source order (no alphabetic sort).
4
+ *
5
+ * Mirrors {@link prepareStringForSearch} pipeline minus the trailing `.sort()`,
6
+ * which is required for any positional / order-aware scoring.
7
+ *
8
+ * NOTE: do NOT use these tokens for cry-search candidate retrieval; those APIs
9
+ * assume sorted lists for efficient binary search.
10
+ */
11
+ export declare function orderedTokenize(input: string): string[];
12
+ /**
13
+ * Result of a scored / ranked search.
14
+ */
15
+ export interface ScoredResult<T extends SearchableObject> {
16
+ /** Item id. */
17
+ id: Id;
18
+ /** The matched item. */
19
+ item: T;
20
+ /** Relevance score (higher = better). 100_000 for exact-key match. */
21
+ score: number;
22
+ }
23
+ /**
24
+ * Options controlling {@link searchAndRank} scoring and retrieval.
25
+ */
26
+ export interface SearchAndRankOptions<T extends SearchableObject> {
27
+ /**
28
+ * Extracts the primary display text used for positional scoring.
29
+ * Typically the human-readable label (`item => item.naziv`).
30
+ */
31
+ primaryTextFn: (item: T) => string | undefined;
32
+ /**
33
+ * Optional: extracts values that must match the raw query string exactly
34
+ * for a top-priority boost (e.g. crtnaKoda for invoice items).
35
+ * Returns score 100_000 on exact equality.
36
+ */
37
+ exactKeysFn?: (item: T) => readonly string[] | undefined;
38
+ /** Maximum number of ranked results to return. Default: Infinity. */
39
+ limit?: number;
40
+ /** Underlying search options passed to candidate retrieval. */
41
+ opts?: SearchOpts;
42
+ }
43
+ /**
44
+ * Computes a relevance score for one item against a query.
45
+ *
46
+ * Scoring components (additive):
47
+ * - **+100_000** if the raw `query` equals any `exactKeys` value (e.g. crtnaKoda).
48
+ * - **+50** base for any subsequence-prefix match of query tokens in primary text.
49
+ * - **+100** if ALL query tokens match (in order) in primary text.
50
+ * - **+50** if first matched position is 0 (primary text starts with first query token).
51
+ * - **+80** if matched positions form a contiguous prefix `[0..k-1]` (perfect prefix).
52
+ * - **+30 * (matchedCount / span)** density bonus (rewards contiguous matches).
53
+ * - **-0.1 * primaryText.length** length penalty (weak tiebreaker — shorter wins).
54
+ *
55
+ * Returns **1** if the item was retrieved as a candidate but its primary text
56
+ * contains no query token (match came from a secondary field).
57
+ *
58
+ * Returns **0** if the query is empty.
59
+ */
60
+ export declare function scoreOne<T extends SearchableObject>(item: T, query: string, qTokens: string[], primaryTextFn: (item: T) => string | undefined, exactKeys?: readonly string[] | undefined): number;
61
+ /**
62
+ * Searches with positional relevance scoring and returns ranked results.
63
+ *
64
+ * Combines candidate retrieval ({@link searchInDataReturnIdsNumeric}, sorted-token
65
+ * AND-match) with a positional scoring layer that rewards in-order matches,
66
+ * first-word matches, perfect prefixes, dense (contiguous) matches, and shorter
67
+ * primary text.
68
+ *
69
+ * Results are sorted by score descending, with a stable tiebreaker on
70
+ * locale-aware primary-text comparison.
71
+ *
72
+ * @typeParam T - Item type (must extend SearchableObject)
73
+ * @param query - Raw query string (tokenized internally)
74
+ * @param data - SearchableData built via {@link arrayToSearchableData}
75
+ * @param metadata - NumericSearchMetadata built via {@link createSearchArrayMetadata}
76
+ * @param options - Scoring + retrieval options
77
+ * @returns Array of `{ id, item, score }` ordered by score descending
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const results = searchAndRank("pot mat", data, meta, {
82
+ * primaryTextFn: (a) => a.naziv,
83
+ * exactKeysFn: (a) => a.crtneKode,
84
+ * limit: 20,
85
+ * });
86
+ * // results[0].item.naziv === "POTROŠNI MATERIAL"
87
+ * ```
88
+ */
89
+ export declare function searchAndRankNumeric<T extends SearchableObject>(query: string, data: SearchableData<T>, metadata: NumericSearchMetadata, options: SearchAndRankOptions<T>): ScoredResult<T>[];
90
+ //# sourceMappingURL=searchAndRank.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"searchAndRank.d.ts","sourceRoot":"","sources":["../../../src/common/numeric/searchAndRank.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,EAAE,EACF,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,UAAU,EACX,MAAM,aAAa,CAAC;AAOrB;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAKvD;AACD;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,gBAAgB;IACtD,eAAe;IACf,EAAE,EAAE,EAAE,CAAC;IACP,wBAAwB;IACxB,IAAI,EAAE,CAAC,CAAC;IACR,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC,SAAS,gBAAgB;IAC9D;;;OAGG;IACH,aAAa,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;IAC/C;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IACzD,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AACD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,gBAAgB,EACjD,IAAI,EAAE,CAAC,EACP,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,aAAa,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,SAAS,EAC9C,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GACxC,MAAM,CAyCR;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,gBAAgB,EAC7D,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,EACvB,QAAQ,EAAE,qBAAqB,EAC/B,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC/B,YAAY,CAAC,CAAC,CAAC,EAAE,CA0BnB"}
package/dist/index.cjs CHANGED
@@ -43,6 +43,9 @@ __export(exports_src, {
43
43
  searchInDataReturnObjects: () => searchInDataReturnObjectsNumeric,
44
44
  searchInDataReturnIds: () => searchInDataReturnIdsNumeric,
45
45
  searchInData: () => searchInDataReturnIdsNumeric,
46
+ searchAndRankNumeric: () => searchAndRankNumeric,
47
+ searchAndRank: () => searchAndRankNumeric,
48
+ scoreOne: () => scoreOne,
46
49
  sanitiseString: () => sanitiseString,
47
50
  resetTokenRegistry: () => resetTokenRegistry,
48
51
  removePrimaryItem: () => removePrimaryItemNumeric,
@@ -54,6 +57,7 @@ __export(exports_src, {
54
57
  prepareStringForSearch: () => prepareStringForSearch,
55
58
  prepareQueryTokens: () => prepareQueryTokens,
56
59
  prepareObjectForSearch: () => prepareObjectForSearchNumeric,
60
+ orderedTokenize: () => orderedTokenize,
57
61
  normalizeDates: () => normalizeDates,
58
62
  matchTokenInSortedListNumeric: () => matchTokenInSortedListNumeric,
59
63
  matchPreparedTokenInSortedList: () => matchPreparedTokenInSortedList,
@@ -1331,6 +1335,82 @@ function syncLinkedItemsForPrimaryNumeric(linked, primaryId, newLinkedItems, for
1331
1335
  clearTokensMap(tokensMap);
1332
1336
  return result;
1333
1337
  }
1338
+ // src/common/numeric/searchAndRank.ts
1339
+ function orderedTokenize(input) {
1340
+ return tokenize(sanitiseString(preprocessString(normalizeDates(input))), DEFAULT_TOKENIZE_CONFIG);
1341
+ }
1342
+ function scoreOne(item, query, qTokens, primaryTextFn, exactKeys) {
1343
+ if (exactKeys && exactKeys.length > 0) {
1344
+ for (let i = 0;i < exactKeys.length; i++) {
1345
+ if (exactKeys[i] === query)
1346
+ return 1e5;
1347
+ }
1348
+ }
1349
+ if (qTokens.length === 0)
1350
+ return 0;
1351
+ const primaryText = primaryTextFn(item) || "";
1352
+ const itemTokens = orderedTokenize(primaryText);
1353
+ if (itemTokens.length === 0)
1354
+ return 1;
1355
+ let qi = 0;
1356
+ const positions = [];
1357
+ for (let i = 0;i < itemTokens.length; i++) {
1358
+ if (qi < qTokens.length && itemTokens[i].startsWith(qTokens[qi])) {
1359
+ positions.push(i);
1360
+ qi++;
1361
+ }
1362
+ }
1363
+ if (qi === 0)
1364
+ return 1;
1365
+ let score = 50;
1366
+ if (qi === qTokens.length)
1367
+ score += 100;
1368
+ if (positions[0] === 0)
1369
+ score += 50;
1370
+ let perfect = qi === qTokens.length;
1371
+ if (perfect) {
1372
+ for (let i = 0;i < positions.length; i++) {
1373
+ if (positions[i] !== i) {
1374
+ perfect = false;
1375
+ break;
1376
+ }
1377
+ }
1378
+ }
1379
+ if (perfect)
1380
+ score += 80;
1381
+ if (positions.length >= 2) {
1382
+ const span = positions[positions.length - 1] - positions[0] + 1;
1383
+ score += positions.length / span * 30;
1384
+ }
1385
+ score -= primaryText.length * 0.1;
1386
+ return score;
1387
+ }
1388
+ function searchAndRankNumeric(query, data, metadata, options) {
1389
+ const { primaryTextFn, exactKeysFn, limit, opts } = options;
1390
+ const ids = searchInDataReturnIdsNumeric(query, metadata, opts);
1391
+ if (ids.length === 0)
1392
+ return [];
1393
+ const qTokens = orderedTokenize(query);
1394
+ const scored = [];
1395
+ for (const id of ids) {
1396
+ const item = data.get(id);
1397
+ if (!item)
1398
+ continue;
1399
+ const exactKeys = exactKeysFn?.(item);
1400
+ const score = scoreOne(item, query, qTokens, primaryTextFn, exactKeys);
1401
+ scored.push({ id, item, score });
1402
+ }
1403
+ scored.sort((x, y) => {
1404
+ if (y.score !== x.score)
1405
+ return y.score - x.score;
1406
+ const xText = primaryTextFn(x.item) || "";
1407
+ const yText = primaryTextFn(y.item) || "";
1408
+ return xText.localeCompare(yText);
1409
+ });
1410
+ if (limit === undefined || limit === Infinity || limit >= scored.length)
1411
+ return scored;
1412
+ return scored.slice(0, limit);
1413
+ }
1334
1414
  // src/common/SearchUniverse.ts
1335
1415
  function tokensEqual3(a, b) {
1336
1416
  if (a.length !== b.length)
package/dist/index.d.cts CHANGED
@@ -22,5 +22,6 @@ export { syncSearchArrayMetadataNumeric as syncSearchArrayMetadata, syncSearchAr
22
22
  export { upsertPrimaryItemNumeric as upsertPrimaryItem, removePrimaryItemNumeric as removePrimaryItem, upsertLinkedItemNumeric as upsertLinkedItem, removeLinkedItemNumeric as removeLinkedItem, syncLinkedItemsForPrimaryNumeric as syncLinkedItemsForPrimary, } from './common/numeric/updateSearchLinkedMetadata';
23
23
  export { matchTokenInSortedListNumeric, allTokensMatchNumeric, binarySearchUint32, prepareQueryTokens, matchPreparedTokenInSortedList, allPreparedTokensMatch, type PreparedQueryToken, } from './utils/matchTokenNumeric';
24
24
  export { isDeletedOrBlocked } from './common/string/createSearchArrayMetadata';
25
+ export { searchAndRankNumeric as searchAndRank, searchAndRankNumeric, scoreOne, orderedTokenize, type ScoredResult, type SearchAndRankOptions, } from './common/numeric/searchAndRank';
25
26
  export { SearchUniverse } from './common/SearchUniverse';
26
27
  //# sourceMappingURL=index.d.ts.map
package/dist/index.d.ts CHANGED
@@ -22,5 +22,6 @@ export { syncSearchArrayMetadataNumeric as syncSearchArrayMetadata, syncSearchAr
22
22
  export { upsertPrimaryItemNumeric as upsertPrimaryItem, removePrimaryItemNumeric as removePrimaryItem, upsertLinkedItemNumeric as upsertLinkedItem, removeLinkedItemNumeric as removeLinkedItem, syncLinkedItemsForPrimaryNumeric as syncLinkedItemsForPrimary, } from './common/numeric/updateSearchLinkedMetadata';
23
23
  export { matchTokenInSortedListNumeric, allTokensMatchNumeric, binarySearchUint32, prepareQueryTokens, matchPreparedTokenInSortedList, allPreparedTokensMatch, type PreparedQueryToken, } from './utils/matchTokenNumeric';
24
24
  export { isDeletedOrBlocked } from './common/string/createSearchArrayMetadata';
25
+ export { searchAndRankNumeric as searchAndRank, searchAndRankNumeric, scoreOne, orderedTokenize, type ScoredResult, type SearchAndRankOptions, } from './common/numeric/searchAndRank';
25
26
  export { SearchUniverse } from './common/SearchUniverse';
26
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,YAAY,EAEV,EAAE,EACF,WAAW,EACX,qBAAqB,EACrB,cAAc,EAEd,YAAY,EACZ,sBAAsB,EACtB,qBAAqB,EAErB,gBAAgB,EAChB,SAAS,EACT,oBAAoB,EAEpB,UAAU,EAEV,oBAAoB,EACpB,4BAA4B,EAC5B,2BAA2B,EAC3B,kBAAkB,EAClB,0BAA0B,EAE1B,oBAAoB,EAEpB,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,6BAA6B,EAE7B,mBAAmB,EACnB,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EAEjB,cAAc,IAAI,oBAAoB,EACtC,mBAAmB,EACnB,cAAc,EACd,kBAAkB,GACnB,MAAM,SAAS,CAAC;AAGjB,YAAY,EAAE,qBAAqB,IAAI,cAAc,EAAE,MAAM,SAAS,CAAC;AAGvE,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAG9C,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAKvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAKxD,OAAO,EACL,MAAM,EACN,aAAa,EACb,YAAY,EACZ,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAO/B,OAAO,EACL,gCAAgC,IAAI,yBAAyB,EAC7D,6BAA6B,IAAI,sBAAsB,EACvD,mBAAmB,EACnB,aAAa,EACb,eAAe,EACf,aAAa,EACb,eAAe,GAChB,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EACL,iCAAiC,IAAI,0BAA0B,EAC/D,KAAK,mCAAmC,GACzC,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,2BAA2B,IAAI,oBAAoB,EACnD,gCAAgC,IAAI,yBAAyB,EAC7D,+BAA+B,IAAI,wBAAwB,EAC3D,oCAAoC,IAAI,6BAA6B,GACtE,MAAM,uCAAuC,CAAC;AAG/C,OAAO,EACL,4BAA4B,IAAI,YAAY,EAC5C,4BAA4B,IAAI,qBAAqB,EACrD,gCAAgC,IAAI,yBAAyB,EAC7D,qCAAqC,IAAI,qBAAqB,GAC/D,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EACL,yBAAyB,EACzB,+BAA+B,GAChC,MAAM,qCAAqC,CAAC;AAG7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAGvE,OAAO,EACL,8BAA8B,IAAI,uBAAuB,EACzD,0CAA0C,IAAI,mCAAmC,GAClF,MAAM,0CAA0C,CAAC;AAGlD,OAAO,EACL,wBAAwB,IAAI,iBAAiB,EAC7C,wBAAwB,IAAI,iBAAiB,EAC7C,uBAAuB,IAAI,gBAAgB,EAC3C,uBAAuB,IAAI,gBAAgB,EAC3C,gCAAgC,IAAI,yBAAyB,GAC9D,MAAM,6CAA6C,CAAC;AAGrD,OAAO,EACL,6BAA6B,EAC7B,qBAAqB,EACrB,kBAAkB,EAElB,kBAAkB,EAClB,8BAA8B,EAC9B,sBAAsB,EACtB,KAAK,kBAAkB,GACxB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAK/E,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1246,6 +1246,82 @@ function syncLinkedItemsForPrimaryNumeric(linked, primaryId, newLinkedItems, for
1246
1246
  clearTokensMap(tokensMap);
1247
1247
  return result;
1248
1248
  }
1249
+ // src/common/numeric/searchAndRank.ts
1250
+ function orderedTokenize(input) {
1251
+ return tokenize(sanitiseString(preprocessString(normalizeDates(input))), DEFAULT_TOKENIZE_CONFIG);
1252
+ }
1253
+ function scoreOne(item, query, qTokens, primaryTextFn, exactKeys) {
1254
+ if (exactKeys && exactKeys.length > 0) {
1255
+ for (let i = 0;i < exactKeys.length; i++) {
1256
+ if (exactKeys[i] === query)
1257
+ return 1e5;
1258
+ }
1259
+ }
1260
+ if (qTokens.length === 0)
1261
+ return 0;
1262
+ const primaryText = primaryTextFn(item) || "";
1263
+ const itemTokens = orderedTokenize(primaryText);
1264
+ if (itemTokens.length === 0)
1265
+ return 1;
1266
+ let qi = 0;
1267
+ const positions = [];
1268
+ for (let i = 0;i < itemTokens.length; i++) {
1269
+ if (qi < qTokens.length && itemTokens[i].startsWith(qTokens[qi])) {
1270
+ positions.push(i);
1271
+ qi++;
1272
+ }
1273
+ }
1274
+ if (qi === 0)
1275
+ return 1;
1276
+ let score = 50;
1277
+ if (qi === qTokens.length)
1278
+ score += 100;
1279
+ if (positions[0] === 0)
1280
+ score += 50;
1281
+ let perfect = qi === qTokens.length;
1282
+ if (perfect) {
1283
+ for (let i = 0;i < positions.length; i++) {
1284
+ if (positions[i] !== i) {
1285
+ perfect = false;
1286
+ break;
1287
+ }
1288
+ }
1289
+ }
1290
+ if (perfect)
1291
+ score += 80;
1292
+ if (positions.length >= 2) {
1293
+ const span = positions[positions.length - 1] - positions[0] + 1;
1294
+ score += positions.length / span * 30;
1295
+ }
1296
+ score -= primaryText.length * 0.1;
1297
+ return score;
1298
+ }
1299
+ function searchAndRankNumeric(query, data, metadata, options) {
1300
+ const { primaryTextFn, exactKeysFn, limit, opts } = options;
1301
+ const ids = searchInDataReturnIdsNumeric(query, metadata, opts);
1302
+ if (ids.length === 0)
1303
+ return [];
1304
+ const qTokens = orderedTokenize(query);
1305
+ const scored = [];
1306
+ for (const id of ids) {
1307
+ const item = data.get(id);
1308
+ if (!item)
1309
+ continue;
1310
+ const exactKeys = exactKeysFn?.(item);
1311
+ const score = scoreOne(item, query, qTokens, primaryTextFn, exactKeys);
1312
+ scored.push({ id, item, score });
1313
+ }
1314
+ scored.sort((x, y) => {
1315
+ if (y.score !== x.score)
1316
+ return y.score - x.score;
1317
+ const xText = primaryTextFn(x.item) || "";
1318
+ const yText = primaryTextFn(y.item) || "";
1319
+ return xText.localeCompare(yText);
1320
+ });
1321
+ if (limit === undefined || limit === Infinity || limit >= scored.length)
1322
+ return scored;
1323
+ return scored.slice(0, limit);
1324
+ }
1249
1325
  // src/common/SearchUniverse.ts
1250
1326
  function tokensEqual3(a, b) {
1251
1327
  if (a.length !== b.length)
@@ -1658,6 +1734,9 @@ export {
1658
1734
  searchInDataReturnObjectsNumeric as searchInDataReturnObjects,
1659
1735
  searchInDataReturnIdsNumeric as searchInDataReturnIds,
1660
1736
  searchInDataReturnIdsNumeric as searchInData,
1737
+ searchAndRankNumeric,
1738
+ searchAndRankNumeric as searchAndRank,
1739
+ scoreOne,
1661
1740
  sanitiseString,
1662
1741
  resetTokenRegistry,
1663
1742
  removePrimaryItemNumeric as removePrimaryItem,
@@ -1669,6 +1748,7 @@ export {
1669
1748
  prepareStringForSearch,
1670
1749
  prepareQueryTokens,
1671
1750
  prepareObjectForSearchNumeric as prepareObjectForSearch,
1751
+ orderedTokenize,
1672
1752
  normalizeDates,
1673
1753
  matchTokenInSortedListNumeric,
1674
1754
  matchPreparedTokenInSortedList,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cry-search",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "A fast, memory-efficient search library for large datasets with support for tokenized matching, linked collections, and per-field match modes.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",