Bug 12478 - fix issue with class loading
[koha.git] / Koha / SearchEngine / Elasticsearch / Search.pm
1 package Koha::SearchEngine::ElasticSearch::Search;
2
3 # Copyright 2014 Catalyst IT
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 3 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 =head1 NAME
21
22 Koha::SearchEngine::ElasticSearch::Search - search functions for Elasticsearch
23
24 =head1 SYNOPSIS
25
26     my $searcher = Koha::SearchEngine::ElasticSearch::Search->new();
27     my $builder = Koha::SearchEngine::Elasticsearch::QueryBuilder->new();
28     my $query = $builder->build_query('perl');
29     my $results = $searcher->search($query);
30     print "There were " . $results->total . " results.\n";
31     $results->each(sub {
32         push @hits, @_[0];
33     });
34
35 =head1 METHODS
36
37 =cut
38
39 use base qw(Koha::ElasticSearch);
40 use Koha::ItemTypes;
41
42 use Catmandu::Store::ElasticSearch;
43
44 use Data::Dumper; #TODO remove
45 use Carp qw(cluck);
46
47 Koha::SearchEngine::ElasticSearch::Search->mk_accessors(qw( store ));
48
49 =head2 search
50
51     my $results = $searcher->search($query, $page, $count);
52
53 Run a search using the query. It'll return C<$count> results, starting at page
54 C<$page> (C<$page> counts from 1, anything less that, or C<undef> becomes 1.)
55
56 C<%options> is a hash containing extra options:
57
58 =over 4
59
60 =item offset
61
62 If provided, this overrides the C<$page> value, and specifies the record as
63 an offset (i.e. the number of the record to start with), rather than a page.
64
65 =back
66
67 =cut
68
69 sub search {
70     my ($self, $query, $page, $count, %options) = @_;
71
72     my $params = $self->get_elasticsearch_params();
73     my %paging;
74     $paging{limit} = $count || 20;
75     # ES doesn't want pages, it wants a record to start from.
76     if (exists $options{offset}) {
77         $paging{start} = $options{offset};
78     } else {
79         $page = (!defined($page) || ($page <= 0)) ? 1 : $page - 1;
80         $paging{start} = $page * $paging{limit};
81     }
82     $self->store(
83         Catmandu::Store::ElasticSearch->new(
84             %$params,
85             trace_calls => 0,
86         )
87     );
88     my $results = $self->store->bag->search( %$query, %paging );
89     return $results;
90 }
91
92 =head2 search_compat
93
94     my ( $error, $results, $facets ) = $search->search_compat(
95         $query,            $simple_query, \@sort_by,       \@servers,
96         $results_per_page, $offset,       $expanded_facet, $branches,
97         $query_type,       $scan
98       )
99
100 A search interface somewhat compatible with L<C4::Search->getRecords>. Anything
101 that is returned in the query created by build_query_compat will probably
102 get ignored here.
103
104 =cut
105
106 sub search_compat {
107     my (
108         $self,     $query,            $simple_query, $sort_by,
109         $servers,  $results_per_page, $offset,       $expanded_facet,
110         $branches, $query_type,       $scan
111     ) = @_;
112
113     my %options;
114     $options{offset} = $offset;
115     my $results = $self->search($query, undef, $results_per_page, %options);
116
117     # Convert each result into a MARC::Record
118     my (@records, $index);
119     $index = $offset; # opac-search expects results to be put in the
120         # right place in the array, according to $offset
121     $results->each(sub {
122             # The results come in an array for some reason
123             my $marc_json = @_[0]->{record};
124             my $marc = $self->json2marc($marc_json);
125             $records[$index++] = $marc;
126         });
127     # consumers of this expect a name-spaced result, we provide the default
128     # configuration.
129     my %result;
130     $result{biblioserver}{hits} = $results->total;
131     $result{biblioserver}{RECORDS} = \@records;
132     return (undef, \%result, $self->_convert_facets($results->{facets}));
133 }
134
135 =head2 json2marc
136
137     my $marc = $self->json2marc($marc_json);
138
139 Converts the form of marc (based on its JSON, but as a Perl structure) that
140 Catmandu stores into a MARC::Record object.
141
142 =cut
143
144 sub json2marc {
145     my ( $self, $marcjson ) = @_;
146
147     my $marc = MARC::Record->new();
148     $marc->encoding('UTF-8');
149
150     # fields are like:
151     # [ '245', '1', '2', 'a' => 'Title', 'b' => 'Subtitle' ]
152     # conveniently, this is the form that MARC::Field->new() likes
153     foreach $field (@$marcjson) {
154         next if @$field < 5;    # Shouldn't be possible, but...
155         if ( $field->[0] eq 'LDR' ) {
156             $marc->leader( $field->[4] );
157         }
158         else {
159             my $marc_field = MARC::Field->new(@$field);
160             $marc->append_fields($marc_field);
161         }
162     }
163     return $marc;
164 }
165
166 =head2 _convert_facets
167
168     my $koha_facets = _convert_facets($es_facets);
169
170 Converts elasticsearch facets types to the form that Koha expects.
171 It expects the ES facet name to match the Koha type, for example C<itype>,
172 C<au>, C<su-to>, etc.
173
174 =cut
175
176 sub _convert_facets {
177     my ( $self, $es ) = @_;
178
179     return undef if !$es;
180
181     # These should correspond to the ES field names, as opposed to the CCL
182     # things that zebra uses.
183     my %type_to_label = (
184         author   => 'Authors',
185         location => 'Location',
186         itype    => 'ItemTypes',
187         se       => 'Series',
188         subject  => 'Topics',
189         'su-geo' => 'Places',
190     );
191
192     # We also have some special cases, e.g. itypes that need to show the
193     # value rather than the code.
194     my $itypes = Koha::ItemTypes->new();
195     my %special = ( itype => sub { $itypes->get_description_for_code(@_) }, );
196     my @res;
197     while ( ( $type, $data ) = each %$es ) {
198         next if !exists( $type_to_label{$type} );
199         my $facet = {
200             type_id => $type . '_id',
201             expand  => $type,
202             expandable => 1,    # TODO figure how that's supposed to work
203             "type_label_$type_to_label{$type}" => 1,
204             type_link_value                    => $type,
205         };
206         foreach my $term ( @{ $data->{terms} } ) {
207             my $t = $term->{term};
208             my $c = $term->{count};
209             if ( exists( $special{$type} ) ) {
210                 $label = $special{$type}->($t);
211             }
212             else {
213                 $label = $t;
214             }
215             push @{ $facet->{facets} }, {
216                 facet_count       => $c,
217                 facet_link_value  => $t,
218                 facet_title_value => $t . " ($c)",
219                 facet_label_value => $label,    # TODO either truncate this,
220                      # or make the template do it like it should anyway
221                 type_link_value => $type,
222             };
223         }
224         push @res, $facet if exists $facet->{facets};
225     }
226     return \@res;
227 }
228
229
230 1;