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 t
::lib
::Mocks
::mock_preference
('DisplayLibraryFacets','both');
248 $query = $qb->build_query('test', %options);
249 ok
( defined $query->{aggregations
}{ccode
}{terms
}{size
},'we need to ask for a size or we get only 5 facet' );
250 is
( $query->{aggregations
}{ccode
}{terms
}{size
}, 37,'we ask for the size as defined by the syspref FacetMaxCount');
251 is
( $query->{aggregations
}{homebranch
}{terms
}{size
}, 37,'we ask for the size as defined by the syspref FacetMaxCount for homebranch');
252 is
( $query->{aggregations
}{holdingbranch
}{terms
}{size
}, 37,'we ask for the size as defined by the syspref FacetMaxCount for holdingbranch');
254 t
::lib
::Mocks
::mock_preference
('DisplayLibraryFacets','both');
255 $query = $qb->build_query();
256 ok
( defined $query->{aggregations
}{homebranch
},
257 'homebranch added to facets if DisplayLibraryFacets=both' );
258 ok
( defined $query->{aggregations
}{holdingbranch
},
259 'holdingbranch added to facets if DisplayLibraryFacets=both' );
260 t
::lib
::Mocks
::mock_preference
('DisplayLibraryFacets','holding');
261 $query = $qb->build_query();
262 ok
( !defined $query->{aggregations
}{homebranch
},
263 'homebranch not added to facets if DisplayLibraryFacets=holding' );
264 ok
( defined $query->{aggregations
}{holdingbranch
},
265 'holdingbranch added to facets if DisplayLibraryFacets=holding' );
266 t
::lib
::Mocks
::mock_preference
('DisplayLibraryFacets','home');
267 $query = $qb->build_query();
268 ok
( defined $query->{aggregations
}{homebranch
},
269 'homebranch added to facets if DisplayLibraryFacets=home' );
270 ok
( !defined $query->{aggregations
}{holdingbranch
},
271 'holdingbranch not added to facets if DisplayLibraryFacets=home' );
273 t
::lib
::Mocks
::mock_preference
( 'QueryAutoTruncate', '' );
275 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
277 $query->{query
}{query_string
}{query
},
279 "query not altered if QueryAutoTruncate disabled"
282 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
284 $query->{query
}{query_string
}{query
},
285 '(title:(donald duck))',
286 'multiple words in a query term are enclosed in parenthesis'
289 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
291 $query->{query
}{query_string
}{query
},
292 '(title:(donald duck)) AND (author:disney)',
293 'multiple query terms are enclosed in parenthesis while a single one is not'
296 my ($simple_query, $query_cgi, $query_desc);
297 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['"donald duck"', 'walt disney'], ['ti', 'au'] );
298 is
($query_cgi, 'idx=ti&q=%22donald%20duck%22&idx=au&q=walt%20disney', 'query cgi ok for multiterm query');
299 is
($query_desc, '(title:("donald duck")) (author:(walt disney))', 'query desc ok for multiterm query');
301 ( undef, $query ) = $qb->build_query_compat( undef, ['2019'], ['yr,st-year'] );
303 $query->{query
}{query_string
}{query
},
304 '(date-of-publication:2019)',
305 'Year in an st-year search is handled properly'
308 ( undef, $query ) = $qb->build_query_compat( undef, ['2018-2019'], ['yr,st-year'] );
310 $query->{query
}{query_string
}{query
},
311 '(date-of-publication:[2018 TO 2019])',
312 'Year range in an st-year search is handled properly'
315 ( undef, $query ) = $qb->build_query_compat( undef, ['-2019'], ['yr,st-year'] );
317 $query->{query
}{query_string
}{query
},
318 '(date-of-publication:[* TO 2019])',
319 'Open start year in year range of an st-year search is handled properly'
322 ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'] );
324 $query->{query
}{query_string
}{query
},
325 '(date-of-publication:[2019 TO *])',
326 'Open end year in year range of an st-year search is handled properly'
329 ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'], ['yr,st-numeric=-2019'] );
331 $query->{query
}{query_string
}{query
},
332 '(date-of-publication:[2019 TO *]) AND copydate:[* TO 2019]',
333 'Open end year in year range of an st-year search is handled properly'
336 # Enable auto-truncation
337 t
::lib
::Mocks
::mock_preference
( 'QueryAutoTruncate', '1' );
339 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
341 $query->{query
}{query_string
}{query
},
343 "simple query is auto truncated when QueryAutoTruncate enabled"
346 # Ensure reserved words are not truncated
347 ( undef, $query ) = $qb->build_query_compat( undef,
348 ['donald or duck and mickey not mouse'] );
350 $query->{query
}{query_string
}{query
},
351 "(donald* or duck* and mickey* not mouse*)",
352 "reserved words are not affected by QueryAutoTruncate"
355 ( undef, $query ) = $qb->build_query_compat( undef, ['donald* duck*'] );
357 $query->{query
}{query_string
}{query
},
359 "query with '*' is unaltered when QueryAutoTruncate is enabled"
362 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck and the mouse'] );
364 $query->{query
}{query_string
}{query
},
365 "(donald* duck* and the* mouse*)",
366 "individual words are all truncated and stopwords ignored"
369 ( undef, $query ) = $qb->build_query_compat( undef, ['*'] );
371 $query->{query
}{query_string
}{query
},
373 "query of just '*' is unaltered when QueryAutoTruncate is enabled"
376 ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck"'], undef, ['available'] );
378 $query->{query
}{query_string
}{query
},
379 '("donald duck") AND onloan:false',
380 "query with quotes is unaltered when QueryAutoTruncate is enabled"
384 ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck" and "the mouse"'] );
386 $query->{query
}{query_string
}{query
},
387 '("donald duck" and "the mouse")',
388 "all quoted strings are unaltered if more than one in query"
391 ( undef, $query ) = $qb->build_query_compat( undef, ['barcode:123456'] );
393 $query->{query
}{query_string
}{query
},
395 "query of specific field is truncated"
398 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:"123456"'] );
400 $query->{query
}{query_string
}{query
},
401 '(local-number:"123456")',
402 "query of specific field including hyphen and quoted is not truncated, field name is converted to lower case"
405 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:123456'] );
407 $query->{query
}{query_string
}{query
},
408 '(local-number:123456*)',
409 "query of specific field including hyphen and not quoted is truncated, field name is converted to lower case"
412 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:123456'] );
414 $query->{query
}{query_string
}{query
},
415 '(local-number.raw:123456*)',
416 "query of specific field including period and not quoted is truncated, field name is converted to lower case"
419 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:"123456"'] );
421 $query->{query
}{query_string
}{query
},
422 '(local-number.raw:"123456")',
423 "query of specific field including period and quoted is not truncated, field name is converted to lower case"
426 ( undef, $query ) = $qb->build_query_compat( undef, ['J.R.R'] );
428 $query->{query
}{query_string
}{query
},
430 "query including period is truncated but not split at periods"
433 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'] );
435 $query->{query
}{query_string
}{query
},
436 '(title:"donald duck")',
437 "query of specific field is not truncated when surrounded by quotes"
440 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
442 $query->{query
}{query_string
}{query
},
443 '(title:(donald* duck*))',
444 'words of a multi-word term are properly truncated'
447 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
449 $query->{query
}{query_string
}{query
},
450 '(title:(donald* duck*)) AND (author:disney*)',
451 'words of a multi-word term and single-word term are properly truncated'
454 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress
=> 1 } );
456 $query->{query
}{query_string
}{query
},
457 '(title:"donald duck") AND suppress:false',
458 "query of specific field is added AND suppress:false"
461 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress
=> 0 } );
463 $query->{query
}{query_string
}{query
},
464 '(title:"donald duck")',
465 "query of specific field is not added AND suppress:0"
468 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['title:"donald duck"'], undef, ['author:Dillinger Escaplan'] );
470 $query->{query
}{query_string
}{query
},
471 '(title:"donald duck") AND author:("Dillinger Escaplan")',
472 "Simple query with limit term quoted in parentheses"
475 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['title:"donald duck"'], undef, ['author:Dillinger Escaplan', 'itype:BOOK'] );
477 $query->{query
}{query_string
}{query
},
478 '(title:"donald duck") AND (author:("Dillinger Escaplan")) AND (itype:("BOOK"))',
479 "Simple query with each limit's term quoted in parentheses"
481 is
($query_cgi, 'idx=&q=title%3A%22donald%20duck%22', 'query cgi');
482 is
($query_desc, 'title:"donald duck"', 'query desc ok');
484 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['title:"donald duck"'], undef, ['author:Dillinger Escaplan', 'mc-itype,phr:BOOK', 'mc-itype,phr:CD'] );
486 $query->{query
}{query_string
}{query
},
487 '(title:"donald duck") AND (author:("Dillinger Escaplan")) AND itype:(("BOOK") OR ("CD"))',
488 "Limits quoted correctly when passed as phrase"
492 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['new'], ['au'], undef, undef, 1 );
494 $query->{query
}{query_string
}{query
},
496 "scan query is properly formed"
499 $query->{aggregations
}{'author'}{'terms'},
501 field
=> 'author__facet',
502 order
=> { '_term' => 'asc' },
503 include
=> '[nN][eE][wW].*'
505 "scan aggregation request is properly formed"
507 is
($query_cgi, 'idx=au&q=new&scan=1', 'query cgi');
508 is
($query_desc, 'new', 'query desc ok');
510 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['new'], [], undef, undef, 1 );
512 $query->{query
}{query_string
}{query
},
514 "scan query is properly formed"
517 $query->{aggregations
}{'subject'}{'terms'},
519 field
=> 'subject__facet',
520 order
=> { '_term' => 'asc' },
521 include
=> '[nN][eE][wW].*'
523 "scan aggregation request is properly formed"
525 is
($query_cgi, 'idx=&q=new&scan=1', 'query cgi');
526 is
($query_desc, 'new', 'query desc ok');
530 subtest
'build query from form subtests' => sub {
533 my $qb = Koha
::SearchEngine
::Elasticsearch
::QueryBuilder
->new({ 'index' => 'authorities' }),
534 #when searching for authorities from a record the form returns marclist with blanks for unentered terms
535 my @marclist = ('mainmainentry','mainentry','match', 'all');
536 my @values = ( undef, 'Hamilton', undef, undef);
537 my @operator = ( 'contains', 'contains', 'contains', 'contains');
539 my $query = $qb->build_authorities_query_compat( \
@marclist, undef,
540 undef, \
@operator , \
@values, 'AUTH_TYPE', 'asc' );
541 is
($query->{query
}->{bool
}->{must
}[0]->{query_string
}->{query
}, "Hamilton*","Expected search is populated");
542 is
( scalar @
{ $query->{query
}->{bool
}->{must
} }, 1,"Only defined search is populated");
544 @values[2] = 'Jefferson';
545 $query = $qb->build_authorities_query_compat( \
@marclist, undef,
546 undef, \
@operator , \
@values, 'AUTH_TYPE', 'asc' );
547 is
($query->{query
}->{bool
}->{must
}[0]->{query_string
}->{query
}, "Hamilton*","First index searched as expected");
548 is
($query->{query
}->{bool
}->{must
}[1]->{query_string
}->{query
}, "Jefferson*","Second index searched when populated");
549 is
( scalar @
{ $query->{query
}->{bool
}->{must
} }, 2,"Only defined searches are populated");
554 subtest
'build_query with weighted fields tests' => sub {
557 $se->mock( '_load_elasticsearch_mappings', sub {
567 marc_type
=> 'marc21',
571 label
=> 'headingmain',
577 marc_type
=> 'marc21',
589 marc_type
=> 'marc21',
598 marc_field
=> '952d',
599 marc_type
=> 'marc21',
602 marc_field
=> '9955',
603 marc_type
=> 'marc21',
614 marc_type
=> 'marc21'
623 marc_field
=> '600a',
624 marc_type
=> 'marc21'
631 my $qb = Koha
::SearchEngine
::Elasticsearch
::QueryBuilder
->new( { index => 'biblios' } );
632 Koha
::SearchFields
->search({})->delete;
633 Koha
::SearchEngine
::Elasticsearch
->reset_elasticsearch_mappings();
636 $search_field = Koha
::SearchFields
->find({ name
=> 'title' });
637 $search_field->update({ weight
=> 25.0 });
638 $search_field = Koha
::SearchFields
->find({ name
=> 'subject' });
639 $search_field->update({ weight
=> 15.5 });
640 Koha
::SearchEngine
::Elasticsearch
->clear_search_fields_cache();
642 my ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
643 undef, undef, undef, { weighted_fields
=> 1 });
645 my $fields = $query->{query
}{query_string
}{fields
};
647 is
(@
{$fields}, 2, 'Search field with no searchable mappings has been excluded');
649 my @found = grep { $_ eq 'title^25.00' } @
{$fields};
650 is
(@found, 1, 'Search field title has correct weight');
652 @found = grep { $_ eq 'subject^15.50' } @
{$fields};
653 is
(@found, 1, 'Search field subject has correct weight');
655 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
656 undef, undef, undef, { weighted_fields
=> 1, is_opac
=> 1 });
658 $fields = $query->{query
}{query_string
}{fields
};
663 'Only OPAC search fields are used when opac search is performed'
666 $qb = Koha
::SearchEngine
::Elasticsearch
::QueryBuilder
->new( { index => 'authorities' } );
667 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
668 undef, undef, undef, { weighted_fields
=> 1 });
669 $fields = $query->{query
}{query_string
}{fields
};
670 is_deeply
( [sort @
$fields], ['heading','headingmain'],'Authorities fields retrieve for authorities index');
672 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
673 undef, undef, undef, { weighted_fields
=> 1, is_opac
=> 1 });
674 $fields = $query->{query
}{query_string
}{fields
};
675 is_deeply
($fields,['headingmain'],'Only opac authorities fields retrieved for authorities index is is_opac');
679 subtest
"_convert_sort_fields() tests" => sub {
685 $qb = Koha
::SearchEngine
::Elasticsearch
::QueryBuilder
->new({ 'index' => 'biblios' }),
686 'Creating new query builder object for biblios'
689 my @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_dsc ));
693 { field
=> 'local-classification', direction
=> 'asc' },
694 { field
=> 'author', direction
=> 'desc' }
696 'sort fields should have been split correctly'
699 # We could expect this to pass, but direction is undef instead of 'desc'
700 @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_desc ));
704 { field
=> 'local-classification', direction
=> 'asc' },
705 { field
=> 'author', direction
=> 'desc' }
707 'sort fields should have been split correctly'
711 subtest
"_sort_field() tests" => sub {
717 $qb = Koha
::SearchEngine
::Elasticsearch
::QueryBuilder
->new({ 'index' => 'biblios' }),
718 'Creating new query builder object for biblios'
721 my $f = $qb->_sort_field('title');
725 'title sort mapped correctly'
728 $f = $qb->_sort_field('subject');
732 'subject sort mapped correctly'
735 $f = $qb->_sort_field('itemnumber');
739 'itemnumber sort mapped correctly'
742 $f = $qb->_sort_field('sortablenumber');
745 'sortablenumber__sort',
746 'sortablenumber sort mapped correctly'
750 $schema->storage->txn_rollback;