Skip to content

Conversation

@mackinleysmith
Copy link

@mackinleysmith mackinleysmith commented Nov 11, 2025

This is a follow-on for #23 -- apologies if there is a better way to set the base branch here, but I couldn't find it, so this contains the content of both PRs.

Summary

  • Wire the queryNearest client helper to the same filter builder used by rectangle queries, carrying filtering and sorting.interval through the Convex query layer.
  • Extend the backend nearest-point execution to intersect indexed FilterKeyRange streams with cell scans so must/should clauses are enforced before document fetches.
  • Refresh generated Convex component bindings to TypeScript, align the README docs with the new API shape, and surface the filter/indexing behavior to users.

Details

  • Client: GeospatialIndex.queryNearest now accepts either a distance or options object and builds filter/sort intervals that are passed to component.query.nearestPoints.
  • Server: ClosestPointQuery factors in filter conditions when choosing cell subdivisions and merges FilterKeyRange streams via Intersection, using PREFETCH_SIZE from the shared constants module.
  • Docs: clarify how indexed filters and maxDistance interact, emphasizing the performance win from narrowing the search space.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Summary by CodeRabbit

  • New Features

    • Added advanced filtering and sort-interval support for geospatial nearest queries and introduced options-based nearest() invocation.
    • Improved streaming/processing for filtered proximity searches to better handle complex filters.
  • Documentation

    • Expanded docs describing nearest() options (point, limit, maxDistance, filter) and filter enforcement/performance notes.
  • Deprecations

    • Deprecated queryNearest(); continues to forward to nearest() for compatibility.
  • Tests

    • Added tests covering must/should filters, sort-intervals, and combined filter behaviors.

✏️ Tip: You can customize this high-level summary in your review settings.

…rting options. Update README to reflect new options for maxDistance and filtering logic. Modify API definitions and implementations to support must/should filter conditions and sorting intervals. Add tests for new filtering functionality in closest point queries.
… and improve performance. Introduce a new buildStreamForCell method to manage filter conditions and optimize data retrieval. Update README to clarify maxDistance usage and filtering logic. Add tests for multiple filter scenarios in closest point queries.
…performance. Replace the buildStreamForCell method with direct database queries for point retrieval, enhancing clarity and efficiency. Introduce new helper methods for filter checks and sorting interval validation.
…etrieval and improving filter checks. Add methods for managing cell processing and fetching point entries in batches, optimizing performance and clarity in the query logic.
…precated filter methods. Introduce FILTER_SUBDIVIDE_THRESHOLD for enhanced performance in point retrieval, streamlining the query process.
…ove point retrieval logic. Introduce CellStreamState for better state handling during processing, and optimize filter checks and batch processing for improved performance.
…necessary type assertions and directly using the context in query executions. This improves code clarity and maintains functionality.
@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 12, 2025

Open in StackBlitz

npm i https://pkg.pr.new/get-convex/geospatial/@convex-dev/geospatial@25

commit: 6f4b7ba

@mackinleysmith
Copy link
Author

@ianmacartney just checking back in, how are we feeling about this?

@ianmacartney
Copy link
Contributor

ianmacartney commented Nov 22, 2025 via email

@mackinleysmith
Copy link
Author

@ianmacartney thanks for the update. I'm here for you if you need anything!

@sethconve sethconve self-requested a review December 3, 2025 20:10
Copy link

@sethconve sethconve left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm taking a look, will post more comments as I get through it!

maxDistanceOrOptions?:
| number
| QueryNearestOptions<GeospatialDocument<Key, Filters>>,
maybeOptions?: QueryNearestOptions<GeospatialDocument<Key, Filters>>,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any concerns about removing maybeOptions, and forcing an options object if you want to specify maxDistance?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sethconve the only concern i had and the reason i did it this way was to try not to make a breaking change for other users of this package. i think what you're suggesting is cleaner, and if convex isn't concerned about making a breaking change here, then i'm happy to tidy this up for you and go with your suggestion!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make a new method nearest(ctx, { point, limit, maxDistance, filter }) and mark queryNearest as deprecated that only takes distance and internally calls nearest

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable to me!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ianmacartney i have updated the interface to be as you described. let me know if you think of anything else!

Copy link

