Skip to content

Commit

Permalink
Merge pull request #385 from inaturalist/elasticmaps
Browse files Browse the repository at this point in the history
Elasticmaps
  • Loading branch information
pleary authored Jun 27, 2023
2 parents 6cfa3fa + 8cc50a8 commit 2b04b9a
Show file tree
Hide file tree
Showing 41 changed files with 6,149 additions and 2,432 deletions.
1 change: 1 addition & 0 deletions config.js.ci
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
uploadsDir: "/tmp/",
tensorappURL: "http://localhost:6006"
},
tileSize: 512,
debug: true,
jwtSecret: "secret",
jwtApplicationSecret: "application_secret"
Expand Down
3 changes: 0 additions & 3 deletions config_example.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
let environment = "development";
if ( global && global.config && global.config.environment ) {
environment = global.config.environment; // eslint-disable-line prefer-destructuring
}
if ( process && process.env && process.env.NODE_ENV ) {
environment = process.env.NODE_ENV;
}
Expand Down
Binary file added lib/assets/blank.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion lib/controllers/v1/computervision_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ const ComputervisionController = class ComputervisionController {
// download the JPG
const parsedPhotoURL = path.parse( photoURL );
const tmpFilename = `${md5( photoURL )}${parsedPhotoURL.ext.replace( /\?.+/, "" )}`;
const tmpPath = path.resolve( global.config.imageProcesing.uploadsDir, tmpFilename );
const tmpPath = path.resolve( config.imageProcesing.uploadsDir, tmpFilename );

const imageRequestAbortController = new AbortController( );
const imageRequestTimeout = setTimeout( ( ) => {
Expand Down
46 changes: 19 additions & 27 deletions lib/controllers/v1/taxa_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,14 +447,12 @@ TaxaController.wanted = async req => {
filters: [{ terms: { id: [req.params.id] } }],
per_page: Number( req.query.per_page || req.query.size ) || 30,
page: Number( req.query.page ) || 1,
sort: req.query.sort || { observations_count: "desc" }
} );
const taxonData = await esClient.connection.search( {
preference: global.config.elasticsearch.preference,
index: `${process.env.NODE_ENV || global.config.environment}_taxa`,
body: taxonQuery,
sort: req.query.sort || { observations_count: "desc" },
_source: ["id"]
} );
const taxonData = await esClient.search( "taxa", {
body: taxonQuery
} );
if ( _.isEmpty( taxonData.hits.hits ) ) {
throw util.httpError( 404, "Not Found" );
}
Expand All @@ -470,14 +468,12 @@ TaxaController.wanted = async req => {
esClient.termFilter( "extinct", true )
],
per_page: 1000,
page: 1
} );
const descendantData = await esClient.connection.search( {
preference: global.config.elasticsearch.preference,
index: `${process.env.NODE_ENV || global.config.environment}_taxa`,
body: descendantQuery,
page: 1,
_source: ["id"]
} );
const descendantData = await esClient.search( "taxa", {
body: descendantQuery
} );
const descendantTaxonIds = _.map( descendantData.hits.hits, h => h._source.id );
// get ids of all observed taxa
const speciesCountsReq = {
Expand Down Expand Up @@ -508,23 +504,21 @@ TaxaController.replaceInactiveTaxa = async ( objects, opts = { } ) => {
const options = { numericalCompareProperty: "count", ...opts };
const objectsKeyedByTaxon = _.keyBy( objects, "taxon_id" );
const taxonIDs = _.keys( objectsKeyedByTaxon );
const searchHash = {
filters: [{ terms: { id: taxonIDs } }],
per_page: taxonIDs.length,
page: 1
};
const defaultSource = [
"id",
"current_synonymous_taxon_ids",
"is_active"
];
const searchHash = {
filters: [{ terms: { id: taxonIDs } }],
per_page: taxonIDs.length,
page: 1,
_source: defaultSource
};
let newTaxonIDs = [];
// quick direct ES query returning only the fields needed to replace inactive taxa
const data = await esClient.connection.search( {
preference: global.config.elasticsearch.preference,
index: `${process.env.NODE_ENV || global.config.environment}_taxa`,
body: esClient.searchHash( searchHash ),
_source: defaultSource
const data = await esClient.search( "taxa", {
body: esClient.searchHash( searchHash )
} );
_.each( data.hits.hits, h => {
if ( h._source.is_active === false ) {
Expand Down Expand Up @@ -696,11 +690,9 @@ TaxaController.searchQuery = async ( req, opts = { } ) => {
source: { _source: req._source || defaultSource }
} );
} else {
data = await esClient.connection.search( {
preference: global.config.elasticsearch.preference,
index: `${process.env.NODE_ENV || global.config.environment}_taxa`,
body: elasticQuery,
_source: req._source || defaultSource
elasticQuery._source = req._source || defaultSource;
data = await esClient.search( "taxa", {
body: elasticQuery
} );
}
const localeOpts = options.localeOpts || util.localeOpts( req );
Expand Down
12 changes: 3 additions & 9 deletions lib/controllers/v1/users_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,7 @@ const UsersController = class UsersController {
const members = await project.members( req.query );
filters.push( esClient.termFilter( "id", _.map( members, m => m.user_id ) ) );
}
const exactResponse = await esClient.connection.search( {
preference: global.config.elasticsearch.preference,
index: `${process.env.NODE_ENV || global.config.environment}_users`,
const exactResponse = await esClient.search( "users", {
body: {
query: {
bool: {
Expand All @@ -112,9 +110,7 @@ const UsersController = class UsersController {
{ match: { login: { query: req.query.q, operator: "and" } } }
];
filters.push( { bool: { should: shoulds } } );
const response = await esClient.connection.search( {
preference: global.config.elasticsearch.preference,
index: `${process.env.NODE_ENV || global.config.environment}_users`,
const response = await esClient.search( "users", {
body: {
query: {
bool: {
Expand Down Expand Up @@ -165,9 +161,7 @@ const UsersController = class UsersController {
}
}

const response = await esClient.connection.search( {
preference: global.config.elasticsearch.preference,
index: `${process.env.NODE_ENV || global.config.environment}_projects`,
const response = await esClient.search( "projects", {
body: {
query: {
bool: {
Expand Down
4 changes: 1 addition & 3 deletions lib/controllers/v2/users_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ const index = async req => {
);
filters.push( esClient.termFilter( "id", _.map( followees, f => f.friend_id ) ) );
}
const response = await esClient.connection.search( {
preference: global.config.elasticsearch.preference,
index: `${process.env.NODE_ENV || global.config.environment}_users`,
const response = await esClient.search( "users", {
body: {
query: {
bool: {
Expand Down
171 changes: 171 additions & 0 deletions lib/elastic_mapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
const querystring = require( "querystring" );
const _ = require( "lodash" );
const Geohash = require( "latlon-geohash" );
const MapGenerator = require( "./map_generator" );
const ElasticRequest = require( "./elastic_request" );
const util = require( "./util" );
const esClient = require( "./es_client" );

const ElasticMapper = { };

ElasticMapper.renderMessage = ( res, message, status ) => {
res.set( "Content-Type", "text/html" );
res.status( status ).send( message ).end( );
};

ElasticMapper.renderError = ( res, error ) => {
util.debug( error );
if ( error.message && error.status ) {
ElasticMapper.renderMessage( res, error.message, error.status );
} else {
ElasticMapper.renderMessage( res, "Error", 500 );
}
};

ElasticMapper.renderResult = ( req, res, data ) => {
if ( req.params.format === "grid.json" ) {
res.jsonp( data );
} else {
res.writeHead( 200, { "Content-Type": "image/png" } );
res.end( data );
}
};

ElasticMapper.geotileGridGeojson = hit => {
const parts = hit.key.split( "/" );
MapGenerator.createMercator( );
const bbox = MapGenerator.merc.bbox( parts[1], parts[2], parts[0] );
return {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [[
[bbox[0], bbox[1]],
[bbox[2], bbox[1]],
[bbox[2], bbox[3]],
[bbox[0], bbox[3]],
[bbox[0], bbox[1]]
]]
},
properties: {
cellCount: hit.doc_count
}
};
};

ElasticMapper.geohashGridGeojson = hit => {
const bbox = Geohash.bounds( hit.key );
return {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [[
[bbox.sw.lon, bbox.sw.lat],
[bbox.sw.lon, bbox.ne.lat],
[bbox.ne.lon, bbox.ne.lat],
[bbox.ne.lon, bbox.sw.lat],
[bbox.sw.lon, bbox.sw.lat]
]]
},
properties: {
cellCount: hit.doc_count
}
};
};

ElasticMapper.csvFromResult = ( req, result ) => {
if ( req.params.dataType === "geojson" ) {
return ElasticMapper.polygonCSVFromResult( req, result );
}
let target;
if ( result.aggregations && result.aggregations.zoom1 ) {
target = _.sortBy( result.aggregations.zoom1.buckets, hit => (
hit.geohash ? hit.geohash.hits.hits[0].sort[0] : null
) );
} else if ( result.hits ) {
target = result.hits.hits;
} else { return []; }
const fieldsToMap = ( req.query.source && req.query.source.includes )
? _.without( req.query.source.includes, "location" )
: [];
fieldsToMap.push( "cellCount" );
const csvData = _.map( target, hit => {
// grids get rendered as polygons
if ( req.geotilegrid && req.params.format !== "grid.json" ) {
return ElasticMapper.geotileGridGeojson( hit );
}
if ( req.geogrid ) {
return ElasticMapper.geohashGridGeojson( hit );
}
const fieldData = hit._source || hit.geohash.hits.hits[0]._source;
fieldData.private_location = !_.isEmpty( fieldData.private_location );
if ( !hit._source && hit.geohash ) {
fieldData.cellCount = hit.geohash.hits.total.value;
}
const properties = { };
let value;
_.each( fieldsToMap, f => {
if ( f.match( /\./ ) ) {
const parts = f.split( "." );
if ( fieldData[parts[0]] && fieldData[parts[0]][parts[1]] ) {
value = fieldData[parts[0]][parts[1]];
} else {
value = null;
}
} else {
value = fieldData[f] ? fieldData[f] : null;
}
if ( value === "F" ) { value = false; }
if ( value === "T" ) { value = true; }
properties[f] = value;
} );
let latitude;
let longitude;
if ( req.geotilegrid ) {
const parts = hit.key.split( "/" );
MapGenerator.createMercator( );
const bbox = MapGenerator.merc.bbox( parts[1], parts[2], parts[0] );
latitude = _.mean( [bbox[1], bbox[3]] );
longitude = _.mean( [bbox[0], bbox[2]] );
} else if ( _.isObject( fieldData.location ) ) {
latitude = fieldData.location.lat;
longitude = fieldData.location.lon;
} else {
const coords = fieldData.location.split( "," );
latitude = Number( coords[0] );
longitude = Number( coords[1] );
}
if ( req.params.format === "grid.json" ) {
properties.latitude = latitude;
properties.longitude = longitude;
}
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: [longitude, latitude]
},
properties
};
} );
return csvData;
};

ElasticMapper.polygonCSVFromResult = ( req, result ) => (
_.map( result.hits.hits, hit => (
{ id: hit._source.id, geojson: hit._source.geometry_geojson }
) )
);

ElasticMapper.printRequestLog = req => {
let logText = `[ ${new Date( ).toString( )}] GET /${req.params.style}`
+ `/${req.params.zoom}/${req.params.x}`
+ `/${req.params.y}.${req.params.format}`;
if ( !_.isEmpty( req.query ) ) {
logText += `?${querystring.stringify( req.query )}`;
}
logText += ` ${req.endTime - req.startTime}ms`;
util.debug( logText );
};

module.exports = ElasticMapper;
Loading

0 comments on commit 2b04b9a

Please sign in to comment.