File Coverage

lib/Apache/Solr/Result.pm
Criterion Covered Total %
statement 30 197 15.2
branch 0 80 0.0
condition 0 45 0.0
subroutine 10 46 21.7
pod 29 31 93.5
total 69 399 17.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::Result;
10 5     5   881 use vars '$VERSION';
  5         8  
  5         272  
11             $VERSION = '1.09';
12              
13              
14 5     5   24 use warnings;
  5         9  
  5         126  
15 5     5   21 no warnings 'recursion'; # linked list of pages can get deep
  5         9  
  5         198  
16              
17 5     5   24 use strict;
  5         9  
  5         141  
18              
19 5     5   23 use Log::Report qw(solr);
  5         31  
  5         37  
20 5     5   2336 use Time::HiRes qw(time);
  5         3116  
  5         33  
21 5     5   585 use Scalar::Util qw(weaken);
  5         9  
  5         312  
22              
23 5     5   1477 use Apache::Solr::Document ();
  5         9  
  5         119  
24              
25 5     5   2396 use Data::Dumper;
  5         24997  
  5         397  
26             $Data::Dumper::Indent = 1;
27             $Data::Dumper::Quotekeys = 0;
28              
29              
30             use overload
31 5         32 '""' => 'endpoint'
32 5     5   36 , bool => 'success';
  5         8  
33              
34             #----------------------
35              
36 0     0 1   sub new(@) { my $c = shift; (bless {}, $c)->init({@_}) }
  0            
37             sub init($)
38 0     0 0   { my ($self, $args) = @_;
39 0 0         my $p = $self->{ASR_params} = $args->{params} or panic;
40 0 0         $self->{ASR_endpoint} = $args->{endpoint} or panic;
41              
42 0           my %params = @$p;
43              
44 0           $self->{ASR_start} = time;
45 0           $self->request($args->{request});
46 0           $self->response($args->{response});
47              
48 0           $self->{ASR_pages} = [ $self ]; # first has non-weak page-table
49 0           weaken $self->{ASR_pages}[0]; # no reference loop!
50              
51 0 0         if($self->{ASR_core} = $args->{core}) { weaken $self->{ASR_core} }
  0            
52 0   0       $self->{ASR_next} = $params{start} || 0;
53 0   0       $self->{ASR_seq} = $args->{sequential} || 0;
54 0           $self->{ASR_fpz} = $args->{_fpz};
55              
56 0           $self;
57             }
58              
59             # replace the pageset with a shared set.
60             sub _pageset($)
61 0     0     { $_[0]->{ASR_pages} = $_[1];
62 0           weaken $_[0]->{ASR_pages}; # otherwise memory leak
63             }
64              
65             #---------------
66              
67 0     0 1   sub start() {shift->{ASR_start}}
68 0     0 1   sub endpoint() {shift->{ASR_endpoint}}
69 0     0 1   sub params() {@{shift->{ASR_params}}}
  0            
70 0     0 1   sub core() {shift->{ASR_core}}
71 0     0 1   sub sequential() {shift->{ASR_seq}}
72              
73             sub request(;$)
74 0     0 1   { my $self = shift;
75 0 0 0       @_ && $_[0] or return $self->{ASR_request};
76 0           $self->{ASR_req_out} = time;
77 0           $self->{ASR_request} = shift;
78             }
79              
80             sub response(;$)
81 0     0 1   { my $self = shift;
82 0 0 0       @_ && $_[0] or return $self->{ASR_response};
83 0           $self->{ASR_resp_in} = time;
84 0           $self->{ASR_response} = shift;
85             }
86              
87             sub decoded(;$)
88 0     0 1   { my $self = shift;
89 0 0         @_ or return $self->{ASR_decoded};
90 0           $self->{ASR_dec_done} = time;
91 0           $self->{ASR_decoded} = shift;
92             }
93              
94             sub elapse()
95 0     0 1   { my $self = shift;
96 0 0         my $done = $self->{ASR_dec_done} or return;
97 0           $done = $self->{ASR_start};
98             }
99              
100              
101 0   0 0 1   sub success() { my $s = shift; $s->{ASR_success} ||= $s->solrStatus==0 }
  0            
