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