File Coverage

blib/lib/Redis/Bayes.pm
Criterion Covered Total %
statement 21 90 23.3
branch 0 24 0.0
condition 0 5 0.0
subroutine 7 19 36.8
pod 4 4 100.0
total 32 142 22.5


line stmt bran cond sub pod time code
1             package Redis::Bayes;
2 1     1   20915 use Redis;
  1         66289  
  1         61  
3 1     1   19 use v5.10;
  1         4  
  1         52  
4 1     1   7 use List::Util qw/sum/;
  1         8  
  1         106  
5 1     1   6 use Carp;
  1         1  
  1         48  
6 1     1   1037 use Moo;
  1         15572  
  1         6  
7 1     1   2461 use Lingua::StopWords qw/getStopWords/;
  1         296  
  1         72  
8 1     1   4 use strict;
  1         2  
  1         1204  
9              
10             our $VERSION = '0.023';
11              
12             has stopwords => (
13             is => 'rw',
14             lazy => 1,
15             builder => '_build_stopwords',
16             );
17              
18             has redis => (
19             is => 'ro',
20             builder => '_build_redis',
21             );
22              
23             has prefix => (
24             is => 'ro',
25             default => sub {'bayes:'},
26             );
27              
28             has tokenizer => (
29             is => 'rw',
30             lazy => 1,
31             builder => '_build_tokenizer',
32             );
33              
34             sub _build_redis {
35 0     0     return Redis->new(
36             reconnect => 2,
37             every => 100
38             );
39             }
40              
41             sub _build_stopwords {
42 0     0     return getStopWords('en');
43             }
44              
45             sub _build_tokenizer {
46 0     0     my $self = shift;
47             return sub {
48 0     0     my ($txt) = @_;
49 0           my @words = grep {
50 0           !$self->stopwords->{$_}
51 0           } grep { $_ } split /[\W_-]+/, lc $txt;
52 0           return @words;
53 0           };
54             }
55              
56             sub _tokenize {
57 0     0     my ($self,$txt) = @_;
58 0           return $self->tokenizer->($txt);
59             }
60              
61             sub _word_counts {
62 0     0     my ($self, @words) = @_;
63 0           my %counts;
64 0           $counts{$_}++ for @words;
65 0           return \%counts;
66             }
67              
68             sub flush {
69 0     0 1   my $self = shift;
70 0           for my $cat ($self->redis->smembers($self->prefix . 'categories')) {
71 0           $self->redis->del($self->prefix . $cat);
72             }
73 0           $self->redis->del($self->prefix . 'categories');
74 0           return 1;
75             }
76              
77             sub train {
78 0     0 1   my ($self,$category,$text) = @_;
79 0           $self->redis->sadd($self->prefix . 'categories', $category);
80 0           my $wc = $self->_word_counts($self->_tokenize($text));
81 0           while (my ($w,$c) = each(%$wc)) {
82 0           $self->redis->hincrby($self->prefix . $category, $w, $c);
83             }
84 0           return 1;
85             }
86              
87             sub untrain {
88 0     0 1   my ($self, $category, $text) = @_;
89 0           my $wc = $self->_word_counts($self->_tokenize($text));
90 0           my ($cur,$new);
91 0           while (my ($w,$c) = each(%$wc)) {
92 0           $cur = $self->redis->hget($self->prefix . $category, $w);
93 0 0         if ($cur) {
94 0           $new = int($cur) - $c;
95 0 0         if ($new > 0) {
96 0           $self->redis->hset($self->prefix . $category, $w, $new)
97             }else{
98 0           $self->redis->hdel($self->prefix . $category, $w);
99             }
100             }
101             }
102 0 0         if ($self->_total($category) == 0) {
103 0           $self->redis->del($self->prefix . $category);
104 0           $self->redis->srem($self->prefix . 'categories', $category);
105             }
106 0           return 1;
107             }
108              
109             sub classify {
110 0     0 1   my ($self, $text, $args) = @_;
111 0   0       $args //= {};
112 0           my $scores = $self->_score($text);
113 0 0         return if (not $scores);
114 0           my @d = sort { $scores->{$b} <=> $scores->{$a} } keys %$scores;
  0            
115 0 0         return 'empty data store' if (not @d);
116 0 0         if ($args->{return_all}) {
117 0           return join ',', map { "$_:" . $scores->{$_} } @d;
  0            
118             }
119 0 0         return ($scores->{$d[0]} ? $d[0] : 'unclassified');
120             }
121              
122             sub _score {
123 0     0     my ($self, $text) = @_;
124 0           my $wc = $self->_word_counts($self->_tokenize($text));
125 0           my %scores;
126 0           for my $category ($self->redis->smembers($self->prefix . 'categories')) {
127 0           my $total = $self->_total($category);
128 0 0         next if ($total == 0);
129 0           $scores{$category} = 0.0;
130 0           while (my ($w,$c) = each %$wc) {
131 0           my $score = $self->redis->hget($self->prefix . $category, $w);
132 0 0 0       croak "invalid values in Redis" unless (not $score or $score > 0);
133 0 0         next if not $score;
134 0           $scores{$category} += $score / $total;
135             }
136             }
137 0           return \%scores;
138             }
139              
140             sub _total {
141 0     0     my ($self, $category) = @_;
142 0           my $total = sum($self->redis->hvals($self->prefix . $category));
143 0 0         return 0 if (not $total);
144 0 0         croak "error in hvals" unless ($total >= 0);
145 0           return $total;
146             }
147              
148              
149              
150             1;
151              
152             __END__