3 # This file is part of Koha.
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
23 use t::lib::TestBuilder;
24 use Test::More tests => 6;
27 use Koha::SearchEngine::Elasticsearch::QueryBuilder;
29 my $schema = Koha::Database->new->schema;
30 $schema->storage->txn_begin;
32 my $se = Test::MockModule->new( 'Koha::SearchEngine::Elasticsearch' );
33 $se->mock( 'get_elasticsearch_mappings', sub {
56 sortablenumber__sort => {
68 $all_mappings{$self->index} = $mappings;
79 $self->sort_fields($sort_fields->{$self->index});
81 return $all_mappings{$self->index};
84 subtest 'build_authorities_query_compat() tests' => sub {
90 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
91 'Creating new query builder object for authorities'
94 my $koha_to_index_name = $Koha::SearchEngine::Elasticsearch::QueryBuilder::koha_to_index_name;
95 my $search_term = 'a';
96 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
97 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
98 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
99 is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
102 is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
105 is( $query->{query}->{bool}->{must}[0]->{query_string}->{analyze_wildcard}, JSON::true, 'Set analyze_wildcard true' );
108 $search_term = 'Donald Duck';
109 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
110 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
111 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
112 is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
113 "(Donald*) AND (Duck*)");
115 is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
116 "(Donald*) AND (Duck*)");
120 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
121 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['is'], [$search_term], 'AUTH_TYPE', 'asc' );
122 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
123 is( $query->{query}->{bool}->{must}[0]->{match_phrase}->{"_all.phrase"},
126 is( $query->{query}->{bool}->{must}[0]->{match_phrase}->{$koha_to_index_name->{$koha_name}.".phrase"},
131 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
132 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'asc' );
133 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
134 is( $query->{query}->{bool}->{must}[0]->{match_phrase_prefix}->{"_all.phrase"},
137 is( $query->{query}->{bool}->{must}[0]->{match_phrase_prefix}->{$koha_to_index_name->{$koha_name}.".phrase"},
143 my $query = $qb->build_authorities_query_compat( [ 'mainentry' ], undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingAsc' );
148 'heading__sort' => 'asc'
151 "ascending sort parameter properly formed"
153 $query = $qb->build_authorities_query_compat( [ 'mainentry' ], undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingDsc' );
158 'heading__sort' => 'desc'
161 "descending sort parameter properly formed"
165 $query = $qb->build_authorities_query_compat( [ 'mainentry' ], undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
167 $query->{query}->{bool}->{filter},
168 { term => { 'authtype' => 'auth_type' } },
169 "authorities type code is used as filter"
174 $qb->build_authorities_query_compat( [ 'tomas' ], undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
176 'Koha::Exceptions::WrongParameter',
177 'Exception thrown on invalid value in the marclist param';
180 subtest 'build_query tests' => sub {
186 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
187 'Creating new query builder object for biblios'
190 my @sort_by = 'title_asc';
191 my @sort_params = $qb->_convert_sort_fields(@sort_by);
193 $options{sort} = \@sort_params;
194 my $query = $qb->build_query('test', %options);
205 "sort parameter properly formed"
208 t::lib::Mocks::mock_preference('FacetMaxCount','37');
209 $query = $qb->build_query('test', %options);
210 ok( defined $query->{aggregations}{ccode}{terms}{size},'we need to ask for a size or we get only 5 facet' );
211 is( $query->{aggregations}{ccode}{terms}{size}, 37,'we ask for the size as defined by the syspref FacetMaxCount');
213 t::lib::Mocks::mock_preference('DisplayLibraryFacets','both');
214 $query = $qb->build_query();
215 ok( defined $query->{aggregations}{homebranch},
216 'homebranch added to facets if DisplayLibraryFacets=both' );
217 ok( defined $query->{aggregations}{holdingbranch},
218 'holdingbranch added to facets if DisplayLibraryFacets=both' );
219 t::lib::Mocks::mock_preference('DisplayLibraryFacets','holding');
220 $query = $qb->build_query();
221 ok( !defined $query->{aggregations}{homebranch},
222 'homebranch not added to facets if DisplayLibraryFacets=holding' );
223 ok( defined $query->{aggregations}{holdingbranch},
224 'holdingbranch added to facets if DisplayLibraryFacets=holding' );
225 t::lib::Mocks::mock_preference('DisplayLibraryFacets','home');
226 $query = $qb->build_query();
227 ok( defined $query->{aggregations}{homebranch},
228 'homebranch added to facets if DisplayLibraryFacets=home' );
229 ok( !defined $query->{aggregations}{holdingbranch},
230 'holdingbranch not added to facets if DisplayLibraryFacets=home' );
232 t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '' );
234 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
236 $query->{query}{query_string}{query},
238 "query not altered if QueryAutoTruncate disabled"
241 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
243 $query->{query}{query_string}{query},
244 '(title:(donald duck))',
245 'multiple words in a query term are enclosed in parenthesis'
248 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
250 $query->{query}{query_string}{query},
251 '(title:(donald duck)) AND (author:disney)',
252 'multiple query terms are enclosed in parenthesis while a single one is not'
255 my ($simple_query, $query_cgi, $query_desc);
256 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['"donald duck"', 'walt disney'], ['ti', 'au'] );
257 is($query_cgi, 'idx=ti&q=%22donald%20duck%22&idx=au&q=walt%20disney', 'query cgi ok for multiterm query');
258 is($query_desc, '(title:("donald duck")) (author:(walt disney))', 'query desc ok for multiterm query');
260 ( undef, $query ) = $qb->build_query_compat( undef, ['2019'], ['yr,st-year'] );
262 $query->{query}{query_string}{query},
263 '(date-of-publication:2019)',
264 'Year in an st-year search is handled properly'
267 ( undef, $query ) = $qb->build_query_compat( undef, ['2018-2019'], ['yr,st-year'] );
269 $query->{query}{query_string}{query},
270 '(date-of-publication:[2018 TO 2019])',
271 'Year range in an st-year search is handled properly'
274 ( undef, $query ) = $qb->build_query_compat( undef, ['-2019'], ['yr,st-year'] );
276 $query->{query}{query_string}{query},
277 '(date-of-publication:[* TO 2019])',
278 'Open start year in year range of an st-year search is handled properly'
281 ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'] );
283 $query->{query}{query_string}{query},
284 '(date-of-publication:[2019 TO *])',
285 'Open end year in year range of an st-year search is handled properly'
288 ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'], ['yr,st-numeric=-2019'] );
290 $query->{query}{query_string}{query},
291 '(date-of-publication:[2019 TO *]) AND copydate:[* TO 2019]',
292 'Open end year in year range of an st-year search is handled properly'
295 # Enable auto-truncation
296 t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '1' );
298 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
300 $query->{query}{query_string}{query},
302 "simple query is auto truncated when QueryAutoTruncate enabled"
305 # Ensure reserved words are not truncated
306 ( undef, $query ) = $qb->build_query_compat( undef,
307 ['donald or duck and mickey not mouse'] );
309 $query->{query}{query_string}{query},
310 "(donald* or duck* and mickey* not mouse*)",
311 "reserved words are not affected by QueryAutoTruncate"
314 ( undef, $query ) = $qb->build_query_compat( undef, ['donald* duck*'] );
316 $query->{query}{query_string}{query},
318 "query with '*' is unaltered when QueryAutoTruncate is enabled"
321 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck and the mouse'] );
323 $query->{query}{query_string}{query},
324 "(donald* duck* and the* mouse*)",
325 "individual words are all truncated and stopwords ignored"
328 ( undef, $query ) = $qb->build_query_compat( undef, ['*'] );
330 $query->{query}{query_string}{query},
332 "query of just '*' is unaltered when QueryAutoTruncate is enabled"
335 ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck"'] );
337 $query->{query}{query_string}{query},
339 "query with quotes is unaltered when QueryAutoTruncate is enabled"
343 ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck" and "the mouse"'] );
345 $query->{query}{query_string}{query},
346 '("donald duck" and "the mouse")',
347 "all quoted strings are unaltered if more than one in query"
350 ( undef, $query ) = $qb->build_query_compat( undef, ['barcode:123456'] );
352 $query->{query}{query_string}{query},
354 "query of specific field is truncated"
357 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:"123456"'] );
359 $query->{query}{query_string}{query},
360 '(local-number:"123456")',
361 "query of specific field including hyphen and quoted is not truncated, field name is converted to lower case"
364 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:123456'] );
366 $query->{query}{query_string}{query},
367 '(local-number:123456*)',
368 "query of specific field including hyphen and not quoted is truncated, field name is converted to lower case"
371 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:123456'] );
373 $query->{query}{query_string}{query},
374 '(local-number.raw:123456*)',
375 "query of specific field including period and not quoted is truncated, field name is converted to lower case"
378 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:"123456"'] );
380 $query->{query}{query_string}{query},
381 '(local-number.raw:"123456")',
382 "query of specific field including period and quoted is not truncated, field name is converted to lower case"
385 ( undef, $query ) = $qb->build_query_compat( undef, ['J.R.R'] );
387 $query->{query}{query_string}{query},
389 "query including period is truncated but not split at periods"
392 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'] );
394 $query->{query}{query_string}{query},
395 '(title:"donald duck")',
396 "query of specific field is not truncated when surrounded by quotes"
399 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
401 $query->{query}{query_string}{query},
402 '(title:(donald* duck*))',
403 'words of a multi-word term are properly truncated'
406 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
408 $query->{query}{query_string}{query},
409 '(title:(donald* duck*)) AND (author:disney*)',
410 'words of a multi-word term and single-word term are properly truncated'
413 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 1 } );
415 $query->{query}{query_string}{query},
416 '(title:"donald duck") AND suppress:0',
417 "query of specific field is added AND suppress:0"
420 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 0 } );
422 $query->{query}{query_string}{query},
423 '(title:"donald duck")',
424 "query of specific field is not added AND suppress:0"
426 is($query_cgi, 'idx=&q=title%3A%22donald%20duck%22', 'query cgi');
427 is($query_desc, 'title:"donald duck"', 'query desc ok');
431 subtest 'build query from form subtests' => sub {
434 my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
435 #when searching for authorities from a record the form returns marclist with blanks for unentered terms
436 my @marclist = ('mainmainentry','mainentry','match', 'all');
437 my @values = ( undef, 'Hamilton', undef, undef);
438 my @operator = ( 'contains', 'contains', 'contains', 'contains');
440 my $query = $qb->build_authorities_query_compat( \@marclist, undef,
441 undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
442 is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","Expected search is populated");
443 is( scalar @{ $query->{query}->{bool}->{must} }, 1,"Only defined search is populated");
445 @values[2] = 'Jefferson';
446 $query = $qb->build_authorities_query_compat( \@marclist, undef,
447 undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
448 is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","First index searched as expected");
449 is($query->{query}->{bool}->{must}[1]->{query_string}->{query}, "Jefferson*","Second index searched when populated");
450 is( scalar @{ $query->{query}->{bool}->{must} }, 2,"Only defined searches are populated");
455 subtest 'build_query with weighted fields tests' => sub {
458 my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'mydb' } );
459 my $db_builder = t::lib::TestBuilder->new();
461 Koha::SearchFields->search({})->delete;
464 source => 'SearchField',
473 source => 'SearchField',
482 source => 'SearchField',
490 my ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
491 undef, undef, undef, { weighted_fields => 1 });
493 my $fields = $query->{query}{query_string}{fields};
494 is(scalar(@$fields), 3, 'Search is done on 3 fields');
495 is($fields->[0], '_all', 'First search field is _all');
496 is($fields->[1], 'title^25.00', 'Second search field is title');
497 is($fields->[2], 'subject^15.00', 'Third search field is subject');
500 subtest "_convert_sort_fields() tests" => sub {
506 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
507 'Creating new query builder object for biblios'
510 my @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_dsc ));
514 { field => 'local-classification', direction => 'asc' },
515 { field => 'author', direction => 'desc' }
517 'sort fields should have been split correctly'
520 # We could expect this to pass, but direction is undef instead of 'desc'
521 @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_desc ));
525 { field => 'local-classification', direction => 'asc' },
526 { field => 'author', direction => 'desc' }
528 'sort fields should have been split correctly'
532 subtest "_sort_field() tests" => sub {
538 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
539 'Creating new query builder object for biblios'
542 my $f = $qb->_sort_field('title');
546 'title sort mapped correctly'
549 $f = $qb->_sort_field('subject');
553 'subject sort mapped correctly'
556 $f = $qb->_sort_field('itemnumber');
560 'itemnumber sort mapped correctly'
563 $f = $qb->_sort_field('sortablenumber');
566 'sortablenumber__sort',
567 'sortablenumber sort mapped correctly'
571 $schema->storage->txn_rollback;