/* * Copyright (C) 2020 StApps * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import { SCConfigFile, SCSearchBooleanFilter, SCSearchFilter, SCSearchQuery, SCSearchSort, SCThingType } from '@openstapps/core'; import { expect } from 'chai'; import {configFile} from '../../../src/common'; import { ElasticsearchConfig, ESBooleanFilter, ESDucetSort, ESGeoDistanceFilter, ESGeoDistanceSort, ESTermFilter, ScriptSort } from '../../../src/storage/elasticsearch/common'; import {buildBooleanFilter, buildFilter, buildQuery, buildSort} from '../../../src/storage/elasticsearch/query'; describe('Query', function () { describe('buildBooleanFilter', function () { const booleanFilter: SCSearchBooleanFilter = { arguments: { operation: 'and', filters: [ { type: 'value', arguments: { field: 'type', value: SCThingType.Catalog } }, { type: 'value', arguments: { field: 'type', value: SCThingType.Building } } ] }, type: 'boolean' }; const booleanFilters: {[key: string]: SCSearchBooleanFilter} = { and: booleanFilter, or: {...booleanFilter, arguments: {...booleanFilter.arguments, operation: 'or'}}, not: {...booleanFilter, arguments: {...booleanFilter.arguments, operation: 'not'}}, }; const expectedEsFilters: Array = [ { term: { 'type.raw': 'catalog' } }, { term: { 'type.raw': 'building' } } ]; it('should create appropriate elasticsearch "and" filter argument', function () { const {must} = buildBooleanFilter(booleanFilters.and); expect(must).to.be.eql(expectedEsFilters); }); it('should create appropriate elasticsearch "or" filter argument', function () { const {should, minimum_should_match} = buildBooleanFilter(booleanFilters.or); expect(should).to.be.eql(expectedEsFilters); expect(minimum_should_match).to.be.equal(1); }); it('should create appropriate elasticsearch "not" filter argument', function () { const {must_not} = buildBooleanFilter(booleanFilters.not); expect(must_not).to.be.eql(expectedEsFilters); }); }); describe('buildQuery', function () { const params: SCSearchQuery = { query: 'mathematics', from: 30, size: 5, sort: [ { type: 'ducet', order: 'desc', arguments: { field: 'name' } }, { type: 'ducet', order: 'desc', arguments: { field: 'categories' } }, ], filter: { type: 'value', arguments: { field: 'type', value: SCThingType.AcademicEvent } } }; let esConfig: ElasticsearchConfig = { name: 'elasticsearch', version: '123', query: { minMatch: '75%', queryType: 'dis_max', matchBoosting: 1.3, fuzziness: 'AUTO', cutoffFrequency: 0.0, tieBreaker: 0, }, }; const query = { minMatch: '75%', queryType: 'dis_max', matchBoosting: 1.3, fuzziness: 'AUTO', cutoffFrequency: 0.0, tieBreaker: 0, } const config: SCConfigFile = { ...configFile }; beforeEach(function () { esConfig = { name: 'elasticsearch', version: '123' }; }); // TODO: check parts of received elasticsearch query for each test case it('should build query that includes sorting when query is undefined', function () { expect(buildQuery(params, config, esConfig)).to.be.an('object'); }); it('should build query that includes sorting when query type is query_string', function () { esConfig.query = {...query, queryType: 'query_string'}; expect(buildQuery(params, config, esConfig)).to.be.an('object'); }); it('should build query that includes sorting when query type is dis_max', function () { esConfig.query = {...query, queryType: 'dis_max'}; expect(buildQuery(params, config, esConfig)).to.be.an('object'); }); it('should build query that includes sorting when query type is dis_max', function () { esConfig.query = {...query, queryType: 'dis_max'}; expect(buildQuery(params, config, esConfig)).to.be.an('object'); }); it('should reject (throw an error) if provided query type is not supported', function () { // @ts-ignore esConfig.query = {...query, queryType: 'invalid_query_type'}; expect(() => buildQuery(params, config, esConfig)).to.throw('query type'); }); }); describe('buildFilter', function () { const searchFilters: {[key: string]: SCSearchFilter} = { value: { type: 'value', arguments: { field: 'type', value: SCThingType.Dish } }, availability: { type: 'availability', arguments: { time: '2017-01-30T12:05:00.000Z', fromField: 'offers.availabilityStarts', toField: 'offers.availabilityEnds' } }, distance: { type: 'distance', arguments: { distance: 1000, field: 'geo.point.coordinates', position: [50.123, 8.123], } }, boolean: { type: 'boolean', arguments: { operation: 'and', filters: [ { type: 'value', arguments: { field: 'type', value: SCThingType.Dish, } }, { type: 'availability', arguments: { fromField: 'offers.availabilityStarts', toField: 'offers.availabilityEnds' } } ] } }, }; it('should build value filter', function () { const filter = buildFilter(searchFilters.value); const expectedFilter: ESTermFilter = { term: { 'type.raw': SCThingType.Dish } }; expect(filter).to.be.eql(expectedFilter); }); it('should build availability filter', function () { const filter = buildFilter(searchFilters.availability); const expectedFilter: ESBooleanFilter = { bool: { should: [ { bool: { must: [ { range: { 'offers.availabilityStarts': { lte: '2017-01-30T12:05:00.000Z' } } }, { range: { 'offers.availabilityEnds': { gte: '2017-01-30T12:05:00.000Z' } } } ] } }, { bool: { must_not: [ { exists: { field: 'offers.availabilityStarts' } }, { exists: { field: 'offers.availabilityEnds' } } ] } } ] } }; expect(filter).to.be.eql(expectedFilter); }); it('should build distance filter', function () { const filter = buildFilter(searchFilters.distance); const expectedFilter: ESGeoDistanceFilter = { geo_distance: { distance: '1000m', 'geo.point.coordinates': { lat: 8.123, lon: 50.123 } } }; expect(filter).to.be.eql(expectedFilter); }); it('should build boolean filter', function () { const filter = buildFilter(searchFilters.boolean); const expectedFilter: ESBooleanFilter = { bool: { minimum_should_match: 0, must: [ { term: { 'type.raw': 'dish' } }, { bool: { should: [ { bool: { must: [ { range: { 'offers.availabilityStarts': { lte: 'now' } } }, { range: { 'offers.availabilityEnds': { gte: 'now' } } } ] } }, { bool: { must_not: [ { exists: { field: 'offers.availabilityStarts' } }, { exists: { field: 'offers.availabilityEnds' } } ] } } ] } } ], must_not: [], should: [] } } expect(filter).to.be.eql(expectedFilter); }); }); describe('buildSort', function () { const searchSCSearchSort: Array = [ { type: 'ducet', order: 'desc', arguments: { field: 'name' }, }, { type: 'distance', order: 'desc', arguments: { field: 'geo.point', position: [8.123, 50.123] }, }, { type: 'price', order: 'asc', arguments: { universityRole: 'student', field: 'offers.prices', } }, ]; let sorts: Array = []; const expectedSorts: {[key: string]: ESDucetSort | ESGeoDistanceSort | ScriptSort} = { ducet: { 'name.sort': 'desc' }, distance: { _geo_distance: { mode: 'avg', order: 'desc', unit: 'm', 'geo.point': { lat: 50.123, lon: 8.123 } } }, price: { _script: { order: 'asc', script: '\n // foo price sort script', type: 'number' } } }; before(function () { sorts = buildSort(searchSCSearchSort); }); it('should build ducet sort', function () { expect(sorts[0]).to.be.eql(expectedSorts.ducet); }); it('should build distance sort', function () { expect(sorts[1]).to.be.eql(expectedSorts.distance); }); it('should build price sort', function () { const priceSortNoScript = {...sorts[2], _script: {...(sorts[2] as ScriptSort)._script, script: (expectedSorts.price as ScriptSort)._script.script}} expect(priceSortNoScript).to.be.eql(expectedSorts.price); }); }); });