File Coverage

blib/lib/Search/Estraier.pm
Criterion Covered Total %
statement 285 549 51.9
branch 81 266 30.4
condition 15 108 13.8
subroutine 60 94 63.8
pod n/a
total 441 1017 43.3


line stmt bran cond sub pod time code
1             package Search::Estraier;
2              
3 5     5   265340 use 5.008;
  5         19  
  5         456  
4 5     5   30 use strict;
  5         9  
  5         165  
5 5     5   30 use warnings;
  5         15  
  5         957  
6              
7             our $VERSION = '0.09';
8              
9             =head1 NAME
10              
11             Search::Estraier - pure perl module to use Hyper Estraier search engine
12              
13             =head1 SYNOPSIS
14              
15             =head2 Simple indexer
16              
17             use Search::Estraier;
18              
19             # create and configure node
20             my $node = new Search::Estraier::Node(
21             url => 'http://localhost:1978/node/test',
22             user => 'admin',
23             passwd => 'admin',
24             create => 1,
25             label => 'Label for node',
26             croak_on_error => 1,
27             );
28              
29             # create document
30             my $doc = new Search::Estraier::Document;
31              
32             # add attributes
33             $doc->add_attr('@uri', "http://estraier.gov/example.txt");
34             $doc->add_attr('@title', "Over the Rainbow");
35              
36             # add body text to document
37             $doc->add_text("Somewhere over the rainbow. Way up high.");
38             $doc->add_text("There's a land that I heard of once in a lullaby.");
39              
40             die "error: ", $node->status,"\n" unless (eval { $node->put_doc($doc) });
41              
42             =head2 Simple searcher
43              
44             use Search::Estraier;
45              
46             # create and configure node
47             my $node = new Search::Estraier::Node(
48             url => 'http://localhost:1978/node/test',
49             user => 'admin',
50             passwd => 'admin',
51             croak_on_error => 1,
52             );
53              
54             # create condition
55             my $cond = new Search::Estraier::Condition;
56              
57             # set search phrase
58             $cond->set_phrase("rainbow AND lullaby");
59              
60             my $nres = $node->search($cond, 0);
61              
62             if (defined($nres)) {
63             print "Got ", $nres->hits, " results\n";
64              
65             # for each document in results
66             for my $i ( 0 ... $nres->doc_num - 1 ) {
67             # get result document
68             my $rdoc = $nres->get_doc($i);
69             # display attribte
70             print "URI: ", $rdoc->attr('@uri'),"\n";
71             print "Title: ", $rdoc->attr('@title'),"\n";
72             print $rdoc->snippet,"\n";
73             }
74             } else {
75             die "error: ", $node->status,"\n";
76             }
77              
78             =head1 DESCRIPTION
79              
80             This module is implementation of node API of Hyper Estraier. Since it's
81             perl-only module with dependencies only on standard perl modules, it will
82             run on all platforms on which perl runs. It doesn't require compilation
83             or Hyper Estraier development files on target machine.
84              
85             It is implemented as multiple packages which closly resamble Ruby
86             implementation. It also includes methods to manage nodes.
87              
88             There are few examples in C directory of this distribution.
89              
90             =cut
91              
92             =head1 Inheritable common methods
93              
94             This methods should really move somewhere else.
95              
96             =head2 _s
97              
98             Remove multiple whitespaces from string, as well as whitespaces at beginning or end
99              
100             my $text = $self->_s(" this is a text ");
101             $text = 'this is a text';
102              
103             =cut
104              
105             sub _s {
106 22     22   37 my $text = $_[1];
107 22 50       46 return unless defined($text);
108 22         40 $text =~ s/\s\s+/ /gs;
109 22         43 $text =~ s/^\s+//;
110 22         37 $text =~ s/\s+$//;
111 22         87 return $text;
112             }
113              
114             package Search::Estraier::Document;
115              
116 5     5   28 use Carp qw/croak confess/;
  5         7  
  5         333  
117              
118 5     5   59 use Search::Estraier;
  5         10  
  5         7723  
