Revert "Bug 23676: (RM follow-up) Test Correction"
[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 t::lib::Mocks;
23 use t::lib::TestBuilder;
24 use Test::More tests => 6;
25
26 use Koha::Database;
27 use Koha::SearchEngine::Elasticsearch::QueryBuilder;
28
29 my $schema = Koha::Database->new->schema;
30 $schema->storage->txn_begin;
31
32 my $se = Test::MockModule->new( 'Koha::SearchEngine::Elasticsearch' );
33 $se->mock( 'get_elasticsearch_mappings', sub {
34     my ($self) = @_;
35
36     my %all_mappings;
37
38     my $mappings = {
39         data => {
40             properties => {
41                 title => {
42                     type => 'text'
43                 },
44                 title__sort => {
45                     type => 'text'
46                 },
47                 subject => {
48                     type => 'text'
49                 },
50                 itemnumber => {
51                     type => 'integer'
52                 },
53                 sortablenumber => {
54                     type => 'integer'
55                 },
56                 sortablenumber__sort => {
57                     type => 'integer'
58                 },
59                 Heading => {
60                     type => 'text'
61                 },
62                 Heading__sort => {
63                     type => 'text'
64                 }
65             }
66         }
67     };
68     $all_mappings{$self->index} = $mappings;
69
70     my $sort_fields = {
71         $self->index => {
72             title => 1,
73             subject => 0,
74             itemnumber => 0,
75             sortablenumber => 1,
76             mainentry => 1
77         }
78     };
79     $self->sort_fields($sort_fields->{$self->index});
80
81     return $all_mappings{$self->index};
82 });
83
84 subtest 'build_authorities_query_compat() tests' => sub {
85     plan tests => 45;
86
87     my $qb;
88
89     ok(
90         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
91         'Creating new query builder object for authorities'
92     );
93
94     my $koha_to_index_name = $Koha::SearchEngine::Elasticsearch::QueryBuilder::koha_to_index_name;
95     my $search_term = 'a';
96     foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
97         my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
98         if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
99             is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
100                 "a*");
101         } else {
102             is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
103                 "a*");
104         }
105         is( $query->{query}->{bool}->{must}[0]->{query_string}->{analyze_wildcard}, JSON::true, 'Set analyze_wildcard true' );
106     }
107
108     $search_term = 'Donald Duck';
109     foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
110         my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
111         if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
112             is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
113                 "(Donald*) AND (Duck*)");
114         } else {
115             is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
116                 "(Donald*) AND (Duck*)");
117         }
118     }
119
120     foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
121         my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['is'], [$search_term], 'AUTH_TYPE', 'asc' );
122         if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
123             is( $query->{query}->{bool}->{must}[0]->{match_phrase}->{"_all.phrase"},
124                 "donald duck");
125         } else {
126             is( $query->{query}->{bool}->{must}[0]->{match_phrase}->{$koha_to_index_name->{$koha_name}.".phrase"},
127                 "donald duck");
128         }
129     }
130
131     foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
132         my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'asc' );
133         if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
134             is( $query->{query}->{bool}->{must}[0]->{match_phrase_prefix}->{"_all.phrase"},
135                 "donald duck");
136         } else {
137             is( $query->{query}->{bool}->{must}[0]->{match_phrase_prefix}->{$koha_to_index_name->{$koha_name}.".phrase"},
138                 "donald duck");
139         }
140     }
141
142     # Sorting
143     my $query = $qb->build_authorities_query_compat( [ 'mainentry' ],  undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingAsc' );
144     is_deeply(
145         $query->{sort},
146         [
147             {
148                 'heading__sort' => 'asc'
149             }
150         ],
151         "ascending sort parameter properly formed"
152     );
153     $query = $qb->build_authorities_query_compat( [ 'mainentry' ],  undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingDsc' );
154     is_deeply(
155         $query->{sort},
156         [
157             {
158                 'heading__sort' => 'desc'
159             }
160         ],
161         "descending sort parameter properly formed"
162     );
163
164     # Authorities type
165     $query = $qb->build_authorities_query_compat( [ 'mainentry' ],  undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
166     is_deeply(
167         $query->{query}->{bool}->{filter},
168         { term => { 'authtype.raw' => 'AUTH_TYPE' } },
169         "authorities type code is used as filter"
170     );
171
172     # Failing case
173     throws_ok {
174         $qb->build_authorities_query_compat( [ 'tomas' ],  undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
175     }
176     'Koha::Exceptions::WrongParameter',
177         'Exception thrown on invalid value in the marclist param';
178 };
179
180 subtest 'build_query tests' => sub {
181     plan tests => 40;
182
183     my $qb;
184
185     ok(
186         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
187         'Creating new query builder object for biblios'
188     );
189
190     my @sort_by = 'title_asc';
191     my @sort_params = $qb->_convert_sort_fields(@sort_by);
192     my %options;
193     $options{sort} = \@sort_params;
194     my $query = $qb->build_query('test', %options);
195
196     is_deeply(
197         $query->{sort},
198         [
199             {
200             'title__sort' => {
201                     'order' => 'asc'
202                 }
203             }
204         ],
205         "sort parameter properly formed"
206     );
207
208     t::lib::Mocks::mock_preference('FacetMaxCount','37');
209     $query = $qb->build_query('test', %options);
210     ok( defined $query->{aggregations}{ccode}{terms}{size},'we need to ask for a size or we get only 5 facet' );
211     is( $query->{aggregations}{ccode}{terms}{size}, 37,'we ask for the size as defined by the syspref FacetMaxCount');
212
213     t::lib::Mocks::mock_preference('DisplayLibraryFacets','both');
214     $query = $qb->build_query();
215     ok( defined $query->{aggregations}{homebranch},
216         'homebranch added to facets if DisplayLibraryFacets=both' );
217     ok( defined $query->{aggregations}{holdingbranch},
218         'holdingbranch added to facets if DisplayLibraryFacets=both' );
219     t::lib::Mocks::mock_preference('DisplayLibraryFacets','holding');
220     $query = $qb->build_query();
221     ok( !defined $query->{aggregations}{homebranch},
222         'homebranch not added to facets if DisplayLibraryFacets=holding' );
223     ok( defined $query->{aggregations}{holdingbranch},
224         'holdingbranch added to facets if DisplayLibraryFacets=holding' );
225     t::lib::Mocks::mock_preference('DisplayLibraryFacets','home');
226     $query = $qb->build_query();
227     ok( defined $query->{aggregations}{homebranch},
228         'homebranch added to facets if DisplayLibraryFacets=home' );
229     ok( !defined $query->{aggregations}{holdingbranch},
230         'holdingbranch not added to facets if DisplayLibraryFacets=home' );
231
232     t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '' );
233
234     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
235     is(
236         $query->{query}{query_string}{query},
237         "(donald duck)",
238         "query not altered if QueryAutoTruncate disabled"
239     );
240
241     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
242     is(
243         $query->{query}{query_string}{query},
244         '(title:(donald duck))',
245         'multiple words in a query term are enclosed in parenthesis'
246     );
247
248     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
249     is(
250         $query->{query}{query_string}{query},
251         '(title:(donald duck)) AND (author:disney)',
252         'multiple query terms are enclosed in parenthesis while a single one is not'
253     );
254
255     my ($simple_query, $query_cgi, $query_desc);
256     ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['"donald duck"', 'walt disney'], ['ti', 'au'] );
257     is($query_cgi, 'idx=ti&q=%22donald%20duck%22&idx=au&q=walt%20disney', 'query cgi ok for multiterm query');
258     is($query_desc, '(title:("donald duck")) (author:(walt disney))', 'query desc ok for multiterm query');
259
260     ( undef, $query ) = $qb->build_query_compat( undef, ['2019'], ['yr,st-year'] );
261     is(
262         $query->{query}{query_string}{query},
263         '(date-of-publication:2019)',
264         'Year in an st-year search is handled properly'
265     );
266
267     ( undef, $query ) = $qb->build_query_compat( undef, ['2018-2019'], ['yr,st-year'] );
268     is(
269         $query->{query}{query_string}{query},
270         '(date-of-publication:[2018 TO 2019])',
271         'Year range in an st-year search is handled properly'
272     );
273
274     ( undef, $query ) = $qb->build_query_compat( undef, ['-2019'], ['yr,st-year'] );
275     is(
276         $query->{query}{query_string}{query},
277         '(date-of-publication:[* TO 2019])',
278         'Open start year in year range of an st-year search is handled properly'
279     );
280
281     ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'] );
282     is(
283         $query->{query}{query_string}{query},
284         '(date-of-publication:[2019 TO *])',
285         'Open end year in year range of an st-year search is handled properly'
286     );
287
288     ( undef, $query ) = $qb->build_query_compat( undef, ['2019-'], ['yr,st-year'], ['yr,st-numeric=-2019'] );
289     is(
290         $query->{query}{query_string}{query},
291         '(date-of-publication:[2019 TO *]) AND copydate:[* TO 2019]',
292         'Open end year in year range of an st-year search is handled properly'
293     );
294
295     # Enable auto-truncation
296     t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '1' );
297
298     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
299     is(
300         $query->{query}{query_string}{query},
301         "(donald* duck*)",
302         "simple query is auto truncated when QueryAutoTruncate enabled"
303     );
304
305     # Ensure reserved words are not truncated
306     ( undef, $query ) = $qb->build_query_compat( undef,
307         ['donald or duck and mickey not mouse'] );
308     is(
309         $query->{query}{query_string}{query},
310         "(donald* or duck* and mickey* not mouse*)",
311         "reserved words are not affected by QueryAutoTruncate"
312     );
313
314     ( undef, $query ) = $qb->build_query_compat( undef, ['donald* duck*'] );
315     is(
316         $query->{query}{query_string}{query},
317         "(donald* duck*)",
318         "query with '*' is unaltered when QueryAutoTruncate is enabled"
319     );
320
321     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck and the mouse'] );
322     is(
323         $query->{query}{query_string}{query},
324         "(donald* duck* and the* mouse*)",
325         "individual words are all truncated and stopwords ignored"
326     );
327
328     ( undef, $query ) = $qb->build_query_compat( undef, ['*'] );
329     is(
330         $query->{query}{query_string}{query},
331         "(*)",
332         "query of just '*' is unaltered when QueryAutoTruncate is enabled"
333     );
334
335     ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck"'] );
336     is(
337         $query->{query}{query_string}{query},
338         '("donald duck")',
339         "query with quotes is unaltered when QueryAutoTruncate is enabled"
340     );
341
342
343     ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck" and "the mouse"'] );
344     is(
345         $query->{query}{query_string}{query},
346         '("donald duck" and "the mouse")',
347         "all quoted strings are unaltered if more than one in query"
348     );
349
350     ( undef, $query ) = $qb->build_query_compat( undef, ['barcode:123456'] );
351     is(
352         $query->{query}{query_string}{query},
353         '(barcode:123456*)',
354         "query of specific field is truncated"
355     );
356
357     ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:"123456"'] );
358     is(
359         $query->{query}{query_string}{query},
360         '(local-number:"123456")',
361         "query of specific field including hyphen and quoted is not truncated, field name is converted to lower case"
362     );
363
364     ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:123456'] );
365     is(
366         $query->{query}{query_string}{query},
367         '(local-number:123456*)',
368         "query of specific field including hyphen and not quoted is truncated, field name is converted to lower case"
369     );
370
371     ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:123456'] );
372     is(
373         $query->{query}{query_string}{query},
374         '(local-number.raw:123456*)',
375         "query of specific field including period and not quoted is truncated, field name is converted to lower case"
376     );
377
378     ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:"123456"'] );
379     is(
380         $query->{query}{query_string}{query},
381         '(local-number.raw:"123456")',
382         "query of specific field including period and quoted is not truncated, field name is converted to lower case"
383     );
384
385     ( undef, $query ) = $qb->build_query_compat( undef, ['J.R.R'] );
386     is(
387         $query->{query}{query_string}{query},
388         '(J.R.R*)',
389         "query including period is truncated but not split at periods"
390     );
391
392     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'] );
393     is(
394         $query->{query}{query_string}{query},
395         '(title:"donald duck")',
396         "query of specific field is not truncated when surrounded by quotes"
397     );
398
399     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'], ['title'] );
400     is(
401         $query->{query}{query_string}{query},
402         '(title:(donald* duck*))',
403         'words of a multi-word term are properly truncated'
404     );
405
406     ( undef, $query ) = $qb->build_query_compat( ['AND'], ['donald duck', 'disney'], ['title', 'author'] );
407     is(
408         $query->{query}{query_string}{query},
409         '(title:(donald* duck*)) AND (author:disney*)',
410         'words of a multi-word term and single-word term are properly truncated'
411     );
412
413     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 1 } );
414     is(
415         $query->{query}{query_string}{query},
416         '(title:"donald duck") AND suppress:0',
417         "query of specific field is added AND suppress:0"
418     );
419
420     ( undef, $query, $simple_query, $query_cgi, $query_desc ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 0 } );
421     is(
422         $query->{query}{query_string}{query},
423         '(title:"donald duck")',
424         "query of specific field is not added AND suppress:0"
425     );
426     is($query_cgi, 'idx=&q=title%3A%22donald%20duck%22', 'query cgi');
427     is($query_desc, 'title:"donald duck"', 'query desc ok');
428 };
429
430
431 subtest 'build query from form subtests' => sub {
432     plan tests => 5;
433
434     my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
435     #when searching for authorities from a record the form returns marclist with blanks for unentered terms
436     my @marclist = ('mainmainentry','mainentry','match', 'all');
437     my @values   = ( undef,         'Hamilton',  undef,   undef);
438     my @operator = ( 'contains', 'contains', 'contains', 'contains');
439
440     my $query = $qb->build_authorities_query_compat( \@marclist, undef,
441                     undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
442     is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","Expected search is populated");
443     is( scalar @{ $query->{query}->{bool}->{must} }, 1,"Only defined search is populated");
444
445     @values[2] = 'Jefferson';
446     $query = $qb->build_authorities_query_compat( \@marclist, undef,
447                     undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
448     is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","First index searched as expected");
449     is($query->{query}->{bool}->{must}[1]->{query_string}->{query}, "Jefferson*","Second index searched when populated");
450     is( scalar @{ $query->{query}->{bool}->{must} }, 2,"Only defined searches are populated");
451
452
453 };
454
455 subtest 'build_query with weighted fields tests' => sub {
456     plan tests => 4;
457
458     my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'mydb' } );
459     my $db_builder = t::lib::TestBuilder->new();
460
461     Koha::SearchFields->search({})->delete;
462
463     $db_builder->build({
464         source => 'SearchField',
465         value => {
466             name    => 'acqdate',
467             label   => 'acqdate',
468             weight  => undef
469         }
470     });
471
472     $db_builder->build({
473         source => 'SearchField',
474         value => {
475             name    => 'title',
476             label   => 'title',
477             weight  => 25
478         }
479     });
480
481     $db_builder->build({
482         source => 'SearchField',
483         value => {
484             name    => 'subject',
485             label   => 'subject',
486             weight  => 15
487         }
488     });
489
490     my ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
491     undef, undef, undef, { weighted_fields => 1 });
492
493     my $fields = $query->{query}{query_string}{fields};
494     is(scalar(@$fields), 3, 'Search is done on 3 fields');
495     is($fields->[0], '_all', 'First search field is _all');
496     is($fields->[1], 'title^25.00', 'Second search field is title');
497     is($fields->[2], 'subject^15.00', 'Third search field is subject');
498 };
499
500 subtest "_convert_sort_fields() tests" => sub {
501     plan tests => 3;
502
503     my $qb;
504
505     ok(
506         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
507         'Creating new query builder object for biblios'
508     );
509
510     my @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_dsc ));
511     is_deeply(
512         \@sort_by,
513         [
514             { field => 'local-classification', direction => 'asc' },
515             { field => 'author',  direction => 'desc' }
516         ],
517         'sort fields should have been split correctly'
518     );
519
520     # We could expect this to pass, but direction is undef instead of 'desc'
521     @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_desc ));
522     is_deeply(
523         \@sort_by,
524         [
525             { field => 'local-classification', direction => 'asc' },
526             { field => 'author',  direction => 'desc' }
527         ],
528         'sort fields should have been split correctly'
529     );
530 };
531
532 subtest "_sort_field() tests" => sub {
533     plan tests => 5;
534
535     my $qb;
536
537     ok(
538         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
539         'Creating new query builder object for biblios'
540     );
541
542     my $f = $qb->_sort_field('title');
543     is(
544         $f,
545         'title__sort',
546         'title sort mapped correctly'
547     );
548
549     $f = $qb->_sort_field('subject');
550     is(
551         $f,
552         'subject.raw',
553         'subject sort mapped correctly'
554     );
555
556     $f = $qb->_sort_field('itemnumber');
557     is(
558         $f,
559         'itemnumber',
560         'itemnumber sort mapped correctly'
561     );
562
563     $f = $qb->_sort_field('sortablenumber');
564     is(
565         $f,
566         'sortablenumber__sort',
567         'sortablenumber sort mapped correctly'
568     );
569 };
570
571 $schema->storage->txn_rollback;