File Coverage

lib/Apache/Solr/Result.pm
Criterion Covered Total %
statement 27 195 13.8
branch 0 80 0.0
condition 0 47 0.0
subroutine 9 45 20.0
pod 29 31 93.5
total 65 398 16.3


line stmt bran cond sub pod time code
1             # Copyrights 2012-2025 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             our $VERSION = '1.11';
11             }
12              
13              
14 6     6   1255 use warnings;
  6         11  
  6         337  
15 6     6   48 no warnings 'recursion'; # linked list of pages can get deep
  6         10  
  6         290  
16              
17 6     6   28 use strict;
  6         9  
  6         136  
18              
19 6     6   22 use Log::Report qw(solr);
  6         8  
  6         33  
20 6     6   1776 use Time::HiRes qw(time);
  6         17  
  6         59  
21 6     6   472 use Scalar::Util qw(weaken);
  6         25  
  6         323  
22              
23 6     6   2123 use Apache::Solr::Document ();
  6         10  
  6         161  
24              
25 6     6   50 use Data::Dumper;
  6         9  
  6         515  
26             $Data::Dumper::Indent = 1;
27             $Data::Dumper::Quotekeys = 0;
28              
29              
30             use overload
31 6         53 '""' => 'endpoint'
32 6     6   36 , bool => 'success';
  6         17  
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              
40 0   0       my $p = $args->{params} || [];
41 0 0         ($p, my $params) = ref $p eq 'HASH' ? ( +[%$p], $p ) : ($p, +{@$p});
42 0           $self->{ASR_params} = $p;
43              
44 0 0         $self->{ASR_endpoint} = $args->{endpoint} or panic;
45 0           $self->{ASR_start} = time;
46 0           $self->request($args->{request});
47 0           $self->response($args->{response});
48              
49 0           $self->{ASR_pages} = [ $self ]; # first has non-weak page-table
50 0           weaken $self->{ASR_pages}[0]; # no reference loop!
51              
52 0 0         if($self->{ASR_core} = $args->{core}) { weaken $self->{ASR_core} }
  0            
53 0   0       $self->{ASR_next} = $params->{start} || 0;
54 0   0       $self->{ASR_seq} = $args->{sequential} || 0;
55 0           $self->{ASR_fpz} = $args->{_fpz};
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) || $self->selectedPageLoad($pagenr, $self->core);
202 0           $page->selected($rank);
203             }
204              
205              
206             sub nextSelected(%)
207 0     0 1   { my $self = shift;
208 0           $self->selected($self->{ASR_next}, @_);
209             }
210              
211              
212             sub highlighted($)
213 0     0 1   { my ($self, $doc) = @_;
214 0           my $rank = $doc->rank;
215 0           my $pagenr = $self->selectedPageNr($rank);
216             my $hl = $self->selectedPage($pagenr)->decoded->{highlighting}
217 0 0         or error __x"There is no highlighting information in the result";
218 0           Apache::Solr::Document->fromResult($hl->{$doc->uniqueId}, $rank);
219             }
220              
221             #--------------------------
222              
223             sub terms($;$)
224 0     0 1   { my ($self, $field) = (shift, shift);
225 0 0         return $self->{ASR_terms}{$field} = shift if @_;
226              
227 0 0         my $r = $self->{ASR_terms}{$field}
228             or error __x"No search for terms on field {field} requested", field => $field;
229              
230 0           $r;
231             }
232              
233             #--------------------------
234              
235 0     0     sub _to_msec($) { sprintf "%.1f", $_[0] * 1000 }
236              
237             sub showTimings(;$)
238 0     0 1   { my ($self, $fh) = @_;
239 0   0       $fh ||= select;
240 0           my $req = $self->request;
241 0 0         my $to = $req ? $req->uri : '(not set yet)';
242 0           my $start = localtime $self->{ASR_start};
243              
244 0           $fh->print("endpoint: $to\nstart: $start\n");
245              
246 0 0         if($req)
247 0           { my $reqsize = length($req->as_string);
248 0           my $reqcons = _to_msec($self->{ASR_req_out} - $self->{ASR_start});
249 0           $fh->print("request: constructed $reqsize bytes in $reqcons ms\n");
250             }
251              
252 0 0         if(my $resp = $self->response)
253 0           { my $respsize = length($resp->as_string);
254 0           my $respcons = _to_msec($self->{ASR_resp_in} - $self->{ASR_req_out});
255 0           $fh->print("response: received $respsize bytes after $respcons ms\n");
256 0           my $ct = $resp->content_type;
257 0           my $status = $resp->status_line;
258 0           $fh->print(" $ct, $status\n");
259             }
260              
261 0 0         if(my $dec = $self->decoded)
262 0           { my $decoder = _to_msec($self->{ASR_dec_done} - $self->{ASR_resp_in});
263 0           $fh->print("decoding: completed in $decoder ms\n");
264 0 0         if(defined(my $qt = $self->solrQTime))
265 0           { $fh->print(" solr processing took "._to_msec($qt)." ms\n");
266             }
267 0 0         if(my $error = $self->solrError)
268 0           { $fh->print(" solr reported error: '$error'\n");
269             }
270 0           my $total = _to_msec($self->{ASR_dec_done} - $self->{ASR_start});
271 0           $fh->print("elapse: $total ms total\n");
272             }
273             }
274              
275              
276 0 0   0 1   sub selectedPageNr($) { my $pz = shift->fullPageSize; $pz ? int(shift() / $pz) : 0 }
  0            