119             our @ISA = qw/Search::Estraier/;
120              
121             =head1 Search::Estraier::Document
122              
123             This class implements Document which is single item in Hyper Estraier.
124              
125             It's is collection of:
126              
127             =over 4
128              
129             =item attributes
130              
131             C<< 'key' => 'value' >> pairs which can later be used for filtering of results
132              
133             You can add common filters to C in estmaster's C<_conf>
134             file for better performance. See C in
135             L.
136              
137             =item vectors
138              
139             also C<< 'key' => 'value' >> pairs
140              
141             =item display text
142              
143             Text which will be used to create searchable corpus of your index and
144             included in snippet output.
145              
146             =item hidden text
147              
148             Text which will be searchable, but will not be included in snippet.
149              
150             =back
151              
152             =head2 new
153              
154             Create new document, empty or from draft.
155              
156             my $doc = new Search::HyperEstraier::Document;
157             my $doc2 = new Search::HyperEstraier::Document( $draft );
158              
159             =cut
160              
161             sub new {
162 5     5   2998 my $class = shift;
163 5         13 my $self = {};
164 5         22 bless($self, $class);
165              
166 5         26 $self->{id} = -1;
167              
168 5         10 my $draft = shift;
169              
170 5 100       18 if ($draft) {
171 1         3 my $in_text = 0;
172 1         8 foreach my $line (split(/\n/, $draft)) {
173              
174 11 100       25 if ($in_text) {
175 4 100       12 if ($line =~ /^\t/) {
176 1         3 push @{ $self->{htexts} }, substr($line, 1);
  1         4  
177             } else {
178 3         4 push @{ $self->{dtexts} }, $line;
  3         7  
179             }
180 4         10 next;
181             }
182              
183 7 100       60 if ($line =~ m/^%VECTOR\t(.+)$/) {
    50          
    50          
    100          
    50          
184 1         7 my @fields = split(/\t/, $1);
185 1 50       7 if ($#fields % 2 == 1) {
186 1         5 $self->{kwords} = { @fields };
187             } else {
188 0         0 warn "can't decode $line\n";
189             }
190 1         4 next;
191             } elsif ($line =~ m/^%SCORE\t(.+)$/) {
192 0         0 $self->{score} = $1;
193 0         0 next;
194             } elsif ($line =~ m/^%/) {
195             # What is this? comment?
196             #warn "$line\n";
197 0         0 next;
198             } elsif ($line =~ m/^$/) {
199 1         3 $in_text = 1;
200 1         2 next;
201             } elsif ($line =~ m/^(.+)=(.*)$/) {
202 5         21 $self->{attrs}->{ $1 } = $2;
203 5         11 next;
204             }
205              
206 0         0 warn "draft ignored: '$line'\n";
207             }
208             }
209              
210 5 50       38 $self ? return $self : return undef;
211             }
212              
213              
214             =head2 add_attr
215              
216             Add an attribute.
217              
218             $doc->add_attr( name => 'value' );
219              
220             Delete attribute using
221              
222             $doc->add_attr( name => undef );
223              
224             =cut
225              
226             sub add_attr {
227 10     10   1718 my $self = shift;
228 10         29 my $attrs = {@_};
229              
230 10         12 while (my ($name, $value) = each %{ $attrs }) {
  20         76  
231 10 100       21 if (! defined($value)) {
232 5         21 delete( $self->{attrs}->{ $self->_s($name) } );
233             } else {
234 5         11 $self->{attrs}->{ $self->_s($name) } = $self->_s($value);
235             }
236             }
237              
238 10         45 return 1;
239             }
240              
241              
242             =head2 add_text
243              
244             Add a sentence of text.
245              
246             $doc->add_text('this is example text to display');
247              
248             =cut
249              
250             sub add_text {
251 3     3   1659 my $self = shift;
252 3         6 my $text = shift;
253 3 50       11 return unless defined($text);
254              
255 3         5 push @{ $self->{dtexts} }, $self->_s($text);
  3         12  
256             }
257              
258              
259             =head2 add_hidden_text
260              
261             Add a hidden sentence.
262              
263             $doc->add_hidden_text('this is example text just for search');
264              
265             =cut
266              
267             sub add_hidden_text {
268 1     1   489 my $self = shift;
269 1         4 my $text = shift;
270 1 50       5 return unless defined($text);
271              
272 1         2 push @{ $self->{htexts} }, $self->_s($text);
  1         6  
273             }
274              
275             =head2 add_vectors
276              
277             Add a vectors
278              
279             $doc->add_vector(
280             'vector_name' => 42,
281             'another' => 12345,
282             );
283              
284             =cut
285              
286             sub add_vectors {
287 1     1   3 my $self = shift;
288 1 50       8 return unless (@_);
289              
290             # this is ugly, but works
291 1 50       7 die "add_vector needs HASH as argument" unless ($#_ % 2 == 1);
292              
293 1         7 $self->{kwords} = {@_};
294             }
295              
296             =head2 set_score
297              
298             Set the substitute score
299              
300             $doc->set_score(12345);
301              
302             =cut
303              
304             sub set_score {
305 1     1   3 my $self = shift;
306 1         3 my $score = shift;
307 1 50       5 return unless (defined($score));
308 1         7 $self->{score} = $score;
309             }
310              
311             =head2 score
312              
313             Get the substitute score
314              
315             =cut
316              
317             sub score {
318 2     2   6 my $self = shift;
319 2 100       14 return -1 unless (defined($self->{score}));
320 1         5 return $self->{score};
321             }
322              
323             =head2 id
324              
325             Get the ID number of document. If the object has never been registred, C<-1> is returned.
326              
327             print $doc->id;
328              
329             =cut
330              
331             sub id {
332 4     4   11 my $self = shift;
333 4         21 return $self->{id};
334             }
335              
336              
337             =head2 attr_names
338              
339             Returns array with attribute names from document object.
340              
341             my @attrs = $doc->attr_names;
342              
343             =cut
344              
345             sub attr_names {
346 3     3   1210 my $self = shift;
347 3 50       16 return unless ($self->{attrs});
348             #croak "attr_names return array, not scalar" if (! wantarray);
349 3         7 return sort keys %{ $self->{attrs} };
  3         30  
350             }
351              
352              
353             =head2 attr
354              
355             Returns value of an attribute.
356              
357             my $value = $doc->attr( 'attribute' );
358              
359             =cut
360              
361             sub attr {
362 12     12   1734 my $self = shift;
363 12         18 my $name = shift;
364 12 100 66     77 return unless (defined($name) && $self->{attrs});
365 11         76 return $self->{attrs}->{ $name };
366             }
367              
368              
369             =head2 texts
370              
371             Returns array with text sentences.
372              
373             my @texts = $doc->texts;
374              
375             =cut
376              
377             sub texts {
378 2     2   5 my $self = shift;
379             #confess "texts return array, not scalar" if (! wantarray);
380 2 100       9 return @{ $self->{dtexts} } if ($self->{dtexts});
  1         8  
381             }
382              
383              
384             =head2 cat_texts
385              
386             Return whole text as single scalar.
387              
388             my $text = $doc->cat_texts;
389              
390             =cut
391              
392             sub cat_texts {
393 2     2   1587 my $self = shift;
394 2 100       18 return join(' ',@{ $self->{dtexts} }) if ($self->{dtexts});
  1         7  
395             }
396              
397              
398             =head2 dump_draft
399              
400             Dump draft data from document object.
401              
402             print $doc->dump_draft;
403              
404             =cut
405              
406             sub dump_draft {
407 4     4   59 my $self = shift;
408 4         8 my $draft;
409              
410 4         9 foreach my $attr_name (sort keys %{ $self->{attrs} }) {
  4         35  
411 10 50       27 next unless defined(my $v = $self->{attrs}->{$attr_name});
412 10         24 $draft .= $attr_name . '=' . $v . "\n";
413             }
414              
415 4 100       19 if ($self->{kwords}) {
416 2         10 $draft .= '%VECTOR';
417 2         6 while (my ($key, $value) = each %{ $self->{kwords} }) {
  8         31  
418 6         15 $draft .= "\t$key\t$value";
419             }
420 2         5 $draft .= "\n";
421             }
422              
423 4 100 66     25 if (defined($self->{score}) && $self->{score} >= 0) {
424 1         6 $draft .= "%SCORE\t" . $self->{score} . "\n";
425             }
426              
427 4         10 $draft .= "\n";
428              
429 4 100       18 $draft .= join("\n", @{ $self->{dtexts} }) . "\n" if ($self->{dtexts});
  2         9  
430 4 100       15 $draft .= "\t" . join("\n\t", @{ $self->{htexts} }) . "\n" if ($self->{htexts});
  2         7  
431              
432 4         31 return $draft;
433             }
434              
435              
436             =head2 delete
437              
438             Empty document object
439              
440             $doc->delete;
441              
442             This function is addition to original Ruby API, and since it was included in C wrappers it's here as a
443             convinience. Document objects which go out of scope will be destroyed
444             automatically.
445              
446             =cut
447              
448             sub delete {
449 1     1   3 my $self = shift;
450              
451 1         3 foreach my $data (qw/attrs dtexts stexts kwords/) {
452 4         8 delete($self->{$data});
453             }
454              
455 1         2 $self->{id} = -1;
456              
457 1         4 return 1;
458             }
459              
460              
461              
462             package Search::Estraier::Condition;
463              
464 5     5   35 use Carp qw/carp confess croak/;
  5         8  
  5         349  
465              
466 5     5   41 use Search::Estraier;
  5         14  
  5         4265  
467             our @ISA = qw/Search::Estraier/;
468              
469             =head1 Search::Estraier::Condition
470              
471             =head2 new
472              
473             my $cond = new Search::HyperEstraier::Condition;
474              
475             =cut
476              
477             sub new {
478 1     1   14 my $class = shift;
479 1         3 my $self = {};
480 1         3 bless($self, $class);
481              
482 1         7 $self->{max} = -1;
483 1         3 $self->{options} = 0;
484              
485 1 50       13 $self ? return $self : return undef;
486             }
487              
488              
489             =head2 set_phrase
490              
491             $cond->set_phrase('search phrase');
492              
493             =cut
494              
495             sub set_phrase {
496 1     1   3 my $self = shift;
497 1         11 $self->{phrase} = $self->_s( shift );
498             }
499              
500              
501             =head2 add_attr
502              
503             $cond->add_attr('@URI STRINC /~dpavlin/');
504              
505             =cut
506              
507             sub add_attr {
508 2     2   5 my $self = shift;
509 2   50     10 my $attr = shift || return;
510 2         4 push @{ $self->{attrs} }, $self->_s( $attr );
  2         14  
511             }
512              
513              
514             =head2 set_order
515              
516             $cond->set_order('@mdate NUMD');
517              
518             =cut
519              
520             sub set_order {
521 1     1   3 my $self = shift;
522 1         6 $self->{order} = shift;
523             }
524              
525              
526             =head2 set_max
527              
528             $cond->set_max(42);
529              
530             =cut
531              
532             sub set_max {
533 2     2   46 my $self = shift;
534 2         4 my $max = shift;
535 2 100       57 croak "set_max needs number, not '$max'" unless ($max =~ m/^\d+$/);
536 1         6 $self->{max} = $max;
537             }
538              
539              
540             =head2 set_options
541              
542             $cond->set_options( 'SURE' );
543              
544             $cond->set_options( qw/AGITO NOIDF SIMPLE/ );
545              
546             Possible options are:
547              
548             =over 8
549              
550             =item SURE
551              
552             check every N-gram
553              
554             =item USUAL
555              
556             check every second N-gram
557              
558             =item FAST
559              
560             check every third N-gram
561              
562             =item AGITO
563              
564             check every fourth N-gram
565              
566             =item NOIDF
567              
568             don't perform TF-IDF tuning
569              
570             =item SIMPLE
571              
572             use simplified query phrase
573              
574             =back
575              
576             Skipping N-grams will speed up search, but reduce accuracy. Every call to C will reset previous
577             options;
578              
579             This option changed in version C<0.04> of this module. It's backwards compatibile.
580              
581             =cut
582              
583             my $options = {
584             SURE => 1 << 0,
585             USUAL => 1 << 1,
586             FAST => 1 << 2,
587             AGITO => 1 << 3,
588             NOIDF => 1 << 4,
589             SIMPLE => 1 << 10,
590             };
591              
592             sub set_options {
593 10     10   46 my $self = shift;
594 10         12 my $opt = 0;
595 10         18 foreach my $option (@_) {
596 16         17 my $mask;
597 16 100       44 unless ($mask = $options->{$option}) {
598 2 100       6 if ($option eq '1') {
599 1         3 next;
600             } else {
601 1         23 croak "unknown option $option";
602             }
603             }
604 14         29 $opt += $mask;
605             }
606 9         46 $self->{options} = $opt;
607             }
608              
609              
610             =head2 phrase
611              
612             Return search phrase.
613              
614             print $cond->phrase;
615              
616             =cut
617              
618             sub phrase {
619 1     1   3 my $self = shift;
620 1         9 return $self->{phrase};
621             }
622              
623              
624             =head2 order
625              
626             Return search result order.
627              
628             print $cond->order;
629              
630             =cut
631              
632             sub order {
633 0     0   0 my $self = shift;
634 0         0 return $self->{order};
635             }
636              
637              
638             =head2 attrs
639              
640             Return search result attrs.
641              
642             my @cond_attrs = $cond->attrs;
643              
644             =cut
645              
646             sub attrs {
647 2     2   4 my $self = shift;
648             #croak "attrs return array, not scalar" if (! wantarray);
649 2 50       10 return @{ $self->{attrs} } if ($self->{attrs});
  2         12  
650             }
651              
652              
653             =head2 max
654              
655             Return maximum number of results.
656              
657             print $cond->max;
658              
659             C<-1> is returned for unitialized value, C<0> is unlimited.
660              
661             =cut
662              
663             sub max {
664 2     2   5 my $self = shift;
665 2         14 return $self->{max};
666             }
667              
668              
669             =head2 options
670              
671             Return options for this condition.
672              
673             print $cond->options;
674              
675             Options are returned in numerical form.
676              
677             =cut
678              
679             sub options {
680 2     2   5 my $self = shift;
681 2         14 return $self->{options};
682             }
683              
684              
685             =head2 set_skip
686              
687             Set number of skipped documents from beginning of results
688              
689             $cond->set_skip(42);
690              
691             Similar to C in RDBMS.
692              
693             =cut
694              
695             sub set_skip {
696 0     0   0 my $self = shift;
697 0         0 $self->{skip} = shift;
698             }
699              
700             =head2 skip
701              
702             Return skip for this condition.
703              
704             print $cond->skip;
705              
706             =cut
707              
708             sub skip {
709 0     0   0 my $self = shift;
710 0         0 return $self->{skip};
711             }
712              
713              
714             =head2 set_distinct
715              
716             $cond->set_distinct('@author');
717              
718             =cut
719              
720             sub set_distinct {
721 1     1   2 my $self = shift;
722 1         5 $self->{distinct} = shift;
723             }
724              
725             =head2 distinct
726              
727             Return distinct attribute
728              
729             print $cond->distinct;
730              
731             =cut
732              
733             sub distinct {
734 1     1   3 my $self = shift;
735 1         5 return $self->{distinct};
736             }
737              
738             =head2 set_mask
739              
740             Filter out some links when searching.
741              
742             Argument array of link numbers, starting with 0 (current node).
743              
744             $cond->set_mask(qw/0 1 4/);
745              
746             =cut
747              
748             sub set_mask {
749 1     1   2 my $self = shift;
750 1 50       4 return unless (@_);
751 1         6 $self->{mask} = \@_;
752             }
753              
754              
755             package Search::Estraier::ResultDocument;
756              
757 5     5   31 use Carp qw/croak/;
  5         9  
  5         1840  
758              
759             #use Search::Estraier;
760             #our @ISA = qw/Search::Estraier/;
761              
762             =head1 Search::Estraier::ResultDocument
763              
764             =head2 new
765              
766             my $rdoc = new Search::HyperEstraier::ResultDocument(
767             uri => 'http://localhost/document/uri/42',
768             attrs => {
769             foo => 1,
770             bar => 2,
771             },
772             snippet => 'this is a text of snippet'
773             keywords => 'this\tare\tkeywords'
774             );
775              
776             =cut
777              
778             sub new {
779 2     2   1193 my $class = shift;
780 2         7 my $self = {@_};
781 2         5 bless($self, $class);
782              
783 2 100       40 croak "missing uri for ResultDocument" unless defined($self->{uri});
784              
785 1 50       10 $self ? return $self : return undef;
786             }
787              
788              
789             =head2 uri
790              
791             Return URI of result document
792              
793             print $rdoc->uri;
794              
795             =cut
796              
797             sub uri {
798 1     1   25 my $self = shift;
799 1         9 return $self->{uri};
800             }
801              
802              
803             =head2 attr_names
804              
805             Returns array with attribute names from result document object.
806              
807             my @attrs = $rdoc->attr_names;
808              
809             =cut
810              
811             sub attr_names {
812 1     1   2 my $self = shift;
813 1 50       6 croak "attr_names return array, not scalar" if (! wantarray);
814 1         2 return sort keys %{ $self->{attrs} };
  1         13  
815             }
816              
817              
818             =head2 attr
819              
820             Returns value of an attribute.
821              
822             my $value = $rdoc->attr( 'attribute' );
823              
824             =cut
825              
826             sub attr {
827 2     2   1268 my $self = shift;
828 2   50     10 my $name = shift || return;
829 2         13 return $self->{attrs}->{ $name };
830             }
831              
832              
833             =head2 snippet
834              
835             Return snippet from result document
836              
837             print $rdoc->snippet;
838              
839             =cut
840              
841             sub snippet {
842 1     1   548 my $self = shift;
843 1         6 return $self->{snippet};
844             }
845              
846              
847             =head2 keywords
848              
849             Return keywords from result document
850              
851             print $rdoc->keywords;
852              
853             =cut
854              
855             sub keywords {
856 1     1   3 my $self = shift;
857 1         6 return $self->{keywords};
858             }
859              
860              
861             package Search::Estraier::NodeResult;
862              
863 5     5   26 use Carp qw/croak/;
  5         10  
  5         2324  
864              
865             #use Search::Estraier;
866             #our @ISA = qw/Search::Estraier/;
867              
868             =head1 Search::Estraier::NodeResult
869              
870             =head2 new
871              
872             my $res = new Search::HyperEstraier::NodeResult(
873             docs => @array_of_rdocs,
874             hits => %hash_with_hints,
875             );
876              
877             =cut
878              
879             sub new {
880 2     2   1282 my $class = shift;
881 2         6 my $self = {@_};
882 2         5 bless($self, $class);
883              
884 2         5 foreach my $f (qw/docs hints/) {
885 3 100       34 croak "missing $f for ResultDocument" unless defined($self->{$f});
886             }
887              
888 1 50       9 $self ? return $self : return undef;
889             }
890              
891              
892             =head2 doc_num
893              
894             Return number of documents
895              
896             print $res->doc_num;
897              
898             This will return real number of documents (limited by C).
899             If you want to get total number of hits, see C.
900              
901             =cut
902              
903             sub doc_num {
904 7     7   2820 my $self = shift;
905 7         10 return $#{$self->{docs}} + 1;
  7         23  
906             }
907              
908              
909             =head2 get_doc
910              
911             Return single document
912              
913             my $doc = $res->get_doc( 42 );
914              
915             Returns undef if document doesn't exist.
916              
917             =cut
918              
919             sub get_doc {
920 5     5   19 my $self = shift;
921 5         7 my $num = shift;
922 5 50       27 croak "expect number as argument, not '$num'" unless ($num =~ m/^\d+$/);
923 5 50 33     31 return undef if ($num < 0 || $num > $self->{docs});
924 5         29 return $self->{docs}->[$num];
925             }
926              
927              
928             =head2 hint
929              
930             Return specific hint from results.
931              
932             print $res->hint( 'VERSION' );
933              
934             Possible hints are: C, C, C, C, C, C,
935             C
936              
937             =cut
938              
939             sub hint {
940 0     0   0 my $self = shift;
941 0   0     0 my $key = shift || return;
942 0         0 return $self->{hints}->{$key};
943             }
944              
945             =head2 hints
946              
947             More perlish version of C. This one returns hash.
948              
949             my %hints = $res->hints;
950              
951             =cut
952              
953             sub hints {
954 0     0   0 my $self = shift;
955 0         0 return $self->{hints};
956             }
957              
958             =head2 hits
959              
960             Syntaxtic sugar for total number of hits for this query
961              
962             print $res->hits;
963              
964             It's same as
965              
966             print $res->hint('HIT');
967              
968             but shorter.
969              
970             =cut
971              
972             sub hits {
973 0     0   0 my $self = shift;
974 0   0     0 return $self->{hints}->{'HIT'} || 0;
975             }
976              
977             package Search::Estraier::Node;
978              
979 5     5   81 use Carp qw/carp croak confess/;
  5         9  
  5         263  
980 5     5   9076 use URI;
  5         32391  
  5         173  
981 5     5   5514 use MIME::Base64;
  5         4712  
  5         383  
982 5     5   6378 use IO::Socket::INET;
  5         172141  
  5         43  
983 5     5   3866 use URI::Escape qw/uri_escape/;
  5         10  
  5         16754  
984              
985             =head1 Search::Estraier::Node
986              
987             =head2 new
988              
989             my $node = new Search::HyperEstraier::Node;
990              
991             or optionally with C as parametar
992              
993             my $node = new Search::HyperEstraier::Node( 'http://localhost:1978/node/test' );
994              
995             or in more verbose form
996              
997             my $node = new Search::HyperEstraier::Node(
998             url => 'http://localhost:1978/node/test',
999             user => 'admin',
1000             passwd => 'admin'
1001             create => 1,
1002             label => 'optional node label',
1003             debug => 1,
1004             croak_on_error => 1
1005             );
1006              
1007             with following arguments:
1008              
1009             =over 4
1010              
1011             =item url
1012              
1013             URL to node
1014              
1015             =item user
1016              
1017             specify username for node server authentication
1018              
1019             =item passwd
1020              
1021             password for authentication
1022              
1023             =item create
1024              
1025             create node if it doesn't exists
1026              
1027             =item label
1028              
1029             optional label for new node if C is used
1030              
1031             =item debug
1032              
1033             dumps a B of debugging output
1034              
1035             =item croak_on_error
1036              
1037             very helpful during development. It will croak on all errors instead of
1038             silently returning C<-1> (which is convention of Hyper Estraier API in other
1039             languages).
1040              
1041             =back
1042              
1043             =cut
1044              
1045             sub new {
1046 1     1   27 my $class = shift;
1047 1         10 my $self = {
1048             pxport => -1,
1049             timeout => 0, # this used to be -1
1050             wwidth => 480,
1051             hwidth => 96,
1052             awidth => 96,
1053             status => -1,
1054             };
1055              
1056 1         4 bless($self, $class);
1057              
1058 1 50       6 if ($#_ == 0) {
1059 0         0 $self->{url} = shift;
1060             } else {
1061 1         14 %$self = ( %$self, @_ );
1062              
1063 1 50       5 $self->set_auth( $self->{user}, $self->{passwd} ) if ($self->{user});
1064              
1065 1 50       5 warn "## Node debug on\n" if ($self->{debug});
1066             }
1067              
1068 1         28 $self->{inform} = {
1069             dnum => -1,
1070             wnum => -1,
1071             size => -1.0,
1072             };
1073              
1074 1 50       6 if ($self->{create}) {
1075 0 0 0     0 if (! eval { $self->name } || $@) {
  0         0  
1076 0 0       0 my $name = $1 if ($self->{url} =~ m#/node/([^/]+)/*#);
1077 0 0       0 croak "can't find node name in '$self->{url}'" unless ($name);
1078 0   0     0 my $label = $self->{label} || $name;
1079 0 0       0 $self->master(
1080             action => 'nodeadd',
1081             name => $name,
1082             label => $label,
1083             ) || croak "can't create node $name ($label)";
1084             }
1085             }
1086              
1087 1 50       10 $self ? return $self : return undef;
1088             }
1089              
1090              
1091             =head2 set_url
1092              
1093             Specify URL to node server
1094              
1095             $node->set_url('http://localhost:1978');
1096              
1097             =cut
1098              
1099             sub set_url {
1100 1     1   2 my $self = shift;
1101 1         7 $self->{url} = shift;
1102             }
1103              
1104              
1105             =head2 set_proxy
1106              
1107             Specify proxy server to connect to node server
1108              
1109             $node->set_proxy('proxy.example.com', 8080);
1110              
1111             =cut
1112              
1113             sub set_proxy {
1114 2     2   40 my $self = shift;
1115 2         5 my ($host,$port) = @_;
1116 2 100       44 croak "proxy port must be number, not '$port'" unless ($port =~ m/^\d+$/);
1117 1         3 $self->{pxhost} = $host;
1118 1         5 $self->{pxport} = $port;
1119             }
1120              
1121              
1122             =head2 set_timeout
1123              
1124             Specify timeout of connection in seconds
1125              
1126             $node->set_timeout( 15 );
1127              
1128             =cut
1129              
1130             sub set_timeout {
1131 2     2   22 my $self = shift;
1132 2         4 my $sec = shift;
1133 2 100       18 croak "timeout must be number, not '$sec'" unless ($sec =~ m/^\d+$/);
1134 1         6 $self->{timeout} = $sec;
1135             }
1136              
1137              
1138             =head2 set_auth
1139              
1140             Specify name and password for authentication to node server.
1141              
1142             $node->set_auth('clint','eastwood');
1143              
1144             =cut
1145              
1146             sub set_auth {
1147 1     1   2 my $self = shift;
1148 1         2 my ($login,$passwd) = @_;
1149 1         14 my $basic_auth = encode_base64( "$login:$passwd" );
1150 1         3 chomp($basic_auth);
1151 1         38 $self->{auth} = $basic_auth;
1152             }
1153              
1154              
1155             =head2 status
1156              
1157             Return status code of last request.
1158              
1159             print $node->status;
1160              
1161             C<-1> means connection failure.
1162              
1163             =cut
1164              
1165             sub status {
1166 1     1   3 my $self = shift;
1167 1         7 return $self->{status};
1168             }
1169              
1170              
1171             =head2 put_doc
1172              
1173             Add a document
1174              
1175             $node->put_doc( $document_draft ) or die "can't add document";
1176              
1177             Return true on success or false on failure.
1178              
1179             =cut
1180              
1181             sub put_doc {
1182 0     0   0 my $self = shift;
1183 0   0     0 my $doc = shift || return;
1184 0 0 0     0 return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));
1185 0 0       0 if ($self->shuttle_url( $self->{url} . '/put_doc',
1186             'text/x-estraier-draft',
1187             $doc->dump_draft,
1188             undef
1189             ) == 200) {
1190 0         0 $self->_clear_info;
1191 0         0 return 1;
1192             }
1193 0         0 return undef;
1194             }
1195              
1196              
1197             =head2 out_doc
1198              
1199             Remove a document
1200              
1201             $node->out_doc( document_id ) or "can't remove document";
1202              
1203             Return true on success or false on failture.
1204              
1205             =cut
1206              
1207             sub out_doc {
1208 0     0   0 my $self = shift;
1209 0   0     0 my $id = shift || return;
1210 0 0       0 return unless ($self->{url});
1211 0 0       0 croak "id must be number, not '$id'" unless ($id =~ m/^\d+$/);
1212 0 0       0 if ($self->shuttle_url( $self->{url} . '/out_doc',
1213             'application/x-www-form-urlencoded',
1214             "id=$id",
1215             undef
1216             ) == 200) {
1217 0         0 $self->_clear_info;
1218 0         0 return 1;
1219             }
1220 0         0 return undef;
1221             }
1222              
1223              
1224             =head2 out_doc_by_uri
1225              
1226             Remove a registrated document using it's uri
1227              
1228             $node->out_doc_by_uri( 'file:///document/uri/42' ) or "can't remove document";
1229              
1230             Return true on success or false on failture.
1231              
1232             =cut
1233              
1234             sub out_doc_by_uri {
1235 0     0   0 my $self = shift;
1236 0   0     0 my $uri = shift || return;
1237 0 0       0 return unless ($self->{url});
1238 0 0       0 if ($self->shuttle_url( $self->{url} . '/out_doc',
1239             'application/x-www-form-urlencoded',
1240             "uri=" . uri_escape($uri),
1241             undef
1242             ) == 200) {
1243 0         0 $self->_clear_info;
1244 0         0 return 1;
1245             }
1246 0         0 return undef;
1247             }
1248              
1249              
1250             =head2 edit_doc
1251              
1252             Edit attributes of a document
1253              
1254             $node->edit_doc( $document_draft ) or die "can't edit document";
1255              
1256             Return true on success or false on failture.
1257              
1258             =cut
1259              
1260             sub edit_doc {
1261 0     0   0 my $self = shift;
1262 0   0     0 my $doc = shift || return;
1263 0 0 0     0 return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));
1264 0 0       0 if ($self->shuttle_url( $self->{url} . '/edit_doc',
1265             'text/x-estraier-draft',
1266             $doc->dump_draft,
1267             undef
1268             ) == 200) {
1269 0         0 $self->_clear_info;
1270 0         0 return 1;
1271             }
1272 0         0 return undef;
1273             }
1274              
1275              
1276             =head2 get_doc
1277              
1278             Retreive document
1279              
1280             my $doc = $node->get_doc( document_id ) or die "can't get document";
1281              
1282             Return true on success or false on failture.
1283              
1284             =cut
1285              
1286             sub get_doc {
1287 0     0   0 my $self = shift;
1288 0   0     0 my $id = shift || return;
1289 0         0 return $self->_fetch_doc( id => $id );
1290             }
1291              
1292              
1293             =head2 get_doc_by_uri
1294              
1295             Retreive document
1296              
1297             my $doc = $node->get_doc_by_uri( 'file:///document/uri/42' ) or die "can't get document";
1298              
1299             Return true on success or false on failture.
1300              
1301             =cut
1302              
1303             sub get_doc_by_uri {
1304 0     0   0 my $self = shift;
1305 0   0     0 my $uri = shift || return;
1306 0         0 return $self->_fetch_doc( uri => $uri );
1307             }
1308              
1309              
1310             =head2 get_doc_attr
1311              
1312             Retrieve the value of an atribute from object
1313              
1314             my $val = $node->get_doc_attr( document_id, 'attribute_name' ) or
1315             die "can't get document attribute";
1316              
1317             =cut
1318              
1319             sub get_doc_attr {
1320 0     0   0 my $self = shift;
1321 0         0 my ($id,$name) = @_;
1322 0 0 0     0 return unless ($id && $name);
1323 0         0 return $self->_fetch_doc( id => $id, attr => $name );
1324             }
1325              
1326              
1327             =head2 get_doc_attr_by_uri
1328              
1329             Retrieve the value of an atribute from object
1330              
1331             my $val = $node->get_doc_attr_by_uri( document_id, 'attribute_name' ) or
1332             die "can't get document attribute";
1333              
1334             =cut
1335              
1336             sub get_doc_attr_by_uri {
1337 0     0   0 my $self = shift;
1338 0         0 my ($uri,$name) = @_;
1339 0 0 0     0 return unless ($uri && $name);
1340 0         0 return $self->_fetch_doc( uri => $uri, attr => $name );
1341             }
1342              
1343              
1344             =head2 etch_doc
1345              
1346             Exctract document keywords
1347              
1348             my $keywords = $node->etch_doc( document_id ) or die "can't etch document";
1349              
1350             =cut
1351              
1352             sub etch_doc {
1353 0     0   0 my $self = shift;
1354 0   0     0 my $id = shift || return;
1355 0         0 return $self->_fetch_doc( id => $id, etch => 1 );
1356             }
1357              
1358             =head2 etch_doc_by_uri
1359              
1360             Retreive document
1361              
1362             my $keywords = $node->etch_doc_by_uri( 'file:///document/uri/42' ) or die "can't etch document";
1363              
1364             Return true on success or false on failture.
1365              
1366             =cut
1367              
1368             sub etch_doc_by_uri {
1369 0     0   0 my $self = shift;
1370 0   0     0 my $uri = shift || return;
1371 0         0 return $self->_fetch_doc( uri => $uri, etch => 1 );
1372             }
1373              
1374              
1375             =head2 uri_to_id
1376              
1377             Get ID of document specified by URI
1378              
1379             my $id = $node->uri_to_id( 'file:///document/uri/42' );
1380              
1381             This method won't croak, even if using C.
1382              
1383             =cut
1384              
1385             sub uri_to_id {
1386 0     0   0 my $self = shift;
1387 0   0     0 my $uri = shift || return;
1388 0         0 return $self->_fetch_doc( uri => $uri, path => '/uri_to_id', chomp_resbody => 1, croak_on_error => 0 );
1389             }
1390              
1391              
1392             =head2 _fetch_doc
1393              
1394             Private function used for implementing of C, C,
1395             C, C.
1396              
1397             # this will decode received draft into Search::Estraier::Document object
1398             my $doc = $node->_fetch_doc( id => 42 );
1399             my $doc = $node->_fetch_doc( uri => 'file:///document/uri/42' );
1400              
1401             # to extract keywords, add etch
1402             my $doc = $node->_fetch_doc( id => 42, etch => 1 );
1403             my $doc = $node->_fetch_doc( uri => 'file:///document/uri/42', etch => 1 );
1404              
1405             # to get document attrubute add attr
1406             my $doc = $node->_fetch_doc( id => 42, attr => '@mdate' );
1407             my $doc = $node->_fetch_doc( uri => 'file:///document/uri/42', attr => '@mdate' );
1408              
1409             # more general form which allows implementation of
1410             # uri_to_id
1411             my $id = $node->_fetch_doc(
1412             uri => 'file:///document/uri/42',
1413             path => '/uri_to_id',
1414             chomp_resbody => 1
1415             );
1416              
1417             =cut
1418              
1419             sub _fetch_doc {
1420 0     0   0 my $self = shift;
1421 0         0 my $a = {@_};
1422 0 0 0     0 return unless ( ($a->{id} || $a->{uri}) && $self->{url} );
      0        
1423              
1424 0         0 my ($arg, $resbody);
1425              
1426 0   0     0 my $path = $a->{path} || '/get_doc';
1427 0 0       0 $path = '/etch_doc' if ($a->{etch});
1428              
1429 0 0       0 if ($a->{id}) {
    0          
1430 0 0       0 croak "id must be number not '$a->{id}'" unless ($a->{id} =~ m/^\d+$/);
1431 0         0 $arg = 'id=' . $a->{id};
1432             } elsif ($a->{uri}) {
1433 0         0 $arg = 'uri=' . uri_escape($a->{uri});
1434             } else {
1435 0         0 confess "unhandled argument. Need id or uri.";
1436             }
1437              
1438 0 0       0 if ($a->{attr}) {
1439 0         0 $path = '/get_doc_attr';
1440 0         0 $arg .= '&attr=' . uri_escape($a->{attr});
1441 0         0 $a->{chomp_resbody} = 1;
1442             }
1443              
1444 0         0 my $rv = $self->shuttle_url( $self->{url} . $path,
1445             'application/x-www-form-urlencoded',
1446             $arg,
1447             \$resbody,
1448             $a->{croak_on_error},
1449             );
1450              
1451 0 0       0 return if ($rv != 200);
1452              
1453 0 0       0 if ($a->{etch}) {
    0          
1454 0         0 $self->{kwords} = {};
1455 0 0       0 return +{} unless ($resbody);
1456 0         0 foreach my $l (split(/\n/, $resbody)) {
1457 0         0 my ($k,$v) = split(/\t/, $l, 2);
1458 0 0       0 $self->{kwords}->{$k} = $v if ($v);
1459             }
1460 0         0 return $self->{kwords};
1461             } elsif ($a->{chomp_resbody}) {
1462 0 0       0 return unless (defined($resbody));
1463 0         0 chomp($resbody);
1464 0         0 return $resbody;
1465             } else {
1466 0         0 return new Search::Estraier::Document($resbody);
1467             }
1468             }
1469              
1470              
1471             =head2 name
1472              
1473             my $node_name = $node->name;
1474              
1475             =cut
1476              
1477             sub name {
1478 0     0   0 my $self = shift;
1479 0 0       0 $self->_set_info unless ($self->{inform}->{name});
1480 0         0 return $self->{inform}->{name};
1481             }
1482              
1483              
1484             =head2 label
1485              
1486             my $node_label = $node->label;
1487              
1488             =cut
1489              
1490             sub label {
1491 0     0   0 my $self = shift;
1492 0 0       0 $self->_set_info unless ($self->{inform}->{label});
1493 0         0 return $self->{inform}->{label};
1494             }
1495              
1496              
1497             =head2 doc_num
1498              
1499             my $documents_in_node = $node->doc_num;
1500              
1501             =cut
1502              
1503             sub doc_num {
1504 0     0   0 my $self = shift;
1505 0 0       0 $self->_set_info if ($self->{inform}->{dnum} < 0);
1506 0         0 return $self->{inform}->{dnum};
1507             }
1508              
1509              
1510             =head2 word_num
1511              
1512             my $words_in_node = $node->word_num;
1513              
1514             =cut
1515              
1516             sub word_num {
1517 0     0   0 my $self = shift;
1518 0 0       0 $self->_set_info if ($self->{inform}->{wnum} < 0);
1519 0         0 return $self->{inform}->{wnum};
1520             }
1521              
1522              
1523             =head2 size
1524              
1525             my $node_size = $node->size;
1526              
1527             =cut
1528              
1529             sub size {
1530 0     0   0 my $self = shift;
1531 0 0       0 $self->_set_info if ($self->{inform}->{size} < 0);
1532 0         0 return $self->{inform}->{size};
1533             }
1534              
1535              
1536             =head2 search
1537              
1538             Search documents which match condition
1539              
1540             my $nres = $node->search( $cond, $depth );
1541              
1542             C<$cond> is C object, while <$depth> specifies
1543             depth for meta search.
1544              
1545             Function results C object.
1546              
1547             =cut
1548              
1549             sub search {
1550 0     0   0 my $self = shift;
1551 0         0 my ($cond, $depth) = @_;
1552 0 0 0     0 return unless ($cond && defined($depth) && $self->{url});
      0        
1553 0 0       0 croak "cond mush be Search::Estraier::Condition, not '$cond->isa'" unless ($cond->isa('Search::Estraier::Condition'));
1554 0 0       0 croak "depth needs number, not '$depth'" unless ($depth =~ m/^\d+$/);
1555              
1556 0         0 my $resbody;
1557              
1558 0         0 my $rv = $self->shuttle_url( $self->{url} . '/search',
1559             'application/x-www-form-urlencoded',
1560             $self->cond_to_query( $cond, $depth ),
1561             \$resbody,
1562             );
1563 0 0       0 return if ($rv != 200);
1564              
1565 0         0 my @records = split /--------\[.*?\]--------(?::END)?\r?\n/, $resbody;
1566 0         0 my $hintsText = splice @records, 0, 2; # starts with empty record
1567 0         0 my $hints = { $hintsText =~ m/^(.*?)\t(.*?)$/gsm };
1568              
1569             # process records
1570 0         0 my $docs = [];
1571 0         0 foreach my $record (@records)
1572             {
1573             # split into keys and snippets
1574 0         0 my ($keys, $snippet) = $record =~ m/^(.*?)\n\n(.*?)$/s;
1575              
1576             # create document hash
1577 0         0 my $doc = { $keys =~ m/^(.*?)=(.*?)$/gsm };
1578 0         0 $doc->{'@keywords'} = $doc->{keywords};
1579 0         0 ($doc->{keywords}) = $keys =~ m/^%VECTOR\t(.*?)$/gm;
1580 0         0 $doc->{snippet} = $snippet;
1581              
1582 0         0 push @$docs, new Search::Estraier::ResultDocument(
1583             attrs => $doc,
1584             uri => $doc->{'@uri'},
1585             snippet => $snippet,
1586             keywords => $doc->{'keywords'},
1587             );
1588             }
1589              
1590 0         0 return new Search::Estraier::NodeResult( docs => $docs, hints => $hints );
1591             }
1592              
1593              
1594             =head2 cond_to_query
1595              
1596             Return URI encoded string generated from Search::Estraier::Condition
1597              
1598             my $args = $node->cond_to_query( $cond, $depth );
1599              
1600             =cut
1601              
1602             sub cond_to_query {
1603 0     0   0 my $self = shift;
1604              
1605 0   0     0 my $cond = shift || return;
1606 0 0       0 croak "condition must be Search::Estraier::Condition, not '$cond->isa'" unless ($cond->isa('Search::Estraier::Condition'));
1607 0         0 my $depth = shift;
1608              
1609 0         0 my @args;
1610              
1611 0 0       0 if (my $phrase = $cond->phrase) {
1612 0         0 push @args, 'phrase=' . uri_escape($phrase);
1613             }
1614              
1615 0 0       0 if (my @attrs = $cond->attrs) {
1616 0         0 for my $i ( 0 .. $#attrs ) {
1617 0 0       0 push @args,'attr' . ($i+1) . '=' . uri_escape( $attrs[$i] ) if ($attrs[$i]);
1618             }
1619             }
1620              
1621 0 0       0 if (my $order = $cond->order) {
1622 0         0 push @args, 'order=' . uri_escape($order);
1623             }
1624            
1625 0 0       0 if (my $max = $cond->max) {
1626 0         0 push @args, 'max=' . $max;
1627             } else {
1628 0         0 push @args, 'max=' . (1 << 30);
1629             }
1630              
1631 0 0       0 if (my $options = $cond->options) {
1632 0         0 push @args, 'options=' . $options;
1633             }
1634              
1635 0 0       0 push @args, 'depth=' . $depth if ($depth);
1636 0         0 push @args, 'wwidth=' . $self->{wwidth};
1637 0         0 push @args, 'hwidth=' . $self->{hwidth};
1638 0         0 push @args, 'awidth=' . $self->{awidth};
1639 0 0       0 push @args, 'skip=' . $cond->{skip} if ($cond->{skip});
1640              
1641 0 0       0 if (my $distinct = $cond->distinct) {
1642 0         0 push @args, 'distinct=' . uri_escape($distinct);
1643             }
1644              
1645 0 0       0 if ($cond->{mask}) {
1646 0         0 my $mask = 0;
1647 0         0 map { $mask += ( 2 ** $_ ) } @{ $cond->{mask} };
  0         0  
  0         0  
1648              
1649 0 0       0 push @args, 'mask=' . $mask if ($mask);
1650             }
1651              
1652 0         0 return join('&', @args);
1653             }
1654              
1655              
1656             =head2 shuttle_url
1657              
1658             This is method which uses C to communicate with Hyper Estraier node
1659             master.
1660              
1661             my $rv = shuttle_url( $url, $content_type, $req_body, \$resbody );
1662              
1663             C<$resheads> and C<$resbody> booleans controll if response headers and/or response
1664             body will be saved within object.
1665              
1666             =cut
1667              
1668 5     5   12012 use LWP::UserAgent;
  5         301676  
  5         12890  
