File Coverage

blib/lib/Text/SimSearch.pm
Criterion Covered Total %
statement 18 112 16.0
branch 0 10 0.0
condition 0 4 0.0
subroutine 6 13 46.1
pod 0 5 0.0
total 24 144 16.6


line stmt bran cond sub pod time code
1             package Text::SimSearch;
2 1     1   5 use strict;
  1         1  
  1         32  
3 1     1   4 use warnings;
  1         1  
  1         22  
4 1     1   3 use strict;
  1         1  
  1         19  
5 1     1   3 use warnings;
  1         2  
  1         26  
6 1     1   959 use Storable qw( nstore retrieve);
  1         12267  
  1         179  
7 1     1   1513 use Time::HiRes qw(gettimeofday tv_interval);
  1         3043  
  1         6  
8              
9             our $VERSION = '0.02';
10              
11             sub new {
12 0     0 0   my $class = shift;
13 0           my $self = bless {@_}, $class;
14 0           return $self;
15             }
16              
17             sub add_item_from_file {
18 0     0 0   my $self = shift;
19 0           my $file = shift;
20 0   0       my $max_posting_size = shift || 1000;
21              
22 0           my $tmp_data;
23             my $labels;
24 0 0         open my $fh, "<", $file
25             or die("can not open $file");
26 0           my $i = 0;
27 0           while (<$fh>) {
28 0           chomp $_;
29 0           my @f = split "\t", $_;
30 0           my $label = shift @f;
31 0           $labels->[$i] = $label;
32              
33 0           my %vec = @f;
34 0           my $vec = $self->_unit_length( \%vec );
35 0           while ( my ( $key, $val ) = each %$vec ) {
36 0 0         next unless $val > 0;
37 0           $tmp_data->{$key}->{$i} = $val;
38             }
39              
40 0           $i++;
41             }
42 0           close($fh);
43              
44             # make "Posting-Lists" from $tmp_data created above.
45             # Note: concatenate the label-ID and the weight as string,
46             # and then convert it to interger value.
47              
48 0           my $posting_lists;
49              
50 0           my $key_scale = int( log( int @$labels ) / log(10) ) + 1;
51 0           my $val_scale = 6;
52              
53 0           $i = 0;
54 0           while ( my ( $key, $ref ) = each %$tmp_data ) {
55              
56 0           my @array;
57 0           for ( keys %$ref ) {
58              
59 0           my $label_id = $_;
60 0           my $weight = $ref->{$label_id};
61              
62             # convert the weight to integer value.
63 0           my $condition = '%.' . $val_scale . 'f';
64 0           my $integer = sprintf( $condition, $weight ) * ( 10**$val_scale );
65              
66             # convert the label_id to integer value, and connect to $interger.
67 0           $condition = '%0' . $key_scale . 'd';
68 0           $integer .= sprintf( $condition, $label_id );
69 0           push @array, $integer;
70             }
71              
72             # cut down posting-list to suitable size and compress it.
73 0           my @tmp;
74 0           my $n = 0;
75 0           LABEL:
76 0           for ( sort { $b <=> $a } @array ) {
77 0           my $p = pack( "w*", $_ );
78 0           push @tmp, $p;
79 0 0         last LABEL if ++$n == $max_posting_size;
80             }
81              
82 0           $posting_lists->{$key} = \@tmp;
83              
84 0           $i++;
85             }
86              
87 0           $self->{index_data} = {
88             posting_lists => $posting_lists,
89             labels => $labels,
90             key_scale => $key_scale,
91             val_scale => $val_scale
92             };
93             }
94              
95             sub search {
96 0     0 0   my $self = shift;
97 0           my $query_vector = shift;
98 0   0       my $number = shift || 10;
99              
100 0           my $t0 = [gettimeofday];
101              
102 0           my $vec = $self->_unit_length($query_vector);
103 0           my $key_scale = $self->{index_data}->{key_scale};
104 0           my $val_scale = $self->{index_data}->{val_scale};
105              
106 0           my $dot_product;
107 0           while ( my ( $q_key, $q_val ) = each %$vec ) {
108 0           my $compressed_array = $self->{index_data}->{posting_lists}->{$q_key};
109 0 0         next if !$compressed_array;
110 0           LABEL:
111             my $max;
112              
113 0           for (@$compressed_array) {
114              
115             # decompress and decode
116 0           my $string = unpack( "w*", $_ );
117 0           my $count = length($string) - $key_scale;
118 0           my $val = substr( $string, 0, $count );
119 0           my $label_id = int substr( $string, $count, $key_scale );
120 0           $val = $val / ( 10**$val_scale );
121              
122             # calculate similarities
123 0           $dot_product->{$label_id} += $q_val * $val;
124             }
125             }
126              
127 0           my @list;
128 0           for (
129 0           sort { $dot_product->{$b} <=> $dot_product->{$a} }
130             keys %$dot_product
131             )
132             {
133 0           my $similarity = sprintf( "%8.6f", $dot_product->{$_} );
134 0           my $label = $self->{index_data}->{labels}->[$_];
135 0           push @list, { label => $label, similarity => $similarity };
136 0 0         last if int @list == $number;
137             }
138 0           my $elapsed = tv_interval($t0);
139              
140             return {
141 0           elapsed => $elapsed,
142             retrieved_list => \@list,
143             return_num => int @list,
144             };
145             }
146              
147             sub save {
148 0     0 0   my $self = shift;
149 0           my $save_file = shift;
150 0           my $index = $self->{index_data};
151 0           nstore( $index, $save_file );
152             }
153              
154             sub load {
155 0     0 0   my $self = shift;
156 0           my $save_file = shift;
157 0           my $index = retrieve($save_file);
158 0           $self->{index_data} = $index;
159             }
160              
161             sub _unit_length {
162 0     0     my $self = shift;
163 0           my $vec = shift;
164 0           my $ret;
165 0           my $norm = $self->_calc_norm($vec);
166 0           while ( my ( $key, $value ) = each %$vec ) {
167 0           $ret->{$key} = $value / $norm;
168             }
169 0           return $ret;
170             }
171              
172             sub _calc_norm {
173 0     0     my $self = shift;
174 0           my $vec = shift;
175              
176 0           my $norm;
177 0           for ( values %$vec ) {
178 0           $norm += $_**2;
179             }
180 0           sqrt($norm);
181             }
182              
183             1;
184             __END__