-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathindex.ts
More file actions
327 lines (304 loc) · 9.71 KB
/
Copy pathindex.ts
File metadata and controls
327 lines (304 loc) · 9.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
import type {
FunctionReference,
FunctionReturnType,
OptionalRestArgs,
} from "convex/server";
import type { Point, Primitive, Rectangle } from "../component/types.js";
import { point, rectangle } from "../component/types.js";
import { LOG_LEVELS, type LogLevel } from "../component/lib/logging.js";
import { FilterBuilderImpl, type GeospatialQuery } from "./query.js";
import type { ComponentApi } from "../component/_generated/component.js";
export type { Point, Primitive, GeospatialQuery, Rectangle };
export { point, rectangle };
declare global {
const Convex: Record<string, unknown>;
}
if (typeof Convex === "undefined") {
throw new Error(
"this is Convex backend code, but it's running somewhere else!",
);
}
export const DEFAULT_MIN_LEVEL = 4;
export const DEFAULT_MAX_LEVEL = 16;
export const DEFAULT_MAX_CELLS = 8;
export const DEFAULT_LEVEL_MOD = 2;
export type GeospatialFilters = Record<string, Primitive | Primitive[]>;
export type GeospatialDocument<
Key extends string = string,
Filters extends GeospatialFilters = GeospatialFilters,
> = {
key: Key;
coordinates: Point;
filterKeys: Filters;
sortKey: number;
};
export type NearestQueryOptions<
Doc extends GeospatialDocument = GeospatialDocument,
> = {
point: Point;
limit: number;
maxDistance?: number;
filter?: NonNullable<GeospatialQuery<Doc>["filter"]>;
};
/**
* @deprecated Use `NearestQueryOptions` with `nearest` instead.
*/
export type QueryNearestOptions<
Doc extends GeospatialDocument = GeospatialDocument,
> = Pick<NearestQueryOptions<Doc>, "maxDistance" | "filter">;
export interface GeospatialIndexOptions {
/**
* The minimum S2 cell level to use when querying. Defaults to 4.
*/
minLevel?: number;
/**
* The maximum S2 cell level to use when querying. Defaults to 16.
*/
maxLevel?: number;
/**
* The distance between levels when indexing, implying a branching factor of `4^levelMod`. Defaults to 2.
*/
levelMod?: number;
/**
* The maximum number of cells to use when querying. Defaults to 8.
*/
maxCells?: number;
/**
* The log level to use when logging. Defaults to the `GEOSPATIAL_LOG_LEVEL` environment variable, or "INFO" if not set.
*/
logLevel?: LogLevel;
}
export class GeospatialIndex<
Key extends string = string,
Filters extends GeospatialFilters = GeospatialFilters,
> {
logLevel: LogLevel;
minLevel: number;
maxLevel: number;
levelMod: number;
maxCells: number;
/**
* Create a new geospatial index, powered by S2 and Convex. This index maps unique string keys to geographic coordinates
* on the Earth's surface, with the ability to efficiently query for all keys within a given geographic area.
*
* @param component - The registered geospatial index from `components`.
* @param options - The options to configure the index.
*/
constructor(
private component: ComponentApi,
options?: GeospatialIndexOptions,
) {
let DEFAULT_LOG_LEVEL: LogLevel = "INFO";
if (process.env.GEOSPATIAL_LOG_LEVEL) {
if (LOG_LEVELS.includes(process.env.GEOSPATIAL_LOG_LEVEL)) {
DEFAULT_LOG_LEVEL = process.env.GEOSPATIAL_LOG_LEVEL as LogLevel;
} else {
console.warn(
`Invalid log level (${process.env.GEOSPATIAL_LOG_LEVEL}), defaulting to "${DEFAULT_LOG_LEVEL}"`,
);
}
}
this.logLevel = options?.logLevel ?? DEFAULT_LOG_LEVEL;
this.minLevel = options?.minLevel ?? DEFAULT_MIN_LEVEL;
this.maxLevel = options?.maxLevel ?? DEFAULT_MAX_LEVEL;
this.levelMod = options?.levelMod ?? DEFAULT_LEVEL_MOD;
this.maxCells = options?.maxCells ?? DEFAULT_MAX_CELLS;
}
/**
* Insert a new key-coordinate pair into the index.
*
* @param ctx - The Convex mutation context.
* @param key - The unique string key to associate with the coordinate.
* @param coordinates - The geographic coordinate `{ latitude, longitude }` to associate with the key.
* @param filterKeys - The filter keys to associate with the key.
* @param sortKey - The sort key to associate with the key, defaults to a randomly generated number.
*/
async insert(
ctx: MutationCtx,
key: Key,
coordinates: Point,
filterKeys: Filters,
sortKey?: number,
) {
await ctx.runMutation(this.component.document.insert, {
document: {
key,
coordinates,
filterKeys,
sortKey: sortKey ?? Math.random(),
},
minLevel: this.minLevel,
maxLevel: this.maxLevel,
levelMod: this.levelMod,
maxCells: this.maxCells,
});
}
/**
* Retrieve the coordinate associated with a specific key.
*
* @param ctx - The Convex query context.
* @param key - The unique string key to retrieve the coordinate for.
* @returns - The geographic coordinate `{ latitude, longitude }` associated with the key, or `null` if the key is not found.
*/
async get(
ctx: QueryCtx,
key: Key,
): Promise<GeospatialDocument<Key, Filters> | null> {
const result = await ctx.runQuery(this.component.document.get, { key });
return result as GeospatialDocument<Key, Filters> | null;
}
/**
* Remove a key-coordinate pair from the index.
*
* @param ctx - The Convex mutation context.
* @param key - The unique string key to remove from the index.
* @returns - `true` if the key was found and removed, `false` otherwise.
*/
async remove(ctx: MutationCtx, key: Key): Promise<boolean> {
return await ctx.runMutation(this.component.document.remove, {
key,
minLevel: this.minLevel,
maxLevel: this.maxLevel,
levelMod: this.levelMod,
maxCells: this.maxCells,
});
}
/**
* Query for keys within a given shape.
*
* @param ctx - The Convex query context.
* @param query - The query to execute.
* @param cursor - The continuation cursor to use for paginating through results.
* @returns - An array of objects with the key-coordinate pairs and optionally a continuation cursor.
*/
async query(
ctx: QueryCtx,
query: GeospatialQuery<GeospatialDocument<Key, Filters>>,
cursor: string | undefined = undefined,
) {
const filterBuilder = new FilterBuilderImpl<
GeospatialDocument<Key, Filters>
>();
if (query.filter) {
query.filter(filterBuilder);
}
const resp = await ctx.runQuery(this.component.query.execute, {
query: {
rectangle: query.shape.rectangle,
filtering: filterBuilder.filterConditions,
sorting: { interval: filterBuilder.interval ?? {} },
maxResults: query.limit ?? 64,
},
cursor,
minLevel: this.minLevel,
maxLevel: this.maxLevel,
levelMod: this.levelMod,
maxCells: this.maxCells,
logLevel: this.logLevel,
});
return resp as {
results: { key: Key; coordinates: Point }[];
nextCursor?: string;
};
}
/**
* Query for the nearest points to a given point.
*
* @param ctx - The Convex query context.
* @param options - The nearest query parameters.
* @returns - An array of objects with the key-coordinate pairs and their distance from the query point in meters.
*/
async nearest(
ctx: QueryCtx,
{
point,
limit,
maxDistance,
filter,
}: NearestQueryOptions<GeospatialDocument<Key, Filters>>,
) {
const filterBuilder = new FilterBuilderImpl<
GeospatialDocument<Key, Filters>
>();
if (filter) {
filter(filterBuilder);
}
const resp = await ctx.runQuery(this.component.query.nearestPoints, {
point,
maxDistance,
maxResults: limit,
minLevel: this.minLevel,
maxLevel: this.maxLevel,
levelMod: this.levelMod,
logLevel: this.logLevel,
filtering: filterBuilder.filterConditions,
sorting: { interval: filterBuilder.interval ?? {} },
});
return resp as { key: Key; coordinates: Point; distance: number }[];
}
/**
* Query for the nearest points to a given point.
*
* @deprecated Use `nearest(ctx, { point, limit, maxDistance, filter })` instead.
*/
async queryNearest(
ctx: QueryCtx,
point: Point,
maxResults: number,
maxDistance?: number,
) {
return this.nearest(ctx, {
point,
limit: maxResults,
maxDistance,
});
}
/**
* Debug the S2 cells that would be queried for a given rectangle.
*
* @param ctx - The Convex query context.
* @param rectangle - The geographic area to query.
* @param maxResolution - The maximum resolution to use when querying.
* @returns - An array of S2 cell identifiers and their vertices.
*/
async debugCells(
ctx: QueryCtx,
rectangle: Rectangle,
maxResolution?: number,
): Promise<{ token: string; vertices: Point[] }[]> {
const resp = await ctx.runQuery(this.component.query.debugCells, {
rectangle,
minLevel: this.minLevel,
maxLevel: maxResolution ?? this.maxLevel,
levelMod: this.levelMod,
maxCells: this.maxCells,
});
return resp;
}
}
export type FilterValue<
Doc extends GeospatialDocument,
FieldName extends keyof Doc["filterKeys"],
> = ExtractArray<Doc["filterKeys"][FieldName]>;
type QueryCtx = {
runQuery: <Query extends FunctionReference<"query", "public" | "internal">>(
query: Query,
...args: OptionalRestArgs<Query>
) => Promise<FunctionReturnType<Query>>;
};
type MutationCtx = {
runMutation: <
Mutation extends FunctionReference<"mutation", "public" | "internal">,
>(
mutation: Mutation,
...args: OptionalRestArgs<Mutation>
) => Promise<FunctionReturnType<Mutation>>;
} & QueryCtx;
export type FilterObject<Doc extends GeospatialDocument> = {
[K in keyof Doc["filterKeys"] & string]: {
filterKey: K;
filterValue: ExtractArray<Doc["filterKeys"][K]>;
occur: "should" | "must";
};
}[keyof Doc["filterKeys"] & string];
type ExtractArray<T> = T extends (infer U)[] ? U : T;