Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scc 4346/non roman 2 #423

Merged
merged 9 commits into from
Dec 10, 2024
Merged
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
28 changes: 14 additions & 14 deletions lib/elasticsearch/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,20 @@ const SEARCH_SCOPES = {
}

const FILTER_CONFIG = {
recordType: { operator: 'match', field: 'recordTypeId', repeatable: true },
owner: { operator: 'match', field: 'items.owner_packed', repeatable: true, path: 'items' },
subjectLiteral: { operator: 'match', field: 'subjectLiteral_exploded', repeatable: true },
holdingLocation: { operator: 'match', field: 'items.holdingLocation_packed', repeatable: true, path: 'items' },
buildingLocation: { operator: 'match', field: 'buildingLocationIds', repeatable: true },
language: { operator: 'match', field: 'language_packed', repeatable: true },
materialType: { operator: 'match', field: 'materialType_packed', repeatable: true },
mediaType: { operator: 'match', field: 'mediaType_packed', repeatable: true },
carrierType: { operator: 'match', field: 'carrierType_packed', repeatable: true },
publisher: { operator: 'match', field: 'publisherLiteral.raw', repeatable: true },
contributorLiteral: { operator: 'match', field: 'contributorLiteral.raw', repeatable: true },
creatorLiteral: { operator: 'match', field: 'creatorLiteral.raw', repeatable: true },
issuance: { operator: 'match', field: 'issuance_packed', repeatable: true },
createdYear: { operator: 'match', field: 'createdYear', repeatable: true },
recordType: { operator: 'match', field: ['recordTypeId'], repeatable: true },
owner: { operator: 'match', field: ['items.owner.id', 'items.owner.label'], repeatable: true, path: 'items' },
subjectLiteral: { operator: 'match', field: ['subjectLiteral_exploded'], repeatable: true },
holdingLocation: { operator: 'match', field: ['items.holdingLocation.id', 'items.holdingLocation.label'], repeatable: true, path: 'items' },
buildingLocation: { operator: 'match', field: ['buildingLocationIds'], repeatable: true },
language: { operator: 'match', field: ['language.id', 'language.label'], repeatable: true },
materialType: { operator: 'match', field: ['materialType.id', 'materialType.label'], repeatable: true },
mediaType: { operator: 'match', field: ['mediaType.id', 'mediaType.label'], repeatable: true },
carrierType: { operator: 'match', field: ['carrierType.id', 'carrierType.label'], repeatable: true },
publisher: { operator: 'match', field: ['publisherLiteral.raw'], repeatable: true },
contributorLiteral: { operator: 'match', field: ['contributorLiteral.raw', 'parallelContributor.raw'], repeatable: true },
creatorLiteral: { operator: 'match', field: ['creatorLiteral.raw', 'parallelCreatorLiteral.raw'], repeatable: true },
issuance: { operator: 'match', field: ['issuance.id', 'issuance.label'], repeatable: true },
createdYear: { operator: 'match', field: ['createdYear'], repeatable: true },
dateAfter: {
operator: 'custom',
type: 'int'
Expand Down
69 changes: 36 additions & 33 deletions lib/elasticsearch/elastic-query-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,40 @@ class ElasticQueryBuilder {
}
}

buildMultiFieldClause (value, fields) {
return {
bool:
{ should: fields.map(field => ({ term: { [field]: value } })) }
}
}

// This builds a filter cause from the value:
buildFilterClause (value, fieldsToMatchOn) {
const filterMatchesOnMoreThanOneField = fieldsToMatchOn.length > 1
if (filterMatchesOnMoreThanOneField) {
return this.buildMultiFieldClause(value, fieldsToMatchOn)
} else {
const field = fieldsToMatchOn[0]
return { term: { [field]: value } }
}
}

buildMatchOperatorFilterQueries (filtersWithMatchOperators) {
return filtersWithMatchOperators.map((prop) => {
const config = FILTER_CONFIG[prop]
let value = this.request.params.filters[prop]

// If multiple values given, let's join them with 'should', causing it to operate as a boolean OR
// Note: using 'must' here makes it a boolean AND
const booleanOperator = 'should'

if (Array.isArray(value) && value.length === 1) value = value.shift()
const clause = (Array.isArray(value)) ? { bool: { [booleanOperator]: value.map((value) => this.buildFilterClause(value, config.field)) } } : this.buildFilterClause(value, config.field)

return { path: config.path, clause }
})
}

/**
* Examine request for user-filters. When found, add them to query.
*/
Expand All @@ -502,41 +536,10 @@ class ElasticQueryBuilder {
)

// Collect those filters that use a simple term match
const simpleMatchFilters = Object.keys(this.request.params.filters)
const filtersWithMatchOperators = Object.keys(this.request.params.filters)
.filter((k) => FILTER_CONFIG[k].operator === 'match')

filterClausesWithPaths = filterClausesWithPaths.concat(simpleMatchFilters.map((prop) => {
const config = FILTER_CONFIG[prop]

let value = this.request.params.filters[prop]

// This builds a filter cause from the value:
const buildClause = (value) => {
// If filtering on a packed field and value isn't a packed value:
if (config.operator === 'match' && value.indexOf('||') < 0 && config.field.match(/_packed$/)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed these references to config.operator === 'match' because we are passing in an array that is already filtered on that condition.

// Figure out the base property (e.g. 'owner')
const baseField = config.field.replace(/_packed$/, '')
// Allow supplied val to match against either id or value:
return {
bool: {
should: [
{ term: { [`${baseField}.id`]: value } },
{ term: { [`${baseField}.label`]: value } }
]
}
}
} else if (config.operator === 'match') return { term: { [config.field]: value } }
}

// If multiple values given, let's join them with 'should', causing it to operate as a boolean OR
// Note: using 'must' here makes it a boolean AND
const booleanOperator = 'should'
// If only one value given, don't wrap it in a useless bool:
if (Array.isArray(value) && value.length === 1) value = value.shift()
const clause = (Array.isArray(value)) ? { bool: { [booleanOperator]: value.map(buildClause) } } : buildClause(value)

return { path: config.path, clause }
}))
filterClausesWithPaths = filterClausesWithPaths.concat(this.buildMatchOperatorFilterQueries(filtersWithMatchOperators))

// Gather root (not nested) filters:
let filterClauses = filterClausesWithPaths
Expand Down
Empty file.
105 changes: 105 additions & 0 deletions test/elastic-query-builder.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,111 @@ const ElasticQueryBuilder = require('../lib/elasticsearch/elastic-query-builder'
const ApiRequest = require('../lib/api-request')

describe('ElasticQueryBuilder', () => {
describe('buildFilterClause', () => {
it('can handle multiple fields', () => {
expect(ElasticQueryBuilder.prototype.buildFilterClause('value', ['field', 'parallelField']))
.to.deep.equal({
bool:
{
should: [
{ term: { field: 'value' } },
{ term: { parallelField: 'value' } }]
}
})
})
it('can handle the simple case', () => {
expect(ElasticQueryBuilder.prototype.buildFilterClause('value', ['field']))
.to.deep.equal({ term: { field: 'value' } })
})
})
describe('buildMatchOperatorFilterQueries', () => {
const mockQueryBuilderFactory = (request) => ({
request,
buildMultiFieldClause: ElasticQueryBuilder.prototype.buildMultiFieldClause,
buildMatchOperatorFilterQueries: ElasticQueryBuilder.prototype.buildMatchOperatorFilterQueries,
buildFilterClause: ElasticQueryBuilder.prototype.buildFilterClause
})
it('can handle (multiple) single value, single match field filters, as arrays', () => {
const request = new ApiRequest({ filters: { buildingLocation: ['toast'], subjectLiteral: ['spaghetti'] } })
const mockQueryBuilder = mockQueryBuilderFactory(request)
const simpleMatchFilters = mockQueryBuilder.buildMatchOperatorFilterQueries(['buildingLocation', 'subjectLiteral'])
expect(simpleMatchFilters).to.deep.equal([
{
path: undefined,
clause: { term: { buildingLocationIds: 'toast' } }
},
{
path: undefined,
clause: { term: { subjectLiteral_exploded: 'spaghetti' } }
}
])
})
it('can handle (multiple) single value, single match field filters, strings', () => {
const request = new ApiRequest({ filters: { buildingLocation: 'toast', subjectLiteral: 'spaghetti' } })
const mockQueryBuilder = mockQueryBuilderFactory(request)
const simpleMatchFilters = mockQueryBuilder.buildMatchOperatorFilterQueries(['buildingLocation', 'subjectLiteral'])
expect(simpleMatchFilters).to.deep.equal([
{
path: undefined,
clause: { term: { buildingLocationIds: 'toast' } }
},
{
path: undefined,
clause: { term: { subjectLiteral_exploded: 'spaghetti' } }
}
])
})
it('can handle multiple values', () => {
const request = new ApiRequest({ filters: { subjectLiteral: ['spaghetti', 'meatballs'] } })
const mockQueryBuilder = mockQueryBuilderFactory(request)
const simpleMatchFilters = mockQueryBuilder.buildMatchOperatorFilterQueries(['subjectLiteral'])
expect(simpleMatchFilters).to.deep.equal([
{
path: undefined,
clause: {
bool: {
should: [
{ term: { subjectLiteral_exploded: 'spaghetti' } },
{ term: { subjectLiteral_exploded: 'meatballs' } }
]
}
}
}
])
})
it('can handle packed values', () => {
const request = new ApiRequest({ filters: { language: ['spanish', 'finnish'] } })
const mockQueryBuilder = mockQueryBuilderFactory(request)
const simpleMatchFilters = mockQueryBuilder.buildMatchOperatorFilterQueries(['language'])
expect(simpleMatchFilters).to.deep.equal([
{
path: undefined,
clause: {
bool: {
should: [
{
bool: {
should: [
{ term: { 'language.id': 'spanish' } },
{ term: { 'language.label': 'spanish' } }
]
}
},
{
bool: {
should: [
{ term: { 'language.id': 'finnish' } },
{ term: { 'language.label': 'finnish' } }
]
}
}
]
}
}
}
])
})
})
describe('search_scope all', () => {
it('generates an "all" query', () => {
const request = new ApiRequest({ q: 'toast' })
Expand Down
Loading