Spatial Filters in pg_featureserv with CQL
pg_featureserv
provides access to the powerful spatial database capabilities of PostGIS and PostgreSQL via a lightweight web service. To do this, it implements the OGC API for Features (OAPIF) RESTful protocol. OAPIF is part of the Open Geospatial Consortium (OGC) OGC API suite of standards.
In a previous post, we announced an exciting new capability for pg_featureserv
: support for CQL filters. CQL (Common Query Language) is another OGC standard that provides the equivalent of SQL WHERE
clauses for web queries.
As you would expect given the OGC focus on promoting ease-of-access to spatial information, CQL supports spatial filtering. This lets us take advantage of PostGIS' ability to query spatial data very efficiently. In this post we'll show some examples of spatial filtering using CQL with pg_featureserv
.
The companion vector tile service pg_tileserv
also supports CQL, and spatial filtering works there as well.
CQL Spatial Filters
Spatial filtering in CQL involves using spatial predicates to test a condition on the geometry property of features. Spatial predicates include the standard OGC Simple Features predicates for spatial relationships:
INTERSECTS
- tests whether two geometries intersectDISJOINT
- tests whether two geometries have no points in commonCONTAINS
- tests whether a geometry contains anotherWITHIN
- tests whether a geometry is within anotherEQUALS
- tests whether two geometries are topologically equalCROSSES
- tests whether the geometries crossOVERLAPS
- tests whether the geometries overlapTOUCHES
- tests whether the geometries touch
pg_featureserv
also implements the distance predicate DWITHIN
.
Spatial predicates are typically used to compare the feature geometry property against a geometry value. Geometry values are expressed in Well-Known Text (WKT):
POINT (1 2)
LINESTRING (0 0, 1 1)
POLYGON ((0 0, 0 9, 9 0, 0 0))
POLYGON ((0 0, 0 9, 9 0, 0 0),(1 1, 1 8, 8 1, 1 1))
MULTIPOINT ((0 0), (0 9))
MULTILINESTRING ((0 0, 1 1),(1 1, 2 2))
MULTIPOLYGON (((1 4, 4 1, 1 1, 1 4)), ((1 9, 4 9, 1 6, 1 9)))
GEOMETRYCOLLECTION(POLYGON ((1 4, 4 1, 1 1, 1 4)), LINESTRING (3 3, 5 5), POINT (1 5))
ENVELOPE (1, 2, 3, 4)
By default, the coordinate reference system (CRS) of geometry values is geodetic (longitude and latitude). If needed a different CRS can be specified by using the filter-crs
parameter. (PostGIS supports a large number of standard coordinate reference systems.)
Here are some examples of spatial filter conditions:
INTERSECTS(geom, ENVELOPE(-100, 49, -90, 50) )
CONTAINS(geom, POINT(-100 49) )
DWITHIN(geom, POINT(-100 49), 0.1)
Of course, these can be combined with attribute conditions to express real-world queries.
Publishing Geographic Names
For these examples we'll use the U.S. Geographic Names Information System (GNIS) dataset. It contains more than 2 million points for named geographical features. We load this data into a PostGIS spatial table called us.geonames
. The point location values are stored in a column called geom
of type geography
. (PostGIS allows storing spatial data as either geometry
or geography
. We'll explain later why for this case it is better to use geography)
.
We create a spatial index on this column to provide fast spatial querying.
CREATE TABLE us.geonames (
id integer PRIMARY KEY,
name text,
lat double precision,
lon double precision,
type text,
state text,
geom geography(Point),
ts tsvector
);
CREATE INDEX us_geonames_gix ON us.geonames USING GIST ( geom );
We can now publish the dataset with pg_featureserv
, and view query results on the web UI.
The service collections
page shows the published tables and views (here we have used the configuration Database.TableIncludes
setting to publish just the us
schema):
http://localhost:9000/collections.html
The collections\us.geonames
page shows the collection metadata:
http://localhost:9000/collections/us.geonames.html
Querying with INTERSECTS
For this example we'll query water features on the San Juan Islands in the state of Washington, USA. Because there is no GNIS attribute providing region information, we have to use a spatial filter to specify the area we want to query. We used QGIS to create a polygon enclosing the islands.
We can convert the polygon to WKT and use it in an INTERSECTS
spatial predicate (since we are querying points, WITHIN
could be used as well - it produces the same result). To retrieve only water features (Lakes and Reservoirs) we add the condition type IN ('LK','RSV')
. The query URL is:
http://localhost:9000/collections/public.geonames/items.html?filter=type IN ('LK','RSV') AND INTERSECTS(geom,POLYGON ((-122.722 48.7054, -122.715 48.6347, -122.7641 48.6046, -122.7027 48.3885, -123.213 48.4536, -123.2638 48.6949, -123.0061 48.7666, -122.722 48.7054)))
The result of the query is a dataset containing 33 GNIS points:
Querying with DWITHIN
Now we'll show an example of using a distance-based spatial filter, using the DWITHIN
predicate. This is the reason we loaded the GNIS data as geography
.DWITHIN
tests whether a feature geometry is within a given distance of a geometry value. By using the geography
type, we can specify the distance in metres, which are the units of measure of the geodetic EPSG:4326 coordinate system. If we had loaded the dataset using the geometry
type, the units would have been degrees, which is awkward to use. Also, geography
computes the distance correctly on the surface of the earth (using the great-circle distance).
Let's query for mountains (type = 'MT'
) within 100 kilometers of Seattle (lat/long of about 47.6,-122.34 - note that WKT requires this as long-lat). The query URL is:
http://localhost:9000/collections/us.geonames_geo/items.html?filter=type = 'MT' AND DWITHIN(geom,Point(-122.34 47.6),100000)&limit=1000
This gives a result showing 695 mountains near Seattle. It's a hilly place!
Try it yourself!
CQL filtering will be included in the forthcoming pg_featureserv
Version 1.3. But you can try it out now by downloading the latest build. Let us know what use cases you find for CQL spatial filtering!
More to come
pg_featureserv
with CQL attribute and spatial filtering provides a highly effective platform for accessing data over the web. And we're continuing to enhance it with new capabilities to unlock even more of the power of PostGIS and PostgreSQL. Stay tuned for posts on advanced query functionality including temporal filtering, geometry transformations, and aggregation.
Related Articles
- Crunchy Data Warehouse: Postgres with Iceberg for High Performance Analytics
8 min read
- Loading the World! OpenStreetMap Import In Under 4 Hours
6 min read
- Easy Totals and Subtotals in Postgres with Rollup and Cube
5 min read
- A change to ResultRelInfo - A Near Miss with Postgres 17.1
8 min read
- Accessing Large Language Models from PostgreSQL
5 min read