diff --git a/package-lock.json b/package-lock.json index f5c42d78..6bf22f79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 0e6db9b8..50c2571d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/storage/elasticsearch/query.ts b/src/storage/elasticsearch/query.ts index 5803b385..cbed5ae4 100644 --- a/src/storage/elasticsearch/query.ts +++ b/src/storage/elasticsearch/query.ts @@ -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 | ESRangeFilter { + ESTermFilter | ESGeoDistanceFilter | ESBooleanFilter | ESGeoShapeFilter | ESBooleanFilter | 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], }; diff --git a/src/storage/elasticsearch/types/elasticsearch.ts b/src/storage/elasticsearch/types/elasticsearch.ts index 9566f11f..0277de0d 100644 --- a/src/storage/elasticsearch/types/elasticsearch.ts +++ b/src/storage/elasticsearch/types/elasticsearch.ts @@ -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; + }; }; } diff --git a/test/storage/elasticsearch/query.spec.ts b/test/storage/elasticsearch/query.spec.ts index ad8921a2..276b8edd 100644 --- a/test/storage/elasticsearch/query.spec.ts +++ b/test/storage/elasticsearch/query.spec.ts @@ -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 = { @@ -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 }