diff --git a/lib/elasticsearch/config.js b/lib/elasticsearch/config.js index 0efe1a3..10fd164 100644 --- a/lib/elasticsearch/config.js +++ b/lib/elasticsearch/config.js @@ -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_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 }, dateAfter: { operator: 'custom', type: 'int' diff --git a/lib/elasticsearch/elastic-query-builder.js b/lib/elasticsearch/elastic-query-builder.js index db85bfa..802aa19 100644 --- a/lib/elasticsearch/elastic-query-builder.js +++ b/lib/elasticsearch/elastic-query-builder.js @@ -488,7 +488,7 @@ class ElasticQueryBuilder { } } - buildPackedFieldClause (field, value) { + buildPackedFieldClause (value, field) { // Figure out the base property (e.g. 'owner') const baseField = field.replace(/_packed$/, '') // Allow supplied val to match against either id or value: @@ -502,27 +502,38 @@ class ElasticQueryBuilder { } } + buildMultiFieldClause (value, fields) { + return { + bool: + { should: fields.map(field => ({ term: { [field]: value } })) } + } + } + // This builds a filter cause from the value: buildClause (value, field) { - // If filtering on a packed field and value isn't a packed value: - if (value.indexOf('||') < 0 && field.match(/_packed$/)) { - return this.buildPackedFieldClause(field, value) + const filterMatchesOnMoreThanOneField = field.length > 1 + if (filterMatchesOnMoreThanOneField) { + return this.buildMultiFieldClause(value, field) + } + field = field[0] + const valueIsNotPackedValue = value.indexOf('||') < 0 + const isPackedField = field.match(/_packed$/) + if (isPackedField && valueIsNotPackedValue) { + return this.buildPackedFieldClause(value, field) } else return { term: { [field]: value } } } buildSimpleMatchFilters (simpleMatchFilters) { return simpleMatchFilters.map((prop) => { const config = FILTER_CONFIG[prop] - const value = this.request.params.filters[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' - let clause - const singleValueArray = (Array.isArray(value) && value.length === 1) - if (singleValueArray) clause = this.buildClause(value[0], config.field) - else clause = { bool: { [booleanOperator]: value.map((value) => this.buildClause(value, config.field)) } } + if (Array.isArray(value) && value.length === 1) value = value.shift() + const clause = (Array.isArray(value)) ? { bool: { [booleanOperator]: value.map((value) => this.buildClause(config.field, value)) } } : this.buildClause(config.field, value) return { path: config.path, clause } }) diff --git a/lib/elasticsearch/elastic-query-filter-builder.js b/lib/elasticsearch/elastic-query-filter-builder.js new file mode 100644 index 0000000..e69de29 diff --git a/test/elastic-query-builder.test.js b/test/elastic-query-builder.test.js index 7e5984d..2f5ca13 100644 --- a/test/elastic-query-builder.test.js +++ b/test/elastic-query-builder.test.js @@ -4,9 +4,38 @@ const ElasticQueryBuilder = require('../lib/elasticsearch/elastic-query-builder' const ApiRequest = require('../lib/api-request') describe('ElasticQueryBuilder', () => { - describe.only('buildSimpleMatchFilters', () => { + describe('buildClause', () => { + it('can handle multiple fields', () => { + expect(ElasticQueryBuilder.prototype.buildClause('value', ['field', 'parallelField'])) + .to.deep.equal({ + bool: + { + should: [ + { term: { field: 'value' } }, + { term: { parallelField: 'value' } }] + } + }) + }) + it('can handle packed fields', () => { + expect(ElasticQueryBuilder.prototype.buildClause('not packed value', ['field_packed'])) + .to.deep.equal({ + bool: { + should: [ + { term: { 'field.id': 'not packed value' } }, + { term: { 'field.label': 'not packed value' } } + ] + } + }) + }) + it('can handle the simple case', () => { + expect(ElasticQueryBuilder.prototype.buildClause('value', ['field'])) + .to.deep.equal({ term: { field: 'value' } }) + }) + }) + describe('buildSimpleMatchFilters', () => { const mockQueryBuilderFactory = (request) => ({ request, + buildMultiFieldClause: ElasticQueryBuilder.prototype.buildMultiFieldClause, buildSimpleMatchFilters: ElasticQueryBuilder.prototype.buildSimpleMatchFilters, buildClause: ElasticQueryBuilder.prototype.buildClause, buildPackedFieldClause: ElasticQueryBuilder.prototype.buildPackedFieldClause @@ -26,25 +55,21 @@ describe('ElasticQueryBuilder', () => { } ]) }) - // it('can handle (multiple) single value, single match field filters, strings', () => { - // const request = new ApiRequest({ filters: { buildingLocation: 'toast', subjectLiteral: 'spaghetti' } }) - // const mockQueryBuilder = { - // request, - // buildSimpleMatchFilters: ElasticQueryBuilder.prototype.buildSimpleMatchFilters, - // buildClause: ElasticQueryBuilder.prototype.buildClause - // } - // const simpleMatchFilters = mockQueryBuilder.buildSimpleMatchFilters(['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.buildSimpleMatchFilters(['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)