File Coverage

blib/lib/Catmandu/Store/Solr.pm
Criterion Covered Total %
statement 24 53 45.2
branch 0 4 0.0
condition n/a
subroutine 8 11 72.7
pod 1 1 100.0
total 33 69 47.8


line stmt bran cond sub pod time code
1             package Catmandu::Store::Solr;
2              
3 3     3   598076 use Catmandu::Sane;
  3         136213  
  3         20  
4 3     3   719 use Catmandu::Util qw(:is :array);
  3         8  
  3         982  
5 3     3   20 use Moo;
  3         7  
  3         21  
6 3     3   2026 use MooX::Aliases;
  3         4593  
  3         22  
7 3     3   2212 use WebService::Solr;
  3         469443  
  3         106  
8 3     3   1015 use Catmandu::Store::Solr::Bag;
  3         14  
  3         106  
9 3     3   23 use Catmandu::Error;
  3         6  
  3         67  
10 3     3   13 use LWP::UserAgent;
  3         6  
  3         1800  
11              
12             with 'Catmandu::Store';
13             with 'Catmandu::Transactional';
14              
15             =head1 NAME
16              
17             Catmandu::Store::Solr - A searchable store backed by Solr
18              
19             =cut
20              
21             our $VERSION = '0.0303';
22              
23             =head1 SYNOPSIS
24              
25             # From the command line
26              
27             # Import data into Solr
28             $ catmandu import JSON to Solr < data.json
29              
30             # Export data from ElasticSearch
31             $ catmandu export Solr to JSON > data.json
32              
33             # Export only one record
34             $ catmandu export Solr --id 1234
35              
36             # Export using an Solr query
37             $ catmandu export Solr --query "name:Recruitment OR name:college"
38              
39             # Export using a CQL query (needs a CQL mapping)
40             $ catmandu export Solr --q "name any college"
41              
42             # From Perl
43             use Catmandu::Store::Solr;
44              
45             my $store = Catmandu::Store::Solr->new(url => 'http://localhost:8983/solr' );
46              
47             my $obj1 = $store->bag->add({ name => 'Patrick' });
48              
49             printf "obj1 stored as %s\n" , $obj1->{_id};
50              
51             # Force an id in the store
52             my $obj2 = $store->bag->add({ _id => 'test123' , name => 'Nicolas' });
53              
54             # send all changes to solr (committed automatically)
55             $store->bag->commit;
56              
57             #transaction: rollback issued after 'die'
58             $store->transaction(sub{
59             $bag->delete_all();
60             die("oops, didn't want to do that!");
61             });
62              
63             my $obj3 = $store->bag->get('test123');
64              
65             $store->bag->delete('test123');
66              
67             $store->bag->delete_all;
68              
69             # All bags are iterators
70             $store->bag->each(sub { ... });
71             $store->bag->take(10)->each(sub { ... });
72              
73             # Some stores can be searched
74             my $hits = $store->bag->search(query => 'name:Patrick');
75              
76             =cut
77              
78             has url => (is => 'ro', default => sub {'http://localhost:8983/solr'});
79             has keep_alive => (is => 'ro', default => sub {0});
80             has solr => (is => 'lazy');
81             has bag_key => (is => 'lazy', alias => 'bag_field');
82             has on_error => (
83             is => 'ro',
84             isa => sub {
85             array_includes([qw(throw ignore)], $_[0])
86             or die("on_error must be 'throw' or 'ignore'");
87             },
88             lazy => 1,
89             default => sub {"throw"}
90             );
91             has _bags_used => (is => 'ro', lazy => 1, default => sub {[];});
92              
93             around 'bag' => sub {
94             my $orig = shift;
95             my $self = shift;
96              
97             my $bags_used = $self->_bags_used;
98             unless (array_includes($bags_used, $_[0])) {
99             push @$bags_used, $_[0];
100             }
101              
102             $orig->($self, @_);
103             };
104              
105             sub _build_solr {
106 0     0     my ($self) = @_;
107 0           WebService::Solr->new(
108             $_[0]->url,
109             {
110             autocommit => 0,
111             default_params => {wt => 'json'},
112             agent => LWP::UserAgent->new(keep_alive => $self->keep_alive),
113             }
114             );
115             }
116              
117             sub _build_bag_key {
118 0     0     $_[0]->key_for('bag');
119             }
120              
121             sub transaction {
122 0     0 1   my ($self, $sub) = @_;
123              
124 0 0         if ($self->{_tx}) {
125 0           return $sub->();
126             }
127 0           my $solr = $self->solr;
128 0           my @res;
129              
130             eval {
131             #flush buffers of all known bags ( with commit=true ), to ensure correct state
132 0           for my $bag_name (@{$self->_bags_used}) {
  0            
133 0           $self->bag($bag_name)->commit;
134             }
135              
136             #mark store as 'in transaction'. All subsequent calls to commit only flushes buffers without setting 'commit' to 'true' in solr
137 0           $self->{_tx} = 1;
138              
139             #transaction
140 0           @res = $sub->();
141              
142             #flushing buffers of all known bags (with commit=false)
143 0           for my $bag_name (@{$self->_bags_used}) {
  0            
144 0           $self->bag($bag_name)->commit;
145             }
146              
147             #commit in solr
148 0           $solr->commit;
149              
150             #remove mark 'in transaction'
151 0           $self->{_tx} = 0;
152 0           1;
153 0 0         } or do {
154 0           my $err = $@;
155              
156             #remove remaining documents from all buffers, because they were added during the transaction
157 0           for my $bag_name (@{$self->_bags_used}) {
  0            
158 0           $self->bag($bag_name)->clear_buffer;
159             }
160              
161             #rollback in solr
162 0           eval {$solr->rollback};
  0            
163              
164             #remove mark 'in transaction'
165 0           $self->{_tx} = 0;
166 0           Catmandu::Error->throw($err);
167             };
168              
169 0           @res;
170             }
171              
172             =head1 SOLR SCHEMA
173              
174             The Solr schema needs to support at least the identifier field (C<_id> by default) and a bag
175             field (C<_bag> by default) to be able to store Catmandu items:
176              
177             # In schema.xml
178             <field name="_id" type="string" indexed="true" stored="true" required="true" />
179             <field name="_bag" type="string" indexed="true" stored="true" required="true" />
180              
181             The names of these fields can optionally be changed using the C<id_field> and C<_bag>
182             configuration parameters of L<Catmandu::Store::Solr>.
183              
184             The C<_id> will contain the record identifier. The C<_bag> field will contain a string
185             to support L<Catmandu::Bag>-s in Solr.
186              
187             =head1 CONFIGURATION
188              
189             =over
190              
191             =item url
192              
193             URL of Solr core
194              
195             Default: C<http://localhost:8983/solr>
196              
197             =item id_field
198              
199             Name of unique field in Solr core.
200              
201             Default: C<_id>
202              
203             This Solr field is mapped to C<_id> when retrieved
204              
205             =item bag_field
206              
207             Name of field in Solr we can use to split the core into 'bags'.
208              
209             Default: C<_bag>
210              
211             This Solr field is mapped to C<_bag> when retrieved
212              
213             =item on_error
214              
215             Action to take when records cannot be saved to Solr. Default: throw. Available: ignore.
216              
217             =back
218              
219             =head1 METHODS
220              
221             =head2 new( url => $url )
222              
223             =head2 new( url => $url, id_field => '_id', bag_field => '_bag' )
224              
225             =head2 new( url => $url, bags => { data => { cql_mapping => \%mapping } } )
226              
227             Creates a new Catmandu::Store::Solr store connected to a Solr core, specificied by $url.
228              
229             The store supports CQL searches when a cql_mapping is provided. This hash
230             contains a translation of CQL fields into Solr searchable fields.
231              
232             # Example mapping
233             $cql_mapping = {
234             title => {
235             op => {
236             'any' => 1 ,
237             'all' => 1 ,
238             '=' => 1 ,
239             '<>' => 1 ,
240             'exact' => {field => 'mytitle.exact' }
241             } ,
242             sort => 1,
243             field => 'mytitle',
244             cb => ['Biblio::Search', 'normalize_title']
245             }
246             }
247              
248             The CQL mapping above will support for the 'title' field the CQL operators: any, all, =, <> and exact.
249              
250             For all the operators the 'title' field will be mapping into the Solr field 'mytitle', except
251             for the 'exact' operator. In case of 'exact' we will search the field 'mytitle.exact'.
252              
253             The CQL has an optional callback field 'cb' which contains a reference to subroutines to rewrite or
254             augment the search query. In this case, in the Biblio::Search package there is a normalize_title
255             subroutine which returns a string or an ARRAY of string with augmented title(s). E.g.
256              
257             package Biblio::Search;
258              
259             sub normalize_title {
260             my ($self,$title) = @_;
261             my $new_title =~ s{[^A-Z0-9]+}{}g;
262             $new_title;
263             }
264              
265             1;
266              
267             =head2 transaction
268              
269             When you issue $bag->commit, all changes made in the buffer are sent to solr, along with a commit.
270             So committing in Catmandu merely means flushing changes;-).
271              
272             When you wrap your subroutine within 'transaction', this behaviour is disabled temporarily.
273             When you call 'die' within the subroutine, a rollback is sent to solr.
274              
275             Remember that transactions happen at store level: after the transaction, all buffers of all bags are flushed to solr,
276             and a commit is issued in solr.
277              
278             # Record 'test' added
279             $bag->add({ _id => "test" });
280              
281             # Buffer flushed, and 'commit' sent to solr
282             $bag->commit();
283              
284             $bag->store->transaction(sub{
285             $bag->add({ _id => "test",title => "test" });
286             # Call to die: rollback sent to solr
287             die("oops, didn't want to do that!");
288             });
289              
290             # Record is still { _id => "test" }
291              
292             =head1 INHERITED METHODS
293              
294             This Catmandu::Store implements:
295              
296             =over 3
297              
298             =item L<Catmandu::Store>
299              
300             =item L<Catmandu::Transactional>
301              
302             =back
303              
304             Each Catmandu::Bag in this Catmandu::Store implements:
305              
306             =over 3
307              
308             =item L<Catmandu::Bag>
309              
310             =item L<Catmandu::Searchable>
311              
312             =item L<Catmandu::CQLSearchable>
313              
314             =back
315              
316             =head1 SEE ALSO
317              
318             L<Catmandu::Store>, L<WebService::Solr>
319              
320             =head1 AUTHOR
321              
322             Nicolas Steenlant, C<< nicolas.steenlant at ugent.be >>
323              
324             Patrick Hochstenbach, C<< patrick.hochstenbach at ugent.be >>
325              
326             Nicolas Franck, C<< nicolas.franck at ugent.be >>
327              
328             Pieter De Praetere
329              
330             =head1 LICENSE AND COPYRIGHT
331              
332             This program is free software; you can redistribute it and/or modify it
333             under the terms of either: the GNU General Public License as published
334             by the Free Software Foundation; or the Artistic License.
335              
336             See http://dev.perl.org/licenses/ for more information.
337              
338             =cut
339              
340             1;