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

(-)a/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm (-14 / +36 lines)
Lines 209-219 sub build_query_compat { Link Here
209
    # each search thing is a handy unit.
209
    # each search thing is a handy unit.
210
    unshift @$operators, undef;    # The first one can't have an op
210
    unshift @$operators, undef;    # The first one can't have an op
211
    my @search_params;
211
    my @search_params;
212
    my $truncate = C4::Context->preference("QueryAutoTruncate") || 0;
212
    my $ea = each_array( @$operands, @$operators, @index_params );
213
    my $ea = each_array( @$operands, @$operators, @index_params );
213
    while ( my ( $oand, $otor, $index ) = $ea->() ) {
214
    while ( my ( $oand, $otor, $index ) = $ea->() ) {
214
        next if ( !defined($oand) || $oand eq '' );
215
        next if ( !defined($oand) || $oand eq '' );
216
        $oand = $self->_clean_search_term($oand);
217
        $oand = $self->_truncate_terms($oand) if ($truncate);
215
        push @search_params, {
218
        push @search_params, {
216
            operand => $self->_clean_search_term($oand),      # the search terms
219
            operand => $oand,      # the search terms
217
            operator => defined($otor) ? uc $otor : undef,    # AND and so on
220
            operator => defined($otor) ? uc $otor : undef,    # AND and so on
218
            $index ? %$index : (),
221
            $index ? %$index : (),
219
        };
222
        };
Lines 314-324 sub build_authorities_query { Link Here
314
        }
317
        }
315
        else {
318
        else {
316
            # regular wordlist stuff
319
            # regular wordlist stuff
317
#            push @query_parts, { match => {$wh => { query => $val, operator => 'and' }} };
320
            my @tokens = $self->_split_query( $val );
318
            my @values = split(' ',$val);
321
            foreach my $token ( @tokens ) {
319
            foreach my $v (@values) {
322
                $token = $self->_truncate_terms(
320
                push @query_parts, { wildcard => { "$wh.phrase" => "*" . lc $v . "*" } };
323
                    $self->_clean_search_term( $token )
324
                );
321
            }
325
            }
326
            my $query = $self->_join_queries( @tokens );
327
            push @query_parts, { query_string => { default_field => $wh, query => $query } };
322
        }
328
        }
323
    }
329
    }
324
330
Lines 714-727 to ensure those parts are correct. Link Here
714
sub _clean_search_term {
720
sub _clean_search_term {
715
    my ( $self, $term ) = @_;
721
    my ( $self, $term ) = @_;
716
722
717
    my $auto_truncation = C4::Context->preference("QueryAutoTruncate") || 0;
718
719
    # Some hardcoded searches (like with authorities) produce things like
723
    # Some hardcoded searches (like with authorities) produce things like
720
    # 'an=123', when it ought to be 'an:123' for our purposes.
724
    # 'an=123', when it ought to be 'an:123' for our purposes.
721
    $term =~ s/=/:/g;
725
    $term =~ s/=/:/g;
722
    $term = $self->_convert_index_strings_freeform($term);
726
    $term = $self->_convert_index_strings_freeform($term);
723
    $term =~ s/[{}]/"/g;
727
    $term =~ s/[{}]/"/g;
724
    $term = $self->_truncate_terms($term) if ($auto_truncation);
725
    return $term;
728
    return $term;
726
}
729
}
727
730
Lines 805-825 operands and double quoted strings. Link Here
805
sub _truncate_terms {
808
sub _truncate_terms {
806
    my ( $self, $query ) = @_;
809
    my ( $self, $query ) = @_;
807
810
808
    # '"donald duck" title:"the mouse" and peter" get split into
811
    my @tokens = $self->_split_query( $query );
809
    # ['', '"donald duck"', '', ' ', '', 'title:"the mouse"', '', ' ', 'and', ' ', 'pete']
810
    my @tokens = split /((?:[\w\-.]+:)?"[^"]+"|\s+)/, $query;
811
812
812
    # Filter out empty tokens
813
    # Filter out empty tokens
813
    my @words = grep { $_ !~ /^\s*$/ } @tokens;
814
    my @words = grep { $_ !~ /^\s*$/ } @tokens;
814
815
815
    # Append '*' to words if needed, ie. if it's not surrounded by quotes, not
816
    # Append '*' to words if needed, ie. if it ends in a word character and is not a keyword
816
    # terminated by '*' and not a keyword
817
    my @terms = map {
817
    my @terms = map {
818
        my $w = $_;
818
        my $w = $_;
819
        (/"$/ or /\*$/ or grep {lc($w) eq $_} qw/and or not/) ? $_ : "$_*";
819
        (/\W$/ or grep {lc($w) eq $_} qw/and or not/) ? $_ : "$_*";
820
    } @words;
820
    } @words;
821
821
822
    return join ' ', @terms;
822
    return join ' ', @terms;
823
}
823
}
824
824
825
=head2 _split_query
826
827
    my @token = $self->_split_query($query_str);
828
829
Given a string query this function splits it to tokens taking into account
830
any field prefixes and quoted strings.
831
832
=cut
833
834
sub _split_query {
835
    my ( $self, $query ) = @_;
836
837
    # '"donald duck" title:"the mouse" and peter" get split into
838
    # ['', '"donald duck"', '', ' ', '', 'title:"the mouse"', '', ' ', 'and', ' ', 'pete']
839
    my @tokens = split /((?:[\w\-.]+:)?"[^"]+"|\s+)/, $query;
840
841
    # Filter out empty values
842
    @tokens = grep( /\S/, @tokens );
843
844
    return @tokens;
845
}
846
825
1;
847
1;
(-)a/t/Koha/SearchEngine/Elasticsearch/QueryBuilder.t (+86 lines)
Line 0 Link Here
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 Test::More tests => 2;
21
22
use Koha::SearchEngine::Elasticsearch::QueryBuilder;
23
24
subtest '_truncate_terms() tests' => sub {
25
    plan tests => 7;
26
27
    my $qb;
28
    ok(
29
        $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX }),
30
        'Creating a new QueryBuilder object'
31
    );