277 0     0 0   sub selectPages() { @{shift->{ASR_pages}} }
  0            
278              
279              
280 0     0 1   sub selectedPage($) { my $pages = shift->{ASR_pages}; $pages->[shift()] }
  0            
281              
282              
283             # The reloads page 0, which may have been purged by sequential reading. Besided,
284             # the name does not cover its content: it's not the size of the select page but
285             # the first page.
286             sub selectedPageSize()
287 0   0 0 1   { my $result = shift->selectedPage(0)->_responseData || {};
288 0   0       my $docs = $result->{doc} // $result->{docs};
289 0 0         ref $docs eq 'HASH' ? 1 : ref $docs eq 'ARRAY' ? scalar @$docs : 50;
    0          
290             }
291              
292              
293 0   0 0 1   sub fullPageSize() { my $self = shift; $self->{ASR_fpz} ||= $self->_calc_page_size }
  0            
294              
295             sub _calc_page_size()
296 0     0     { my $self = shift;
297 0           my $docs = $self->_docs($self->selectedPage(0)->_responseData);
298             #warn "CALC PZ=", scalar @$docs;
299 0           scalar @$docs;
300             }
301              
302              
303             sub selectedPageLoad($;$)
304 0     0 1   { my ($self, $pagenr, $client) = @_;
305 0 0         $client
306             or error __x"Cannot autoload page {nr}, no client provided", nr => $pagenr;
307              
308 0           my $fpz = $self->fullPageSize;
309 0           my @params = $self->replaceParams( { start => $pagenr * $fpz, rows => $fpz }, $self->params);
310              
311 0           my $seq = $self->sequential;
312 0           my $page = $client->select({sequential => $seq, _fpz => $fpz}, @params);
313 0           my $pages = $self->{ASR_pages};
314              
315             # put new page in shared table of pages
316 0           $pages->[$pagenr] = $page;
317 0           $page->_pageset($pages);
318              
319             # purge cached previous pages when in sequential mode
320 0 0 0       if($seq && $pagenr != 0)
321 0           { $pages->[$_] = undef for 0..$pagenr-1;
322             }
323              
324 0           $page;
325             }
326              
327              
328             sub replaceParams($@)
329 0     0 1   { my ($self, $new) = (shift, shift);
330 0           my @out;
331 0           while(@_)
332 0           { my ($k, $v) = (shift, shift);
333 0 0         $v = delete $new->{$k} if $new->{$k};
334 0           push @out, $k => $v;
335             }
336 0           (@out, %$new);
337             }
338              
339             1;