@pulseindex/sdk 4.0.1 → 5.0.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/CHANGELOG.md +100 -0
- package/dist/index.d.mts +101 -19
- package/dist/index.d.ts +101 -19
- package/dist/index.js +167 -49
- package/dist/index.mjs +167 -49
- package/package.json +1 -1
- package/proto/engine.proto +107 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.0.0
|
|
4
|
+
|
|
5
|
+
Needs a PulseIndex engine at v2.0.0 or later. The wire contract is a
|
|
6
|
+
breaking change: an SDK at 4.x cannot talk to a v2 engine, and this cannot
|
|
7
|
+
talk to a v1 one.
|
|
8
|
+
|
|
9
|
+
### A radius that means what it says, and "the nearest K"
|
|
10
|
+
|
|
11
|
+
A circle used to be a union of geohash cells, and a union of cells is a superset
|
|
12
|
+
of the circle. Measured against a million entities, a 1 km search returned
|
|
13
|
+
**2,479** rows where **1,241** were really inside — and nothing in the answer
|
|
14
|
+
said which. You hydrated all 2,479 from your own store and measured them again.
|
|
15
|
+
|
|
16
|
+
Send a position with the record and the engine settles the edge itself:
|
|
17
|
+
|
|
18
|
+
points: { where: { lat: 41.0369, lon: 28.9850 } }
|
|
19
|
+
|
|
20
|
+
Then `withinRadius({ ..., field: 'where' })` narrows on the cells and measures
|
|
21
|
+
the true distance, or `within('where', lat, lon, km)` measures without them.
|
|
22
|
+
Degrees go on the wire and the engine packs them: a representation split between
|
|
23
|
+
this package and the engine, with nothing comparing the two, is how the geo
|
|
24
|
+
defects in 4.0.0 happened.
|
|
25
|
+
|
|
26
|
+
`nearest('where', lat, lon)` orders by distance, nearest first. That was not a
|
|
27
|
+
question you could ask before at any price. It needs no radius guessed to make
|
|
28
|
+
it quick, and combines with every other filter, so "the closest ten available
|
|
29
|
+
drivers with room" is one request.
|
|
30
|
+
|
|
31
|
+
Ordering is to the centimetre, which is the precision a stored position has.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### The result count now says whether it is the whole count
|
|
35
|
+
|
|
36
|
+
A paged search stops as soon as the page is full, so the `totalMatches` it
|
|
37
|
+
carried was only what the engine had reached by then. Nothing said so, and it
|
|
38
|
+
does not look partial: measured on 500,000 records that all matched, a page of
|
|
39
|
+
20 reported **65,536**; a harder query reported **48,105** against a true total
|
|
40
|
+
of **333,895**. Any interface printing "N results" from that was wrong by
|
|
41
|
+
several times over and looked fine.
|
|
42
|
+
|
|
43
|
+
`totalIsExact` now comes from the engine instead of being guessed from
|
|
44
|
+
`limit === 0`, and `searchWithTotal()` is **one request instead of two** — the
|
|
45
|
+
wire can ask for a page and a true count together. A page whose matches all fit
|
|
46
|
+
inside it is reported exact, which the old guess got wrong.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### A record carries whatever numbers you name, and no field the engine chose
|
|
50
|
+
|
|
51
|
+
An entity used to be forced through one `uint32` called `price` and one `uint64`
|
|
52
|
+
bitfield called `locationPrefix`. That is a schema this SDK had no business
|
|
53
|
+
imposing: one number per record, under a name we picked, with no negatives, and
|
|
54
|
+
nothing above 4,294,967,295. A rating, a capacity, a timestamp, an elevation and
|
|
55
|
+
a balance were all the same field or no field at all.
|
|
56
|
+
|
|
57
|
+
Both are gone. A record now carries `numbers`, under your own names:
|
|
58
|
+
|
|
59
|
+
client.index('1001', {
|
|
60
|
+
categories: ['feature:pool'],
|
|
61
|
+
numbers: { price_cents: 45000, bedrooms: 3, built_at: 1712000000 },
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
Any name, any 64-bit integer, any number of them, and every one is filterable
|
|
65
|
+
through `range()` and orderable through `sortBy()`. A name means nothing to the
|
|
66
|
+
engine beyond its hash.
|
|
67
|
+
|
|
68
|
+
### Three things this fixes that were losing data quietly
|
|
69
|
+
|
|
70
|
+
**A fraction was floored without a word.** `4.3` was sent as `4`, `0.5` as `0`,
|
|
71
|
+
`199.99` as `199` — measured, not inferred. The engine's column is a 64-bit
|
|
72
|
+
integer, so a fraction is refused now, with the scaling it needs named. Keep the
|
|
73
|
+
scale on your side: a price in cents, a rating out of 100.
|
|
74
|
+
|
|
75
|
+
**A field of your own called `price` disappeared.** Sixteen key names were
|
|
76
|
+
reserved out of your attributes, and anything under one was dropped: measured,
|
|
77
|
+
`{price: 250, rating: 4.3, lat: 41, kind: 'villa'}` came out as
|
|
78
|
+
`["rating:4.3", "kind:villa"]` — `price` and `lat` gone, no error. Numbers go
|
|
79
|
+
through `numbers` now, so nothing in your own object is swallowed.
|
|
80
|
+
|
|
81
|
+
**A range or an order on a field nothing carries was answered, not refused.**
|
|
82
|
+
It excluded every entity, or left the page in insertion order and reported it as
|
|
83
|
+
sorted. At ten million records `bedrooms 3..6` returned 0 while the tag
|
|
84
|
+
`bedrooms:3` returned 1,666,667. The engine refuses it by name now — but only
|
|
85
|
+
when the tenant holds entities and none of them carries that field, because an
|
|
86
|
+
empty tenant has nothing to be wrong about.
|
|
87
|
+
|
|
88
|
+
### Migrating
|
|
89
|
+
|
|
90
|
+
- `index(id, {price: N})` → `index(id, {numbers: {price: N}})`. A top-level
|
|
91
|
+
`price` is no longer special; it becomes the tag `price:N` like any other
|
|
92
|
+
scalar attribute.
|
|
93
|
+
- `indexEntity(id, categories, price, locationPrefix, tenantId)` →
|
|
94
|
+
`indexEntity(id, categories, numbers, tenantId)`.
|
|
95
|
+
- `.location(prefix)` on the query builder is gone. Nothing ever sent it: both
|
|
96
|
+
SDKs passed 0 on every request.
|
|
97
|
+
- A range bound may now be negative or past 4,294,967,295.
|
|
98
|
+
|
|
99
|
+
This needs an engine built from the same commit. The proto is a breaking change:
|
|
100
|
+
field numbers 2 and 3 on `IndexEntityRequest`, and 1 on `SearchQueryRequest`, are
|
|
101
|
+
reserved rather than reused.
|
|
102
|
+
|
|
3
103
|
## 4.0.1
|
|
4
104
|
|
|
5
105
|
### The covering threshold, corrected against a real app
|
package/dist/index.d.mts
CHANGED
|
@@ -27,19 +27,38 @@ interface SortSpec {
|
|
|
27
27
|
field: string;
|
|
28
28
|
/** Largest first when true; smallest first otherwise. */
|
|
29
29
|
descending: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Order by distance from the query's geo predicate instead of by a field
|
|
32
|
+
* value. `field` is ignored.
|
|
33
|
+
*
|
|
34
|
+
* Ordering is to the centimetre, which is the precision a stored position
|
|
35
|
+
* has. Rows closer together than that tie, and a tie breaks on the entity id
|
|
36
|
+
* so the same query returns the same page.
|
|
37
|
+
*/
|
|
38
|
+
byDistance?: boolean;
|
|
30
39
|
}
|
|
31
40
|
interface RangePredicate {
|
|
32
41
|
field: string;
|
|
33
42
|
minVal: number;
|
|
34
43
|
maxVal: number;
|
|
35
44
|
}
|
|
45
|
+
/** A circle, and the position field to measure it against. */
|
|
46
|
+
interface GeoPredicate {
|
|
47
|
+
field: string;
|
|
48
|
+
lat: number;
|
|
49
|
+
lon: number;
|
|
50
|
+
/** Inclusive. 0 means no radius bound, only an origin to measure from. */
|
|
51
|
+
radiusKm: number;
|
|
52
|
+
}
|
|
36
53
|
interface SearchQueryRequest {
|
|
37
|
-
locationPrefix: string;
|
|
38
54
|
filters: FilterPredicate[];
|
|
55
|
+
geo?: GeoPredicate;
|
|
39
56
|
ranges: RangePredicate[];
|
|
40
57
|
limit: number;
|
|
41
58
|
offset: number;
|
|
42
59
|
tenantId: string;
|
|
60
|
+
/** Count every match rather than stopping when the page is full. */
|
|
61
|
+
exactTotal: boolean;
|
|
43
62
|
/** Absent returns matches in entity-id order. */
|
|
44
63
|
sort?: SortSpec;
|
|
45
64
|
}
|
|
@@ -61,8 +80,8 @@ interface SearchResponse {
|
|
|
61
80
|
}
|
|
62
81
|
interface IndexEntityRequest {
|
|
63
82
|
entityId: string;
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
numbers: Record<string, number>;
|
|
84
|
+
points: Record<string, GeoPoint>;
|
|
66
85
|
categories: string[];
|
|
67
86
|
tenantId: string;
|
|
68
87
|
}
|
|
@@ -88,10 +107,21 @@ interface RadiusOptions {
|
|
|
88
107
|
lon?: number;
|
|
89
108
|
radiusKm: number;
|
|
90
109
|
precision?: number;
|
|
110
|
+
/**
|
|
111
|
+
* The position field to measure against, as named in `points` when indexing.
|
|
112
|
+
*
|
|
113
|
+
* Given one, the engine narrows on the geohash cells and then measures the
|
|
114
|
+
* true distance, so the answer holds only what is really inside the circle.
|
|
115
|
+
* Without it the cells are the whole answer, and a union of cells is a
|
|
116
|
+
* superset: measured at a million entities, a 1 km search returned 2,479
|
|
117
|
+
* rows where 1,241 were inside.
|
|
118
|
+
*/
|
|
119
|
+
field?: string;
|
|
91
120
|
}
|
|
92
121
|
interface SearchRequestOptions {
|
|
93
122
|
tenantId?: string;
|
|
94
|
-
|
|
123
|
+
/** Count every match rather than stopping when the page is full. */
|
|
124
|
+
exactTotal?: boolean;
|
|
95
125
|
must?: string | string[];
|
|
96
126
|
should?: string | string[];
|
|
97
127
|
mustNot?: string | string[];
|
|
@@ -118,9 +148,7 @@ interface SearchRequestOptions {
|
|
|
118
148
|
interface EntityAttributes {
|
|
119
149
|
categories?: unknown;
|
|
120
150
|
tags?: unknown;
|
|
121
|
-
|
|
122
|
-
locationPrefix?: unknown;
|
|
123
|
-
location_prefix?: unknown;
|
|
151
|
+
numbers?: unknown;
|
|
124
152
|
tenantId?: unknown;
|
|
125
153
|
tenant_id?: unknown;
|
|
126
154
|
latitude?: unknown;
|
|
@@ -137,9 +165,7 @@ interface EntityInput {
|
|
|
137
165
|
attributes?: EntityAttributes;
|
|
138
166
|
categories?: unknown;
|
|
139
167
|
tags?: unknown;
|
|
140
|
-
|
|
141
|
-
locationPrefix?: unknown;
|
|
142
|
-
location_prefix?: unknown;
|
|
168
|
+
numbers?: unknown;
|
|
143
169
|
tenantId?: unknown;
|
|
144
170
|
tenant_id?: unknown;
|
|
145
171
|
latitude?: unknown;
|
|
@@ -156,11 +182,21 @@ interface BatchEntityInput {
|
|
|
156
182
|
attributes?: EntityAttributes;
|
|
157
183
|
[key: string]: unknown;
|
|
158
184
|
}
|
|
185
|
+
/** One position in degrees. The engine packs it; this SDK does not. */
|
|
186
|
+
interface GeoPoint {
|
|
187
|
+
lat: number;
|
|
188
|
+
lon: number;
|
|
189
|
+
}
|
|
159
190
|
interface EncodedEntity {
|
|
160
191
|
entityId: string;
|
|
161
192
|
categories: string[];
|
|
162
|
-
|
|
163
|
-
|
|
193
|
+
/** Positions under your own names. */
|
|
194
|
+
points: Record<string, GeoPoint>;
|
|
195
|
+
/**
|
|
196
|
+
* Numeric fields under your own names. Any name, any integer, any number of
|
|
197
|
+
* them. This replaced a single `price` field the engine named for you.
|
|
198
|
+
*/
|
|
199
|
+
numbers: Record<string, number>;
|
|
164
200
|
tenantId: string;
|
|
165
201
|
}
|
|
166
202
|
interface PulseIndexClientConfig {
|
|
@@ -198,7 +234,15 @@ declare class QueryBuilder {
|
|
|
198
234
|
private state;
|
|
199
235
|
constructor(executor?: QueryExecutor | null);
|
|
200
236
|
tenant(tenantId: string): QueryBuilder;
|
|
201
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Count every match instead of stopping as soon as the page is full.
|
|
239
|
+
*
|
|
240
|
+
* A paged search stops early, so the `totalMatches` it carries is only what
|
|
241
|
+
* the engine had counted by then — a lower bound, and one that does not look
|
|
242
|
+
* like one. This makes the count exact in the same request; `totalIsExact`
|
|
243
|
+
* on the response says which you got.
|
|
244
|
+
*/
|
|
245
|
+
exactTotal(enabled?: boolean): QueryBuilder;
|
|
202
246
|
must(attribute: string | string[]): QueryBuilder;
|
|
203
247
|
/**
|
|
204
248
|
* At least one of these has to match.
|
|
@@ -216,6 +260,15 @@ declare class QueryBuilder {
|
|
|
216
260
|
inGeoHash(geohash: string): QueryBuilder;
|
|
217
261
|
withinRadius(lat: number, lon: number, radiusKm: number, precision?: number): QueryBuilder;
|
|
218
262
|
withinRadius(options: RadiusOptions): QueryBuilder;
|
|
263
|
+
/**
|
|
264
|
+
* Filter on a numeric field's inclusive range.
|
|
265
|
+
*
|
|
266
|
+
* `price` is the only number an entity carries. Naming any other field is
|
|
267
|
+
* refused by the engine rather than answered, because a field nothing carries
|
|
268
|
+
* can only match nothing, and an empty page looks exactly like a real one.
|
|
269
|
+
* Model any other number as a category token instead: `must('bedrooms:3')`,
|
|
270
|
+
* or several in one SHOULD group for a range of values.
|
|
271
|
+
*/
|
|
219
272
|
range(field: string, min: number, max: number): QueryBuilder;
|
|
220
273
|
/**
|
|
221
274
|
* How many ids to return. Zero asks the engine for the number of matches
|
|
@@ -234,11 +287,31 @@ declare class QueryBuilder {
|
|
|
234
287
|
/** Order the page by a numeric field, largest first. */
|
|
235
288
|
sortDesc(field: string): QueryBuilder;
|
|
236
289
|
/**
|
|
237
|
-
* Order the page by a numeric field.
|
|
238
|
-
*
|
|
239
|
-
*
|
|
290
|
+
* Order the page by a numeric field.
|
|
291
|
+
*
|
|
292
|
+
* Bounded exactly as {@link range} is: `price` is the only field an entity
|
|
293
|
+
* carries, and any other name is refused rather than silently ignored. An
|
|
294
|
+
* order by a field nothing carries used to leave the page in insertion order
|
|
295
|
+
* and report it as sorted.
|
|
240
296
|
*/
|
|
241
297
|
sortBy(field: string, descending?: boolean): QueryBuilder;
|
|
298
|
+
/**
|
|
299
|
+
* Keep only entities within `radiusKm` of the point, measured exactly.
|
|
300
|
+
*
|
|
301
|
+
* This is the circle on its own. {@link withinRadius} with a `field` adds
|
|
302
|
+
* the geohash cells too, which is what stops the engine opening every part
|
|
303
|
+
* of the index to find them.
|
|
304
|
+
*/
|
|
305
|
+
within(field: string, lat: number, lon: number, radiusKm: number): QueryBuilder;
|
|
306
|
+
/**
|
|
307
|
+
* Order the page by distance from the point, nearest first.
|
|
308
|
+
*
|
|
309
|
+
* Without a radius this is "the nearest K of whatever else matched"; combine
|
|
310
|
+
* it with {@link within} or {@link withinRadius} to bound the search as well.
|
|
311
|
+
* It used to be impossible: a radius returned everything inside it unordered,
|
|
312
|
+
* so you hydrated every id from your own store before you could sort them.
|
|
313
|
+
*/
|
|
314
|
+
nearest(field: string, lat: number, lon: number): QueryBuilder;
|
|
242
315
|
toRequest(defaultTenantId?: string): SearchQueryRequest;
|
|
243
316
|
toArray(defaultTenantId?: string): SearchQueryRequest;
|
|
244
317
|
execute(): Promise<SearchResponse>;
|
|
@@ -318,12 +391,21 @@ declare class PulseIndexClient implements QueryExecutor {
|
|
|
318
391
|
* reported 10,866 for a page of 100. Anything that prints "page 1 of N" from
|
|
319
392
|
* that number is wrong by an order of magnitude and looks fine.
|
|
320
393
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
394
|
+
* One request. This used to run the whole query twice — once for the page,
|
|
395
|
+
* once for the count — because the wire had no way to ask for both. It does
|
|
396
|
+
* now, so this is the same round trip with `exactTotal` set, and the total
|
|
397
|
+
* you get back can be divided by a page size.
|
|
323
398
|
*/
|
|
324
399
|
searchWithTotal(query: QueryBuilder | SearchRequestOptions): Promise<SearchResponse>;
|
|
325
400
|
index(entityIdOrInput: EntityId | EntityInput, attributes?: EntityAttributes): Promise<IndexEntityResponse>;
|
|
326
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Index one record.
|
|
403
|
+
*
|
|
404
|
+
* `numbers` are yours to name: `{price_cents: 45000, bedrooms: 3}`. This
|
|
405
|
+
* used to take a single `price` and a `locationPrefix`, which was a schema
|
|
406
|
+
* this SDK had no business imposing.
|
|
407
|
+
*/
|
|
408
|
+
indexEntity(entityId: EntityId, categories?: string[], numbers?: Record<string, number>, tenantId?: string): Promise<boolean>;
|
|
327
409
|
batchIndex(entities: Array<EntityInput | BatchEntityInput>): Promise<BatchIndexResponse>;
|
|
328
410
|
delete(entityId: EntityId, tenantId?: string): Promise<DeleteResponse>;
|
|
329
411
|
deleteEntity(entityId: EntityId, tenantId?: string): Promise<boolean>;
|
package/dist/index.d.ts
CHANGED
|
@@ -27,19 +27,38 @@ interface SortSpec {
|
|
|
27
27
|
field: string;
|
|
28
28
|
/** Largest first when true; smallest first otherwise. */
|
|
29
29
|
descending: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Order by distance from the query's geo predicate instead of by a field
|
|
32
|
+
* value. `field` is ignored.
|
|
33
|
+
*
|
|
34
|
+
* Ordering is to the centimetre, which is the precision a stored position
|
|
35
|
+
* has. Rows closer together than that tie, and a tie breaks on the entity id
|
|
36
|
+
* so the same query returns the same page.
|
|
37
|
+
*/
|
|
38
|
+
byDistance?: boolean;
|
|
30
39
|
}
|
|
31
40
|
interface RangePredicate {
|
|
32
41
|
field: string;
|
|
33
42
|
minVal: number;
|
|
34
43
|
maxVal: number;
|
|
35
44
|
}
|
|
45
|
+
/** A circle, and the position field to measure it against. */
|
|
46
|
+
interface GeoPredicate {
|
|
47
|
+
field: string;
|
|
48
|
+
lat: number;
|
|
49
|
+
lon: number;
|
|
50
|
+
/** Inclusive. 0 means no radius bound, only an origin to measure from. */
|
|
51
|
+
radiusKm: number;
|
|
52
|
+
}
|
|
36
53
|
interface SearchQueryRequest {
|
|
37
|
-
locationPrefix: string;
|
|
38
54
|
filters: FilterPredicate[];
|
|
55
|
+
geo?: GeoPredicate;
|
|
39
56
|
ranges: RangePredicate[];
|
|
40
57
|
limit: number;
|
|
41
58
|
offset: number;
|
|
42
59
|
tenantId: string;
|
|
60
|
+
/** Count every match rather than stopping when the page is full. */
|
|
61
|
+
exactTotal: boolean;
|
|
43
62
|
/** Absent returns matches in entity-id order. */
|
|
44
63
|
sort?: SortSpec;
|
|
45
64
|
}
|
|
@@ -61,8 +80,8 @@ interface SearchResponse {
|
|
|
61
80
|
}
|
|
62
81
|
interface IndexEntityRequest {
|
|
63
82
|
entityId: string;
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
numbers: Record<string, number>;
|
|
84
|
+
points: Record<string, GeoPoint>;
|
|
66
85
|
categories: string[];
|
|
67
86
|
tenantId: string;
|
|
68
87
|
}
|
|
@@ -88,10 +107,21 @@ interface RadiusOptions {
|
|
|
88
107
|
lon?: number;
|
|
89
108
|
radiusKm: number;
|
|
90
109
|
precision?: number;
|
|
110
|
+
/**
|
|
111
|
+
* The position field to measure against, as named in `points` when indexing.
|
|
112
|
+
*
|
|
113
|
+
* Given one, the engine narrows on the geohash cells and then measures the
|
|
114
|
+
* true distance, so the answer holds only what is really inside the circle.
|
|
115
|
+
* Without it the cells are the whole answer, and a union of cells is a
|
|
116
|
+
* superset: measured at a million entities, a 1 km search returned 2,479
|
|
117
|
+
* rows where 1,241 were inside.
|
|
118
|
+
*/
|
|
119
|
+
field?: string;
|
|
91
120
|
}
|
|
92
121
|
interface SearchRequestOptions {
|
|
93
122
|
tenantId?: string;
|
|
94
|
-
|
|
123
|
+
/** Count every match rather than stopping when the page is full. */
|
|
124
|
+
exactTotal?: boolean;
|
|
95
125
|
must?: string | string[];
|
|
96
126
|
should?: string | string[];
|
|
97
127
|
mustNot?: string | string[];
|
|
@@ -118,9 +148,7 @@ interface SearchRequestOptions {
|
|
|
118
148
|
interface EntityAttributes {
|
|
119
149
|
categories?: unknown;
|
|
120
150
|
tags?: unknown;
|
|
121
|
-
|
|
122
|
-
locationPrefix?: unknown;
|
|
123
|
-
location_prefix?: unknown;
|
|
151
|
+
numbers?: unknown;
|
|
124
152
|
tenantId?: unknown;
|
|
125
153
|
tenant_id?: unknown;
|
|
126
154
|
latitude?: unknown;
|
|
@@ -137,9 +165,7 @@ interface EntityInput {
|
|
|
137
165
|
attributes?: EntityAttributes;
|
|
138
166
|
categories?: unknown;
|
|
139
167
|
tags?: unknown;
|
|
140
|
-
|
|
141
|
-
locationPrefix?: unknown;
|
|
142
|
-
location_prefix?: unknown;
|
|
168
|
+
numbers?: unknown;
|
|
143
169
|
tenantId?: unknown;
|
|
144
170
|
tenant_id?: unknown;
|
|
145
171
|
latitude?: unknown;
|
|
@@ -156,11 +182,21 @@ interface BatchEntityInput {
|
|
|
156
182
|
attributes?: EntityAttributes;
|
|
157
183
|
[key: string]: unknown;
|
|
158
184
|
}
|
|
185
|
+
/** One position in degrees. The engine packs it; this SDK does not. */
|
|
186
|
+
interface GeoPoint {
|
|
187
|
+
lat: number;
|
|
188
|
+
lon: number;
|
|
189
|
+
}
|
|
159
190
|
interface EncodedEntity {
|
|
160
191
|
entityId: string;
|
|
161
192
|
categories: string[];
|
|
162
|
-
|
|
163
|
-
|
|
193
|
+
/** Positions under your own names. */
|
|
194
|
+
points: Record<string, GeoPoint>;
|
|
195
|
+
/**
|
|
196
|
+
* Numeric fields under your own names. Any name, any integer, any number of
|
|
197
|
+
* them. This replaced a single `price` field the engine named for you.
|
|
198
|
+
*/
|
|
199
|
+
numbers: Record<string, number>;
|
|
164
200
|
tenantId: string;
|
|
165
201
|
}
|
|
166
202
|
interface PulseIndexClientConfig {
|
|
@@ -198,7 +234,15 @@ declare class QueryBuilder {
|
|
|
198
234
|
private state;
|
|
199
235
|
constructor(executor?: QueryExecutor | null);
|
|
200
236
|
tenant(tenantId: string): QueryBuilder;
|
|
201
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Count every match instead of stopping as soon as the page is full.
|
|
239
|
+
*
|
|
240
|
+
* A paged search stops early, so the `totalMatches` it carries is only what
|
|
241
|
+
* the engine had counted by then — a lower bound, and one that does not look
|
|
242
|
+
* like one. This makes the count exact in the same request; `totalIsExact`
|
|
243
|
+
* on the response says which you got.
|
|
244
|
+
*/
|
|
245
|
+
exactTotal(enabled?: boolean): QueryBuilder;
|
|
202
246
|
must(attribute: string | string[]): QueryBuilder;
|
|
203
247
|
/**
|
|
204
248
|
* At least one of these has to match.
|
|
@@ -216,6 +260,15 @@ declare class QueryBuilder {
|
|
|
216
260
|
inGeoHash(geohash: string): QueryBuilder;
|
|
217
261
|
withinRadius(lat: number, lon: number, radiusKm: number, precision?: number): QueryBuilder;
|
|
218
262
|
withinRadius(options: RadiusOptions): QueryBuilder;
|
|
263
|
+
/**
|
|
264
|
+
* Filter on a numeric field's inclusive range.
|
|
265
|
+
*
|
|
266
|
+
* `price` is the only number an entity carries. Naming any other field is
|
|
267
|
+
* refused by the engine rather than answered, because a field nothing carries
|
|
268
|
+
* can only match nothing, and an empty page looks exactly like a real one.
|
|
269
|
+
* Model any other number as a category token instead: `must('bedrooms:3')`,
|
|
270
|
+
* or several in one SHOULD group for a range of values.
|
|
271
|
+
*/
|
|
219
272
|
range(field: string, min: number, max: number): QueryBuilder;
|
|
220
273
|
/**
|
|
221
274
|
* How many ids to return. Zero asks the engine for the number of matches
|
|
@@ -234,11 +287,31 @@ declare class QueryBuilder {
|
|
|
234
287
|
/** Order the page by a numeric field, largest first. */
|
|
235
288
|
sortDesc(field: string): QueryBuilder;
|
|
236
289
|
/**
|
|
237
|
-
* Order the page by a numeric field.
|
|
238
|
-
*
|
|
239
|
-
*
|
|
290
|
+
* Order the page by a numeric field.
|
|
291
|
+
*
|
|
292
|
+
* Bounded exactly as {@link range} is: `price` is the only field an entity
|
|
293
|
+
* carries, and any other name is refused rather than silently ignored. An
|
|
294
|
+
* order by a field nothing carries used to leave the page in insertion order
|
|
295
|
+
* and report it as sorted.
|
|
240
296
|
*/
|
|
241
297
|
sortBy(field: string, descending?: boolean): QueryBuilder;
|
|
298
|
+
/**
|
|
299
|
+
* Keep only entities within `radiusKm` of the point, measured exactly.
|
|
300
|
+
*
|
|
301
|
+
* This is the circle on its own. {@link withinRadius} with a `field` adds
|
|
302
|
+
* the geohash cells too, which is what stops the engine opening every part
|
|
303
|
+
* of the index to find them.
|
|
304
|
+
*/
|
|
305
|
+
within(field: string, lat: number, lon: number, radiusKm: number): QueryBuilder;
|
|
306
|
+
/**
|
|
307
|
+
* Order the page by distance from the point, nearest first.
|
|
308
|
+
*
|
|
309
|
+
* Without a radius this is "the nearest K of whatever else matched"; combine
|
|
310
|
+
* it with {@link within} or {@link withinRadius} to bound the search as well.
|
|
311
|
+
* It used to be impossible: a radius returned everything inside it unordered,
|
|
312
|
+
* so you hydrated every id from your own store before you could sort them.
|
|
313
|
+
*/
|
|
314
|
+
nearest(field: string, lat: number, lon: number): QueryBuilder;
|
|
242
315
|
toRequest(defaultTenantId?: string): SearchQueryRequest;
|
|
243
316
|
toArray(defaultTenantId?: string): SearchQueryRequest;
|
|
244
317
|
execute(): Promise<SearchResponse>;
|
|
@@ -318,12 +391,21 @@ declare class PulseIndexClient implements QueryExecutor {
|
|
|
318
391
|
* reported 10,866 for a page of 100. Anything that prints "page 1 of N" from
|
|
319
392
|
* that number is wrong by an order of magnitude and looks fine.
|
|
320
393
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
394
|
+
* One request. This used to run the whole query twice — once for the page,
|
|
395
|
+
* once for the count — because the wire had no way to ask for both. It does
|
|
396
|
+
* now, so this is the same round trip with `exactTotal` set, and the total
|
|
397
|
+
* you get back can be divided by a page size.
|
|
323
398
|
*/
|
|
324
399
|
searchWithTotal(query: QueryBuilder | SearchRequestOptions): Promise<SearchResponse>;
|
|
325
400
|
index(entityIdOrInput: EntityId | EntityInput, attributes?: EntityAttributes): Promise<IndexEntityResponse>;
|
|
326
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Index one record.
|
|
403
|
+
*
|
|
404
|
+
* `numbers` are yours to name: `{price_cents: 45000, bedrooms: 3}`. This
|
|
405
|
+
* used to take a single `price` and a `locationPrefix`, which was a schema
|
|
406
|
+
* this SDK had no business imposing.
|
|
407
|
+
*/
|
|
408
|
+
indexEntity(entityId: EntityId, categories?: string[], numbers?: Record<string, number>, tenantId?: string): Promise<boolean>;
|
|
327
409
|
batchIndex(entities: Array<EntityInput | BatchEntityInput>): Promise<BatchIndexResponse>;
|
|
328
410
|
delete(entityId: EntityId, tenantId?: string): Promise<DeleteResponse>;
|
|
329
411
|
deleteEntity(entityId: EntityId, tenantId?: string): Promise<boolean>;
|