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   58619 use strict;
  2         11  
  2         53  
4 2     2   10 use warnings;
  2         2  
  2         43  
5              
6 2     2   9 use Scalar::Util;
  2         2  
  2         76  
7              
8 2     2   340 use WARC::Index;
  2         5  
  2         48  
9 2     2   871 use WARC::Index::Builder;
  2         4  
  2         51  
10 2     2   9 use WARC::Index::Entry;
  2         4  
  2         62  
11              
12             our @ISA = qw(WARC::Index WARC::Index::Builder);
13              
14 2     2   9 use WARC; *WARC::Index::Volatile::VERSION = \$WARC::VERSION;
  2         4  
  2         49  
15              
16 2     2   8 use Carp;
  2         3  
  2         3988  
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   300 my $self = shift;
45              
46             my $out =
47 3         7 (ref $self)." over ".(scalar @{$self->{volumes}})." volume(s):\n";
  3         10  
48 3         5 $out .= " $_\n" for map $_->filename, @{$self->{volumes}};
  3         11  
49              
50 3         11 return $out;
51             }
52              
53             # implement WARC::Index interface
54 5     5 1 526 sub attach { my $class = shift; $class->build(from => \@_) }
  5         15  
55              
56             # override build method inherited from WARC::Index
57             sub build {
58 13     13 1 2973 my $class = shift;
59              
60 13         35 my @columns = @Default_Column_Set;
61 13         23 my @from = ();
62              
63 13 100       37 unless (defined wantarray)
64 1         115 { carp "building volatile index in void context"; return }
  1         8  
65              
66 12         39 while (@_) {
67 17         22 my $key = shift;
68 17 100       47 if ($key eq 'from') {
    100          
69 10 100       41 if (UNIVERSAL::isa($_[0], 'ARRAY')) { @from = @{(shift)} }
  8         14  
  8         25  
70 2         8 else { @from = splice @_ }
71 6         11 } elsif ($key eq 'columns') { @columns = @{(shift)} }
  6         20  
72 1         218 else { croak "unknown option '$key' building volatile index" }
73             }
74              
75 11         24 my $_dvmap = \%WARC::Index::Entry::_distance_value_map;
76             croak "unknown index column requested"
77 11 100       153 if grep !defined $_dvmap->{$_}, @columns;
78              
79             my $index = bless
80 13         62 { by => {(map {$_ => {}} (grep $_dvmap->{$_}[0] eq 'exact', @columns)),
81 10         46 (map {$_ => []} (grep $_dvmap->{$_}[0] eq 'prefix', @columns))}
  2         8  
82             }, $class;
83              
84 10         51 $index->add($_) for @from;
85              
86 10         19 { our $_total_constructed; $_total_constructed++ }
  10         12  
  10         18  
87              
88 10         46 return $index;
89             }
90 10     10   2070 sub DESTROY { our $_total_destroyed; $_total_destroyed++ }
  10         464  
