[Maps] migrate mvt routes to use data.search service instead of directly calling elasticsearch (#89904) (#90104)

* [Maps] migrate mvt routes to use data.search service instead of directly calling elasticsearch

* pass search session id to mvt requests

* move grid tile tests to integration tests

* replace getTile unit test with integration test

* add comment about request param

* revert total meta change

* tslint fixes

* update jest tests
This commit is contained in:
Nathan Reese 2021-02-02 18:11:43 -07:00 committed by GitHub
parent 1f0ecfbfc1
commit d2c26cf28b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 306 additions and 414 deletions

View file

@ -278,6 +278,17 @@ describe('ESGeoGridSource', () => {
"rootdir/api/maps/mvt/getGridTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628)))))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point"
);
});
it('should include searchSourceId in urlTemplateWithMeta', async () => {
const urlTemplateWithMeta = await geogridSource.getUrlTemplateWithMeta({
...vectorSourceRequestMeta,
searchSessionId: '1',
});
expect(urlTemplateWithMeta.urlTemplate).toBe(
"rootdir/api/maps/mvt/getGridTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628)))))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point&searchSessionId=1"
);
});
});
describe('Gold+ usage', () => {

View file

@ -443,12 +443,22 @@ export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingle
);
const geoField = await this._getGeoField();
const urlTemplate = `${mvtUrlServicePath}?x={x}&y={y}&z={z}&geometryFieldName=${this._descriptor.geoField}&index=${indexPattern.title}&requestBody=${risonDsl}&requestType=${this._descriptor.requestType}&geoFieldType=${geoField.type}`;
const urlTemplate = `${mvtUrlServicePath}\
?x={x}\
&y={y}\
&z={z}\
&geometryFieldName=${this._descriptor.geoField}\
&index=${indexPattern.title}\
&requestBody=${risonDsl}\
&requestType=${this._descriptor.requestType}\
&geoFieldType=${geoField.type}`;
return {
layerName: this.getLayerName(),
minSourceZoom: this.getMinZoom(),
maxSourceZoom: this.getMaxZoom(),
urlTemplate,
urlTemplate: searchFilters.searchSessionId
? urlTemplate + `&searchSessionId=${searchFilters.searchSessionId}`
: urlTemplate,
};
}

View file

@ -116,6 +116,20 @@ describe('ESSearchSource', () => {
`rootdir/api/maps/mvt/getTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':fieldsFromSource,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))&geoFieldType=geo_shape`
);
});
it('should include searchSourceId in urlTemplateWithMeta', async () => {
const esSearchSource = new ESSearchSource({
geoField: geoFieldName,
indexPatternId: 'ipId',
});
const urlTemplateWithMeta = await esSearchSource.getUrlTemplateWithMeta({
...searchFilters,
searchSessionId: '1',
});
expect(urlTemplateWithMeta.urlTemplate).toBe(
`rootdir/api/maps/mvt/getTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=foobar-title-*&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:(),title:'foobar-title-*')),'1':('0':size,'1':1000),'2':('0':filter,'1':!()),'3':('0':query),'4':('0':index,'1':(fields:(),title:'foobar-title-*')),'5':('0':query,'1':(language:KQL,query:'tooltipField: foobar',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':fieldsFromSource,'1':!(tooltipField,styleField)),'7':('0':source,'1':!(tooltipField,styleField))))&geoFieldType=geo_shape&searchSessionId=1`
);
});
});
});

View file

