Bug 19365: Clean up, fix and add new 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 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 => 44;
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]->{wildcard}->{"_all.phrase"},
100                 "*a*");
101         } else {
102             is( $query->{query}->{bool}->{must}[0]->{wildcard}->{$koha_to_index_name->{$koha_name}.".phrase"},
103                 "*a*");
104         }
105     }
106
107     $search_term = 'Donald Duck';
108     foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
109         my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
110         if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
111             is( $query->{query}->{bool}->{must}[0]->{wildcard}->{"_all.phrase"},
112                 "*donald*");
113             is( $query->{query}->{bool}->{must}[1]->{wildcard}->{"_all.phrase"},
114                 "*duck*");
115         } else {
116             is( $query->{query}->{bool}->{must}[0]->{wildcard}->{$koha_to_index_name->{$koha_name}.".phrase"},
117                 "*donald*");
118             is( $query->{query}->{bool}->{must}[1]->{wildcard}->{$koha_to_index_name->{$koha_name}.".phrase"},
119                 "*duck*");
120         }
121     }
122
123     foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
124         my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['is'], [$search_term], 'AUTH_TYPE', 'asc' );
125         if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
126             is( $query->{query}->{bool}->{must}[0]->{match_phrase}->{"_all.phrase"},
127                 "donald duck");
128         } else {
129             is( $query->{query}->{bool}->{must}[0]->{match_phrase}->{$koha_to_index_name->{$koha_name}.".phrase"},
130                 "donald duck");
131         }
132     }
133
134     foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
135         my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'asc' );
136         if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
137             is( $query->{query}->{bool}->{must}[0]->{match_phrase_prefix}->{"_all.phrase"},
138                 "donald duck");
139         } else {
140             is( $query->{query}->{bool}->{must}[0]->{match_phrase_prefix}->{$koha_to_index_name->{$koha_name}.".phrase"},
141                 "donald duck");
142         }
143     }
144
145     # Sorting
146     my $query = $qb->build_authorities_query_compat( [ 'mainentry' ],  undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingAsc' );
147     is_deeply(
148         $query->{sort},
149         [
150             {
151                 'Heading__sort.phrase' => 'asc'
152             }
153         ],
154         "ascending sort parameter properly formed"
155     );
156     $query = $qb->build_authorities_query_compat( [ 'mainentry' ],  undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'HeadingDsc' );
157     is_deeply(
158         $query->{sort},
159         [
160             {
161                 'Heading__sort.phrase' => 'desc'
162             }
163         ],
164         "descending sort parameter properly formed"
165     );
166
167     # Failing case
168     throws_ok {
169         $qb->build_authorities_query_compat( [ 'tomas' ],  undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
170     }
171     'Koha::Exceptions::WrongParameter',
172         'Exception thrown on invalid value in the marclist param';
173 };
174
175 subtest 'build_query tests' => sub {
176     plan tests => 26;
177
178     my $qb;
179
180     ok(
181         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
182         'Creating new query builder object for biblios'
183     );
184
185     my @sort_by = 'title_asc';
186     my @sort_params = $qb->_convert_sort_fields(@sort_by);
187     my %options;
188     $options{sort} = \@sort_params;
189     my $query = $qb->build_query('test', %options);
190
191     is_deeply(
192         $query->{sort},
193         [
194             {
195             'title__sort.phrase' => {
196                     'order' => 'asc'
197                 }
198             }
199         ],
200         "sort parameter properly formed"
201     );
202
203     t::lib::Mocks::mock_preference('DisplayLibraryFacets','both');
204     $query = $qb->build_query();
205     ok( defined $query->{aggregations}{homebranch},
206         'homebranch added to facets if DisplayLibraryFacets=both' );
207     ok( defined $query->{aggregations}{holdingbranch},
208         'holdingbranch added to facets if DisplayLibraryFacets=both' );
209     t::lib::Mocks::mock_preference('DisplayLibraryFacets','holding');
210     $query = $qb->build_query();
211     ok( !defined $query->{aggregations}{homebranch},
212         'homebranch not added to facets if DisplayLibraryFacets=holding' );
213     ok( defined $query->{aggregations}{holdingbranch},
214         'holdingbranch added to facets if DisplayLibraryFacets=holding' );
215     t::lib::Mocks::mock_preference('DisplayLibraryFacets','home');
216     $query = $qb->build_query();
217     ok( defined $query->{aggregations}{homebranch},
218         'homebranch added to facets if DisplayLibraryFacets=home' );
219     ok( !defined $query->{aggregations}{holdingbranch},
220         'holdingbranch not added to facets if DisplayLibraryFacets=home' );
221
222     t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '' );
223
224     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
225     is(
226         $query->{query}{query_string}{query},
227         "(donald duck)",
228         "query not altered if QueryAutoTruncate disabled"
229     );
230
231     t::lib::Mocks::mock_preference( 'QueryAutoTruncate', '1' );
232
233     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck'] );
234     is(
235         $query->{query}{query_string}{query},
236         "(donald* duck*)",
237         "simple query is auto truncated when QueryAutoTruncate enabled"
238     );
239
240     # Ensure reserved words are not truncated
241     ( undef, $query ) = $qb->build_query_compat( undef,
242         ['donald or duck and mickey not mouse'] );
243     is(
244         $query->{query}{query_string}{query},
245         "(donald* or duck* and mickey* not mouse*)",
246         "reserved words are not affected by QueryAutoTruncate"
247     );
248
249     ( undef, $query ) = $qb->build_query_compat( undef, ['donald* duck*'] );
250     is(
251         $query->{query}{query_string}{query},
252         "(donald* duck*)",
253         "query with '*' is unaltered when QueryAutoTruncate is enabled"
254     );
255
256     ( undef, $query ) = $qb->build_query_compat( undef, ['donald duck and the mouse'] );
257     is(
258         $query->{query}{query_string}{query},
259         "(donald* duck* and the* mouse*)",
260         "individual words are all truncated and stopwords ignored"
261     );
262
263     ( undef, $query ) = $qb->build_query_compat( undef, ['*'] );
264     is(
265         $query->{query}{query_string}{query},
266         "(*)",
267         "query of just '*' is unaltered when QueryAutoTruncate is enabled"
268     );
269
270     ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck"'] );
271     is(
272         $query->{query}{query_string}{query},
273         '("donald duck")',
274         "query with quotes is unaltered when QueryAutoTruncate is enabled"
275     );
276
277
278     ( undef, $query ) = $qb->build_query_compat( undef, ['"donald duck" and "the mouse"'] );
279     is(
280         $query->{query}{query_string}{query},
281         '("donald duck" and "the mouse")',
282         "all quoted strings are unaltered if more than one in query"
283     );
284
285     ( undef, $query ) = $qb->build_query_compat( undef, ['barcode:123456'] );
286     is(
287         $query->{query}{query_string}{query},
288         '(barcode:123456*)',
289         "query of specific field is truncated"
290     );
291
292     ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:"123456"'] );
293     is(
294         $query->{query}{query_string}{query},
295         '(Local-number:"123456")',
296         "query of specific field including hyphen and quoted is not truncated"
297     );
298
299     ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number:123456'] );
300     is(
301         $query->{query}{query_string}{query},
302         '(Local-number:123456*)',
303         "query of specific field including hyphen and not quoted is truncated"
304     );
305
306     ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:123456'] );
307     is(
308         $query->{query}{query_string}{query},
309         '(Local-number.raw:123456*)',
310         "query of specific field including period and not quoted is truncated"
311     );
312
313     ( undef, $query ) = $qb->build_query_compat( undef, ['Local-number.raw:"123456"'] );
314     is(
315         $query->{query}{query_string}{query},
316         '(Local-number.raw:"123456")',
317         "query of specific field including period and quoted is not truncated"
318     );
319
320     ( undef, $query ) = $qb->build_query_compat( undef, ['J.R.R'] );
321     is(
322         $query->{query}{query_string}{query},
323         '(J.R.R*)',
324         "query including period is truncated but not split at periods"
325     );
326
327     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'] );
328     is(
329         $query->{query}{query_string}{query},
330         '(title:"donald duck")',
331         "query of specific field is not truncated when surrouned by quotes"
332     );
333
334     ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 1 } );
335     is(
336         $query->{query}{query_string}{query},
337         '(title:"donald duck") AND suppress:0',
338         "query of specific field is added AND suppress:0"
339     );
340
341     my ($simple_query, $query_cgi);
342     ( undef, $query, $simple_query, $query_cgi ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef, undef, undef, undef, { suppress => 0 } );
343     is(
344         $query->{query}{query_string}{query},
345         '(title:"donald duck")',
346         "query of specific field is not added AND suppress:0"
347     );
348     is($query_cgi, 'q=title%3A%22donald%20duck%22', 'query cgi');
349 };
350
351
352 subtest 'build query from form subtests' => sub {
353     plan tests => 5;
354
355     my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'authorities' }),
356     #when searching for authorities from a record the form returns marclist with blanks for unentered terms
357     my @marclist = ('mainmainentry','mainentry','match', 'all');
358     my @values   = ( undef,         'Hamilton',  undef,   undef);
359     my @operator = ( 'contains', 'contains', 'contains', 'contains');
360
361     my $query = $qb->build_authorities_query_compat( \@marclist, undef,
362                     undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
363     is($query->{query}->{bool}->{must}[0]->{wildcard}->{'Heading.phrase'}, "*hamilton*","Expected search is populated");
364     is( scalar @{ $query->{query}->{bool}->{must} }, 1,"Only defined search is populated");
365
366     @values[2] = 'Jefferson';
367     $query = $qb->build_authorities_query_compat( \@marclist, undef,
368                     undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
369     is($query->{query}->{bool}->{must}[0]->{wildcard}->{'Heading.phrase'}, "*hamilton*","First index searched as expected");
370     is($query->{query}->{bool}->{must}[1]->{wildcard}->{'Match.phrase'}, "*jefferson*","Second index searched when populated");
371     is( scalar @{ $query->{query}->{bool}->{must} }, 2,"Only defined searches are populated");
372
373
374 };
375
376 subtest 'build_query with weighted fields tests' => sub {
377     plan tests => 4;
378
379     my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'mydb' } );
380     my $db_builder = t::lib::TestBuilder->new();
381
382     Koha::SearchFields->search({})->delete;
383
384     $db_builder->build({
385         source => 'SearchField',
386         value => {
387             name    => 'acqdate',
388             label   => 'acqdate',
389             weight  => undef
390         }
391     });
392
393     $db_builder->build({
394         source => 'SearchField',
395         value => {
396             name    => 'title',
397             label   => 'title',
398             weight  => 25
399         }
400     });
401
402     $db_builder->build({
403         source => 'SearchField',
404         value => {
405             name    => 'subject',
406             label   => 'subject',
407             weight  => 15
408         }
409     });
410
411     my ( undef, $query ) = $qb->build_query_compat( undef, ['title:"donald duck"'], undef, undef,
412     undef, undef, undef, { weighted_fields => 1 });
413
414     my $fields = $query->{query}{query_string}{fields};
415     is(scalar(@$fields), 3, 'Search is done on 3 fields');
416     is($fields->[0], '_all', 'First search field is _all');
417     is($fields->[1], 'title^25.00', 'Second search field is title');
418     is($fields->[2], 'subject^15.00', 'Third search field is subject');
419 };
420
421 subtest "_convert_sort_fields() tests" => sub {
422     plan tests => 3;
423
424     my $qb;
425
426     ok(
427         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
428         'Creating new query builder object for biblios'
429     );
430
431     my @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_dsc ));
432     is_deeply(
433         \@sort_by,
434         [
435             { field => 'callnum', direction => 'asc' },
436             { field => 'author',  direction => 'desc' }
437         ],
438         'sort fields should have been split correctly'
439     );
440
441     # We could expect this to pass, but direction is undef instead of 'desc'
442     @sort_by = $qb->_convert_sort_fields(qw( call_number_asc author_desc ));
443     is_deeply(
444         \@sort_by,
445         [
446             { field => 'callnum', direction => 'asc' },
447             { field => 'author',  direction => 'desc' }
448         ],
449         'sort fields should have been split correctly'
450     );
451 };
452
453 subtest "_sort_field() tests" => sub {
454     plan tests => 5;
455
456     my $qb;
457
458     ok(
459         $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => 'biblios' }),
460         'Creating new query builder object for biblios'
461     );
462
463     my $f = $qb->_sort_field('title');
464     is(
465         $f,
466         'title__sort.phrase',
467         'title sort mapped correctly'
468     );
469
470     $f = $qb->_sort_field('subject');
471     is(
472         $f,
473         'subject.raw',
474         'subject sort mapped correctly'
475     );
476
477     $f = $qb->_sort_field('itemnumber');
478     is(
479         $f,
480         'itemnumber',
481         'itemnumber sort mapped correctly'
482     );
483
484     $f = $qb->_sort_field('sortablenumber');
485     is(
486         $f,
487         'sortablenumber__sort',
488         'sortablenumber sort mapped correctly'
489     );
490 };
491
492 $schema->storage->txn_rollback;