91              
92             sub searchable {
93 3     3 1 7 my $self = shift;
94 3         5 my $key = shift;
95              
96 3         14 return defined $self->{by}{$key};
97             }
98              
99             sub search {
100 57     57 1 3360 my $self = shift;
101              
102 57 100       115 unless (defined wantarray)
103 1         107 { carp "calling 'search' method in void context"; return }
  1         58  
104              
105 56 100       180 croak "no arguments given to 'search' method"
106             unless scalar @_;
107 55 100       180 croak "odd number of arguments given to 'search' method"
108             if scalar @_ % 2;
109              
110 54         64 my $key = undef; my $val = undef;
  54         71  
111 54         115 for (my $i = 0; $i < $#_; $i += 2)
112 71 100       237 { (($key, $val) = @_[$i,1+$i]), last if ref $self->{by}{$_[$i]} eq 'ARRAY' }
113 54         109 for (my $i = 0; $i < $#_; $i += 2)
114 54 100       172 { (($key, $val) = @_[$i,1+$i]), last if ref $self->{by}{$_[$i]} eq 'HASH' }
115 54 100       198 croak "no usable search key" unless $key;
116              
117 53         79 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         68 my $rows;
121 53 100       90 if ($mode eq 'exact') {
    100          
122 51         109 $rows = $self->{by}{$key}{$val};
123             } elsif ($mode eq 'prefix') {
124 1         3 $rows = [grep $_->value($refkey) =~ m/^\Q$val/, @{$self->{by}{$key}}];
  1         4  
125 1         13 } else { die "unimplemented search mode $mode" }
126              
127 52 100       81 if (wantarray)
128 49         97 { return grep { $_->distance(@_) >= 0 } @$rows }
  68         179  
129             else {
130 3         3 my $result = undef; my $result_distance = -1;
  3         5  
131 3         7 foreach my $entry (@$rows) {
132 13         24 my $distance = $entry->distance(@_);
133 13 100       22 unless (0 > $distance) {
134 10 100 100     29 if ($result_distance < 0 # first match found
135             or $distance < $result_distance) # or better match found
136 4         5 { $result = $entry; $result_distance = $distance }
  4         5  
137             }
138 13 100       24 return $result if $result_distance == 0; # no better match possible
139             }
140 1         3 return $result;
141             }
142             }
143              
144             sub first_entry {
145 9     9 1 2141 my $self = shift;
146              
147 9         29 return $self->{entries}{$self->{volumes}[0]->_file_tag}[0];
148             }
149              
150             # implement WARC::Index::Builder interface
151             sub _intern_volume ($$) {
152 244     244   280 my $index = shift;
153 244         263 my $volume = shift;
154              
155 244         489 my $voltag = $volume->_file_tag;
156             $index->{volume_position_by_tag}{$voltag} =
157 13         49 ((push @{$index->{volumes}}, $volume) - 1)
158 244 100       837 unless defined $index->{volume_position_by_tag}{$voltag};
159 244         424 $volume = $index->{volumes}[$index->{volume_position_by_tag}{$voltag}];
160              
161 244         562 return $volume, $voltag;
162             }
163             sub _index_entry ($$) {
164 238     238   263 my $index = shift;
165 238         292 my $entry = shift;
166              
167 238         275 foreach my $key (keys %{$index->{by}}) {
  238         597  
168 367         565 my $refkey = $WARC::Index::Entry::_distance_value_map{$key}[1];
169 367 100       567 next unless defined $entry->value($refkey);
170 353 100       718 if (ref $index->{by}{$key} eq 'HASH')
    100          
171 306         377 { push @{$index->{by}{$key}{$entry->{$refkey}}}, $entry }
  306         1270  
172             elsif (ref $index->{by}{$key} eq 'ARRAY')
173 46         56 { push @{$index->{by}{$key}}, $entry } # defer sort to outer call
  46         151  
174 1         9 else { die "unknown object in $key index slot" }
175             }
176             }
177              
178             sub _add_record {
179 218     218   280 my $index = shift;
180 218         248 my $record = shift;
181              
182 218         278 my $volume; my $voltag;
183 218         412 ($volume, $voltag) = _intern_volume $index, $record->volume;
184              
185 218         637 my $offset = $record->offset;
186 218         932 my $entry = WARC::Index::Volatile::Entry->_new
187             ( _index => $index, _volume => $volume, _record_offset => $offset );
188              
189             # intern entry
190 218 100       525 return if defined $index->{entry_position_by_tag_offset}{$voltag}{$offset};
191             $index->{entry_position_by_tag_offset}{$voltag}{$offset} =
192 213         247 ((push @{$index->{entries}{$voltag}}, $entry) - 1);
  213         660  
193              
194             # populate entry
195 213         490 $entry->{time} = $record->date;
196 213         485 $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     489 && $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     703 && defined $record->field('WARC-Target-URI');
      100        
203              
204 213         393 _index_entry $index, $entry;
205             }
206              
207             sub _add_entry ($$) {
208 51     51   62 my $index = shift;
209 51         53 my $source = shift;
210              
211 51 100       53 if (grep !defined $source->value($_), keys %{$index->{by}})
  51         127  
212             # at least one column not in source index; index the record instead
213 25         55 { $index->_add_record($source->record) }
214             else {
215             # volume and offset can be retrieved from a stub record without I/O
216 26         70 my $rstub = $source->record; my $volume; my $voltag;
  26         35  
217 26         54 ($volume, $voltag) = _intern_volume $index, $rstub->volume;
218 26         64 my $offset = $rstub->offset;
219             my $entry = WARC::Index::Volatile::Entry->_new
220             ( _index => $index, _volume => $volume, _record_offset => $offset,
221 26         81 map { $_ => $source->value($_) } keys %{$index->{by}} );
  27         40  
  26         59  
222              
223             # intern entry
224 26 100       63 return if defined $index->{entry_position_by_tag_offset}{$voltag}{$offset};
225             $index->{entry_position_by_tag_offset}{$voltag}{$offset} =
226 25         29 ((push @{$index->{entries}{$voltag}}, $entry) - 1);
  25         65  
227              
228 25         40 _index_entry $index, $entry;
229             }
230             }
231              
232             sub add {
233 17     17 1 755 my $self = shift;
234              
235 17         81 $self->SUPER::add(@_);
236              
237             # sort any array-based columns
238 15         28 foreach my $key (keys %{$self->{by}}) {
  15         47  
239 23 100       71 next unless ref $self->{by}{$key} eq 'ARRAY';
240 2         5 my $refkey = $WARC::Index::Entry::_distance_value_map{$key}[1];
241 2         8 @{$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   279 sub index { (shift)->{_index} }
266 489     489   1065 sub volume { (shift)->{_volume} }
267 431     431   1448 sub record_offset { (shift)->{_record_offset} }
268              
269             sub next {
270 208     208   24960 my $self = shift;
271              
272 208         306 my $idx = $self->index;
273 208         298 my $vt = $self->volume->_file_tag;
274 208         541 my $off = $self->record_offset;
275              
276             my $next = $idx->{entries}{$vt}
277 208         483 [1+$idx->{entry_position_by_tag_offset}{$vt}{$off}];
278 208 100       760 return $next if defined $next;
279              
280 10         23 my $nextvol = $idx->{volumes}[1+$idx->{volume_position_by_tag}{$vt}];
281 10 100       25 $next = $idx->{entries}{$nextvol->_file_tag}[0] if defined $nextvol;
282              
283 10         34 return $next;
284             }
285              
286             sub value {
287 1197     1197   2956 my $self = shift;
288 1197         1250 my $key = shift;
289              
290 1197 100       1930 return undef if $key =~ m/^_/;
291 1196         2426 return $self->{$key};
292             }
293              
294             sub _new {
295 244     244   332 my $class = shift;
296              
297 244         653 my $entry = bless { @_ }, $class;
298 244         732 Scalar::Util::weaken $entry->{_index};
299 244         340 return $entry;
300             }
301             }
302              
303             1;
304             __END__