32
33
    my $res = $qb->_truncate_terms('donald');
34
    is_deeply($res, 'donald*', 'single search term returned correctly');
35
36
    $res = $qb->_truncate_terms('donald duck');
37
    is_deeply($res, 'donald* duck*', 'two search terms returned correctly');
38
39
    $res = $qb->_truncate_terms(' donald   duck ');
40
    is_deeply($res, 'donald* duck*', 'two search terms surrounded by spaces returned correctly');
41
42
    $res = $qb->_truncate_terms('"donald duck"');
43
    is_deeply($res, '"donald duck"', 'quoted search term returned correctly');
44
45
    $res = $qb->_truncate_terms('"donald, duck"');
46
    is_deeply($res, '"donald, duck"', 'quoted search term with comma returned correctly');
47
48
    $res = $qb->_truncate_terms(' "donald   duck" ');
49
    is_deeply($res, '"donald   duck"', 'quoted search terms surrounded by spaces correctly');
50
};
51
52
subtest '_split_query() tests' => sub {
53
    plan tests => 7;
54
55
    my $qb;
56
    ok(
57
        $qb = Koha::SearchEngine::Elasticsearch::QueryBuilder->new({ 'index' => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX }),
58
        'Creating a new QueryBuilder object'
59
    );
60
61
    my @res = $qb->_split_query('donald');
62
    my @exp = 'donald';
63
    is_deeply(\@res, \@exp, 'single search term returned correctly');
64
65
    @res = $qb->_split_query('donald duck');
66
    @exp = ('donald', 'duck');
67
    is_deeply(\@res, \@exp, 'two search terms returned correctly');
68
69
    @res = $qb->_split_query(' donald   duck ');
70
    @exp = ('donald', 'duck');
71
    is_deeply(\@res, \@exp, 'two search terms surrounded by spaces returned correctly');
72
73
    @res = $qb->_split_query('"donald duck"');
74
    @exp = ( '"donald duck"' );
75
    is_deeply(\@res, \@exp, 'quoted search term returned correctly');
76
77
    @res = $qb->_split_query('"donald, duck"');
78
    @exp = ( '"donald, duck"' );
79
    is_deeply(\@res, \@exp, 'quoted search term with comma returned correctly');
80
81
    @res = $qb->_split_query(' "donald   duck" ');
82
    @exp = ( '"donald   duck"' );
83
    is_deeply(\@res, \@exp, 'quoted search terms surrounded by spaces correctly');
84
};
85
86
1;
(-)a/t/db_dependent/Koha/SearchEngine/Elasticsearch/QueryBuilder.t (-17 / +12 lines)
Lines 82-88 $se->mock( 'get_elasticsearch_mappings', sub { Link Here
82
});
82
});
83
83
84
subtest 'build_authorities_query_compat() tests' => sub {
84
subtest 'build_authorities_query_compat() tests' => sub {
85
    plan tests => 44;
85
    plan tests => 36;
86
86
87
    my $qb;
87
    my $qb;
88
88
Lines 96-106 subtest 'build_authorities_query_compat() tests' => sub { Link Here
96
    foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
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' );
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' ) {
98
        if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
99
            is( $query->{query}->{bool}->{must}[0]->{wildcard}->{"_all.phrase"},
99
            is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
100
                "*a*");
100
                "a*");
101
        } else {
101
        } else {
102
            is( $query->{query}->{bool}->{must}[0]->{wildcard}->{$koha_to_index_name->{$koha_name}.".phrase"},
102
            is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
103
                "*a*");
103
                "a*");
104
        }
