File Coverage

lib/Apache/Solr.pm
Criterion Covered Total %
statement 137 303 45.2
branch 54 192 28.1
condition 32 76 42.1
subroutine 26 49 53.0
pod 24 26 92.3
total 273 646 42.2


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