File Coverage

lib/Apache/Solr.pm
Criterion Covered Total %
statement 135 291 46.3
branch 54 184 29.3
condition 30 69 43.4
subroutine 26 49 53.0
pod 24 26 92.3
total 269 619 43.4


line stmt bran cond sub pod time code
1             # Copyrights 2012-2022 by [Mark Overmeer].
2             # For other contributors see ChangeLog.
3             # See the manual pages for details on the licensing terms.
4             # Pod stripped from pm file by OODoc 2.03.
5             # This code is part of distribution Apache-Solr. Meta-POD processed with
6             # OODoc into POD and HTML manual-pages. See README.md
7             # Copyright Mark Overmeer. Licensed under the same terms as Perl itself.
8              
9             package Apache::Solr;
10 5     5   4798 use vars '$VERSION';
  5         22  
  5         278  
11             $VERSION = '1.08';
12              
13              
14 5     5   29 use warnings;
  5         6  
  5         121  
15 5     5   24 use strict;
  5         6  
  5         94  
16              
17 5     5   1609 use Apache::Solr::Tables;
  5         12  
  5         635  
18 5     5   2386 use Log::Report qw(solr);
  5         573021  
  5         36  
19              
20 5     5   1663 use Scalar::Util qw/blessed/;
  5         12  
  5         241  
21 5     5   29 use Encode qw/encode/;
  5         11  
  5         216  
22 5     5   27 use Scalar::Util qw/weaken/;
  5         8  
  5         183  
23              
24 5     5   2696 use URI ();
  5         22888  
  5         115  
25 5     5   3527 use LWP::UserAgent ();
  5         217005  
  5         195  
26 5     5   2675 use MIME::Types ();
  5         20954  
  5         133  
27              
28 5     5   37 use constant LATEST_SOLR_VERSION => '4.5'; # newest support by this module
  5         10  
  5         19691  
29              
30             # overrule this when your host has a different unique field
31             our $uniqueKey = 'id';
32             my $mimetypes = MIME::Types->new;
33             my $http_agent;
34              
35             sub _to_bool($)
36 22     22   49 { my $b = shift;
37 22 100 66     141 !defined $b ? undef
    50          
38             : ($b && $b ne 'false' && $b ne 'off') ? 'true'
39             : 'false';
40             }
41              
42              
43             sub new(@)
44 4     4 1 536 { my ($class, %args) = @_;
45 4 100       21 if($class eq __PACKAGE__)
46 2   50     15 { my $format = delete $args{format} || 'XML';
47 2 50 33     9 $format eq 'XML' || $format eq 'JSON'
48             or panic "unknown communication format '$format' for solr";
49 2         6 $class .= '::' . $format;
50 2 50       90 eval "require $class"; panic $@ if $@;
  2         17  
51             }
52 4         33 (bless {}, $class)->init(\%args)
53             }
54              
55             sub init($)
56 4     4 0 13 { my ($self, $args) = @_;
57 4         30 $self->server($args->{server});
58 4         16 $self->{AS_core} = $args->{core};
59 4 50       23 $self->{AS_commit} = exists $args->{autocommit} ? $args->{autocommit} : 1;
60 4   50     32 $self->{AS_sversion} = $args->{server_version} || LATEST_SOLR_VERSION;
61              
62             $http_agent = $self->{AS_agent}
63 4   33     123 = $args->{agent} || $http_agent || LWP::UserAgent->new(keep_alive=>1);
64              
65 4         18839 weaken $http_agent;
66              
67 4         12 $self;
68             }
69              
70             #---------------
71              
72 1 50   1 1 3 sub core(;$) { my $s = shift; @_ ? $s->{AS_core} = shift : $s->{AS_core} }
  1         6  
73             sub autocommit(;$)
74 0 0   0 1 0 { my $s = shift; @_ ? $s->{AS_commit} = shift : $s->{AS_commit} }
  0         0  
