@findlane/server 0.14.0 → 0.16.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 +12 -0
- package/dist/index.cjs +38 -0
- package/dist/index.d.cts +340 -0
- package/dist/index.d.ts +340 -0
- package/dist/index.js +38 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,7 @@ const results = await products.search({ q }, { visitor: { ip: req.ip, userAgent:
|
|
|
48
48
|
| Method | What it does |
|
|
49
49
|
| --- | --- |
|
|
50
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
|
+
| `querySuggestions({ q, limit? })`, `popularSearches({ limit? })` | Whole searches to offer as a visitor types, `[{ q, typed }]`, and the index's popular searches for an empty box. Not counted as searches. |
|
|
51
52
|
| `upsert(records)` | Add or update up to 1,000 records by `objectID`. |
|
|
52
53
|
| `replaceAll(records)` | Replace the whole catalog in one request (up to 100,000 records). |
|
|
53
54
|
| `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. |
|
|
@@ -55,11 +56,14 @@ const results = await products.search({ q }, { visitor: { ip: req.ip, userAgent:
|
|
|
55
56
|
| `getRecord(objectID)` | A record and its version. |
|
|
56
57
|
| `upsertOne(record, { expectedVersion })`, `deleteOne(objectID, { expectedVersion })` | Write only if the record is still at `expectedVersion` (0 for one that never existed); retried with one idempotency key. |
|
|
57
58
|
| `getSettings()`, `replaceSettings(settings)`, `waitForSettings()` | Read and change searchable fields, facets, filters, sorts, custom ranking, synonyms, languages, typo tolerance, and text normalization. |
|
|
59
|
+
| `compareSettings(settings, { queries, searched }?)` | Compare settings with the index's own without saving them: its search tests and most searched queries, each searched with both. Says which tests would fail and whose results change. |
|
|
60
|
+
| `getSearchTests()`, `setSearchTests(tests)` | Searches that must keep working, saved apart from the settings, such as a record in the top 3 or at least 100 results. `compareSettings` checks them. |
|
|
58
61
|
| `getIndexingStatus()`, `waitUntilIndexed(revision)` | Writes return an accepted revision and are searchable when accepted, so `waitUntilIndexed` returns at once. |
|
|
59
62
|
| `getPublicSearchAccess()`, `setPublicSearchPolicy(policy)`, `usePublicIndexDefaults()` | Restrict what browser search may read and query. |
|
|
60
63
|
| `events.send(events, { visitorId, visitId }?)`, `events.importOrders(orders)`, `events.cancelOrder(orderID)` | Orders and other events for the index's Events page and Bought together; the key needs the `events` or `write` scope. Your own anonymous visitor IDs link a shopper's events, when the index takes them. |
|
|
61
64
|
| `events.forget(ids)` | Deletes the events sent with these visitor or visit IDs, within a week. |
|
|
62
65
|
| `events.proxy(request)`, `events.relay(body, visitor)` | A route on your own site for the browser SDK's events (its `eventsUrl`), which ad blockers let through. See [Events](https://findlane.dev/docs/events#through-your-own-domain). |
|
|
66
|
+
| `analytics.searches()`, `.queries({ list, limit })`, `.filters()`, `.events()`, `.products()`, `.productCounts()`, `.product(objectID)` | The index's analytics, for your own admin: searches over time, top queries with their clicks, queries without results or clicks, the filters used, events, and each product's views, add-to-carts and orders. The key needs the `analytics` scope, and works only from a server. See [Analytics](https://findlane.dev/docs/api#analytics). |
|
|
63
67
|
|
|
64
68
|
### Settings
|
|
65
69
|
|
|
@@ -72,6 +76,14 @@ const results = await products.search({ q }, { visitor: { ip: req.ip, userAgent:
|
|
|
72
76
|
|
|
73
77
|
A large `searchable` or `rebuild` change continues in the background, with its progress in `rebuild` (also returned by `getSettings`). Searches use the previous settings until it finishes, and `waitForSettings()` waits for it.
|
|
74
78
|
|
|
79
|
+
Before saving a change to rules, synonyms, typo tolerance, or ranking, `compareSettings` shows what it does, and `{}` runs the search tests with the current settings:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const comparison = await products.compareSettings({ rules: nextRules })
|
|
83
|
+
const failing = comparison.tests.filter((test) => test.before && !test.after)
|
|
84
|
+
if (!failing.length) await products.replaceSettings({ ...(await products.getSettings()), rules: nextRules })
|
|
85
|
+
```
|
|
86
|
+
|
|
75
87
|
### Browser search
|
|
76
88
|
|
|
77
89
|
Only upload fields you intend to make public if browser search is enabled. `getPublicSearchAccess()` shows the current mode. `setPublicSearchPolicy({ returnFields, searchFields, filterFields, facetFields, sortIds, defaultSort })` restricts the index at the API, and `usePublicIndexDefaults()` removes the restrictions. Only a workspace owner can change allowed browser origins, in the dashboard.
|
package/dist/index.cjs
CHANGED
|
@@ -67,6 +67,16 @@ class StagedReplacementError extends Error {
|
|
|
67
67
|
this.name = "StagedReplacementError";
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
function analyticsQuery(options) {
|
|
71
|
+
const params = new URLSearchParams;
|
|
72
|
+
for (const name of ["range", "from", "to", "interval", "source", "list", "limit"]) {
|
|
73
|
+
const value = options[name];
|
|
74
|
+
if (value !== undefined)
|
|
75
|
+
params.set(name, value instanceof Date ? value.toISOString() : String(value));
|
|
76
|
+
}
|
|
77
|
+
const query = params.toString();
|
|
78
|
+
return query ? `?${query}` : "";
|
|
79
|
+
}
|
|
70
80
|
var eventsPerRequest = 200;
|
|
71
81
|
var requestOptions = (value) => value === undefined ? {} : ("aborted" in value) ? { signal: value } : value;
|
|
72
82
|
var relayedBytes = 256 * 1024;
|
|
@@ -222,6 +232,7 @@ class SearchServerClient {
|
|
|
222
232
|
await flush();
|
|
223
233
|
return result;
|
|
224
234
|
};
|
|
235
|
+
const report = (name2, options) => retry(() => request(`/analytics/${name2}${analyticsQuery(options)}`, "GET", undefined, options.signal), options.signal);
|
|
225
236
|
const stagePath = (stageId) => `/staged-replacements/${encodeURIComponent(stageId)}`;
|
|
226
237
|
const beginReplacement = (stageId, signal) => retry(() => request(stagePath(stageId), "POST", undefined, signal), signal);
|
|
227
238
|
const uploadReplacementBatch = (stageId, batchId, records, signal) => retry(() => request(`${stagePath(stageId)}/batches/${encodeURIComponent(batchId)}`, "POST", { records }, signal), signal);
|
|
@@ -242,6 +253,14 @@ class SearchServerClient {
|
|
|
242
253
|
const { signal, visitor } = requestOptions(options);
|
|
243
254
|
return (await request("/suggest", "POST", query, signal, visitorHeaders(visitor))).didYouMean;
|
|
244
255
|
},
|
|
256
|
+
querySuggestions: async (query, options) => {
|
|
257
|
+
const { signal } = requestOptions(options);
|
|
258
|
+
return (await request("/query-suggestions", "POST", query, signal)).suggestions;
|
|
259
|
+
},
|
|
260
|
+
popularSearches: async (options = {}, requestOptionsOrSignal) => {
|
|
261
|
+
const { signal } = requestOptions(requestOptionsOrSignal);
|
|
262
|
+
return (await request("/query-suggestions", "POST", { q: "", limit: options.limit ?? 6 }, signal)).suggestions;
|
|
263
|
+
},
|
|
245
264
|
recommendations: (objectID, options = {}, requestOptionsOrSignal) => {
|
|
246
265
|
const { signal, visitor } = requestOptions(requestOptionsOrSignal);
|
|
247
266
|
return request("/recommendations", "POST", { objectID, ...options }, signal, visitorHeaders(visitor));
|
|
@@ -279,6 +298,15 @@ class SearchServerClient {
|
|
|
279
298
|
},
|
|
280
299
|
relay: (body, visitor, signal) => relayEvents(body, visitor, signal)
|
|
281
300
|
},
|
|
301
|
+
analytics: {
|
|
302
|
+
searches: (options = {}) => report("searches", options),
|
|
303
|
+
queries: (options = {}) => report("queries", options),
|
|
304
|
+
filters: (options = {}) => report("filters", options),
|
|
305
|
+
events: (options = {}) => report("events", options),
|
|
306
|
+
products: (options = {}) => report("products", options),
|
|
307
|
+
productCounts: (options = {}) => report("product-counts", options),
|
|
308
|
+
product: (objectID, options = {}) => report(`products/${encodeURIComponent(objectID)}`, options)
|
|
309
|
+
},
|
|
282
310
|
upsert: (records, signal) => request("/records", "POST", { records }, signal),
|
|
283
311
|
getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
|
|
284
312
|
upsertOne: (record, options) => writeOne("upsert", record.objectID, record, options),
|
|
@@ -337,6 +365,16 @@ class SearchServerClient {
|
|
|
337
365
|
const { rebuild: _rebuild, rebuilt: _rebuilt, applied: _applied, ...body } = settings;
|
|
338
366
|
return request("/config", "PUT", body, signal);
|
|
339
367
|
},
|
|
368
|
+
compareSettings: (settings, options = {}) => {
|
|
369
|
+
const { rebuild: _rebuild, rebuilt: _rebuilt, applied: _applied, ...body } = settings;
|
|
370
|
+
return request("/compare", "POST", {
|
|
371
|
+
settings: body,
|
|
372
|
+
...options.queries ? { queries: options.queries } : {},
|
|
373
|
+
...options.searched === false ? { searched: false } : {}
|
|
374
|
+
}, options.signal);
|
|
375
|
+
},
|
|
376
|
+
getSearchTests: async (signal) => (await request("/tests", "GET", undefined, signal)).tests,
|
|
377
|
+
setSearchTests: async (tests, signal) => (await request("/tests", "PUT", { tests }, signal)).tests,
|
|
340
378
|
waitForSettings: async (options = {}) => {
|
|
341
379
|
const deadline = Date.now() + (options.timeoutMs ?? 30 * 60000);
|
|
342
380
|
while (true) {
|
package/dist/index.d.cts
CHANGED
|
@@ -63,6 +63,9 @@ export type IndexSettings = {
|
|
|
63
63
|
* Exact matches rank first. Searches use it once the index's word list is built, a minute or so after it's
|
|
64
64
|
* turned on. Off by default. */
|
|
65
65
|
compoundWords?: boolean;
|
|
66
|
+
/** Query suggestions: whole searches offered as shoppers type, and popular searches for an empty box,
|
|
67
|
+
* from what shoppers search and the catalog. On by default. Changes apply within a few minutes. */
|
|
68
|
+
querySuggestions?: QuerySuggestionSettings;
|
|
66
69
|
normalizationProfile?: "default-v2" | "default-v1" | "icelandic-ascii-v1";
|
|
67
70
|
/** An object field of lat and lng numbers, such as `_geoloc`, that `near` and `within` search; null for none. */
|
|
68
71
|
locationField?: string | null;
|
|
@@ -71,6 +74,33 @@ export type IndexSettings = {
|
|
|
71
74
|
fieldTypes?: Record<string, "text" | "number" | "boolean" | "text[]" | "number[]" | "boolean[]" | "location">;
|
|
72
75
|
[key: string]: unknown;
|
|
73
76
|
};
|
|
77
|
+
/** An index's query suggestion settings. When saving, give only those to change. */
|
|
78
|
+
export type QuerySuggestionSettings = {
|
|
79
|
+
/** false: no suggestions. */
|
|
80
|
+
enabled?: boolean;
|
|
81
|
+
/** false: no popular searches for an empty box; typed suggestions still use them. */
|
|
82
|
+
popularSearches?: boolean;
|
|
83
|
+
/** Popular searches an empty box shows, 1 to 10; 6 by default. */
|
|
84
|
+
popularCount?: number;
|
|
85
|
+
/** A shoppers' search becomes a suggestion once searched this many times (3 by default), on this many
|
|
86
|
+
* different days (2 by default). */
|
|
87
|
+
minSearches?: number;
|
|
88
|
+
minDays?: number;
|
|
89
|
+
/** The facet and searchable fields whose values are suggested, up to 20; null (the default) chooses
|
|
90
|
+
* readable ones from the records, leaving out slugs, sizes and long text. */
|
|
91
|
+
fields?: string[] | null;
|
|
92
|
+
/** Searches to suggest, up to 1,000; to show first, in the empty box too, up to 20; and never to show,
|
|
93
|
+
* nor anything with their words in a row, up to 1,000. One to six words each. */
|
|
94
|
+
added?: string[];
|
|
95
|
+
pinned?: string[];
|
|
96
|
+
hidden?: string[];
|
|
97
|
+
};
|
|
98
|
+
/** A whole search to offer as a shopper types, or in an empty box. `typed`: where what was typed is in
|
|
99
|
+
* `q`, as UTF-16 offsets (end exclusive); popular searches have none. */
|
|
100
|
+
export type QuerySuggestion = {
|
|
101
|
+
q: string;
|
|
102
|
+
typed?: [number, number];
|
|
103
|
+
};
|
|
74
104
|
/** When a query is, starts with, or contains one of a rule's phrases, the rule's `then` applies to that
|
|
75
105
|
* search. Phrases are compared as synonyms are: without case, accents, or the index languages' stop words,
|
|
76
106
|
* and with their plurals and other spellings, so a rule for "balloon" also fires for "balloons". */
|
|
@@ -136,6 +166,101 @@ export type SavedSettings = IndexSettings & {
|
|
|
136
166
|
rebuilt: boolean;
|
|
137
167
|
rebuild: SettingsRebuild | null;
|
|
138
168
|
};
|
|
169
|
+
/** What a search test expects of its query's results: a record among the first `inTop` results (1 to
|
|
170
|
+
* 20), a record not among the first `notInTop`, or at least `minResults` results (1 to 10,000). */
|
|
171
|
+
export type TestExpectation = {
|
|
172
|
+
objectID: string;
|
|
173
|
+
inTop: number;
|
|
174
|
+
} | {
|
|
175
|
+
objectID: string;
|
|
176
|
+
notInTop: number;
|
|
177
|
+
} | {
|
|
178
|
+
minResults: number;
|
|
179
|
+
};
|
|
180
|
+
/** A search that must keep working, saved apart from the settings, which `compareSettings` checks: a query
|
|
181
|
+
* (empty browses), its filters on facet, filter or sort fields, and what its results should hold. IDs are
|
|
182
|
+
* 1 to 64 letters, digits, `_`, `.`, `:`, or `-`, unique in the index; notes up to 200 characters. */
|
|
183
|
+
export type SearchTest = {
|
|
184
|
+
id: string;
|
|
185
|
+
q: string;
|
|
186
|
+
filters?: SearchRequest["filters"];
|
|
187
|
+
expect: TestExpectation;
|
|
188
|
+
note?: string;
|
|
189
|
+
};
|
|
190
|
+
/** How many compared queries changed in a way, and their searches in the last 30 days. */
|
|
191
|
+
export type CompareCount = {
|
|
192
|
+
queries: number;
|
|
193
|
+
searches: number;
|
|
194
|
+
};
|
|
195
|
+
export type ComparedHit = {
|
|
196
|
+
objectID: string;
|
|
197
|
+
title: string;
|
|
198
|
+
};
|
|
199
|
+
/** Proposed settings compared with the index's own, without saving them (`compareSettings`). */
|
|
200
|
+
export type SettingsComparison = {
|
|
201
|
+
/** The queries compared, and their searches in the last 30 days. */
|
|
202
|
+
compared: number;
|
|
203
|
+
searches: number;
|
|
204
|
+
/** Time (20 seconds) ran out before every query was compared. */
|
|
205
|
+
truncated: boolean;
|
|
206
|
+
summary: {
|
|
207
|
+
topChanged: CompareCount;
|
|
208
|
+
moreResults: CompareCount;
|
|
209
|
+
fewerResults: CompareCount;
|
|
210
|
+
nowNothing: CompareCount;
|
|
211
|
+
nowSomething: CompareCount;
|
|
212
|
+
};
|
|
213
|
+
/** Every search test: whether it passes with the current settings (`before`) and the proposed ones
|
|
214
|
+
* (`after`), or `skipped` when time ran out first, or the search's `error`. */
|
|
215
|
+
tests: (Omit<SearchTest, "note"> & {
|
|
216
|
+
before?: boolean;
|
|
217
|
+
after?: boolean;
|
|
218
|
+
skipped?: true;
|
|
219
|
+
error?: string;
|
|
220
|
+
})[];
|
|
221
|
+
/** Up to 50 queries whose top 10 or total changed, the tests' queries first, then the most searched: the
|
|
222
|
+
* totals and first 5 results with each, and the results that left, joined, and moved within the top 10.
|
|
223
|
+
* Places are in the first 20 results; null is outside them. */
|
|
224
|
+
changes: {
|
|
225
|
+
q: string;
|
|
226
|
+
filters?: SearchRequest["filters"];
|
|
227
|
+
searches: number;
|
|
228
|
+
before: {
|
|
229
|
+
total: number;
|
|
230
|
+
top: ComparedHit[];
|
|
231
|
+
};
|
|
232
|
+
after: {
|
|
233
|
+
total: number;
|
|
234
|
+
top: ComparedHit[];
|
|
235
|
+
};
|
|
236
|
+
gone: (ComparedHit & {
|
|
237
|
+
from: number;
|
|
238
|
+
to: number | null;
|
|
239
|
+
})[];
|
|
240
|
+
added: (ComparedHit & {
|
|
241
|
+
from: number | null;
|
|
242
|
+
to: number;
|
|
243
|
+
})[];
|
|
244
|
+
moved: (ComparedHit & {
|
|
245
|
+
from: number;
|
|
246
|
+
to: number;
|
|
247
|
+
})[];
|
|
248
|
+
}[];
|
|
249
|
+
errors: {
|
|
250
|
+
q: string;
|
|
251
|
+
error: string;
|
|
252
|
+
}[];
|
|
253
|
+
};
|
|
254
|
+
export type CompareOptions = {
|
|
255
|
+
/** More queries to compare, such as those a change is for: text, or `{ q, filters }`. Up to 200. */
|
|
256
|
+
queries?: (string | {
|
|
257
|
+
q: string;
|
|
258
|
+
filters?: SearchRequest["filters"];
|
|
259
|
+
})[];
|
|
260
|
+
/** false leaves out the index's 100 most searched queries of the last 30 days. */
|
|
261
|
+
searched?: boolean;
|
|
262
|
+
signal?: AbortSignal;
|
|
263
|
+
};
|
|
139
264
|
export type PublicSearchPolicy = {
|
|
140
265
|
returnFields: string[];
|
|
141
266
|
searchFields: string[];
|
|
@@ -474,6 +599,166 @@ export type EventOptions = {
|
|
|
474
599
|
visitorId?: string;
|
|
475
600
|
visitId?: string;
|
|
476
601
|
};
|
|
602
|
+
/** An analytics window, in UTC: `range` (7d by default), or `from` and `to`. A date as `from` is the start of
|
|
603
|
+
* that day, and as `to` includes the whole day; `to` is now by default. Analytics go back 90 days: a window
|
|
604
|
+
* starting earlier starts then, and the report's `from` says so. */
|
|
605
|
+
export type AnalyticsOptions = {
|
|
606
|
+
range?: "24h" | "7d" | "30d" | "90d";
|
|
607
|
+
from?: string | Date;
|
|
608
|
+
to?: string | Date;
|
|
609
|
+
/** Searches from browsers (browser search), from servers (API keys), or both, by default. */
|
|
610
|
+
source?: "all" | "browser" | "server";
|
|
611
|
+
signal?: AbortSignal;
|
|
612
|
+
};
|
|
613
|
+
/** What every analytics report starts with. `sampled`: the counts are estimates, because Analytics Engine,
|
|
614
|
+
* which keeps them, sampled some of them; totals and lists stay close, small counts less so. */
|
|
615
|
+
export type AnalyticsReport = {
|
|
616
|
+
index: string;
|
|
617
|
+
from: string;
|
|
618
|
+
to: string;
|
|
619
|
+
source: "all" | "browser" | "server";
|
|
620
|
+
sampled: boolean;
|
|
621
|
+
};
|
|
622
|
+
export type SearchTotals = {
|
|
623
|
+
searches: number;
|
|
624
|
+
/** Searches whose query and result count were recorded: the base for `noResultRate`. */
|
|
625
|
+
tracked: number;
|
|
626
|
+
noResults: number;
|
|
627
|
+
/** `noResults` of `tracked`, 0 to 1; null without tracked searches. */
|
|
628
|
+
noResultRate: number | null;
|
|
629
|
+
browser: number;
|
|
630
|
+
server: number;
|
|
631
|
+
uniqueQueries: number;
|
|
632
|
+
/** Response times at the API, in ms. */
|
|
633
|
+
medianMs: number;
|
|
634
|
+
p95Ms: number;
|
|
635
|
+
};
|
|
636
|
+
export type SearchesReport = AnalyticsReport & {
|
|
637
|
+
interval: "hour" | "day";
|
|
638
|
+
totals: SearchTotals;
|
|
639
|
+
/** The same span just before `from`. */
|
|
640
|
+
previous: SearchTotals & {
|
|
641
|
+
from: string;
|
|
642
|
+
to: string;
|
|
643
|
+
};
|
|
644
|
+
/** Every hour or day of the window, the first one starting at or before `from`. */
|
|
645
|
+
series: {
|
|
646
|
+
t: string;
|
|
647
|
+
searches: number;
|
|
648
|
+
tracked: number;
|
|
649
|
+
noResults: number;
|
|
650
|
+
}[];
|
|
651
|
+
};
|
|
652
|
+
/** A top query: its clicks on results and their average place (from 1), or null when clicks aren't available. */
|
|
653
|
+
export type TopQuery = {
|
|
654
|
+
query: string;
|
|
655
|
+
searches: number;
|
|
656
|
+
noResults: number;
|
|
657
|
+
clicks: number | null;
|
|
658
|
+
averagePosition: number | null;
|
|
659
|
+
};
|
|
660
|
+
export type QueryCount = {
|
|
661
|
+
query: string;
|
|
662
|
+
searches: number;
|
|
663
|
+
};
|
|
664
|
+
export type QueryLists = {
|
|
665
|
+
top: TopQuery;
|
|
666
|
+
noResults: QueryCount;
|
|
667
|
+
noClicks: QueryCount;
|
|
668
|
+
};
|
|
669
|
+
export type QueriesReport<L extends keyof QueryLists = "top"> = AnalyticsReport & {
|
|
670
|
+
list: L;
|
|
671
|
+
queries: QueryLists[L][];
|
|
672
|
+
};
|
|
673
|
+
export type FiltersReport = AnalyticsReport & {
|
|
674
|
+
/** Searches whose filters and sort were recorded. */
|
|
675
|
+
searches: number;
|
|
676
|
+
/** Each filter field with the searches that filtered on it, up to 100. */
|
|
677
|
+
filters: {
|
|
678
|
+
field: string;
|
|
679
|
+
searches: number;
|
|
680
|
+
}[];
|
|
681
|
+
/** Each sort with its searches; `sort` is null for searches that named none, which use the index's default. */
|
|
682
|
+
sorts: {
|
|
683
|
+
sort: string | null;
|
|
684
|
+
searches: number;
|
|
685
|
+
}[];
|
|
686
|
+
};
|
|
687
|
+
export type EventType = "view" | "click" | "addToCart" | "purchase" | "cancel";
|
|
688
|
+
export type EventsReport = AnalyticsReport & {
|
|
689
|
+
interval: "hour" | "day";
|
|
690
|
+
/** Accepted events of each type, by source. */
|
|
691
|
+
types: {
|
|
692
|
+
type: EventType;
|
|
693
|
+
browser: number;
|
|
694
|
+
server: number;
|
|
695
|
+
}[];
|
|
696
|
+
/** Orders accepted, and of them those stored, which Bought together and the order counts use. */
|
|
697
|
+
orders: {
|
|
698
|
+
accepted: number;
|
|
699
|
+
stored: number;
|
|
700
|
+
};
|
|
701
|
+
/** Events from crawlers and scripts, which are dropped. */
|
|
702
|
+
bots: number;
|
|
703
|
+
/** Refused events, by why. */
|
|
704
|
+
refused: {
|
|
705
|
+
reason: string;
|
|
706
|
+
events: number;
|
|
707
|
+
}[];
|
|
708
|
+
series: ({
|
|
709
|
+
t: string;
|
|
710
|
+
} & Record<EventType, number>)[];
|
|
711
|
+
};
|
|
712
|
+
/** A product's counts. `viewers` is how many visitor IDs viewed it, or null when the index's views carry
|
|
713
|
+
* none. `searchClicks` is clicks on it outside recommendations: in search results and product lists. */
|
|
714
|
+
export type ProductCount = {
|
|
715
|
+
objectID: string;
|
|
716
|
+
views: number;
|
|
717
|
+
viewers: number | null;
|
|
718
|
+
searchClicks: number;
|
|
719
|
+
carts: number;
|
|
720
|
+
orders: number;
|
|
721
|
+
};
|
|
722
|
+
export type ListedProduct = ProductCount & {
|
|
723
|
+
title: string;
|
|
724
|
+
image: string | null;
|
|
725
|
+
};
|
|
726
|
+
/** Product reports are exact and about 2 minutes behind; `computedAt` is when they were counted, up to 5
|
|
727
|
+
* minutes ago. `orders` is the orders stored in the window. */
|
|
728
|
+
export type ProductListsReport = AnalyticsReport & {
|
|
729
|
+
computedAt: string;
|
|
730
|
+
orders: number;
|
|
731
|
+
/** The most viewed, most added to carts, and most ordered products, by views. */
|
|
732
|
+
products: ListedProduct[];
|
|
733
|
+
/** Products with 50 or more views, the fewest orders per view first; null without orders. */
|
|
734
|
+
viewedNotBought: ListedProduct[] | null;
|
|
735
|
+
};
|
|
736
|
+
export type ProductCountsReport = AnalyticsReport & {
|
|
737
|
+
computedAt: string;
|
|
738
|
+
orders: number;
|
|
739
|
+
/** false when more than 50,000 products had events, and only the most viewed are listed. */
|
|
740
|
+
complete: boolean;
|
|
741
|
+
products: ProductCount[];
|
|
742
|
+
};
|
|
743
|
+
export type ProductReport = AnalyticsReport & {
|
|
744
|
+
objectID: string;
|
|
745
|
+
interval: "hour" | "day";
|
|
746
|
+
totals: Omit<ProductCount, "objectID">;
|
|
747
|
+
series: ({
|
|
748
|
+
t: string;
|
|
749
|
+
} & Omit<ProductCount, "objectID">)[];
|
|
750
|
+
/** The queries whose results it was clicked in, the most clicks first, up to 20. */
|
|
751
|
+
searchedFor: {
|
|
752
|
+
query: string;
|
|
753
|
+
clicks: number;
|
|
754
|
+
}[];
|
|
755
|
+
/** Clicks on it in each recommendation list. */
|
|
756
|
+
recommendationClicks: {
|
|
757
|
+
goesWith: number;
|
|
758
|
+
similar: number;
|
|
759
|
+
together: number;
|
|
760
|
+
};
|
|
761
|
+
};
|
|
477
762
|
export declare class FindlaneError extends Error {
|
|
478
763
|
status: number;
|
|
479
764
|
constructor(status: number, message: string);
|
|
@@ -503,6 +788,18 @@ export declare class SearchServerClient {
|
|
|
503
788
|
* same request (its q, filters, and fields matter). Needs the `didYouMean` setting; not counted as
|
|
504
789
|
* a search. */
|
|
505
790
|
suggest: (query: SearchRequest, options?: AbortSignal | RequestOptions) => Promise<DidYouMean | null>;
|
|
791
|
+
/** Query suggestions: whole searches to offer as a shopper types `q`, the best first; with `q` empty, the
|
|
792
|
+
* index's popular searches. The API key needs the `search` scope; not counted as searches. `limit`: 1 to
|
|
793
|
+
* 10, 5 by default. */
|
|
794
|
+
querySuggestions: (query: {
|
|
795
|
+
q: string;
|
|
796
|
+
limit?: number;
|
|
797
|
+
}, options?: AbortSignal | RequestOptions) => Promise<QuerySuggestion[]>;
|
|
798
|
+
/** The index's popular searches, for an empty search box, with the owner's pinned searches first;
|
|
799
|
+
* empty until the index has enough searches. The list changes daily. */
|
|
800
|
+
popularSearches: (options?: {
|
|
801
|
+
limit?: number;
|
|
802
|
+
}, requestOptionsOrSignal?: AbortSignal | RequestOptions) => Promise<QuerySuggestion[]>;
|
|
506
803
|
/** A product's recommendations (the recommendations add-on): products that go with it, and similar products.
|
|
507
804
|
* The API key needs the `search` scope; not counted as a search. */
|
|
508
805
|
recommendations: <R = T>(objectID: string, options?: RecommendationsRequest, requestOptionsOrSignal?: AbortSignal | RequestOptions) => Promise<RecommendationsResponse<R>>;
|
|
@@ -532,6 +829,39 @@ export declare class SearchServerClient {
|
|
|
532
829
|
* and its visitor. Answers with the API's response. */
|
|
533
830
|
relay: (body: string, visitor: RelayedVisitor, signal?: AbortSignal) => Promise<Response>;
|
|
534
831
|
};
|
|
832
|
+
/** The index's search analytics. The API key needs the `analytics` scope, and works only from a server:
|
|
833
|
+
* requests from web pages are refused. */
|
|
834
|
+
analytics: {
|
|
835
|
+
/** Searches, searches without results and response times, beside the same span before, and a series per
|
|
836
|
+
* hour (up to 7 days, by default) or day. */
|
|
837
|
+
searches: (options?: AnalyticsOptions & {
|
|
838
|
+
interval?: "hour" | "day";
|
|
839
|
+
}) => Promise<SearchesReport>;
|
|
840
|
+
/** Queries, the most searched first: `top` (by default) with their clicks, `noResults`, or `noClicks`, which
|
|
841
|
+
* found something nobody clicked. `limit` is 1 to 1,000, 100 by default. */
|
|
842
|
+
queries: <L extends keyof QueryLists = "top">(options?: AnalyticsOptions & {
|
|
843
|
+
list?: L;
|
|
844
|
+
limit?: number;
|
|
845
|
+
}) => Promise<QueriesReport<L>>;
|
|
846
|
+
/** The filter fields and sorts searches used. */
|
|
847
|
+
filters: (options?: AnalyticsOptions) => Promise<FiltersReport>;
|
|
848
|
+
/** Events of each type over time and by source, orders stored, and refused and bot events. */
|
|
849
|
+
events: (options?: AnalyticsOptions & {
|
|
850
|
+
interval?: "hour" | "day";
|
|
851
|
+
}) => Promise<EventsReport>;
|
|
852
|
+
/** The most viewed, most added to carts and most ordered products, with titles and images, and those
|
|
853
|
+
* viewed but rarely bought. Exact, from every event kept (13 months, from 9 October 2026). */
|
|
854
|
+
products: (options?: AnalyticsOptions) => Promise<ProductListsReport>;
|
|
855
|
+
/** Every product with events or orders in the window, the most viewed first, up to 50,000: views,
|
|
856
|
+
* viewers, clicks, add-to-carts and orders. Exact, like `products`. */
|
|
857
|
+
productCounts: (options?: AnalyticsOptions) => Promise<ProductCountsReport>;
|
|
858
|
+
/** One product's views, viewers, clicks, add-to-carts and orders per hour or day, the queries it was
|
|
859
|
+
* clicked from, and its clicks in recommendations. Fast, for a product page in an admin: views and
|
|
860
|
+
* clicks are estimates when `sampled`, and an add-to-cart of several products counts toward the first. */
|
|
861
|
+
product: (objectID: string, options?: AnalyticsOptions & {
|
|
862
|
+
interval?: "hour" | "day";
|
|
863
|
+
}) => Promise<ProductReport>;
|
|
864
|
+
};
|
|
535
865
|
upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
|
|
536
866
|
getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
|
|
537
867
|
upsertOne: (record: T, options: ConditionalWriteOptions) => Promise<WriteReceipt>;
|
|
@@ -545,6 +875,16 @@ export declare class SearchServerClient {
|
|
|
545
875
|
delete: (objectID: string, signal?: AbortSignal) => Promise<WriteReceipt>;
|
|
546
876
|
getSettings: (signal?: AbortSignal) => Promise<CurrentSettings>;
|
|
547
877
|
replaceSettings: (settings: IndexSettings, signal?: AbortSignal) => Promise<SavedSettings>;
|
|
878
|
+
/** Compares settings with the index's own without saving them: its search tests, `queries`, and its 100
|
|
879
|
+
* most searched queries of the last 30 days, each searched with both, for up to 20 seconds. Takes the
|
|
880
|
+
* settings `replaceSettings` does, or only those that change; `{}` runs the tests with the current
|
|
881
|
+
* settings. A change to what the index stores (new searchable, facet, filter, sort or location fields,
|
|
882
|
+
* or normalization) throws a 409: save it first. Not counted as searches. */
|
|
883
|
+
compareSettings: (settings: Partial<IndexSettings>, options?: CompareOptions) => Promise<SettingsComparison>;
|
|
884
|
+
/** The index's search tests. */
|
|
885
|
+
getSearchTests: (signal?: AbortSignal) => Promise<SearchTest[]>;
|
|
886
|
+
/** Replaces the index's search tests, up to 200; `[]` deletes them. */
|
|
887
|
+
setSearchTests: (tests: SearchTest[], signal?: AbortSignal) => Promise<SearchTest[]>;
|
|
548
888
|
/** Waits until no settings rebuild is running; throws if it failed. */
|
|
549
889
|
waitForSettings: (options?: {
|
|
550
890
|
timeoutMs?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -63,6 +63,9 @@ export type IndexSettings = {
|
|
|
63
63
|
* Exact matches rank first. Searches use it once the index's word list is built, a minute or so after it's
|
|
64
64
|
* turned on. Off by default. */
|
|
65
65
|
compoundWords?: boolean;
|
|
66
|
+
/** Query suggestions: whole searches offered as shoppers type, and popular searches for an empty box,
|
|
67
|
+
* from what shoppers search and the catalog. On by default. Changes apply within a few minutes. */
|
|
68
|
+
querySuggestions?: QuerySuggestionSettings;
|
|
66
69
|
normalizationProfile?: "default-v2" | "default-v1" | "icelandic-ascii-v1";
|
|
67
70
|
/** An object field of lat and lng numbers, such as `_geoloc`, that `near` and `within` search; null for none. */
|
|
68
71
|
locationField?: string | null;
|
|
@@ -71,6 +74,33 @@ export type IndexSettings = {
|
|
|
71
74
|
fieldTypes?: Record<string, "text" | "number" | "boolean" | "text[]" | "number[]" | "boolean[]" | "location">;
|
|
72
75
|
[key: string]: unknown;
|
|
73
76
|
};
|
|
77
|
+
/** An index's query suggestion settings. When saving, give only those to change. */
|
|
78
|
+
export type QuerySuggestionSettings = {
|
|
79
|
+
/** false: no suggestions. */
|
|
80
|
+
enabled?: boolean;
|
|
81
|
+
/** false: no popular searches for an empty box; typed suggestions still use them. */
|
|
82
|
+
popularSearches?: boolean;
|
|
83
|
+
/** Popular searches an empty box shows, 1 to 10; 6 by default. */
|
|
84
|
+
popularCount?: number;
|
|
85
|
+
/** A shoppers' search becomes a suggestion once searched this many times (3 by default), on this many
|
|
86
|
+
* different days (2 by default). */
|
|
87
|
+
minSearches?: number;
|
|
88
|
+
minDays?: number;
|
|
89
|
+
/** The facet and searchable fields whose values are suggested, up to 20; null (the default) chooses
|
|
90
|
+
* readable ones from the records, leaving out slugs, sizes and long text. */
|
|
91
|
+
fields?: string[] | null;
|
|
92
|
+
/** Searches to suggest, up to 1,000; to show first, in the empty box too, up to 20; and never to show,
|
|
93
|
+
* nor anything with their words in a row, up to 1,000. One to six words each. */
|
|
94
|
+
added?: string[];
|
|
95
|
+
pinned?: string[];
|
|
96
|
+
hidden?: string[];
|
|
97
|
+
};
|
|
98
|
+
/** A whole search to offer as a shopper types, or in an empty box. `typed`: where what was typed is in
|
|
99
|
+
* `q`, as UTF-16 offsets (end exclusive); popular searches have none. */
|
|
100
|
+
export type QuerySuggestion = {
|
|
101
|
+
q: string;
|
|
102
|
+
typed?: [number, number];
|
|
103
|
+
};
|
|
74
104
|
/** When a query is, starts with, or contains one of a rule's phrases, the rule's `then` applies to that
|
|
75
105
|
* search. Phrases are compared as synonyms are: without case, accents, or the index languages' stop words,
|
|
76
106
|
* and with their plurals and other spellings, so a rule for "balloon" also fires for "balloons". */
|
|
@@ -136,6 +166,101 @@ export type SavedSettings = IndexSettings & {
|
|
|
136
166
|
rebuilt: boolean;
|
|
137
167
|
rebuild: SettingsRebuild | null;
|
|
138
168
|
};
|
|
169
|
+
/** What a search test expects of its query's results: a record among the first `inTop` results (1 to
|
|
170
|
+
* 20), a record not among the first `notInTop`, or at least `minResults` results (1 to 10,000). */
|
|
171
|
+
export type TestExpectation = {
|
|
172
|
+
objectID: string;
|
|
173
|
+
inTop: number;
|
|
174
|
+
} | {
|
|
175
|
+
objectID: string;
|
|
176
|
+
notInTop: number;
|
|
177
|
+
} | {
|
|
178
|
+
minResults: number;
|
|
179
|
+
};
|
|
180
|
+
/** A search that must keep working, saved apart from the settings, which `compareSettings` checks: a query
|
|
181
|
+
* (empty browses), its filters on facet, filter or sort fields, and what its results should hold. IDs are
|
|
182
|
+
* 1 to 64 letters, digits, `_`, `.`, `:`, or `-`, unique in the index; notes up to 200 characters. */
|
|
183
|
+
export type SearchTest = {
|
|
184
|
+
id: string;
|
|
185
|
+
q: string;
|
|
186
|
+
filters?: SearchRequest["filters"];
|
|
187
|
+
expect: TestExpectation;
|
|
188
|
+
note?: string;
|
|
189
|
+
};
|
|
190
|
+
/** How many compared queries changed in a way, and their searches in the last 30 days. */
|
|
191
|
+
export type CompareCount = {
|
|
192
|
+
queries: number;
|
|
193
|
+
searches: number;
|
|
194
|
+
};
|
|
195
|
+
export type ComparedHit = {
|
|
196
|
+
objectID: string;
|
|
197
|
+
title: string;
|
|
198
|
+
};
|
|
199
|
+
/** Proposed settings compared with the index's own, without saving them (`compareSettings`). */
|
|
200
|
+
export type SettingsComparison = {
|
|
201
|
+
/** The queries compared, and their searches in the last 30 days. */
|
|
202
|
+
compared: number;
|
|
203
|
+
searches: number;
|
|
204
|
+
/** Time (20 seconds) ran out before every query was compared. */
|
|
205
|
+
truncated: boolean;
|
|
206
|
+
summary: {
|
|
207
|
+
topChanged: CompareCount;
|
|
208
|
+
moreResults: CompareCount;
|
|
209
|
+
fewerResults: CompareCount;
|
|
210
|
+
nowNothing: CompareCount;
|
|
211
|
+
nowSomething: CompareCount;
|
|
212
|
+
};
|
|
213
|
+
/** Every search test: whether it passes with the current settings (`before`) and the proposed ones
|
|
214
|
+
* (`after`), or `skipped` when time ran out first, or the search's `error`. */
|
|
215
|
+
tests: (Omit<SearchTest, "note"> & {
|
|
216
|
+
before?: boolean;
|
|
217
|
+
after?: boolean;
|
|
218
|
+
skipped?: true;
|
|
219
|
+
error?: string;
|
|
220
|
+
})[];
|
|
221
|
+
/** Up to 50 queries whose top 10 or total changed, the tests' queries first, then the most searched: the
|
|
222
|
+
* totals and first 5 results with each, and the results that left, joined, and moved within the top 10.
|
|
223
|
+
* Places are in the first 20 results; null is outside them. */
|
|
224
|
+
changes: {
|
|
225
|
+
q: string;
|
|
226
|
+
filters?: SearchRequest["filters"];
|
|
227
|
+
searches: number;
|
|
228
|
+
before: {
|
|
229
|
+
total: number;
|
|
230
|
+
top: ComparedHit[];
|
|
231
|
+
};
|
|
232
|
+
after: {
|
|
233
|
+
total: number;
|
|
234
|
+
top: ComparedHit[];
|
|
235
|
+
};
|
|
236
|
+
gone: (ComparedHit & {
|
|
237
|
+
from: number;
|
|
238
|
+
to: number | null;
|
|
239
|
+
})[];
|
|
240
|
+
added: (ComparedHit & {
|
|
241
|
+
from: number | null;
|
|
242
|
+
to: number;
|
|
243
|
+
})[];
|
|
244
|
+
moved: (ComparedHit & {
|
|
245
|
+
from: number;
|
|
246
|
+
to: number;
|
|
247
|
+
})[];
|
|
248
|
+
}[];
|
|
249
|
+
errors: {
|
|
250
|
+
q: string;
|
|
251
|
+
error: string;
|
|
252
|
+
}[];
|
|
253
|
+
};
|
|
254
|
+
export type CompareOptions = {
|
|
255
|
+
/** More queries to compare, such as those a change is for: text, or `{ q, filters }`. Up to 200. */
|
|
256
|
+
queries?: (string | {
|
|
257
|
+
q: string;
|
|
258
|
+
filters?: SearchRequest["filters"];
|
|
259
|
+
})[];
|
|
260
|
+
/** false leaves out the index's 100 most searched queries of the last 30 days. */
|
|
261
|
+
searched?: boolean;
|
|
262
|
+
signal?: AbortSignal;
|
|
263
|
+
};
|
|
139
264
|
export type PublicSearchPolicy = {
|
|
140
265
|
returnFields: string[];
|
|
141
266
|
searchFields: string[];
|
|
@@ -474,6 +599,166 @@ export type EventOptions = {
|
|
|
474
599
|
visitorId?: string;
|
|
475
600
|
visitId?: string;
|
|
476
601
|
};
|
|
602
|
+
/** An analytics window, in UTC: `range` (7d by default), or `from` and `to`. A date as `from` is the start of
|
|
603
|
+
* that day, and as `to` includes the whole day; `to` is now by default. Analytics go back 90 days: a window
|
|
604
|
+
* starting earlier starts then, and the report's `from` says so. */
|
|
605
|
+
export type AnalyticsOptions = {
|
|
606
|
+
range?: "24h" | "7d" | "30d" | "90d";
|
|
607
|
+
from?: string | Date;
|
|
608
|
+
to?: string | Date;
|
|
609
|
+
/** Searches from browsers (browser search), from servers (API keys), or both, by default. */
|
|
610
|
+
source?: "all" | "browser" | "server";
|
|
611
|
+
signal?: AbortSignal;
|
|
612
|
+
};
|
|
613
|
+
/** What every analytics report starts with. `sampled`: the counts are estimates, because Analytics Engine,
|
|
614
|
+
* which keeps them, sampled some of them; totals and lists stay close, small counts less so. */
|
|
615
|
+
export type AnalyticsReport = {
|
|
616
|
+
index: string;
|
|
617
|
+
from: string;
|
|
618
|
+
to: string;
|
|
619
|
+
source: "all" | "browser" | "server";
|
|
620
|
+
sampled: boolean;
|
|
621
|
+
};
|
|
622
|
+
export type SearchTotals = {
|
|
623
|
+
searches: number;
|
|
624
|
+
/** Searches whose query and result count were recorded: the base for `noResultRate`. */
|
|
625
|
+
tracked: number;
|
|
626
|
+
noResults: number;
|
|
627
|
+
/** `noResults` of `tracked`, 0 to 1; null without tracked searches. */
|
|
628
|
+
noResultRate: number | null;
|
|
629
|
+
browser: number;
|
|
630
|
+
server: number;
|
|
631
|
+
uniqueQueries: number;
|
|
632
|
+
/** Response times at the API, in ms. */
|
|
633
|
+
medianMs: number;
|
|
634
|
+
p95Ms: number;
|
|
635
|
+
};
|
|
636
|
+
export type SearchesReport = AnalyticsReport & {
|
|
637
|
+
interval: "hour" | "day";
|
|
638
|
+
totals: SearchTotals;
|
|
639
|
+
/** The same span just before `from`. */
|
|
640
|
+
previous: SearchTotals & {
|
|
641
|
+
from: string;
|
|
642
|
+
to: string;
|
|
643
|
+
};
|
|
644
|
+
/** Every hour or day of the window, the first one starting at or before `from`. */
|
|
645
|
+
series: {
|
|
646
|
+
t: string;
|
|
647
|
+
searches: number;
|
|
648
|
+
tracked: number;
|
|
649
|
+
noResults: number;
|
|
650
|
+
}[];
|
|
651
|
+
};
|
|
652
|
+
/** A top query: its clicks on results and their average place (from 1), or null when clicks aren't available. */
|
|
653
|
+
export type TopQuery = {
|
|
654
|
+
query: string;
|
|
655
|
+
searches: number;
|
|
656
|
+
noResults: number;
|
|
657
|
+
clicks: number | null;
|
|
658
|
+
averagePosition: number | null;
|
|
659
|
+
};
|
|
660
|
+
export type QueryCount = {
|
|
661
|
+
query: string;
|
|
662
|
+
searches: number;
|
|
663
|
+
};
|
|
664
|
+
export type QueryLists = {
|
|
665
|
+
top: TopQuery;
|
|
666
|
+
noResults: QueryCount;
|
|
667
|
+
noClicks: QueryCount;
|
|
668
|
+
};
|
|
669
|
+
export type QueriesReport<L extends keyof QueryLists = "top"> = AnalyticsReport & {
|
|
670
|
+
list: L;
|
|
671
|
+
queries: QueryLists[L][];
|
|
672
|
+
};
|
|
673
|
+
export type FiltersReport = AnalyticsReport & {
|
|
674
|
+
/** Searches whose filters and sort were recorded. */
|
|
675
|
+
searches: number;
|
|
676
|
+
/** Each filter field with the searches that filtered on it, up to 100. */
|
|
677
|
+
filters: {
|
|
678
|
+
field: string;
|
|
679
|
+
searches: number;
|
|
680
|
+
}[];
|
|
681
|
+
/** Each sort with its searches; `sort` is null for searches that named none, which use the index's default. */
|
|
682
|
+
sorts: {
|
|
683
|
+
sort: string | null;
|
|
684
|
+
searches: number;
|
|
685
|
+
}[];
|
|
686
|
+
};
|
|
687
|
+
export type EventType = "view" | "click" | "addToCart" | "purchase" | "cancel";
|
|
688
|
+
export type EventsReport = AnalyticsReport & {
|
|
689
|
+
interval: "hour" | "day";
|
|
690
|
+
/** Accepted events of each type, by source. */
|
|
691
|
+
types: {
|
|
692
|
+
type: EventType;
|
|
693
|
+
browser: number;
|
|
694
|
+
server: number;
|
|
695
|
+
}[];
|
|
696
|
+
/** Orders accepted, and of them those stored, which Bought together and the order counts use. */
|
|
697
|
+
orders: {
|
|
698
|
+
accepted: number;
|
|
699
|
+
stored: number;
|
|
700
|
+
};
|
|
701
|
+
/** Events from crawlers and scripts, which are dropped. */
|
|
702
|
+
bots: number;
|
|
703
|
+
/** Refused events, by why. */
|
|
704
|
+
refused: {
|
|
705
|
+
reason: string;
|
|
706
|
+
events: number;
|
|
707
|
+
}[];
|
|
708
|
+
series: ({
|
|
709
|
+
t: string;
|
|
710
|
+
} & Record<EventType, number>)[];
|
|
711
|
+
};
|
|
712
|
+
/** A product's counts. `viewers` is how many visitor IDs viewed it, or null when the index's views carry
|
|
713
|
+
* none. `searchClicks` is clicks on it outside recommendations: in search results and product lists. */
|
|
714
|
+
export type ProductCount = {
|
|
715
|
+
objectID: string;
|
|
716
|
+
views: number;
|
|
717
|
+
viewers: number | null;
|
|
718
|
+
searchClicks: number;
|
|
719
|
+
carts: number;
|
|
720
|
+
orders: number;
|
|
721
|
+
};
|
|
722
|
+
export type ListedProduct = ProductCount & {
|
|
723
|
+
title: string;
|
|
724
|
+
image: string | null;
|
|
725
|
+
};
|
|
726
|
+
/** Product reports are exact and about 2 minutes behind; `computedAt` is when they were counted, up to 5
|
|
727
|
+
* minutes ago. `orders` is the orders stored in the window. */
|
|
728
|
+
export type ProductListsReport = AnalyticsReport & {
|
|
729
|
+
computedAt: string;
|
|
730
|
+
orders: number;
|
|
731
|
+
/** The most viewed, most added to carts, and most ordered products, by views. */
|
|
732
|
+
products: ListedProduct[];
|
|
733
|
+
/** Products with 50 or more views, the fewest orders per view first; null without orders. */
|
|
734
|
+
viewedNotBought: ListedProduct[] | null;
|
|
735
|
+
};
|
|
736
|
+
export type ProductCountsReport = AnalyticsReport & {
|
|
737
|
+
computedAt: string;
|
|
738
|
+
orders: number;
|
|
739
|
+
/** false when more than 50,000 products had events, and only the most viewed are listed. */
|
|
740
|
+
complete: boolean;
|
|
741
|
+
products: ProductCount[];
|
|
742
|
+
};
|
|
743
|
+
export type ProductReport = AnalyticsReport & {
|
|
744
|
+
objectID: string;
|
|
745
|
+
interval: "hour" | "day";
|
|
746
|
+
totals: Omit<ProductCount, "objectID">;
|
|
747
|
+
series: ({
|
|
748
|
+
t: string;
|
|
749
|
+
} & Omit<ProductCount, "objectID">)[];
|
|
750
|
+
/** The queries whose results it was clicked in, the most clicks first, up to 20. */
|
|
751
|
+
searchedFor: {
|
|
752
|
+
query: string;
|
|
753
|
+
clicks: number;
|
|
754
|
+
}[];
|
|
755
|
+
/** Clicks on it in each recommendation list. */
|
|
756
|
+
recommendationClicks: {
|
|
757
|
+
goesWith: number;
|
|
758
|
+
similar: number;
|
|
759
|
+
together: number;
|
|
760
|
+
};
|
|
761
|
+
};
|
|
477
762
|
export declare class FindlaneError extends Error {
|
|
478
763
|
status: number;
|
|
479
764
|
constructor(status: number, message: string);
|
|
@@ -503,6 +788,18 @@ export declare class SearchServerClient {
|
|
|
503
788
|
* same request (its q, filters, and fields matter). Needs the `didYouMean` setting; not counted as
|
|
504
789
|
* a search. */
|
|
505
790
|
suggest: (query: SearchRequest, options?: AbortSignal | RequestOptions) => Promise<DidYouMean | null>;
|
|
791
|
+
/** Query suggestions: whole searches to offer as a shopper types `q`, the best first; with `q` empty, the
|
|
792
|
+
* index's popular searches. The API key needs the `search` scope; not counted as searches. `limit`: 1 to
|
|
793
|
+
* 10, 5 by default. */
|
|
794
|
+
querySuggestions: (query: {
|
|
795
|
+
q: string;
|
|
796
|
+
limit?: number;
|
|
797
|
+
}, options?: AbortSignal | RequestOptions) => Promise<QuerySuggestion[]>;
|
|
798
|
+
/** The index's popular searches, for an empty search box, with the owner's pinned searches first;
|
|
799
|
+
* empty until the index has enough searches. The list changes daily. */
|
|
800
|
+
popularSearches: (options?: {
|
|
801
|
+
limit?: number;
|
|
802
|
+
}, requestOptionsOrSignal?: AbortSignal | RequestOptions) => Promise<QuerySuggestion[]>;
|
|
506
803
|
/** A product's recommendations (the recommendations add-on): products that go with it, and similar products.
|
|
507
804
|
* The API key needs the `search` scope; not counted as a search. */
|
|
508
805
|
recommendations: <R = T>(objectID: string, options?: RecommendationsRequest, requestOptionsOrSignal?: AbortSignal | RequestOptions) => Promise<RecommendationsResponse<R>>;
|
|
@@ -532,6 +829,39 @@ export declare class SearchServerClient {
|
|
|
532
829
|
* and its visitor. Answers with the API's response. */
|
|
533
830
|
relay: (body: string, visitor: RelayedVisitor, signal?: AbortSignal) => Promise<Response>;
|
|
534
831
|
};
|
|
832
|
+
/** The index's search analytics. The API key needs the `analytics` scope, and works only from a server:
|
|
833
|
+
* requests from web pages are refused. */
|
|
834
|
+
analytics: {
|
|
835
|
+
/** Searches, searches without results and response times, beside the same span before, and a series per
|
|
836
|
+
* hour (up to 7 days, by default) or day. */
|
|
837
|
+
searches: (options?: AnalyticsOptions & {
|
|
838
|
+
interval?: "hour" | "day";
|
|
839
|
+
}) => Promise<SearchesReport>;
|
|
840
|
+
/** Queries, the most searched first: `top` (by default) with their clicks, `noResults`, or `noClicks`, which
|
|
841
|
+
* found something nobody clicked. `limit` is 1 to 1,000, 100 by default. */
|
|
842
|
+
queries: <L extends keyof QueryLists = "top">(options?: AnalyticsOptions & {
|
|
843
|
+
list?: L;
|
|
844
|
+
limit?: number;
|
|
845
|
+
}) => Promise<QueriesReport<L>>;
|
|
846
|
+
/** The filter fields and sorts searches used. */
|
|
847
|
+
filters: (options?: AnalyticsOptions) => Promise<FiltersReport>;
|
|
848
|
+
/** Events of each type over time and by source, orders stored, and refused and bot events. */
|
|
849
|
+
events: (options?: AnalyticsOptions & {
|
|
850
|
+
interval?: "hour" | "day";
|
|
851
|
+
}) => Promise<EventsReport>;
|
|
852
|
+
/** The most viewed, most added to carts and most ordered products, with titles and images, and those
|
|
853
|
+
* viewed but rarely bought. Exact, from every event kept (13 months, from 9 October 2026). */
|
|
854
|
+
products: (options?: AnalyticsOptions) => Promise<ProductListsReport>;
|
|
855
|
+
/** Every product with events or orders in the window, the most viewed first, up to 50,000: views,
|
|
856
|
+
* viewers, clicks, add-to-carts and orders. Exact, like `products`. */
|
|
857
|
+
productCounts: (options?: AnalyticsOptions) => Promise<ProductCountsReport>;
|
|
858
|
+
/** One product's views, viewers, clicks, add-to-carts and orders per hour or day, the queries it was
|
|
859
|
+
* clicked from, and its clicks in recommendations. Fast, for a product page in an admin: views and
|
|
860
|
+
* clicks are estimates when `sampled`, and an add-to-cart of several products counts toward the first. */
|
|
861
|
+
product: (objectID: string, options?: AnalyticsOptions & {
|
|
862
|
+
interval?: "hour" | "day";
|
|
863
|
+
}) => Promise<ProductReport>;
|
|
864
|
+
};
|
|
535
865
|
upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
|
|
536
866
|
getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
|
|
537
867
|
upsertOne: (record: T, options: ConditionalWriteOptions) => Promise<WriteReceipt>;
|
|
@@ -545,6 +875,16 @@ export declare class SearchServerClient {
|
|
|
545
875
|
delete: (objectID: string, signal?: AbortSignal) => Promise<WriteReceipt>;
|
|
546
876
|
getSettings: (signal?: AbortSignal) => Promise<CurrentSettings>;
|
|
547
877
|
replaceSettings: (settings: IndexSettings, signal?: AbortSignal) => Promise<SavedSettings>;
|
|
878
|
+
/** Compares settings with the index's own without saving them: its search tests, `queries`, and its 100
|
|
879
|
+
* most searched queries of the last 30 days, each searched with both, for up to 20 seconds. Takes the
|
|
880
|
+
* settings `replaceSettings` does, or only those that change; `{}` runs the tests with the current
|
|
881
|
+
* settings. A change to what the index stores (new searchable, facet, filter, sort or location fields,
|
|
882
|
+
* or normalization) throws a 409: save it first. Not counted as searches. */
|
|
883
|
+
compareSettings: (settings: Partial<IndexSettings>, options?: CompareOptions) => Promise<SettingsComparison>;
|
|
884
|
+
/** The index's search tests. */
|
|
885
|
+
getSearchTests: (signal?: AbortSignal) => Promise<SearchTest[]>;
|
|
886
|
+
/** Replaces the index's search tests, up to 200; `[]` deletes them. */
|
|
887
|
+
setSearchTests: (tests: SearchTest[], signal?: AbortSignal) => Promise<SearchTest[]>;
|
|
548
888
|
/** Waits until no settings rebuild is running; throws if it failed. */
|
|
549
889
|
waitForSettings: (options?: {
|
|
550
890
|
timeoutMs?: number;
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,16 @@ class StagedReplacementError extends Error {
|
|
|
21
21
|
this.name = "StagedReplacementError";
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
function analyticsQuery(options) {
|
|
25
|
+
const params = new URLSearchParams;
|
|
26
|
+
for (const name of ["range", "from", "to", "interval", "source", "list", "limit"]) {
|
|
27
|
+
const value = options[name];
|
|
28
|
+
if (value !== undefined)
|
|
29
|
+
params.set(name, value instanceof Date ? value.toISOString() : String(value));
|
|
30
|
+
}
|
|
31
|
+
const query = params.toString();
|
|
32
|
+
return query ? `?${query}` : "";
|
|
33
|
+
}
|
|
24
34
|
var eventsPerRequest = 200;
|
|
25
35
|
var requestOptions = (value) => value === undefined ? {} : ("aborted" in value) ? { signal: value } : value;
|
|
26
36
|
var relayedBytes = 256 * 1024;
|
|
@@ -176,6 +186,7 @@ class SearchServerClient {
|
|
|
176
186
|
await flush();
|
|
177
187
|
return result;
|
|
178
188
|
};
|
|
189
|
+
const report = (name2, options) => retry(() => request(`/analytics/${name2}${analyticsQuery(options)}`, "GET", undefined, options.signal), options.signal);
|
|
179
190
|
const stagePath = (stageId) => `/staged-replacements/${encodeURIComponent(stageId)}`;
|
|
180
191
|
const beginReplacement = (stageId, signal) => retry(() => request(stagePath(stageId), "POST", undefined, signal), signal);
|
|
181
192
|
const uploadReplacementBatch = (stageId, batchId, records, signal) => retry(() => request(`${stagePath(stageId)}/batches/${encodeURIComponent(batchId)}`, "POST", { records }, signal), signal);
|
|
@@ -196,6 +207,14 @@ class SearchServerClient {
|
|
|
196
207
|
const { signal, visitor } = requestOptions(options);
|
|
197
208
|
return (await request("/suggest", "POST", query, signal, visitorHeaders(visitor))).didYouMean;
|
|
198
209
|
},
|
|
210
|
+
querySuggestions: async (query, options) => {
|
|
211
|
+
const { signal } = requestOptions(options);
|
|
212
|
+
return (await request("/query-suggestions", "POST", query, signal)).suggestions;
|
|
213
|
+
},
|
|
214
|
+
popularSearches: async (options = {}, requestOptionsOrSignal) => {
|
|
215
|
+
const { signal } = requestOptions(requestOptionsOrSignal);
|
|
216
|
+
return (await request("/query-suggestions", "POST", { q: "", limit: options.limit ?? 6 }, signal)).suggestions;
|
|
217
|
+
},
|
|
199
218
|
recommendations: (objectID, options = {}, requestOptionsOrSignal) => {
|
|
200
219
|
const { signal, visitor } = requestOptions(requestOptionsOrSignal);
|
|
201
220
|
return request("/recommendations", "POST", { objectID, ...options }, signal, visitorHeaders(visitor));
|
|
@@ -233,6 +252,15 @@ class SearchServerClient {
|
|
|
233
252
|
},
|
|
234
253
|
relay: (body, visitor, signal) => relayEvents(body, visitor, signal)
|
|
235
254
|
},
|
|
255
|
+
analytics: {
|
|
256
|
+
searches: (options = {}) => report("searches", options),
|
|
257
|
+
queries: (options = {}) => report("queries", options),
|
|
258
|
+
filters: (options = {}) => report("filters", options),
|
|
259
|
+
events: (options = {}) => report("events", options),
|
|
260
|
+
products: (options = {}) => report("products", options),
|
|
261
|
+
productCounts: (options = {}) => report("product-counts", options),
|
|
262
|
+
product: (objectID, options = {}) => report(`products/${encodeURIComponent(objectID)}`, options)
|
|
263
|
+
},
|
|
236
264
|
upsert: (records, signal) => request("/records", "POST", { records }, signal),
|
|
237
265
|
getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
|
|
238
266
|
upsertOne: (record, options) => writeOne("upsert", record.objectID, record, options),
|
|
@@ -291,6 +319,16 @@ class SearchServerClient {
|
|
|
291
319
|
const { rebuild: _rebuild, rebuilt: _rebuilt, applied: _applied, ...body } = settings;
|
|
292
320
|
return request("/config", "PUT", body, signal);
|
|
293
321
|
},
|
|
322
|
+
compareSettings: (settings, options = {}) => {
|
|
323
|
+
const { rebuild: _rebuild, rebuilt: _rebuilt, applied: _applied, ...body } = settings;
|
|
324
|
+
return request("/compare", "POST", {
|
|
325
|
+
settings: body,
|
|
326
|
+
...options.queries ? { queries: options.queries } : {},
|
|
327
|
+
...options.searched === false ? { searched: false } : {}
|
|
328
|
+
}, options.signal);
|
|
329
|
+
},
|
|
330
|
+
getSearchTests: async (signal) => (await request("/tests", "GET", undefined, signal)).tests,
|
|
331
|
+
setSearchTests: async (tests, signal) => (await request("/tests", "PUT", { tests }, signal)).tests,
|
|
294
332
|
waitForSettings: async (options = {}) => {
|
|
295
333
|
const deadline = Date.now() + (options.timeoutMs ?? 30 * 60000);
|
|
296
334
|
while (true) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@findlane/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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",
|