File Coverage

blib/lib/WARC/Index/Volatile.pm
Criterion Covered Total %
statement 178 178 100.0
branch 59 60 98.3
condition 10 12 83.3
subroutine 26 26 100.0
pod 6 6 100.0
total 279 282 98.9


line stmt bran cond sub pod time code
1             package WARC::Index::Volatile; # -*- CPerl -*-
2              
3 2     2   58576 use strict;
  2         11  
  2         49  
4 2     2   9 use warnings;
  2         3  
  2         41  
5              
6 2     2   8 use Scalar::Util;
  2         3  
  2         73  
7              
8 2     2   343 use WARC::Index;
  2         4  
  2         44  
9 2     2   743 use WARC::Index::Builder;
  2         4  
  2         48  
10 2     2   9 use WARC::Index::Entry;
  2         3  
  2         58  
11              
12             our @ISA = qw(WARC::Index WARC::Index::Builder);
13              
14 2     2   8 use WARC; *WARC::Index::Volatile::VERSION = \$WARC::VERSION;
  2         3  
  2         44  
15              
16 2     2   8 use Carp;
  2         3  
  2         3585  
17              
18             require WARC::Volume;
19              
20             our @Default_Column_Set = qw/record_id/;
21              
22             WARC::Index::register(filename => qr/[.]warc(?:[.]gz)?$/);
23              
24             # This implementation uses a hash as the underlying structure.
25             # Keys defined by this class:
26             #
27             # volumes
28             # array of WARC::Volume objects known to this index
29             # -- This field is used to intern volume objects within an index.
30             # volume_position_by_tag
31             # hash mapping volume tags to positions in volumes array
32             # -- This field is used to intern volume objects within an index.
33             # entries
34             # hash mapping volume tags to arrays of index entries
35             # -- This field is used to intern index entries within an index.
36             # entry_position_by_tag_offset
37             # hash mapping volume tags to hashes mapping record offset pairs to
38             # positions in entries arrays
39             # -- This field is used to intern index entries within an index.
40             # by
41             # nested hash mapping indexed columns and values to arrays of entries
42              
43             sub _dbg_dump {
44 3     3   477 my $self = shift;
45              
46             my $out =
47 3         11 (ref $self)." over ".(scalar @{$self->{volumes}})." volume(s):\n";
  3         8  
48 3         6 $out .= " $_\n" for map $_->filename, @{$self->{volumes}};
  3         10  
49              
50 3         14 return $out;
51             }
52              
53             # implement WARC::Index interface
54 5     5 1 768 sub attach { my $class = shift; $class->build(from => \@_) }
  5         17  
55              
56             # override build method inherited from WARC::Index
57             sub build {
58 13     13 1 3604 my $class = shift;
59              
60 13         33 my @columns = @Default_Column_Set;
61 13         26 my @from = ();
62              
63 13 100       43 unless (defined wantarray)
64 1         116 { carp "building volatile index in void context"; return }
  1         9  
65              
66 12         32 while (@_) {
67 17         28 my $key = shift;
68 17 100       53 if ($key eq 'from') {
    100          
69 10 100       50 if (UNIVERSAL::isa($_[0], 'ARRAY')) { @from = @{(shift)} }
  8         16  
  8         25  
70 2         8 else { @from = splice @_ }
71 6         9 } elsif ($key eq 'columns') { @columns = @{(shift)} }
  6         25  
72 1         205 else { croak "unknown option '$key' building volatile index" }
73             }
74              
75 11         25 my $_dvmap = \%WARC::Index::Entry::_distance_value_map;
76             croak "unknown index column requested"
77 11 100       198 if grep !defined $_dvmap->{$_}, @columns;
78              
79             my $index = bless
80 13         67 { by => {(map {$_ => {}} (grep $_dvmap->{$_}[0] eq 'exact', @columns)),
81 10         40 (map {$_ => []} (grep $_dvmap->{$_}[0] eq 'prefix', @columns))}
  2         8  
82             }, $class;
83              
84 10         40 $index->add($_) for @from;
85              
86 10         19 { our $_total_constructed; $_total_constructed++ }
  10         11  
  10         19  
87              
88 10         47 return $index;
89             }
90 10     10   2649 sub DESTROY { our $_total_destroyed; $_total_destroyed++ }
  10         497  