1669              
1670             sub shuttle_url {
1671 1     1   223 my $self = shift;
1672              
1673 1         4 my ($url, $content_type, $reqbody, $resbody, $croak_on_error) = @_;
1674              
1675 1 50       6 $croak_on_error = $self->{croak_on_error} unless defined($croak_on_error);
1676              
1677 1         5 $self->{status} = -1;
1678              
1679 1 50       6 warn "## $url\n" if ($self->{debug});
1680              
1681 1         7 $url = new URI($url);
1682 1 50 33     78 if (
      33        
      33        
      33        
      33        
1683             !$url || !$url->scheme || !$url->scheme eq 'http' ||
1684             !$url->host || !$url->port || $url->port < 1
1685             ) {
1686 0         0 carp "can't parse $url\n";
1687 0         0 return -1;
1688             }
1689              
1690 1         257 my $ua = LWP::UserAgent->new;
1691 1         63613 $ua->agent( "Search-Estraier/$Search::Estraier::VERSION" );
1692              
1693 1         121 my $req;
1694 1 50       9 if ($reqbody) {
1695 0         0 $req = HTTP::Request->new(POST => $url);
1696             } else {
1697 1         15 $req = HTTP::Request->new(GET => $url);
1698             }
1699              
1700 1         213 $req->headers->header( 'Host' => $url->host . ":" . $url->port );
1701 1         170 $req->headers->header( 'Connection', 'close' );
1702 1 50       72 $req->headers->header( 'Authorization', 'Basic ' . $self->{auth} ) if ($self->{auth});
1703 1         81 $req->content_type( $content_type );
1704              
1705 1 50       63 warn $req->headers->as_string,"\n" if ($self->{debug});
1706              
1707 1 50       6 if ($reqbody) {
1708 0 0       0 warn "$reqbody\n" if ($self->{debug});
1709 0         0 $req->content( $reqbody );
1710             }
1711              
1712 1   33     8 my $res = $ua->request($req) || croak "can't make request to $url: $!";
1713              
1714 1 50       53370 warn "## response status: ",$res->status_line,"\n" if ($self->{debug});
1715              
1716 1         6 ($self->{status}, $self->{status_message}) = split(/\s+/, $res->status_line, 2);
1717              
1718 1 50       19 if (! $res->is_success) {
1719 1 50       14 if ($croak_on_error) {
1720 1         8 croak("can't get $url: ",$res->status_line);
1721             } else {
1722 0         0 return -1;
1723             }
1724             }
1725              
1726 0         0 $$resbody .= $res->content;
1727              
1728 0 0 0     0 warn "## response body:\n$$resbody\n" if ($resbody && $self->{debug});
1729              
1730 0         0 return $self->{status};
1731             }
1732              
1733              
1734             =head2 set_snippet_width
1735              
1736             Set width of snippets in results
1737              
1738             $node->set_snippet_width( $wwidth, $hwidth, $awidth );
1739              
1740             C<$wwidth> specifies whole width of snippet. It's C<480> by default. If it's C<0> snippet
1741             is not sent with results. If it is negative, whole document text is sent instead of snippet.
1742              
1743             C<$hwidth> specified width of strings from beginning of string. Default
1744             value is C<96>. Negative or zero value keep previous value.
1745              
1746             C<$awidth> specifies width of strings around each highlighted word. It's C<96> by default.
1747             If negative of zero value is provided previous value is kept unchanged.
1748              
1749             =cut
1750              
1751             sub set_snippet_width {
1752 0     0   0 my $self = shift;
1753              
1754 0         0 my ($wwidth, $hwidth, $awidth) = @_;
1755 0         0 $self->{wwidth} = $wwidth;
1756 0 0       0 $self->{hwidth} = $hwidth if ($hwidth >= 0);
1757 0 0       0 $self->{awidth} = $awidth if ($awidth >= 0);
1758             }
1759              
1760              
1761             =head2 set_user
1762              
1763             Manage users of node
1764              
1765             $node->set_user( 'name', $mode );
1766              
1767             C<$mode> can be one of:
1768              
1769             =over 4
1770              
1771             =item 0
1772              
1773             delete account
1774              
1775             =item 1
1776              
1777             set administrative right for user
1778              
1779             =item 2
1780              
1781             set user account as guest
1782              
1783             =back
1784              
1785             Return true on success, otherwise false.
1786              
1787             =cut
1788              
1789             sub set_user {
1790 0     0   0 my $self = shift;
1791 0         0 my ($name, $mode) = @_;
1792              
1793 0 0       0 return unless ($self->{url});
1794 0 0       0 croak "mode must be number, not '$mode'" unless ($mode =~ m/^\d+$/);
1795              
1796 0         0 $self->shuttle_url( $self->{url} . '/_set_user',
1797             'application/x-www-form-urlencoded',
1798             'name=' . uri_escape($name) . '&mode=' . $mode,
1799             undef
1800             ) == 200;
1801             }
1802              
1803              
1804             =head2 set_link
1805              
1806             Manage node links
1807              
1808             $node->set_link('http://localhost:1978/node/another', 'another node label', $credit);
1809              
1810             If C<$credit> is negative, link is removed.
1811              
1812             =cut
1813              
1814             sub set_link {
1815 0     0   0 my $self = shift;
1816 0         0 my ($url, $label, $credit) = @_;
1817              
1818 0 0       0 return unless ($self->{url});
1819 0 0       0 croak "mode credit be number, not '$credit'" unless ($credit =~ m/^\d+$/);
1820              
1821 0         0 my $reqbody = 'url=' . uri_escape($url) . '&label=' . uri_escape($label);
1822 0 0       0 $reqbody .= '&credit=' . $credit if ($credit > 0);
1823              
1824 0 0       0 if ($self->shuttle_url( $self->{url} . '/_set_link',
1825             'application/x-www-form-urlencoded',
1826             $reqbody,
1827             undef
1828             ) == 200) {
1829             # refresh node info after adding link
1830 0         0 $self->_clear_info;
1831 0         0 return 1;
1832             }
1833 0         0 return undef;
1834             }
1835              
1836             =head2 admins
1837              
1838             my @admins = @{ $node->admins };
1839              
1840             Return array of users with admin rights on node
1841              
1842             =cut
1843              
1844             sub admins {
1845 0     0   0 my $self = shift;
1846 0 0       0 $self->_set_info unless ($self->{inform}->{name});
1847 0         0 return $self->{inform}->{admins};
1848             }
1849              
1850             =head2 guests
1851              
1852             my @guests = @{ $node->guests };
1853              
1854             Return array of users with guest rights on node
1855              
1856             =cut
1857              
1858             sub guests {
1859 0     0   0 my $self = shift;
1860 0 0       0 $self->_set_info unless ($self->{inform}->{name});
1861 0         0 return $self->{inform}->{guests};
1862             }
1863              
1864             =head2 links
1865              
1866             my $links = @{ $node->links };
1867              
1868             Return array of links for this node
1869              
1870             =cut
1871              
1872             sub links {
1873 0     0   0 my $self = shift;
1874 0 0       0 $self->_set_info unless ($self->{inform}->{name});
1875 0         0 return $self->{inform}->{links};
1876             }
1877              
1878             =head2 cacheusage
1879              
1880             Return cache usage for a node
1881              
1882             my $cache = $node->cacheusage;
1883              
1884             =cut
1885              
1886             sub cacheusage {
1887 0     0   0 my $self = shift;
1888              
1889 0 0       0 return unless ($self->{url});
1890              
1891 0         0 my $resbody;
1892 0         0 my $rv = $self->shuttle_url( $self->{url} . '/cacheusage',
1893             'text/plain',
1894             undef,
1895             \$resbody,
1896             );
1897              
1898 0 0 0     0 return if ($rv != 200 || !$resbody);
1899              
1900 0         0 return $resbody;
1901             }
1902              
1903             =head2 master
1904              
1905             Set actions on Hyper Estraier node master (C process)
1906              
1907             $node->master(
1908             action => 'sync'
1909             );
1910              
1911             All available actions are documented in
1912             L
1913              
1914             =cut
1915              
1916             my $estmaster_rest = {
1917             shutdown => {
1918             status => 202,
1919             },
1920             sync => {
1921             status => 202,
1922             },
1923             backup => {
1924             status => 202,
1925             },
1926             userlist => {
1927             status => 200,
1928             returns => [ qw/name passwd flags fname misc/ ],
1929             },
1930             useradd => {
1931             required => [ qw/name passwd flags/ ],
1932             optional => [ qw/fname misc/ ],
1933             status => 200,
1934             },
1935             userdel => {
1936             required => [ qw/name/ ],
1937             status => 200,
1938             },
1939             nodelist => {
1940             status => 200,
1941             returns => [ qw/name label doc_num word_num size/ ],
1942             },
1943             nodeadd => {
1944             required => [ qw/name/ ],
1945             optional => [ qw/label/ ],
1946             status => 200,
1947             },
1948             nodedel => {
1949             required => [ qw/name/ ],
1950             status => 200,
1951             },
1952             nodeclr => {
1953             required => [ qw/name/ ],
1954             status => 200,
1955             },
1956             nodertt => {
1957             status => 200,
1958             },
1959             };
1960              
1961             sub master {
1962 1     1   3 my $self = shift;
1963              
1964 1         3 my $args = {@_};
1965              
1966             # have action?
1967             my $action = $args->{action} || croak "need action, available: ",
1968 1   33     7 join(", ",keys %{ $estmaster_rest });
1969              
1970             # check if action is valid
1971 1         3 my $rest = $estmaster_rest->{$action};
1972 0         0 croak "action '$action' is not supported, available actions: ",
1973 1 50       5 join(", ",keys %{ $estmaster_rest }) unless ($rest);
1974              
1975 1 50       7 croak "BUG: action '$action' needs return status" unless ($rest->{status});
1976              
1977 1         5 my @args;
1978              
1979 1 50 33     10 if ($rest->{required} || $rest->{optional}) {
1980              
1981 0 0       0 map {
1982 0         0 croak "need parametar '$_' for action '$action'" unless ($args->{$_});
1983 0         0 push @args, $_ . '=' . uri_escape( $args->{$_} );
1984 0         0 } ( @{ $rest->{required} } );
1985              
1986 0 0       0 map {
1987 0         0 push @args, $_ . '=' . uri_escape( $args->{$_} ) if ($args->{$_});
1988 0         0 } ( @{ $rest->{optional} } );
1989              
1990             }
1991              
1992 1         10 my $uri = new URI( $self->{url} );
1993              
1994 1         10624 my $resbody;
1995              
1996 1 0       12 my $status = $self->shuttle_url(
1997             'http://' . $uri->host_port . '/master?action=' . $action ,
1998             'application/x-www-form-urlencoded',
1999             join('&', @args),
2000             \$resbody,
2001             1,
2002             ) or confess "shuttle_url failed";
2003              
2004 0 0         if ($status == $rest->{status}) {
2005              
2006             # refresh node info after sync
2007 0 0 0       $self->_clear_info if ($action eq 'sync' || $action =~ m/^node(?:add|del|clr)$/);
2008              
2009 0 0 0       if ($rest->{returns} && wantarray) {
    0          
2010              
2011 0           my @results;
2012 0           my $fields = $#{$rest->{returns}};
  0            
2013              
2014 0           foreach my $line ( split(/[\r\n]/,$resbody) ) {
2015 0           my @e = split(/\t/, $line, $fields + 1);
2016 0           my $row;
2017 0           foreach my $i ( 0 .. $fields) {
2018 0           $row->{ $rest->{returns}->[$i] } = $e[ $i ];
2019             }
2020 0           push @results, $row;
2021             }
2022              
2023 0           return @results;
2024              
2025             } elsif ($resbody) {
2026 0           chomp $resbody;
2027 0           return $resbody;
2028             } else {
2029 0           return 0E0;
2030             }
2031             }
2032              
2033 0           carp "expected status $rest->{status}, but got $status";
2034 0           return undef;
2035             }
2036              
2037             =head1 PRIVATE METHODS
2038              
2039             You could call those directly, but you don't have to. I hope.
2040              
2041             =head2 _set_info
2042              
2043             Set information for node
2044              
2045             $node->_set_info;
2046              
2047             =cut
2048              
2049             sub _set_info {
2050 0     0     my $self = shift;
2051              
2052 0           $self->{status} = -1;
2053 0 0         return unless ($self->{url});
2054              
2055 0           my $resbody;
2056 0           my $rv = $self->shuttle_url( $self->{url} . '/inform',
2057             'text/plain',
2058             undef,
2059             \$resbody,
2060             );
2061              
2062 0 0 0       return if ($rv != 200 || !$resbody);
2063              
2064 0           my @lines = split(/[\r\n]/,$resbody);
2065              
2066 0           $self->_clear_info;
2067              
2068 0           ( $self->{inform}->{name}, $self->{inform}->{label}, $self->{inform}->{dnum},
2069             $self->{inform}->{wnum}, $self->{inform}->{size} ) = split(/\t/, shift @lines, 5);
2070              
2071 0 0         return $resbody unless (@lines);
2072              
2073 0           shift @lines;
2074              
2075 0           while(my $admin = shift @lines) {
2076 0           push @{$self->{inform}->{admins}}, $admin;
  0            
2077             }
2078              
2079 0           while(my $guest = shift @lines) {
2080 0           push @{$self->{inform}->{guests}}, $guest;
  0            
2081             }
2082              
2083 0           while(my $link = shift @lines) {
2084 0           push @{$self->{inform}->{links}}, $link;
  0            
2085             }
2086              
2087 0           return $resbody;
2088              
2089             }
2090              
2091             =head2 _clear_info
2092              
2093             Clear information for node
2094              
2095             $node->_clear_info;
2096              
2097             On next call to C, C
2098             info will be fetch again from Hyper Estraier.
2099              
2100             =cut
2101             sub _clear_info {
2102 0     0     my $self = shift;
2103 0           $self->{inform} = {
2104             dnum => -1,
2105             wnum => -1,
2106             size => -1.0,
2107             };
2108             }
2109              
2110             ###
2111              
2112             =head1 EXPORT
2113              
2114             Nothing.
2115              
2116             =head1 SEE ALSO
2117              
2118             L
2119              
2120             Hyper Estraier Ruby interface on which this module is based.
2121              
2122             Hyper Estraier now also has pure-perl binding included in distribution. It's
2123             a faster way to access databases directly if you are not running
2124             C P2P server.
2125              
2126             =head1 AUTHOR
2127              
2128             Dobrica Pavlinusic, Edpavlin@rot13.orgE
2129              
2130             Robert Klep Erobert@klep.nameE contributed refactored search code
2131              
2132             =head1 COPYRIGHT AND LICENSE
2133              
2134             Copyright (C) 2005-2006 by Dobrica Pavlinusic
2135              
2136             This library is free software; you can redistribute it and/or modify
2137             it under the GPL v2 or later.
2138              
2139             =cut
2140              
2141             1;