From dbca822e0f442fc923491a87a11f3c64be2ed6be Mon Sep 17 00:00:00 2001 From: Alex Arnaud Date: Thu, 30 Jan 2020 16:13:29 +0000 Subject: [PATCH] Bug 24555: Replace query string with boolean query Test plan: Just make simple and advanced searches --- Koha/SearchEngine/Elasticsearch/QueryBuilder.pm | 152 ++++++++++++++++-------- 1 file changed, 100 insertions(+), 52 deletions(-) diff --git a/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm b/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm index c1011ec..b7619d2 100644 --- a/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm +++ b/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm @@ -85,23 +85,7 @@ sub build_query { $query = '*' unless defined $query; my $res; - my $fields = $self->_search_fields({ - is_opac => $options{is_opac}, - weighted_fields => $options{weighted_fields}, - }); - if ($options{whole_record}) { - push @$fields, 'marc_data_array.*'; - } - $res->{query} = { - query_string => { - query => $query, - fuzziness => $fuzzy_enabled ? 'auto' : '0', - default_operator => 'AND', - fields => $fields, - lenient => JSON::true, - analyze_wildcard => JSON::true, - } - }; + $res->{query} = $query; if ( $options{sort} ) { foreach my $sort ( @{ $options{sort} } ) { @@ -195,23 +179,62 @@ sub build_query_compat { }; } - # We build a string query from limits and the queries. An alternative - # would be to pass them separately into build_query and let it build - # them into a structured ES query itself. Maybe later, though that'd be - # more robust. - $search_param_query_str = join( ' ', $self->_create_query_string(@search_params) ); - $query_str = join( ' AND ', - $search_param_query_str || (), - $self->_join_queries( $self->_convert_index_strings(@$limits) ) || () ); - - # If there's no query on the left, let's remove the junk left behind - $query_str =~ s/^ AND //; + my %options; $options{sort} = \@sort_params; $options{is_opac} = $params->{is_opac}; $options{weighted_fields} = $params->{weighted_fields}; $options{whole_record} = $params->{whole_record}; - $query = $self->build_query( $query_str, %options ); + + my $fields = $self->_search_fields({ + is_opac => $options{is_opac}, + weighted_fields => $options{weighted_fields}, + }); + if ($options{whole_record}) { + push @$fields, 'marc_data_array.*'; + } + + my $bool_query = { + bool => { + minimum_should_match => 1, + should => [ + { + bool => { + must => [] + } + } + ] + } + }; + + foreach my $search_param (@search_params) { + next if $search_param->{operand} eq '*'; + my $operator = $search_param->{operator} // ''; + my $expression = $self->_create_match_expression($search_param, \%options); + if (defined($expression->{multi_match})) { + $expression->{multi_match}->{fields} = $fields; + } + + if ($operator eq 'OR') { + push @{ $bool_query->{bool}->{should} }, { + bool => { + must => [$expression], + }, + }; + } else { + push @{ $bool_query->{bool}->{should}->[-1]->{bool}->{must} }, $expression; + } + } + + unless (@{ $bool_query->{bool}->{should}->[0]->{bool}->{must} }) { + $bool_query->{bool}->{should}->[0] = { match_all => {} }; + } + + if (@$limits) { + $bool_query->{bool}->{filter} = $self->_build_limit_filters($limits); + } + + $query = $self->build_query( $bool_query, %options ); } # We roughly emulate the CGI parameters of the zebra query builder @@ -246,6 +269,27 @@ sub build_query_compat { ); } +sub _create_match_expression { + my ($self, $search_param) = @_; + + my $expression = {}; + unless (defined($search_param->{field})) { + $expression->{multi_match} = { + query => $search_param->{operand}, + lenient => JSON::true + + }; + return $expression; + } + + my $field = $search_param->{field}; + $expression->{match} = { + $field => $search_param->{operand} + }; + + return $expression; +} + =head2 build_authorities_query my $query = $builder->build_authorities_query(\%search); @@ -735,6 +779,33 @@ sub _convert_index_fields { } @indexes; } +=head2 _build_limit_filters + my @filters = $self->_build_limit_filters + +Transform limit strings to Elasticsearch filters + +=cut + +sub _build_limit_filters { + my ( $self, $limits ) = @_; + my @res; + foreach my $s (@$limits) { + next if $s eq ''; + my ( $field, $term ) = $s =~ /^\s*([\w,-]*?):(.*)/; + next unless ( defined($field) && defined($term) ); + + my ($conv) = $self->_convert_index_fields($field); + my $facet_name = $conv->{field} . '__facet'; + push @res, { + term => { + $facet_name => $term + } + }; + } + + return \@res; +} + =head2 _convert_index_strings my @searches = $self->_convert_index_strings(@searches); @@ -880,29 +951,6 @@ sub _make_phrases { map { s/^\s*(\w*?:)(.*)$/$1"$2"/r } @parts; } -=head2 _create_query_string - - my @query_strings = $self->_create_query_string(@queries); - -Given a list of hashrefs, it will turn them into a lucene-style query string. -The hash should contain field, type (both for the indexes), operator, and -operand. - -=cut - -sub _create_query_string { - my ( $self, @queries ) = @_; - - map { - my $otor = $_->{operator} ? $_->{operator} . ' ' : ''; - my $field = $_->{field} ? $_->{field} . ':' : ''; - - my $oand = $self->_modify_string_by_type(%$_); - $oand = "($oand)" if $field && scalar(split(/\s+/, $oand)) > 1 && (!defined $_->{type} || $_->{type} ne 'st-year'); - "$otor($field$oand)"; - } @queries; -} - =head2 _clean_search_term my $term = $self->_clean_search_term($term); -- 2.7.4