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 +26 -0
- package/README.md +65 -0
- package/UNIVERSE.md +18 -2
- package/dist/common/SearchUniverse.d.ts +28 -13
- package/dist/common/SearchUniverse.d.ts.map +1 -1
- package/dist/common/numeric/searchAndRank.d.ts +90 -0
- package/dist/common/numeric/searchAndRank.d.ts.map +1 -0
- package/dist/index.cjs +80 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +80 -0
- package/package.json +1 -1
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
|
-
│
|
|
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
|
```
|
|
@@ -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;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
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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",
|