View | Details | Raw Unified | Return to bug 20589
Collapse All | Expand All

(-)a/Koha/Schema/Result/SearchField.pm (+4 lines)
Lines 87-92 __PACKAGE__->add_columns( Link Here
87
  { data_type => "decimal", is_nullable => 1, size => [5, 2] },
87
  { data_type => "decimal", is_nullable => 1, size => [5, 2] },
88
  "facet_order",
88
  "facet_order",
89
  { data_type => "tinyint", is_nullable => 1 },
89
  { data_type => "tinyint", is_nullable => 1 },
90
  "staff_client",
91
  { data_type => "tinyint", size => 1, is_nullable => 0, default => 1 },
92
  "opac",
93
  { data_type => "tinyint", size => 1, is_nullable => 0, default => 1 },
90
);
94
);
91
95
92
=head1 PRIMARY KEY
96
=head1 PRIMARY KEY
(-)a/Koha/Schema/Result/SearchMarcToField.pm (+2 lines)
Lines 71-76 __PACKAGE__->add_columns( Link Here
71
  { data_type => "tinyint", default_value => 0, is_nullable => 1 },
71
  { data_type => "tinyint", default_value => 0, is_nullable => 1 },
72
  "sort",
72
  "sort",
73
  { data_type => "tinyint", is_nullable => 1 },
73
  { data_type => "tinyint", is_nullable => 1 },
74
  "search",
75
  { data_type => "tinyint", size => 1, is_nullable => 0, default => 1 },
74
);
76
);
75
77
76
=head1 PRIMARY KEY
78
=head1 PRIMARY KEY
(-)a/Koha/SearchEngine/Elasticsearch.pm (-12 / +36 lines)
Lines 198-204 sub get_elasticsearch_mappings { Link Here
198
        my $marcflavour = lc C4::Context->preference('marcflavour');
198
        my $marcflavour = lc C4::Context->preference('marcflavour');
199
        $self->_foreach_mapping(
199
        $self->_foreach_mapping(
200
            sub {
200
            sub {
201
                my ( $name, $type, $facet, $suggestible, $sort, $marc_type ) = @_;
201
                my ( $name, $type, $facet, $suggestible, $sort, $search, $marc_type ) = @_;
202
                return if $marc_type ne $marcflavour;
202
                return if $marc_type ne $marcflavour;
203
                # TODO if this gets any sort of complexity to it, it should
203
                # TODO if this gets any sort of complexity to it, it should
204
                # be broken out into its own function.
204
                # be broken out into its own function.
Lines 214-220 sub get_elasticsearch_mappings { Link Here
214
                    $es_type = 'stdno';
214
                    $es_type = 'stdno';
215
                }
215
                }
216
216
217
                $mappings->{data}{properties}{$name} = _get_elasticsearch_mapping('search', $es_type);
217
                if ($search) {
218
                    $mappings->{data}{properties}{$name} = _get_elasticsearch_mapping('search', $es_type);
219
                }
218
220
219
                if ($facet) {
221
                if ($facet) {
220
                    $mappings->{data}{properties}{ $name . '__facet' } = _get_elasticsearch_mapping('facet', $es_type);
222
                    $mappings->{data}{properties}{ $name . '__facet' } = _get_elasticsearch_mapping('facet', $es_type);
Lines 283-294 sub reset_elasticsearch_mappings { Link Here
283
        while ( my ( $field_name, $data ) = each %$fields ) {
285
        while ( my ( $field_name, $data ) = each %$fields ) {
284
            my $field_type = $data->{type};
286
            my $field_type = $data->{type};
285
            my $field_label = $data->{label};
287
            my $field_label = $data->{label};
288
            my $staff_client = exists $data->{staff_client} ? $data->{staff_client} : 1;
289
            my $opac = exists $data->{opac} ? $data->{opac} : 1;
286
            my $mappings = $data->{mappings};
290
            my $mappings = $data->{mappings};
287
            my $facet_order = $data->{facet_order};
291
            my $facet_order = $data->{facet_order};
288
            my $search_field = Koha::SearchFields->find_or_create({
292
            my $search_field = Koha::SearchFields->find_or_create({
289
                name  => $field_name,
293
                name  => $field_name,
290
                label => $field_label,
294
                label => $field_label,
291
                type  => $field_type,
295
                type  => $field_type,
296
                staff_client => $staff_client,
297
                opac => $opac
292
            },
298
            },
293
            {
299
            {
294
                key => 'name'
300
                key => 'name'
Lines 299-306 sub reset_elasticsearch_mappings { Link Here
299
                }
305
                }
300
            );
306
            );
301
            for my $mapping ( @$mappings ) {
307
            for my $mapping ( @$mappings ) {
302
                my $marc_field = Koha::SearchMarcMaps->find_or_create({ index_name => $index_name, marc_type => $mapping->{marc_type}, marc_field => $mapping->{marc_field} });
308
                my $marc_field = Koha::SearchMarcMaps->find_or_create({
303
                $search_field->add_to_search_marc_maps($marc_field, { facet => $mapping->{facet} || 0, suggestible => $mapping->{suggestible} || 0, sort => $mapping->{sort} } );
309
                    index_name => $index_name,
310
                    marc_type => $mapping->{marc_type},
311
                    marc_field => $mapping->{marc_field}
312
                });
313
                $search_field->add_to_search_marc_maps($marc_field, {
314
                    facet => $mapping->{facet} || 0,
315
                    suggestible => $mapping->{suggestible} || 0,
316
                    sort => $mapping->{sort},
317
                    search => $mapping->{search} || 1
318
                });
304
            }
319
            }
305
        }
320
        }
306
    }
321
    }
Lines 562-570 sub marc_records_to_documents { Link Here
562
    return \@record_documents;
577
    return \@record_documents;
563
}
578
}
564
579
565
=head2 _field_mappings($facet, $suggestible, $sort, $target_name, $target_type, $range)
580
=head2 _field_mappings($facet, $suggestible, $sort, $search, $target_name, $target_type, $range)
566
581
567
    my @mappings = _field_mappings($facet, $suggestible, $sort, $target_name, $target_type, $range)
582
    my @mappings = _field_mappings($facet, $suggestible, $sort, $search, $target_name, $target_type, $range)
568
583
569
Get mappings, an internal data structure later used by
584
Get mappings, an internal data structure later used by
570
L<_process_mappings($mappings, $data, $record_document, $altscript)> to process MARC target
585
L<_process_mappings($mappings, $data, $record_document, $altscript)> to process MARC target
Lines 593-598 Boolean indicating whether to create a suggestion field for this mapping. Link Here
593
608
594
Boolean indicating whether to create a sort field for this mapping.
609
Boolean indicating whether to create a sort field for this mapping.
595
610
611
=item C<$search>
612
613
Boolean indicating whether to create a search field for this mapping.
614
596
=item C<$target_name>
615
=item C<$target_name>
597
616
598
Elasticsearch document target field name.
617
Elasticsearch document target field name.
Lines 618-624 be extracted. Link Here
618
=cut
637
=cut
619
638
620
sub _field_mappings {
639
sub _field_mappings {
621
    my ($_self, $facet, $suggestible, $sort, $target_name, $target_type, $range) = @_;
640
    my ($_self, $facet, $suggestible, $sort, $search, $target_name, $target_type, $range) = @_;
622
    my %mapping_defaults = ();
641
    my %mapping_defaults = ();
623
    my @mappings;
642
    my @mappings;
624
643
Lines 646-653 sub _field_mappings { Link Here
646
        };
665
        };
647
    }
666
    }
648
667
649
    my $mapping = [$target_name, $default_options];
668
    if ($search) {
650
    push @mappings, $mapping;
669
        my $mapping = [$target_name, $default_options];
670
        push @mappings, $mapping;
671
    }
651
672
652
    my @suffixes = ();
673
    my @suffixes = ();
653
    push @suffixes, 'facet' if $facet;
674
    push @suffixes, 'facet' if $facet;
Lines 704-710 sub _get_marc_mapping_rules { Link Here
704
    };
725
    };
705
726
706
    $self->_foreach_mapping(sub {
727
    $self->_foreach_mapping(sub {
707
        my ($name, $type, $facet, $suggestible, $sort, $marc_type, $marc_field) = @_;
728
        my ($name, $type, $facet, $suggestible, $sort, $search, $marc_type, $marc_field) = @_;
708
        return if $marc_type ne $marcflavour;
729
        return if $marc_type ne $marcflavour;
709
730
710
        if ($type eq 'sum') {
731
        if ($type eq 'sum') {
Lines 767-773 sub _get_marc_mapping_rules { Link Here
767
            }
788
            }
768
789
769
            my $range = defined $3 ? $3 : undef;
790
            my $range = defined $3 ? $3 : undef;
770
            my @mappings = $self->_field_mappings($facet, $suggestible, $sort, $name, $type, $range);
791
            my @mappings = $self->_field_mappings($facet, $suggestible, $sort, $search, $name, $type, $range);
771
792
772
            if ($field_tag < 10) {
793
            if ($field_tag < 10) {
773
                $rules->{control_fields}->{$field_tag} //= [];
794
                $rules->{control_fields}->{$field_tag} //= [];
Lines 787-793 sub _get_marc_mapping_rules { Link Here
787
        }
808
        }
788
        elsif ($marc_field =~ $leader_regexp) {
809
        elsif ($marc_field =~ $leader_regexp) {
789
            my $range = defined $1 ? $1 : undef;
810
            my $range = defined $1 ? $1 : undef;
790
            my @mappings = $self->_field_mappings($facet, $suggestible, $sort, $name, $type, $range);
811
            my @mappings = $self->_field_mappings($facet, $suggestible, $sort, $search, $name, $type, $range);
791
            push @{$rules->{leader}}, @mappings;
812
            push @{$rules->{leader}}, @mappings;
792
        }
813
        }
793
        else {
814
        else {
Lines 866-871 sub _foreach_mapping { Link Here
866
                'search_marc_to_fields.facet',
887
                'search_marc_to_fields.facet',
867
                'search_marc_to_fields.suggestible',
888
                'search_marc_to_fields.suggestible',
868
                'search_marc_to_fields.sort',
889
                'search_marc_to_fields.sort',
890
                'search_marc_to_fields.search',
869
                'search_marc_map.marc_type',
891
                'search_marc_map.marc_type',
870
                'search_marc_map.marc_field',
892
                'search_marc_map.marc_field',
871
            ],
893
            ],
Lines 873-878 sub _foreach_mapping { Link Here
873
                'facet',
895
                'facet',
874
                'suggestible',
896
                'suggestible',
875
                'sort',
897
                'sort',
898
                'search',
876
                'marc_type',
899
                'marc_type',
877
                'marc_field',
900
                'marc_field',
878
            ],
901
            ],
Lines 888-893 sub _foreach_mapping { Link Here
888
            $search_field->get_column('facet'),
911
            $search_field->get_column('facet'),
889
            $search_field->get_column('suggestible'),
912
            $search_field->get_column('suggestible'),
890
            $search_field->get_column('sort'),
913
            $search_field->get_column('sort'),
914
            $search_field->get_column('search'),
891
            $search_field->get_column('marc_type'),
915
            $search_field->get_column('marc_type'),
892
            $search_field->get_column('marc_field'),
916
            $search_field->get_column('marc_field'),
893
        );
917
        );
(-)a/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm (-46 / +185 lines)
Lines 48-53 use URI::Escape; Link Here
48
48
49
use C4::Context;
49
use C4::Context;
50
use Koha::Exceptions;
50
use Koha::Exceptions;
51
use Koha::Caches;
51
52
52
=head2 build_query
53
=head2 build_query
53
54
Lines 90-98 sub build_query { Link Here
90
            query            => $query,
91
            query            => $query,
91
            fuzziness        => $fuzzy_enabled ? 'auto' : '0',
92
            fuzziness        => $fuzzy_enabled ? 'auto' : '0',
92
            default_operator => 'AND',
93
            default_operator => 'AND',
93
            default_field    => '_all',
94
            fields          => $self->_search_fields({ is_opac => $options{is_opac}, weighted_fields => $options{weighted_fields} }),
94
            lenient          => JSON::true,
95
            lenient          => JSON::true,
95
            fields           => $options{fields} || [],
96
        }
96
        }
97
    };
97
    };
98
98
Lines 231-247 sub build_query_compat { Link Here
231
        join( ' ', $self->_create_query_string(@search_params) ) || (),
231
        join( ' ', $self->_create_query_string(@search_params) ) || (),
232
        $self->_join_queries( $self->_convert_index_strings(@$limits) ) || () );
232
        $self->_join_queries( $self->_convert_index_strings(@$limits) ) || () );
233
233
234
    my @fields = '_all';
235
    if ( defined($params->{weighted_fields}) && $params->{weighted_fields} ) {
236
        push @fields, sprintf("%s^%s", $_->name, $_->weight) for Koha::SearchFields->weighted_fields;
237
    }
238
239
    # If there's no query on the left, let's remove the junk left behind
234
    # If there's no query on the left, let's remove the junk left behind
240
    $query_str =~ s/^ AND //;
235
    $query_str =~ s/^ AND //;
241
    my %options;
236
    my %options;
242
    $options{fields} = \@fields;
243
    $options{sort} = \@sort_params;
237
    $options{sort} = \@sort_params;
244
    $options{expanded_facet} = $params->{expanded_facet};
238
    $options{expanded_facet} = $params->{expanded_facet};
239
    $options{is_opac} = $params->{is_opac};
240
    $options{weighted_fields} = $params->{weighted_fields};
245
    my $query = $self->build_query( $query_str, %options );
241
    my $query = $self->build_query( $query_str, %options );
246
242
247
    #die Dumper($query);
243
    #die Dumper($query);
Lines 302-323 sub build_authorities_query { Link Here
302
298
303
    foreach my $s ( @{ $search->{searches} } ) {
299
    foreach my $s ( @{ $search->{searches} } ) {
304
        my ( $wh, $op, $val ) = @{$s}{qw(where operator value)};
300
        my ( $wh, $op, $val ) = @{$s}{qw(where operator value)};
305
        $wh = '_all' if $wh eq '';
301
        if ( $op eq 'is' || $op eq '=' || $op eq 'exact') {
306
        if ( $op eq 'is' || $op eq '='  || $op eq 'exact' ) {
302
            if ($wh) {
307
303
                # Match the whole field, case insensitive, UTF normalized.
308
            # look for something that matches a term completely
304
                push @query_parts, { term => { "$wh.ci_raw" => $val } };
309
            # note, '=' is about numerical vals. May need special handling.
305
            }
310
            # Also, we lowercase our search because the ES
306
            else {
311
            # index lowercases its values, and term searches don't get the
307
                # Match the whole field for all searchable fields, case insensitive,
312
            # search analyzer applied to them.
308
                # UTF normalized.
313
            push @query_parts, { match_phrase => {"$wh.phrase" => lc $val} };
309
                # Given that field data is "The quick brown fox"
310
                # "The quick brown fox" and "the quick brown fox" will match
311
                # but not "quick brown fox".
312
                push @query_parts, {
313
                    multi_match => {
314
                        query => $val,
315
                        fields => $self->_search_fields({ subfield => 'ci_raw' }),
316
                    }
317
                };
318
            }
314
        }
319
        }
315
        elsif ( $op eq 'start' ) {
320
        elsif ( $op eq 'start') {
316
            # startswith search, uses lowercase untokenized version of heading
321
            # Match the prefix within a field for all searchable fields.
317
            push @query_parts, { match_phrase_prefix => {"$wh.phrase" => lc $val} };
322
            # Given that field data is "The quick brown fox"
323
            # "The quick bro" will match, but not "quick bro"
324
325
            # Does not seems to be a multi prefix query
326
            # so we need to create one
327
            if ($wh) {
328
                # Match prefix of the field.
329
                push @query_parts, { prefix => {"$wh.ci_raw" => $val} };
330
            }
331
            else {
332
                my @prefix_queries;
333
                foreach my $field (@{$self->_search_fields()}) {
334
                    push @prefix_queries, {
335
                        prefix => { "$field.ci_raw" => $val }
336
                    };
337
                }
338
                push @query_parts, {
339
                    'bool' => {
340
                        'should' => \@prefix_queries,
341
                        'minimum_should_match' => 1
342
                    }
343
                };
344
            }
318
        }
345
        }
319
        else {
346
        else {
320
            # regular wordlist stuff
347
            # Query all searchable fields.
348
            # Given that field data is "The quick brown fox"
349
            # a search containing any of the words will match, regardless
350
            # of order.
351
321
            my @tokens = $self->_split_query( $val );
352
            my @tokens = $self->_split_query( $val );
322
            foreach my $token ( @tokens ) {
353
            foreach my $token ( @tokens ) {
323
                $token = $self->_truncate_terms(
354
                $token = $self->_truncate_terms(
Lines 325-360 sub build_authorities_query { Link Here
325
                );
356
                );
326
            }
357
            }
327
            my $query = $self->_join_queries( @tokens );
358
            my $query = $self->_join_queries( @tokens );
328
            push @query_parts, { query_string => { default_field => $wh, query => $query } };
359
360
            if ($wh) {
361
                push @query_parts, { query_string => { default_field => $wh, query => $query } };
362
            }
363
            else {
364
                push @query_parts, {
365
                    query_string => {
366
                        query => $query,
367
                        fields => $self->_search_fields(),
368
                    }
369
                };
370
            }
329
        }
371
        }
330
    }
372
    }
331
373
332
    # Merge the query parts appropriately
374
    # Merge the query parts appropriately
333
    # 'should' behaves like 'or'
375
    # 'should' behaves like 'or'
334
    # 'must' behaves like 'and'
376
    # 'must' behaves like 'and'
335
    # Zebra results seem to match must so using that here
377
    # Zebra behaviour seem to match must so using that here
336
    my $query = { query =>
378
    my $elastic_query = {};
337
                 { bool =>
379
    $elastic_query->{bool}->{must} = \@query_parts;
338
                     { must => \@query_parts  }
380
339
                 }
381
    # Filter by authtypecode if set
340
             };
382
    if ($search->{authtypecode}) {
341
383
        $elastic_query->{bool}->{filter} = {
342
    my %s;
384
            term => {
343
    if ( exists $search->{sort} ) {
385
                "authtype.raw" => $search->{authtypecode}
344
        foreach my $k ( keys %{ $search->{sort} } ) {
386
            }
345
            my $f = $self->_sort_field($k);
387
        };
346
            $s{$f} = $search->{sort}{$k};
347
        }
348
        $search->{sort} = \%s;
349
    }
388
    }
350
389
351
    # add the sort stuff
390
    my $query = {
352
    $query->{sort} = [ $search->{sort} ]  if exists $search->{sort};
391
        query => $elastic_query
392
    };
393
394
    # Add the sort stuff
395
    $query->{sort} = [ $search->{sort} ] if exists $search->{sort};
353
396
354
    return $query;
397
    return $query;
355
}
398
}
356
399
357
358
=head2 build_authorities_query_compat
400
=head2 build_authorities_query_compat
359
401
360
    my ($query) =
402
    my ($query) =
Lines 452-459 sub build_authorities_query_compat { Link Here
452
494
453
    my %sort;
495
    my %sort;
454
    my $sort_field =
496
    my $sort_field =
455
        ( $orderby =~ /^heading/ ) ? 'heading'
497
        ( $orderby =~ /^heading/ ) ? 'heading__sort'
456
      : ( $orderby =~ /^auth/ )    ? 'local-number'
498
      : ( $orderby =~ /^auth/ )    ? 'local-number__sort'
457
      :                              undef;
499
      :                              undef;
458
    if ($sort_field) {
500
    if ($sort_field) {
459
        my $sort_order = ( $orderby =~ /asc$/ ) ? 'asc' : 'desc';
501
        my $sort_order = ( $orderby =~ /asc$/ ) ? 'asc' : 'desc';
Lines 520-526 types. Link Here
520
=cut
562
=cut
521
563
522
our %index_field_convert = (
564
our %index_field_convert = (
523
    'kw' => '_all',
565
    'kw' => '',
524
    'ab' => 'abstract',
566
    'ab' => 'abstract',
525
    'au' => 'author',
567
    'au' => 'author',
526
    'lcn' => 'local-classification',
568
    'lcn' => 'local-classification',
Lines 614-620 sub _convert_index_fields { Link Here
614
    # If a field starts with mc- we save it as it's used (and removed) later
656
    # If a field starts with mc- we save it as it's used (and removed) later
615
    # when joining things, to indicate we make it an 'OR' join.
657
    # when joining things, to indicate we make it an 'OR' join.
616
    # (Sorry, this got a bit ugly after special cases were found.)
658
    # (Sorry, this got a bit ugly after special cases were found.)
617
    grep { $_->{field} } map {
659
    map {
618
        # Lower case all field names
660
        # Lower case all field names
619
        my ( $f, $t ) = map(lc, split /,/);
661
        my ( $f, $t ) = map(lc, split /,/);
620
        my $mc = '';
662
        my $mc = '';
Lines 627-633 sub _convert_index_fields { Link Here
627
            type  => $index_type_convert{ $t // '__default' }
669
            type  => $index_type_convert{ $t // '__default' }
628
        };
670
        };
629
        $r->{field} = ($mc . $r->{field}) if $mc && $r->{field};
671
        $r->{field} = ($mc . $r->{field}) if $mc && $r->{field};
630
        $r;
672
        $r->{field} ? $r : undef;
631
    } @indexes;
673
    } @indexes;
632
}
674
}
633
675
Lines 656-663 sub _convert_index_strings { Link Here
656
            push @res, $s;
698
            push @res, $s;
657
            next;
699
            next;
658
        }
700
        }
659
        push @res, $conv->{field} . ":"
701
        push @res, ($conv->{field} ? $conv->{field} . ':' : '')
660
          . $self->_modify_string_by_type( %$conv, operand => $term );
702
            . $self->_modify_string_by_type( %$conv, operand => $term );
661
    }
703
    }
662
    return @res;
704
    return @res;
663
}
705
}
Lines 945-948 sub _split_query { Link Here
945
    return @tokens;
987
    return @tokens;
946
}
988
}
947
989
990
=head2 _search_fields
991
    my $weighted_fields = $self->_search_fields({
992
        is_opac => 0,
993
        weighted_fields => 1,
994
        subfield => 'raw'
995
    });
996
997
Generate a list of searchable fields to be used for Elasticsearch queries
998
applied to multiple fields.
999
1000
Returns an arrayref of field names for either OPAC or Staff client, with
1001
possible weights and subfield appended to each field name depending on the
1002
options provided.
1003
1004
=over 4
1005
1006
=item C<$params>
1007
1008
Hashref with options. The parameter C<is_opac> indicates whether the searchable
1009
fields for OPAC or Staff client should be retrieved. If C<weighted_fields> is set
1010
fields weights will be applied on returned fields. C<subfield> can be used to
1011
provide a subfield that will be appended to fields as "C<field_name>.C<subfield>".
1012
1013
=back
1014
1015
=cut
1016
1017
sub _search_fields {
1018
    my ($self, $params) = @_;
1019
    $params //= {
1020
        is_opac => 0,
1021
        weighted_fields => 0,
1022
        # This is a hack for authorities build_authorities_query
1023
        # can hopefully be removed in the future
1024
        subfield => undef,
1025
    };
1026
1027
    my $cache = Koha::Caches->get_instance();
1028
    my $cache_key = 'elasticsearch_search_fields' . ($params->{is_opac} ? '_opac' : '_staff_client');
1029
    my $search_fields = $cache->get_from_cache($cache_key, { unsafe => 1 });
1030
1031
    if (!$search_fields) {
1032
        # The reason we don't use Koha::SearchFields->search here is we don't
1033
        # want or need resultset wrapped as Koha::SearchField object.
1034
        # It does not make any sense in this context and would cause
1035
        # unnecessary overhead sice we are only querying for data
1036
        # Also would not work, or produce strange results, with the "columns"
1037
        # option.
1038
        my $schema = Koha::Database->schema;
1039
        my $result = $schema->resultset('SearchField')->search(
1040
            {
1041
                $params->{is_opac} ? (
1042
                    'opac' => 1,
1043
                ) : (
1044
                    'staff_client' => 1
1045
                ),
1046
                'search_marc_map.index_name' => $self->index,
1047
                'search_marc_map.marc_type' => C4::Context->preference('marcflavour'),
1048
                'search_marc_to_fields.search' => 1,
1049
            },
1050
            {
1051
                columns => [qw/name weight/],
1052
                collapse => 1,
1053
                join => {search_marc_to_fields => 'search_marc_map'},
1054
            }
1055
        );
1056
        my @search_fields;
1057
        while (my $search_field = $result->next) {
1058
            push @search_fields, [
1059
                $search_field->name,
1060
                $search_field->weight ? $search_field->weight : ()
1061
            ];
1062
        }
1063
        $search_fields = \@search_fields;
1064
        $cache->set_in_cache($cache_key, $search_fields);
1065
    }
1066
    if ($params->{subfield}) {
1067
        my $subfield = $params->{subfield};
1068
        $search_fields = [
1069
            map {
1070
                # Copy values to avoid mutating cached
1071
                # data (since unsafe is used)
1072
                my ($field, $weight) = @{$_};
1073
                ["${field}.${subfield}", $weight];
1074
            } @{$search_fields}
1075
        ];
1076
    }
1077
1078
    if ($params->{weighted_fields}) {
1079
        return [map { join('^', @{$_}) } @{$search_fields}];
1080
    }
1081
    else {
1082
        # Exclude weight from field
1083
        return [map { $_->[0] } @{$search_fields}];
1084
    }
1085
}
1086
948
1;
1087
1;
(-)a/Koha/SearchField.pm (-1 lines)
Lines 20-26 use Modern::Perl; Link Here
20
use Carp;
20
use Carp;
21
21
22
use Koha::Database;
22
use Koha::Database;
23
use Koha::SearchMarcMaps;
24
23
25
use base qw(Koha::Object);
24
use base qw(Koha::Object);
26
25
(-)a/Koha/SearchFields.pm (-15 lines)
Lines 35-55 Koha::SearchFields - Koha SearchField Object set class Link Here
35
35
36
=cut
36
=cut
37
37
38
=head3 weighted_fields
39
40
my (@w_fields, @weight) = Koha::SearchFields->weighted_fields();
41
42
=cut
43
44
sub weighted_fields {
45
    my ($self) = @_;
46
47
    return $self->search(
48
        { weight => { '>' => 0, '!=' => undef } },
49
        { order_by => { -desc => 'weight' } }
50
    );
51
}
52
53
=head3 type
38
=head3 type
54
39
55
=cut
40
=cut
(-)a/admin/searchengine/elasticsearch/field_config.yaml (-21 / +13 lines)
Lines 1-9 Link Here
1
---
1
---
2
# General field configuration
2
# General field configuration
3
general:
3
general:
4
  _all:
5
    type: string
6
    analyzer: analyser_standard
7
  properties:
4
  properties:
8
    marc_data:
5
    marc_data:
9
      store: true
6
      store: true
Lines 26-56 search: Link Here
26
    null_value: 0
23
    null_value: 0
27
  stdno:
24
  stdno:
28
    type: text
25
    type: text
29
    analyzer: analyser_stdno
26
    analyzer: analyzer_stdno
30
    search_analyzer: analyser_stdno
27
    search_analyzer: analyzer_stdno
31
    fields:
28
    fields:
32
      phrase:
29
      phrase:
33
        type: text
30
        type: text
34
        analyzer: analyser_stdno
31
        analyzer: analyzer_phrase
35
        search_analyzer: analyser_stdno
32
        search_analyzer: analyzer_phrase
36
      raw:
33
      raw:
37
        type: keyword
34
        type: keyword
38
    copy_to: _all
39
  default:
35
  default:
40
    type: text
36
    type: text
41
    analyzer: analyser_standard
37
    analyzer: analyzer_standard
42
    search_analyzer: analyser_standard
38
    search_analyzer: analyzer_standard
43
    fields:
39
    fields:
44
      phrase:
40
      phrase:
45
        type: text
41
        type: text
46
        analyzer: analyser_phrase
42
        analyzer: analyzer_phrase
47
        search_analyzer: analyser_phrase
43
        search_analyzer: analyzer_phrase
48
      raw:
44
      raw:
49
        type: keyword
45
        type: keyword
50
      lc_raw:
46
        normalizer: nfkc_cf_normalizer
47
      ci_raw:
51
        type: keyword
48
        type: keyword
52
        normalizer: my_normalizer
49
        normalizer: icu_folding_normalizer
53
    copy_to: _all
54
# Facets
50
# Facets
55
facet:
51
facet:
56
  default:
52
  default:
Lines 64-72 suggestible: Link Here
64
# Sort
60
# Sort
65
sort:
61
sort:
66
  default:
62
  default:
67
    type: text
63
    type: icu_collation_keyword
68
    analyzer: analyser_phrase
64
    index: false
69
    search_analyzer: analyser_phrase
70
    fields:
71
      phrase:
72
        type: keyword
(-)a/admin/searchengine/elasticsearch/index_config.yaml (-6 / +6 lines)
Lines 3-31 Link Here
3
index:
3
index:
4
  analysis:
4
  analysis:
5
    analyzer:
5
    analyzer:
6
      # Phrase analyzer is used for phrases (phrase match, sorting)
6
      # Phrase analyzer is used for phrases (exact phrase match)
7
      analyser_phrase:
7
      analyzer_phrase:
8
        tokenizer: keyword
8
        tokenizer: keyword
9
        filter:
9
        filter:
10
          - icu_folding
10
          - icu_folding
11
        char_filter:
11
        char_filter:
12
          - punctuation
12
          - punctuation
13
      analyser_standard:
13
      analyzer_standard:
14
        tokenizer: icu_tokenizer
14
        tokenizer: icu_tokenizer
15
        filter:
15
        filter:
16
          - icu_folding
16
          - icu_folding
17
      analyser_stdno:
17
      analyzer_stdno:
18
        tokenizer: whitespace
18
        tokenizer: whitespace
19
        filter:
19
        filter:
20
          - icu_folding
20
          - icu_folding
21
        char_filter:
21
        char_filter:
22
          - punctuation
22
          - punctuation
23
    normalizer:
23
    normalizer:
24
      normalizer_keyword:
24
      icu_folding_normalizer:
25
        type: custom
25
        type: custom
26
        filter:
26
        filter:
27
          - icu_folding
27
          - icu_folding
28
      my_normalizer:
28
      nfkc_cf_normalizer:
29
        type: custom
29
        type: custom
30
        char_filter: icu_normalizer
30
        char_filter: icu_normalizer
31
    char_filter:
31
    char_filter:
(-)a/admin/searchengine/elasticsearch/mappings.pl (-18 / +58 lines)
Lines 27-32 use Koha::SearchEngine::Elasticsearch; Link Here
27
use Koha::SearchEngine::Elasticsearch::Indexer;
27
use Koha::SearchEngine::Elasticsearch::Indexer;
28
use Koha::SearchMarcMaps;
28
use Koha::SearchMarcMaps;
29
use Koha::SearchFields;
29
use Koha::SearchFields;
30
use Koha::Caches;
30
31
31
use Try::Tiny;
32
use Try::Tiny;
32
33
Lines 68-73 my $update_mappings = sub { Link Here
68
    }
69
    }
69
};
70
};
70
71
72
my $cache = Koha::Caches->get_instance();
73
my $clear_cache = sub {
74
    $cache->clear_from_cache('elasticsearch_search_fields_staff_client');
75
    $cache->clear_from_cache('elasticsearch_search_fields_opac');
76
};
77
71
if ( $op eq 'edit' ) {
78
if ( $op eq 'edit' ) {
72
79
73
    $schema->storage->txn_begin;
80
    $schema->storage->txn_begin;
Lines 76-87 if ( $op eq 'edit' ) { Link Here
76
    my @field_label = $input->multi_param('search_field_label');
83
    my @field_label = $input->multi_param('search_field_label');
77
    my @field_type = $input->multi_param('search_field_type');
84
    my @field_type = $input->multi_param('search_field_type');
78
    my @field_weight = $input->multi_param('search_field_weight');
85
    my @field_weight = $input->multi_param('search_field_weight');
86
    my @field_staff_client = $input->multi_param('search_field_staff_client');
87
    my @field_opac = $input->multi_param('search_field_opac');
79
88
80
    my @index_name          = $input->multi_param('mapping_index_name');
89
    my @index_name          = $input->multi_param('mapping_index_name');
81
    my @search_field_name  = $input->multi_param('mapping_search_field_name');
90
    my @search_field_name   = $input->multi_param('mapping_search_field_name');
82
    my @mapping_sort        = $input->multi_param('mapping_sort');
91
    my @mapping_sort        = $input->multi_param('mapping_sort');
83
    my @mapping_facet       = $input->multi_param('mapping_facet');
92
    my @mapping_facet       = $input->multi_param('mapping_facet');
84
    my @mapping_suggestible = $input->multi_param('mapping_suggestible');
93
    my @mapping_suggestible = $input->multi_param('mapping_suggestible');
94
    my @mapping_search      = $input->multi_param('mapping_search');
85
    my @mapping_marc_field  = $input->multi_param('mapping_marc_field');
95
    my @mapping_marc_field  = $input->multi_param('mapping_marc_field');
86
    my @faceted_field_names = $input->multi_param('display_facet');
96
    my @faceted_field_names = $input->multi_param('display_facet');
87
97
Lines 92-97 if ( $op eq 'edit' ) { Link Here
92
            my $field_label = $field_label[$i];
102
            my $field_label = $field_label[$i];
93
            my $field_type = $field_type[$i];
103
            my $field_type = $field_type[$i];
94
            my $field_weight = $field_weight[$i];
104
            my $field_weight = $field_weight[$i];
105
            my $field_staff_client = $field_staff_client[$i];
106
            my $field_opac = $field_opac[$i];
95
107
96
            my $search_field = Koha::SearchFields->find( { name => $field_name }, { key => 'name' } );
108
            my $search_field = Koha::SearchFields->find( { name => $field_name }, { key => 'name' } );
97
            $search_field->label($field_label);
109
            $search_field->label($field_label);
Lines 106-111 if ( $op eq 'edit' ) { Link Here
106
            else {
118
            else {
107
                $search_field->weight($field_weight);
119
                $search_field->weight($field_weight);
108
            }
120
            }
121
            $search_field->staff_client($field_staff_client ? 1 : 0);
122
            $search_field->opac($field_opac ? 1 : 0);
109
123
110
            my $facet_order = first { $faceted_field_names[$_] eq $field_name } 0 .. $#faceted_field_names;
124
            my $facet_order = first { $faceted_field_names[$_] eq $field_name } 0 .. $#faceted_field_names;
111
            $search_field->facet_order(defined $facet_order ? $facet_order + 1 : undef);
125
            $search_field->facet_order(defined $facet_order ? $facet_order + 1 : undef);
Lines 118-136 if ( $op eq 'edit' ) { Link Here
118
132
119
        for my $i ( 0 .. scalar(@index_name) - 1 ) {
133
        for my $i ( 0 .. scalar(@index_name) - 1 ) {
120
            my $index_name          = $index_name[$i];
134
            my $index_name          = $index_name[$i];
121
            my $search_field_name  = $search_field_name[$i];
135
            my $search_field_name   = $search_field_name[$i];
122
            my $mapping_marc_field  = $mapping_marc_field[$i];
136
            my $mapping_marc_field  = $mapping_marc_field[$i];
123
            my $mapping_facet       = $mapping_facet[$i];
137
            my $mapping_facet       = $mapping_facet[$i];
124
            my $mapping_suggestible = $mapping_suggestible[$i];
125
            my $mapping_sort        = $mapping_sort[$i];
126
            $mapping_sort = undef if $mapping_sort eq 'undef';
127
            $mapping_facet = ( grep {/^$search_field_name$/} @facetable_field_names ) ? $mapping_facet : 0;
138
            $mapping_facet = ( grep {/^$search_field_name$/} @facetable_field_names ) ? $mapping_facet : 0;
139
            my $mapping_suggestible = $mapping_suggestible[$i];
140
            my $mapping_sort        = $mapping_sort[$i] eq 'undef' ? undef : $mapping_sort[$i];
141
            my $mapping_search      = $mapping_search[$i];
128
142
129
            my $search_field = Koha::SearchFields->find({ name => $search_field_name }, { key => 'name' });
143
            my $search_field = Koha::SearchFields->find({ name => $search_field_name }, { key => 'name' });
130
            # TODO Check mapping format
144
            # TODO Check mapping format
131
            my $marc_field = Koha::SearchMarcMaps->find_or_create({ index_name => $index_name, marc_type => $marc_type, marc_field => $mapping_marc_field });
145
            my $marc_field = Koha::SearchMarcMaps->find_or_create({
132
            $search_field->add_to_search_marc_maps($marc_field, { facet => $mapping_facet, suggestible => $mapping_suggestible, sort => $mapping_sort } );
146
                index_name => $index_name,
133
147
                marc_type => $marc_type,
148
                marc_field => $mapping_marc_field
149
            });
150
            $search_field->add_to_search_marc_maps($marc_field, {
151
                facet => $mapping_facet,
152
                suggestible => $mapping_suggestible,
153
                sort => $mapping_sort,
154
                search => $mapping_search
155
            });
134
        }
156
        }
135
    };
157
    };
136
    if ($@) {
158
    if ($@) {
Lines 139-144 if ( $op eq 'edit' ) { Link Here
139
    } else {
161
    } else {
140
        push @messages, { type => 'message', code => 'success_on_update' };
162
        push @messages, { type => 'message', code => 'success_on_update' };
141
        $schema->storage->txn_commit;
163
        $schema->storage->txn_commit;
164
        $clear_cache->();
142
        $update_mappings->();
165
        $update_mappings->();
143
    }
166
    }
144
}
167
}
Lines 146-158 elsif( $op eq 'reset_confirmed' ) { Link Here
146
    Koha::SearchMarcMaps->delete;
169
    Koha::SearchMarcMaps->delete;
147
    Koha::SearchFields->delete;
170
    Koha::SearchFields->delete;
148
    Koha::SearchEngine::Elasticsearch->reset_elasticsearch_mappings;
171
    Koha::SearchEngine::Elasticsearch->reset_elasticsearch_mappings;
172
    $clear_cache->();
149
    push @messages, { type => 'message', code => 'success_on_reset' };
173
    push @messages, { type => 'message', code => 'success_on_reset' };
150
}
174
}
151
elsif( $op eq 'reset_confirm' ) {
175
elsif( $op eq 'reset_confirm' ) {
152
    $template->param( reset_confirm => 1 );
176
    $template->param( reset_confirm => 1 );
153
}
177
}
154
178
155
156
my @indexes;
179
my @indexes;
157
180
158
for my $index_name (@index_names) {
181
for my $index_name (@index_names) {
Lines 179-207 for my $index_name (@index_names) { Link Here
179
my @facetable_fields = Koha::SearchEngine::Elasticsearch->get_facetable_fields();
202
my @facetable_fields = Koha::SearchEngine::Elasticsearch->get_facetable_fields();
180
for my $index_name (@index_names) {
203
for my $index_name (@index_names) {
181
    my $search_fields = Koha::SearchFields->search(
204
    my $search_fields = Koha::SearchFields->search(
182
        { 'search_marc_map.index_name' => $index_name, 'search_marc_map.marc_type' => $marc_type, },
205
        {
183
        {   join => { search_marc_to_fields => 'search_marc_map' },
206
            'search_marc_map.index_name' => $index_name,
184
            '+select' => [ 'search_marc_to_fields.facet', 'search_marc_to_fields.suggestible', 'search_marc_to_fields.sort', 'search_marc_map.marc_field' ],
207
            'search_marc_map.marc_type' => $marc_type,
185
            '+as'     => [ 'facet',                       'suggestible',                       'sort',                       'marc_field' ],
208
        },
209
        {
210
            join => { search_marc_to_fields => 'search_marc_map' },
211
            '+select' => [
212
                'search_marc_to_fields.facet',
213
                'search_marc_to_fields.suggestible',
214
                'search_marc_to_fields.sort',
215
                'search_marc_to_fields.search',
216
                'search_marc_map.marc_field'
217
            ],
218
            '+as' => [
219
                'facet',
220
                'suggestible',
221
                'sort',
222
                'search',
223
                'marc_field'
224
            ],
186
            order_by => { -asc => [qw/name marc_field/] }
225
            order_by => { -asc => [qw/name marc_field/] }
187
        }
226
         }
188
    );
227
     );
189
228
190
    my @mappings;
229
    my @mappings;
191
    my @facetable_field_names = map { $_->name } @facetable_fields;
230
    my @facetable_field_names = map { $_->name } @facetable_fields;
192
231
193
    while ( my $s = $search_fields->next ) {
232
    while ( my $s = $search_fields->next ) {
194
        my $name = $s->name;
233
        my $name = $s->name;
195
        push @mappings,
234
        push @mappings, {
196
          { search_field_name  => $name,
235
            search_field_name  => $name,
197
            search_field_label => $s->label,
236
            search_field_label => $s->label,
198
            search_field_type  => $s->type,
237
            search_field_type  => $s->type,
199
            marc_field         => $s->get_column('marc_field'),
238
            marc_field         => $s->get_column('marc_field'),
200
            sort               => $s->get_column('sort') // 'undef', # To avoid warnings "Use of uninitialized value in lc"
239
            sort               => $s->get_column('sort') // 'undef', # To avoid warnings "Use of uninitialized value in lc"
201
            suggestible        => $s->get_column('suggestible'),
240
            suggestible        => $s->get_column('suggestible'),
241
            search             => $s->get_column('search'),
202
            facet              => $s->get_column('facet'),
242
            facet              => $s->get_column('facet'),
203
            is_facetable       => ( grep {/^$name$/} @facetable_field_names ) ? 1 : 0,
243
            is_facetable       => ( grep {/^$name$/} @facetable_field_names ) ? 1 : 0,
204
          };
244
        };
205
    }
245
    }
206
246
207
    push @indexes, { index_name => $index_name, mappings => \@mappings };
247
    push @indexes, { index_name => $index_name, mappings => \@mappings };
(-)a/catalogue/search.pl (-6 / +1 lines)
Lines 464-474 my $expanded_facet = $params->{'expand'}; Link Here
464
# Define some global variables
464
# Define some global variables
465
my ( $error,$query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type);
465
my ( $error,$query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type);
466
466
467
my $build_params;
468
unless ( $cgi->param('advsearch') ) {
469
    $build_params->{weighted_fields} = 1;
470
}
471
472
my $builder = Koha::SearchEngine::QueryBuilder->new(
467
my $builder = Koha::SearchEngine::QueryBuilder->new(
473
    { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
468
    { index => $Koha::SearchEngine::BIBLIOS_INDEX } );
474
my $searcher = Koha::SearchEngine::Search->new(
469
my $searcher = Koha::SearchEngine::Search->new(
Lines 481-487 my $searcher = Koha::SearchEngine::Search->new( Link Here
481
    $query_type
476
    $query_type
482
  )
477
  )
483
  = $builder->build_query_compat( \@operators, \@operands, \@indexes, \@limits,
478
  = $builder->build_query_compat( \@operators, \@operands, \@indexes, \@limits,
484
    \@sort_by, $scan, $lang, $build_params );
479
    \@sort_by, $scan, $lang, { weighted_fields => !$cgi->param('advsearch') });
485
480
486
## parse the query_cgi string and put it into a form suitable for <input>s
481
## parse the query_cgi string and put it into a form suitable for <input>s
487
my @query_inputs;
482
my @query_inputs;
(-)a/installer/data/mysql/atomicupdate/bug_20589_add_columns.perl (+14 lines)
Line 0 Link Here
1
$DBversion = 'XXX';
2
if (CheckVersion($DBversion)) {
3
  if (!column_exists('search_marc_to_field', 'search')) {
4
    $dbh->do("ALTER TABLE `search_marc_to_field` ADD COLUMN `search` tinyint(1) NOT NULL DEFAULT 1");
5
  }
6
  if (!column_exists('search_field', 'staff_client')) {
7
    $dbh->do("ALTER TABLE `search_field` ADD COLUMN `staff_client` tinyint(1) NOT NULL DEFAULT 1");
8
  }
9
  if (!column_exists('search_field', 'opac')) {
10
    $dbh->do("ALTER TABLE `search_field` ADD COLUMN `opac` tinyint(1) NOT NULL DEFAULT 1");
11
  }
12
  SetVersion($DBversion)
13
}
14
print "Upgrade to $DBversion done (Bug 20589 - Add field boosting and use elastic query fields parameter instead of depricated _all)\n";
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/searchengine/elasticsearch/mappings.tt (-6 / +60 lines)
Lines 160-167 a.add, a.delete { Link Here
160
                    <th>Name</th>
160
                    <th>Name</th>
161
                    <th>Label</th>
161
                    <th>Label</th>
162
                    <th>Type</th>
162
                    <th>Type</th>
163
                    <th colspan="2">Searchable</th>
163
                    <th>Weight</th>
164
                    <th>Weight</th>
164
                  </tr>
165
                  </tr>
166
                  <tr>
167
                    <th colspan=3>&nbsp;</th>
168
                    <th>Staff client</th>
169
                    <th>OPAC</th>
170
                    <th>&nbsp;</th>
171
                  </tr>
165
                </thead>
172
                </thead>
166
                <tbody>
173
                <tbody>
167
                  [% FOREACH search_field IN all_search_fields %]
174
                  [% FOREACH search_field IN all_search_fields %]
Lines 169-175 a.add, a.delete { Link Here
169
                      <td>
176
                      <td>
170
                        <input type="text" name="search_field_name" value="[% search_field.name | html %]" />
177
                        <input type="text" name="search_field_name" value="[% search_field.name | html %]" />
171
                      </td>
178
                      </td>
172
                      <td><input type="text" name="search_field_label" value="[% search_field.label | html %]" />
179
                      <td>
180
                        <input type="text" name="search_field_label" value="[% search_field.label | html %]" />
181
                      </td>
173
                      <td>
182
                      <td>
174
                        <select name="search_field_type">
183
                        <select name="search_field_type">
175
                          <option value=""></option>
184
                          <option value=""></option>
Lines 211-221 a.add, a.delete { Link Here
211
                        </select>
220
                        </select>
212
                      </td>
221
                      </td>
213
                      <td>
222
                      <td>
214
                      [% IF search_field.mapped_biblios %]
223
                        <select name="search_field_staff_client">
215
                        <input type="number" step="0.01" min="0.01" max="999.99" name="search_field_weight" value="[% search_field.weight | html %]" />
224
                          [% IF search_field.staff_client %]
216
                      [% ELSE %]
225
                            <option value="1" selected="selected">Yes</option>
217
                        <input type="hidden" name="search_field_weight" value="">
226
                            <option value="0">No</option>
218
                      [% END %]
227
                          [% ELSE %]
228
                            <option value="1">Yes</option>
229
                            <option value="0" selected="selected">No</option>
230
                          [% END %]
231
                        </select>
232
                      </td>
233
                      <td>
234
                        <select name="search_field_opac">
235
                          [% IF search_field.opac %]
236
                            <option value="1" selected="selected">Yes</option>
237
                            <option value="0">No</option>
238
                          [% ELSE %]
239
                            <option value="1">Yes</option>
240
                            <option value="0" selected="selected">No</option>
241
                          [% END %]
242
                        </select>
243
                      </td>
244
                      <td>
245
                        [% IF search_field.mapped_biblios %]
246
                            <input type="number" step="0.01" min="0.01" max="999.99" name="search_field_weight" value="[% search_field.weight | html %]" />
247
                        [% ELSE %]
248
                            <input type="hidden" name="search_field_weight" value="">
249
                        [% END %]
219
                      </td>
250
                      </td>
220
                    </tr>
251
                    </tr>
221
                  [% END %]
252
                  [% END %]
Lines 231-236 a.add, a.delete { Link Here
231
                          <th>Sortable</th>
262
                          <th>Sortable</th>
232
                          <th>Facetable</th>
263
                          <th>Facetable</th>
233
                          <th>Suggestible</th>
264
                          <th>Suggestible</th>
265
                          <th>Searchable</th>
234
                          <th>Mapping</th>
266
                          <th>Mapping</th>
235
                          <th></th>
267
                          <th></th>
236
                        </tr>
268
                        </tr>
Lines 290-295 a.add, a.delete { Link Here
290
                              </select>
322
                              </select>
291
                            </td>
323
                            </td>
292
                            <td>
324
                            <td>
325
                              <select name="mapping_search">
326
                                [% IF mapping.search %]
327
                                  <option value="0">No</option>
328
                                  <option value="1" selected="selected">Yes</option>
329
                                [% ELSE %]
330
                                  <option value="0" selected="selected">No</option>
331
                                  <option value="1">Yes</option>
332
                                [% END %]
333
                              </select>
334
                            </td>
335
                            <td>
293
                                <input name="mapping_marc_field" type="text" value="[% mapping.marc_field | html %]" />
336
                                <input name="mapping_marc_field" type="text" value="[% mapping.marc_field | html %]" />
294
                            </td>
337
                            </td>
295
                            <td><a class="btn btn-default btn-xs delete" style="cursor: pointer;"><i class="fa fa-trash"></i> Delete</a></td>
338
                            <td><a class="btn btn-default btn-xs delete" style="cursor: pointer;"><i class="fa fa-trash"></i> Delete</a></td>
Lines 335-340 a.add, a.delete { Link Here
335
                              [% END %]
378
                              [% END %]
336
                            </select>
379
                            </select>
337
                          </td>
380
                          </td>
381
                          <td>
382
                            <select data-id="mapping_search">
383
                              [% IF mapping.search %]
384
                                <option value="0">No</option>
385
                                <option value="1" selected="selected">Yes</option>
386
                              [% ELSE %]
387
                                <option value="0" selected="selected">No</option>
388
                                <option value="1">Yes</option>
389
                              [% END %]
390
                            </select>
391
                          </td>
338
                          <td><input data-id="mapping_marc_field" type="text" /></td>
392
                          <td><input data-id="mapping_marc_field" type="text" /></td>
339
                          <td><a class="btn btn-default btn-xs add"><i class="fa fa-plus"></i> Add</a></td>
393
                          <td><a class="btn btn-default btn-xs add"><i class="fa fa-plus"></i> Add</a></td>
340
                        </tr>
394
                        </tr>
(-)a/opac/opac-search.pl (-11 / +15 lines)
Lines 549-567 if (C4::Context->preference('OpacSuppression')) { Link Here
549
    }
549
    }
550
}
550
}
551
551
552
my $build_params = {
553
    expanded_facet => $expanded_facet,
554
    suppress => $suppress
555
};
556
557
unless ( $cgi->param('advsearch') ) {
558
    $build_params->{weighted_fields} = 1;
559
}
560
561
## I. BUILD THE QUERY
552
## I. BUILD THE QUERY
562
( $error,$query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type)
553
( $error,$query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$query_type)
563
  = $builder->build_query_compat( \@operators, \@operands,
554
  = $builder->build_query_compat(
564
    \@indexes, \@limits, \@sort_by, 0, $lang, $build_params);
555
    \@operators,
556
    \@operands,
557
    \@indexes,
558
    \@limits,
559
    \@sort_by,
560
    0,
561
    $lang,
562
    {
563
        expanded_facet => $expanded_facet,
564
        suppress => $suppress,
565
        is_opac => 1,
566
        weighted_fields => !$cgi->param('advsearch')
567
    }
568
);
565
569
566
sub _input_cgi_parse {
570
sub _input_cgi_parse {
567
    my @elements;
571
    my @elements;
(-)a/t/db_dependent/Koha/SearchEngine/Elasticsearch/QueryBuilder.t (-22 / +41 lines)
Lines 23-28 use t::lib::Mocks; Link Here
23
use t::lib::TestBuilder;
23
use t::lib::TestBuilder;
24
use Test::More tests => 6;
24
use Test::More tests => 6;
25
25
26
use List::Util qw( all );
27
26
use Koha::Database;
28
use Koha::Database;
27
use Koha::SearchEngine::Elasticsearch::QueryBuilder;
29
use Koha::SearchEngine::Elasticsearch::QueryBuilder;
28
30
Lines 81-88 $se->mock( 'get_elasticsearch_mappings', sub { Link Here
81
    return $all_mappings{$self->index};
83
    return $all_mappings{$self->index};
82
});
84
});
83
85
86
my $cache = Koha::Caches->get_instance();
87
my $clear_search_fields_cache = sub {
88
    $cache->clear_from_cache('elasticsearch_search_fields_staff_client');
89
    $cache->clear_from_cache('elasticsearch_search_fields_opac');
90
};
91
84
subtest 'build_authorities_query_compat() tests' => sub {
92
subtest 'build_authorities_query_compat() tests' => sub {
85
    plan tests => 36;
93
    plan tests => 46;
86
94
87
    my $qb;
95
    my $qb;
88
96
Lines 107-140 subtest 'build_authorities_query_compat() tests' => sub { Link Here
107
    $search_term = 'Donald Duck';
115
    $search_term = 'Donald Duck';
108
    foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
116
    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' );
117
        my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['contains'], [$search_term], 'AUTH_TYPE', 'asc' );
118
        is( $query->{query}->{bool}->{must}[0]->{query_string}->{query}, "(Donald*) AND (Duck*)" );
110
        if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
119
        if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
111
            is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
120
            isa_ok( $query->{query}->{bool}->{must}[0]->{query_string}->{fields}, 'ARRAY')
112
                "(Donald*) AND (Duck*)");
113
        } else {
121
        } else {
114
            is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
122
            is( $query->{query}->{bool}->{must}[0]->{query_string}->{default_field}, $koha_to_index_name->{$koha_name} );
115
                "(Donald*) AND (Duck*)");
116
        }
123
        }
117
    }
124
    }
118
125
119
    foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
126
    foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
120
        my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['is'], [$search_term], 'AUTH_TYPE', 'asc' );
127
        my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['is'], [$search_term], 'AUTH_TYPE', 'asc' );
121
        if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
128
        if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
122
            is( $query->{query}->{bool}->{must}[0]->{match_phrase}->{"_all.phrase"},
129
            is(
123
                "donald duck");
130
                $query->{query}->{bool}->{must}[0]->{multi_match}->{query},
131
                "Donald Duck"
132
            );
133
            my $all_matches = all { /\.ci_raw$/ }
134
                @{$query->{query}->{bool}->{must}[0]->{multi_match}->{fields}};
135
            ok( $all_matches, 'Correct fields parameter for "is" query in "any" or "all"' );
124
        } else {
136
        } else {
125
            is( $query->{query}->{bool}->{must}[0]->{match_phrase}->{$koha_to_index_name->{$koha_name}.".phrase"},
137
            is(
126
                "donald duck");
138
                $query->{query}->{bool}->{must}[0]->{term}->{$koha_to_index_name->{$koha_name} . ".ci_raw"},
139
                "Donald Duck"
140
            );
127
        }
141
        }
128
    }
142
    }
129
143
130
    foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
144
    foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
131
        my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'asc' );
145
        my $query = $qb->build_authorities_query_compat( [ $koha_name ],  undef, undef, ['start'], [$search_term], 'AUTH_TYPE', 'asc' );
132
        if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
146
        if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
133
            is( $query->{query}->{bool}->{must}[0]->{match_phrase_prefix}->{"_all.phrase"},
147
            my $all_matches = all { (%{$_->{prefix}})[0] =~ /\.ci_raw$/ && (%{$_->{prefix}})[1] eq "Donald Duck" }
134
                "donald duck");
148
                @{$query->{query}->{bool}->{must}[0]->{bool}->{should}};
149
            ok( $all_matches, "Correct multiple prefix query" );
135
        } else {
150
        } else {
136
            is( $query->{query}->{bool}->{must}[0]->{match_phrase_prefix}->{$koha_to_index_name->{$koha_name}.".phrase"},
151
            is( $query->{query}->{bool}->{must}[0]->{prefix}->{$koha_to_index_name->{$koha_name} . ".ci_raw"}, "Donald Duck" );
137
                "donald duck");
138
        }
152
        }