102              
103              
104             sub solrStatus()
105 0 0   0 1   { my $dec = shift->decoded or return 500;
106 0           $dec->{responseHeader}{status};
107             }
108              
109             sub solrQTime()
110 0 0   0 1   { my $dec = shift->decoded or return;
111 0           my $qtime = $dec->{responseHeader}{QTime};
112 0 0         defined $qtime ? $qtime/1000 : undef;
113             }
114              
115             sub solrError()
116 0 0   0 1   { my $dec = shift->decoded or return;
117 0   0       my $err = $dec->{error} || {};
118 0   0       my $msg = $err->{msg} || '';
119 0           $msg =~ s/\s*$//s;
120 0 0         length $msg ? $msg : ();
121             }
122              
123             sub httpError()
124 0 0   0 1   { my $resp = shift->response or return;
125 0           $resp->status_line;
126             }
127              
128             sub serverError()
129 0 0   0 1   { my $resp = shift->response or return;
130 0 0         $resp->code != 200 or return;
131 0           my $ct = $resp->content_type;
132 0 0         $ct eq 'text/html' or return;
133 0   0       my $body = $resp->decoded_content || $resp->content;
134 0           $body =~ s!.*!!;
135 0           $body =~ s!.*!!;
136 0           $body =~ s!|

!\n!g; # cheap reformatter
137 0           $body =~ s!\s*!: !g;
138 0           $body =~ s!<[^>]*>!!gs;
139 0           $body;
140             }
141              
142              
143             sub errors()
144 0     0 1   { my $self = shift;
145 0           my @errors;
146 0 0         if(my $h = $self->httpError) { push @errors, "HTTP error:", " $h" }
  0            
147 0 0         if(my $a = $self->serverError)
148 0           { $a =~ s/^/ /gm;
149 0           push @errors, "Server error:", $a;
150             }
151 0 0         if(my $s = $self->solrError) { push @errors, "Solr error:", " $s" }
  0            
152 0           join "\n", @errors, '';
153             }
154              
155             #--------------------------
156              
157             sub _responseData()
158 0     0     { my $dec = shift->decoded;
159 0   0       $dec->{result} // $dec->{response};
160             }
161              
162             sub nrSelected()
163 0 0   0 1   { my $results = shift->_responseData
164             or panic "there are no results (yet)";
165              
166 0           $results->{numFound};
167             }
168              
169              
170             sub _docs($)
171 0     0     { my ($self, $data) = @_;
172 0   0       my $docs = $data->{doc} // $data->{docs} // [];
      0        
173              
174             # Decoding XML without schema may give unexpect results
175 0 0         $docs = [ $docs ] if ref $docs eq 'HASH'; # when only one result
176 0           $docs;
177             }
178              
179             sub selected($%)
180 0     0 1   { my ($self, $rank, %options) = @_;
181 0 0         my $data = $self->_responseData
182             or panic __x"there are no results in the answer";
183              
184             # start for next
185 0           $self->{ASR_next} = $rank +1;
186              
187             # in this page?
188 0           my $startnr = $data->{start};
189 0 0         if($rank >= $startnr)
190 0           { my $docs = $self->_docs($data);
191 0 0         if($rank - $startnr < @$docs)
192 0           { my $doc = $docs->[$rank - $startnr];
193 0           return Apache::Solr::Document->fromResult($doc, $rank);
194             }
195             }
196              
197             $rank < $data->{numFound} # outside answer range
198 0 0         or return ();
199            
200 0           my $pagenr = $self->selectedPageNr($rank);
201 0   0       my $page = $self->selectedPage($pagenr)
202             || $self->selectedPageLoad($pagenr, $self->core);
203              
204 0           $page->selected($rank);
205             }
206              
207              
208             sub nextSelected(%)
209 0     0 1   { my $self = shift;
210 0           $self->selected($self->{ASR_next}, @_);
211             }
212              
213              
214             sub highlighted($)
215 0     0 1   { my ($self, $doc) = @_;
216 0           my $rank = $doc->rank;
217 0           my $pagenr = $self->selectedPageNr($rank);
218             my $hl = $self->selectedPage($pagenr)->decoded->{highlighting}
219 0 0         or error __x"there is no highlighting information in the result";
220 0           Apache::Solr::Document->fromResult($hl->{$doc->uniqueId}, $rank);
221             }
222              
223             #--------------------------
224              
225             sub terms($;$)
226 0     0 1   { my ($self, $field) = (shift, shift);
227 0 0         return $self->{ASR_terms}{$field} = shift if @_;
228              
229 0 0         my $r = $self->{ASR_terms}{$field}
230             or error __x"no search for terms on field {field} requested"
231             , field => $field;
232              
233 0           $r;
234             }
235              
236             #--------------------------
237              
238 0     0     sub _to_msec($) { sprintf "%.1f", $_[0] * 1000 }
239              
240             sub showTimings(;$)
241 0     0 1   { my ($self, $fh) = @_;
242 0   0       $fh ||= select;
243 0           my $req = $self->request;
244 0 0         my $to = $req ? $req->uri : '(not set yet)';
245 0           my $start = localtime $self->{ASR_start};
246              
247 0           $fh->print("endpoint: $to\nstart: $start\n");
248              
249 0 0         if($req)
250 0           { my $reqsize = length($req->as_string);
251 0           my $reqcons = _to_msec($self->{ASR_req_out} - $self->{ASR_start});
252 0           $fh->print("request: constructed $reqsize bytes in $reqcons ms\n");
253             }
254              
255 0 0         if(my $resp = $self->response)
256 0           { my $respsize = length($resp->as_string);
257 0           my $respcons = _to_msec($self->{ASR_resp_in} - $self->{ASR_req_out});
258 0           $fh->print("response: received $respsize bytes after $respcons ms\n");
259 0           my $ct = $resp->content_type;
260 0           my $status = $resp->status_line;
261 0           $fh->print(" $ct, $status\n");
262             }
263              
264 0 0         if(my $dec = $self->decoded)
265 0           { my $decoder = _to_msec($self->{ASR_dec_done} - $self->{ASR_resp_in});
266 0           $fh->print("decoding: completed in $decoder ms\n");
267 0 0         if(defined(my $qt = $self->solrQTime))
268 0           { $fh->print(" solr processing took "._to_msec($qt)." ms\n");
269             }
270 0 0         if(my $error = $self->solrError)
271 0           { $fh->print(" solr reported error: '$error'\n");
272             }
273 0           my $total = _to_msec($self->{ASR_dec_done} - $self->{ASR_start});
274 0           $fh->print("elapse: $total ms total\n");
275             }
276             }
277              
278              
279 0 0   0 1   sub selectedPageNr($) { my $pz = shift->fullPageSize; $pz ? int(shift() / $pz) : 0 }
  0            
