@findlane/server 0.12.0 → 0.14.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,25 +12,57 @@ 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
32
58
  * search that finds nothing. Off by default. */
33
59
  didYouMean?: boolean;
60
+ /** Compound words find each other written as one word or apart, in any language: a query word that the
61
+ * index's words show is a compound also matches its parts written apart ("sjóræningjabúningur" finds
62
+ * "Sjóræningja búningur"), and neighbouring query words the word they make ("face paint" finds "facepaint").
63
+ * Exact matches rank first. Searches use it once the index's word list is built, a minute or so after it's
64
+ * turned on. Off by default. */
65
+ compoundWords?: boolean;
34
66
  normalizationProfile?: "default-v2" | "default-v1" | "icelandic-ascii-v1";
35
67
  /** An object field of lat and lng numbers, such as `_geoloc`, that `near` and `within` search; null for none. */
36
68
  locationField?: string | null;
@@ -39,6 +71,40 @@ export type IndexSettings = {
39
71
  fieldTypes?: Record<string, "text" | "number" | "boolean" | "text[]" | "number[]" | "boolean[]" | "location">;
40
72
  [key: string]: unknown;
41
73
  };
74
+ /** When a query is, starts with, or contains one of a rule's phrases, the rule's `then` applies to that
75
+ * search. Phrases are compared as synonyms are: without case, accents, or the index languages' stop words,
76
+ * and with their plurals and other spellings, so a rule for "balloon" also fires for "balloons". */
77
+ export type QueryRule = {
78
+ /** 1 to 64 letters, digits, `_`, `.`, `:`, or `-`, unique in the index. */
79
+ id: string;
80
+ /** Up to 200 characters, for the people reading the settings. */
81
+ description?: string;
82
+ /** false keeps the rule without applying it. Reading settings always returns it; true when left out. */
83
+ enabled?: boolean;
84
+ if: {
85
+ /** 1 to 20 phrases of one to four words. */
86
+ phrases: string[];
87
+ /** The query is one of the phrases, starts with one, or contains one; "contains" when left out. */
88
+ match?: "is" | "startsWith" | "contains";
89
+ };
90
+ /** One or more of these. `filters`, `boost` and `bury` take a search's filters, on 1 to 10 fields. */
91
+ then: {
92
+ /** Added to the search's filters, except on fields the search (or an earlier rule) already filters.
93
+ * Records without the field drop out, so `boost` is usually the better choice. */
94
+ filters?: SearchRequest["filters"];
95
+ /** Records matching these come before the others, after `rankFirst`'s groups, in searches with a query
96
+ * sorted by relevance. Several rules' boosts add up. */
97
+ boost?: SearchRequest["filters"];
98
+ /** Records matching these come after the others, in the same searches as `boost`. */
99
+ bury?: SearchRequest["filters"];
100
+ /** Up to 100 objectIDs left out of the results. */
101
+ hide?: string[];
102
+ /** Words searched instead of the matched phrase, up to 64 characters; "" removes the phrase. */
103
+ replace?: string;
104
+ /** JSON up to 4 KB returned with the results in `appliedRules`, such as a banner or a redirect. */
105
+ userData?: unknown;
106
+ };
107
+ };
42
108
  /** A settings change running in the background (with the settings it applies), or the last one's
43
109
  * failure. `kind` is "searchable" (new searchable fields filled in) or "rebuild" (a full copy). */