139
    }
153
    }
140
154
Lines 370-388 subtest 'build query from form subtests' => sub { Link Here
370
};
384
};
371
385
372
subtest 'build_query with weighted fields tests' => sub {
386
subtest 'build_query with weighted fields tests' => sub {
373
    plan tests => 4;
387
    plan tests => 2;
374
388
375
    my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'mydb' } );
389
    my $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'mydb' } );
376
    my $db_builder = t::lib::TestBuilder->new();
390
    my $db_builder = t::lib::TestBuilder->new();
377
391
378
    Koha::SearchFields->search({})->delete;
392
    Koha::SearchFields->search({})->delete;
393
    $clear_search_fields_cache->();
379
394
380
    $db_builder->build({
395
    $db_builder->build({
381
        source => 'SearchField',
396
        source => 'SearchField',
382
        value => {
397
        value => {
383
            name    => 'acqdate',
398
            name    => 'acqdate',
384
            label   => 'acqdate',
399
            label   => 'acqdate',
385
            weight  => undef
400
            weight  => undef,
401
            staff_client => 1
386
        }
402
        }
387
    });
403
    });
388
404
Lines 391-397 subtest 'build_query with weighted fields tests' => sub { Link Here
391
        value => {
407
        value => {
392
            name    => 'title',
408
            name    => 'title',
393
            label   => 'title',
409
            label   => 'title',
394
            weight  => 25
410
            weight  => 25,
411
            staff_client => 1
395
        }
412
        }
396
    });