280 0     0 0   sub selectPages() { @{shift->{ASR_pages}} }
  0            
281              
282              
283 0     0 1   sub selectedPage($) { my $pages = shift->{ASR_pages}; $pages->[shift()] }
  0            
284              
285              
286             # The reloads page 0, which may have been purged by sequential reading. Besided,
287             # the name does not cover its content: it's not the size of the select page but
288             # the first page.
289             sub selectedPageSize()
290 0   0 0 1   { my $result = shift->selectedPage(0)->_responseData || {};
291 0   0       my $docs = $result->{doc} // $result->{docs};
292 0 0         ref $docs eq 'HASH' ? 1 : ref $docs eq 'ARRAY' ? scalar @$docs : 50;
    0          
293             }
294              
295              
296 0   0 0 1   sub fullPageSize() { my $self = shift; $self->{ASR_fpz} ||= $self->_calc_page_size }
  0            
297              
298             sub _calc_page_size()
299 0     0     { my $self = shift;
300 0           my $docs = $self->_docs($self->selectedPage(0)->_responseData);
301             #warn "CALC PZ=", scalar @$docs;
302 0           scalar @$docs;
303             }
304              
305              
306             sub selectedPageLoad($;$)
307 0     0 1   { my ($self, $pagenr, $client) = @_;
308 0 0         $client
309             or error __x"cannot autoload page {nr}, no client provided"
310             , nr => $pagenr;
311              
312 0           my $fpz = $self->fullPageSize;
313 0           my @params = $self->replaceParams
314             ( { start => $pagenr * $fpz, rows => $fpz }, $self->params);
315              
316 0           my $seq = $self->sequential;
317 0           my $page = $client->select({sequential => $seq, _fpz => $fpz}, @params);
318 0           my $pages = $self->{ASR_pages};
319              
320             # put new page in shared table of pages
321 0           $pages->[$pagenr] = $page;
322 0           $page->_pageset($pages);
323              
324             # purge cached previous pages when in sequential mode
325 0 0 0       if($seq && $pagenr != 0)
326 0           { $pages->[$_] = undef for 0..$pagenr-1;
327             }
328              
329 0           $page;
330             }
331              
332              
333             sub replaceParams($@)
334 0     0 1   { my ($self, $new) = (shift, shift);
335 0           my @out;
336 0           while(@_)
337 0           { my ($k, $v) = (shift, shift);
338 0 0         $v = delete $new->{$k} if $new->{$k};
339 0           push @out, $k => $v;
340             }
341 0           (@out, %$new);
342             }
343              
344             1;