Bug 28316: avoid messing up regexes in the search queries
[koha.git] / Koha / SearchEngine / Elasticsearch / Browse.pm
1 package Koha::SearchEngine::Elasticsearch::Browse;
2
3 # Copyright 2015 Catalyst IT
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 =head1 NAME
21
22 Koha::SearchEngine::ElasticSearch::Browse - browse functions for Elasticsearch
23
24 =head1 SYNOPSIS
25
26     my $browser =
27       Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } );
28     my $results = $browser->browse(
29         'prefi', 'title',
30         {
31             results   => '500',
32             fuzziness => 2,
33         }
34     );
35     foreach my $r (@$results) {
36         push @hits, $r->{text};
37     }
38
39 =head1 DESCRIPTION
40
41 This provides an easy interface to the "browse" functionality. Essentially,
42 it does a fast prefix search on defined fields. The fields have to be marked
43 as "suggestible" in the database when indexing takes place.
44
45 =head1 METHODS
46
47 =cut
48
49 use base qw(Koha::SearchEngine::Elasticsearch);
50 use Modern::Perl;
51
52 =head2 browse
53
54     my $results = $browser->browse($prefix, $field, \%options);
55
56 Does a prefix search for C<$prefix>, looking in C<$field>. Options are:
57
58 =over 4
59
60 =item count
61
62 The number of results to return. For Koha browse purposes, this should
63 probably be fairly high. Defaults to 500.
64
65 =item fuzziness
66
67 How much allowing for typos and misspellings is done. If 0, then it must match
68 exactly. If unspecified, it defaults to '1', which is probably the most useful.
69 Otherwise, it is a number specifying the Levenshtein edit distance relative to
70 the string length, according to the following lengths:
71
72 =over 4
73
74 =item 0..2
75
76 must match exactly
77
78 =item 3..5
79
80 C<fuzziness> edits allowed
81
82 =item >5
83
84 C<fuzziness>+1 edits allowed
85
86 =back
87
88 In all cases the maximum number of edits allowed is two (an elasticsearch
89 restriction.)
90
91 =back
92
93 =head3 Returns
94
95 This returns an arrayref of hashrefs. Each hashref contains a "text" element
96 that contains the field as returned. There may be other fields in that
97 hashref too, but they're less likely to be important.
98
99 The array will be ordered as returned from Elasticsearch, which seems to be
100 in order of some form of relevance.
101
102 =cut
103
104 sub browse {
105     my ($self, $prefix, $field, $options) = @_;
106
107     my $query = $self->_build_query($prefix, $field, $options);
108     my $elasticsearch = $self->get_elasticsearch();
109     my $results = $elasticsearch->search(
110         index => $self->index_name,
111         body => $query
112     );
113
114     return $results->{suggest}{suggestions}[0]{options};
115 }
116
117 =head2 _build_query
118
119     my $query = $self->_build_query($prefix, $field, $options);
120
121 Arguments are the same as for L<browse>. This will return a query structure
122 for elasticsearch to use.
123
124 =cut
125
126 sub _build_query {
127     my ( $self, $prefix, $field, $options ) = @_;
128
129     $options = {} unless $options;
130     my $f = $options->{fuzziness} // 1;
131     my $l = length($prefix);
132     my $fuzzie;
133     if ( $l <= 2 ) {
134         $fuzzie = 0;
135     }
136     elsif ( $l <= 5 ) {
137         $fuzzie = $f;
138     }
139     else {
140         $fuzzie = $f + 1;
141     }
142     $fuzzie = 2 if $fuzzie > 2;
143
144     my $size = $options->{count} // 500;
145     my $query = {
146         # this is an annoying thing, if we set size to 0 it gets rewritten
147         # to 10. There's a bug somewhere in one of the libraries.
148         size    => 1,
149         suggest => {
150             suggestions => {
151                 text       => $prefix,
152                 completion => {
153                     field => $field . '__suggestion',
154                     size  => $size,
155                     fuzzy => {
156                         fuzziness => $fuzzie,
157                     }
158                 }
159             }
160         }
161     };
162     return $query;
163 }
164
165 1;
166
167 __END__
168
169 =head1 AUTHOR
170
171 =over 4
172
173 =item Robin Sheat << <robin@catalyst.net.nz> >>
174
175 =back
176
177 =cut