104
        }
105
    }
105
    }
106
106
Lines 108-122 subtest 'build_authorities_query_compat() tests' => sub { Link Here
108
    foreach my $koha_name ( keys %{ $koha_to_index_name } ) {
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' );
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' ) {
110
        if ( $koha_name eq 'all' || $koha_name eq 'any' ) {
111
            is( $query->{query}->{bool}->{must}[0]->{wildcard}->{"_all.phrase"},
111
            is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
112
                "*donald*");
112
                "(Donald*) AND (Duck*)");
113
            is( $query->{query}->{bool}->{must}[1]->{wildcard}->{"_all.phrase"},
114
                "*duck*");
115
        } else {
113
        } else {
116
            is( $query->{query}->{bool}->{must}[0]->{wildcard}->{$koha_to_index_name->{$koha_name}.".phrase"},
114
            is( $query->{query}->{bool}->{must}[0]->{query_string}->{query},
117
                "*donald*");
115
                "(Donald*) AND (Duck*)");
118
            is( $query->{query}->{bool}->{must}[1]->{wildcard}->{$koha_to_index_name->{$koha_name}.".phrase"},
119
                "*duck*");
120
        }
116
        }
121
    }
117
    }
122
118
Lines 360-373 subtest 'build query from form subtests' => sub { Link Here
360
356
361
    my $query = $qb->build_authorities_query_compat( \@marclist, undef,
357
    my $query = $qb->build_authorities_query_compat( \@marclist, undef,
362
                    undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
358
                    undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
363
    is($query->{query}->{bool}->{must}[0]->{wildcard}->{'Heading.phrase'}, "*hamilton*","Expected search is populated");
359
    is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","Expected search is populated");
364
    is( scalar @{ $query->{query}->{bool}->{must} }, 1,"Only defined search is populated");
360
    is( scalar @{ $query->{query}->{bool}->{must} }, 1,"Only defined search is populated");
365
361
366
    @values[2] = 'Jefferson';
362
    @values[2] = 'Jefferson';
367
    $query = $qb->build_authorities_query_compat( \@marclist, undef,
363
    $query = $qb->build_authorities_query_compat( \@marclist, undef,
368
                    undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
364
                    undef, \@operator , \@values, 'AUTH_TYPE', 'asc' );
369
    is($query->{query}->{bool}->{must}[0]->{wildcard}->{'Heading.phrase'}, "*hamilton*","First index searched as expected");
365
    is($query->{query}->{bool}->{must}[0]->{query_string}->{query}, "Hamilton*","First index searched as expected");
370
    is($query->{query}->{bool}->{must}[1]->{wildcard}->{'Match.phrase'}, "*jefferson*","Second index searched when populated");
366
    is($query->{query}->{bool}->{must}[1]->{query_string}->{query}, "Jefferson*","Second index searched when populated");
371
    is( scalar @{ $query->{query}->{bool}->{must} }, 2,"Only defined searches are populated");
367
    is( scalar @{ $query->{query}->{bool}->{must} }, 2,"Only defined searches are populated");
372
368
373
369
374
- 

Return to bug 21084