@ -729,12 +729,21 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye
const geoField = await this._getGeoField();
const urlTemplate = `${mvtUrlServicePath}?x={x}&y={y}&z={z}&geometryFieldName=${this._descriptor.geoField}&index=${indexPattern.title}&requestBody=${risonDsl}&geoFieldType=${geoField.type}`;
const urlTemplate = `${mvtUrlServicePath}\
?x={x}\
&y={y}\
&z={z}\
&geometryFieldName=${this._descriptor.geoField}\
&index=${indexPattern.title}\
&requestBody=${risonDsl}\
&geoFieldType=${geoField.type}`;
return {
layerName: this.getLayerName(),
minSourceZoom: this.getMinZoom(),
maxSourceZoom: this.getMaxZoom(),
urlTemplate,
urlTemplate: searchFilters.searchSessionId
? urlTemplate + `&searchSessionId=${searchFilters.searchSessionId}`
: urlTemplate,
};
}
}

View file

@ -1 +0,0 @@
{"took":2,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":10000,"relation":"gte"},"max_score":null,"hits":[]},"aggregations":{"gridSplit":{"buckets":[{"key":"7/37/48","doc_count":42637,"avg_of_TOTAL_AV":{"value":5398920.390458991},"gridCentroid":{"location":{"lat":40.77936432658204,"lon":-73.96795676049909},"count":42637}}]}}}

View file

@ -1 +0,0 @@
{"took":0,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":1,"relation":"eq"},"max_score":0,"hits":[{"_index":"poly","_id":"G7PRMXQBgyyZ-h5iYibj","_score":0,"_source":{"coordinates":{"coordinates":[[[-106.171875,36.59788913307022],[-50.625,-22.91792293614603],[4.921875,42.8115217450979],[-33.046875,63.54855223203644],[-66.796875,63.860035895395306],[-106.171875,36.59788913307022]]],"type":"polygon"}}}]}}

View file

@ -1,35 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import * as path from 'path';
import * as fs from 'fs';
export const TILE_SEARCHES = {
'0.0.0': {
countResponse: {
count: 1,
_shards: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
},
},
searchResponse: loadJson('./json/0_0_0_search.json'),
},
};
export const TILE_GRIDAGGS = {
'0.0.0': {
gridAggResponse: loadJson('./json/0_0_0_gridagg.json'),
},
};
function loadJson(filePath: string) {
const absolutePath = path.resolve(__dirname, filePath);
const rawContents = fs.readFileSync(absolutePath);
return JSON.parse((rawContents as unknown) as string);
}

View file

@ -1,261 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getGridTile, getTile } from './get_tile';
import { TILE_GRIDAGGS, TILE_SEARCHES } from './__fixtures__/tile_es_responses';
import { Logger } from 'src/core/server';
import {
ES_GEO_FIELD_TYPE,
KBN_IS_CENTROID_FEATURE,
MVT_SOURCE_LAYER_NAME,
RENDER_AS,
} from '../../common/constants';
// @ts-expect-error
import { VectorTile, VectorTileLayer } from '@mapbox/vector-tile';
// @ts-expect-error
import Protobuf from 'pbf';
interface ITileLayerJsonExpectation {
version: number;
name: string;
extent: number;
features: Array<{
id: string | number | undefined;
type: number;
properties: object;
extent: number;
pointArrays: object;
}>;
}
describe('getTile', () => {
const mockCallElasticsearch = jest.fn();
const requestBody = {
_source: { excludes: [] },
docvalue_fields: [],
query: { bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } },
script_fields: {},
size: 10000,
stored_fields: ['*'],
};
const geometryFieldName = 'coordinates';
beforeEach(() => {
mockCallElasticsearch.mockReset();
});
test('0.0.0 - under limit', async () => {
mockCallElasticsearch.mockImplementation((type) => {
if (type === 'count') {
return TILE_SEARCHES['0.0.0'].countResponse;
} else if (type === 'search') {
return TILE_SEARCHES['0.0.0'].searchResponse;
} else {
throw new Error(`${type} not recognized`);
}
});
const pbfTile = await getTile({
x: 0,
y: 0,
z: 0,
index: 'world_countries',
requestBody,
geometryFieldName,
logger: ({
info: () => {},
} as unknown) as Logger,
callElasticsearch: mockCallElasticsearch,
geoFieldType: ES_GEO_FIELD_TYPE.GEO_SHAPE,
});
const jsonTile = new VectorTile(new Protobuf(pbfTile));
compareJsonTiles(jsonTile, {
version: 2,
name: 'source_layer',
extent: 4096,
features: [
{
id: undefined,
type: 3,
properties: {
__kbn__feature_id__: 'poly:G7PRMXQBgyyZ-h5iYibj:0',
_id: 'G7PRMXQBgyyZ-h5iYibj',
_index: 'poly',
},
extent: 4096,
pointArrays: [
[
{ x: 840, y: 1600 },
{ x: 1288, y: 1096 },
{ x: 1672, y: 1104 },
{ x: 2104, y: 1508 },
{ x: 1472, y: 2316 },
{ x: 840, y: 1600 },
],
],
},
{
id: undefined,
type: 1,
properties: {
__kbn__feature_id__: 'poly:G7PRMXQBgyyZ-h5iYibj:0',
_id: 'G7PRMXQBgyyZ-h5iYibj',
_index: 'poly',
[KBN_IS_CENTROID_FEATURE]: true,
},
extent: 4096,
pointArrays: [[{ x: 1470, y: 1702 }]],
},
],
});
});
});
describe('getGridTile', () => {
const mockCallElasticsearch = jest.fn();
const geometryFieldName = 'geometry';
// For mock-purposes only. The ES-call response is mocked in 0_0_0_gridagg.json file
const requestBody = {
_source: { excludes: [] },
aggs: {
gridSplit: {
aggs: {
// eslint-disable-next-line @typescript-eslint/naming-convention
avg_of_TOTAL_AV: { avg: { field: 'TOTAL_AV' } },
gridCentroid: { geo_centroid: { field: geometryFieldName } },
},
geotile_grid: {
bounds: null,
field: geometryFieldName,
precision: null,
shard_size: 65535,
size: 65535,
},
},
},
docvalue_fields: [],
query: {
bool: {
filter: [],
},
},
script_fields: {},
size: 0,
stored_fields: ['*'],
};
beforeEach(() => {
mockCallElasticsearch.mockReset();
mockCallElasticsearch.mockImplementation((type) => {
return TILE_GRIDAGGS['0.0.0'].gridAggResponse;
});
});
const defaultParams = {
x: 0,
y: 0,
z: 0,
index: 'manhattan',
requestBody,
geometryFieldName,
logger: ({
info: () => {},
} as unknown) as Logger,
callElasticsearch: mockCallElasticsearch,
requestType: RENDER_AS.POINT,
geoFieldType: ES_GEO_FIELD_TYPE.GEO_POINT,
};
test('0.0.0 tile (clusters)', async () => {
const pbfTile = await getGridTile(defaultParams);
const jsonTile = new VectorTile(new Protobuf(pbfTile));
compareJsonTiles(jsonTile, {
version: 2,
name: 'source_layer',
extent: 4096,
features: [
{
id: undefined,
type: 1,
properties: {
['avg_of_TOTAL_AV']: 5398920.390458991,
doc_count: 42637,
},
extent: 4096,
pointArrays: [[{ x: 1206, y: 1539 }]],
},
],
});
});
test('0.0.0 tile (grids)', async () => {
const pbfTile = await getGridTile({ ...defaultParams, requestType: RENDER_AS.GRID });
const jsonTile = new VectorTile(new Protobuf(pbfTile));
compareJsonTiles(jsonTile, {
version: 2,
name: 'source_layer',
extent: 4096,
features: [
{
id: undefined,
type: 3,
properties: {
['avg_of_TOTAL_AV']: 5398920.390458991,
doc_count: 42637,
},
extent: 4096,
pointArrays: [
[
{ x: 1216, y: 1536 },
{ x: 1216, y: 1568 },
{ x: 1184, y: 1568 },
{ x: 1184, y: 1536 },
{ x: 1216, y: 1536 },
],
],
},
{
id: undefined,
type: 1,
properties: {
['avg_of_TOTAL_AV']: 5398920.390458991,
doc_count: 42637,
[KBN_IS_CENTROID_FEATURE]: true,
},
extent: 4096,
pointArrays: [[{ x: 1200, y: 1552 }]],
},
],
});
});
});
/**
* Verifies JSON-representation of tile-contents
* @param actualTileJson
* @param expectedLayer
*/
function compareJsonTiles(actualTileJson: VectorTile, expectedLayer: ITileLayerJsonExpectation) {
const actualLayer: VectorTileLayer = actualTileJson.layers[MVT_SOURCE_LAYER_NAME];
expect(actualLayer.version).toEqual(expectedLayer.version);
expect(actualLayer.extent).toEqual(expectedLayer.extent);
expect(actualLayer.name).toEqual(expectedLayer.name);
expect(actualLayer.length).toEqual(expectedLayer.features.length);
expectedLayer.features.forEach((expectedFeature, index) => {
const actualFeature = actualLayer.feature(index);
expect(actualFeature.type).toEqual(expectedFeature.type);
expect(actualFeature.extent).toEqual(expectedFeature.extent);
expect(actualFeature.id).toEqual(expectedFeature.id);
expect(actualFeature.properties).toEqual(expectedFeature.properties);
expect(actualFeature.loadGeometry()).toEqual(expectedFeature.pointArrays);
});
}

