Skip to content

Commit 42ed0d2

Browse files
authored
Merge pull request #25 from mackinleysmith/feat-nearest-filters-via-indexed-filter-ranges
Compute nearest filters via indexed filter ranges. This allows different sets of points to be stored and queried together more efficiently when only a subset of them should be acceptable results..
2 parents 91ebb88 + 9d4e134 commit 42ed0d2

File tree

12 files changed

+400
-63
lines changed

12 files changed

+400
-63
lines changed

README.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ const example = query({
136136
});
137137
```
138138

139+
The legacy `queryNearest` helper now delegates to `nearest` and is deprecated.
140+
It only accepts a `maxDistance` numeric argument for backwards compatibility.
141+
New integrations should prefer `nearest`.
142+
139143
This query will find all points that lie within the query rectangle, sort them
140144
in ascending `sortKey` order, and return at most 16 results.
141145

@@ -265,18 +269,24 @@ const example = query({
265269
handler: async (ctx) => {
266270
const maxResults = 16;
267271
const maxDistance = 10000;
268-
const result = await geospatial.queryNearest(
269-
ctx,
270-
{ latitude: 40.7813, longitude: -73.9737 },
271-
maxResults,
272+
const result = await geospatial.nearest(ctx, {
273+
point: { latitude: 40.7813, longitude: -73.9737 },
274+
limit: maxResults,
272275
maxDistance,
273-
);
276+
filter: (q) => q.eq("category", "coffee"),
277+
});
274278
return result;
275279
},
276280
});
277281
```
278282

279-
The `maxDistance` parameter is optional, but providing it can greatly speed up
283+
The second argument is an options object containing `point`, `limit`, and
284+
optionally `maxDistance` and `filter`. You can combine `maxDistance` with the
285+
same filter builder used by `query`, including `eq`, `in`, `gte`, and `lt`
286+
conditions. These filters are enforced through the indexed `pointsByFilterKey`
287+
range before documents are loaded, so the database does the heavy lifting and
288+
the query avoids reading unrelated points. Pairing that with a sensible
289+
`maxDistance` further constrains the search space and can greatly speed up
280290
searching the index.
281291

282292
## Example

