@findlane/server 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,7 @@ const results = await products.search({ q }, { visitor: { ip: req.ip, userAgent:
47
47
 
48
48
  | Method | What it does |
49
49
  | --- | --- |
50
- | `search(request, options?)` | Search with a query, filters, facets, a sort, paging, and highlighting. `options` takes `signal` and `visitor` (below). |
50
+ | `search(request, options?)` | Search with a query, filters, facets, a sort, paging, and highlighting. `explain: true` says why each hit matched and what ordered it, and `appliedRules` lists the index's query rules the query fired. `options` takes `signal` and `visitor` (below). |
51
51
  | `upsert(records)` | Add or update up to 1,000 records by `objectID`. |
52
52
  | `replaceAll(records)` | Replace the whole catalog in one request (up to 100,000 records). |
53
53
  | `replaceAllStaged(records, options)` | Replace a larger catalog in batches under one stage ID, switched in one commit. Retrying with the same `stageId` resumes. `abortReplacement(stageId)` drops a stage. |
package/dist/index.d.cts CHANGED
@@ -12,20 +12,46 @@ export type SortOption = SortRule & {
12
12
  thenBy: SortRule[];
13
13
  };
14
14
  export type IndexSettings = {
15
- /** One to five fields, matched with their weight. */
15
+ /** One to five fields, matched with their weight. `typos: false` matches a field's words exactly or as
16
+ * prefixes, never with typos, for codes, SKUs, and brands. */
16
17
  searchableAttributes: {
17
18
  field: string;
18
19
  weight: number;
20
+ typos?: false;
19
21
  }[];
20
22
  facetFields: string[];
21
23
  filterFields?: string[];
22
24
  typoTolerance: boolean;
25
+ /** Words of at least `oneTypo` letters match with one typo, and of at least `twoTypos` with two.
26
+ * `{ oneTypo: 4, twoTypos: 7 }` by default; oneTypo is 3 to 20, twoTypos from oneTypo to 50. */
27
+ typoWordLengths?: {
28
+ oneTypo: number;
29
+ twoTypos: number;
30
+ };
31
+ /** Up to 1,000 single words that never match with typos, though prefixes still do, such as brand names. */
32
+ exactWords?: string[];
23
33
  sorts: SortOption[];
24
34
  defaultSort: string;
25
- /** Orders equally relevant results (within 10% of the best score) by up to three sort fields. */
35
+ /** Orders every result by up to two sort fields before relevance, in relevance and named sorts (not
36
+ * distance), such as `[{ field: "soldOut", direction: "asc" }]` for in stock first. Relevance and
37
+ * custom ranking order each group. Records without the field come last. */
38
+ rankFirst?: SortRule[];
39
+ /** Orders equally relevant results (within `relevanceTolerance` of the best score) by up to three sort fields. */
26
40
  customRanking?: SortRule[];
41
+ /** How close to the best match's score, in percent (1 to 50), results count as equally relevant for
42
+ * custom ranking, and for distance in searches with a query and `near`. 10 by default. */
43
+ relevanceTolerance?: number;
27
44
  /** Groups of interchangeable words or phrases (up to four words each). */
28
45
  synonyms?: string[][];
46
+ /** A query for an input also matches its 1 to 20 synonyms, but not the other way round. Up to 1,000,
47
+ * each term up to four words. */
48
+ oneWaySynonyms?: {
49
+ input: string;
50
+ synonyms: string[];
51
+ }[];
52
+ /** Up to 1,000 query rules, applied at query time, so a change takes effect at once. Up to 10 fire per
53
+ * search, in this order, and an earlier rule's filter on a field wins over a later one's. */
54
+ rules?: QueryRule[];
29
55
  /** Languages whose common words queries drop, and whose plurals and alternative spellings match. */
30
56
  languages?: Array<"da" | "de" | "en" | "is" | "no" | "sv">;
31
57
  /** Searches say which words typo tolerance corrected, and `suggest()` offers a corrected query for a
@@ -39,6 +65,40 @@ export type IndexSettings = {
39
65
  fieldTypes?: Record<string, "text" | "number" | "boolean" | "text[]" | "number[]" | "boolean[]" | "location">;
40
66
  [key: string]: unknown;
41
67
  };
68
+ /** When a query is, starts with, or contains one of a rule's phrases, the rule's `then` applies to that
69
+ * search. Phrases are compared as synonyms are: without case, accents, or the index languages' stop words,
70
+ * and with their plurals and other spellings, so a rule for "balloon" also fires for "balloons". */
71
+ export type QueryRule = {
72
+ /** 1 to 64 letters, digits, `_`, `.`, `:`, or `-`, unique in the index. */
73
+ id: string;
74
+ /** Up to 200 characters, for the people reading the settings. */
75
+ description?: string;
76
+ /** false keeps the rule without applying it. Reading settings always returns it; true when left out. */
77
+ enabled?: boolean;
78
+ if: {
79
+ /** 1 to 20 phrases of one to four words. */
80
+ phrases: string[];
81
+ /** The query is one of the phrases, starts with one, or contains one; "contains" when left out. */
82
+ match?: "is" | "startsWith" | "contains";
83
+ };
84
+ /** One or more of these. `filters`, `boost` and `bury` take a search's filters, on 1 to 10 fields. */
85
+ then: {
86
+ /** Added to the search's filters, except on fields the search (or an earlier rule) already filters.
87
+ * Records without the field drop out, so `boost` is usually the better choice. */
88
+ filters?: SearchRequest["filters"];
89
+ /** Records matching these come before the others, after `rankFirst`'s groups, in searches with a query
90
+ * sorted by relevance. Several rules' boosts add up. */
91
+ boost?: SearchRequest["filters"];
92
+ /** Records matching these come after the others, in the same searches as `boost`. */
93
+ bury?: SearchRequest["filters"];
94
+ /** Up to 100 objectIDs left out of the results. */
95
+ hide?: string[];
96
+ /** Words searched instead of the matched phrase, up to 64 characters; "" removes the phrase. */
97
+ replace?: string;
98
+ /** JSON up to 4 KB returned with the results in `appliedRules`, such as a banner or a redirect. */
99
+ userData?: unknown;
100
+ };
101
+ };
42
102
  /** A settings change running in the background (with the settings it applies), or the last one's
43
103
  * failure. `kind` is "searchable" (new searchable fields filled in) or "rebuild" (a full copy). */
44
104
  export type SettingsRebuild = {
@@ -100,7 +160,13 @@ export type SearchRequest = {
100
160
  q?: string;
101
161
  /** A sort ID from the index settings, "relevance", or "distance" (with `near`). */
102
162
  sort?: string;
103
- /** Field values to keep: a value, `{ in: [...] }`, or a range `{ gte, lte }` for numbers and dates. */
163
+ /** Field values to keep. Each field takes a value (a list field contains it), or an object of
164
+ * conditions that all apply: `in` (any of up to 100 values), `all` (a list field has every one),
165
+ * `not` and `notIn` (records without the field stay in), and `gte`/`lte` for numbers and dates, such
166
+ * as `{ tags: { in: ["kids", "adult"], notIn: ["party"] }, price: { lte: 50 } }`. `"$or"` takes one to
167
+ * four groups of field filters and keeps records that match any group:
168
+ * `{ inStock: true, $or: [{ brand: "Acme" }, { price: { lte: 20 } }] }`. Up to 20 field filters,
169
+ * counting those in groups. */
104
170
  filters?: Record<string, unknown>;
105
171
  facets?: string[];
106
172
  /** Facets (also in `facets`) counted without the filter on their own field, so a list of choices
@@ -122,6 +188,13 @@ export type SearchRequest = {
122
188
  preTag?: string;
123
189
  postTag?: string;
124
190
  };
191
+ /** Say why each hit matched and what ordered it, in each hit's `explain`: each query word's best match in
192
+ * each searchable field, the relevance band, and the ranking fields' values. For understanding and tuning
193
+ * ranking; browser search doesn't explain results. */
194
+ explain?: boolean;
195
+ /** false searches without the index's query rules, such as to compare results with and without them. On by
196
+ * default; browser search doesn't take it. */
197
+ enableRules?: boolean;
125
198
  /** Nearest first from a point, with each hit's `distance` in meters; `radius` (meters) keeps only what's
126
199
  * within it. With a query, the best matches come first and distance orders them. Needs the index's
127
200
  * location field. Records without a location are left out. */
@@ -144,6 +217,60 @@ export type GeoBox = {
144
217
  east: number;
145
218
  west: number;
146
219
  };
220
+ /** How a query word matched: as itself, as the start of a longer word, within its typos, through a synonym, or
221
+ * as another form of the word in the index's languages (a plural or another spelling). */
222
+ export type MatchKind = "exact" | "prefix" | "typo" | "synonym" | "form";
223
+ /** A query word's best match in one searchable field. `word` is the matched word (or a synonym's phrase) as
224
+ * the record writes it. `weight` is what that kind of match weighs in the search: the field's weight times 1
225
+ * for exact, 0.8 for a synonym or word form, 0.2 for a prefix, and 0.1 for a typo. A typo has `edits`, or
226
+ * `firstLetter` when the first letter differs (tried only when nothing matched without); a synonym has the
227
+ * `synonym` that matched. */
228
+ export type FieldMatch = {
229
+ field: string;
230
+ word: string;
231
+ how: MatchKind;
232
+ weight: number;
233
+ edits?: number;
234
+ firstLetter?: true;
235
+ synonym?: string;
236
+ };
237
+ /** A ranking field and the hit's value for it; null when the record has none (it comes last). */
238
+ export type RankingValue = SortRule & {
239
+ value: unknown;
240
+ };
241
+ /** Why a hit matched and what ordered it, with `explain`. Scores are for the whole record, so matches carry
242
+ * weights rather than points that add up to `score`. */
243
+ export type HitExplanation = {
244
+ /** Each query word (after the languages' stop words, up to eight) with its best match in each searchable
245
+ * field, strongest first. Every word matches somewhere. Empty without a query. */
246
+ words: {
247
+ query: string;
248
+ matches: FieldMatch[];
249
+ }[];
250
+ /** With custom ranking (or `near`) and a query: 0 for results within `relevanceTolerance` of `bestScore`,
251
+ * 1 for the next band down, and so on. Distance (with `near`), then custom ranking, orders the results
252
+ * within a band. */
253
+ band?: number | null;
254
+ /** The best score in the hit's rank first group, among results as boosted by the search's rules (the whole
255
+ * search without either). */
256
+ bestScore?: number | null;
257
+ /** The index's rank first fields. */
258
+ rankFirst?: RankingValue[];
259
+ /** The index's custom ranking fields, when sorting by relevance. */
260
+ customRanking?: RankingValue[];
261
+ /** The named sort's fields, when sorting by one. */
262
+ sort?: RankingValue[];
263
+ /** When the query's rules boosted or buried results: those whose filters the hit matched, if any. */
264
+ rules?: {
265
+ id: string;
266
+ effect: "boost" | "bury";
267
+ }[];
268
+ };
269
+ /** A query rule the search fired, with its `userData`, if it has any. */
270
+ export type AppliedRule = {
271
+ id: string;
272
+ userData?: unknown;
273
+ };
147
274
  export type SearchResponse<T> = {
148
275
  hits: {
149
276
  id: string;
@@ -153,6 +280,8 @@ export type SearchResponse<T> = {
153
280
  distance?: number;
154
281
  /** With `highlight`: matched searchable fields by path; list fields give one string per value. */
155
282
  highlights?: Record<string, string | string[]>;
283
+ /** With `explain`: why the hit matched and what ordered it. */
284
+ explain?: HitExplanation;
156
285
  }[];
157
286
  total: number;
158
287
  /** "gte" when `total` is a lower bound (page count mode). */
@@ -172,12 +301,17 @@ export type SearchResponse<T> = {
172
301
  /** With the index's `didYouMean` setting: the query with the words typo tolerance corrected, when the
173
302
  * results matched some words only by typo ("Showing results for …"), else null. */
174
303
  didYouMean?: DidYouMean | null;
304
+ /** The index's query rules the query fired, in the index's order. Left out when none fired. */
305
+ appliedRules?: AppliedRule[];
306
+ /** When a rule replaced words: the query that was searched. */
307
+ rewrittenQuery?: string;
175
308
  };
176
309
  /** How a product's recommendations are served (the recommendations add-on). */
177
310
  export type RecommendationsRequest = {
178
311
  /** Up to this many products in each list, 1 to 20; 8 by default. */
179
312
  limit?: number;
180
- /** Only picks matching these filters, as in a search (fields configured as filters), such as `{ inStock: true }`. */
313
+ /** Only picks matching these filters, as in a search (fields configured as filters), such as `{ inStock: true }`.
314
+ * Each field's filter takes the same shapes; `$or` is for searches only. */
181
315
  filters?: SearchRequest["filters"];
182
316
  /** The record fields to return; all of them by default. */
183
317
  attributesToRetrieve?: string[];
package/dist/index.d.ts CHANGED
@@ -12,20 +12,46 @@ export type SortOption = SortRule & {
12
12
  thenBy: SortRule[];
13
13
  };
14
14
  export type IndexSettings = {
15
- /** One to five fields, matched with their weight. */
15
+ /** One to five fields, matched with their weight. `typos: false` matches a field's words exactly or as
16
+ * prefixes, never with typos, for codes, SKUs, and brands. */
16
17
  searchableAttributes: {
17
18
  field: string;
18
19
  weight: number;
20
+ typos?: false;
19
21
  }[];
20
22
  facetFields: string[];
21
23
  filterFields?: string[];
22
24
  typoTolerance: boolean;
25
+ /** Words of at least `oneTypo` letters match with one typo, and of at least `twoTypos` with two.
26
+ * `{ oneTypo: 4, twoTypos: 7 }` by default; oneTypo is 3 to 20, twoTypos from oneTypo to 50. */
27
+ typoWordLengths?: {
28
+ oneTypo: number;
29
+ twoTypos: number;
30
+ };
31
+ /** Up to 1,000 single words that never match with typos, though prefixes still do, such as brand names. */
32
+ exactWords?: string[];
23
33
  sorts: SortOption[];
24
34
  defaultSort: string;
25
- /** Orders equally relevant results (within 10% of the best score) by up to three sort fields. */
35
+ /** Orders every result by up to two sort fields before relevance, in relevance and named sorts (not
36
+ * distance), such as `[{ field: "soldOut", direction: "asc" }]` for in stock first. Relevance and
37
+ * custom ranking order each group. Records without the field come last. */
38
+ rankFirst?: SortRule[];
39
+ /** Orders equally relevant results (within `relevanceTolerance` of the best score) by up to three sort fields. */
26
40
  customRanking?: SortRule[];
41
+ /** How close to the best match's score, in percent (1 to 50), results count as equally relevant for
42
+ * custom ranking, and for distance in searches with a query and `near`. 10 by default. */
43
+ relevanceTolerance?: number;
27
44
  /** Groups of interchangeable words or phrases (up to four words each). */
28
45
  synonyms?: string[][];
46
+ /** A query for an input also matches its 1 to 20 synonyms, but not the other way round. Up to 1,000,
47
+ * each term up to four words. */
48
+ oneWaySynonyms?: {
49
+ input: string;
50
+ synonyms: string[];
51
+ }[];
52
+ /** Up to 1,000 query rules, applied at query time, so a change takes effect at once. Up to 10 fire per
53
+ * search, in this order, and an earlier rule's filter on a field wins over a later one's. */
54
+ rules?: QueryRule[];
29
55
  /** Languages whose common words queries drop, and whose plurals and alternative spellings match. */
30
56
  languages?: Array<"da" | "de" | "en" | "is" | "no" | "sv">;
31
57
  /** Searches say which words typo tolerance corrected, and `suggest()` offers a corrected query for a
@@ -39,6 +65,40 @@ export type IndexSettings = {
39
65
  fieldTypes?: Record<string, "text" | "number" | "boolean" | "text[]" | "number[]" | "boolean[]" | "location">;
40
66
  [key: string]: unknown;
41
67
  };
68
+ /** When a query is, starts with, or contains one of a rule's phrases, the rule's `then` applies to that
69
+ * search. Phrases are compared as synonyms are: without case, accents, or the index languages' stop words,
70
+ * and with their plurals and other spellings, so a rule for "balloon" also fires for "balloons". */
71
+ export type QueryRule = {
72
+ /** 1 to 64 letters, digits, `_`, `.`, `:`, or `-`, unique in the index. */
73
+ id: string;
74
+ /** Up to 200 characters, for the people reading the settings. */
75
+ description?: string;
76
+ /** false keeps the rule without applying it. Reading settings always returns it; true when left out. */
77
+ enabled?: boolean;
78
+ if: {
79
+ /** 1 to 20 phrases of one to four words. */
80
+ phrases: string[];
81
+ /** The query is one of the phrases, starts with one, or contains one; "contains" when left out. */
82
+ match?: "is" | "startsWith" | "contains";
83
+ };
84
+ /** One or more of these. `filters`, `boost` and `bury` take a search's filters, on 1 to 10 fields. */
85
+ then: {
86
+ /** Added to the search's filters, except on fields the search (or an earlier rule) already filters.
87
+ * Records without the field drop out, so `boost` is usually the better choice. */
88
+ filters?: SearchRequest["filters"];
89
+ /** Records matching these come before the others, after `rankFirst`'s groups, in searches with a query
90
+ * sorted by relevance. Several rules' boosts add up. */
91
+ boost?: SearchRequest["filters"];
92
+ /** Records matching these come after the others, in the same searches as `boost`. */
93
+ bury?: SearchRequest["filters"];
94
+ /** Up to 100 objectIDs left out of the results. */
95
+ hide?: string[];
96
+ /** Words searched instead of the matched phrase, up to 64 characters; "" removes the phrase. */
97
+ replace?: string;
98
+ /** JSON up to 4 KB returned with the results in `appliedRules`, such as a banner or a redirect. */
99
+ userData?: unknown;
100
+ };
101
+ };
42
102
  /** A settings change running in the background (with the settings it applies), or the last one's
43
103
  * failure. `kind` is "searchable" (new searchable fields filled in) or "rebuild" (a full copy). */
44
104
  export type SettingsRebuild = {
@@ -100,7 +160,13 @@ export type SearchRequest = {
100
160
  q?: string;
101
161
  /** A sort ID from the index settings, "relevance", or "distance" (with `near`). */
102
162
  sort?: string;
103
- /** Field values to keep: a value, `{ in: [...] }`, or a range `{ gte, lte }` for numbers and dates. */
163
+ /** Field values to keep. Each field takes a value (a list field contains it), or an object of
164
+ * conditions that all apply: `in` (any of up to 100 values), `all` (a list field has every one),
165
+ * `not` and `notIn` (records without the field stay in), and `gte`/`lte` for numbers and dates, such
166
+ * as `{ tags: { in: ["kids", "adult"], notIn: ["party"] }, price: { lte: 50 } }`. `"$or"` takes one to
167
+ * four groups of field filters and keeps records that match any group:
168
+ * `{ inStock: true, $or: [{ brand: "Acme" }, { price: { lte: 20 } }] }`. Up to 20 field filters,
169
+ * counting those in groups. */
104
170
  filters?: Record<string, unknown>;
105
171
  facets?: string[];
106
172
  /** Facets (also in `facets`) counted without the filter on their own field, so a list of choices
@@ -122,6 +188,13 @@ export type SearchRequest = {
122
188
  preTag?: string;
123
189
  postTag?: string;
124
190
  };
191
+ /** Say why each hit matched and what ordered it, in each hit's `explain`: each query word's best match in
192
+ * each searchable field, the relevance band, and the ranking fields' values. For understanding and tuning
193
+ * ranking; browser search doesn't explain results. */
194
+ explain?: boolean;
195
+ /** false searches without the index's query rules, such as to compare results with and without them. On by
196
+ * default; browser search doesn't take it. */
197
+ enableRules?: boolean;
125
198
  /** Nearest first from a point, with each hit's `distance` in meters; `radius` (meters) keeps only what's
126
199
  * within it. With a query, the best matches come first and distance orders them. Needs the index's
127
200
  * location field. Records without a location are left out. */
@@ -144,6 +217,60 @@ export type GeoBox = {
144
217
  east: number;
145
218
  west: number;
146
219
  };
220
+ /** How a query word matched: as itself, as the start of a longer word, within its typos, through a synonym, or
221
+ * as another form of the word in the index's languages (a plural or another spelling). */
222
+ export type MatchKind = "exact" | "prefix" | "typo" | "synonym" | "form";
223
+ /** A query word's best match in one searchable field. `word` is the matched word (or a synonym's phrase) as
224
+ * the record writes it. `weight` is what that kind of match weighs in the search: the field's weight times 1
225
+ * for exact, 0.8 for a synonym or word form, 0.2 for a prefix, and 0.1 for a typo. A typo has `edits`, or
226
+ * `firstLetter` when the first letter differs (tried only when nothing matched without); a synonym has the
227
+ * `synonym` that matched. */
228
+ export type FieldMatch = {
229
+ field: string;
230
+ word: string;
231
+ how: MatchKind;
232
+ weight: number;
233
+ edits?: number;
234
+ firstLetter?: true;
235
+ synonym?: string;
236
+ };
237
+ /** A ranking field and the hit's value for it; null when the record has none (it comes last). */
238
+ export type RankingValue = SortRule & {
239
+ value: unknown;
240
+ };
241
+ /** Why a hit matched and what ordered it, with `explain`. Scores are for the whole record, so matches carry
242
+ * weights rather than points that add up to `score`. */
243
+ export type HitExplanation = {
244
+ /** Each query word (after the languages' stop words, up to eight) with its best match in each searchable
245
+ * field, strongest first. Every word matches somewhere. Empty without a query. */
246
+ words: {
247
+ query: string;
248
+ matches: FieldMatch[];
249
+ }[];
250
+ /** With custom ranking (or `near`) and a query: 0 for results within `relevanceTolerance` of `bestScore`,
251
+ * 1 for the next band down, and so on. Distance (with `near`), then custom ranking, orders the results
252
+ * within a band. */
253
+ band?: number | null;
254
+ /** The best score in the hit's rank first group, among results as boosted by the search's rules (the whole
255
+ * search without either). */
256
+ bestScore?: number | null;
257
+ /** The index's rank first fields. */
258
+ rankFirst?: RankingValue[];
259
+ /** The index's custom ranking fields, when sorting by relevance. */
260
+ customRanking?: RankingValue[];
261
+ /** The named sort's fields, when sorting by one. */
262
+ sort?: RankingValue[];
263
+ /** When the query's rules boosted or buried results: those whose filters the hit matched, if any. */
264
+ rules?: {
265
+ id: string;
266
+ effect: "boost" | "bury";
267
+ }[];
268
+ };
269
+ /** A query rule the search fired, with its `userData`, if it has any. */
270
+ export type AppliedRule = {
271
+ id: string;
272
+ userData?: unknown;
273
+ };
147
274
  export type SearchResponse<T> = {
148
275
  hits: {
149
276
  id: string;
@@ -153,6 +280,8 @@ export type SearchResponse<T> = {
153
280
  distance?: number;
154
281
  /** With `highlight`: matched searchable fields by path; list fields give one string per value. */
155
282
  highlights?: Record<string, string | string[]>;
283
+ /** With `explain`: why the hit matched and what ordered it. */
284
+ explain?: HitExplanation;
156
285
  }[];
157
286
  total: number;
158
287
  /** "gte" when `total` is a lower bound (page count mode). */
@@ -172,12 +301,17 @@ export type SearchResponse<T> = {
172
301
  /** With the index's `didYouMean` setting: the query with the words typo tolerance corrected, when the
173
302
  * results matched some words only by typo ("Showing results for …"), else null. */
174
303
  didYouMean?: DidYouMean | null;
304
+ /** The index's query rules the query fired, in the index's order. Left out when none fired. */
305
+ appliedRules?: AppliedRule[];
306
+ /** When a rule replaced words: the query that was searched. */
307
+ rewrittenQuery?: string;
175
308
  };
176
309
  /** How a product's recommendations are served (the recommendations add-on). */
177
310
  export type RecommendationsRequest = {
178
311
  /** Up to this many products in each list, 1 to 20; 8 by default. */
179
312
  limit?: number;
180
- /** Only picks matching these filters, as in a search (fields configured as filters), such as `{ inStock: true }`. */
313
+ /** Only picks matching these filters, as in a search (fields configured as filters), such as `{ inStock: true }`.
314
+ * Each field's filter takes the same shapes; `$or` is for searches only. */
181
315
  filters?: SearchRequest["filters"];
182
316
  /** The record fields to return; all of them by default. */
183
317
  attributesToRetrieve?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@findlane/server",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Findlane server SDK: upload catalogs, manage index settings, and search with a secret workspace API key.",
5
5
  "keywords": ["findlane", "search", "full-text search", "typo tolerance", "ecommerce search", "search api", "sdk"],
6
6
  "license": "MIT",