Bug 35138: (follow-up) Mark DisplayLibraryFacets as zebra only
[koha.git] / t / db_dependent / Koha / SearchEngine / Elasticsearch / QueryBuilder.t
1 #!/usr/bin/perl
2 #
3 # This file is part of Koha.
4 #
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.
9 #
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.
14 #
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>.
17
18 use Modern::Perl;
19
20 use C4::Context;
21 use Test::Exception;
22 use Test::Warn;
23 use t::lib::Mocks;
24 use t::lib::TestBuilder;
25 use Test::More tests => 8;
26
27 use List::Util qw( all );
28
29 use Koha::Database;
30 use Koha::SearchEngine::Elasticsearch::QueryBuilder;
31 use Koha::SearchFilters;
32
33 my $schema = Koha::Database->new->schema;
34 $schema->storage->txn_begin;
35
36 my $se = Test::MockModule->new( 'Koha::SearchEngine::Elasticsearch' );
37 $se->mock( 'get_elasticsearch_mappings', sub {
38     my ($self) = @_;
39
40     my %all_mappings;
41
42     my $mappings = {
43         properties => {
44             title => {
45                 type => 'text'
46             },
47             title__sort => {
48                 type => 'text'
49             },
50             subject => {
51                 type => 'text',
52                 facet => 1
53             },
54             'subject-heading-thesaurus' => {
55                 type => 'text',
56                 facet => 1
57             },
58             'subject-heading-thesaurus-conventions' => {
59                 type => 'text'
60             },
61             itemnumber => {
62                 type => 'integer'
63             },
64             sortablenumber => {
65                 type => 'integer'
66             },
67             sortablenumber__sort => {
68                 type => 'integer'
69             },
70             heading => {
71                 type => 'text'
72             },
73             'heading-main' => {
74                 type => 'text'
75             },
76             heading__sort => {
77                 type => 'text'
78             },
79             match => {
80                 type => 'text'
81             },
82             'match-heading' => {
83                 type => 'text'
84             },
85             'match-heading-see-from' => {
86                 type => 'text'
87             },
88         }
89     };
90     $all_mappings{$self->index} = $mappings;
91
92     my $sort_fields = {
93         $self->index => {
94             title => 1,
95             subject => 0,
96             itemnumber => 0,
97             sortablenumber => 1,
98             mainentry => 1
99         }
100     };
101     $self->sort_fields($sort_fields->{$self->index});
102
103     return $all_mappings{$self->index};
104 });
105
106 subtest 'build_authorities_query_compat() tests' => sub {
107
108     plan tests => 72;
109
110     my $qb;
111
112     ok(
113         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
114         'Creating new query builder object for authorities'
115     );
116
117     my $koha_to_index_name = $Koha::SearchEngine::Elasticsearch::QueryBuilder::koha_to_index_name;
118     my $search_term = 'a';
119     foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
120         my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
121         if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
122             is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
123                 "a*");
124         } else {
125             is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
126                 "a*");
127         }
128         is( $query->{query}->{bool}->{must}[0]->{query_string}->{analyze_wildcard}, JSON::true, 'Set analyze_wildcard true' );
129         is( $query->{query}->{bool}->{must}[0]->{query_string}->{lenient}, JSON::true, 'Set lenient true' );
130     }
131
132     $search_term = 'Donald Duck';
133     foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
134         my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
135         is( $query->{query}->{bool}->{must}[0]->{query_string}->{query}, "(Donald*) AND (Duck*)" );
136         if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
137             isa_ok( $query->{query}->{bool}->{must}[0]->{query_string}->{fields}, 'ARRAY')
138         } else {
139             is( $query->{query}->{bool}->{must}[0]->{query_string}->{default_field}, $koha_to_index_name->{$koha_name} );
140         }
141     }
142
143     foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
144         my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['is'], [$search_term], 'AUTH_TYPE', 'asc' );
145         if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
146             is(
147                 $query->{query}->{bool}->{must}[0]->{multi_match}->{query},
148                 "Donald Duck"
149             );
150             my $all_matches = all { /\.ci_raw$/ }
151                 @{$query->{query}->{bool}->{must}[0]->{multi_match}->{fields}};
152             ok( $all_matches, 'Correct fields parameter for "is" query in "any" or "all"' );
153         } else {
154             is(
155                 $query->{query}->{bool}->{must}[0]->{term}->{$koha_to_index_name->{$koha_name} . ".ci_raw"},
156                 "Donald Duck"
157             );
158         }
159     }
160
161     foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
162         my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'asc' );
163         if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
164             my $all_matches = all { (%{$_->{prefix}})[0] =~ /\.ci_raw$/ && (%{$_->{prefix}})[1] eq "Donald Duck" }
165                 @{$query->{query}->{bool}->{must}[0]->{bool}->{should}};
166             ok( $all_matches, "Correct multiple prefix query" );
167         } else {
168             is( $query->{query}->{bool}->{must}[0]->{prefix}->{$koha_to_index_name->{$koha_name} . ".ci_raw"}, "Donald Duck" );
169         }
170     }
171
172     # Sorting
173     my $query = $qb->build_authorities_query_compat( [ 'mainentry' ],  undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingAsc' );
174     is_deeply(
175         $query->{sort},
176         [
177             {
178                 'heading__sort' => 'asc'
179             }
180         ],
181         "ascending sort parameter properly formed"
182     );
183     $query = $qb->build_authorities_query_compat( [ 'mainentry' ],  undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingDsc' );
184     is_deeply(
185         $query->{sort},
186         [
187             {
188                 'heading__sort' => 'desc'
189             }
190         ],
191         "descending sort parameter properly formed"
192     );
193
194     # Authorities type
195     $query = $qb->build_authorities_query_compat( [ 'mainentry' ],  undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
196     is_deeply(
197         $query->{query}->{bool}->{filter},
198         { term => { 'authtype.raw' => 'AUTH_TYPE' } },
199         "authorities type code is used as filter"
200     );
201
202     # Authorities marclist check
203     warning_like {
204         $query = $qb->build_authorities_query_compat( [ 'tomas','mainentry' ],  undef, undef, ['contains'], [$search_term,$search_term], 'AUTH_TYPE', 'asc' )
205     }
206     qr/Unknown search field tomas/,
207     "Warning for unknown field in marclist";
208     is_deeply(
209         $query->{query}->{bool}->{must}[0]->{query_string}->{default_field},
210         'tomas',
211         "If no mapping for marclist the index is passed through as defined"
212     );
213     is_deeply(
214         $query->{query}->{bool}->{must}[1]->{query_string}{default_field},
215         'heading',
216         "If mapping found for marclist the index is passed through converted"
217     );
218
219 };
220
221 subtest 'build_query tests' => sub {
222     plan tests => 57;
223
224     my $qb;
225
226     ok(
227         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
228         'Creating new query builder object for biblios'
229     );
230
231     my @sort_by = 'title_asc';
232     my @sort_params = $qb->_convert_sort_fields(@sort_by);
233     my %options;
234     $options{sort} = \@sort_params;
235     my $query = $qb->build_query('test', %options);
236
237     is_deeply(
238         $query->{sort},
239         [
240             {
241             'title__sort' => {
242                     'order' => 'asc'
243                 }
244             }
245         ],
246         "sort parameter properly formed"
247     );
248
249     t::lib::Mocks::mock_preference('FacetMaxCount','37');
250     t::lib::Mocks::mock_preference('DisplayLibraryFacets','both');
251     $query = $qb->build_query('test', %options);
252     ok( defined $query->{aggregations}{ccode}{terms}{size},'we need to ask for a size or we get only 5 facet' );
253     is( $query->{aggregations}{ccode}{terms}{size}, 37,'we ask for the size as defined by the syspref FacetMaxCount');
254     is( $query->{aggregations}{homebranch}{terms}{size}, 37,'we ask for the size as defined by the syspref FacetMaxCount for homebranch');
255     is( $query->{aggregations}{holdingbranch}{terms}{size}, 37,'we ask for the size as defined by the syspref FacetMaxCount for holdingbranch');
256
257     t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '' );
258
259     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
260     is(
261         $query->{query}{query_string}{query},
262         "(donald duck)",
263         "query not altered if QueryAutoTruncate disabled"
264     );
265
266     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['kw,phr'] );
267     is(
268         $query->{query}{query_string}{query},
269         '("donald duck")',
270         "keyword as phrase correctly quotes search term and strips index"
271     );
272
273     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
274     is(
275         $query->{query}{query_string}{query},
276         '(title:(donald duck))',
277         'multiple words in a query term are enclosed in parenthesis'
278     );
279
280     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
281     is(
282         $query->{query}{query_string}{query},
283         '(title:(donald duck)) AND (author:disney)',
284         'multiple query terms are enclosed in parenthesis while a single one is not'
285     );
286
287     my ($simple_query, $query_cgi, $query_desc);
288     ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['"donald duck"', 'walt disney'], ['ti', 'au'] );
289     is($query_cgi, 'idx=ti&q=%22donald%20duck%22&idx=au&q=walt%20disney', 'query cgi ok for multiterm query');
290     is($query_desc, '(title:("donald duck")) (author:(walt disney))', 'query desc ok for multiterm query');
291
292     ( undef, $query ) = $qb->build_query_compat( undef, ['2019'], ['yr,st-year'] );
293     is(
294         $query->{query}{query_string}{query},
295         '(date-of-publication:2019)',
296         'Year in an st-year search is handled properly'
297     );
298
299     ( undef, $query ) = $qb->build_query_compat( undef, ['2018-2019'], ['yr,st-year'] );
300     is(
301         $query->{query}{query_string}{query},
302         '(date-of-publication:[2018 TO 2019])',
303         'Year range in an st-year search is handled properly'
304     );
305
306     ( undef, $query ) = $qb->build_query_compat( undef, ['-2019'], ['yr,st-year'] );
307     is(
308         $query->{query}{query_string}{query},
309         '(date-of-publication:[* TO 2019])',
310         'Open start year in year range of an st-year search is handled properly'
311     );
312
313     ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'] );
314     is(
315         $query->{query}{query_string}{query},
316         '(date-of-publication:[2019 TO *])',
317         'Open end year in year range of an st-year search is handled properly'
318     );
319
320     ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'], ['yr,st-numeric=-2019'] );
321     is(
322         $query->{query}{query_string}{query},
323         '(date-of-publication:[2019 TO *]) AND date-of-publication:[* TO 2019]',
324         'Open end year in year range of an st-year search is handled properly'
325     );
326
327     ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'],
328         ['yr,st-numeric:-2019','yr,st-numeric:2005','yr,st-numeric:1984-2022'] );
329     is(
330         $query->{query}{query_string}{query},
331         '(date-of-publication:[2019 TO *]) AND (date-of-publication:[* TO 2019]) AND (date-of-publication:2005) AND (date-of-publication:[1984 TO 2022])',
332         'Limit on year search is handled properly when colon used'
333     );
334
335     # Enable auto-truncation
336     t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '1' );
337
338     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
339     is(
340         $query->{query}{query_string}{query},
341         "(donald* duck*)",
342         "simple query is auto truncated when QueryAutoTruncate enabled"
343     );
344
345     # Ensure reserved words are not truncated
346     ( undef, $query ) = $qb->build_query_compat( undef,
347         ['donald or duck and mickey not mouse'] );
348     is(
349         $query->{query}{query_string}{query},
350         "(donald* or duck* and mickey* not mouse*)",
351         "reserved words are not affected by QueryAutoTruncate"
352     );
353
354     ( undef, $query ) = $qb->build_query_compat( undef, ['donald* duck*'] );
355     is(
356         $query->{query}{query_string}{query},
357         "(donald* duck*)",
358         "query with '*' is unaltered when QueryAutoTruncate is enabled"
359     );
360
361     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck and the mouse'] );
362     is(
363         $query->{query}{query_string}{query},
364         "(donald* duck* and the* mouse*)",
365         "individual words are all truncated and stopwords ignored"
366     );
367
368     ( undef, $query ) = $qb->build_query_compat( undef, ['*'] );
369     is(
370         $query->{query}{query_string}{query},
371         "(*)",
372         "query of just '*' is unaltered when QueryAutoTruncate is enabled"
373     );
374
375     ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck"'], undef, ['available'] );
376     is(
377         $query->{query}{query_string}{query},
378         '("donald duck") AND available:true',
379         "query with quotes is unaltered when QueryAutoTruncate is enabled"
380     );
381
382
383     ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck" and "the mouse"'] );
384     is(
385         $query->{query}{query_string}{query},
386         '("donald duck" and "the mouse")',
387         "all quoted strings are unaltered if more than one in query"
388     );
389
390     # Reset ESPreventAutoTruncate syspref
391     t::lib::Mocks::mock_preference( 'ESPreventAutoTruncate', '' );
392
393     ( undef, $query ) = $qb->build_query_compat( undef, ['barcode:123456'] );
394     is(
395         $query->{query}{query_string}{query},
396         '(barcode:123456*)',
397         "query of specific field is truncated"
398     );
399
400     ( undef, $query ) = $qb->build_query_compat( undef, ['Personal-name:"donald"'] );
401     is(
402         $query->{query}{query_string}{query},
403         '(personal-name:"donald")',
404         "query of specific field including hyphen and quoted is not truncated, field name is converted to lower case"
405     );
406
407     ( undef, $query ) = $qb->build_query_compat( undef, ['Personal-name:donald'] );
408     is(
409         $query->{query}{query_string}{query},
410         '(personal-name:donald*)',
411         "query of specific field including hyphen and not quoted is truncated, field name is converted to lower case"
412     );
413
414     ( undef, $query ) = $qb->build_query_compat( undef, ['Personal-name.raw:donald'] );
415     is(
416         $query->{query}{query_string}{query},
417         '(personal-name.raw:donald*)',
418         "query of specific field including period and not quoted is truncated, field name is converted to lower case"
419     );
420
421     ( undef, $query ) = $qb->build_query_compat( undef, ['Personal-name.raw:"donald"'] );
422     is(
423         $query->{query}{query_string}{query},
424         '(personal-name.raw:"donald")',
425         "query of specific field including period and quoted is not truncated, field name is converted to lower case"
426     );
427
428     # Set ESPreventAutoTruncate syspref
429     t::lib::Mocks::mock_preference( 'ESPreventAutoTruncate', 'barcode' );
430
431     ( undef, $query ) = $qb->build_query_compat( undef, ['barcode:123456'] );
432     is(
433         $query->{query}{query_string}{query},
434         '(barcode:123456)',
435         "query of specific field excluded by ESPreventAutoTruncate is not truncated"
436     );
437
438     ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:123456'] );
439     is(
440         $query->{query}{query_string}{query},
441         '(local-number:123456)',
442         "query of identifier is not truncated even if QueryAutoTruncate is set"
443     );
444
445     ( undef, $query ) = $qb->build_query_compat( undef, ['onloan:true'] );
446     is(
447         $query->{query}{query_string}{query},
448         '(onloan:true)',
449         "query of boolean type field is not truncated even if QueryAutoTruncate is set"
450     );
451
452     ( undef, $query ) = $qb->build_query_compat( undef, ['J.R.R'] );
453     is(
454         $query->{query}{query_string}{query},
455         '(J.R.R*)',
456         "query including period is truncated but not split at periods"
457     );
458
459     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'] );
460     is(
461         $query->{query}{query_string}{query},
462         '(title:"donald duck")',
463         "query of specific field is not truncated when surrounded by quotes"
464     );
465
466     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
467     is(
468         $query->{query}{query_string}{query},
469         '(title:(donald* duck*))',
470         'words of a multi-word term are properly truncated'
471     );
472
473     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
474     is(
475         $query->{query}{query_string}{query},
476         '(title:(donald* duck*)) AND (author:disney*)',
477         'words of a multi-word term and single-word term are properly truncated'
478     );
479
480     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 1 } );
481     is(
482         $query->{query}{query_string}{query},
483         '(title:"donald duck") AND suppress:false',
484         "query of specific field is added AND suppress:false"
485     );
486
487     ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 0 } );
488     is(
489         $query->{query}{query_string}{query},
490         '(title:"donald duck")',
491         "query of specific field is not added AND suppress:0"
492     );
493
494     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['title:"donald duck"'], undef, ['author:Dillinger Escaplan'] );
495     is(
496         $query->{query}{query_string}{query},
497         '(title:"donald duck") AND author:("Dillinger Escaplan")',
498         "Simple query with limit term quoted in parentheses"
499     );
500
501     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['title:"donald duck"'], undef, ['author:Dillinger Escaplan', 'itype:BOOK'] );
502     is(
503         $query->{query}{query_string}{query},
504         '(title:"donald duck") AND (author:("Dillinger Escaplan")) AND (itype:("BOOK"))',
505         "Simple query with each limit's term quoted in parentheses"
506     );
507     is($query_cgi, 'idx=&q=title%3A%22donald%20duck%22', 'query cgi');
508     is($query_desc, 'title:"donald duck"', 'query desc ok');
509
510     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['title:"donald duck"'], undef, ['author:Dillinger Escaplan', 'mc-itype,phr:BOOK', 'mc-itype,phr:CD'] );
511     is(
512         $query->{query}{query_string}{query},
513         '(title:"donald duck") AND (author:("Dillinger Escaplan")) AND itype:(("BOOK") OR ("CD"))',
514         "Limits quoted correctly when passed as phrase"
515     );
516
517     ( undef, $query ) = $qb->build_query_compat( ['OR'], ['title:"donald duck"', 'author:"Dillinger Escaplan"'], undef, ['itype:BOOK'] );
518     is(
519         $query->{query}{query_string}{query},
520         '((title:"donald duck") OR (author:"Dillinger Escaplan")) AND itype:("BOOK")',
521         "OR query with limit"
522     );
523
524     ( undef, $query ) = $qb->build_query_compat( undef, undef, undef, ['itype:BOOK'] );
525     is(
526         $query->{query}{query_string}{query},
527         'itype:("BOOK")',
528         "Limit only"
529     );
530
531     # Scan queries
532     ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['new'], ['au'], undef, undef, 1 );
533     is(
534         $query->{query}{query_string}{query},
535         '*',
536         "scan query is properly formed"
537     );
538     is_deeply(
539         $query->{aggregations}{'author'}{'terms'},
540         {
541             field => 'author__facet',
542             order => { '_key' => 'asc' },
543             include => '[nN][eE][wW].*'
544         },
545         "scan aggregation request is properly formed"
546     );
547     is($query_cgi, 'idx=au&q=new&scan=1', 'query cgi');
548     is($query_desc, 'new', 'query desc ok');
549
550     ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['new'], [], undef, undef, 1 );
551     is(
552         $query->{query}{query_string}{query},
553         '*',
554         "scan query is properly formed"
555     );
556     is_deeply(
557         $query->{aggregations}{'subject'}{'terms'},
558         {
559             field => 'subject__facet',
560             order => { '_key' => 'asc' },
561             include => '[nN][eE][wW].*'
562         },
563         "scan aggregation request is properly formed"
564     );
565     is($query_cgi, 'idx=&q=new&scan=1', 'query cgi');
566     is($query_desc, 'new', 'query desc ok');
567
568     my( $limit, $limit_cgi, $limit_desc );
569     ( undef, $query, $simple_query, $query_cgi, $query_desc, $limit, $limit_cgi, $limit_desc ) = $qb->build_query_compat( ['AND'], ['kw:""'], undef, ['author:Dillinger Escaplan', 'mc-itype,phr:BOOK', 'mc-itype,phr:CD'] );
570     is( $limit, '(author:("Dillinger Escaplan")) AND itype:(("BOOK") OR ("CD"))', "Limit formed correctly when no search terms");
571     is( $limit_cgi,'&limit=author%3ADillinger%20Escaplan&limit=mc-itype%2Cphr%3ABOOK&limit=mc-itype%2Cphr%3ACD', "Limit CGI formed correctly when no search terms");
572     is( $limit_desc,'(author:("Dillinger Escaplan")) AND itype:(("BOOK") OR ("CD"))',"Limit desc formed correctly when no search terms");
573 };
574
575
576 subtest 'build query from form subtests' => sub {
577     plan tests => 5;
578
579     my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
580     #when searching for authorities from a record the form returns marclist with blanks for unentered terms
581     my @marclist = ('mainmainentry','mainentry','match', 'all');
582     my @values   = ( undef,         'Hamilton',  undef,   undef);
583     my @operator = ( 'contains', 'contains', 'contains', 'contains');
584
585     my $query = $qb->build_authorities_query_compat( \@marclist, undef,
586                     undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
587     is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","Expected search is populated");
588     is( scalar @{ $query->{query}->{bool}->{must} }, 1,"Only defined search is populated");
589
590     @values[2] = 'Jefferson';
591     $query = $qb->build_authorities_query_compat( \@marclist, undef,
592                     undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
593     is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","First index searched as expected");
594     is($query->{query}->{bool}->{must}[1]->{query_string}->{query}, "Jefferson*","Second index searched when populated");
595     is( scalar @{ $query->{query}->{bool}->{must} }, 2,"Only defined searches are populated");
596
597
598 };
599
600 subtest 'build_query with weighted fields tests' => sub {
601     plan tests => 6;
602
603     $se->mock( '_load_elasticsearch_mappings', sub {
604         return {
605             authorities => {
606                 Heading => {
607                     label => 'heading',
608                     type => 'string',
609                     opac => 0,
610                     staff_client => 1,
611                     mappings => [{
612                         marc_field => '150',
613                         marc_type => 'marc21',
614                     }]
615                 },
616                 Headingmain => {
617                     label => 'headingmain',
618                     type => 'string',
619                     opac => 1,
620                     staff_client => 1,
621                     mappings => [{
622                         marc_field => '150',
623                         marc_type => 'marc21',
624                     }]
625                 }
626             },
627             biblios => {
628                 abstract => {
629                     label => 'abstract',
630                     type => 'string',
631                     opac => 1,
632                     staff_client => 0,
633                     mappings => [{
634                         marc_field => '520',
635                         marc_type => 'marc21',
636                     }]
637                 },
638                 acqdate => {
639                     label => 'acqdate',
640                     type => 'string',
641                     opac => 0,
642                     staff_client => 1,
643                     mappings => [{
644                         marc_field => '952d',
645                         marc_type => 'marc21',
646                         search => 0,
647                     }, {
648                         marc_field => '9955',
649                         marc_type => 'marc21',
650                         search => 0,
651                     }]
652                 },
653                 title => {
654                     label => 'title',
655                     type => 'string',
656                     opac => 0,
657                     staff_client => 1,
658                     mappings => [{
659                         marc_field => '130',
660                         marc_type => 'marc21'
661                     }]
662                 },
663                 subject => {
664                     label => 'subject',
665                     type => 'string',
666                     opac => 0,
667                     staff_client => 1,
668                     mappings => [{
669                         marc_field => '600a',
670                         marc_type => 'marc21'
671                     }]
672                 }
673             }
674         };
675     });
676
677     my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'biblios' } );
678     Koha::SearchFields->search({})->delete;
679     Koha::SearchEngine::Elasticsearch->reset_elasticsearch_mappings();
680
681     my $search_field;
682     $search_field = Koha::SearchFields->find({ name => 'title' });
683     $search_field->update({ weight => 25.0 });
684     $search_field = Koha::SearchFields->find({ name => 'subject' });
685     $search_field->update({ weight => 15.5 });
686     Koha::SearchEngine::Elasticsearch->clear_search_fields_cache();
687
688     my ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
689     undef, undef, undef, { weighted_fields => 1 });
690
691     my $fields = $query->{query}{query_string}{fields};
692
693     is(@{$fields}, 2, 'Search field with no searchable mappings has been excluded');
694
695     my @found = grep { $_ eq 'title^25.00' } @{$fields};
696     is(@found, 1, 'Search field title has correct weight');
697
698     @found = grep { $_ eq 'subject^15.50' } @{$fields};
699     is(@found, 1, 'Search field subject has correct weight');
700
701     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
702     undef, undef, undef, { weighted_fields => 1, is_opac => 1 });
703
704     $fields = $query->{query}{query_string}{fields};
705
706     is_deeply(
707         $fields,
708         ['abstract'],
709         'Only OPAC search fields are used when opac search is performed'
710     );
711
712     $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'authorities' } );
713     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
714     undef, undef, undef, { weighted_fields => 1 });
715     $fields = $query->{query}{query_string}{fields};
716     is_deeply( [sort @$fields], ['heading','headingmain'],'Authorities fields retrieve for authorities index');
717
718     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
719     undef, undef, undef, { weighted_fields => 1, is_opac => 1 });
720     $fields = $query->{query}{query_string}{fields};
721     is_deeply($fields,['headingmain'],'Only opac authorities fields retrieved for authorities index is is_opac');
722
723 };
724
725 subtest 'build_query_compat() SearchLimitLibrary tests' => sub {
726
727     plan tests => 18;
728
729     $schema->storage->txn_begin;
730
731     my $builder = t::lib::TestBuilder->new;
732
733     my $branch_1 = $builder->build_object({ class => 'Koha::Libraries' });
734     my $branch_2 = $builder->build_object({ class => 'Koha::Libraries' });
735     my $group    = $builder->build_object({ class => 'Koha::Library::Groups', value => {
736             ft_search_groups_opac => 1,
737             ft_search_groups_staff => 1,
738             parent_id => undef,
739             branchcode => undef
740         }
741     });
742     my $group_1  = $builder->build_object({ class => 'Koha::Library::Groups', value => {
743             parent_id => $group->id,
744             branchcode => $branch_1->id
745         }
746     });
747     my $group_2  = $builder->build_object({ class => 'Koha::Library::Groups', value => {
748             parent_id => $group->id,
749             branchcode => $branch_2->id
750         }
751     });
752     my $groupid = $group->id;
753     my @branchcodes = sort { $a cmp $b } ( $branch_1->id, $branch_2->id );
754
755
756     my $query_builder = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
757     t::lib::Mocks::mock_preference('SearchLimitLibrary', 'both');
758     my ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
759         $query_builder->build_query_compat( undef, undef, undef, [ "branch:CPL" ], undef, undef, undef, undef );
760     is( $limit, '(homebranch: "CPL" OR holdingbranch: "CPL")', "Branch limit expanded to home/holding branch");
761     is( $limit_desc, '(homebranch: "CPL" OR holdingbranch: "CPL")', "Limit description correctly expanded");
762     is( $limit_cgi, '&limit=branch%3ACPL', "Limit cgi does not get expanded");
763     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
764         $query_builder->build_query_compat( undef, undef, undef, [ "multibranchlimit:$groupid" ], undef, undef, undef, undef );
765     is( $limit, "(homebranch: \"$branchcodes[0]\" OR homebranch: \"$branchcodes[1]\" OR holdingbranch: \"$branchcodes[0]\" OR holdingbranch: \"$branchcodes[1]\")", "Multibranch limit expanded to home/holding branches");
766     is( $limit_desc, "(homebranch: \"$branchcodes[0]\" OR homebranch: \"$branchcodes[1]\" OR holdingbranch: \"$branchcodes[0]\" OR holdingbranch: \"$branchcodes[1]\")", "Multibranch limit description correctly expanded");
767     is( $limit_cgi, "&limit=multibranchlimit%3A$groupid", "Multibranch limit cgi does not get expanded");
768
769     t::lib::Mocks::mock_preference('SearchLimitLibrary', 'homebranch');
770     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
771         $query_builder->build_query_compat( undef, undef, undef, [ "branch:CPL" ], undef, undef, undef, undef );
772     is( $limit, "(homebranch: \"CPL\")", "branch limit expanded to home branch");
773     is( $limit_desc, "(homebranch: \"CPL\")", "limit description correctly expanded");
774     is( $limit_cgi, "&limit=branch%3ACPL", "limit cgi does not get expanded");
775     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
776         $query_builder->build_query_compat( undef, undef, undef, [ "multibranchlimit:$groupid" ], undef, undef, undef, undef );
777     is( $limit, "(homebranch: \"$branchcodes[0]\" OR homebranch: \"$branchcodes[1]\")", "branch limit expanded to home branch");
778     is( $limit_desc, "(homebranch: \"$branchcodes[0]\" OR homebranch: \"$branchcodes[1]\")", "limit description correctly expanded");
779     is( $limit_cgi, "&limit=multibranchlimit%3A$groupid", "Limit cgi does not get expanded");
780
781     t::lib::Mocks::mock_preference('SearchLimitLibrary', 'holdingbranch');
782     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
783         $query_builder->build_query_compat( undef, undef, undef, [ "branch:CPL" ], undef, undef, undef, undef );
784     is( $limit, "(holdingbranch: \"CPL\")", "branch limit expanded to holding branch");
785     is( $limit_desc, "(holdingbranch: \"CPL\")", "Limit description correctly expanded");
786     is( $limit_cgi, "&limit=branch%3ACPL", "Limit cgi does not get expanded");
787     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
788         $query_builder->build_query_compat( undef, undef, undef, [ "multibranchlimit:$groupid" ], undef, undef, undef, undef );
789     is( $limit, "(holdingbranch: \"$branchcodes[0]\" OR holdingbranch: \"$branchcodes[1]\")", "branch limit expanded to holding branch");
790     is( $limit_desc, "(holdingbranch: \"$branchcodes[0]\" OR holdingbranch: \"$branchcodes[1]\")", "Limit description correctly expanded");
791     is( $limit_cgi, "&limit=multibranchlimit%3A$groupid", "Limit cgi does not get expanded");
792
793 };
794
795 subtest "Handle search filters" => sub {
796     plan tests => 7;
797
798     my $qb;
799
800     ok(
801         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
802         'Creating new query builder object for biblios'
803     );
804
805     my $filter = Koha::SearchFilter->new({
806         name => "test",
807         query => q|{"operands":["cat","bat","rat"],"indexes":["kw","ti","au"],"operators":["AND","OR"]}|,
808         limits => q|{"limits":["mc-itype,phr:BK","mc-itype,phr:MU","available"]}|,
809     })->store;
810     my $filter_id = $filter->id;
811
812     my ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc ) = $qb->build_query_compat( undef, undef, undef, ["search_filter:$filter_id"] );
813
814     is( $limit,q{(available:true) AND ((cat) AND title:(bat) OR author:(rat)) AND itype:(("BK") OR ("MU"))},"Limit correctly formed");
815     is( $limit_cgi,"&limit=search_filter%3A$filter_id","CGI limit is not expanded");
816     is( $limit_desc,q{(available:true) AND ((cat) AND title:(bat) OR author:(rat)) AND itype:(("BK") OR ("MU"))},"Limit description is correctly expanded");
817
818     $filter = Koha::SearchFilter->new({
819         name => "test",
820         query => q|{"operands":["su:biography"],"indexes":[],"operators":[]}|,
821         limits => q|{"limits":[]}|,
822     })->store;
823     $filter_id = $filter->id;
824
825     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc ) = $qb->build_query_compat( undef, undef, undef, ["search_filter:$filter_id"] );
826
827     is( $limit,q{(subject:biography)},"Limit correctly formed for ccl type query");
828     is( $limit_cgi,"&limit=search_filter%3A$filter_id","CGI limit is not expanded");
829     is( $limit_desc,q{(subject:biography)},"Limit description is correctly handled for ccl type query");
830
831 };
832
833 subtest "_convert_sort_fields() tests" => sub {
834     plan tests => 3;
835
836     my $qb;
837
838     ok(
839         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
840         'Creating new query builder object for biblios'
841     );
842
843     my @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_dsc ));
844     is_deeply(
845         \@sort_by,
846         [
847             { field => 'cn-sort', direction => 'asc' },
848             { field => 'author',  direction => 'desc' }
849         ],
850         'sort fields should have been split correctly'
851     );
852
853     # We could expect this to pass, but direction is undef instead of 'desc'
854     @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_desc ));
855     is_deeply(
856         \@sort_by,
857         [
858             { field => 'cn-sort', direction => 'asc' },
859             { field => 'author',  direction => 'desc' }
860         ],
861         'sort fields should have been split correctly'
862     );
863 };
864
865 subtest "_sort_field() tests" => sub {
866     plan tests => 5;
867
868     my $qb;
869
870     ok(
871         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
872         'Creating new query builder object for biblios'
873     );
874
875     my $f = $qb->_sort_field('title');
876     is(
877         $f,
878         'title__sort',
879         'title sort mapped correctly'
880     );
881
882     $f = $qb->_sort_field('subject');
883     is(
884         $f,
885         'subject.raw',
886         'subject sort mapped correctly'
887     );
888
889     $f = $qb->_sort_field('itemnumber');
890     is(
891         $f,
892         'itemnumber',
893         'itemnumber sort mapped correctly'
894     );
895
896     $f = $qb->_sort_field('sortablenumber');
897     is(
898         $f,
899         'sortablenumber__sort',
900         'sortablenumber sort mapped correctly'
901     );
902 };
903
904 $schema->storage->txn_rollback;