example/convex/example.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,11 @@ export const nearestPoints = query({
2626
maxDistance: v.optional(v.number()),
2727
},
2828
handler: async (ctx, { point, maxRows, maxDistance }) => {
29-
const results = await geospatial.queryNearest(
30-
ctx,
29+
const results = await geospatial.nearest(ctx, {
3130
point,
32-
maxRows,
31+
limit: maxRows,
3332
maxDistance,
34-
);
33+
});
3534
return await Promise.all(
3635
results.map(async (result) => {
3736
const row = await ctx.db.get(result.key as Id<"locations">);

package-lock.json

Lines changed: 2 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,4 @@
106106
},
107107
"types": "./dist/client/index.d.ts",
108108
"module": "./dist/client/index.js"
109-
}
109+
}

src/client/index.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ export type GeospatialDocument<
3737
sortKey: number;
3838
};
3939

40+
export type NearestQueryOptions<
41+
Doc extends GeospatialDocument = GeospatialDocument,
42+
> = {
43+
point: Point;
44+
limit: number;
45+
maxDistance?: number;
46+
filter?: NonNullable<GeospatialQuery<Doc>["filter"]>;
47+
};
48+
49+
/**
50+
* @deprecated Use `NearestQueryOptions` with `nearest` instead.
51+
*/
52+
export type QueryNearestOptions<
53+
Doc extends GeospatialDocument = GeospatialDocument,
54+
> = Pick<NearestQueryOptions<Doc>, "maxDistance" | "filter">;
55+
4056
export interface GeospatialIndexOptions {
4157
/**
4258
* The minimum S2 cell level to use when querying. Defaults to 4.
@@ -207,29 +223,57 @@ export class GeospatialIndex<
207223
* Query for the nearest points to a given point.
208224
*
209225
* @param ctx - The Convex query context.
210-
* @param point - The point to query for.
211-
* @param maxResults - The maximum number of results to return.
212-
* @param maxDistance - The maximum distance to return results within in meters.
226+
* @param options - The nearest query parameters.
213227
* @returns - An array of objects with the key-coordinate pairs and their distance from the query point in meters.
214228
*/
215-
async queryNearest(
229+
async nearest(
216230
ctx: QueryCtx,
217-
point: Point,
218-
maxResults: number,
219-
maxDistance?: number,
231+
{
232+
point,
233+
limit,
234+
maxDistance,
235+
filter,
236+
}: NearestQueryOptions<GeospatialDocument<Key, Filters>>,
220237
) {
238+
const filterBuilder = new FilterBuilderImpl<
239+
GeospatialDocument<Key, Filters>
240+
>();
241+
if (filter) {
242+
filter(filterBuilder);
243+
}
244+
221245
const resp = await ctx.runQuery(this.component.query.nearestPoints, {
222246
point,
223247
maxDistance,
224-
maxResults,
248+
maxResults: limit,
225249
minLevel: this.minLevel,
226250
maxLevel: this.maxLevel,
227251
levelMod: this.levelMod,
228252
logLevel: this.logLevel,
253+
filtering: filterBuilder.filterConditions,
254+
sorting: { interval: filterBuilder.interval ?? {} },
229255
});
230256
return resp as { key: Key; coordinates: Point; distance: number }[];
231257
}
232258

259+
/**
260+
* Query for the nearest points to a given point.
261+
*
262+
* @deprecated Use `nearest(ctx, { point, limit, maxDistance, filter })` instead.
263+
*/
264+
async queryNearest(
265+
ctx: QueryCtx,
266+
point: Point,
267+
maxResults: number,
268+
maxDistance?: number,
269+
) {
270+
return this.nearest(ctx, {
271+
point,
272+
limit: maxResults,
273+
maxDistance,
274+
});
275+
}
276+
233277
/**
234278
* Debug the S2 cells that would be queried for a given rectangle.
235279
*

src/component/_generated/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type * as lib_tupleKey from "../lib/tupleKey.js";
2222
import type * as lib_xxhash from "../lib/xxhash.js";
2323
import type * as query from "../query.js";
2424
import type * as streams_cellRange from "../streams/cellRange.js";
25+
import type * as streams_constants from "../streams/constants.js";
2526
import type * as streams_databaseRange from "../streams/databaseRange.js";
2627
import type * as streams_filterKeyRange from "../streams/filterKeyRange.js";
2728
import type * as streams_intersection from "../streams/intersection.js";
@@ -51,6 +52,7 @@ const fullApi: ApiFromModules<{
5152
"lib/xxhash": typeof lib_xxhash;
5253
query: typeof query;
5354
"streams/cellRange": typeof streams_cellRange;
55+
"streams/constants": typeof streams_constants;
5456
"streams/databaseRange": typeof streams_databaseRange;
5557
"streams/filterKeyRange": typeof streams_filterKeyRange;
5658
"streams/intersection": typeof streams_intersection;

src/component/_generated/component.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
147147
"query",
148148
"internal",
149149
{
150+
filtering: Array<{
151+
filterKey: string;
152+
filterValue: string | number | boolean | null | bigint;
153+
occur: "should" | "must";
154+
}>;
150155
levelMod: number;
151156
logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
152157
maxDistance?: number;
@@ -155,6 +160,9 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
155160
minLevel: number;
156161
nextCursor?: string;
157162
point: { latitude: number; longitude: number };
163+
sorting: {
164+
interval: { endExclusive?: number; startInclusive?: number };
165+
};
158166
},
159167
Array<{
160168
coordinates: { latitude: number; longitude: number };

0 commit comments

Comments
 (0)