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>.
24 use t::lib::TestBuilder;
25 use Test::More tests => 6;
27 use List::Util qw( all );
30 use Koha::SearchEngine::Elasticsearch::QueryBuilder;
32 my $schema = Koha::Database->new->schema;
33 $schema->storage->txn_begin;
35 my $se = Test::MockModule->new( 'Koha::SearchEngine::Elasticsearch' );
36 $se->mock( 'get_elasticsearch_mappings', sub {
54 'subject-heading-thesaurus' => {
64 sortablenumber__sort => {
82 'match-heading-see-from' => {
88 $all_mappings{$self->index} = $mappings;
99 $self->sort_fields($sort_fields->{$self->index});
101 return $all_mappings{$self->index};
104 subtest 'build_authorities_query_compat() tests' => sub {
111 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
112 'Creating new query builder object for authorities'
115 my $koha_to_index_name = $Koha::SearchEngine::Elasticsearch::QueryBuilder::koha_to_index_name;
116 my $search_term = 'a';
117 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
118 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
119 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
120 is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
123 is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
126 is( $query->{query}->{bool}->{must}[0]->{query_string}->{analyze_wildcard}, JSON::true, 'Set analyze_wildcard true' );
129 $search_term = 'Donald Duck';
130 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
131 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
132 is( $query->{query}->{bool}->{must}[0]->{query_string}->{query}, "(Donald*) AND (Duck*)" );
133 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
134 isa_ok( $query->{query}->{bool}->{must}[0]->{query_string}->{fields}, 'ARRAY')
136 is( $query->{query}->{bool}->{must}[0]->{query_string}->{default_field}, $koha_to_index_name->{$koha_name} );
140 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
141 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['is'], [$search_term], 'AUTH_TYPE', 'asc' );
142 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
144 $query->{query}->{bool}->{must}[0]->{multi_match}->{query},
147 my $all_matches = all { /\.ci_raw$/ }
148 @{$query->{query}->{bool}->{must}[0]->{multi_match}->{fields}};
149 ok( $all_matches, 'Correct fields parameter for "is" query in "any" or "all"' );
152 $query->{query}->{bool}->{must}[0]->{term}->{$koha_to_index_name->{$koha_name} . ".ci_raw"},
158 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
159 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'asc' );
160 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
161 my $all_matches = all { (%{$_->{prefix}})[0] =~ /\.ci_raw$/ && (%{$_->{prefix}})[1] eq "Donald Duck" }
162 @{$query->{query}->{bool}->{must}[0]->{bool}->{should}};
163 ok( $all_matches, "Correct multiple prefix query" );
165 is( $query->{query}->{bool}->{must}[0]->{prefix}->{$koha_to_index_name->{$koha_name} . ".ci_raw"}, "Donald Duck" );
170 my $query = $qb->build_authorities_query_compat( [ 'mainentry' ], undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingAsc' );
175 'heading__sort' => 'asc'
178 "ascending sort parameter properly formed"
180 $query = $qb->build_authorities_query_compat( [ 'mainentry' ], undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingDsc' );
185 'heading__sort' => 'desc'
188 "descending sort parameter properly formed"
192 $query = $qb->build_authorities_query_compat( [ 'mainentry' ], undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
194 $query->{query}->{bool}->{filter},
195 { term => { 'authtype.raw' => 'AUTH_TYPE' } },
196 "authorities type code is used as filter"
199 # Authorities marclist check
201 $query = $qb->build_authorities_query_compat( [ 'tomas','mainentry' ], undef, undef, ['contains'], [$search_term,$search_term], 'AUTH_TYPE', 'asc' )
203 qr/Unknown search field tomas/,
204 "Warning for unknown field in marclist";
206 $query->{query}->{bool}->{must}[0]->{query_string}->{default_field},
208 "If no mapping for marclist the index is passed through as defined"
211 $query->{query}->{bool}->{must}[1]->{query_string}{default_field},
213 "If mapping found for marclist the index is passed through converted"
218 subtest 'build_query tests' => sub {
224 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
225 'Creating new query builder object for biblios'
228 my @sort_by = 'title_asc';
229 my @sort_params = $qb->_convert_sort_fields(@sort_by);
231 $options{sort} = \@sort_params;
232 my $query = $qb->build_query('test', %options);
243 "sort parameter properly formed"
246 t::lib::Mocks::mock_preference('FacetMaxCount','37');
247 $query = $qb->build_query('test', %options);
248 ok( defined $query->{aggregations}{ccode}{terms}{size},'we need to ask for a size or we get only 5 facet' );
249 is( $query->{aggregations}{ccode}{terms}{size}, 37,'we ask for the size as defined by the syspref FacetMaxCount');
251 t::lib::Mocks::mock_preference('DisplayLibraryFacets','both');
252 $query = $qb->build_query();
253 ok( defined $query->{aggregations}{homebranch},
254 'homebranch added to facets if DisplayLibraryFacets=both' );
255 ok( defined $query->{aggregations}{holdingbranch},
256 'holdingbranch added to facets if DisplayLibraryFacets=both' );
257 t::lib::Mocks::mock_preference('DisplayLibraryFacets','holding');
258 $query = $qb->build_query();
259 ok( !defined $query->{aggregations}{homebranch},
260 'homebranch not added to facets if DisplayLibraryFacets=holding' );
261 ok( defined $query->{aggregations}{holdingbranch},
262 'holdingbranch added to facets if DisplayLibraryFacets=holding' );
263 t::lib::Mocks::mock_preference('DisplayLibraryFacets','home');
264 $query = $qb->build_query();
265 ok( defined $query->{aggregations}{homebranch},
266 'homebranch added to facets if DisplayLibraryFacets=home' );
267 ok( !defined $query->{aggregations}{holdingbranch},
268 'holdingbranch not added to facets if DisplayLibraryFacets=home' );
270 t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '' );
272 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
274 $query->{query}{query_string}{query},
276 "query not altered if QueryAutoTruncate disabled"
279 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
281 $query->{query}{query_string}{query},
282 '(title:(donald duck))',
283 'multiple words in a query term are enclosed in parenthesis'
286 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
288 $query->{query}{query_string}{query},
289 '(title:(donald duck)) AND (author:disney)',
290 'multiple query terms are enclosed in parenthesis while a single one is not'
293 my ($simple_query, $query_cgi, $query_desc);
294 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['"donald duck"', 'walt disney'], ['ti', 'au'] );
295 is($query_cgi, 'idx=ti&q=%22donald%20duck%22&idx=au&q=walt%20disney', 'query cgi ok for multiterm query');
296 is($query_desc, '(title:("donald duck")) (author:(walt disney))', 'query desc ok for multiterm query');
298 ( undef, $query ) = $qb->build_query_compat( undef, ['2019'], ['yr,st-year'] );
300 $query->{query}{query_string}{query},
301 '(date-of-publication:2019)',
302 'Year in an st-year search is handled properly'
305 ( undef, $query ) = $qb->build_query_compat( undef, ['2018-2019'], ['yr,st-year'] );
307 $query->{query}{query_string}{query},
308 '(date-of-publication:[2018 TO 2019])',
309 'Year range in an st-year search is handled properly'
312 ( undef, $query ) = $qb->build_query_compat( undef, ['-2019'], ['yr,st-year'] );
314 $query->{query}{query_string}{query},
315 '(date-of-publication:[* TO 2019])',
316 'Open start year in year range of an st-year search is handled properly'
319 ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'] );
321 $query->{query}{query_string}{query},
322 '(date-of-publication:[2019 TO *])',
323 'Open end year in year range of an st-year search is handled properly'
326 ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'], ['yr,st-numeric=-2019'] );
328 $query->{query}{query_string}{query},
329 '(date-of-publication:[2019 TO *]) AND copydate:[* TO 2019]',
330 'Open end year in year range of an st-year search is handled properly'
333 # Enable auto-truncation
334 t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '1' );
336 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
338 $query->{query}{query_string}{query},
340 "simple query is auto truncated when QueryAutoTruncate enabled"
343 # Ensure reserved words are not truncated
344 ( undef, $query ) = $qb->build_query_compat( undef,
345 ['donald or duck and mickey not mouse'] );
347 $query->{query}{query_string}{query},
348 "(donald* or duck* and mickey* not mouse*)",
349 "reserved words are not affected by QueryAutoTruncate"
352 ( undef, $query ) = $qb->build_query_compat( undef, ['donald* duck*'] );
354 $query->{query}{query_string}{query},
356 "query with '*' is unaltered when QueryAutoTruncate is enabled"
359 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck and the mouse'] );
361 $query->{query}{query_string}{query},
362 "(donald* duck* and the* mouse*)",
363 "individual words are all truncated and stopwords ignored"
366 ( undef, $query ) = $qb->build_query_compat( undef, ['*'] );
368 $query->{query}{query_string}{query},
370 "query of just '*' is unaltered when QueryAutoTruncate is enabled"
373 ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck"'], undef, ['available'] );
375 $query->{query}{query_string}{query},
376 '("donald duck") AND onloan:false',
377 "query with quotes is unaltered when QueryAutoTruncate is enabled"
381 ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck" and "the mouse"'] );
383 $query->{query}{query_string}{query},
384 '("donald duck" and "the mouse")',
385 "all quoted strings are unaltered if more than one in query"
388 ( undef, $query ) = $qb->build_query_compat( undef, ['barcode:123456'] );
390 $query->{query}{query_string}{query},
392 "query of specific field is truncated"
395 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:"123456"'] );
397 $query->{query}{query_string}{query},
398 '(local-number:"123456")',
399 "query of specific field including hyphen and quoted is not truncated, field name is converted to lower case"
402 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:123456'] );
404 $query->{query}{query_string}{query},
405 '(local-number:123456*)',
406 "query of specific field including hyphen and not quoted is truncated, field name is converted to lower case"
409 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:123456'] );
411 $query->{query}{query_string}{query},
412 '(local-number.raw:123456*)',
413 "query of specific field including period and not quoted is truncated, field name is converted to lower case"
416 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:"123456"'] );
418 $query->{query}{query_string}{query},
419 '(local-number.raw:"123456")',
420 "query of specific field including period and quoted is not truncated, field name is converted to lower case"
423 ( undef, $query ) = $qb->build_query_compat( undef, ['J.R.R'] );
425 $query->{query}{query_string}{query},
427 "query including period is truncated but not split at periods"
430 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'] );
432 $query->{query}{query_string}{query},
433 '(title:"donald duck")',
434 "query of specific field is not truncated when surrounded by quotes"
437 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
439 $query->{query}{query_string}{query},
440 '(title:(donald* duck*))',
441 'words of a multi-word term are properly truncated'
444 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
446 $query->{query}{query_string}{query},
447 '(title:(donald* duck*)) AND (author:disney*)',
448 'words of a multi-word term and single-word term are properly truncated'
451 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 1 } );
453 $query->{query}{query_string}{query},
454 '(title:"donald duck") AND suppress:false',
455 "query of specific field is added AND suppress:false"
458 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 0 } );
460 $query->{query}{query_string}{query},
461 '(title:"donald duck")',
462 "query of specific field is not added AND suppress:0"
465 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['title:"donald duck"'], undef, ['author:Dillinger Escaplan'] );
467 $query->{query}{query_string}{query},
468 '(title:"donald duck") AND author:("Dillinger Escaplan")',
469 "Simple query with limit term quoted in parentheses"
472 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['title:"donald duck"'], undef, ['author:Dillinger Escaplan', 'itype:BOOK'] );
474 $query->{query}{query_string}{query},
475 '(title:"donald duck") AND (author:("Dillinger Escaplan")) AND (itype:("BOOK"))',
476 "Simple query with each limit's term quoted in parentheses"
478 is($query_cgi, 'idx=&q=title%3A%22donald%20duck%22', 'query cgi');
479 is($query_desc, 'title:"donald duck"', 'query desc ok');
482 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['new'], ['au'], undef, undef, 1 );
484 $query->{query}{query_string}{query},
486 "scan query is properly formed"
489 $query->{aggregations}{'author'}{'terms'},
491 field => 'author__facet',
492 order => { '_term' => 'asc' },
493 include => '[nN][eE][wW].*'
495 "scan aggregation request is properly formed"
497 is($query_cgi, 'idx=au&q=new&scan=1', 'query cgi');
498 is($query_desc, 'new', 'query desc ok');
500 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['new'], [], undef, undef, 1 );
502 $query->{query}{query_string}{query},
504 "scan query is properly formed"
507 $query->{aggregations}{'subject'}{'terms'},
509 field => 'subject__facet',
510 order => { '_term' => 'asc' },
511 include => '[nN][eE][wW].*'
513 "scan aggregation request is properly formed"
515 is($query_cgi, 'idx=&q=new&scan=1', 'query cgi');
516 is($query_desc, 'new', 'query desc ok');
520 subtest 'build query from form subtests' => sub {
523 my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
524 #when searching for authorities from a record the form returns marclist with blanks for unentered terms
525 my @marclist = ('mainmainentry','mainentry','match', 'all');
526 my @values = ( undef, 'Hamilton', undef, undef);
527 my @operator = ( 'contains', 'contains', 'contains', 'contains');
529 my $query = $qb->build_authorities_query_compat( \@marclist, undef,
530 undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
531 is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","Expected search is populated");
532 is( scalar @{ $query->{query}->{bool}->{must} }, 1,"Only defined search is populated");
534 @values[2] = 'Jefferson';
535 $query = $qb->build_authorities_query_compat( \@marclist, undef,
536 undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
537 is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","First index searched as expected");
538 is($query->{query}->{bool}->{must}[1]->{query_string}->{query}, "Jefferson*","Second index searched when populated");
539 is( scalar @{ $query->{query}->{bool}->{must} }, 2,"Only defined searches are populated");
544 subtest 'build_query with weighted fields tests' => sub {
547 $se->mock( '_load_elasticsearch_mappings', sub {
557 marc_type => 'marc21',
561 label => 'headingmain',
567 marc_type => 'marc21',
579 marc_type => 'marc21',
588 marc_field => '952d',
589 marc_type => 'marc21',
592 marc_field => '9955',
593 marc_type => 'marc21',
604 marc_type => 'marc21'
613 marc_field => '600a',
614 marc_type => 'marc21'
621 my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'biblios' } );
622 Koha::SearchFields->search({})->delete;
623 Koha::SearchEngine::Elasticsearch->reset_elasticsearch_mappings();
626 $search_field = Koha::SearchFields->find({ name => 'title' });
627 $search_field->update({ weight => 25.0 });
628 $search_field = Koha::SearchFields->find({ name => 'subject' });
629 $search_field->update({ weight => 15.5 });
630 Koha::SearchEngine::Elasticsearch->clear_search_fields_cache();
632 my ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
633 undef, undef, undef, { weighted_fields => 1 });
635 my $fields = $query->{query}{query_string}{fields};
637 is(@{$fields}, 2, 'Search field with no searchable mappings has been excluded');
639 my @found = grep { $_ eq 'title^25.00' } @{$fields};
640 is(@found, 1, 'Search field title has correct weight');
642 @found = grep { $_ eq 'subject^15.50' } @{$fields};
643 is(@found, 1, 'Search field subject has correct weight');
645 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
646 undef, undef, undef, { weighted_fields => 1, is_opac => 1 });
648 $fields = $query->{query}{query_string}{fields};
653 'Only OPAC search fields are used when opac search is performed'
656 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'authorities' } );
657 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
658 undef, undef, undef, { weighted_fields => 1 });
659 $fields = $query->{query}{query_string}{fields};
660 is_deeply( [sort @$fields], ['heading','headingmain'],'Authorities fields retrieve for authorities index');
662 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
663 undef, undef, undef, { weighted_fields => 1, is_opac => 1 });
664 $fields = $query->{query}{query_string}{fields};
665 is_deeply($fields,['headingmain'],'Only opac authorities fields retrieved for authorities index is is_opac');
669 subtest "_convert_sort_fields() tests" => sub {
675 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
676 'Creating new query builder object for biblios'
679 my @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_dsc ));
683 { field => 'local-classification', direction => 'asc' },
684 { field => 'author', direction => 'desc' }
686 'sort fields should have been split correctly'
689 # We could expect this to pass, but direction is undef instead of 'desc'
690 @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_desc ));
694 { field => 'local-classification', direction => 'asc' },
695 { field => 'author', direction => 'desc' }
697 'sort fields should have been split correctly'
701 subtest "_sort_field() tests" => sub {
707 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
708 'Creating new query builder object for biblios'
711 my $f = $qb->_sort_field('title');
715 'title sort mapped correctly'
718 $f = $qb->_sort_field('subject');
722 'subject sort mapped correctly'
725 $f = $qb->_sort_field('itemnumber');
729 'itemnumber sort mapped correctly'
732 $f = $qb->_sort_field('sortablenumber');
735 'sortablenumber__sort',
736 'sortablenumber sort mapped correctly'
740 $schema->storage->txn_rollback;