@findlane/server 0.6.0 → 0.8.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 +11 -1
- package/dist/index.cjs +20 -4
- package/dist/index.d.cts +46 -3
- package/dist/index.d.ts +46 -3
- package/dist/index.js +20 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,13 +31,23 @@ The workspace ID is under Settings in the dashboard, and keys are under API keys
|
|
|
31
31
|
- `search`: `search()`.
|
|
32
32
|
- `write`: record writes, settings, public search restrictions, and indexing status.
|
|
33
33
|
|
|
34
|
+
## Searching for a visitor
|
|
35
|
+
|
|
36
|
+
When your server searches for someone, such as a shopper loading a server-rendered search page, pass the visitor. The index's limits and bot rules then apply to them as they do to browser search, and searches from verified crawlers like Googlebot are free:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const results = await products.search({ q }, { visitor: { ip: req.ip, userAgent: req.get("user-agent") } })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`suggest()` and `recommendations()` take the same option. Without it, a search counts as your server's own.
|
|
43
|
+
|
|
34
44
|
## An index's methods
|
|
35
45
|
|
|
36
46
|
`client.index<T>(name)` returns these; `T` types your records, which each need a string `objectID`.
|
|
37
47
|
|
|
38
48
|
| Method | What it does |
|
|
39
49
|
| --- | --- |
|
|
40
|
-
| `search(request)` | Search with a query, filters, facets, a sort, paging, and highlighting. |
|
|
50
|
+
| `search(request, options?)` | Search with a query, filters, facets, a sort, paging, and highlighting. `options` takes `signal` and `visitor` (below). |
|
|
41
51
|
| `upsert(records)` | Add or update up to 1,000 records by `objectID`. |
|
|
42
52
|
| `replaceAll(records)` | Replace the whole catalog in one request (up to 100,000 records). |
|
|
43
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.cjs
CHANGED
|
@@ -67,6 +67,11 @@ class StagedReplacementError extends Error {
|
|
|
67
67
|
this.name = "StagedReplacementError";
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
var requestOptions = (value) => value === undefined ? {} : ("aborted" in value) ? { signal: value } : value;
|
|
71
|
+
var visitorHeaders = (visitor) => visitor?.ip ? {
|
|
72
|
+
"findlane-visitor-ip": visitor.ip.trim(),
|
|
73
|
+
...visitor.userAgent ? { "findlane-visitor-agent": visitor.userAgent.replace(/[^\x20-\x7e]/g, "").slice(0, 512) } : {}
|
|
74
|
+
} : {};
|
|
70
75
|
|
|
71
76
|
class FindlaneError extends Error {
|
|
72
77
|
status;
|
|
@@ -93,12 +98,13 @@ class SearchServerClient {
|
|
|
93
98
|
}
|
|
94
99
|
index(name) {
|
|
95
100
|
const path = `/api/teams/${encodeURIComponent(this.workspaceId)}/indexes/${encodeURIComponent(name)}`;
|
|
96
|
-
const request = async (suffix, method = "GET", payload, signal) => {
|
|
101
|
+
const request = async (suffix, method = "GET", payload, signal, extra = {}) => {
|
|
97
102
|
const response = await this.fetcher(this.baseUrl + path + suffix, {
|
|
98
103
|
method,
|
|
99
104
|
headers: {
|
|
100
105
|
authorization: `Bearer ${this.key}`,
|
|
101
|
-
...payload === undefined ? {} : { "content-type": "application/json" }
|
|
106
|
+
...payload === undefined ? {} : { "content-type": "application/json" },
|
|
107
|
+
...extra
|
|
102
108
|
},
|
|
103
109
|
body: payload === undefined ? undefined : JSON.stringify(payload),
|
|
104
110
|
signal
|
|
@@ -142,8 +148,18 @@ class SearchServerClient {
|
|
|
142
148
|
return retry(() => request("/record-writes", "POST", payload, options.signal), options.signal);
|
|
143
149
|
};
|
|
144
150
|
return {
|
|
145
|
-
search: (query = {},
|
|
146
|
-
|
|
151
|
+
search: (query = {}, options) => {
|
|
152
|
+
const { signal, visitor } = requestOptions(options);
|
|
153
|
+
return request("/search", "POST", query, signal, visitorHeaders(visitor));
|
|
154
|
+
},
|
|
155
|
+
suggest: async (query, options) => {
|
|
156
|
+
const { signal, visitor } = requestOptions(options);
|
|
157
|
+
return (await request("/suggest", "POST", query, signal, visitorHeaders(visitor))).didYouMean;
|
|
158
|
+
},
|
|
159
|
+
recommendations: (objectID, options = {}, requestOptionsOrSignal) => {
|
|
160
|
+
const { signal, visitor } = requestOptions(requestOptionsOrSignal);
|
|
161
|
+
return request("/recommendations", "POST", { objectID, ...options }, signal, visitorHeaders(visitor));
|
|
162
|
+
},
|
|
147
163
|
upsert: (records, signal) => request("/records", "POST", { records }, signal),
|
|
148
164
|
getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
|
|
149
165
|
upsertOne: (record, options) => writeOne("upsert", record.objectID, record, options),
|
package/dist/index.d.cts
CHANGED
|
@@ -173,6 +173,31 @@ export type SearchResponse<T> = {
|
|
|
173
173
|
* results matched some words only by typo ("Showing results for …"), else null. */
|
|
174
174
|
didYouMean?: DidYouMean | null;
|
|
175
175
|
};
|
|
176
|
+
/** How a product's recommendations are served (the recommendations add-on). */
|
|
177
|
+
export type RecommendationsRequest = {
|
|
178
|
+
/** Up to this many products in each list, 1 to 20; 8 by default. */
|
|
179
|
+
limit?: number;
|
|
180
|
+
/** Only picks matching these filters, as in a search (fields configured as filters), such as `{ inStock: true }`. */
|
|
181
|
+
filters?: SearchRequest["filters"];
|
|
182
|
+
/** The record fields to return; all of them by default. */
|
|
183
|
+
attributesToRetrieve?: string[];
|
|
184
|
+
/** Goes with picks cost at most this many times the product; null for no limit. By default, the index's setting. */
|
|
185
|
+
maxPriceRatio?: number | null;
|
|
186
|
+
};
|
|
187
|
+
export type RecommendationHit<T> = {
|
|
188
|
+
id: string;
|
|
189
|
+
score: number | null;
|
|
190
|
+
record: T;
|
|
191
|
+
};
|
|
192
|
+
export type RecommendationsResponse<T> = {
|
|
193
|
+
objectID: string;
|
|
194
|
+
/** false until the product's lists are built; both lists are empty meanwhile. */
|
|
195
|
+
ready: boolean;
|
|
196
|
+
/** Products a shopper would want with this one, the likeliest first; `score` is the decision model's probability. */
|
|
197
|
+
goesWith: RecommendationHit<T>[];
|
|
198
|
+
/** Products a shopper could choose instead, then (unless the index hides them) variants of this one. */
|
|
199
|
+
similar: RecommendationHit<T>[];
|
|
200
|
+
};
|
|
176
201
|
/** A corrected query. `applied`: the results are already for `q` (a search's "Showing results for");
|
|
177
202
|
* otherwise searching `q` finds results that the query didn't ("Did you mean"). `changes` lists each
|
|
178
203
|
* changed part of the query as typed. */
|
|
@@ -215,6 +240,20 @@ export declare class StagedReplacementError extends Error {
|
|
|
215
240
|
stageId: string;
|
|
216
241
|
constructor(stageId: string, cause: unknown);
|
|
217
242
|
}
|
|
243
|
+
/** The visitor a server searches for, such as a shopper loading a server-rendered search page. With it,
|
|
244
|
+
* the index's limits and bot rules apply to that visitor, as they do to browser search, and searches
|
|
245
|
+
* from verified crawlers like Googlebot are free. */
|
|
246
|
+
export type Visitor = {
|
|
247
|
+
/** The visitor's IP address: on Express `req.ip`, on Next.js the first address in `x-forwarded-for`. */
|
|
248
|
+
ip: string;
|
|
249
|
+
/** The visitor's User-Agent header. */
|
|
250
|
+
userAgent?: string;
|
|
251
|
+
};
|
|
252
|
+
/** Options for a search, a suggestion, or recommendations. */
|
|
253
|
+
export type RequestOptions = {
|
|
254
|
+
signal?: AbortSignal;
|
|
255
|
+
visitor?: Visitor;
|
|
256
|
+
};
|
|
218
257
|
export declare class FindlaneError extends Error {
|
|
219
258
|
status: number;
|
|
220
259
|
constructor(status: number, message: string);
|
|
@@ -237,12 +276,16 @@ export declare class SearchServerClient {
|
|
|
237
276
|
index<T extends {
|
|
238
277
|
objectID: string;
|
|
239
278
|
} = JsonRecord>(name: string): {
|
|
240
|
-
/** Searches the index; the API key needs the `search` scope.
|
|
241
|
-
|
|
279
|
+
/** Searches the index; the API key needs the `search` scope. Pass `{ visitor }` when searching for
|
|
280
|
+
* someone, such as a shopper loading a server-rendered page (see Visitor). */
|
|
281
|
+
search: <R = T>(query?: SearchRequest, options?: AbortSignal | RequestOptions) => Promise<SearchResponse<R>>;
|
|
242
282
|
/** Did you mean: for a search that found nothing, a query that finds something, or null. Takes the
|
|
243
283
|
* same request (its q, filters, and fields matter). Needs the `didYouMean` setting; not counted as
|
|
244
284
|
* a search. */
|
|
245
|
-
suggest: (query: SearchRequest,
|
|
285
|
+
suggest: (query: SearchRequest, options?: AbortSignal | RequestOptions) => Promise<DidYouMean | null>;
|
|
286
|
+
/** A product's recommendations (the recommendations add-on): products that go with it, and similar products.
|
|
287
|
+
* The API key needs the `search` scope; not counted as a search. */
|
|
288
|
+
recommendations: <R = T>(objectID: string, options?: RecommendationsRequest, requestOptionsOrSignal?: AbortSignal | RequestOptions) => Promise<RecommendationsResponse<R>>;
|
|
246
289
|
upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
|
|
247
290
|
getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
|
|
248
291
|
upsertOne: (record: T, options: ConditionalWriteOptions) => Promise<WriteReceipt>;
|
package/dist/index.d.ts
CHANGED
|
@@ -173,6 +173,31 @@ export type SearchResponse<T> = {
|
|
|
173
173
|
* results matched some words only by typo ("Showing results for …"), else null. */
|
|
174
174
|
didYouMean?: DidYouMean | null;
|
|
175
175
|
};
|
|
176
|
+
/** How a product's recommendations are served (the recommendations add-on). */
|
|
177
|
+
export type RecommendationsRequest = {
|
|
178
|
+
/** Up to this many products in each list, 1 to 20; 8 by default. */
|
|
179
|
+
limit?: number;
|
|
180
|
+
/** Only picks matching these filters, as in a search (fields configured as filters), such as `{ inStock: true }`. */
|
|
181
|
+
filters?: SearchRequest["filters"];
|
|
182
|
+
/** The record fields to return; all of them by default. */
|
|
183
|
+
attributesToRetrieve?: string[];
|
|
184
|
+
/** Goes with picks cost at most this many times the product; null for no limit. By default, the index's setting. */
|
|
185
|
+
maxPriceRatio?: number | null;
|
|
186
|
+
};
|
|
187
|
+
export type RecommendationHit<T> = {
|
|
188
|
+
id: string;
|
|
189
|
+
score: number | null;
|
|
190
|
+
record: T;
|
|
191
|
+
};
|
|
192
|
+
export type RecommendationsResponse<T> = {
|
|
193
|
+
objectID: string;
|
|
194
|
+
/** false until the product's lists are built; both lists are empty meanwhile. */
|
|
195
|
+
ready: boolean;
|
|
196
|
+
/** Products a shopper would want with this one, the likeliest first; `score` is the decision model's probability. */
|
|
197
|
+
goesWith: RecommendationHit<T>[];
|
|
198
|
+
/** Products a shopper could choose instead, then (unless the index hides them) variants of this one. */
|
|
199
|
+
similar: RecommendationHit<T>[];
|
|
200
|
+
};
|
|
176
201
|
/** A corrected query. `applied`: the results are already for `q` (a search's "Showing results for");
|
|
177
202
|
* otherwise searching `q` finds results that the query didn't ("Did you mean"). `changes` lists each
|
|
178
203
|
* changed part of the query as typed. */
|
|
@@ -215,6 +240,20 @@ export declare class StagedReplacementError extends Error {
|
|
|
215
240
|
stageId: string;
|
|
216
241
|
constructor(stageId: string, cause: unknown);
|
|
217
242
|
}
|
|
243
|
+
/** The visitor a server searches for, such as a shopper loading a server-rendered search page. With it,
|
|
244
|
+
* the index's limits and bot rules apply to that visitor, as they do to browser search, and searches
|
|
245
|
+
* from verified crawlers like Googlebot are free. */
|
|
246
|
+
export type Visitor = {
|
|
247
|
+
/** The visitor's IP address: on Express `req.ip`, on Next.js the first address in `x-forwarded-for`. */
|
|
248
|
+
ip: string;
|
|
249
|
+
/** The visitor's User-Agent header. */
|
|
250
|
+
userAgent?: string;
|
|
251
|
+
};
|
|
252
|
+
/** Options for a search, a suggestion, or recommendations. */
|
|
253
|
+
export type RequestOptions = {
|
|
254
|
+
signal?: AbortSignal;
|
|
255
|
+
visitor?: Visitor;
|
|
256
|
+
};
|
|
218
257
|
export declare class FindlaneError extends Error {
|
|
219
258
|
status: number;
|
|
220
259
|
constructor(status: number, message: string);
|
|
@@ -237,12 +276,16 @@ export declare class SearchServerClient {
|
|
|
237
276
|
index<T extends {
|
|
238
277
|
objectID: string;
|
|
239
278
|
} = JsonRecord>(name: string): {
|
|
240
|
-
/** Searches the index; the API key needs the `search` scope.
|
|
241
|
-
|
|
279
|
+
/** Searches the index; the API key needs the `search` scope. Pass `{ visitor }` when searching for
|
|
280
|
+
* someone, such as a shopper loading a server-rendered page (see Visitor). */
|
|
281
|
+
search: <R = T>(query?: SearchRequest, options?: AbortSignal | RequestOptions) => Promise<SearchResponse<R>>;
|
|
242
282
|
/** Did you mean: for a search that found nothing, a query that finds something, or null. Takes the
|
|
243
283
|
* same request (its q, filters, and fields matter). Needs the `didYouMean` setting; not counted as
|
|
244
284
|
* a search. */
|
|
245
|
-
suggest: (query: SearchRequest,
|
|
285
|
+
suggest: (query: SearchRequest, options?: AbortSignal | RequestOptions) => Promise<DidYouMean | null>;
|
|
286
|
+
/** A product's recommendations (the recommendations add-on): products that go with it, and similar products.
|
|
287
|
+
* The API key needs the `search` scope; not counted as a search. */
|
|
288
|
+
recommendations: <R = T>(objectID: string, options?: RecommendationsRequest, requestOptionsOrSignal?: AbortSignal | RequestOptions) => Promise<RecommendationsResponse<R>>;
|
|
246
289
|
upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
|
|
247
290
|
getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
|
|
248
291
|
upsertOne: (record: T, options: ConditionalWriteOptions) => Promise<WriteReceipt>;
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,11 @@ class StagedReplacementError extends Error {
|
|
|
21
21
|
this.name = "StagedReplacementError";
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
var requestOptions = (value) => value === undefined ? {} : ("aborted" in value) ? { signal: value } : value;
|
|
25
|
+
var visitorHeaders = (visitor) => visitor?.ip ? {
|
|
26
|
+
"findlane-visitor-ip": visitor.ip.trim(),
|
|
27
|
+
...visitor.userAgent ? { "findlane-visitor-agent": visitor.userAgent.replace(/[^\x20-\x7e]/g, "").slice(0, 512) } : {}
|
|
28
|
+
} : {};
|
|
24
29
|
|
|
25
30
|
class FindlaneError extends Error {
|
|
26
31
|
status;
|
|
@@ -47,12 +52,13 @@ class SearchServerClient {
|
|
|
47
52
|
}
|
|
48
53
|
index(name) {
|
|
49
54
|
const path = `/api/teams/${encodeURIComponent(this.workspaceId)}/indexes/${encodeURIComponent(name)}`;
|
|
50
|
-
const request = async (suffix, method = "GET", payload, signal) => {
|
|
55
|
+
const request = async (suffix, method = "GET", payload, signal, extra = {}) => {
|
|
51
56
|
const response = await this.fetcher(this.baseUrl + path + suffix, {
|
|
52
57
|
method,
|
|
53
58
|
headers: {
|
|
54
59
|
authorization: `Bearer ${this.key}`,
|
|
55
|
-
...payload === undefined ? {} : { "content-type": "application/json" }
|
|
60
|
+
...payload === undefined ? {} : { "content-type": "application/json" },
|
|
61
|
+
...extra
|
|
56
62
|
},
|
|
57
63
|
body: payload === undefined ? undefined : JSON.stringify(payload),
|
|
58
64
|
signal
|
|
@@ -96,8 +102,18 @@ class SearchServerClient {
|
|
|
96
102
|
return retry(() => request("/record-writes", "POST", payload, options.signal), options.signal);
|
|
97
103
|
};
|
|
98
104
|
return {
|
|
99
|
-
search: (query = {},
|
|
100
|
-
|
|
105
|
+
search: (query = {}, options) => {
|
|
106
|
+
const { signal, visitor } = requestOptions(options);
|
|
107
|
+
return request("/search", "POST", query, signal, visitorHeaders(visitor));
|
|
108
|
+
},
|
|
109
|
+
suggest: async (query, options) => {
|
|
110
|
+
const { signal, visitor } = requestOptions(options);
|
|
111
|
+
return (await request("/suggest", "POST", query, signal, visitorHeaders(visitor))).didYouMean;
|
|
112
|
+
},
|
|
113
|
+
recommendations: (objectID, options = {}, requestOptionsOrSignal) => {
|
|
114
|
+
const { signal, visitor } = requestOptions(requestOptionsOrSignal);
|
|
115
|
+
return request("/recommendations", "POST", { objectID, ...options }, signal, visitorHeaders(visitor));
|
|
116
|
+
},
|
|
101
117
|
upsert: (records, signal) => request("/records", "POST", { records }, signal),
|
|
102
118
|
getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
|
|
103
119
|
upsertOne: (record, options) => writeOne("upsert", record.objectID, record, options),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@findlane/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|