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