Documentation Index
Fetch the complete documentation index at: https://docs.uselayers.com/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Layers supports geographic filtering and sorting against any product attribute typed as geo. Once an attribute is geo-typed, products carrying that attribute can be:
- Filtered to only those within a radius, polygon, or bounding box of an origin.
- Sorted by distance from an origin (ascending or descending).
Use this to power experiences like “stores near me”, “products available in this delivery zone”, or “tours within the highlighted region on the map”.
Geo data is stored in a dedicated PostGIS-backed sidecar table and queried via spatial indexes, so geo filters and sorts run at the same scale as the rest of the catalog.
Configuring a geo attribute
A geo attribute is a catalog attribute whose value_type is set to geo. Geo attributes are sourced from product metafields or referenced metaobjects.
Supported attribute code patterns
| Code pattern | Source |
|---|
metafields.{namespace}.{key} | Reads the geometry directly from a product metafield value |
metafields.{namespace}.{key}.{metaobject_field} | Reads the geometry from a named field on a metaobject referenced by the metafield (single or list) |
For metaobject-reference sources, every referenced metaobject’s named field contributes one row — a product can have many geo points (e.g. all retail locations that carry it). Filters and sorts use EXISTS / MIN() semantics so a product matches if any of its geo rows match.
Geometry types
| Geometry type | Use for |
|---|
point | A single coordinate (store location, event venue, pickup point) |
polygon | A bounded region (delivery zone, service area, neighborhood) |
multipolygon | A set of disjoint regions (multi-zone delivery, distributed coverage areas) |
The ingestion pipeline normalizes several common shapes into canonical GeoJSON before storing:
// Shorthand point
{ "lat": 37.7749, "lng": -122.4194 }
{ "latitude": 37.7749, "longitude": -122.4194 }
// GeoJSON Point
{ "type": "Point", "coordinates": [-122.4194, 37.7749] }
// GeoJSON Polygon (rings must be closed — first and last position equal)
{
"type": "Polygon",
"coordinates": [
[
[-122.43, 37.76],
[-122.40, 37.76],
[-122.40, 37.79],
[-122.43, 37.79],
[-122.43, 37.76]
]
]
}
// GeoJSON MultiPolygon
{
"type": "MultiPolygon",
"coordinates": [ /* array of polygons */ ]
}
GeoJSON coordinates are [longitude, latitude] — the reverse of conversational lat, lng order. The shorthand { lat, lng } form is provided as a convenience.
Polygon match mode
When the source geometry on a product is itself a polygon (e.g. a delivery zone), you can configure whether the filter polygon must contain the product polygon or merely intersect it:
polygon_match | Behavior |
|---|
intersects (default) | The filter polygon and the product geometry share any area — useful for “any zone that overlaps the search area” |
contains | The filter polygon must fully contain the product geometry — useful for “zones entirely within the selected region” |
This setting lives on the attribute definition, not on the filter request, so the matching semantics are consistent across surfaces.
Geo filter operators
Three new operators extend the filter expression language. Each takes a single payload object inside the values array.
| Operator | Payload | Matches products whose geo attribute |
|---|
geoRadius | { lat, lng, radius_meters } | Lies within radius_meters of (lat, lng) |
geoBoundingBox | { north_east: { lat, lng }, south_west: { lat, lng } } | Lies inside the bounding rectangle |
geoPolygon | GeoJSON Polygon or MultiPolygon | Matches the filter polygon per the attribute’s polygon_match mode |
All operators accept either lat/lng or latitude/longitude (and lon) on coordinate fields. Bounding boxes also accept northEast/southWest camelCase corners. Radius can be supplied as radius_meters or radiusMeters.
Examples
Products within 5 km of a shopper’s location:
{
"filter_group": {
"conditional": "AND",
"expressions": [
{
"property": "metafields.locations.coordinates",
"operator": "geoRadius",
"values": [
{ "lat": 37.7749, "lng": -122.4194, "radius_meters": 5000 }
]
}
]
}
}
Products inside the visible map viewport:
{
"property": "metafields.locations.coordinates",
"operator": "geoBoundingBox",
"values": [
{
"north_east": { "lat": 37.81, "lng": -122.36 },
"south_west": { "lat": 37.72, "lng": -122.48 }
}
]
}
Products whose delivery zone covers a hand-drawn region:
{
"property": "metafields.fulfillment.delivery_zone.geometry",
"operator": "geoPolygon",
"values": [
{
"type": "Polygon",
"coordinates": [
[
[-122.45, 37.74],
[-122.39, 37.74],
[-122.39, 37.80],
[-122.45, 37.80],
[-122.45, 37.74]
]
]
}
]
}
Validation
Geo operator payloads are validated before the query runs:
lat must be between -90 and 90; lng between -180 and 180.
radius_meters must be greater than 0.
- Polygon rings must be closed (first and last position equal) and contain at least 4 positions.
- Unsupported geometry types (e.g.
LineString) are rejected.
A geo filter against a non-geo attribute, a missing attribute, or a malformed payload matches zero products rather than erroring on the whole request.
Sort by distance
The geo_distance sort order ranks products by distance from a given origin against a geo-typed attribute.
{
"sort_order": {
"type": "geo_distance",
"attribute": "metafields.locations.coordinates",
"origin_lat": 37.7749,
"origin_lng": -122.4194,
"direction": "asc"
}
}
| Field | Required | Description |
|---|
type | Yes | Must be geo_distance |
attribute | Yes | Code of the geo attribute to measure against |
origin_lat | Yes | Latitude of the origin point |
origin_lng | Yes | Longitude of the origin point |
direction | No | asc (nearest first, default) or desc (farthest first) |
Multi-point products
When a product has multiple geo rows (e.g. one per retail location), the distance used for sorting is the minimum distance across all rows. A product carried at three stores sorts by the closest of the three.
Products without geo data
Products with no rows for the geo attribute are placed in a separate tier that always sorts after all products with a distance, regardless of direction. This prevents missing-data products from polluting the top of the result set.
Pairing filters and sort
Filter and sort can target the same or different geo attributes. A common pattern is to filter by radius and sort by distance against the same attribute, so the result set contains every product within range, ordered from nearest to farthest:
{
"filter_group": {
"conditional": "AND",
"expressions": [
{
"property": "metafields.locations.coordinates",
"operator": "geoRadius",
"values": [{ "lat": 37.7749, "lng": -122.4194, "radius_meters": 10000 }]
}
]
},
"sort_order": {
"type": "geo_distance",
"attribute": "metafields.locations.coordinates",
"origin_lat": 37.7749,
"origin_lng": -122.4194,
"direction": "asc"
}
}
Ingestion behavior
Geo values are extracted from products during the catalog sync that powers searchable data:
- For a
metafields.{ns}.{key} attribute, the metafield value itself is normalized and stored as one row.
- For a
metafields.{ns}.{key}.{field} attribute, every metaobject reference (single or list) contributes one row per metaobject whose named field holds a geometry.
- Malformed payloads are silently skipped — a single bad metaobject does not block ingestion for the rest.
- Each row records its source (
metafield or metaobject) and a source_ref (the metaobject ID), making per-product geo data inspectable.
Shopify Locations also gain a generated geography column during the standard location sync, ready for future location-backed filtering features. No action is required to opt in.
Troubleshooting
A geo filter returns no products. Confirm the attribute’s value_type is geo and that the source metafield (or metaobject field) contains a recognized GeoJSON or { lat, lng } payload. Check the most recent catalog sync ran after the geo data was added.
Distance sort puts unexpected products at the top. Products with no geo data sort last by design. If a clearly-far product appears first, verify the product’s geo rows actually reflect the intended coordinates — multi-point products use MIN(distance) so a single bad row pulls the product up.
Polygon filter matches too aggressively. The attribute’s polygon_match mode controls this. Switch from intersects (the default) to contains if you want only fully enclosed product geometries to match.
Coordinate ordering looks reversed. GeoJSON uses [lng, lat], not [lat, lng]. If you’re constructing GeoJSON by hand, double-check the order — using the shorthand { lat, lng } form avoids the mismatch entirely.
See also