91              
92             sub searchable {
93 3     3 1 6 my $self = shift;
94 3         6 my $key = shift;
95              
96 3         22 return defined $self->{by}{$key};
97             }
98              
99             sub search {
100 57     57 1 4049 my $self = shift;
101              
102 57 100       110 unless (defined wantarray)
103 1         110 { carp "calling 'search' method in void context"; return }
  1         60  
104              
105 56 100       171 croak "no arguments given to 'search' method"
106             unless scalar @_;
107 55 100       205 croak "odd number of arguments given to 'search' method"
108             if scalar @_ % 2;
109              
110 54         61 my $key = undef; my $val = undef;
  54         57  
111 54         119 for (my $i = 0; $i < $#_; $i += 2)
112 71 100       239 { (($key, $val) = @_[$i,1+$i]), last if ref $self->{by}{$_[$i]} eq 'ARRAY' }
113 54         118 for (my $i = 0; $i < $#_; $i += 2)
114 54 100       167 { (($key, $val) = @_[$i,1+$i]), last if ref $self->{by}{$_[$i]} eq 'HASH' }
115 54 100       180 croak "no usable search key" unless $key;
116              
117 53         78 my $mode = $WARC::Index::Entry::_distance_value_map{$key}[0];
118 53         68 my $refkey = $WARC::Index::Entry::_distance_value_map{$key}[1];
119              
120 53         58 my $rows;
121 53 100       90 if ($mode eq 'exact') {
    100          
122 51         92 $rows = $self->{by}{$key}{$val};
123             } elsif ($mode eq 'prefix') {
124 1         3 $rows = [grep $_->value($refkey) =~ m/^\Q$val/, @{$self->{by}{$key}}];
  1         24  
125 1         11 } else { die "unimplemented search mode $mode" }
126              
127 52 100       76 if (wantarray)
128 49         91 { return grep { $_->distance(@_) >= 0 } @$rows }
  68         162  
129             else {
130 3         4 my $result = undef; my $result_distance = -1;
  3         4  
131 3         7 foreach my $entry (@$rows) {
132 13         29 my $distance = $entry->distance(@_);
133 13 100       24 unless (0 > $distance) {
134 10 100 100     30 if ($result_distance < 0 # first match found
135             or $distance < $result_distance) # or better match found
136 4         6 { $result = $entry; $result_distance = $distance }
  4         4  
137             }
138 13 100       26 return $result if $result_distance == 0; # no better match possible
139             }
140 1         4 return $result;
141             }
142             }
143              
144             sub first_entry {
145 9     9 1 2787 my $self = shift;
146              
147 9         30 return $self->{entries}{$self->{volumes}[0]->_file_tag}[0];
148             }
149              
150             # implement WARC::Index::Builder interface
151             sub _intern_volume ($$) {
152 244     244   312 my $index = shift;
153 244         297 my $volume = shift;
154              
155 244         476 my $voltag = $volume->_file_tag;
156             $index->{volume_position_by_tag}{$voltag} =
157 13         52 ((push @{$index->{volumes}}, $volume) - 1)
158 244 100       817 unless defined $index->{volume_position_by_tag}{$voltag};
159 244         419 $volume = $index->{volumes}[$index->{volume_position_by_tag}{$voltag}];
160              
161 244         571 return $volume, $voltag;
162             }
163             sub _index_entry ($$) {
164 238     238   262 my $index = shift;
165 238         258 my $entry = shift;
166              
167 238         275 foreach my $key (keys %{$index->{by}}) {
  238         559  
168 368         581 my $refkey = $WARC::Index::Entry::_distance_value_map{$key}[1];
169 368 100       569 next unless defined $entry->value($refkey);
170 353 100       737 if (ref $index->{by}{$key} eq 'HASH')
    100          
171 306         352 { push @{$index->{by}{$key}{$entry->{$refkey}}}, $entry }
  306         1331  
172             elsif (ref $index->{by}{$key} eq 'ARRAY')
173 46         56 { push @{$index->{by}{$key}}, $entry } # defer sort to outer call
  46         140  
174 1         10 else { die "unknown object in $key index slot" }
175             }
176             }
177              
178             sub _add_record {
179 218     218   304 my $index = shift;
180 218         253 my $record = shift;
181              
182 218         287 my $volume; my $voltag;
183 218         416 ($volume, $voltag) = _intern_volume $index, $record->volume;
184              
185 218         596 my $offset = $record->offset;
186 218         942 my $entry = WARC::Index::Volatile::Entry->_new
187             ( _index => $index, _volume => $volume, _record_offset => $offset );
188              
189             # intern entry
190 218 100       523 return if defined $index->{entry_position_by_tag_offset}{$voltag}{$offset};
191             $index->{entry_position_by_tag_offset}{$voltag}{$offset} =
192 213         256 ((push @{$index->{entries}{$voltag}}, $entry) - 1);
  213         615  
193              
194             # populate entry
195 213         519 $entry->{time} = $record->date;
196 213         481 $entry->{record_id} = $record->id;
197             $entry->{segment_origin_id} = $record->field('WARC-Segment-Origin-ID')
198             if exists $index->{by}{segment_origin_id}
199 213 50 33     464 && $record->type eq 'continuation';
200             $entry->{url} = $record->field('WARC-Target-URI')
201             if (exists $index->{by}{url} || exists $index->{by}{url_prefix})
202 213 100 100     713 && defined $record->field('WARC-Target-URI');
      100        
203              
204 213         405 _index_entry $index, $entry;
205             }
206              
207             sub _add_entry ($$) {
208 51     51   65 my $index = shift;
209 51         95 my $source = shift;
210              
211 51 100       59 if (grep !defined $source->value($_), keys %{$index->{by}})
  51         135  
212             # at least one column not in source index; index the record instead
213 25         61 { $index->_add_record($source->record) }
214             else {
215             # volume and offset can be retrieved from a stub record without I/O
216 26         57 my $rstub = $source->record; my $volume; my $voltag;
  26         35  
217 26         54 ($volume, $voltag) = _intern_volume $index, $rstub->volume;
218 26         61 my $offset = $rstub->offset;
219             my $entry = WARC::Index::Volatile::Entry->_new
220             ( _index => $index, _volume => $volume, _record_offset => $offset,
221 26         34 map { $_ => $source->value($_) } keys %{$index->{by}} );
  27         48  
  26         69  
222              
223             # intern entry
224 26 100       67 return if defined $index->{entry_position_by_tag_offset}{$voltag}{$offset};
225             $index->{entry_position_by_tag_offset}{$voltag}{$offset} =
226 25         28 ((push @{$index->{entries}{$voltag}}, $entry) - 1);
  25         56  
227              
228 25         47 _index_entry $index, $entry;
229             }
230             }
231              
232             sub add {
233 17     17 1 1122 my $self = shift;
234              
235 17         96 $self->SUPER::add(@_);
236              
237             # sort any array-based columns
238 15         27 foreach my $key (keys %{$self->{by}}) {
  15         54  
239 23 100       70 next unless ref $self->{by}{$key} eq 'ARRAY';
240 2         5 my $refkey = $WARC::Index::Entry::_distance_value_map{$key}[1];
241 2         10 @{$self->{by}{$key}} =
242 2         4 sort { $a->value($refkey) cmp $b->value($refkey) } @{$self->{by}{$key}};
  144         181  
  2         10  
243             }
244             }
245              
246             {
247             package WARC::Index::Volatile::Entry;
248              
249             our @ISA = qw(WARC::Index::Entry);
250              
251             # This implementation uses a hash as the underlying structure.
252             #
253             # Accessible search keys are stored directly in the hash, while internal
254             # values are stored with names with a leading underscore.
255              
256             # Keys defined by this class:
257             #
258             # _index
259             # weak reference to parent index
260             # _volume
261             # reference to volume containing record
262             # _record_offset
263             # offset of record within containing volume
264              
265 208     208   297 sub index { (shift)->{_index} }
266 489     489   958 sub volume { (shift)->{_volume} }
267 431     431   1379 sub record_offset { (shift)->{_record_offset} }
268              
269             sub next {
270 208     208   28997 my $self = shift;
271              
272 208         365 my $idx = $self->index;
273 208         325 my $vt = $self->volume->_file_tag;
274 208         563 my $off = $self->record_offset;
275              
276             my $next = $idx->{entries}{$vt}
277 208         530 [1+$idx->{entry_position_by_tag_offset}{$vt}{$off}];
278 208 100       852 return $next if defined $next;
279              
280 10         26 my $nextvol = $idx->{volumes}[1+$idx->{volume_position_by_tag}{$vt}];
281 10 100       26 $next = $idx->{entries}{$nextvol->_file_tag}[0] if defined $nextvol;
282              
283 10         34 return $next;
284             }
285              
286             sub value {
287 1198     1198   3395 my $self = shift;
288 1198         1266 my $key = shift;
289              
290 1198 100       1813 return undef if $key =~ m/^_/;
291 1197         2386 return $self->{$key};
292             }
293              
294             sub _new {
295 244     244   313 my $class = shift;
296              
297 244         742 my $entry = bless { @_ }, $class;
298 244         768 Scalar::Util::weaken $entry->{_index};
299 244         369 return $entry;
300             }
301             }
302              
303             1;
304             __END__