@sethconve sethconve left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mackinleysmith looks great over all. One small question on simplifying the function signature. Do you have strong opinions there?

…and code to reflect changes, ensuring backward compatibility with `maxDistance` argument. Adjust related types and methods for improved clarity and consistency.
@coderabbitai
Copy link

coderabbitai bot commented Dec 5, 2025

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (2)
  • src/component/_generated/api.ts is excluded by !**/_generated/**
  • src/component/_generated/component.ts is excluded by !**/_generated/**

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Deprecates the legacy queryNearest and introduces a unified nearest(ctx, { point, limit, maxDistance, filter }) API; threads filter and sort interval parameters through the query pipeline and implements multi-condition filtering and sort-interval enforcement in ClosestPointQuery.

Changes

Cohort / File(s) Summary
API types & client API
src/client/index.ts
Added NearestQueryOptions<Doc> and deprecated QueryNearestOptions<Doc>; replaced queryNearest(ctx, point, maxResults, maxDistance?) implementation with nearest(ctx, { point, limit, maxDistance, filter }) and added a deprecated queryNearest overload that forwards to nearest.
Query pipeline & args
src/component/query.ts
Re-exported PREFETCH_SIZE from streams constants; added filtering and sorting.interval args to nearestPoints/execute payloads and propagated these through cell ranges and ClosestPointQuery construction.
Point query filtering & streaming
src/component/lib/pointQuery.ts
Extended ClosestPointQuery constructor with filtering and interval; added FilterCondition handling (must/should), streaming per-cell PointSet creation, matchesFilters/pointMatchesCondition/shouldStopProcessingCell/withinSortInterval helpers, adjusted subdivision logic and cell stream state management.
Streams constants
src/component/streams/constants.ts
Added exported PREFETCH_SIZE = 16.
Intersection import update
src/component/streams/intersection.ts
Updated PREFETCH_SIZE import from ../query.js to ./constants.js.
Tests for filtering & sorting
src/component/tests/pointQuery.test.ts
Added category-based filterKeys test data and tests covering must/should filters and sort-key interval behavior.
Example update
example/convex/example.ts
Updated example usage to call geospatial.nearest(ctx, { point, limit: maxRows, maxDistance }) instead of the legacy queryNearest call.
Docs
README.md
Documented the new nearest options object (point, limit, maxDistance, filter) and noted filters are enforced via the index; removed legacy positional usage guidance.
Package metadata
package.json
End-of-file newline formatting change only.

Sequence Diagram

sequenceDiagram
    participant Client
    participant GeospatialIndex
    participant FilterBuilder
    participant QueryPipeline
    participant ClosestPointQuery

    Client->>GeospatialIndex: nearest(ctx, {point, limit, maxDistance, filter})
    GeospatialIndex->>FilterBuilder: build filter conditions (must/should)
    FilterBuilder-->>GeospatialIndex: FilterCondition[] (must/should)
    GeospatialIndex->>QueryPipeline: execute with filtering & sorting.interval
    QueryPipeline->>ClosestPointQuery: new ClosestPointQuery(..., filtering, interval)

    rect rgb(230,245,255)
    Note over ClosestPointQuery: For each cell: create/get stream, apply stream-level filters\napply matchesFilters, enforce sort interval, accumulate results, stop if thresholds met
    end

    ClosestPointQuery-->>QueryPipeline: filtered & ordered results
    QueryPipeline-->>GeospatialIndex: result set
    GeospatialIndex-->>Client: nearest result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Pay special attention to:
    • src/component/lib/pointQuery.ts (filtering logic, streaming, subdivision changes).
    • src/client/index.ts (API signature change, deprecated overload correctness).
    • Threading of filtering and sorting.interval through src/component/query.ts and stream consumers.
    • Tests in src/component/tests/pointQuery.test.ts to ensure behavior matches intended semantics.

Poem

🐰 I hopped through cells with a curious cheer,

filters in paw and an interval near,
nearest now listens to limit and kind,
musts and shoulds sorted, neat in my mind,
old queryNearest whispers — new hops appear. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing filter computation via indexed filter ranges for the nearest query operation, which aligns with the refactored API and core implementation updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/component/lib/pointQuery.ts (1)

150-152: Double-checking filters on final results is defensive but adds overhead.

The indexed streams (FilterKeyRange + Intersection) should already filter by must/should clauses and sort interval. Re-applying matchesFilters here acts as a safety net but may be redundant. If the stream filtering is correct, this is unnecessary work.

Consider whether this is intentional (defense in depth) or can be removed for performance.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91ebb88 and 9579328.

⛔ Files ignored due to path filters (3)
  • package-lock.json is excluded by !**/package-lock.json
  • src/component/_generated/api.d.ts is excluded by !**/_generated/**
  • src/component/_generated/component.ts is excluded by !**/_generated/**
📒 Files selected for processing (9)
  • README.md (2 hunks)
  • example/convex/example.ts (1 hunks)
  • package.json (1 hunks)
  • src/client/index.ts (2 hunks)
  • src/component/lib/pointQuery.ts (7 hunks)
  • src/component/query.ts (3 hunks)
  • src/component/streams/constants.ts (1 hunks)
  • src/component/streams/intersection.ts (1 hunks)
  • src/component/tests/pointQuery.test.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/component/streams/constants.ts (1)
src/component/query.ts (1)
  • PREFETCH_SIZE (18-18)
src/client/index.ts (4)
example/convex/_generated/dataModel.d.ts (1)
  • Doc (30-33)
src/component/types.ts (2)
  • Point (18-18)
  • point (14-17)
src/client/query.ts (1)
  • GeospatialQuery (7-20)
example/convex/_generated/server.d.ts (1)
  • QueryCtx (107-107)
src/component/lib/pointQuery.ts (13)
src/client/index.ts (2)
  • Primitive (12-12)
  • point (13-13)
src/component/lib/primitive.ts (1)
  • Primitive (10-10)
src/component/types.ts (2)
  • Primitive (3-3)
  • point (14-17)
src/component/lib/interval.ts (2)
  • Interval (7-7)
  • interval (3-6)
src/component/lib/s2Bindings.ts (1)
  • cellIDToken (39-48)
src/component/lib/tupleKey.ts (1)
  • decodeTupleKey (38-74)
src/component/streams/zigzag.ts (2)
  • Stats (30-35)
  • PointSet (3-28)
src/component/streams/cellRange.ts (1)
  • CellRange (8-99)
src/component/query.ts (1)
  • PREFETCH_SIZE (18-18)
src/component/streams/constants.ts (1)
  • PREFETCH_SIZE (1-1)
src/component/streams/filterKeyRange.ts (1)
  • FilterKeyRange (9-106)
src/component/streams/union.ts (1)
  • Union (10-119)
src/component/streams/intersection.ts (1)
  • Intersection (5-113)
src/component/tests/pointQuery.test.ts (1)
src/component/lib/pointQuery.ts (1)
  • ClosestPointQuery (23-353)
🔇 Additional comments (24)
package.json (1)

1-109: Package configuration is sound; only EOF newline changed.

The package.json maintains consistent dependency health with convex pinned at 1.29.0 in dev and peer-constrained to ^1.24.8, allowing flexibility for consumers. The version bump from 0.1.x to 0.2.0 appropriately reflects the API additions in this PR (new nearest method and queryNearest enhancements). No dependency conflicts, export changes, or script modifications are present.

README.md (2)

139-141: Documentation correctly reflects the deprecation.

The deprecation notice for queryNearest is clear and directs users to the new nearest method.


272-277: API example demonstrates the new nearest method well.

The code example clearly shows the options-based API with point, limit, maxDistance, and filter parameters.

src/component/streams/constants.ts (1)

1-2: Good refactor to centralize the prefetch constant.

Extracting PREFETCH_SIZE to a dedicated constants module improves maintainability and ensures consistency across intersection.ts, query.ts, and pointQuery.ts.

src/component/streams/intersection.ts (1)

2-2: Import path correctly updated.

The import now sources PREFETCH_SIZE from the dedicated constants module, aligning with the refactoring in this PR.

example/convex/example.ts (1)

29-33: Example correctly migrated to new nearest API.

The migration from queryNearest to nearest with the options object { point, limit, maxDistance } is correct and demonstrates the intended usage pattern.

src/component/tests/pointQuery.test.ts (5)

30-42: Good test data setup with distinct filter categories.

The test points now have meaningful filterKeys ("coffee" and "tea") that enable thorough testing of the must/should filter logic.


102-123: Must filter test correctly validates filtering behavior.

The test verifies that a must filter on category: "coffee" returns only the two matching points (point1 and point3), excluding point2 which has category: "tea".


125-145: Should filter test correctly validates OR semantics.

When a single should filter is provided, it acts as a required filter, correctly returning only point2 with category: "tea".


147-162: Sort interval test validates boundary behavior.

Using startInclusive: 3 correctly filters to only point3 (which has sortKey: 3), excluding point1 and point2 with lower sort keys.


164-191: Multiple should filters test validates OR combination.

The test correctly verifies that multiple should clauses are combined with OR semantics, returning all three points when both "tea" and "coffee" categories are specified.

src/component/query.ts (3)

16-18: Clean import and re-export of PREFETCH_SIZE.

The constant is now sourced from the dedicated constants module and re-exported for backward compatibility with any consumers that import from query.ts.


278-281: New filtering and sorting parameters correctly added.

The nearestPoints query now accepts filtering and sorting parameters, enabling filtered nearest-neighbor queries through the same filter builder used by rectangle queries.


300-301: Parameters correctly propagated to ClosestPointQuery.

The filtering array and sorting.interval are passed to the ClosestPointQuery constructor, wiring the new filter functionality through to the query execution.

src/component/lib/pointQuery.ts (6)

17-21: FilterCondition type cleanly defines filter metadata.

The type structure with filterKey, filterValue, and occur ("must" | "should") aligns well with the filter builder pattern used in the client API.


31-36: Private fields properly encapsulate filter state and caching.

The separation of mustFilters and shouldFilters, along with the cellStreams cache, supports efficient filtered query execution.


85-92: Verify the subdivide threshold logic.

The condition approxRows >= ClosestPointQuery.FILTER_SUBDIVIDE_THRESHOLD (where threshold is 8) seems inverted. Typically, you'd subdivide when the cell has more rows than the threshold, but here FILTER_SUBDIVIDE_THRESHOLD = 8 and shouldSubdivide is true when approxRows >= 1 OR when filters are active and approxRows >= 8. This means cells with ≥8 rows always subdivide when filters are active.

Is this intentional? It seems like the condition should be approxRows <= FILTER_SUBDIVIDE_THRESHOLD to avoid subdividing small cells when filtering can efficiently narrow results.


108-137: Streaming cell processing is well-structured.

The loop correctly:

  1. Checks early termination via shouldStopProcessingCell
  2. Validates sort interval bounds before fetching documents
  3. Applies matchesFilters before adding results
  4. Properly advances the stream and marks completion

190-251: Stream composition correctly implements filter semantics.

The implementation properly:

  • Creates a CellRange for spatial filtering
  • Adds individual FilterKeyRange streams for each must filter
  • Wraps should filters in a Union (OR semantics)
  • Combines all with Intersection (AND semantics)

This ensures must clauses are all satisfied while any should clause suffices.


289-298: pointMatchesCondition correctly handles array-valued filter keys.

The implementation properly checks if the filter value exists in an array or matches a scalar, supporting multi-valued filter keys.

src/client/index.ts (4)

40-47: NearestQueryOptions type is well-designed.

The type cleanly encapsulates all nearest query parameters with appropriate optionality. Using NonNullable<GeospatialQuery<Doc>["filter"]> for the filter ensures type consistency with the rectangle query API.


49-54: Deprecation is correctly documented.

The @deprecated JSDoc annotation with clear migration guidance helps users transition to the new API.


229-257: New nearest method correctly implements the agreed-upon API.

The implementation:

  • Uses FilterBuilderImpl consistently with the query method
  • Passes filterConditions and interval to the backend
  • Maps limit to maxResults for the backend call

This follows the pattern discussed in past review comments where @ianmacartney suggested creating a new nearest method with options object.


259-275: Deprecated queryNearest correctly delegates to nearest.

The wrapper preserves backward compatibility by forwarding to the new method with appropriate parameter mapping (maxResultslimit).

mackinleysmith and others added 2 commits December 5, 2025 17:11
…y function, detailing the options object and its properties, including `point`, `limit`, `maxDistance`, and `filter`. This enhances understanding of the query capabilities and maintains consistency with recent changes.
We currently don't rebuild them in CI, so it is useful to commit the _generrated files
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants