Bug 32707: Unit tests
[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 => 63;
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('DisplayLibraryFacets','both');
258     $query = $qb->build_query();
259     ok( defined $query->{aggregations}{homebranch},
260         'homebranch added to facets if DisplayLibraryFacets=both' );
261     ok( defined $query->{aggregations}{holdingbranch},
262         'holdingbranch added to facets if DisplayLibraryFacets=both' );
263     t::lib::Mocks::mock_preference('DisplayLibraryFacets','holding');
264     $query = $qb->build_query();
265     ok( !defined $query->{aggregations}{homebranch},
266         'homebranch not added to facets if DisplayLibraryFacets=holding' );
267     ok( defined $query->{aggregations}{holdingbranch},
268         'holdingbranch added to facets if DisplayLibraryFacets=holding' );
269     t::lib::Mocks::mock_preference('DisplayLibraryFacets','home');
270     $query = $qb->build_query();
271     ok( defined $query->{aggregations}{homebranch},
272         'homebranch added to facets if DisplayLibraryFacets=home' );
273     ok( !defined $query->{aggregations}{holdingbranch},
274         'holdingbranch not added to facets if DisplayLibraryFacets=home' );
275
276     t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '' );
277
278     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
279     is(
280         $query->{query}{query_string}{query},
281         "(donald duck)",
282         "query not altered if QueryAutoTruncate disabled"
283     );
284
285     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['kw,phr'] );
286     is(
287         $query->{query}{query_string}{query},
288         '("donald duck")',
289         "keyword as phrase correctly quotes search term and strips index"
290     );
291
292     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
293     is(
294         $query->{query}{query_string}{query},
295         '(title:(donald duck))',
296         'multiple words in a query term are enclosed in parenthesis'
297     );
298
299     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
300     is(
301         $query->{query}{query_string}{query},
302         '(title:(donald duck)) AND (author:disney)',
303         'multiple query terms are enclosed in parenthesis while a single one is not'
304     );
305
306     my ($simple_query, $query_cgi, $query_desc);
307     ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['"donald duck"', 'walt disney'], ['ti', 'au'] );
308     is($query_cgi, 'idx=ti&q=%22donald%20duck%22&idx=au&q=walt%20disney', 'query cgi ok for multiterm query');
309     is($query_desc, '(title:("donald duck")) (author:(walt disney))', 'query desc ok for multiterm query');
310
311     ( undef, $query ) = $qb->build_query_compat( undef, ['2019'], ['yr,st-year'] );
312     is(
313         $query->{query}{query_string}{query},
314         '(date-of-publication:2019)',
315         'Year in an st-year search is handled properly'
316     );
317
318     ( undef, $query ) = $qb->build_query_compat( undef, ['2018-2019'], ['yr,st-year'] );
319     is(
320         $query->{query}{query_string}{query},
321         '(date-of-publication:[2018 TO 2019])',
322         'Year range in an st-year search is handled properly'
323     );
324
325     ( undef, $query ) = $qb->build_query_compat( undef, ['-2019'], ['yr,st-year'] );
326     is(
327         $query->{query}{query_string}{query},
328         '(date-of-publication:[* TO 2019])',
329         'Open start year in year range of an st-year search is handled properly'
330     );
331
332     ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'] );
333     is(
334         $query->{query}{query_string}{query},
335         '(date-of-publication:[2019 TO *])',
336         'Open end year in year range of an st-year search is handled properly'
337     );
338
339     ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'], ['yr,st-numeric=-2019'] );
340     is(
341         $query->{query}{query_string}{query},
342         '(date-of-publication:[2019 TO *]) AND date-of-publication:[* TO 2019]',
343         'Open end year in year range of an st-year search is handled properly'
344     );
345
346     ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'],
347         ['yr,st-numeric:-2019','yr,st-numeric:2005','yr,st-numeric:1984-2022'] );
348     is(
349         $query->{query}{query_string}{query},
350         '(date-of-publication:[2019 TO *]) AND (date-of-publication:[* TO 2019]) AND (date-of-publication:2005) AND (date-of-publication:[1984 TO 2022])',
351         'Limit on year search is handled properly when colon used'
352     );
353
354     # Enable auto-truncation
355     t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '1' );
356
357     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
358     is(
359         $query->{query}{query_string}{query},
360         "(donald* duck*)",
361         "simple query is auto truncated when QueryAutoTruncate enabled"
362     );
363
364     # Ensure reserved words are not truncated
365     ( undef, $query ) = $qb->build_query_compat( undef,
366         ['donald or duck and mickey not mouse'] );
367     is(
368         $query->{query}{query_string}{query},
369         "(donald* or duck* and mickey* not mouse*)",
370         "reserved words are not affected by QueryAutoTruncate"
371     );
372
373     ( undef, $query ) = $qb->build_query_compat( undef, ['donald* duck*'] );
374     is(
375         $query->{query}{query_string}{query},
376         "(donald* duck*)",
377         "query with '*' is unaltered when QueryAutoTruncate is enabled"
378     );
379
380     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck and the mouse'] );
381     is(
382         $query->{query}{query_string}{query},
383         "(donald* duck* and the* mouse*)",
384         "individual words are all truncated and stopwords ignored"
385     );
386
387     ( undef, $query ) = $qb->build_query_compat( undef, ['*'] );
388     is(
389         $query->{query}{query_string}{query},
390         "(*)",
391         "query of just '*' is unaltered when QueryAutoTruncate is enabled"
392     );
393
394     ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck"'], undef, ['available'] );
395     is(
396         $query->{query}{query_string}{query},
397         '("donald duck") AND available:true',
398         "query with quotes is unaltered when QueryAutoTruncate is enabled"
399     );
400
401
402     ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck" and "the mouse"'] );
403     is(
404         $query->{query}{query_string}{query},
405         '("donald duck" and "the mouse")',
406         "all quoted strings are unaltered if more than one in query"
407     );
408
409     # Reset ESPreventAutoTruncate syspref
410     t::lib::Mocks::mock_preference( 'ESPreventAutoTruncate', '' );
411
412     ( undef, $query ) = $qb->build_query_compat( undef, ['barcode:123456'] );
413     is(
414         $query->{query}{query_string}{query},
415         '(barcode:123456*)',
416         "query of specific field is truncated"
417     );
418
419     ( undef, $query ) = $qb->build_query_compat( undef, ['Personal-name:"donald"'] );
420     is(
421         $query->{query}{query_string}{query},
422         '(personal-name:"donald")',
423         "query of specific field including hyphen and quoted is not truncated, field name is converted to lower case"
424     );
425
426     ( undef, $query ) = $qb->build_query_compat( undef, ['Personal-name:donald'] );
427     is(
428         $query->{query}{query_string}{query},
429         '(personal-name:donald*)',
430         "query of specific field including hyphen and not quoted is truncated, field name is converted to lower case"
431     );
432
433     ( undef, $query ) = $qb->build_query_compat( undef, ['Personal-name.raw:donald'] );
434     is(
435         $query->{query}{query_string}{query},
436         '(personal-name.raw:donald*)',
437         "query of specific field including period and not quoted is truncated, field name is converted to lower case"
438     );
439
440     ( undef, $query ) = $qb->build_query_compat( undef, ['Personal-name.raw:"donald"'] );
441     is(
442         $query->{query}{query_string}{query},
443         '(personal-name.raw:"donald")',
444         "query of specific field including period and quoted is not truncated, field name is converted to lower case"
445     );
446
447     # Set ESPreventAutoTruncate syspref
448     t::lib::Mocks::mock_preference( 'ESPreventAutoTruncate', 'barcode' );
449
450     ( undef, $query ) = $qb->build_query_compat( undef, ['barcode:123456'] );
451     is(
452         $query->{query}{query_string}{query},
453         '(barcode:123456)',
454         "query of specific field excluded by ESPreventAutoTruncate is not truncated"
455     );
456
457     ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:123456'] );
458     is(
459         $query->{query}{query_string}{query},
460         '(local-number:123456)',
461         "query of identifier is not truncated even if QueryAutoTruncate is set"
462     );
463
464     ( undef, $query ) = $qb->build_query_compat( undef, ['onloan:true'] );
465     is(
466         $query->{query}{query_string}{query},
467         '(onloan:true)',
468         "query of boolean type field is not truncated even if QueryAutoTruncate is set"
469     );
470
471     ( undef, $query ) = $qb->build_query_compat( undef, ['J.R.R'] );
472     is(
473         $query->{query}{query_string}{query},
474         '(J.R.R*)',
475         "query including period is truncated but not split at periods"
476     );
477
478     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'] );
479     is(
480         $query->{query}{query_string}{query},
481         '(title:"donald duck")',
482         "query of specific field is not truncated when surrounded by quotes"
483     );
484
485     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
486     is(
487         $query->{query}{query_string}{query},
488         '(title:(donald* duck*))',
489         'words of a multi-word term are properly truncated'
490     );
491
492     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
493     is(
494         $query->{query}{query_string}{query},
495         '(title:(donald* duck*)) AND (author:disney*)',
496         'words of a multi-word term and single-word term are properly truncated'
497     );
498
499     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 1 } );
500     is(
501         $query->{query}{query_string}{query},
502         '(title:"donald duck") AND suppress:false',
503         "query of specific field is added AND suppress:false"
504     );
505
506     ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 0 } );
507     is(
508         $query->{query}{query_string}{query},
509         '(title:"donald duck")',
510         "query of specific field is not added AND suppress:0"
511     );
512
513     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['title:"donald duck"'], undef, ['author:Dillinger Escaplan'] );
514     is(
515         $query->{query}{query_string}{query},
516         '(title:"donald duck") AND author:("Dillinger Escaplan")',
517         "Simple query with limit term quoted in parentheses"
518     );
519
520     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['title:"donald duck"'], undef, ['author:Dillinger Escaplan', 'itype:BOOK'] );
521     is(
522         $query->{query}{query_string}{query},
523         '(title:"donald duck") AND (author:("Dillinger Escaplan")) AND (itype:("BOOK"))',
524         "Simple query with each limit's term quoted in parentheses"
525     );
526     is($query_cgi, 'idx=&q=title%3A%22donald%20duck%22', 'query cgi');
527     is($query_desc, 'title:"donald duck"', 'query desc ok');
528
529     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['title:"donald duck"'], undef, ['author:Dillinger Escaplan', 'mc-itype,phr:BOOK', 'mc-itype,phr:CD'] );
530     is(
531         $query->{query}{query_string}{query},
532         '(title:"donald duck") AND (author:("Dillinger Escaplan")) AND itype:(("BOOK") OR ("CD"))',
533         "Limits quoted correctly when passed as phrase"
534     );
535
536     ( undef, $query ) = $qb->build_query_compat( ['OR'], ['title:"donald duck"', 'author:"Dillinger Escaplan"'], undef, ['itype:BOOK'] );
537     is(
538         $query->{query}{query_string}{query},
539         '((title:"donald duck") OR (author:"Dillinger Escaplan")) AND itype:("BOOK")',
540         "OR query with limit"
541     );
542
543     ( undef, $query ) = $qb->build_query_compat( undef, undef, undef, ['itype:BOOK'] );
544     is(
545         $query->{query}{query_string}{query},
546         'itype:("BOOK")',
547         "Limit only"
548     );
549
550     # Scan queries
551     ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['new'], ['au'], undef, undef, 1 );
552     is(
553         $query->{query}{query_string}{query},
554         '*',
555         "scan query is properly formed"
556     );
557     is_deeply(
558         $query->{aggregations}{'author'}{'terms'},
559         {
560             field => 'author__facet',
561             order => { '_key' => 'asc' },
562             include => '[nN][eE][wW].*'
563         },
564         "scan aggregation request is properly formed"
565     );
566     is($query_cgi, 'idx=au&q=new&scan=1', 'query cgi');
567     is($query_desc, 'new', 'query desc ok');
568
569     ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['new'], [], undef, undef, 1 );
570     is(
571         $query->{query}{query_string}{query},
572         '*',
573         "scan query is properly formed"
574     );
575     is_deeply(
576         $query->{aggregations}{'subject'}{'terms'},
577         {
578             field => 'subject__facet',
579             order => { '_key' => 'asc' },
580             include => '[nN][eE][wW].*'
581         },
582         "scan aggregation request is properly formed"
583     );
584     is($query_cgi, 'idx=&q=new&scan=1', 'query cgi');
585     is($query_desc, 'new', 'query desc ok');
586
587     my( $limit, $limit_cgi, $limit_desc );
588     ( 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'] );
589     is( $limit, '(author:("Dillinger Escaplan")) AND itype:(("BOOK") OR ("CD"))', "Limit formed correctly when no search terms");
590     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");
591     is( $limit_desc,'(author:("Dillinger Escaplan")) AND itype:(("BOOK") OR ("CD"))',"Limit desc formed correctly when no search terms");
592 };
593
594
595 subtest 'build query from form subtests' => sub {
596     plan tests => 5;
597
598     my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
599     #when searching for authorities from a record the form returns marclist with blanks for unentered terms
600     my @marclist = ('mainmainentry','mainentry','match', 'all');
601     my @values   = ( undef,         'Hamilton',  undef,   undef);
602     my @operator = ( 'contains', 'contains', 'contains', 'contains');
603
604     my $query = $qb->build_authorities_query_compat( \@marclist, undef,
605                     undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
606     is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","Expected search is populated");
607     is( scalar @{ $query->{query}->{bool}->{must} }, 1,"Only defined search is populated");
608
609     @values[2] = 'Jefferson';
610     $query = $qb->build_authorities_query_compat( \@marclist, undef,
611                     undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
612     is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","First index searched as expected");
613     is($query->{query}->{bool}->{must}[1]->{query_string}->{query}, "Jefferson*","Second index searched when populated");
614     is( scalar @{ $query->{query}->{bool}->{must} }, 2,"Only defined searches are populated");
615
616
617 };
618
619 subtest 'build_query with weighted fields tests' => sub {
620     plan tests => 6;
621
622     $se->mock( '_load_elasticsearch_mappings', sub {
623         return {
624             authorities => {
625                 Heading => {
626                     label => 'heading',
627                     type => 'string',
628                     opac => 0,
629                     staff_client => 1,
630                     mappings => [{
631                         marc_field => '150',
632                         marc_type => 'marc21',
633                     }]
634                 },
635                 Headingmain => {
636                     label => 'headingmain',
637                     type => 'string',
638                     opac => 1,
639                     staff_client => 1,
640                     mappings => [{
641                         marc_field => '150',
642                         marc_type => 'marc21',
643                     }]
644                 }
645             },
646             biblios => {
647                 abstract => {
648                     label => 'abstract',
649                     type => 'string',
650                     opac => 1,
651                     staff_client => 0,
652                     mappings => [{
653                         marc_field => '520',
654                         marc_type => 'marc21',
655                     }]
656                 },
657                 acqdate => {
658                     label => 'acqdate',
659                     type => 'string',
660                     opac => 0,
661                     staff_client => 1,
662                     mappings => [{
663                         marc_field => '952d',
664                         marc_type => 'marc21',
665                         search => 0,
666                     }, {
667                         marc_field => '9955',
668                         marc_type => 'marc21',
669                         search => 0,
670                     }]
671                 },
672                 title => {
673                     label => 'title',
674                     type => 'string',
675                     opac => 0,
676                     staff_client => 1,
677                     mappings => [{
678                         marc_field => '130',
679                         marc_type => 'marc21'
680                     }]
681                 },
682                 subject => {
683                     label => 'subject',
684                     type => 'string',
685                     opac => 0,
686                     staff_client => 1,
687                     mappings => [{
688                         marc_field => '600a',
689                         marc_type => 'marc21'
690                     }]
691                 }
692             }
693         };
694     });
695
696     my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'biblios' } );
697     Koha::SearchFields->search({})->delete;
698     Koha::SearchEngine::Elasticsearch->reset_elasticsearch_mappings();
699
700     my $search_field;
701     $search_field = Koha::SearchFields->find({ name => 'title' });
702     $search_field->update({ weight => 25.0 });
703     $search_field = Koha::SearchFields->find({ name => 'subject' });
704     $search_field->update({ weight => 15.5 });
705     Koha::SearchEngine::Elasticsearch->clear_search_fields_cache();
706
707     my ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
708     undef, undef, undef, { weighted_fields => 1 });
709
710     my $fields = $query->{query}{query_string}{fields};
711
712     is(@{$fields}, 2, 'Search field with no searchable mappings has been excluded');
713
714     my @found = grep { $_ eq 'title^25.00' } @{$fields};
715     is(@found, 1, 'Search field title has correct weight');
716
717     @found = grep { $_ eq 'subject^15.50' } @{$fields};
718     is(@found, 1, 'Search field subject has correct weight');
719
720     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
721     undef, undef, undef, { weighted_fields => 1, is_opac => 1 });
722
723     $fields = $query->{query}{query_string}{fields};
724
725     is_deeply(
726         $fields,
727         ['abstract'],
728         'Only OPAC search fields are used when opac search is performed'
729     );
730
731     $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'authorities' } );
732     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
733     undef, undef, undef, { weighted_fields => 1 });
734     $fields = $query->{query}{query_string}{fields};
735     is_deeply( [sort @$fields], ['heading','headingmain'],'Authorities fields retrieve for authorities index');
736
737     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
738     undef, undef, undef, { weighted_fields => 1, is_opac => 1 });
739     $fields = $query->{query}{query_string}{fields};
740     is_deeply($fields,['headingmain'],'Only opac authorities fields retrieved for authorities index is is_opac');
741
742 };
743
744 subtest 'build_query_compat() SearchLimitLibrary tests' => sub {
745
746     plan tests => 18;
747
748     $schema->storage->txn_begin;
749
750     my $builder = t::lib::TestBuilder->new;
751
752     my $branch_1 = $builder->build_object({ class => 'Koha::Libraries' });
753     my $branch_2 = $builder->build_object({ class => 'Koha::Libraries' });
754     my $group    = $builder->build_object({ class => 'Koha::Library::Groups', value => {
755             ft_search_groups_opac => 1,
756             ft_search_groups_staff => 1,
757             parent_id => undef,
758             branchcode => undef
759         }
760     });
761     my $group_1  = $builder->build_object({ class => 'Koha::Library::Groups', value => {
762             parent_id => $group->id,
763             branchcode => $branch_1->id
764         }
765     });
766     my $group_2  = $builder->build_object({ class => 'Koha::Library::Groups', value => {
767             parent_id => $group->id,
768             branchcode => $branch_2->id
769         }
770     });
771     my $groupid = $group->id;
772     my @branchcodes = sort { $a cmp $b } ( $branch_1->id, $branch_2->id );
773
774
775     my $query_builder = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({index => $Koha::SearchEngine::BIBLIOS_INDEX});
776     t::lib::Mocks::mock_preference('SearchLimitLibrary', 'both');
777     my ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
778         $query_builder->build_query_compat( undef, undef, undef, [ "branch:CPL" ], undef, undef, undef, undef );
779     is( $limit, '(homebranch: "CPL" OR holdingbranch: "CPL")', "Branch limit expanded to home/holding branch");
780     is( $limit_desc, '(homebranch: "CPL" OR holdingbranch: "CPL")', "Limit description correctly expanded");
781     is( $limit_cgi, '&limit=branch%3ACPL', "Limit cgi does not get expanded");
782     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
783         $query_builder->build_query_compat( undef, undef, undef, [ "multibranchlimit:$groupid" ], undef, undef, undef, undef );
784     is( $limit, "(homebranch: \"$branchcodes[0]\" OR homebranch: \"$branchcodes[1]\" OR holdingbranch: \"$branchcodes[0]\" OR holdingbranch: \"$branchcodes[1]\")", "Multibranch limit expanded to home/holding branches");
785     is( $limit_desc, "(homebranch: \"$branchcodes[0]\" OR homebranch: \"$branchcodes[1]\" OR holdingbranch: \"$branchcodes[0]\" OR holdingbranch: \"$branchcodes[1]\")", "Multibranch limit description correctly expanded");
786     is( $limit_cgi, "&limit=multibranchlimit%3A$groupid", "Multibranch limit cgi does not get expanded");
787
788     t::lib::Mocks::mock_preference('SearchLimitLibrary', 'homebranch');
789     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
790         $query_builder->build_query_compat( undef, undef, undef, [ "branch:CPL" ], undef, undef, undef, undef );
791     is( $limit, "(homebranch: \"CPL\")", "branch limit expanded to home branch");
792     is( $limit_desc, "(homebranch: \"CPL\")", "limit description correctly expanded");
793     is( $limit_cgi, "&limit=branch%3ACPL", "limit cgi does not get expanded");
794     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
795         $query_builder->build_query_compat( undef, undef, undef, [ "multibranchlimit:$groupid" ], undef, undef, undef, undef );
796     is( $limit, "(homebranch: \"$branchcodes[0]\" OR homebranch: \"$branchcodes[1]\")", "branch limit expanded to home branch");
797     is( $limit_desc, "(homebranch: \"$branchcodes[0]\" OR homebranch: \"$branchcodes[1]\")", "limit description correctly expanded");
798     is( $limit_cgi, "&limit=multibranchlimit%3A$groupid", "Limit cgi does not get expanded");
799
800     t::lib::Mocks::mock_preference('SearchLimitLibrary', 'holdingbranch');
801     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
802         $query_builder->build_query_compat( undef, undef, undef, [ "branch:CPL" ], undef, undef, undef, undef );
803     is( $limit, "(holdingbranch: \"CPL\")", "branch limit expanded to holding branch");
804     is( $limit_desc, "(holdingbranch: \"CPL\")", "Limit description correctly expanded");
805     is( $limit_cgi, "&limit=branch%3ACPL", "Limit cgi does not get expanded");
806     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc, undef ) =
807         $query_builder->build_query_compat( undef, undef, undef, [ "multibranchlimit:$groupid" ], undef, undef, undef, undef );
808     is( $limit, "(holdingbranch: \"$branchcodes[0]\" OR holdingbranch: \"$branchcodes[1]\")", "branch limit expanded to holding branch");
809     is( $limit_desc, "(holdingbranch: \"$branchcodes[0]\" OR holdingbranch: \"$branchcodes[1]\")", "Limit description correctly expanded");
810     is( $limit_cgi, "&limit=multibranchlimit%3A$groupid", "Limit cgi does not get expanded");
811
812 };
813
814 subtest "Handle search filters" => sub {
815     plan tests => 7;
816
817     my $qb;
818
819     ok(
820         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
821         'Creating new query builder object for biblios'
822     );
823
824     my $filter = Koha::SearchFilter->new({
825         name => "test",
826         query => q|{"operands":["cat","bat","rat"],"indexes":["kw","ti","au"],"operators":["AND","OR"]}|,
827         limits => q|{"limits":["mc-itype,phr:BK","mc-itype,phr:MU","available"]}|,
828     })->store;
829     my $filter_id = $filter->id;
830
831     my ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc ) = $qb->build_query_compat( undef, undef, undef, ["search_filter:$filter_id"] );
832
833     is( $limit,q{(available:true) AND ((cat) AND title:(bat) OR author:(rat)) AND itype:(("BK") OR ("MU"))},"Limit correctly formed");
834     is( $limit_cgi,"&limit=search_filter%3A$filter_id","CGI limit is not expanded");
835     is( $limit_desc,q{(available:true) AND ((cat) AND title:(bat) OR author:(rat)) AND itype:(("BK") OR ("MU"))},"Limit description is correctly expanded");
836
837     $filter = Koha::SearchFilter->new({
838         name => "test",
839         query => q|{"operands":["su:biography"],"indexes":[],"operators":[]}|,
840         limits => q|{"limits":[]}|,
841     })->store;
842     $filter_id = $filter->id;
843
844     ( undef, undef, undef, undef, undef, $limit, $limit_cgi, $limit_desc ) = $qb->build_query_compat( undef, undef, undef, ["search_filter:$filter_id"] );
845
846     is( $limit,q{(subject:biography)},"Limit correctly formed for ccl type query");
847     is( $limit_cgi,"&limit=search_filter%3A$filter_id","CGI limit is not expanded");
848     is( $limit_desc,q{(subject:biography)},"Limit description is correctly handled for ccl type query");
849
850 };
851
852 subtest "_convert_sort_fields() tests" => sub {
853     plan tests => 3;
854
855     my $qb;
856
857     ok(
858         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
859         'Creating new query builder object for biblios'
860     );
861
862     my @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_dsc ));
863     is_deeply(
864         \@sort_by,
865         [
866             { field => 'cn-sort', direction => 'asc' },
867             { field => 'author',  direction => 'desc' }
868         ],
869         'sort fields should have been split correctly'
870     );
871
872     # We could expect this to pass, but direction is undef instead of 'desc'
873     @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_desc ));
874     is_deeply(
875         \@sort_by,
876         [
877             { field => 'cn-sort', direction => 'asc' },
878             { field => 'author',  direction => 'desc' }
879         ],
880         'sort fields should have been split correctly'
881     );
882 };
883
884 subtest "_sort_field() tests" => sub {
885     plan tests => 5;
886
887     my $qb;
888
889     ok(
890         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
891         'Creating new query builder object for biblios'
892     );
893
894     my $f = $qb->_sort_field('title');
895     is(
896         $f,
897         'title__sort',
898         'title sort mapped correctly'
899     );
900
901     $f = $qb->_sort_field('subject');
902     is(
903         $f,
904         'subject.raw',
905         'subject sort mapped correctly'
906     );
907
908     $f = $qb->_sort_field('itemnumber');
909     is(
910         $f,
911         'itemnumber',
912         'itemnumber sort mapped correctly'
913     );
914
915     $f = $qb->_sort_field('sortablenumber');
916     is(
917         $f,
918         'sortablenumber__sort',
919         'sortablenumber sort mapped correctly'
920     );
921 };
922
923 $schema->storage->txn_rollback;