413
    });
397
414
Lines 400-406 subtest 'build_query with weighted fields tests' => sub { Link Here
400
        value => {
417
        value => {
401
            name    => 'subject',
418
            name    => 'subject',
402
            label   => 'subject',
419
            label   => 'subject',
403
            weight  => 15
420
            weight  => 15,
421
            staff_client => 1
404
        }
422
        }
405
    });
423
    });
406
424
Lines 408-417 subtest 'build_query with weighted fields tests' => sub { Link Here
408
    undef, undef, undef, { weighted_fields => 1 });
426
    undef, undef, undef, { weighted_fields => 1 });
409
427
410
    my $fields = $query->{query}{query_string}{fields};
428
    my $fields = $query->{query}{query_string}{fields};
411
    is(scalar(@$fields), 3, 'Search is done on 3 fields');
429
412
    is($fields->[0], '_all', 'First search field is _all');
430
    my @found = grep { $_ eq 'title^25.00' } @{$fields};
413
    is($fields->[1], 'title^25.00', 'Second search field is title');
431
    is(@found, 1, 'Search field is title has correct weight'); # Fails
414
    is($fields->[2], 'subject^15.00', 'Third search field is subject');
432
433
    @found = grep { $_ eq 'subject^15.00' } @{$fields};
434
    is(@found, 1, 'Search field subject has correct weight'); # Fails
415
};
435
};
416
436
417
subtest "_convert_sort_fields() tests" => sub {
437
subtest "_convert_sort_fields() tests" => sub {
418
- 

Return to bug 20589