Bug 20589: Remove expanded_facet variable and fix tests
[koha.git] / t / db_dependent / Koha / SearchEngine / Elasticsearch / QueryBuilder.t
blob5e695a07383623ef2a45517ab7b3be1b1405d4a9
1 #!/usr/bin/perl
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>.
18 use Modern::Perl;
20 use C4::Context;
21 use Test::Exception;
22 use t::lib::Mocks;
23 use t::lib::TestBuilder;
24 use Test::More tests => 6;
26 use List::Util qw( all );
28 use Koha::Database;
29 use Koha::SearchEngine::Elasticsearch::QueryBuilder;
31 my $schema = Koha::Database->new->schema;
32 $schema->storage->txn_begin;
34 my $se = Test::MockModule->new( 'Koha::SearchEngine::Elasticsearch' );
35 $se->mock( 'get_elasticsearch_mappings', sub {
36 my ($self) = @_;
38 my %all_mappings;
40 my $mappings = {
41 data => {
42 properties => {
43 title => {
44 type => 'text'
46 title__sort => {
47 type => 'text'
49 subject => {
50 type => 'text'
52 itemnumber => {
53 type => 'integer'
55 sortablenumber => {
56 type => 'integer'
58 sortablenumber__sort => {
59 type => 'integer'
61 Heading => {
62 type => 'text'
64 Heading__sort => {
65 type => 'text'
70 $all_mappings{$self->index} = $mappings;
72 my $sort_fields = {
73 $self->index => {
74 title => 1,
75 subject => 0,
76 itemnumber => 0,
77 sortablenumber => 1,
78 mainentry => 1
81 $self->sort_fields($sort_fields->{$self->index});
83 return $all_mappings{$self->index};
84 });
86 my $cache = Koha::Caches->get_instance();
87 my $clear_search_fields_cache = sub {
88 $cache->clear_from_cache('elasticsearch_search_fields_staff_client');
89 $cache->clear_from_cache('elasticsearch_search_fields_opac');
92 subtest 'build_authorities_query_compat() tests' => sub {
93 plan tests => 47;
95 my $qb;
97 ok(
98 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
99 'Creating new query builder object for authorities'
102 my $koha_to_index_name = $Koha::SearchEngine::Elasticsearch::QueryBuilder::koha_to_index_name;
103 my $search_term = 'a';
104 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
105 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
106 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
107 is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
108 "a*");
109 } else {
110 is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
111 "a*");
115 $search_term = 'Donald Duck';
116 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
117 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
118 is( $query->{query}->{bool}->{must}[0]->{query_string}->{query}, "(Donald*) AND (Duck*)" );
119 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
120 isa_ok( $query->{query}->{bool}->{must}[0]->{query_string}->{fields}, 'ARRAY')
121 } else {
122 is( $query->{query}->{bool}->{must}[0]->{query_string}->{default_field}, $koha_to_index_name->{$koha_name} );
126 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
127 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['is'], [$search_term], 'AUTH_TYPE', 'asc' );
128 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
130 $query->{query}->{bool}->{must}[0]->{multi_match}->{query},
131 "Donald Duck"
133 my $all_matches = all { /\.ci_raw$/ }
134 @{$query->{query}->{bool}->{must}[0]->{multi_match}->{fields}};
135 ok( $all_matches, 'Correct fields parameter for "is" query in "any" or "all"' );
136 } else {
138 $query->{query}->{bool}->{must}[0]->{term}->{$koha_to_index_name->{$koha_name} . ".ci_raw"},
139 "Donald Duck"
144 foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
145 my $query = $qb->build_authorities_query_compat( [ $koha_name ], undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'asc' );
146 if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
147 my $all_matches = all { (%{$_->{prefix}})[0] =~ /\.ci_raw$/ && (%{$_->{prefix}})[1] eq "Donald Duck" }
148 @{$query->{query}->{bool}->{must}[0]->{bool}->{should}};
149 ok( $all_matches, "Correct multiple prefix query" );
150 } else {
151 is( $query->{query}->{bool}->{must}[0]->{prefix}->{$koha_to_index_name->{$koha_name} . ".ci_raw"}, "Donald Duck" );
155 # Sorting
156 my $query = $qb->build_authorities_query_compat( [ 'mainentry' ], undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingAsc' );
157 is_deeply(
158 $query->{sort},
161 'heading__sort' => 'asc'
164 "ascending sort parameter properly formed"
166 $query = $qb->build_authorities_query_compat( [ 'mainentry' ], undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingDsc' );
167 is_deeply(
168 $query->{sort},
171 'heading__sort' => 'desc'
174 "descending sort parameter properly formed"
177 # Authorities type
178 $query = $qb->build_authorities_query_compat( [ 'mainentry' ], undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
179 is_deeply(
180 $query->{query}->{bool}->{filter},
181 { term => { 'authtype.raw' => 'AUTH_TYPE' } },
182 "authorities type code is used as filter"
185 # Failing case
186 throws_ok {
187 $qb->build_authorities_query_compat( [ 'tomas' ], undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
189 'Koha::Exceptions::WrongParameter',
190 'Exception thrown on invalid value in the marclist param';
193 subtest 'build_query tests' => sub {
194 plan tests => 40;
196 my $qb;
199 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
200 'Creating new query builder object for biblios'
203 my @sort_by = 'title_asc';
204 my @sort_params = $qb->_convert_sort_fields(@sort_by);
205 my %options;
206 $options{sort} = \@sort_params;
207 my $query = $qb->build_query('test', %options);
209 is_deeply(
210 $query->{sort},
213 'title__sort.phrase' => {
214 'order' => 'asc'
218 "sort parameter properly formed"
221 t::lib::Mocks::mock_preference('FacetMaxCount','37');
222 $query = $qb->build_query('test', %options);
223 ok( defined $query->{aggregations}{ccode}{terms}{size},'we need to ask for a size or we get only 5 facet' );
224 is( $query->{aggregations}{ccode}{terms}{size}, 37,'we ask for the size as defined by the syspref FacetMaxCount');
226 t::lib::Mocks::mock_preference('DisplayLibraryFacets','both');
227 $query = $qb->build_query();
228 ok( defined $query->{aggregations}{homebranch},
229 'homebranch added to facets if DisplayLibraryFacets=both' );
230 ok( defined $query->{aggregations}{holdingbranch},
231 'holdingbranch added to facets if DisplayLibraryFacets=both' );
232 t::lib::Mocks::mock_preference('DisplayLibraryFacets','holding');
233 $query = $qb->build_query();
234 ok( !defined $query->{aggregations}{homebranch},
235 'homebranch not added to facets if DisplayLibraryFacets=holding' );
236 ok( defined $query->{aggregations}{holdingbranch},
237 'holdingbranch added to facets if DisplayLibraryFacets=holding' );
238 t::lib::Mocks::mock_preference('DisplayLibraryFacets','home');
239 $query = $qb->build_query();
240 ok( defined $query->{aggregations}{homebranch},
241 'homebranch added to facets if DisplayLibraryFacets=home' );
242 ok( !defined $query->{aggregations}{holdingbranch},
243 'holdingbranch not added to facets if DisplayLibraryFacets=home' );
245 t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '' );
247 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
249 $query->{query}{query_string}{query},
250 "(donald duck)",
251 "query not altered if QueryAutoTruncate disabled"
254 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
256 $query->{query}{query_string}{query},
257 '(title:(donald duck))',
258 'multiple words in a query term are enclosed in parenthesis'
261 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
263 $query->{query}{query_string}{query},
264 '(title:(donald duck)) AND (author:disney)',
265 'multiple query terms are enclosed in parenthesis while a single one is not'
268 my ($simple_query, $query_cgi, $query_desc);
269 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['"donald duck"', 'walt disney'], ['ti', 'au'] );
270 is($query_cgi, 'idx=ti&q=%22donald%20duck%22&idx=au&q=walt%20disney', 'query cgi ok for multiterm query');
271 is($query_desc, '(title:("donald duck")) (author:(walt disney))', 'query desc ok for multiterm query');
273 ( undef, $query ) = $qb->build_query_compat( undef, ['2019'], ['yr,st-year'] );
275 $query->{query}{query_string}{query},
276 '(date-of-publication:2019)',
277 'Year in an st-year search is handled properly'
280 ( undef, $query ) = $qb->build_query_compat( undef, ['2018-2019'], ['yr,st-year'] );
282 $query->{query}{query_string}{query},
283 '(date-of-publication:[2018 TO 2019])',
284 'Year range in an st-year search is handled properly'
287 ( undef, $query ) = $qb->build_query_compat( undef, ['-2019'], ['yr,st-year'] );
289 $query->{query}{query_string}{query},
290 '(date-of-publication:[* TO 2019])',
291 'Open start year in year range of an st-year search is handled properly'
294 ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'] );
296 $query->{query}{query_string}{query},
297 '(date-of-publication:[2019 TO *])',
298 'Open end year in year range of an st-year search is handled properly'
301 ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'], ['yr,st-numeric=-2019'] );
303 $query->{query}{query_string}{query},
304 '(date-of-publication:[2019 TO *]) AND copydate:[* TO 2019]',
305 'Open end year in year range of an st-year search is handled properly'
308 # Enable auto-truncation
309 t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '1' );
311 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
313 $query->{query}{query_string}{query},
314 "(donald* duck*)",
315 "simple query is auto truncated when QueryAutoTruncate enabled"
318 # Ensure reserved words are not truncated
319 ( undef, $query ) = $qb->build_query_compat( undef,
320 ['donald or duck and mickey not mouse'] );
322 $query->{query}{query_string}{query},
323 "(donald* or duck* and mickey* not mouse*)",
324 "reserved words are not affected by QueryAutoTruncate"
327 ( undef, $query ) = $qb->build_query_compat( undef, ['donald* duck*'] );
329 $query->{query}{query_string}{query},
330 "(donald* duck*)",
331 "query with '*' is unaltered when QueryAutoTruncate is enabled"
334 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck and the mouse'] );
336 $query->{query}{query_string}{query},
337 "(donald* duck* and the* mouse*)",
338 "individual words are all truncated and stopwords ignored"
341 ( undef, $query ) = $qb->build_query_compat( undef, ['*'] );
343 $query->{query}{query_string}{query},
344 "(*)",
345 "query of just '*' is unaltered when QueryAutoTruncate is enabled"
348 ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck"'] );
350 $query->{query}{query_string}{query},
351 '("donald duck")',
352 "query with quotes is unaltered when QueryAutoTruncate is enabled"
356 ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck" and "the mouse"'] );
358 $query->{query}{query_string}{query},
359 '("donald duck" and "the mouse")',
360 "all quoted strings are unaltered if more than one in query"
363 ( undef, $query ) = $qb->build_query_compat( undef, ['barcode:123456'] );
365 $query->{query}{query_string}{query},
366 '(barcode:123456*)',
367 "query of specific field is truncated"
370 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:"123456"'] );
372 $query->{query}{query_string}{query},
373 '(local-number:"123456")',
374 "query of specific field including hyphen and quoted is not truncated, field name is converted to lower case"
377 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:123456'] );
379 $query->{query}{query_string}{query},
380 '(local-number:123456*)',
381 "query of specific field including hyphen and not quoted is truncated, field name is converted to lower case"
384 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:123456'] );
386 $query->{query}{query_string}{query},
387 '(local-number.raw:123456*)',
388 "query of specific field including period and not quoted is truncated, field name is converted to lower case"
391 ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:"123456"'] );
393 $query->{query}{query_string}{query},
394 '(local-number.raw:"123456")',
395 "query of specific field including period and quoted is not truncated, field name is converted to lower case"
398 ( undef, $query ) = $qb->build_query_compat( undef, ['J.R.R'] );
400 $query->{query}{query_string}{query},
401 '(J.R.R*)',
402 "query including period is truncated but not split at periods"
405 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'] );
407 $query->{query}{query_string}{query},
408 '(title:"donald duck")',
409 "query of specific field is not truncated when surrounded by quotes"
412 ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
414 $query->{query}{query_string}{query},
415 '(title:(donald* duck*))',
416 'words of a multi-word term are properly truncated'
419 ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
421 $query->{query}{query_string}{query},
422 '(title:(donald* duck*)) AND (author:disney*)',
423 'words of a multi-word term and single-word term are properly truncated'
426 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 1 } );
428 $query->{query}{query_string}{query},
429 '(title:"donald duck") AND suppress:0',
430 "query of specific field is added AND suppress:0"
433 ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 0 } );
435 $query->{query}{query_string}{query},
436 '(title:"donald duck")',
437 "query of specific field is not added AND suppress:0"
439 is($query_cgi, 'idx=&q=title%3A%22donald%20duck%22', 'query cgi');
440 is($query_desc, 'title:"donald duck"', 'query desc ok');
444 subtest 'build query from form subtests' => sub {
445 plan tests => 5;
447 my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
448 #when searching for authorities from a record the form returns marclist with blanks for unentered terms
449 my @marclist = ('mainmainentry','mainentry','match', 'all');
450 my @values = ( undef, 'Hamilton', undef, undef);
451 my @operator = ( 'contains', 'contains', 'contains', 'contains');
453 my $query = $qb->build_authorities_query_compat( \@marclist, undef,
454 undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
455 is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","Expected search is populated");
456 is( scalar @{ $query->{query}->{bool}->{must} }, 1,"Only defined search is populated");
458 @values[2] = 'Jefferson';
459 $query = $qb->build_authorities_query_compat( \@marclist, undef,
460 undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
461 is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","First index searched as expected");
462 is($query->{query}->{bool}->{must}[1]->{query_string}->{query}, "Jefferson*","Second index searched when populated");
463 is( scalar @{ $query->{query}->{bool}->{must} }, 2,"Only defined searches are populated");
468 subtest 'build_query with weighted fields tests' => sub {
469 plan tests => 4;
471 $se->mock( '_load_elasticsearch_mappings', sub {
472 return {
473 biblios => {
474 abstract => {
475 label => 'abstract',
476 type => 'string',
477 opac => 1,
478 staff_client => 0,
479 mappings => [{
480 marc_field => '520',
481 marc_type => 'marc21',
484 acqdate => {
485 label => 'acqdate',
486 type => 'string',
487 opac => 0,
488 staff_client => 1,
489 mappings => [{
490 marc_field => '952d',
491 marc_type => 'marc21',
492 search => 0,
493 }, {
494 marc_field => '9955',
495 marc_type => 'marc21',
496 search => 0,
499 title => {
500 label => 'title',
501 type => 'string',
502 opac => 0,
503 staff_client => 1,
504 mappings => [{
505 marc_field => '130',
506 marc_type => 'marc21'
509 subject => {
510 label => 'subject',
511 type => 'string',
512 opac => 0,
513 staff_client => 1,
514 mappings => [{
515 marc_field => '600a',
516 marc_type => 'marc21'
523 my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'biblios' } );
524 Koha::SearchFields->search({})->delete;
525 Koha::SearchEngine::Elasticsearch->reset_elasticsearch_mappings();
527 my $search_field;
528 $search_field = Koha::SearchFields->find({ name => 'title' });
529 $search_field->update({ weight => 25.0 });
530 $search_field = Koha::SearchFields->find({ name => 'subject' });
531 $search_field->update({ weight => 15.5 });
532 $clear_search_fields_cache->();
534 my ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
535 undef, undef, undef, { weighted_fields => 1 });
537 my $fields = $query->{query}{query_string}{fields};
539 is(@{$fields}, 2, 'Search field with no searchable mappings has been excluded');
541 my @found = grep { $_ eq 'title^25.00' } @{$fields};
542 is(@found, 1, 'Search field title has correct weight');
544 @found = grep { $_ eq 'subject^15.50' } @{$fields};
545 is(@found, 1, 'Search field subject has correct weight');
547 ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
548 undef, undef, undef, { weighted_fields => 1, is_opac => 1 });
550 $fields = $query->{query}{query_string}{fields};
552 is_deeply(
553 $fields,
554 ['abstract'],
555 'Only OPAC search fields are used when opac search is performed'
559 subtest "_convert_sort_fields() tests" => sub {
560 plan tests => 3;
562 my $qb;
565 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
566 'Creating new query builder object for biblios'
569 my @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_dsc ));
570 is_deeply(
571 \@sort_by,
573 { field => 'local-classification', direction => 'asc' },
574 { field => 'author', direction => 'desc' }
576 'sort fields should have been split correctly'
579 # We could expect this to pass, but direction is undef instead of 'desc'
580 @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_desc ));
581 is_deeply(
582 \@sort_by,
584 { field => 'local-classification', direction => 'asc' },
585 { field => 'author', direction => 'desc' }
587 'sort fields should have been split correctly'
591 subtest "_sort_field() tests" => sub {
592 plan tests => 5;
594 my $qb;
597 $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
598 'Creating new query builder object for biblios'
601 my $f = $qb->_sort_field('title');
604 'title__sort.phrase',
605 'title sort mapped correctly'
608 $f = $qb->_sort_field('subject');
611 'subject.raw',
612 'subject sort mapped correctly'
615 $f = $qb->_sort_field('itemnumber');
618 'itemnumber',
619 'itemnumber sort mapped correctly'
622 $f = $qb->_sort_field('sortablenumber');
625 'sortablenumber__sort',
626 'sortablenumber sort mapped correctly'
630 $schema->storage->txn_rollback;