refactor: simplify geo queries

This commit is contained in:
Thea Schöbl
2022-05-13 08:26:23 +00:00
parent d3c509ccb4
commit 9ce8c58b98
5 changed files with 198 additions and 31 deletions

6
package-lock.json generated
View File

@@ -536,9 +536,9 @@
}
},
"@openstapps/core": {
"version": "0.65.1",
"resolved": "https://registry.npmjs.org/@openstapps/core/-/core-0.65.1.tgz",
"integrity": "sha512-ZffFN3bRMC4eq96KSOnZScfTdx/ioiDuNLVrm0azqBqz+PKncknO65k+hoAClHi8BOlvZc2bPk4/19suas7PQQ==",
"version": "0.66.0",
"resolved": "https://registry.npmjs.org/@openstapps/core/-/core-0.66.0.tgz",
"integrity": "sha512-79oNo8TLjs/oaq/MzcPQx2xiM89kOI4bjV8sQakycmkuke3cI6sdhAaRSAYCNsjCz74VIx/8ht01zKx3jll+Cg==",
"requires": {
"@openstapps/core-tools": "0.30.0",
"@types/geojson": "1.0.6",

View File

@@ -33,7 +33,7 @@
},
"dependencies": {
"@elastic/elasticsearch": "5.6.22",
"@openstapps/core": "0.65.1",
"@openstapps/core": "0.66.0",
"@openstapps/core-tools": "0.30.0",
"@openstapps/logger": "0.8.0",
"@types/express-prometheus-middleware": "1.2.1",

View File

@@ -34,7 +34,7 @@ import {
ESFunctionScoreQuery,
ESFunctionScoreQueryFunction,
ESGenericRange,
ESGenericSort,
ESGenericSort, ESGeoBoundingBoxFilter,
ESGeoDistanceFilter,
ESGeoDistanceFilterArguments,
ESGeoDistanceSort,
@@ -96,7 +96,7 @@ export function buildBooleanFilter(booleanFilter: SCSearchBooleanFilter): ESBool
* @param filter A search filter for the retrieval of the data
*/
export function buildFilter(filter: SCSearchFilter):
ESTermFilter | ESGeoDistanceFilter | ESGeoShapeFilter | ESBooleanFilter<unknown> | ESRangeFilter {
ESTermFilter | ESGeoDistanceFilter | ESBooleanFilter<ESGeoShapeFilter | ESGeoBoundingBoxFilter> | ESGeoShapeFilter | ESBooleanFilter<unknown> | ESRangeFilter {
switch (filter.type) {
case 'value':
@@ -125,10 +125,10 @@ export function buildFilter(filter: SCSearchFilter):
case 'distance':
const geoObject: ESGeoDistanceFilterArguments = {
distance: `${filter.arguments.distance}m`,
};
geoObject[filter.arguments.field] = {
lat: filter.arguments.position[1],
lon: filter.arguments.position[0],
[`${filter.arguments.field}.point.coordinates`]: {
lat: filter.arguments.position[1],
lon: filter.arguments.position[0],
},
};
return {
@@ -179,12 +179,50 @@ export function buildFilter(filter: SCSearchFilter):
return dateRangeFilter;
case 'geo':
return {
[filter.arguments.field]: {
shape: filter.arguments.shape,
relation: filter.arguments.spatialRelation,
// TODO: on ES upgrade, use just geo_shape filters
const geoShapeFilter: ESGeoShapeFilter = {
geo_shape: {
/**
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html#_ignore_unmapped_3
*/
// tslint:disable-next-line:ban-ts-ignore
// @ts-ignore unfortunately, typescript is stupid and won't allow me to map this to an actual type.
ignore_unmapped: true,
[`${filter.arguments.field}.polygon`]: {
shape: filter.arguments.shape,
relation: filter.arguments.spatialRelation,
},
},
};
if ((typeof filter.arguments.spatialRelation === 'undefined' || filter.arguments.spatialRelation === 'intersects')
&& filter.arguments.shape.type === 'envelope'
) {
return {
bool: {
minimum_should_match: 1,
should: [
geoShapeFilter,
{
geo_bounding_box: {
/**
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html#_ignore_unmapped_3
*/
// tslint:disable-next-line:ban-ts-ignore
// @ts-ignore unfortunately, typescript is stupid and won't allow me to map this to an actual type.
ignore_unmapped: true,
[`${filter.arguments.field}.point.coordinates`]: {
bottom_right: filter.arguments.shape.coordinates[0],
top_left: filter.arguments.shape.coordinates[1],
},
},
},
],
},
};
}
return geoShapeFilter;
}
}
@@ -395,7 +433,7 @@ export function buildSort(
unit: 'm',
};
args[sort.arguments.field] = {
args[`${sort.arguments.field}.point.coordinates`] = {
lat: sort.arguments.position[1],
lon: sort.arguments.position[0],
};

View File

@@ -18,7 +18,7 @@ import {SCThing, SCThingType} from '@openstapps/core';
// tslint:disable-next-line:no-implicit-dependencies
import {NameList} from 'elasticsearch';
// tslint:disable-next-line:no-implicit-dependencies
import {Polygon} from 'geojson';
import {Polygon, Position} from 'geojson';
/**
* An elasticsearch aggregation bucket
@@ -366,23 +366,67 @@ export interface ESGeoDistanceFilter {
geo_distance: ESGeoDistanceFilterArguments;
}
/**
* A rectangular geo shape, representing the top-left and bottom-right corners
*
* This is an extension of the Geojson type
* http://geojson.org/geojson-spec.html
*/
export interface ESEnvelope {
/**
* The top-left and bottom-right corners of the bounding box
*/
coordinates: [Position, Position];
/**
* The type of the geometry
*/
type: 'envelope';
}
/**
* An Elasticsearch geo bounding box filter
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-bounding-box-query.html
*/
export interface ESGeoBoundingBoxFilter {
/**
* An Elasticsearch geo bounding box filter
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-bounding-box-query.html
*/
geo_bounding_box: {
[fieldName: string]: {
/**
* Geo Shape
*/
bottom_right: Position;
/**
* Geo Shape
*/
top_left: Position;
};
};
}
/**
* An Elasticsearch geo shape filter
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html
*/
export interface ESGeoShapeFilter {
[fieldName: string]: {
/**
* Relation of the two shapes
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html#_spatial_relations
*/
relation?: 'intersects' | 'disjoint' | 'within' | 'contains';
geo_shape: {
[fieldName: string]: {
/**
* Relation of the two shapes
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html#_spatial_relations
*/
relation?: 'intersects' | 'disjoint' | 'within' | 'contains';
/**
* Geo Shape
*/
shape: Polygon;
/**
* Geo Shape
*/
shape: Polygon | ESEnvelope;
};
};
}

View File

@@ -32,7 +32,7 @@ import {
ESGeoDistanceFilter,
ESGeoDistanceSort,
ESTermFilter,
ScriptSort
ScriptSort,
} from '../../../src/storage/elasticsearch/types/elasticsearch';
import {configFile} from '../../../src/common';
import {buildBooleanFilter, buildFilter, buildQuery, buildSort} from '../../../src/storage/elasticsearch/query';
@@ -205,10 +205,37 @@ describe('Query', function () {
type: 'distance',
arguments: {
distance: 1000,
field: 'geo.point.coordinates',
field: 'geo',
position: [50.123, 8.123],
}
},
geoPoint: {
type: 'geo',
arguments: {
field: 'geo',
shape: {
type: 'envelope',
coordinates: [
[50.123, 8.123],
[50.123, 8.123],
]
}
}
},
geoShape: {
type: 'geo',
arguments: {
field: 'geo',
spatialRelation: 'contains',
shape: {
type: 'envelope',
coordinates: [
[50.123, 8.123],
[50.123, 8.123],
]
}
}
},
boolean: {
type: 'boolean',
arguments: {
@@ -447,6 +474,64 @@ describe('Query', function () {
expect(filter).to.be.eql(expectedFilter);
});
it('should build geo filter for shapes and points', function () {
const filter = buildFilter(searchFilters.geoPoint);
const expectedFilter = {
bool: {
minimum_should_match: 1,
should: [
{
geo_shape: {
'geo.polygon': {
relation: undefined,
shape: {
type: 'envelope',
coordinates: [
[50.123, 8.123],
[50.123, 8.123]
]
},
},
ignore_unmapped: true,
}
},
{
geo_bounding_box: {
'geo.point.coordinates': {
bottom_right: [50.123, 8.123],
top_left: [50.123, 8.123]
},
ignore_unmapped: true,
},
},
]
}
};
expect(filter).to.be.eql(expectedFilter);
});
it('should build geo filter for shapes only', function () {
const filter = buildFilter(searchFilters.geoShape);
const expectedFilter = {
geo_shape: {
'geo.polygon': {
relation: 'contains',
shape: {
type: 'envelope',
coordinates: [
[50.123, 8.123],
[50.123, 8.123]
]
},
},
ignore_unmapped: true,
}
};
expect(filter).to.be.eql(expectedFilter);
});
it('should build boolean filter', function () {
const filter = buildFilter(searchFilters.boolean);
const expectedFilter: ESBooleanFilter<any> = {
@@ -497,7 +582,7 @@ describe('Query', function () {
type: 'distance',
order: 'desc',
arguments: {
field: 'geo.point',
field: 'geo',
position: [8.123, 50.123]
},
},
@@ -523,7 +608,7 @@ describe('Query', function () {
mode: 'avg',
order: 'desc',
unit: 'm',
'geo.point': {
'geo.point.coordinates': {
lat: 50.123,
lon: 8.123
}