75 0     0 1 0 sub agent() {shift->{AS_agent}}
76 26     26 1 46 sub serverVersion() {shift->{AS_sversion}}
77              
78              
79             sub server(;$)
80 5     5 1 14 { my ($self, $uri) = @_;
81 5 100       29 $uri or return $self->{AS_server};
82 4 50 33     60 $uri = URI->new($uri)
83             unless blessed $uri && $uri->isa('URI');
84 4         29296 $self->{AS_server} = $uri;
85             }
86              
87              
88             #--------------------------
89              
90             sub select(@)
91 0     0 1 0 { my $self = shift;
92 0 0 0     0 my $args = @_ && ref $_[0] eq 'HASH' ? shift : {};
93 0         0 $self->_select($args, scalar $self->expandSelect(@_));
94             }
95 0     0   0 sub _select($$) {panic "not extended"}
96              
97              
98             sub queryTerms(@)
99 0     0 1 0 { my $self = shift;
100 0         0 $self->_terms(scalar $self->expandTerms(@_));
101             }
102 0     0   0 sub _terms(@) {panic "not implemented"}
103              
104             #-------------------------------------
105              
106             sub addDocument($%)
107 0     0 1 0 { my ($self, $docs, %args) = @_;
108 0 0       0 $docs = [ $docs ] if ref $docs ne 'ARRAY';
109              
110 0         0 my $sv = $self->serverVersion;
111              
112 0         0 my (%attrs, %params);
113             $params{commit}
114 0 0       0 = _to_bool(exists $args{commit} ? $args{commit} : $self->autocommit);
115              
116 0 0       0 if(my $cw = $args{commitWithin})
117 0 0       0 { if($sv lt '3.4') { $attrs{commit} = 'true' }
  0         0  
118 0         0 else { $attrs{commitWithin} = int($cw * 1000) }
119             }
120              
121             $attrs{overwrite} = _to_bool delete $args{overwrite}
122 0 0       0 if exists $args{overwrite};
123              
124 0         0 foreach my $depr (qw/allowDups overwritePending overwriteCommitted/)
125 0 0       0 { if(exists $args{$depr})
126 0 0       0 { if($sv ge '4.0') { $self->removed("add($depr)"); delete $args{$depr} }
  0 0       0  
  0         0  
127 0         0 elsif($sv ge '1.0') { $self->deprecated("add($depr)") }
128 0         0 else { $attrs{$depr} = _to_bool delete $args{$depr} }
129             }
130             }
131              
132 0         0 $self->_add($docs, \%attrs, \%params);
133             }
134              
135              
136             sub commit(%)
137 0     0 1 0 { my ($self, %args) = @_;
138 0         0 my $sv = $self->serverVersion;
139              
140 0         0 my %attrs;
141 0 0       0 if(exists $args{waitFlush})
142 0 0       0 { if($sv ge '4.0')
    0          
143 0         0 { $self->removed("commit(waitFlush)"); delete $args{waitFlush} }
  0         0  
144 0         0 elsif($sv ge '1.4') { $self->deprecated("commit(waitFlush)") }
145 0         0 else { $attrs{waitFlush} = _to_bool delete $args{waitFlush} }
146             }
147              
148             $attrs{waitSearcher} = _to_bool delete $args{waitSearcher}
149 0 0       0 if exists $args{waitSearcher};
150              
151 0 0       0 if(exists $args{softCommit})
152 0 0       0 { if($sv lt '4.0') { $self->ignored("commit(softCommit)") }
  0         0  
153 0         0 else { $attrs{softCommit} = _to_bool delete $args{softCommit} }
154             }
155              
156 0 0       0 if(exists $args{expungeDeletes})
157 0 0       0 { if($sv lt '1.4') { $self->ignored("commit(expungeDeletes)") }
  0         0  
158 0         0 else { $attrs{expungeDeletes} = _to_bool delete $args{expungeDeletes} }
159             }
160              
161 0         0 $self->_commit(\%attrs);
162             }
163 0     0   0 sub _commit($) {panic "not implemented"}
164              
165              
166             sub optimize(%)
167 0     0 1 0 { my ($self, %args) = @_;
168 0         0 my $sv = $self->serverVersion;
169              
170 0         0 my %attrs;
171 0 0       0 if(exists $args{waitFlush})
172 0 0       0 { if($sv ge '4.0')
    0          
173 0         0 { $self->removed("commit(waitFlush)"); delete $args{waitFlush} }
  0         0  
174 0         0 elsif($sv ge '1.4') { $self->deprecated("optimize(waitFlush)") }
175 0         0 else { $attrs{waitFlush} = _to_bool delete $args{waitFlush} }
176             }
177              
178             $attrs{waitSearcher} = _to_bool delete $args{waitSearcher}
179 0 0       0 if exists $args{waitSearcher};
180              
181 0 0       0 if(exists $args{softCommit})
182 0 0       0 { if($sv lt '4.0') { $self->ignored("optimize(softCommit)") }
  0         0  
183 0         0 else { $attrs{softCommit} = _to_bool delete $args{softCommit} }
184             }
185              
186 0 0       0 if(exists $args{maxSegments})
187 0 0       0 { if($sv lt '1.3') { $self->ignored("optimize(maxSegments)") }
  0         0  
188 0         0 else { $attrs{maxSegments} = delete $args{maxSegments} }
189             }
190              
191 0         0 $self->_optimize(\%attrs);
192             }
193 0     0   0 sub _optimize($) {panic "not implemented"}
194              
195              
196             sub delete(%)
197 0     0 1 0 { my ($self, %args) = @_;
198              
199 0         0 my %attrs;
200             $attrs{commit}
201 0 0       0 = _to_bool(exists $args{commit} ? $args{commit} : $self->autocommit);
202              
203 0 0       0 if(exists $args{fromPending})
204 0         0 { $self->deprecated("delete(fromPending)");
205 0         0 $attrs{fromPending} = _to_bool delete $args{fromPending};
206             }
207 0 0       0 if(exists $args{fromCommitted})
208 0         0 { $self->deprecated("delete(fromCommitted)");
209 0         0 $attrs{fromCommitted} = _to_bool delete $args{fromCommitted};
210             }
211              
212 0         0 my @which;
213 0 0       0 if(my $id = $args{id})
214 0 0       0 { push @which, map +(id => $_), ref $id eq 'ARRAY' ? @$id : $id;
215             }
216 0 0       0 if(my $q = $args{query})
217 0 0       0 { push @which, map +(query => $_), ref $q eq 'ARRAY' ? @$q : $q;
218             }
219 0 0       0 @which or return;
220              
221             # JSON calls do not accept multiple ids at once (it seems in 4.0)
222 0         0 my $result;
223 0 0 0     0 if($self->serverVersion ge '1.4' && !$self->isa('Apache::Solr::JSON'))
224 0         0 { $result = $self->_delete(\%attrs, \@which);
225             }
226             else
227             { # old servers accept only one id or query per delete
228 0         0 $result = $self->_delete(\%attrs, [splice @which, 0, 2]) while @which;
229             }
230 0         0 $result;
231             }
232 0     0   0 sub _delete(@) {panic "not implemented"}
233              
234              
235             sub rollback()
236 0     0 1 0 { my $self = shift;
237 0 0       0 $self->serverVersion ge '1.4'
238             or error __x"rollback not supported by solr server";
239              
240 0         0 $self->_rollback;
241             }
242              
243              
244             sub extractDocument(@)
245 0     0 1 0 { my $self = shift;
246              
247 0 0       0 $self->serverVersion ge '1.4'
248             or error __x"extractDocument() requires Solr v1.4 or higher";
249            
250 0         0 my %p = $self->expandExtract(@_);
251 0         0 my $data;
252              
253             # expand* changes '_' into '.'
254 0         0 my $ct = delete $p{'content.type'};
255 0         0 my $fn = delete $p{file};
256 0 0 0     0 $p{'resource.name'} ||= $fn if $fn && !ref $fn;
      0        
257              
258             $p{commit} = _to_bool $self->autocommit
259 0 0       0 unless exists $p{commit};
260              
261 0 0       0 if(defined $p{string})
    0          
262             { # try to avoid copying the data, which can be huge
263             $data = $ct =~ m!^text/!i
264             ? \encode(utf8 =>
265 0         0 (ref $p{string} eq 'SCALAR' ? ${$p{string}} : $p{string}))
266 0 0       0 : (ref $p{string} eq 'SCALAR' ? $p{string} : \$p{string} );
    0          
    0          
267              
268 0         0 delete $p{string};
269             }
270             elsif($fn)
271 0         0 { local $/;
272 0 0       0 if(ref $fn eq 'GLOB') { $data = \<$fn> }
  0         0  
273             else
274 0         0 { local *IN;
275 0 0       0 open IN, '<:raw', $fn
276             or fault __x"cannot read document from {fn}", fn => $fn;
277 0         0 $data = \;
278 0 0       0 close IN
279             or fault __x"read error for document {fn}", fn => $fn;
280 0   0     0 $ct ||= $mimetypes->mimeTypeOf($fn);
281             }
282             }
283             else
284 0         0 { error __x"extract requires document as file or string";
285             }
286              
287 0         0 $self->_extract([%p], $data, $ct);
288             }
289 0     0   0 sub _extract($){panic "not implemented"}
290              
291             #-------------------------
292              
293             sub _core_admin($@)
294 0     0   0 { my ($self, $action, $params) = @_;
295 0   0     0 $params->{core} ||= $self->core;
296            
297 0         0 my $endpoint = $self->endpoint('cores', core => 'admin'
298             , params => $params);
299              
300 0         0 my @params = %$params;
301 0         0 my $result = Apache::Solr::Result->new(params => [ %$params ]
302             , endpoint => $endpoint, core => $self);
303              
304 0         0 $self->request($endpoint, $result);
305 0         0 $result;
306             }
307              
308              
309             sub coreStatus(%)
310 0     0 1 0 { my ($self, %args) = @_;
311 0         0 $self->_core_admin('STATUS', \%args);
312             }
313              
314              
315             sub coreReload(%)
316 0     0 1 0 { my ($self, %args) = @_;
317 0         0 $self->_core_admin('RELOAD', \%args);
318             }
319              
320              
321             sub coreUnload($%)
322 0     0 1 0 { my ($self, %args) = @_;
323 0         0 $self->_core_admin('UNLOAD', \%args);
324             }
325              
326             #--------------------------
327              
328             sub _calling_sub()
329 1     1   6 { for(my $i=0;$i <10; $i++)
330 4         16 { my $sub = (caller $i)[3];
331 4 100 66     24 return $sub if !$sub || index($sub, 'Apache::Solr::') < 0;
332             }
333             }
334              
335             sub _simpleExpand($$$)
336 26     26   41 { my ($self, $p, $prefix) = @_;
337 26 100       86 my @p = ref $p eq 'HASH' ? %$p : @$p;
338 26         50 my $sv = $self->serverVersion;
339              
340 26         33 my @t;
341 26         50 while(@p)
342 90         146 { my ($k, $v) = (shift @p, shift @p);
343 90         135 $k =~ s/_/./g;
344 90 100 100     239 $k = $prefix.$k if defined $prefix && index($k, $prefix)!=0;
345 90 100       185 my $param = $k =~ m/^f\.[^\.]+\.(.*)/ ? $1 : $k;
346              
347 90         97 my ($dv, $iv);
348 90 100 66     308 if(($dv = $deprecated{$param}) && $sv ge $dv)
    50 66        
349 1         3 { my $command = _calling_sub;
350 1         10 $self->deprecated("$command($param) since $dv");
351             }
352             elsif(($iv = $introduced{$param}) && $iv gt $sv)
353 0         0 { my $command = _calling_sub;
354 0         0 $self->ignored("$command($param) introduced in $iv");
355 0         0 next;
356             }
357              
358             push @t, $k => $boolparams{$param} ? _to_bool($_) : $_
359 90 100       697 for ref $v eq 'ARRAY' ? @$v : $v;
    100          
360             }
361 26         146 @t;
362             }
363              
364              
365             sub expandTerms(@)
366 1     1 1 995 { my $self = shift;
367 1 50       8 my $p = @_==1 ? shift : [@_];
368 1         8 my @t = $self->_simpleExpand($p, 'terms.');
369 1 50       9 wantarray ? @t : \@t;
370             }
371              
372              
373             sub _expand_flatten($$)
374 4     4   9 { my ($self, $v, $prefix) = @_;
375 4 50       12 my @l = ref $v eq 'HASH' ? %$v : @$v;
376 4         5 my @s;
377 4         15 push @s, $prefix.(shift @l) => (shift @l) while @l;
378 4         12 @s;
379             }
380              
381             sub expandExtract(@)
382 1     1 1 751 { my $self = shift;
383 1 50       9 my @p = @_==1 ? @{(shift)} : @_;
  0         0  
384 1         2 my @s;
385 1         4 while(@p)
386 10         20 { my ($k, $v) = (shift @p, shift @p);
387 10 100 66     40 if(!ref $v || ref $v eq 'SCALAR')
    100 100        
    50 66        
      33        
388 6         13 { push @s, $k => $v }
389             elsif($k eq 'literal' || $k eq 'literals')
390 2         7 { push @s, $self->_expand_flatten($v, 'literal.') }
391             elsif($k eq 'fmap' || $k eq 'boost' || $k eq 'resource')
392 2         5 { push @s, $self->_expand_flatten($v, "$k.") }
393 0         0 else { panic "unknown set '$k'" }
394             }
395              
396 1 50       6 my @t = @s ? $self->_simpleExpand(\@s) : ();
397 1 50       27 wantarray ? @t : \@t;
398             }
399              
400              
401             # probably more config later, currently only one column
402             # "also-per-field" means, not only $set.$more, but also f.$field.$set.$more
403             my %sets = #also-per-field?
404             ( expand => [0]
405             , facet => [1]
406             , hl => [1]
407             , mlt => [0]
408             , stats => [0]
409             , suggest => [0]
410             , group => [0]
411             );
412            
413             sub expandSelect(@)
414 9     9 1 1793 { my $self = shift;
415 9         32 my @s;
416 9         11 my (@flat, %seen_set);
417 9         27 while(@_)
418 37         60 { my ($k, $v) = (shift, shift);
419 37         77 $k =~ s/_/./g;
420 37         69 my @p = split /\./, $k;
421              
422             # fields are $set.$more or f.$field.$set.$more
423 37   66     77 my $per_field = $p[0] eq 'f' && @p > 2;
424 37 100       79 my ($set, $more) = $per_field ? @p[2,3] : @p[0,1];
425              
426 37 100       83 if(my $def = $sets{$set})
    50          
427 15         23 { $seen_set{$set} = 1;
428 15 50 66     35 !$per_field || $def->[0]
429             or error __x"set {set} cannot be used per field, in {field}"
430             , set => $set, field => $k;
431              
432 15 50       30 if(ref $v eq 'HASH')
    0          
433 15 50       26 { !$more
434             or error __x"field {field} is not simple for a set", field => $k;
435 15         44 push @s, $self->_simpleExpand($v, "$k.");
436             }
437             elsif($more) # skip $set=true for now
438 0         0 { push @flat, $k => $v;
439             }
440             }
441             elsif(ref $v eq 'HASH')
442 0         0 { error __x"unknown set {set}", set => $set;
443             }
444             else
445 22         55 { push @flat, $k => $v;
446             }
447             }
448 9         22 push @flat, %seen_set;
449 9         24 unshift @s, $self->_simpleExpand(\@flat);
450 9 100       58 wantarray ? @s : \@s;
451             }
452              
453              
454             sub deprecated($)
455 1     1 1 3 { my ($self, $msg) = @_;
456 1 50       6 return if $self->{AS_depr_msg}{$msg}++; # report only once
457 1         5 warning __x"deprecated solr {message}", message => $msg;
458             }
459              
460              
461             sub ignored($)
462 0     0 1 0 { my ($self, $msg) = @_;
463 0 0       0 return if $self->{AS_ign_msg}{$msg}++; # report only once
464 0         0 warning __x"ignored solr {message}", message => $msg;
465             }
466              
467              
468             sub removed($)
469 0     0 1 0 { my ($self, $msg) = @_;
470 0 0       0 return if $self->{AS_rem_msg}{$msg}++; # report only once
471 0         0 warning __x"removed solr {message}", message => $msg;
472             }
473              
474              
475             #------------------------
476              
477             sub endpoint($@)
478 1     1 1 1027 { my ($self, $action, %args) = @_;
479 1   33     9 my $core = $args{core} || $self->core;
480 1         4 my $take = $self->server->clone;
481 1 50       71 $take->path ($take->path . (defined $core ? "/$core" : '') . "/$action");
482              
483             # make parameters ordered
484 1   50     78 my $params = $args{params} || [];
485 1 50       5 $params = [ %$params ] if ref $params eq 'HASH';
486 1 50       4 @$params or return $take;
487              
488             # remove paramers with undefined value
489 1         3 my @p = @$params;
490 1         2 my @params;
491 1         4 while(@p)
492 2 50       6 { push @params, $p[0] => $p[1] if defined $p[1];
493 2         5 shift @p, shift @p;
494             }
495              
496 1 50       11 $take->query_form(@params) if @params;
497 1         139 $take;
498             }
499            
500             sub request($$;$$)
501 0     0 0   { my ($self, $url, $result, $body, $body_ct) = @_;
502              
503 0           my $req;
504 0 0         if(!$body)
505             { # request without payload
506 0           $req = HTTP::Request->new(GET => $url);
507             }
508             else
509             { # request with 'form' payload
510 0 0         $req = HTTP::Request->new
511             ( POST => $url
512             , [ Content_Type => $body_ct
513             , Content_Disposition => 'form-data; name="content"'
514             ]
515             , (ref $body eq 'SCALAR' ? $$body : $body)
516             );
517             }
518              
519 0           $result->request($req);
520              
521 0           my $resp;
522             RETRY:
523 0           { $resp = $self->agent->request($req);
  0            
524 0 0         unless($resp->is_success)
525 0           { alert "Solr request failed with ".$resp->code;
526 0           sleep 5; # let remote settle a bit
527 0           goto RETRY;
528             }
529             }
530             #use Data::Dumper;
531             #warn Dumper $resp;
532 0           $result->response($resp);
533 0           $resp;
534             }
535              
536             #----------------------------------
537              
538             1;