Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/controllers/v1/observations_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ ObservationsController.searchCacheWrapper = async req => (
);

ObservationsController.search = async ( req, options = { } ) => {
if ( req.query.read_term_id_and_value_id_as_pairs === "true"
&& req.query.term_id && req.query.term_value_id
&& util.paramArray( req.query.term_id ).length
!== util.paramArray( req.query.term_value_id ).length
) {
const e = new Error( );
e.status = 400;
e.message = "term_id and term_value_id must be the same length when read_term_id_and_value_id_as_pairs = true.";
throw e;
}
if ( req.query.return_bounds === "true" ) {
// If we've been asked to return the bounds but also to return obs in a
// place, assume that the bounds are the bounding box of the place and not
Expand Down
63 changes: 46 additions & 17 deletions lib/models/observation_query_builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -943,27 +943,56 @@ ObservationQueryBuilder.reqToElasticQueryComponents = async req => {
}

if ( params.term_id ) {
const initialFilters = [];
initialFilters.push(
esClient.termFilter( "annotations.controlled_attribute_id.keyword", params.term_id )
);
initialFilters.push( { range: { "annotations.vote_score_short": { gte: 0 } } } );
const nestedQuery = {
nested: {
path: "annotations",
query: {
bool: {
filter: initialFilters
if ( params.read_term_id_and_value_id_as_pairs === "true" && params.term_value_id
&& util.paramArray( params.term_id ).length
=== util.paramArray( params.term_value_id ).length ) {
const termValueIdArray = util.paramArray( params.term_value_id );
const termIdTermValuePairs = _.map(
util.paramArray( params.term_id ), ( termId, index ) => ( {
termId,
termValueId: termValueIdArray[index]
} )
);
_.each( termIdTermValuePairs, pair => {
const termIdFilter = esClient.termFilter( "annotations.controlled_attribute_id.keyword", pair.termId );
const voteScoreZeroOrPosFilter = { range: { "annotations.vote_score_short": { gte: 0 } } };
const termValueIdFilter = esClient.termFilter( "annotations.controlled_value_id.keyword", pair.termValueId );
const nestedQuery = {
nested: {
path: "annotations",
query: {
bool: {
filter: [termIdFilter, voteScoreZeroOrPosFilter, termValueIdFilter]
}
}
}
};
// each pair is its own searchFilter to ensure obsv with ALL pairs would pass.
searchFilters.push( nestedQuery );
} );
} else {
const initialFilters = [];
initialFilters.push(
esClient.termFilter( "annotations.controlled_attribute_id.keyword", params.term_id )
);
initialFilters.push( { range: { "annotations.vote_score_short": { gte: 0 } } } );
const nestedQuery = {
nested: {
path: "annotations",
query: {
bool: {
filter: initialFilters
}
}
}
};
if ( params.term_value_id ) {
nestedQuery.nested.query.bool.filter.push(
esClient.termFilter( "annotations.controlled_value_id.keyword", params.term_value_id )
);
}
};
if ( params.term_value_id ) {
nestedQuery.nested.query.bool.filter.push(
esClient.termFilter( "annotations.controlled_value_id.keyword", params.term_value_id )
);
searchFilters.push( nestedQuery );
}
searchFilters.push( nestedQuery );
if ( params.without_term_value_id ) {
inverseFilters.push( ObservationQueryBuilder.annotationTermValueNestedFilter(
params.term_id, params.without_term_value_id
Expand Down
1 change: 1 addition & 0 deletions lib/views/_observation_search_params_v1.yml.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
- $ref: "#/parameters/without_term_id"
- $ref: "#/parameters/without_term_value_id"
- $ref: "#/parameters/term_id_or_unknown"
- $ref: "#/parameters/read_term_id_and_value_id_as_pairs"
- $ref: "#/parameters/annotation_user_id"
# other
- $ref: "#/parameters/acc_above"
Expand Down
12 changes: 12 additions & 0 deletions lib/views/swagger_v1.yml.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -2233,6 +2233,18 @@ parameters:
in: query
description: |
Observations that have been favorited by at least one user
read_term_id_and_value_id_as_pairs:
name: read_term_id_and_value_id_as_pairs
type: boolean
in: query
description: |
Determines how to interpret the term_id and term_value_id parameters when both are provided and are of same length.
It does not affect how without_term_value_id will be interpreted.
If false, observations will satisfy having at least one of term_value_id's for any of the provided term_id's.
If true, term_id and term_value_id are grouped and filtered for in pairs.
For example, term_id=1,1,3 and term_value_id=2,5,6 is interpreted as the following {term_id, term_value_id} pairs:
{1, 2}, {1, 5}, and {3, 6}. Observations will satisfy having an entry for every pair with a 0 or positive vote score.
If this parameter is true, but term_id and term_value_id are not the same length, error code 400 is returned.
sounds:
name: sounds
type: boolean
Expand Down
10 changes: 10 additions & 0 deletions openapi/schema/request/observations_search.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ module.exports = Joi.object( ).keys( {
term_value_id: Joi.array( ).items( Joi.number( ).integer( ) ),
without_term_value_id: Joi.array( ).items( Joi.number( ).integer( ) ),
term_id_or_unknown: Joi.array( ).items( Joi.number( ).integer( ) ),
read_term_id_and_value_id_as_pairs: Joi.boolean( )
.description(
"Determines how to interpret the term_id and term_value_id parameters when both are provided and are of same length. "
+ "It does not affect how without_term_value_id will be interpreted.\n\n"
+ "If false, observations will satisfy having at least one of term_value_id's for any of the provided term_id's.\n"
+ "If true, term_id and term_value_id are grouped and filtered for in pairs. "
+ "For example, term_id=1,1,3 and term_value_id=2,5,6 is interpreted as the following {term_id, term_value_id} pairs: "
+ "{1, 2}, {1, 5}, and {3, 6}. Observations will satisfy having an entry for every pair with a 0 or positive vote score.\n"
+ "If this parameter is true, but term_id and term_value_id are not the same length, error code 400 is returned. "
),
annotation_user_id: Joi.array( ).items( Joi.string( ) ),
acc_above: Joi.number( ).integer( ),
acc_below: Joi.number( ).integer( ),
Expand Down
66 changes: 66 additions & 0 deletions schema/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,72 @@
}
],
"sounds_count": 1
},
{
"id": 2025102201,
"user": { "id": 2025102299 },
"annotations": [
{
"controlled_attribute_id": 10,
"controlled_value_id": 20,
"concatenated_attr_val": "10|20",
"vote_score_short": 0
},
{
"controlled_attribute_id": 10,
"controlled_value_id": 21,
"concatenated_attr_val": "10|21",
"vote_score_short": 0
},
{
"controlled_attribute_id": 12,
"controlled_value_id": 22,
"concatenated_attr_val": "12|22",
"vote_score_short": 0
}
]
},
{
"id": 2025102202,
"user": { "id": 2025102299 },
"annotations": [
{
"controlled_attribute_id": 10,
"controlled_value_id": 20,
"concatenated_attr_val": "10|20",
"vote_score_short": 0
},
{
"controlled_attribute_id": 12,
"controlled_value_id": 22,
"concatenated_attr_val": "12|22",
"vote_score_short": 0
},
{
"controlled_attribute_id": 13,
"controlled_value_id": 23,
"concatenated_attr_val": "13|23",
"vote_score_short": 0
}
]
},
{
"id": 2025102203,
"user": { "id": 2025102299 },
"annotations": [
{
"controlled_attribute_id": 10,
"controlled_value_id": 20,
"concatenated_attr_val": "10|20",
"vote_score_short": 0
},
{
"controlled_attribute_id": 10,
"controlled_value_id": 21,
"concatenated_attr_val": "10|21",
"vote_score_short": 0
}
]
}
]
},
Expand Down
31 changes: 31 additions & 0 deletions test/integration/v1/observations.js
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,28 @@ describe( "Observations", ( ) => {
} ).expect( 200, done );
} );

it( "filters by term_id and term_value_id in pairs when requested", function ( done ) {
request( this.app ).get( "/v1/observations?term_id=10,10,12&term_value_id=20,21,22&read_term_id_and_value_id_as_pairs=true" )
.expect( res => {
expect( res.body.results.length ).to.eq( 1 );
expect( res.body.results.map( r => r.id ) ).to.contain( 2025102201 );
} ).expect( 200, done );
} );

it( "filters by term_id and term_value_id in pairs when requested -- multiple results", function ( done ) {
request( this.app ).get( "/v1/observations?term_id=10,12&term_value_id=20,22&read_term_id_and_value_id_as_pairs=true" )
.expect( res => {
expect( res.body.results.length ).to.eq( 2 );
expect( res.body.results.map( r => r.id ) ).to.contain( 2025102201 );
expect( res.body.results.map( r => r.id ) ).to.contain( 2025102202 );
} ).expect( 200, done );
} );

it( "throws 400 when filtering by term_id and term_value_id are unequal length when requesting to read them as pairs", function ( done ) {
request( this.app ).get( "/v1/observations?term_id=10,10,12&term_value_id=20,21&read_term_id_and_value_id_as_pairs=true" )
.expect( 400, done );
} );

it( "filters by without_term_id", function ( done ) {
request( this.app ).get( "/v1/observations?without_term_id=1&id=6,7,9" )
.expect( res => {
Expand Down Expand Up @@ -733,6 +755,15 @@ describe( "Observations", ( ) => {
} ).expect( 200, done );
} );

it( "filters by without_term_value_id unaffected by read_term_id_and_value_id_as_pairs", function ( done ) {
request( this.app ).get( "/v1/observations?term_id=1&without_term_value_id=1&read_term_id_and_value_id_as_pairs=true" )
.expect( res => {
expect( res.body.results.map( r => r.id ) ).to.contain( 8 );
expect( res.body.results.map( r => r.id ) ).not.to.contain( 7 );
expect( res.body.results.map( r => r.id ) ).not.to.contain( 6 );
} ).expect( 200, done );
} );

it( "can return only ids", function ( done ) {
request( this.app ).get( "/v1/observations?id=2&only_id=true&per_page=1" )
.expect( res => {
Expand Down