File Coverage

lib/Apache/Solr.pm
Criterion Covered Total %
statement 135 285 47.3
branch 54 182 29.6
condition 30 69 43.4
subroutine 26 49 53.0
pod 24 26 92.3
total 269 611 44.0


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