Bug 17600: Standardize our EXPORT_OK
[srvgit] / Koha / SearchEngine / Elasticsearch / Indexer.pm
1 package Koha::SearchEngine::Elasticsearch::Indexer;
2
3 # Copyright 2013 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 use Carp qw( carp croak );
21 use Modern::Perl;
22 use Try::Tiny qw( catch try );
23 use List::Util qw( any );
24 use base qw(Koha::SearchEngine::Elasticsearch);
25
26 use Koha::Exceptions;
27 use Koha::Exceptions::Elasticsearch;
28 use Koha::SearchEngine::Zebra::Indexer;
29 use C4::AuthoritiesMarc qw//;
30 use C4::Biblio;
31 use C4::Context;
32
33 =head1 NAME
34
35 Koha::SearchEngine::Elasticsearch::Indexer - handles adding new records to the index
36
37 =head1 SYNOPSIS
38
39     my $indexer = Koha::SearchEngine::Elasticsearch::Indexer->new(
40         { index => Koha::SearchEngine::BIBLIOS_INDEX } );
41     $indexer->drop_index();
42     $indexer->update_index(\@biblionumbers, \@records);
43
44
45 =head1 CONSTANTS
46
47 =over 4
48
49 =item C<Koha::SearchEngine::Elasticsearch::Indexer::INDEX_STATUS_OK>
50
51 Represents an index state where index is created and in a working state.
52
53 =item C<Koha::SearchEngine::Elasticsearch::Indexer::INDEX_STATUS_REINDEX_REQUIRED>
54
55 Not currently used, but could be useful later, for example if can detect when new field or mapping added.
56
57 =item C<Koha::SearchEngine::Elasticsearch::Indexer::INDEX_STATUS_RECREATE_REQUIRED>
58
59 Representings an index state where index needs to be recreated and is not in a working state.
60
61 =back
62
63 =cut
64
65 use constant {
66     INDEX_STATUS_OK => 0,
67     INDEX_STATUS_REINDEX_REQUIRED => 1,
68     INDEX_STATUS_RECREATE_REQUIRED => 2,
69 };
70
71 =head1 FUNCTIONS
72
73 =head2 update_index($biblionums, $records)
74
75     try {
76         $self->update_index($biblionums, $records);
77     } catch {
78         die("Something went wrong trying to update index:" .  $_[0]);
79     }
80
81 Converts C<MARC::Records> C<$records> to Elasticsearch documents and performs
82 an update request for these records on the Elasticsearch index.
83
84 =over 4
85
86 =item C<$biblionums>
87
88 Arrayref of biblio numbers for the C<$records>, the order must be the same as
89 and match up with C<$records>.
90
91 =item C<$records>
92
93 Arrayref of C<MARC::Record>s.
94
95 =back
96
97 =cut
98
99 sub update_index {
100     my ($self, $biblionums, $records) = @_;
101
102     my $documents = $self->marc_records_to_documents($records);
103     my @body;
104     for (my $i = 0; $i < scalar @$biblionums; $i++) {
105         my $id = $biblionums->[$i];
106         my $document = $documents->[$i];
107         push @body, {
108             index => {
109                 _id => "$id"
110             }
111         };
112         push @body, $document;
113     }
114     my $response;
115     if (@body) {
116         try{
117             my $elasticsearch = $self->get_elasticsearch();
118             $response = $elasticsearch->bulk(
119                 index => $self->index_name,
120                 type => 'data', # is just hard coded in Indexer.pm?
121                 body => \@body
122             );
123             if ($response->{errors}) {
124                 carp "One or more ElasticSearch errors occurred when indexing documents";
125             }
126         } catch {
127             Koha::Exceptions::Elasticsearch::BadResponse->throw(
128                 type => $_->{type},
129                 details => $_->{text},
130             );
131         };
132     }
133     return $response;
134 }
135
136 =head2 set_index_status_ok
137
138 Convenience method for setting index status to C<INDEX_STATUS_OK>.
139
140 =cut
141
142 sub set_index_status_ok {
143     my ($self) = @_;
144     $self->index_status(INDEX_STATUS_OK);
145 }
146
147 =head2 is_index_status_ok
148
149 Convenience method for checking if index status is C<INDEX_STATUS_OK>.
150
151 =cut
152
153 sub is_index_status_ok {
154     my ($self) = @_;
155     return $self->index_status == INDEX_STATUS_OK;
156 }
157
158 =head2 set_index_status_reindex_required
159
160 Convenience method for setting index status to C<INDEX_REINDEX_REQUIRED>.
161
162 =cut
163
164 sub set_index_status_reindex_required {
165     my ($self) = @_;
166     $self->index_status(INDEX_STATUS_REINDEX_REQUIRED);
167 }
168
169 =head2 is_index_status_reindex_required
170
171 Convenience method for checking if index status is C<INDEX_STATUS_REINDEX_REQUIRED>.
172
173 =cut
174
175 sub is_index_status_reindex_required {
176     my ($self) = @_;
177     return $self->index_status == INDEX_STATUS_REINDEX_REQUIRED;
178 }
179
180 =head2 set_index_status_recreate_required
181
182 Convenience method for setting index status to C<INDEX_STATUS_RECREATE_REQUIRED>.
183
184 =cut
185
186 sub set_index_status_recreate_required {
187     my ($self) = @_;
188     $self->index_status(INDEX_STATUS_RECREATE_REQUIRED);
189 }
190
191 =head2 is_index_status_recreate_required
192
193 Convenience method for checking if index status is C<INDEX_STATUS_RECREATE_REQUIRED>.
194
195 =cut
196
197 sub is_index_status_recreate_required {
198     my ($self) = @_;
199     return $self->index_status == INDEX_STATUS_RECREATE_REQUIRED;
200 }
201
202 =head2 index_status($status)
203
204 Will either set the current index status to C<$status> and return C<$status>,
205 or return the current index status if called with no arguments.
206
207 =over 4
208
209 =item C<$status>
210
211 Optional argument. If passed will set current index status to C<$status> if C<$status> is
212 a valid status. See L</CONSTANTS>.
213
214 =back
215
216 =cut
217
218 sub index_status {
219     my ($self, $status) = @_;
220     my $key = 'ElasticsearchIndexStatus_' . $self->index;
221
222     if (defined $status) {
223         unless (any { $status == $_ } (
224                 INDEX_STATUS_OK,
225                 INDEX_STATUS_REINDEX_REQUIRED,
226                 INDEX_STATUS_RECREATE_REQUIRED,
227             )
228         ) {
229             Koha::Exceptions::Exception->throw("Invalid index status: $status");
230         }
231         C4::Context->set_preference($key, $status);
232         return $status;
233     }
234     else {
235         return C4::Context->preference($key);
236     }
237 }
238
239 =head2 update_mappings
240
241 Generate Elasticsearch mappings from mappings stored in database and
242 perform a request to update Elasticsearch index mappings. Will throw an
243 error and set index status to C<INDEX_STATUS_RECREATE_REQUIRED> if update
244 failes.
245
246 =cut
247
248 sub update_mappings {
249     my ($self) = @_;
250     my $elasticsearch = $self->get_elasticsearch();
251     my $mappings = $self->get_elasticsearch_mappings();
252
253     foreach my $type (keys %{$mappings}) {
254         try {
255             my $response = $elasticsearch->indices->put_mapping(
256                 index => $self->index_name,
257                 type => $type,
258                 body => {
259                     $type => $mappings->{$type}
260                 }
261             );
262         } catch {
263             $self->set_index_status_recreate_required();
264             my $reason = $_[0]->{vars}->{body}->{error}->{reason};
265             my $index_name = $self->index_name;
266             Koha::Exceptions::Exception->throw(
267                 error => "Unable to update mappings for index \"$index_name\". Reason was: \"$reason\". Index needs to be recreated and reindexed",
268             );
269         };
270     }
271     $self->set_index_status_ok();
272 }
273
274 =head2 update_index_background($biblionums, $records)
275
276 This has exactly the same API as C<update_index> however it'll
277 return immediately. It'll start a background process that does the adding.
278
279 If it fails to add to Elasticsearch then it'll add to a queue that will cause
280 it to be updated by a regular index cron job in the future.
281
282 =cut
283
284 # TODO implement in the future - I don't know the best way of doing this yet.
285 # If fork: make sure process group is changed so apache doesn't wait for us.
286
287 sub update_index_background {
288     my $self = shift;
289     $self->update_index(@_);
290 }
291
292 =head2 index_records
293
294 This function takes an array of record numbers and fetches the records to send to update_index
295 for actual indexing.
296
297 If $records parameter is provided the records will be used as-is, this is only utilized for authorities
298 at the moment.
299
300 The other variables are used for parity with Zebra indexing calls. Currently the calls are passed through
301 to Zebra as well.
302
303 =cut
304
305 sub index_records {
306     my ( $self, $record_numbers, $op, $server, $records ) = @_;
307     $record_numbers = [$record_numbers] if ref $record_numbers ne 'ARRAY' && defined $record_numbers;
308     $records = [$records] if ref $records ne 'ARRAY' && defined $records;
309     if ( $op eq 'specialUpdate' ) {
310         my $index_record_numbers;
311         if ($records){
312             $index_record_numbers = $record_numbers;
313         } else {
314             foreach my $record_number ( @$record_numbers ){
315                 my $record = _get_record( $record_number, $server );
316                 if( $record ){
317                     push @$records, $record;
318                     push @$index_record_numbers, $record_number;
319                 }
320             }
321         }
322         $self->update_index_background( $index_record_numbers, $records ) if $index_record_numbers && $records;
323     }
324     elsif ( $op eq 'recordDelete' ) {
325         $self->delete_index_background( $record_numbers );
326     }
327     #FIXME Current behaviour is to index Zebra when using ES, at some point we should stop
328     Koha::SearchEngine::Zebra::Indexer::index_records( $self, $record_numbers, $op, $server, undef );
329 }
330
331 sub _get_record {
332     my ( $id, $server ) = @_;
333     return $server eq 'biblioserver'
334         ? C4::Biblio::GetMarcBiblio({ biblionumber => $id, embed_items  => 1 })
335         : C4::AuthoritiesMarc::GetAuthority($id);
336 }
337
338 =head2 delete_index($biblionums)
339
340 C<$biblionums> is an arrayref of biblionumbers to delete from the index.
341
342 =cut
343
344 sub delete_index {
345     my ($self, $biblionums) = @_;
346
347     my $elasticsearch = $self->get_elasticsearch();
348     my @body = map { { delete => { _id => "$_" } } } @{$biblionums};
349     my $result = $elasticsearch->bulk(
350         index => $self->index_name,
351         type => 'data',
352         body => \@body,
353     );
354     if ($result->{errors}) {
355         croak "An Elasticsearch error occurred during bulk delete";
356     }
357 }
358
359 =head2 delete_index_background($biblionums)
360
361 Identical to L</delete_index($biblionums)>
362
363 =cut
364
365 # TODO: Should be made async
366 sub delete_index_background {
367     my $self = shift;
368     $self->delete_index(@_);
369 }
370
371 =head2 drop_index
372
373 Drops the index from the Elasticsearch server.
374
375 =cut
376
377 sub drop_index {
378     my ($self) = @_;
379     if ($self->index_exists) {
380         my $elasticsearch = $self->get_elasticsearch();
381         $elasticsearch->indices->delete(index => $self->index_name);
382         $self->set_index_status_recreate_required();
383     }
384 }
385
386 =head2 create_index
387
388 Creates the index (including mappings) on the Elasticsearch server.
389
390 =cut
391
392 sub create_index {
393     my ($self) = @_;
394     my $settings = $self->get_elasticsearch_settings();
395     my $elasticsearch = $self->get_elasticsearch();
396     $elasticsearch->indices->create(
397         index => $self->index_name,
398         body => {
399             settings => $settings
400         }
401     );
402     $self->update_mappings();
403 }
404
405 =head2 index_exists
406
407 Checks if index has been created on the Elasticsearch server. Returns C<1> or the
408 empty string to indicate whether index exists or not.
409
410 =cut
411
412 sub index_exists {
413     my ($self) = @_;
414     my $elasticsearch = $self->get_elasticsearch();
415     return $elasticsearch->indices->exists(
416         index => $self->index_name,
417     );
418 }
419
420 1;
421
422 __END__
423
424 =head1 AUTHOR
425
426 =over 4
427
428 =item Chris Cormack C<< <chrisc@catalyst.net.nz> >>
429
430 =item Robin Sheat C<< <robin@catalyst.net.nz> >>
431
432 =back