|
Lines 141-185
sub build_query {
Link Here
|
| 141 |
return $res; |
141 |
return $res; |
| 142 |
} |
142 |
} |
| 143 |
|
143 |
|
| 144 |
=head2 build_browse_query |
|
|
| 145 |
|
| 146 |
my $browse_query = $builder->build_browse_query($field, $query); |
| 147 |
|
| 148 |
This performs a "starts with" style query on a particular field. The field |
| 149 |
to be searched must have been indexed with an appropriate mapping as a |
| 150 |
"phrase" subfield, which pretty much everything has. |
| 151 |
|
| 152 |
=cut |
| 153 |
|
| 154 |
# XXX this isn't really a browse query like we want in the end |
| 155 |
sub build_browse_query { |
| 156 |
my ( $self, $field, $query ) = @_; |
| 157 |
|
| 158 |
my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0; |
| 159 |
|
| 160 |
return { query => '*' } if !defined $query; |
| 161 |
|
| 162 |
# TODO this should come from Koha::SearchEngine::Elasticsearch |
| 163 |
my %field_whitelist = ( |
| 164 |
title => 1, |
| 165 |
author => 1, |
| 166 |
); |
| 167 |
$field = 'title' if !exists $field_whitelist{$field}; |
| 168 |
my $sort = $self->_sort_field($field); |
| 169 |
my $res = { |
| 170 |
query => { |
| 171 |
match_phrase_prefix => { |
| 172 |
"$field.phrase" => { |
| 173 |
query => $query, |
| 174 |
operator => 'or', |
| 175 |
fuzziness => $fuzzy_enabled ? 'auto' : '0', |
| 176 |
} |
| 177 |
} |
| 178 |
}, |
| 179 |
sort => [ { $sort => { order => "asc" } } ], |
| 180 |
}; |
| 181 |
} |
| 182 |
|
| 183 |
=head2 build_query_compat |
144 |
=head2 build_query_compat |
| 184 |
|
145 |
|
| 185 |
my ( |
146 |
my ( |
|
Lines 205-254
sub build_query_compat {
Link Here
|
| 205 |
$lang, $params ) |
166 |
$lang, $params ) |
| 206 |
= @_; |
167 |
= @_; |
| 207 |
|
168 |
|
| 208 |
#die Dumper ( $self, $operators, $operands, $indexes, $orig_limits, $sort_by, $scan, $lang ); |
169 |
my $query; |
| 209 |
my @sort_params = $self->_convert_sort_fields(@$sort_by); |
170 |
my $query_str = ''; |
| 210 |
my @index_params = $self->_convert_index_fields(@$indexes); |
171 |
my $search_param_query_str = ''; |
| 211 |
my $limits = $self->_fix_limit_special_cases($orig_limits); |
172 |
my $limits = (); |
| 212 |
if ( $params->{suppress} ) { push @$limits, "suppress:0"; } |
173 |
if ( $scan ) { |
| 213 |
# Merge the indexes in with the search terms and the operands so that |
174 |
($query, $query_str) = $self->_build_scan_query( $operands, $indexes ); |
| 214 |
# each search thing is a handy unit. |
175 |
$search_param_query_str = $query_str; |
| 215 |
unshift @$operators, undef; # The first one can't have an op |
176 |
} else { |
| 216 |
my @search_params; |
177 |
my @sort_params = $self->_convert_sort_fields(@$sort_by); |
| 217 |
my $truncate = C4::Context->preference("QueryAutoTruncate") || 0; |
178 |
my @index_params = $self->_convert_index_fields(@$indexes); |
| 218 |
my $ea = each_array( @$operands, @$operators, @index_params ); |
179 |
my $limits = $self->_fix_limit_special_cases($orig_limits); |
| 219 |
while ( my ( $oand, $otor, $index ) = $ea->() ) { |
180 |
if ( $params->{suppress} ) { push @$limits, "suppress:0"; } |
| 220 |
next if ( !defined($oand) || $oand eq '' ); |
181 |
# Merge the indexes in with the search terms and the operands so that |
| 221 |
$oand = $self->_clean_search_term($oand); |
182 |
# each search thing is a handy unit. |
| 222 |
$oand = $self->_truncate_terms($oand) if ($truncate); |
183 |
unshift @$operators, undef; # The first one can't have an op |
| 223 |
push @search_params, { |
184 |
my @search_params; |
| 224 |
operand => $oand, # the search terms |
185 |
my $truncate = C4::Context->preference("QueryAutoTruncate") || 0; |
| 225 |
operator => defined($otor) ? uc $otor : undef, # AND and so on |
186 |
my $ea = each_array( @$operands, @$operators, @index_params ); |
| 226 |
$index ? %$index : (), |
187 |
while ( my ( $oand, $otor, $index ) = $ea->() ) { |
| 227 |
}; |
188 |
next if ( !defined($oand) || $oand eq '' ); |
| 228 |
} |
189 |
$oand = $self->_clean_search_term($oand); |
|
|
190 |
$oand = $self->_truncate_terms($oand) if ($truncate); |
| 191 |
push @search_params, { |
| 192 |
operand => $oand, # the search terms |
| 193 |
operator => defined($otor) ? uc $otor : undef, # AND and so on |
| 194 |
$index ? %$index : (), |
| 195 |
}; |
| 196 |
} |
| 229 |
|
197 |
|
| 230 |
# We build a string query from limits and the queries. An alternative |
198 |
# We build a string query from limits and the queries. An alternative |
| 231 |
# would be to pass them separately into build_query and let it build |
199 |
# would be to pass them separately into build_query and let it build |
| 232 |
# them into a structured ES query itself. Maybe later, though that'd be |
200 |
# them into a structured ES query itself. Maybe later, though that'd be |
| 233 |
# more robust. |
201 |
# more robust. |
| 234 |
my $search_param_query_str = join( ' ', $self->_create_query_string(@search_params) ); |
202 |
$search_param_query_str = join( ' ', $self->_create_query_string(@search_params) ); |
| 235 |
my $query_str = join( ' AND ', |
203 |
$query_str = join( ' AND ', |
| 236 |
$search_param_query_str || (), |
204 |
$search_param_query_str || (), |
| 237 |
$self->_join_queries( $self->_convert_index_strings(@$limits) ) || () ); |
205 |
$self->_join_queries( $self->_convert_index_strings(@$limits) ) || () ); |
| 238 |
|
206 |
|
| 239 |
# If there's no query on the left, let's remove the junk left behind |
207 |
# If there's no query on the left, let's remove the junk left behind |
| 240 |
$query_str =~ s/^ AND //; |
208 |
$query_str =~ s/^ AND //; |
| 241 |
my %options; |
209 |
my %options; |
| 242 |
$options{sort} = \@sort_params; |
210 |
$options{sort} = \@sort_params; |
| 243 |
$options{is_opac} = $params->{is_opac}; |
211 |
$options{is_opac} = $params->{is_opac}; |
| 244 |
$options{weighted_fields} = $params->{weighted_fields}; |
212 |
$options{weighted_fields} = $params->{weighted_fields}; |
| 245 |
$options{whole_record} = $params->{whole_record}; |
213 |
$options{whole_record} = $params->{whole_record}; |
| 246 |
my $query = $self->build_query( $query_str, %options ); |
214 |
$query = $self->build_query( $query_str, %options ); |
|
|
215 |
} |
| 247 |
|
216 |
|
| 248 |
# We roughly emulate the CGI parameters of the zebra query builder |
217 |
# We roughly emulate the CGI parameters of the zebra query builder |
| 249 |
my $query_cgi = ''; |
218 |
my $query_cgi = ''; |
| 250 |
shift @$operators; # Shift out the one we unshifted before |
219 |
shift @$operators; # Shift out the one we unshifted before |
| 251 |
$ea = each_array( @$operands, @$operators, @$indexes ); |
220 |
my $ea = each_array( @$operands, @$operators, @$indexes ); |
| 252 |
while ( my ( $oand, $otor, $index ) = $ea->() ) { |
221 |
while ( my ( $oand, $otor, $index ) = $ea->() ) { |
| 253 |
$query_cgi .= '&' if $query_cgi; |
222 |
$query_cgi .= '&' if $query_cgi; |
| 254 |
$query_cgi .= 'idx=' . uri_escape_utf8( $index // '') . '&q=' . uri_escape_utf8( $oand ); |
223 |
$query_cgi .= 'idx=' . uri_escape_utf8( $index // '') . '&q=' . uri_escape_utf8( $oand ); |
|
Lines 528-533
sub build_authorities_query_compat {
Link Here
|
| 528 |
return $query; |
497 |
return $query; |
| 529 |
} |
498 |
} |
| 530 |
|
499 |
|
|
|
500 |
=head2 _build_scan_query |
| 501 |
|
| 502 |
my ($query, $query_str) = $builder->_build_scan_query(\@operands, \@indexes) |
| 503 |
|
| 504 |
This will build an aggregation scan query that can be issued to elasticsearch from |
| 505 |
the provided string input. |
| 506 |
|
| 507 |
=cut |
| 508 |
|
| 509 |
our %scan_field_convert = ( |
| 510 |
'ti' => 'title', |
| 511 |
'au' => 'author', |
| 512 |
'su' => 'subject', |
| 513 |
'se' => 'title-series', |
| 514 |
'pb' => 'publisher', |
| 515 |
); |
| 516 |
|
| 517 |
sub _build_scan_query { |
| 518 |
my ( $self, $operands, $indexes ) = @_; |
| 519 |
|
| 520 |
my $term = scalar( @$operands ) == 0 ? '' : $operands->[0]; |
| 521 |
my $index = scalar( @$indexes ) == 0 ? 'subject' : $indexes->[0]; |
| 522 |
|
| 523 |
my ( $f, $d ) = split( /,/, $index); |
| 524 |
$index = $scan_field_convert{$f} || $f; |
| 525 |
|
| 526 |
my $res; |
| 527 |
$res->{query} = { |
| 528 |
query_string => { |
| 529 |
query => '*' |
| 530 |
} |
| 531 |
}; |
| 532 |
$res->{aggregations} = { |
| 533 |
$index => { |
| 534 |
terms => { |
| 535 |
field => $index . '__facet', |
| 536 |
order => { '_term' => 'asc' }, |
| 537 |
include => $self->_create_regex_filter($self->_clean_search_term($term)) . '.*' |
| 538 |
} |
| 539 |
} |
| 540 |
}; |
| 541 |
return ($res, $term); |
| 542 |
} |
| 543 |
|
| 544 |
=head2 _create_regex_filter |
| 545 |
|
| 546 |
my $filter = $builder->_create_regex_filter('term') |
| 547 |
|
| 548 |
This will create a regex filter that can be used with an aggregation query. |
| 549 |
|
| 550 |
=cut |
| 551 |
|
| 552 |
sub _create_regex_filter { |
| 553 |
my ($self, $term) = @_; |
| 554 |
|
| 555 |
my $result = ''; |
| 556 |
foreach my $c (split(//, quotemeta($term))) { |
| 557 |
my $lc = lc($c); |
| 558 |
my $uc = uc($c); |
| 559 |
$result .= $lc ne $uc ? '[' . $lc . $uc . ']' : $c; |
| 560 |
} |
| 561 |
return $result; |
| 562 |
} |
| 563 |
|
| 531 |
=head2 _convert_sort_fields |
564 |
=head2 _convert_sort_fields |
| 532 |
|
565 |
|
| 533 |
my @sort_params = _convert_sort_fields(@sort_by) |
566 |
my @sort_params = _convert_sort_fields(@sort_by) |
|
Lines 774-780
sub _modify_string_by_type {
Link Here
|
| 774 |
return $str unless $str; # Empty or undef, we can't use it. |
807 |
return $str unless $str; # Empty or undef, we can't use it. |
| 775 |
|
808 |
|
| 776 |
$str .= '*' if $type eq 'right-truncate'; |
809 |
$str .= '*' if $type eq 'right-truncate'; |
| 777 |
$str = '"' . $str . '"' if $type eq 'phrase'; |
810 |
$str = '"' . $str . '"' if $type eq 'phrase' && $str !~ /^".*"$/; |
| 778 |
if ($type eq 'st-year') { |
811 |
if ($type eq 'st-year') { |
| 779 |
if ($str =~ /^(.*)-(.*)$/) { |
812 |
if ($str =~ /^(.*)-(.*)$/) { |
| 780 |
my $from = $1 || '*'; |
813 |
my $from = $1 || '*'; |