@convex-dev/geospatial

中文文档
0.2.1发布于 2025/12/11Apache-2.0TypeScriptES Module近 30 天下载 11.1万

A geospatial index for Convex

原始使用文档
文档目录7 节
  1. Pre-requisite: Convex
  2. Installation
  3. Inserting points
  4. Querying points within a shape
  5. Querying the points nearest a query point
  6. Example
  7. Development

Convex Geospatial Index (Beta)

npm version

image

This component adds a geospatial index to Convex, allowing you to efficiently store and query points on the Earth's surface.

  • Insert points into the geospatial key value store along with their geographic coordinates.
  • Efficiently query for all points within a given rectangle on the sphere.
  • Control the sort order for the results with a custom sorting key.
  • Filter query results with equality and IN clauses.
  • And since it's built on Convex, everything is automatically consistent, reactive, and cached!

This component is currently in beta. It's missing some functionality, but what's there should work. We've tested the example app up to about 1,000,000 points, so reach out if you're using a much larger dataset. If you find a bug or have a feature request, you can file it here.

Pre-requisite: Convex

You'll need an existing Convex project to use the component. Convex is a hosted backend platform, including a database, serverless functions, and a ton more you can learn about here.

Run npm create convex or follow any of the quickstarts to set one up.

Installation

First, add @convex-dev/geospatial to your Convex project:

npm install @convex-dev/geospatial

Then, install the component into your Convex project within the convex/convex.config.ts file:

// convex/convex.config.ts
import geospatial from "@convex-dev/geospatial/convex.config.js";
import { defineApp } from "convex/server";

const app = defineApp();
app.use(geospatial);

export default app;

Finally, create a new GeospatialIndex within your convex/ folder, and point it to the installed component:

// convex/index.ts
import { GeospatialIndex } from "@convex-dev/geospatial";
import { components } from "./_generated/api";

const geospatial = new GeospatialIndex(components.geospatial);

Inserting points

After installing the component, you can insert, get, and remove points from the index. You can specify a filterKeys record for filtering at query time and optionally a sortKey for the query result order. We currently only support ascending order on the sortKey.

// convex/index.ts

const example = mutation({
  handler: async (ctx) => {
    const cityId = await ctx.db.insert("cities", {...});
    await geospatial.insert(
      ctx,
      "American Museum of Natural History",
      {
        latitude: 40.7813,
        longitude: -73.9737,
      },
      { category: "museum" },
      28.0, // Price used as the sort key
    );
    const result = await geospatial.get(ctx, cityId);
    await geospatial.remove(ctx, cityId);
  },
});

If you would like some more typesafety, you can specify a type argument for the GeospatialIndex class. This will also provide you with auto-complete for the filterKeys and sortKey parameters. Above the key was "American Museum of Natural History" but most commonly the key will be an ID in another table of yours.

// convex/index.ts
import { GeospatialIndex, Point } from "@convex-dev/geospatial";
import { components } from "./_generated/api";
import { Id } from "./_generated/dataModel";

export const geospatial = new GeospatialIndex<
  Id<"museums">,
  { category: string; anotherFilter?: number }
>(components.geospatial);

Querying points within a shape

After inserting some points, you can query them with the query API.

// convex/index.ts

const example = query({
  handler: async (ctx) => {
    const rectangle = {
      west: -73.9712,
      south: 40.7831,
      east: -72.9712,
      north: 41.7831,
    };
    const result = await geospatial.query(ctx, {
      shape: { type: "rectangle", rectangle },
      limit: 16,
    });
    return result;
  },
});

The legacy queryNearest helper now delegates to nearest and is deprecated. It only accepts a maxDistance numeric argument for backwards compatibility. New integrations should prefer nearest.

This query will find all points that lie within the query rectangle, sort them in ascending sortKey order, and return at most 16 results.

You can optionally add filter conditions to queries.

The first type of filter condition is an in() filter, which requires that a matching document have a filter field with a value in a specified set.

// convex/index.ts

const example = query({
  handler: async (ctx) => {
    const rectangle = {
      west: -73.9712,
      south: 40.7831,
      east: -72.9712,
      north: 41.7831,
    };
    const result = await geospatial.query(ctx, {
      shape: { type: "rectangle", rectangle },
      filter: (q) => q.in("category", ["museum", "restaurant"]),
    });
    return result;
  },
});

The second type of filter condition is an eq() filter, which requires that a matching document have a filter field with a value equal to a specified value.

