Bug 20248: Improve Elasticsearch mappings UI and rebuild_elastic_search.pl.
[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 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 use Carp;
21 use Modern::Perl;
22 use base qw(Koha::SearchEngine::Elasticsearch);
23 use Data::Dumper;
24
25 # For now just marc, but we can do anything here really
26 use Catmandu::Importer::MARC;
27 use Catmandu::Store::ElasticSearch;
28
29 Koha::SearchEngine::Elasticsearch::Indexer->mk_accessors(qw( store ));
30
31 =head1 NAME
32
33 Koha::SearchEngine::Elasticsearch::Indexer - handles adding new records to the index
34
35 =head1 SYNOPSIS
36
37     my $indexer = Koha::SearchEngine::Elasticsearch::Indexer->new(
38         { index => Koha::SearchEngine::BIBLIOS_INDEX } );
39     $indexer->drop_index();
40     $indexer->update_index(\@biblionumbers, \@records);
41
42 =head1 FUNCTIONS
43
44 =head2 $indexer->update_index($biblionums, $records);
45
46 C<$biblionums> is an arrayref containing the biblionumbers for the records.
47
48 C<$records> is an arrayref containing the L<MARC::Record>s themselves.
49
50 The values in the arrays must match up, and the 999$c value in the MARC record
51 will be rewritten using the values in C<$biblionums> to ensure they are correct.
52 If C<$biblionums> is C<undef>, this won't happen, but you should be sure that
53 999$c is correct on your own then.
54
55 Note that this will modify the original record if C<$biblionums> is supplied.
56 If that's a problem, clone them first.
57
58 =cut
59
60 sub update_index {
61     my ($self, $biblionums, $records) = @_;
62
63     # TODO should have a separate path for dealing with a large number
64     # of records at once where we use the bulk update functions in ES.
65     if ($biblionums) {
66         $self->_sanitise_records($biblionums, $records);
67     }
68
69     my $from    = $self->_convert_marc_to_json($records);
70     if ( !$self->store ) {
71         my $params  = $self->get_elasticsearch_params();
72         $self->store(
73             Catmandu::Store::ElasticSearch->new(
74                 %$params,
75                 index_settings => $self->get_elasticsearch_settings(),
76                 index_mappings => $self->get_elasticsearch_mappings(),
77             )
78         );
79     }
80
81     #print Data::Dumper::Dumper( $from->to_array );
82     $self->store->bag->add_many($from);
83     $self->store->bag->commit;
84     return 1;
85 }
86
87 =head2 $indexer->update_index_background($biblionums, $records)
88
89 This has exactly the same API as C<update_index_background> however it'll
90 return immediately. It'll start a background process that does the adding.
91
92 If it fails to add to Elasticsearch then it'll add to a queue that will cause
93 it to be updated by a regular index cron job in the future.
94
95 # TODO implement in the future - I don't know the best way of doing this yet.
96 # If fork: make sure process group is changed so apache doesn't wait for us.
97
98 =cut
99
100 sub update_index_background {
101     my $self = shift;
102     $self->update_index(@_);
103 }
104
105 =head2 $indexer->delete_index($biblionums)
106
107 C<$biblionums> is an arrayref of biblionumbers to delete from the index.
108
109 =cut
110
111 sub delete_index {
112     my ($self, $biblionums) = @_;
113
114     if ( !$self->store ) {
115         my $params  = $self->get_elasticsearch_params();
116         $self->store(
117             Catmandu::Store::ElasticSearch->new(
118                 %$params,
119                 index_settings => $self->get_elasticsearch_settings(),
120                 index_mappings => $self->get_elasticsearch_mappings(),
121             )
122         );
123     }
124     $self->store->bag->delete($_) foreach @$biblionums;
125     $self->store->bag->commit;
126 }
127
128 =head2 $indexer->delete_index_background($biblionums)
129
130 Identical to L<delete_index>, this will return immediately and start a
131 background process to do the actual deleting.
132
133 =cut
134
135 # TODO implement in the future
136
137 sub delete_index_background {
138     my $self = shift;
139     $self->delete_index(@_);
140 }
141
142 =head2 $indexer->create_index();
143
144 Create an index on the Elasticsearch server.
145
146 =cut
147
148 sub create_index {
149     my ($self) = @_;
150
151     if (!$self->store) {
152         my $params  = $self->get_elasticsearch_params();
153         $self->store(
154             Catmandu::Store::ElasticSearch->new(
155                 %$params,
156                 index_settings => $self->get_elasticsearch_settings(),
157                 index_mappings => $self->get_elasticsearch_mappings(),
158             )
159         );
160     }
161     $self->store->bag->commit;
162 }
163
164 =head2 $indexer->drop_index();
165
166 Drops the index from the elasticsearch server. Calling C<update_index>
167 after this will recreate it again.
168
169 =cut
170
171 sub drop_index {
172     my ($self) = @_;
173
174     if (!$self->store) {
175         # If this index doesn't exist, this will create it. Then it'll be
176         # deleted. That's not the end of the world however.
177         my $params  = $self->get_elasticsearch_params();
178         $self->store(
179             Catmandu::Store::ElasticSearch->new(
180                 %$params,
181                 index_settings => $self->get_elasticsearch_settings(),
182                 index_mappings => $self->get_elasticsearch_mappings(),
183             )
184         );
185     }
186     my $store = $self->store;
187     $self->store(undef);
188     $store->drop();
189 }
190
191 sub _sanitise_records {
192     my ($self, $biblionums, $records) = @_;
193
194     confess "Unequal number of values in \$biblionums and \$records." if (@$biblionums != @$records);
195
196     my $c = @$biblionums;
197     for (my $i=0; $i<$c; $i++) {
198         my $bibnum = $biblionums->[$i];
199         my $rec = $records->[$i];
200         # I've seen things you people wouldn't believe. Attack ships on fire
201         # off the shoulder of Orion. I watched C-beams glitter in the dark near
202         # the Tannhauser gate. MARC records where 999$c doesn't match the
203         # biblionumber column. All those moments will be lost in time... like
204         # tears in rain...
205         if ( $rec ) {
206             $rec->delete_fields($rec->field('999'));
207             $rec->append_fields(MARC::Field->new('999','','','c' => $bibnum, 'd' => $bibnum));
208         }
209     }
210 }
211
212 sub _convert_marc_to_json {
213     my $self    = shift;
214     my $records = shift;
215     my $importer =
216       Catmandu::Importer::MARC->new( records => $records, id => '999c' );
217     my $fixer = Catmandu::Fix->new( fixes => $self->get_fixer_rules() );
218     $importer = $fixer->fix($importer);
219     return $importer;
220 }
221
222 1;
223
224 __END__
225
226 =head1 AUTHOR
227
228 =over 4
229
230 =item Chris Cormack C<< <chrisc@catalyst.net.nz> >>
231
232 =item Robin Sheat C<< <robin@catalyst.net.nz> >>
233
234 =back