44
110
  export type SettingsRebuild = {
@@ -100,7 +166,13 @@ export type SearchRequest = {
100
166
  q?: string;
101
167
  /** A sort ID from the index settings, "relevance", or "distance" (with `near`). */
102
168
  sort?: string;
103
- /** Field values to keep: a value, `{ in: [...] }`, or a range `{ gte, lte }` for numbers and dates. */
169
+ /** Field values to keep. Each field takes a value (a list field contains it), or an object of
170
+ * conditions that all apply: `in` (any of up to 100 values), `all` (a list field has every one),
171
+ * `not` and `notIn` (records without the field stay in), and `gte`/`lte` for numbers and dates, such
172
+ * as `{ tags: { in: ["kids", "adult"], notIn: ["party"] }, price: { lte: 50 } }`. `"$or"` takes one to
173
+ * four groups of field filters and keeps records that match any group:
174
+ * `{ inStock: true, $or: [{ brand: "Acme" }, { price: { lte: 20 } }] }`. Up to 20 field filters,
175
+ * counting those in groups. */
104
176
  filters?: Record<string, unknown>;
105
177
  facets?: string[];
106
178
  /** Facets (also in `facets`) counted without the filter on their own field, so a list of choices
@@ -122,6 +194,13 @@ export type SearchRequest = {
122
194
  preTag?: string;
123
195
  postTag?: string;
124
196
  };
197
+ /** Say why each hit matched and what ordered it, in each hit's `explain`: each query word's best match in
198
+ * each searchable field, the relevance band, and the ranking fields' values. For understanding and tuning
199
+ * ranking; browser search doesn't explain results. */
200
+ explain?: boolean;
201
+ /** false searches without the index's query rules, such as to compare results with and without them. On by
202
+ * default; browser search doesn't take it. */
203
+ enableRules?: boolean;
125
204
  /** Nearest first from a point, with each hit's `distance` in meters; `radius` (meters) keeps only what's
126
205
  * within it. With a query, the best matches come first and distance orders them. Needs the index's
127
206
  * location field. Records without a location are left out. */
@@ -144,6 +223,62 @@ export type GeoBox = {
144
223
  east: number;
145
224
  west: number;
146
225
  };
226
+ /** How a query word matched: as itself, as the start of a longer word, within its typos, through a synonym,
227
+ * as another form of the word in the index's languages (a plural or another spelling), or, with the
228
+ * `compoundWords` setting, as a compound: its parts written apart, or the word it makes with a neighbouring
229
+ * query word. */
230
+ export type MatchKind = "exact" | "prefix" | "typo" | "synonym" | "form" | "compound";
231
+ /** A query word's best match in one searchable field. `word` is the matched word (or a synonym's phrase, or a
232
+ * compound's parts) as the record writes it. `weight` is what that kind of match weighs in the search: the
233
+ * field's weight times 1 for exact, 0.8 for a synonym or word form, 0.4 for a compound, 0.2 for a prefix, and
234
+ * 0.1 for a typo. A typo has `edits`, or `firstLetter` when the first letter differs (tried only when nothing
235
+ * matched without); a synonym has the `synonym` that matched. */
236
+ export type FieldMatch = {
237
+ field: string;
238
+ word: string;
239
+ how: MatchKind;
240
+ weight: number;
241
+ edits?: number;
242
+ firstLetter?: true;
243
+ synonym?: string;
244
+ };
245
+ /** A ranking field and the hit's value for it; null when the record has none (it comes last). */
246
+ export type RankingValue = SortRule & {
247
+ value: unknown;
248
+ };
249
+ /** Why a hit matched and what ordered it, with `explain`. Scores are for the whole record, so matches carry
250
+ * weights rather than points that add up to `score`. */
251
+ export type HitExplanation = {
252
+ /** Each query word (after the languages' stop words, up to eight) with its best match in each searchable
253
+ * field, strongest first. Every word matches somewhere. Empty without a query. */
254
+ words: {
255
+ query: string;
256
+ matches: FieldMatch[];
257
+ }[];
258
+ /** With custom ranking (or `near`) and a query: 0 for results within `relevanceTolerance` of `bestScore`,
259
+ * 1 for the next band down, and so on. Distance (with `near`), then custom ranking, orders the results
260
+ * within a band. */
261
+ band?: number | null;
262
+ /** The best score in the hit's rank first group, among results as boosted by the search's rules (the whole
263
+ * search without either). */
264
+ bestScore?: number | null;
265
+ /** The index's rank first fields. */
266
+ rankFirst?: RankingValue[];
267
+ /** The index's custom ranking fields, when sorting by relevance. */
268
+ customRanking?: RankingValue[];
269
+ /** The named sort's fields, when sorting by one. */
270
+ sort?: RankingValue[];
271
+ /** When the query's rules boosted or buried results: those whose filters the hit matched, if any. */
272
+ rules?: {
273
+ id: string;
274
+ effect: "boost" | "bury";
275
+ }[];
276
+ };
277
+ /** A query rule the search fired, with its `userData`, if it has any. */
278
+ export type AppliedRule = {
279
+ id: string;
280
+ userData?: unknown;
281
+ };
147
282
  export type SearchResponse<T> = {
148
283
  hits: {
149
284
  id: string;
@@ -153,6 +288,8 @@ export type SearchResponse<T> = {
153
288
  distance?: number;
154
289
  /** With `highlight`: matched searchable fields by path; list fields give one string per value. */
155
290
  highlights?: Record<string, string | string[]>;
291
+ /** With `explain`: why the hit matched and what ordered it. */
292
+ explain?: HitExplanation;
156
293
  }[];
157
294
  total: number;
158
295
  /** "gte" when `total` is a lower bound (page count mode). */
@@ -172,12 +309,17 @@ export type SearchResponse<T> = {
172
309
  /** With the index's `didYouMean` setting: the query with the words typo tolerance corrected, when the
173
310
  * results matched some words only by typo ("Showing results for …"), else null. */
174
311
  didYouMean?: DidYouMean | null;
312
+ /** The index's query rules the query fired, in the index's order. Left out when none fired. */
313
+ appliedRules?: AppliedRule[];
314
+ /** When a rule replaced words: the query that was searched. */
315
+ rewrittenQuery?: string;
175
316
  };
176
317
  /** How a product's recommendations are served (the recommendations add-on). */
177
318
  export type RecommendationsRequest = {
178
319
  /** Up to this many products in each list, 1 to 20; 8 by default. */
179
320
  limit?: number;
180
- /** Only picks matching these filters, as in a search (fields configured as filters), such as `{ inStock: true }`. */
321
+ /** Only picks matching these filters, as in a search (fields configured as filters), such as `{ inStock: true }`.
322
+ * Each field's filter takes the same shapes; `$or` is for searches only. */
181
323
  filters?: SearchRequest["filters"];
182
324
  /** The record fields to return; all of them by default. */
183
325
  attributesToRetrieve?: string[];
package/dist/index.d.ts CHANGED
@@ -12,25 +12,57 @@ 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
32
58
  * search that finds nothing. Off by default. */
33
59
  didYouMean?: boolean;
60
+ /** Compound words find each other written as one word or apart, in any language: a query word that the
61
+ * index's words show is a compound also matches its parts written apart ("sjóræningjabúningur" finds
62
+ * "Sjóræningja búningur"), and neighbouring query words the word they make ("face paint" finds "facepaint").
63
+ * Exact matches rank first. Searches use it once the index's word list is built, a minute or so after it's
64
+ * turned on. Off by default. */
65
+ compoundWords?: boolean;
34
66
  normalizationProfile?: "default-v2" | "default-v1" | "icelandic-ascii-v1";
35
67
  /** An object field of lat and lng numbers, such as `_geoloc`, that `near` and `within` search; null for none. */
36
68
  locationField?: string | null;
@@ -39,6 +71,40 @@ export type IndexSettings = {
39
71
  fieldTypes?: Record<string, "text" | "number" | "boolean" | "text[]" | "number[]" | "boolean[]" | "location">;
40
72
  [key: string]: unknown;
41
73
  };
74
+ /** When a query is, starts with, or contains one of a rule's phrases, the rule's `then` applies to that
75
+ * search. Phrases are compared as synonyms are: without case, accents, or the index languages' stop words,
76
+ * and with their plurals and other spellings, so a rule for "balloon" also fires for "balloons". */
77
+ export type QueryRule = {
78
+ /** 1 to 64 letters, digits, `_`, `.`, `:`, or `-`, unique in the index. */
79
+ id: string;
80
+ /** Up to 200 characters, for the people reading the settings. */
81
+ description?: string;
82
+ /** false keeps the rule without applying it. Reading settings always returns it; true when left out. */
83
+ enabled?: boolean;
84
+ if: {
85
+ /** 1 to 20 phrases of one to four words. */
86
+ phrases: string[];
87
+ /** The query is one of the phrases, starts with one, or contains one; "contains" when left out. */
88
+ match?: "is" | "startsWith" | "contains";
89
+ };
90
+ /** One or more of these. `filters`, `boost` and `bury` take a search's filters, on 1 to 10 fields. */
91
+ then: {
92
+ /** Added to the search's filters, except on fields the search (or an earlier rule) already filters.
93
+ * Records without the field drop out, so `boost` is usually the better choice. */
94
+ filters?: SearchRequest["filters"];
95
+ /** Records matching these come before the others, after `rankFirst`'s groups, in searches with a query
96
+ * sorted by relevance. Several rules' boosts add up. */
97
+ boost?: SearchRequest["filters"];
98
+ /** Records matching these come after the others, in the same searches as `boost`. */
99
+ bury?: SearchRequest["filters"];
100
+ /** Up to 100 objectIDs left out of the results. */
101
+ hide?: string[];
102
+ /** Words searched instead of the matched phrase, up to 64 characters; "" removes the phrase. */
103
+ replace?: string;
104
+ /** JSON up to 4 KB returned with the results in `appliedRules`, such as a banner or a redirect. */
105
+ userData?: unknown;
106
+ };
107
+ };
42
108
  /** A settings change running in the background (with the settings it applies), or the last one's
43
109
  * failure. `kind` is "searchable" (new searchable fields filled in) or "rebuild" (a full copy). */
44
110
  export type SettingsRebuild = {
@@ -100,7 +166,13 @@ export type SearchRequest = {
100
166
  q?: string;
101
167
  /** A sort ID from the index settings, "relevance", or "distance" (with `near`). */
102
168
  sort?: string;
103
- /** Field values to keep: a value, `{ in: [...] }`, or a range `{ gte, lte }` for numbers and dates. */
169
+ /** Field values to keep. Each field takes a value (a list field contains it), or an object of
170
+ * conditions that all apply: `in` (any of up to 100 values), `all` (a list field has every one),
171
+ * `not` and `notIn` (records without the field stay in), and `gte`/`lte` for numbers and dates, such
172
+ * as `{ tags: { in: ["kids", "adult"], notIn: ["party"] }, price: { lte: 50 } }`. `"$or"` takes one to
173
+ * four groups of field filters and keeps records that match any group:
174
+ * `{ inStock: true, $or: [{ brand: "Acme" }, { price: { lte: 20 } }] }`. Up to 20 field filters,
175
+ * counting those in groups. */
104
176
  filters?: Record<string, unknown>;
105
177
  facets?: string[];
106
178
  /** Facets (also in `facets`) counted without the filter on their own field, so a list of choices
@@ -122,6 +194,13 @@ export type SearchRequest = {
122
194
  preTag?: string;
123
195
  postTag?: string;
124
196
  };
197
+ /** Say why each hit matched and what ordered it, in each hit's `explain`: each query word's best match in
198
+ * each searchable field, the relevance band, and the ranking fields' values. For understanding and tuning
199
+ * ranking; browser search doesn't explain results. */
200
+ explain?: boolean;
201
+ /** false searches without the index's query rules, such as to compare results with and without them. On by
202
+ * default; browser search doesn't take it. */
203
+ enableRules?: boolean;
125
204
  /** Nearest first from a point, with each hit's `distance` in meters; `radius` (meters) keeps only what's
126
205
  * within it. With a query, the best matches come first and distance orders them. Needs the index's
127
206
  * location field. Records without a location are left out. */
@@ -144,6 +223,62 @@ export type GeoBox = {
144
223
  east: number;
145
224
  west: number;
146
225
  };
226
+ /** How a query word matched: as itself, as the start of a longer word, within its typos, through a synonym,
227
+ * as another form of the word in the index's languages (a plural or another spelling), or, with the
228
+ * `compoundWords` setting, as a compound: its parts written apart, or the word it makes with a neighbouring
229
+ * query word. */
230
+ export type MatchKind = "exact" | "prefix" | "typo" | "synonym" | "form" | "compound";
231
+ /** A query word's best match in one searchable field. `word` is the matched word (or a synonym's phrase, or a
232
+ * compound's parts) as the record writes it. `weight` is what that kind of match weighs in the search: the
233
+ * field's weight times 1 for exact, 0.8 for a synonym or word form, 0.4 for a compound, 0.2 for a prefix, and
234
+ * 0.1 for a typo. A typo has `edits`, or `firstLetter` when the first letter differs (tried only when nothing
235
+ * matched without); a synonym has the `synonym` that matched. */
236
+ export type FieldMatch = {
237
+ field: string;
238
+ word: string;
239
+ how: MatchKind;
240
+ weight: number;
241
+ edits?: number;
242
+ firstLetter?: true;
243
+ synonym?: string;
244
+ };
245
+ /** A ranking field and the hit's value for it; null when the record has none (it comes last). */
246
+ export type RankingValue = SortRule & {
247
+ value: unknown;
248
+ };
249
+ /** Why a hit matched and what ordered it, with `explain`. Scores are for the whole record, so matches carry
250
+ * weights rather than points that add up to `score`. */
251
+ export type HitExplanation = {
252
+ /** Each query word (after the languages' stop words, up to eight) with its best match in each searchable
253
+ * field, strongest first. Every word matches somewhere. Empty without a query. */
254
+ words: {
255
+ query: string;
256
+ matches: FieldMatch[];
257
+ }[];
258
+ /** With custom ranking (or `near`) and a query: 0 for results within `relevanceTolerance` of `bestScore`,
259
+ * 1 for the next band down, and so on. Distance (with `near`), then custom ranking, orders the results
260
+ * within a band. */
261
+ band?: number | null;
262
+ /** The best score in the hit's rank first group, among results as boosted by the search's rules (the whole
263
+ * search without either). */
264
+ bestScore?: number | null;
265
+ /** The index's rank first fields. */
266
+ rankFirst?: RankingValue[];
267
+ /** The index's custom ranking fields, when sorting by relevance. */
268
+ customRanking?: RankingValue[];
269
+ /** The named sort's fields, when sorting by one. */
270
+ sort?: RankingValue[];
271
+ /** When the query's rules boosted or buried results: those whose filters the hit matched, if any. */
272
+ rules?: {
273
+ id: string;
274
+ effect: "boost" | "bury";
275
+ }[];
276
+ };
277
+ /** A query rule the search fired, with its `userData`, if it has any. */
278
+ export type AppliedRule = {
279
+ id: string;
280
+ userData?: unknown;
281
+ };
147
282
  export type SearchResponse<T> = {
148
283
  hits: {
149
284
  id: string;
@@ -153,6 +288,8 @@ export type SearchResponse<T> = {
153
288
  distance?: number;
154
289
  /** With `highlight`: matched searchable fields by path; list fields give one string per value. */
155
290
  highlights?: Record<string, string | string[]>;
291
+ /** With `explain`: why the hit matched and what ordered it. */
292
+ explain?: HitExplanation;
156
293
  }[];
157
294
  total: number;
158
295
  /** "gte" when `total` is a lower bound (page count mode). */
@@ -172,12 +309,17 @@ export type SearchResponse<T> = {
172
309
  /** With the index's `didYouMean` setting: the query with the words typo tolerance corrected, when the
173
310
  * results matched some words only by typo ("Showing results for …"), else null. */
174
311
  didYouMean?: DidYouMean | null;
312
+ /** The index's query rules the query fired, in the index's order. Left out when none fired. */
313
+ appliedRules?: AppliedRule[];
314
+ /** When a rule replaced words: the query that was searched. */
315
+ rewrittenQuery?: string;
175
316
  };
176
317
  /** How a product's recommendations are served (the recommendations add-on). */
177
318
  export type RecommendationsRequest = {
178
319
  /** Up to this many products in each list, 1 to 20; 8 by default. */
179
320
  limit?: number;
180
- /** Only picks matching these filters, as in a search (fields configured as filters), such as `{ inStock: true }`. */
321
+ /** Only picks matching these filters, as in a search (fields configured as filters), such as `{ inStock: true }`.
322
+ * Each field's filter takes the same shapes; `$or` is for searches only. */
181
323
  filters?: SearchRequest["filters"];
182
324
  /** The record fields to return; all of them by default. */
183
325
  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.14.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",