View file

@ -8,7 +8,8 @@
import geojsonvt from 'geojson-vt';
// @ts-expect-error
import vtpbf from 'vt-pbf';
import { Logger } from 'src/core/server';
import { Logger, RequestHandlerContext } from 'src/core/server';
import type { DataApiRequestHandlerContext } from 'src/plugins/data/server';
import { Feature, FeatureCollection, Polygon } from 'geojson';
import {
ES_GEO_FIELD_TYPE,
@ -28,7 +29,7 @@ import { getCentroidFeatures } from '../../common/get_centroid_features';
export async function getGridTile({
logger,
callElasticsearch,
context,
index,
geometryFieldName,
x,
@ -37,17 +38,19 @@ export async function getGridTile({
requestBody = {},
requestType = RENDER_AS.POINT,
geoFieldType = ES_GEO_FIELD_TYPE.GEO_POINT,
searchSessionId,
}: {
x: number;
y: number;
z: number;
geometryFieldName: string;
index: string;
callElasticsearch: (type: string, ...args: any[]) => Promise<unknown>;
context: RequestHandlerContext & { search: DataApiRequestHandlerContext };
logger: Logger;
requestBody: any;
requestType: RENDER_AS;
geoFieldType: ES_GEO_FIELD_TYPE;
searchSessionId?: string;
}): Promise<Buffer | null> {
const esBbox: ESBounds = tileToESBbox(x, y, z);
try {
@ -79,13 +82,20 @@ export async function getGridTile({
);
requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = esBbox;
const esGeotileGridQuery = {
index,
body: requestBody,
};
const gridAggResult = await callElasticsearch('search', esGeotileGridQuery);
const features: Feature[] = convertRegularRespToGeoJson(gridAggResult, requestType);
const response = await context
.search!.search(
{
params: {
index,
body: requestBody,
},
},
{
sessionId: searchSessionId,
}
)
.toPromise();
const features: Feature[] = convertRegularRespToGeoJson(response.rawResponse, requestType);
const featureCollection: FeatureCollection = {
features,
type: 'FeatureCollection',
@ -100,7 +110,7 @@ export async function getGridTile({
export async function getTile({
logger,
callElasticsearch,
context,
index,
geometryFieldName,
x,
@ -108,112 +118,121 @@ export async function getTile({
z,
requestBody = {},
geoFieldType,
searchSessionId,
}: {
x: number;
y: number;
z: number;
geometryFieldName: string;
index: string;
callElasticsearch: (type: string, ...args: any[]) => Promise<unknown>;
context: RequestHandlerContext & { search: DataApiRequestHandlerContext };
logger: Logger;
requestBody: any;
geoFieldType: ES_GEO_FIELD_TYPE;
searchSessionId?: string;
}): Promise<Buffer | null> {
const geojsonBbox = tileToGeoJsonPolygon(x, y, z);
let resultFeatures: Feature[];
let features: Feature[];
try {
let result;
try {
const geoShapeFilter = {
geo_shape: {
[geometryFieldName]: {
shape: geojsonBbox,
relation: 'INTERSECTS',
requestBody.query.bool.filter.push({
geo_shape: {
[geometryFieldName]: {
shape: tileToGeoJsonPolygon(x, y, z),
relation: 'INTERSECTS',
},
},
});
const searchOptions = {
sessionId: searchSessionId,
};
const countResponse = await context
.search!.search(
{
params: {
index,
body: {
size: 0,
query: requestBody.query,
},
},
},
};
requestBody.query.bool.filter.push(geoShapeFilter);
searchOptions
)
.toPromise();
const esSearchQuery = {
index,
body: requestBody,
};
const esCountQuery = {
index,
body: {
query: requestBody.query,
},
};
const countResult = await callElasticsearch('count', esCountQuery);
// @ts-expect-error
if (countResult.count > requestBody.size) {
// Generate "too many features"-bounds
const bboxAggName = 'data_bounds';
const bboxQuery = {
index,
body: {
size: 0,
query: requestBody.query,
aggs: {
[bboxAggName]: {
geo_bounds: {
field: geometryFieldName,
if (countResponse.rawResponse.hits.total > requestBody.size) {
// Generate "too many features"-bounds
const bboxResponse = await context
.search!.search(
{
params: {
index,
body: {
size: 0,
query: requestBody.query,
aggs: {
data_bounds: {
geo_bounds: {
field: geometryFieldName,
},
},
},
},
},
},
};
searchOptions
)
.toPromise();
const bboxResult = await callElasticsearch('search', bboxQuery);
// @ts-expect-error
const bboxForData = esBboxToGeoJsonPolygon(bboxResult.aggregations[bboxAggName].bounds);
resultFeatures = [
features = [
{
type: 'Feature',
properties: {
[KBN_TOO_MANY_FEATURES_PROPERTY]: true,
},
geometry: esBboxToGeoJsonPolygon(
bboxResponse.rawResponse.aggregations.data_bounds.bounds
),
},
];
} else {
const documentsResponse = await context
.search!.search(
{
type: 'Feature',
properties: {
[KBN_TOO_MANY_FEATURES_PROPERTY]: true,
params: {
index,
body: requestBody,
},
geometry: bboxForData,
},
];
} else {
result = await callElasticsearch('search', esSearchQuery);
searchOptions
)
.toPromise();
// Todo: pass in epochMillies-fields
const featureCollection = hitsToGeoJson(
// @ts-expect-error
result.hits.hits,
(hit: Record<string, unknown>) => {
return flattenHit(geometryFieldName, hit);
},
geometryFieldName,
geoFieldType,
[]
);
// Todo: pass in epochMillies-fields
const featureCollection = hitsToGeoJson(
documentsResponse.rawResponse.hits.hits,
(hit: Record<string, unknown>) => {
return flattenHit(geometryFieldName, hit);
},
geometryFieldName,
geoFieldType,
[]
);
resultFeatures = featureCollection.features;
features = featureCollection.features;
// Correct system-fields.
for (let i = 0; i < resultFeatures.length; i++) {
const props = resultFeatures[i].properties;
if (props !== null) {
props[FEATURE_ID_PROPERTY_NAME] = resultFeatures[i].id;
}
// Correct system-fields.
for (let i = 0; i < features.length; i++) {
const props = features[i].properties;
if (props !== null) {
props[FEATURE_ID_PROPERTY_NAME] = features[i].id;
}
}
} catch (e) {
logger.warn(e.message);
throw e;
}
const featureCollection: FeatureCollection = {
features: resultFeatures,
features,
type: 'FeatureCollection',
};

View file

@ -13,6 +13,7 @@ import {
RequestHandlerContext,
} from 'src/core/server';
import { IRouter } from 'src/core/server';
import type { DataApiRequestHandlerContext } from 'src/plugins/data/server';
import {
MVT_GETTILE_API_PATH,
API_ROOT_PATH,
@ -24,7 +25,13 @@ import { getGridTile, getTile } from './get_tile';
const CACHE_TIMEOUT = 0; // Todo. determine good value. Unsure about full-implications (e.g. wrt. time-based data).
export function initMVTRoutes({ router, logger }: { logger: Logger; router: IRouter }) {
export function initMVTRoutes({
router,
logger,
}: {
router: IRouter<RequestHandlerContext & { search: DataApiRequestHandlerContext }>;
logger: Logger;
}) {
router.get(
{
path: `${API_ROOT_PATH}/${MVT_GETTILE_API_PATH}`,
@ -37,11 +44,12 @@ export function initMVTRoutes({ router, logger }: { logger: Logger; router: IRou
requestBody: schema.string(),
index: schema.string(),
geoFieldType: schema.string(),
searchSessionId: schema.maybe(schema.string()),
}),
},
},
async (
context: RequestHandlerContext,
context: RequestHandlerContext & { search: DataApiRequestHandlerContext },
request: KibanaRequest<unknown, Record<string, any>, unknown>,
response: KibanaResponseFactory
) => {
@ -50,7 +58,7 @@ export function initMVTRoutes({ router, logger }: { logger: Logger; router: IRou
const tile = await getTile({
logger,
callElasticsearch: makeCallElasticsearch(context),
context,
geometryFieldName: query.geometryFieldName as string,
x: query.x as number,
y: query.y as number,
@ -58,6 +66,7 @@ export function initMVTRoutes({ router, logger }: { logger: Logger; router: IRou
index: query.index as string,
requestBody: requestBodyDSL as any,
geoFieldType: query.geoFieldType as ES_GEO_FIELD_TYPE,
searchSessionId: query.searchSessionId,
});
return sendResponse(response, tile);
@ -77,11 +86,12 @@ export function initMVTRoutes({ router, logger }: { logger: Logger; router: IRou
index: schema.string(),
requestType: schema.string(),
geoFieldType: schema.string(),
searchSessionId: schema.maybe(schema.string()),
}),
},
},
async (
context: RequestHandlerContext,
context: RequestHandlerContext & { search: DataApiRequestHandlerContext },
request: KibanaRequest<unknown, Record<string, any>, unknown>,
response: KibanaResponseFactory
) => {
@ -90,7 +100,7 @@ export function initMVTRoutes({ router, logger }: { logger: Logger; router: IRou
const tile = await getGridTile({
logger,
callElasticsearch: makeCallElasticsearch(context),
context,
geometryFieldName: query.geometryFieldName as string,
x: query.x as number,
y: query.y as number,
@ -99,6 +109,7 @@ export function initMVTRoutes({ router, logger }: { logger: Logger; router: IRou
requestBody: requestBodyDSL as any,
requestType: query.requestType as RENDER_AS,
geoFieldType: query.geoFieldType as ES_GEO_FIELD_TYPE,
searchSessionId: query.searchSessionId,
});
return sendResponse(response, tile);
@ -125,9 +136,3 @@ function sendResponse(response: KibanaResponseFactory, tile: any) {
});
}
}
function makeCallElasticsearch(context: RequestHandlerContext) {
return async (type: string, ...args: any[]): Promise<unknown> => {
return context.core.elasticsearch.legacy.client.callAsCurrentUser(type, ...args);
};
}

View file

@ -4,26 +4,92 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { VectorTile } from '@mapbox/vector-tile';
import Protobuf from 'pbf';
import expect from '@kbn/expect';
import {
KBN_IS_CENTROID_FEATURE,
MVT_SOURCE_LAYER_NAME,
} from '../../../../plugins/maps/common/constants';
export default function ({ getService }) {
const supertest = getService('supertest');
describe('getGridTile', () => {
it('should validate params', async () => {
await supertest
it('should return vector tile containing cluster features', async () => {
const resp = await supertest
.get(
`/api/maps/mvt/getGridTile?x=0&y=0&z=0&geometryFieldName=coordinates&index=logstash*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:coordinates))),geotile_grid:(bounds:!n,field:coordinates,precision:!n,shard_size:65535,size:65535))),docvalue_fields:!((field:%27@timestamp%27,format:date_time),(field:timestamp,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(timestamp:(format:strict_date_optional_time,gte:%272020-09-16T13:57:36.734Z%27,lte:%272020-09-23T13:57:36.734Z%27)))),must:!(),must_not:!(),should:!())),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))&requestType=point&geoFieldType=geo_point`
`/api/maps/mvt/getGridTile\
?x=2\
&y=3\
&z=3\
&geometryFieldName=geo.coordinates\
&index=logstash-*\
&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(avg_of_bytes:(avg:(field:bytes)),gridCentroid:(geo_centroid:(field:geo.coordinates))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))\
&requestType=point\
&geoFieldType=geo_point`
)
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME];
expect(layer.length).to.be(1);
const clusterFeature = layer.feature(0);
expect(clusterFeature.type).to.be(1);
expect(clusterFeature.extent).to.be(4096);
expect(clusterFeature.id).to.be(undefined);
expect(clusterFeature.properties).to.eql({ doc_count: 1, avg_of_bytes: 9252 });
expect(clusterFeature.loadGeometry()).to.eql([[{ x: 87, y: 667 }]]);
});
it('should not validate when required params are missing', async () => {
await supertest
it('should return vector tile containing grid features', async () => {
const resp = await supertest
.get(
`/api/maps/mvt/getGridTile?x=0&y=0&z=0&geometryFieldName=coordinates&index=logstash*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:coordinates))),geotile_grid:(bounds:!n,field:coordinates,precision:!n,shard_size:65535,size:65535))),docvalue_fields:!((field:%27@timestamp%27,format:date_time),(field:timestamp,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(timestamp:(format:strict_date_optional_time,gte:%272020-09-16T13:57:36.734Z%27,lte:%272020-09-23T13:57:36.734Z%27)))),must:!(),must_not:!(),should:!())),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))&requestType=point`
`/api/maps/mvt/getGridTile\
?x=2\
&y=3\
&z=3\
&geometryFieldName=geo.coordinates\
&index=logstash-*\
&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(avg_of_bytes:(avg:(field:bytes)),gridCentroid:(geo_centroid:(field:geo.coordinates))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:%27@timestamp%27,format:date_time),(field:%27relatedContent.article:modified_time%27,format:date_time),(field:%27relatedContent.article:published_time%27,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27@timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))\
&requestType=grid\
&geoFieldType=geo_point`
)
.set('kbn-xsrf', 'kibana')
.expect(400);
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME];
expect(layer.length).to.be(2);
const gridFeature = layer.feature(0);
expect(gridFeature.type).to.be(3);
expect(gridFeature.extent).to.be(4096);
expect(gridFeature.id).to.be(undefined);
expect(gridFeature.properties).to.eql({ doc_count: 1, avg_of_bytes: 9252 });
expect(gridFeature.loadGeometry()).to.eql([
[
{ x: 96, y: 640 },
{ x: 96, y: 672 },
{ x: 64, y: 672 },
{ x: 64, y: 640 },
{ x: 96, y: 640 },
],
]);
const clusterFeature = layer.feature(1);
expect(clusterFeature.type).to.be(1);
expect(clusterFeature.extent).to.be(4096);
expect(clusterFeature.id).to.be(undefined);
expect(clusterFeature.properties).to.eql({
doc_count: 1,
avg_of_bytes: 9252,
[KBN_IS_CENTROID_FEATURE]: true,
});
expect(clusterFeature.loadGeometry()).to.eql([[{ x: 80, y: 656 }]]);
});
});
}

View file

@ -4,26 +4,82 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { VectorTile } from '@mapbox/vector-tile';
import Protobuf from 'pbf';
import expect from '@kbn/expect';
import { MVT_SOURCE_LAYER_NAME } from '../../../../plugins/maps/common/constants';
export default function ({ getService }) {
const supertest = getService('supertest');
describe('getTile', () => {
it('should validate params', async () => {
await supertest
it('should return vector tile containing document', async () => {
const resp = await supertest
.get(
`/api/maps/mvt/getTile?x=15&y=11&z=5&geometryFieldName=coordinates&index=logstash*&requestBody=(_source:(includes:!(coordinates)),docvalue_fields:!(),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(coordinates))&geoFieldType=geo_point`
`/api/maps/mvt/getTile\
?x=1\
&y=1\
&z=2\
&geometryFieldName=geo.coordinates\
&index=logstash-*\
&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10000,stored_fields:!(bytes,geo.coordinates,machine.os.raw))\
&geoFieldType=geo_point`
)
.set('kbn-xsrf', 'kibana')
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME];
expect(layer.length).to.be(2);
const feature = layer.feature(0);
expect(feature.type).to.be(1);
expect(feature.extent).to.be(4096);
expect(feature.id).to.be(undefined);
expect(feature.properties).to.eql({
__kbn__feature_id__: 'logstash-2015.09.20:AU_x3_BsGFA8no6Qjjug:0',
_id: 'AU_x3_BsGFA8no6Qjjug',
_index: 'logstash-2015.09.20',
bytes: 9252,
['machine.os.raw']: 'ios',
});
expect(feature.loadGeometry()).to.eql([[{ x: 44, y: 2382 }]]);
});
it('should not validate when required params are missing', async () => {
await supertest
it('should return vector tile containing bounds when count exceeds size', async () => {
const resp = await supertest
// requestBody sets size=1 to force count exceeded
.get(
`/api/maps/mvt/getTile?&index=logstash*&requestBody=(_source:(includes:!(coordinates)),docvalue_fields:!(),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),script_fields:(),size:10000,stored_fields:!(coordinates))`
`/api/maps/mvt/getTile\
?x=1\
&y=1\
&z=2\
&geometryFieldName=geo.coordinates\
&index=logstash-*\
&requestBody=(_source:!f,docvalue_fields:!(bytes,geo.coordinates,machine.os.raw),query:(bool:(filter:!((match_all:()),(range:(%27@timestamp%27:(format:strict_date_optional_time,gte:%272015-09-20T00:00:00.000Z%27,lte:%272015-09-20T01:00:00.000Z%27)))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:1,stored_fields:!(bytes,geo.coordinates,machine.os.raw))\
&geoFieldType=geo_point`
)
.set('kbn-xsrf', 'kibana')
.expect(400);
.responseType('blob')
.expect(200);
const jsonTile = new VectorTile(new Protobuf(resp.body));
const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME];
expect(layer.length).to.be(1);
const feature = layer.feature(0);
expect(feature.type).to.be(3);
expect(feature.extent).to.be(4096);
expect(feature.id).to.be(undefined);
expect(feature.properties).to.eql({ __kbn_too_many_features__: true });
expect(feature.loadGeometry()).to.eql([
[
{ x: 44, y: 2382 },
{ x: 44, y: 1913 },
{ x: 550, y: 1913 },
{ x: 550, y: 2382 },
{ x: 44, y: 2382 },
],
]);
});
});
}