@findlane/server 0.18.0 → 0.19.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/dist/index.cjs CHANGED
@@ -232,7 +232,7 @@ class SearchServerClient {
232
232
  await flush();
233
233
  return result;
234
234
  };
235
- const report = (name2, options) => retry(() => request(`/analytics/${name2}${analyticsQuery(options)}`, "GET", undefined, options.signal), options.signal);
235
+ const report = (name, options) => retry(() => request(`/analytics/${name}${analyticsQuery(options)}`, "GET", undefined, options.signal), options.signal);
236
236
  const stagePath = (stageId) => `/staged-replacements/${encodeURIComponent(stageId)}`;
237
237
  const beginReplacement = (stageId, signal) => retry(() => request(stagePath(stageId), "POST", undefined, signal), signal);
238
238
  const uploadReplacementBatch = (stageId, batchId, records, signal) => retry(() => request(`${stagePath(stageId)}/batches/${encodeURIComponent(batchId)}`, "POST", { records }, signal), signal);
@@ -295,16 +295,16 @@ class SearchServerClient {
295
295
  forgotten += (await postEvents({ forget: ids.slice(start, start + 1000) }, signal)).forgotten ?? 0;
296
296
  return { forgotten };
297
297
  },
298
- proxy: async (request2, visitor = {}) => {
299
- if (request2.method !== "POST")
298
+ proxy: async (request, visitor = {}) => {
299
+ if (request.method !== "POST")
300
300
  return jsonResponse(JSON.stringify({ error: "Only POST is allowed" }), 405, { allow: "POST" });
301
301
  const tooLarge = () => jsonResponse(JSON.stringify({ error: "The request is too large" }), 413);
302
- if (Number(request2.headers.get("content-length") ?? 0) > relayedBytes)
302
+ if (Number(request.headers.get("content-length") ?? 0) > relayedBytes)
303
303
  return tooLarge();
304
- const body = await request2.text();
304
+ const body = await request.text();
305
305
  if (new TextEncoder().encode(body).length > relayedBytes)
306
306
  return tooLarge();
307
- const found = { ...requestVisitor(request2), ...visitor };
307
+ const found = { ...requestVisitor(request), ...visitor };
308
308
  if (!found.ip)
309
309
  throw new Error("No visitor IP address in the request's headers; pass it: events.proxy(request, { ip })");
310
310
  return relayEvents(body, { ...found, ip: found.ip });
package/dist/index.d.cts CHANGED
@@ -77,6 +77,18 @@ export type IndexSettings = {
77
77
  normalizationProfile?: "default-v2" | "default-v1" | "icelandic-ascii-v1";
78
78
  /** An object field of lat and lng numbers, such as `_geoloc`, that `near` and `within` search; null for none. */
79
79
  locationField?: string | null;
80
+ /** Shows each value of a field once, such as a product whose colours or sizes are records sharing its ID:
81
+ * the record (or up to `limit`, 1 to 4) that best matches the search. A text or number field with one value;
82
+ * `total` then counts groups. Null for none. */
83
+ distinct?: {
84
+ field: string;
85
+ limit?: number;
86
+ } | null;
87
+ /** Variants listed inside each record, such as a product's flavours: a list of objects at `field`. Each hit
88
+ * then says which one the search matched (`variant`). Null for none. */
89
+ variants?: {
90
+ field: string;
91
+ } | null;
80
92
  /** When saving: types for fields no record has yet, so settings can come before the records. Records'
81
93
  * types win once they have the field. Not returned when reading settings. */
82
94
  fieldTypes?: Record<string, "text" | "number" | "boolean" | "text[]" | "number[]" | "boolean[]" | "location">;
@@ -345,6 +357,9 @@ export type SearchRequest = {
345
357
  within?: GeoBox;
346
358
  /** With the index's `relatedResults` on: false asks for no related results (0.18.0). */
347
359
  related?: boolean;
360
+ /** With the index's `distinct` setting: false shows every record, a number 1 to 4 how many of each group's
361
+ * records. */
362
+ distinct?: boolean | number;
348
363
  };
349
364
  /** A point in degrees: lat from -90 to 90, lng from -180 to 180. */
350
365
  export type GeoPoint = {
@@ -425,8 +440,23 @@ export type SearchResponse<T> = {
425
440
  highlights?: Record<string, string | string[]>;
426
441
  /** With `explain`: why the hit matched and what ordered it. */
427
442
  explain?: HitExplanation;
443
+ /** With the index's `distinct` setting: the hit's group (its value of the group field, null for a record
444
+ * without one) and how many of the group's records matched. */
445
+ group?: {
446
+ value: string | number | null;
447
+ matched: number;
448
+ };
449
+ /** With the index's `variants` setting: which item of the record's variants list the search matched, by
450
+ * its words or by filters on the variants' fields, such as the flavour a shopper typed. Absent when none
451
+ * stands out. */
452
+ variant?: {
453
+ index: number;
454
+ };
428
455
  }[];
456
+ /** The matches; with the index's `distinct` setting, the groups. */
429
457
  total: number;
458
+ /** With `distinct`: the matching records, which `total` counts as groups. */
459
+ totalRecords?: number;
430
460
  /** "gte" when `total` is a lower bound (page count mode). */
431
461
  totalRelation?: "eq" | "gte";
432
462
  hasMore?: boolean;
package/dist/index.d.ts CHANGED
@@ -77,6 +77,18 @@ export type IndexSettings = {
77
77
  normalizationProfile?: "default-v2" | "default-v1" | "icelandic-ascii-v1";
78
78
  /** An object field of lat and lng numbers, such as `_geoloc`, that `near` and `within` search; null for none. */
79
79
  locationField?: string | null;
80
+ /** Shows each value of a field once, such as a product whose colours or sizes are records sharing its ID:
81
+ * the record (or up to `limit`, 1 to 4) that best matches the search. A text or number field with one value;
82
+ * `total` then counts groups. Null for none. */
83
+ distinct?: {
84
+ field: string;
85
+ limit?: number;
86
+ } | null;
87
+ /** Variants listed inside each record, such as a product's flavours: a list of objects at `field`. Each hit
88
+ * then says which one the search matched (`variant`). Null for none. */
89
+ variants?: {
90
+ field: string;
91
+ } | null;
80
92
  /** When saving: types for fields no record has yet, so settings can come before the records. Records'
81
93
  * types win once they have the field. Not returned when reading settings. */
82
94
  fieldTypes?: Record<string, "text" | "number" | "boolean" | "text[]" | "number[]" | "boolean[]" | "location">;
@@ -345,6 +357,9 @@ export type SearchRequest = {
345
357
  within?: GeoBox;
346
358
  /** With the index's `relatedResults` on: false asks for no related results (0.18.0). */
347
359
  related?: boolean;
360
+ /** With the index's `distinct` setting: false shows every record, a number 1 to 4 how many of each group's
361
+ * records. */
362
+ distinct?: boolean | number;
348
363
  };
349
364
  /** A point in degrees: lat from -90 to 90, lng from -180 to 180. */
350
365
  export type GeoPoint = {
@@ -425,8 +440,23 @@ export type SearchResponse<T> = {
425
440
  highlights?: Record<string, string | string[]>;
426
441
  /** With `explain`: why the hit matched and what ordered it. */
427
442
  explain?: HitExplanation;
443
+ /** With the index's `distinct` setting: the hit's group (its value of the group field, null for a record
444
+ * without one) and how many of the group's records matched. */
445
+ group?: {
446
+ value: string | number | null;
447
+ matched: number;
448
+ };
449
+ /** With the index's `variants` setting: which item of the record's variants list the search matched, by
450
+ * its words or by filters on the variants' fields, such as the flavour a shopper typed. Absent when none
451
+ * stands out. */
452
+ variant?: {
453
+ index: number;
454
+ };
428
455
  }[];
456
+ /** The matches; with the index's `distinct` setting, the groups. */
429
457
  total: number;
458
+ /** With `distinct`: the matching records, which `total` counts as groups. */
459
+ totalRecords?: number;
430
460
  /** "gte" when `total` is a lower bound (page count mode). */
431
461
  totalRelation?: "eq" | "gte";
432
462
  hasMore?: boolean;
package/dist/index.js CHANGED
@@ -186,7 +186,7 @@ class SearchServerClient {
186
186
  await flush();
187
187
  return result;
188
188
  };
189
- const report = (name2, options) => retry(() => request(`/analytics/${name2}${analyticsQuery(options)}`, "GET", undefined, options.signal), options.signal);
189
+ const report = (name, options) => retry(() => request(`/analytics/${name}${analyticsQuery(options)}`, "GET", undefined, options.signal), options.signal);
190
190
  const stagePath = (stageId) => `/staged-replacements/${encodeURIComponent(stageId)}`;
191
191
  const beginReplacement = (stageId, signal) => retry(() => request(stagePath(stageId), "POST", undefined, signal), signal);
192
192
  const uploadReplacementBatch = (stageId, batchId, records, signal) => retry(() => request(`${stagePath(stageId)}/batches/${encodeURIComponent(batchId)}`, "POST", { records }, signal), signal);
@@ -249,16 +249,16 @@ class SearchServerClient {
249
249
  forgotten += (await postEvents({ forget: ids.slice(start, start + 1000) }, signal)).forgotten ?? 0;
250
250
  return { forgotten };
251
251
  },
252
- proxy: async (request2, visitor = {}) => {
253
- if (request2.method !== "POST")
252
+ proxy: async (request, visitor = {}) => {
253
+ if (request.method !== "POST")
254
254
  return jsonResponse(JSON.stringify({ error: "Only POST is allowed" }), 405, { allow: "POST" });
255
255
  const tooLarge = () => jsonResponse(JSON.stringify({ error: "The request is too large" }), 413);
256
- if (Number(request2.headers.get("content-length") ?? 0) > relayedBytes)
256
+ if (Number(request.headers.get("content-length") ?? 0) > relayedBytes)
257
257
  return tooLarge();
258
- const body = await request2.text();
258
+ const body = await request.text();
259
259
  if (new TextEncoder().encode(body).length > relayedBytes)
260
260
  return tooLarge();
261
- const found = { ...requestVisitor(request2), ...visitor };
261
+ const found = { ...requestVisitor(request), ...visitor };
262
262
  if (!found.ip)
263
263
  throw new Error("No visitor IP address in the request's headers; pass it: events.proxy(request, { ip })");
264
264
  return relayEvents(body, { ...found, ip: found.ip });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@findlane/server",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",