// convex/index.ts

const example = query({
  handler: async (ctx) => {
    const result = await geospatial.query(ctx, {
      shape: { type: "rectangle", rectangle },
      filter: (q) => q.eq("category", "museum"),
    });
    return result;
  },
});

The final type of filter condition allows you to specify ranges over the sortKey. We currently only support (optional) inclusive lower bounds and exclusive upper bounds.

// convex/index.ts

const example = query({
  handler: async (ctx) => {
    const rectangle = {
      west: -73.9712,
      south: 40.7831,
      east: -72.9712,
      north: 41.7831,
    };
    const result = await geospatial.query(ctx, {
      shape: { type: "rectangle", rectangle },
      filter: (q) => q.gte("sortKey", 10).lt("sortKey", 30),
    });
    return result;
  },
});

Queries take in a limit, which bounds the maximum number of rows returned. If this limit is hit, the query will return a nextCursor for continuation. The query may also return a nextCursor with fewer than limit results if it runs out of its IO budget while executing.

In either case, you can continue the stream by passing nextCursor to the next call's cursor parameter.

// convex/index.ts

const example = query({
  handler: async (ctx) => {
    const rectangle = {
      west: -73.9712,
      south: 40.7831,
      east: -72.9712,
      north: 41.7831,
    };
    const startCursor = undefined;
    const result = await geospatial.query(
      ctx,
      {
        shape: { type: "rectangle", rectangle },
        limit: 16,
      },
      startCursor,
    );
    if (result.nextCursor) {
      // Continue the query, starting from the first query's cursor.
      const nextResult = await geospatial.query(
        ctx,
        {
          shape: { type: "rectangle", rectangle },
          limit: 16,
        },
        result.nextCursor,
      );
      return [...result.results, ...nextResult.results];
    }
    return result.results; // { key, coordinates }[]
  },
});

Note: you typically pass the nextCursor in from a client that is paginating through results, to avoid loading too much data in a single query.

Querying the points nearest a query point

You can also query for the points closest to a given point, optionally limiting to a maximum distance (in meters).

// convex/index.ts

const example = query({
  handler: async (ctx) => {
    const maxResults = 16;
    const maxDistance = 10000;
    const result = await geospatial.nearest(ctx, {
      point: { latitude: 40.7813, longitude: -73.9737 },
      limit: maxResults,
      maxDistance,
      filter: (q) => q.eq("category", "coffee"),
    });
    return result;
  },
});

The second argument is an options object containing point, limit, and optionally maxDistance and filter. You can combine maxDistance with the same filter builder used by query, including eq, in, gte, and lt conditions. These filters are enforced through the indexed pointsByFilterKey range before documents are loaded, so the database does the heavy lifting and the query avoids reading unrelated points. Pairing that with a sensible maxDistance further constrains the search space and can greatly speed up searching the index.

Example

See example/ for a full example with a Leaflet-based frontend.

Development

Install dependencies and fire up the example app to get started.

npm install
cd example
npm install
npm run dev

The component definition is in src/ and reflects what users of the component will install. The example app, which is entirely independent, lives in example/.

版本与发布记录

@convex-dev/geospatial 共发布 11 个版本,首次发布于 2024/10/16;过去 12 个月发布了 2 个版本。

版本发布日期距今
0.2.1latest2025/12/1110 个月前
0.2.02025/11/1111 个月前
0.1.92025/05/311.4 年前
0.1.82025/02/081.8 年前
0.1.72025/02/061.8 年前
0.1.62024/12/241.9 年前

关于 @convex-dev/geospatial 的常见问题

@convex-dev/geospatial 是什么?

A geospatial index for Convex

如何安装 @convex-dev/geospatial?

在项目目录运行 npm install @convex-dev/geospatial,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:convex(^1.24.8)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@convex-dev/geospatial 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@convex-dev/geospatial 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@convex-dev/geospatial 可以商用吗?

@convex-dev/geospatial 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@convex-dev/geospatial 的下载量有多少?

根据 npm 官方下载统计,@convex-dev/geospatial 在 2026/10/11 – 2026/11/09 这 30 天内被下载 110,850 次,日均约 3,695 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@convex-dev/geospatial 还在维护吗?

最新版本 0.2.1 发布于 2025/12/11(10 个月前);至今共发布 11 个版本,其中过去 12 个月发布了 2 个;首次发布于 2024/10/16。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。