/[Search-Estraier]/trunk/lib/Search/Estraier.pm
This is repository of my old source code which isn't updated any more. Go to git.rot13.org for current projects!
ViewVC logotype

Diff of /trunk/lib/Search/Estraier.pm

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 58 by dpavlin, Fri Jan 6 21:05:05 2006 UTC revision 186 by dpavlin, Sun Nov 5 15:53:13 2006 UTC
# Line 4  use 5.008; Line 4  use 5.008;
4  use strict;  use strict;
5  use warnings;  use warnings;
6    
7  our $VERSION = '0.00';  our $VERSION = '0.08_1';
8    
9  =head1 NAME  =head1 NAME
10    
# Line 12  Search::Estraier - pure perl module to u Line 12  Search::Estraier - pure perl module to u
12    
13  =head1 SYNOPSIS  =head1 SYNOPSIS
14    
15    use Search::Estraier;  =head2 Simple indexer
16    my $est = new Search::Estraier();  
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  =head1 DESCRIPTION
79    
# Line 25  or Hyper Estraier development files on t Line 85  or Hyper Estraier development files on t
85  It is implemented as multiple packages which closly resamble Ruby  It is implemented as multiple packages which closly resamble Ruby
86  implementation. It also includes methods to manage nodes.  implementation. It also includes methods to manage nodes.
87    
88    There are few examples in C<scripts> directory of this distribution.
89    
90  =cut  =cut
91    
92  =head1 Inheritable common methods  =head1 Inheritable common methods
# Line 41  Remove multiple whitespaces from string, Line 103  Remove multiple whitespaces from string,
103  =cut  =cut
104    
105  sub _s {  sub _s {
106          my $text = $_[1] || return;          my $text = $_[1];
107            return unless defined($text);
108          $text =~ s/\s\s+/ /gs;          $text =~ s/\s\s+/ /gs;
109          $text =~ s/^\s+//;          $text =~ s/^\s+//;
110          $text =~ s/\s+$//;          $text =~ s/\s+$//;
# Line 57  our @ISA = qw/Search::Estraier/; Line 120  our @ISA = qw/Search::Estraier/;
120    
121  =head1 Search::Estraier::Document  =head1 Search::Estraier::Document
122    
123  This class implements Document which is collection of attributes  This class implements Document which is single item in Hyper Estraier.
124  (key=value), vectors (also key value) display text and hidden text.  
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<attrindex> in estmaster's C<_conf>
134    file for better performance. See C<attrindex> in
135    L<Hyper Estraier P2P Guide|http://hyperestraier.sourceforge.net/nguide-en.html>.
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  =head2 new
153    
# Line 94  sub new { Line 182  sub new {
182    
183                          if ($line =~ m/^%VECTOR\t(.+)$/) {                          if ($line =~ m/^%VECTOR\t(.+)$/) {
184                                  my @fields = split(/\t/, $1);                                  my @fields = split(/\t/, $1);
185                                  for my $i ( 0 .. ($#fields - 1) ) {                                  if ($#fields % 2 == 1) {
186                                          $self->{kwords}->{ $fields[ $i ] } = $fields[ $i + 1 ];                                          $self->{kwords} = { @fields };
187                                          $i++;                                  } else {
188                                            warn "can't decode $line\n";
189                                  }                                  }
190                                  next;                                  next;
191                            } elsif ($line =~ m/^%SCORE\t(.+)$/) {
192                                $self->{score} = $1;
193                                next;
194                          } elsif ($line =~ m/^%/) {                          } elsif ($line =~ m/^%/) {
195                                  # What is this? comment?                                  # What is this? comment?
196                                  #warn "$line\n";                                  #warn "$line\n";
# Line 106  sub new { Line 198  sub new {
198                          } elsif ($line =~ m/^$/) {                          } elsif ($line =~ m/^$/) {
199                                  $in_text = 1;                                  $in_text = 1;
200                                  next;                                  next;
201                          } elsif ($line =~ m/^(.+)=(.+)$/) {                          } elsif ($line =~ m/^(.+)=(.*)$/) {
202                                  $self->{attrs}->{ $1 } = $2;                                  $self->{attrs}->{ $1 } = $2;
203                                  next;                                  next;
204                          }                          }
205    
206                          warn "draft ignored: $line\n";                          warn "draft ignored: '$line'\n";
207                  }                  }
208          }          }
209    
# Line 180  sub add_hidden_text { Line 272  sub add_hidden_text {
272          push @{ $self->{htexts} }, $self->_s($text);          push @{ $self->{htexts} }, $self->_s($text);
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            my $self = shift;
288            return unless (@_);
289    
290            # this is ugly, but works
291            die "add_vector needs HASH as argument" unless ($#_ % 2 == 1);
292    
293            $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        my $self = shift;
306        my $score = shift;
307        return unless (defined($score));
308        $self->{score} = $score;
309    }
310    
311    =head2 score
312    
313    Get the substitute score
314    
315    =cut
316    
317    sub score {
318        my $self = shift;
319        return -1 unless (defined($self->{score}));
320        return $self->{score};
321    }
322    
323  =head2 id  =head2 id
324    
# Line 205  Returns array with attribute names from Line 344  Returns array with attribute names from
344    
345  sub attr_names {  sub attr_names {
346          my $self = shift;          my $self = shift;
347          croak "attr_names return array, not scalar" if (! wantarray);          return unless ($self->{attrs});
348            #croak "attr_names return array, not scalar" if (! wantarray);
349          return sort keys %{ $self->{attrs} };          return sort keys %{ $self->{attrs} };
350  }  }
351    
# Line 221  Returns value of an attribute. Line 361  Returns value of an attribute.
361  sub attr {  sub attr {
362          my $self = shift;          my $self = shift;
363          my $name = shift;          my $name = shift;
364            return unless (defined($name) && $self->{attrs});
365          return $self->{'attrs'}->{ $name };          return $self->{attrs}->{ $name };
366  }  }
367    
368    
# Line 236  Returns array with text sentences. Line 376  Returns array with text sentences.
376    
377  sub texts {  sub texts {
378          my $self = shift;          my $self = shift;
379          confess "texts return array, not scalar" if (! wantarray);          #confess "texts return array, not scalar" if (! wantarray);
380          return @{ $self->{dtexts} };          return @{ $self->{dtexts} } if ($self->{dtexts});
381  }  }
382    
383    
# Line 251  Return whole text as single scalar. Line 391  Return whole text as single scalar.
391    
392  sub cat_texts {  sub cat_texts {
393          my $self = shift;          my $self = shift;
394          return join(' ',@{ $self->{dtexts} });          return join(' ',@{ $self->{dtexts} }) if ($self->{dtexts});
395  }  }
396    
397    
# Line 268  sub dump_draft { Line 408  sub dump_draft {
408          my $draft;          my $draft;
409    
410          foreach my $attr_name (sort keys %{ $self->{attrs} }) {          foreach my $attr_name (sort keys %{ $self->{attrs} }) {
411                  $draft .= $attr_name . '=' . $self->{attrs}->{$attr_name} . "\n";                  next unless defined(my $v = $self->{attrs}->{$attr_name});
412                    $draft .= $attr_name . '=' . $v . "\n";
413          }          }
414    
415          if ($self->{kwords}) {          if ($self->{kwords}) {
416                  $draft .= '%%VECTOR';                  $draft .= '%VECTOR';
417                  while (my ($key, $value) = each %{ $self->{kwords} }) {                  while (my ($key, $value) = each %{ $self->{kwords} }) {
418                          $draft .= "\t$key\t$value";                          $draft .= "\t$key\t$value";
419                  }                  }
420                  $draft .= "\n";                  $draft .= "\n";
421          }          }
422    
423            if (defined($self->{score}) && $self->{score} >= 0) {
424                $draft .= "%SCORE\t" . $self->{score} . "\n";
425            }
426    
427          $draft .= "\n";          $draft .= "\n";
428    
429          $draft .= join("\n", @{ $self->{dtexts} }) . "\n" if ($self->{dtexts});          $draft .= join("\n", @{ $self->{dtexts} }) . "\n" if ($self->{dtexts});
# Line 316  sub delete { Line 461  sub delete {
461    
462  package Search::Estraier::Condition;  package Search::Estraier::Condition;
463    
464  use Carp qw/confess croak/;  use Carp qw/carp confess croak/;
465    
466  use Search::Estraier;  use Search::Estraier;
467  our @ISA = qw/Search::Estraier/;  our @ISA = qw/Search::Estraier/;
# Line 394  sub set_max { Line 539  sub set_max {
539    
540  =head2 set_options  =head2 set_options
541    
542    $cond->set_options( SURE => 1 );    $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<set_options> 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  =cut
582    
583  my $options = {  my $options = {
         # check N-gram keys skipping by three  
584          SURE => 1 << 0,          SURE => 1 << 0,
         # check N-gram keys skipping by two  
585          USUAL => 1 << 1,          USUAL => 1 << 1,
         # without TF-IDF tuning  
586          FAST => 1 << 2,          FAST => 1 << 2,
         # with the simplified phrase  
587          AGITO => 1 << 3,          AGITO => 1 << 3,
         # check every N-gram key  
588          NOIDF => 1 << 4,          NOIDF => 1 << 4,
         # check N-gram keys skipping by one  
589          SIMPLE => 1 << 10,          SIMPLE => 1 << 10,
590  };  };
591    
592  sub set_options {  sub set_options {
593          my $self = shift;          my $self = shift;
594          my $option = shift;          my $opt = 0;
595          confess "unknown option" unless ($options->{$option});          foreach my $option (@_) {
596          $self->{options} ||= $options->{$option};                  my $mask;
597                    unless ($mask = $options->{$option}) {
598                            if ($option eq '1') {
599                                    next;
600                            } else {
601                                    croak "unknown option $option";
602                            }
603                    }
604                    $opt += $mask;
605            }
606            $self->{options} = $opt;
607  }  }
608    
609    
# Line 460  Return search result attrs. Line 646  Return search result attrs.
646  sub attrs {  sub attrs {
647          my $self = shift;          my $self = shift;
648          #croak "attrs return array, not scalar" if (! wantarray);          #croak "attrs return array, not scalar" if (! wantarray);
649          return @{ $self->{attrs} };          return @{ $self->{attrs} } if ($self->{attrs});
650  }  }
651    
652    
# Line 496  sub options { Line 682  sub 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<offset> in RDBMS.
692    
693    =cut
694    
695    sub set_skip {
696            my $self = shift;
697            $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            my $self = shift;
710            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            my $self = shift;
722            $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            my $self = shift;
735            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            my $self = shift;
750            return unless (@_);
751            $self->{mask} = \@_;
752    }
753    
754    
755  package Search::Estraier::ResultDocument;  package Search::Estraier::ResultDocument;
756    
757  use Carp qw/croak/;  use Carp qw/croak/;
# Line 524  sub new { Line 780  sub new {
780          my $self = {@_};          my $self = {@_};
781          bless($self, $class);          bless($self, $class);
782    
783          foreach my $f (qw/uri attrs snippet keywords/) {          croak "missing uri for ResultDocument" unless defined($self->{uri});
                 croak "missing $f for ResultDocument" unless defined($self->{$f});  
         }  
784    
785          $self ? return $self : return undef;          $self ? return $self : return undef;
786  }  }
# Line 641  Return number of documents Line 895  Return number of documents
895    
896    print $res->doc_num;    print $res->doc_num;
897    
898    This will return real number of documents (limited by C<max>).
899    If you want to get total number of hits, see C<hits>.
900    
901  =cut  =cut
902    
903  sub doc_num {  sub doc_num {
# Line 672  sub get_doc { Line 929  sub get_doc {
929    
930  Return specific hint from results.  Return specific hint from results.
931    
932    print $rec->hint( 'VERSION' );    print $res->hint( 'VERSION' );
933    
934  Possible hints are: C<VERSION>, C<NODE>, C<HIT>, C<HINT#n>, C<DOCNUM>, C<WORDNUM>,  Possible hints are: C<VERSION>, C<NODE>, C<HIT>, C<HINT#n>, C<DOCNUM>, C<WORDNUM>,
935  C<TIME>, C<LINK#n>, C<VIEW>.  C<TIME>, C<LINK#n>, C<VIEW>.
# Line 685  sub hint { Line 942  sub hint {
942          return $self->{hints}->{$key};          return $self->{hints}->{$key};
943  }  }
944    
945    =head2 hints
946    
947    More perlish version of C<hint>. This one returns hash.
948    
949      my %hints = $res->hints;
950    
951    =cut
952    
953    sub hints {
954            my $self = shift;
955            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            my $self = shift;
974            return $self->{hints}->{'HIT'} || 0;
975    }
976    
977  package Search::Estraier::Node;  package Search::Estraier::Node;
978    
# Line 700  use URI::Escape qw/uri_escape/; Line 988  use URI::Escape qw/uri_escape/;
988    
989    my $node = new Search::HyperEstraier::Node;    my $node = new Search::HyperEstraier::Node;
990    
991    or optionally with C<url> 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<create> is used
1030    
1031    =item debug
1032    
1033    dumps a B<lot> 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  =cut
1044    
1045  sub new {  sub new {
# Line 707  sub new { Line 1047  sub new {
1047          my $self = {          my $self = {
1048                  pxport => -1,                  pxport => -1,
1049                  timeout => 0,   # this used to be -1                  timeout => 0,   # this used to be -1
                 dnum => -1,  
                 wnum => -1,  
                 size => -1.0,  
1050                  wwidth => 480,                  wwidth => 480,
1051                  hwidth => 96,                  hwidth => 96,
1052                  awidth => 96,                  awidth => 96,
1053                  status => -1,                  status => -1,
1054          };          };
1055    
1056          bless($self, $class);          bless($self, $class);
1057    
1058          my $args = {@_};          if ($#_ == 0) {
1059                    $self->{url} = shift;
1060            } else {
1061                    %$self = ( %$self, @_ );
1062    
1063                    $self->set_auth( $self->{user}, $self->{passwd} ) if ($self->{user});
1064    
1065                    warn "## Node debug on\n" if ($self->{debug});
1066            }
1067    
1068          $self->{debug} = $args->{debug};          $self->{inform} = {
1069          warn "## Node debug on\n" if ($self->{debug});                  dnum => -1,
1070                    wnum => -1,
1071                    size => -1.0,
1072            };
1073    
1074            if ($self->{create}) {
1075                    if (! eval { $self->name } || $@) {
1076                            my $name = $1 if ($self->{url} =~ m#/node/([^/]+)/*#);
1077                            croak "can't find node name in '$self->{url}'" unless ($name);
1078                            my $label = $self->{label} || $name;
1079                            $self->master(
1080                                    action => 'nodeadd',
1081                                    name => $name,
1082                                    label => $label,
1083                            ) || croak "can't create node $name ($label)";
1084                    }
1085            }
1086    
1087          $self ? return $self : return undef;          $self ? return $self : return undef;
1088  }  }
# Line 812  Add a document Line 1174  Add a document
1174    
1175    $node->put_doc( $document_draft ) or die "can't add document";    $node->put_doc( $document_draft ) or die "can't add document";
1176    
1177  Return true on success or false on failture.  Return true on success or false on failure.
1178    
1179  =cut  =cut
1180    
# Line 820  sub put_doc { Line 1182  sub put_doc {
1182          my $self = shift;          my $self = shift;
1183          my $doc = shift || return;          my $doc = shift || return;
1184          return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));          return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));
1185          $self->shuttle_url( $self->{url} . '/put_doc',          if ($self->shuttle_url( $self->{url} . '/put_doc',
1186                  'text/x-estraier-draft',                  'text/x-estraier-draft',
1187                  $doc->dump_draft,                  $doc->dump_draft,
1188                  undef                  undef
1189          ) == 200;          ) == 200) {
1190                    $self->_clear_info;
1191                    return 1;
1192            }
1193            return undef;
1194  }  }
1195    
1196    
# Line 843  sub out_doc { Line 1209  sub out_doc {
1209          my $id = shift || return;          my $id = shift || return;
1210          return unless ($self->{url});          return unless ($self->{url});
1211          croak "id must be number, not '$id'" unless ($id =~ m/^\d+$/);          croak "id must be number, not '$id'" unless ($id =~ m/^\d+$/);
1212          $self->shuttle_url( $self->{url} . '/out_doc',          if ($self->shuttle_url( $self->{url} . '/out_doc',
1213                  'application/x-www-form-urlencoded',                  'application/x-www-form-urlencoded',
1214                  "id=$id",                  "id=$id",
1215                  undef                  undef
1216          ) == 200;          ) == 200) {
1217                    $self->_clear_info;
1218                    return 1;
1219            }
1220            return undef;
1221  }  }
1222    
1223    
# Line 865  sub out_doc_by_uri { Line 1235  sub out_doc_by_uri {
1235          my $self = shift;          my $self = shift;
1236          my $uri = shift || return;          my $uri = shift || return;
1237          return unless ($self->{url});          return unless ($self->{url});
1238          $self->shuttle_url( $self->{url} . '/out_doc',          if ($self->shuttle_url( $self->{url} . '/out_doc',
1239                  'application/x-www-form-urlencoded',                  'application/x-www-form-urlencoded',
1240                  "uri=" . uri_escape($uri),                  "uri=" . uri_escape($uri),
1241                  undef                  undef
1242          ) == 200;          ) == 200) {
1243                    $self->_clear_info;
1244                    return 1;
1245            }
1246            return undef;
1247  }  }
1248    
1249    
# Line 887  sub edit_doc { Line 1261  sub edit_doc {
1261          my $self = shift;          my $self = shift;
1262          my $doc = shift || return;          my $doc = shift || return;
1263          return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));          return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));
1264          $self->shuttle_url( $self->{url} . '/edit_doc',          if ($self->shuttle_url( $self->{url} . '/edit_doc',
1265                  'text/x-estraier-draft',                  'text/x-estraier-draft',
1266                  $doc->dump_draft,                  $doc->dump_draft,
1267                  undef                  undef
1268          ) == 200;          ) == 200) {
1269                    $self->_clear_info;
1270                    return 1;
1271            }
1272            return undef;
1273  }  }
1274    
1275    
# Line 1000  Get ID of document specified by URI Line 1378  Get ID of document specified by URI
1378    
1379    my $id = $node->uri_to_id( 'file:///document/uri/42' );    my $id = $node->uri_to_id( 'file:///document/uri/42' );
1380    
1381    This method won't croak, even if using C<croak_on_error>.
1382    
1383  =cut  =cut
1384    
1385  sub uri_to_id {  sub uri_to_id {
1386          my $self = shift;          my $self = shift;
1387          my $uri = shift || return;          my $uri = shift || return;
1388          return $self->_fetch_doc( uri => $uri, path => '/uri_to_id', chomp_resbody => 1 );          return $self->_fetch_doc( uri => $uri, path => '/uri_to_id', chomp_resbody => 1, croak_on_error => 0 );
1389  }  }
1390    
1391    
# Line 1047  sub _fetch_doc { Line 1427  sub _fetch_doc {
1427          $path = '/etch_doc' if ($a->{etch});          $path = '/etch_doc' if ($a->{etch});
1428    
1429          if ($a->{id}) {          if ($a->{id}) {
1430                  croak "id must be numberm not '$a->{id}'" unless ($a->{id} =~ m/^\d+$/);                  croak "id must be number not '$a->{id}'" unless ($a->{id} =~ m/^\d+$/);
1431                  $arg = 'id=' . $a->{id};                  $arg = 'id=' . $a->{id};
1432          } elsif ($a->{uri}) {          } elsif ($a->{uri}) {
1433                  $arg = 'uri=' . uri_escape($a->{uri});                  $arg = 'uri=' . uri_escape($a->{uri});
# Line 1065  sub _fetch_doc { Line 1445  sub _fetch_doc {
1445                  'application/x-www-form-urlencoded',                  'application/x-www-form-urlencoded',
1446                  $arg,                  $arg,
1447                  \$resbody,                  \$resbody,
1448                    $a->{croak_on_error},
1449          );          );
1450    
1451          return if ($rv != 200);          return if ($rv != 200);
# Line 1095  sub _fetch_doc { Line 1476  sub _fetch_doc {
1476    
1477  sub name {  sub name {
1478          my $self = shift;          my $self = shift;
1479          $self->_set_info unless ($self->{name});          $self->_set_info unless ($self->{inform}->{name});
1480          return $self->{name};          return $self->{inform}->{name};
1481  }  }
1482    
1483    
# Line 1108  sub name { Line 1489  sub name {
1489    
1490  sub label {  sub label {
1491          my $self = shift;          my $self = shift;
1492          $self->_set_info unless ($self->{label});          $self->_set_info unless ($self->{inform}->{label});
1493          return $self->{label};          return $self->{inform}->{label};
1494  }  }
1495    
1496    
# Line 1121  sub label { Line 1502  sub label {
1502    
1503  sub doc_num {  sub doc_num {
1504          my $self = shift;          my $self = shift;
1505          $self->_set_info if ($self->{dnum} < 0);          $self->_set_info if ($self->{inform}->{dnum} < 0);
1506          return $self->{dnum};          return $self->{inform}->{dnum};
1507  }  }
1508    
1509    
# Line 1134  sub doc_num { Line 1515  sub doc_num {
1515    
1516  sub word_num {  sub word_num {
1517          my $self = shift;          my $self = shift;
1518          $self->_set_info if ($self->{wnum} < 0);          $self->_set_info if ($self->{inform}->{wnum} < 0);
1519          return $self->{wnum};          return $self->{inform}->{wnum};
1520  }  }
1521    
1522    
# Line 1147  sub word_num { Line 1528  sub word_num {
1528    
1529  sub size {  sub size {
1530          my $self = shift;          my $self = shift;
1531          $self->_set_info if ($self->{size} < 0);          $self->_set_info if ($self->{inform}->{size} < 0);
1532          return $self->{size};          return $self->{inform}->{size};
1533  }  }
1534    
1535    
# Line 1176  sub search { Line 1557  sub search {
1557    
1558          my $rv = $self->shuttle_url( $self->{url} . '/search',          my $rv = $self->shuttle_url( $self->{url} . '/search',
1559                  'application/x-www-form-urlencoded',                  'application/x-www-form-urlencoded',
1560                  $self->cond_to_query( $cond ),                  $self->cond_to_query( $cond, $depth ),
1561                  \$resbody,                  \$resbody,
1562          );          );
1563          return if ($rv != 200);          return if ($rv != 200);
1564    
1565          my (@docs, $hints);          my @records     = split /--------\[.*?\]--------(?::END)?\r?\n/, $resbody;
1566            my $hintsText   = splice @records, 0, 2; # starts with empty record
1567          my @lines = split(/\n/, $resbody);          my $hints               = { $hintsText =~ m/^(.*?)\t(.*?)$/gsm };
1568          return unless (@lines);  
1569            # process records
1570          my $border = $lines[0];          my $docs = [];
1571          my $isend = 0;          foreach my $record (@records)
1572          my $lnum = 1;          {
1573                    # split into keys and snippets
1574          while ( $lnum <= $#lines ) {                  my ($keys, $snippet) = $record =~ m/^(.*?)\n\n(.*?)$/s;
                 my $line = $lines[$lnum];  
                 $lnum++;  
   
                 #warn "## $line\n";  
                 if ($line && $line =~ m/^\Q$border\E(:END)*$/) {  
                         $isend = $1;  
                         last;  
                 }  
   
                 if ($line =~ /\t/) {  
                         my ($k,$v) = split(/\t/, $line, 2);  
                         $hints->{$k} = $v;  
                 }  
         }  
   
         my $snum = $lnum;  
   
         while( ! $isend && $lnum <= $#lines ) {  
                 my $line = $lines[$lnum];  
                 #warn "# $lnum: $line\n";  
                 $lnum++;  
   
                 if ($line && $line =~ m/^\Q$border\E/) {  
                         if ($lnum > $snum) {  
                                 my $rdattrs;  
                                 my $rdvector;  
                                 my $rdsnippet;  
                                   
                                 my $rlnum = $snum;  
                                 while ($rlnum < $lnum - 1 ) {  
                                         #my $rdline = $self->_s($lines[$rlnum]);  
                                         my $rdline = $lines[$rlnum];  
                                         $rlnum++;  
                                         last unless ($rdline);  
                                         if ($rdline =~ /^%/) {  
                                                 $rdvector = $1 if ($rdline =~ /^%VECTOR\t(.+)$/);  
                                         } elsif($rdline =~ /=/) {  
                                                 $rdattrs->{$1} = $2 if ($rdline =~ /^(.+)=(.+)$/);  
                                         } else {  
                                                 confess "invalid format of response";  
                                         }  
                                 }  
                                 while($rlnum < $lnum - 1) {  
                                         my $rdline = $lines[$rlnum];  
                                         $rlnum++;  
                                         $rdsnippet .= "$rdline\n";  
                                 }  
                                 #warn Dumper($rdvector, $rdattrs, $rdsnippet);  
                                 if (my $rduri = $rdattrs->{'@uri'}) {  
                                         push @docs, new Search::Estraier::ResultDocument(  
                                                 uri => $rduri,  
                                                 attrs => $rdattrs,  
                                                 snippet => $rdsnippet,  
                                                 keywords => $rdvector,  
                                         );  
                                 }  
                         }  
                         $snum = $lnum;  
                         #warn "### $line\n";  
                         $isend = 1 if ($line =~ /:END$/);  
                 }  
   
         }  
1575    
1576          if (! $isend) {                  # create document hash
1577                  warn "received result doesn't have :END\n$resbody";                  my $doc                         = { $keys =~ m/^(.*?)=(.*?)$/gsm };
1578                  return;                  $doc->{'@keywords'}     = $doc->{keywords};
1579                    ($doc->{keywords})      = $keys =~ m/^%VECTOR\t(.*?)$/gm;
1580                    $doc->{snippet}         = $snippet;
1581    
1582                    push @$docs, new Search::Estraier::ResultDocument(
1583                            attrs           => $doc,
1584                            uri             => $doc->{'@uri'},
1585                            snippet         => $snippet,
1586                            keywords        => $doc->{'keywords'},
1587                    );
1588          }          }
1589    
1590          #warn Dumper(\@docs, $hints);          return new Search::Estraier::NodeResult( docs => $docs, hints => $hints );
   
         return new Search::Estraier::NodeResult( docs => \@docs, hints => $hints );  
1591  }  }
1592    
1593    
# Line 1270  sub search { Line 1595  sub search {
1595    
1596  Return URI encoded string generated from Search::Estraier::Condition  Return URI encoded string generated from Search::Estraier::Condition
1597    
1598    my $args = $node->cond_to_query( $cond );    my $args = $node->cond_to_query( $cond, $depth );
1599    
1600  =cut  =cut
1601    
# Line 1279  sub cond_to_query { Line 1604  sub cond_to_query {
1604    
1605          my $cond = shift || return;          my $cond = shift || return;
1606          croak "condition must be Search::Estraier::Condition, not '$cond->isa'" unless ($cond->isa('Search::Estraier::Condition'));          croak "condition must be Search::Estraier::Condition, not '$cond->isa'" unless ($cond->isa('Search::Estraier::Condition'));
1607            my $depth = shift;
1608    
1609          my @args;          my @args;
1610    
# Line 1288  sub cond_to_query { Line 1614  sub cond_to_query {
1614    
1615          if (my @attrs = $cond->attrs) {          if (my @attrs = $cond->attrs) {
1616                  for my $i ( 0 .. $#attrs ) {                  for my $i ( 0 .. $#attrs ) {
1617                          push @args,'attr' . ($i+1) . '=' . uri_escape( $attrs[$i] );                          push @args,'attr' . ($i+1) . '=' . uri_escape( $attrs[$i] ) if ($attrs[$i]);
1618                  }                  }
1619          }          }
1620    
# Line 1306  sub cond_to_query { Line 1632  sub cond_to_query {
1632                  push @args, 'options=' . $options;                  push @args, 'options=' . $options;
1633          }          }
1634    
1635          push @args, 'depth=' . $self->{depth} if ($self->{depth});          push @args, 'depth=' . $depth if ($depth);
1636          push @args, 'wwidth=' . $self->{wwidth};          push @args, 'wwidth=' . $self->{wwidth};
1637          push @args, 'hwidth=' . $self->{hwidth};          push @args, 'hwidth=' . $self->{hwidth};
1638          push @args, 'awidth=' . $self->{awidth};          push @args, 'awidth=' . $self->{awidth};
1639            push @args, 'skip=' . $cond->{skip} if ($cond->{skip});
1640    
1641            if (my $distinct = $cond->distinct) {
1642                    push @args, 'distinct=' . uri_escape($distinct);
1643            }
1644    
1645            if ($cond->{mask}) {
1646                    my $mask = 0;
1647                    map { $mask += ( 2 ** $_ ) } @{ $cond->{mask} };
1648    
1649                    push @args, 'mask=' . $mask if ($mask);
1650            }
1651    
1652          return join('&', @args);          return join('&', @args);
1653  }  }
# Line 1317  sub cond_to_query { Line 1655  sub cond_to_query {
1655    
1656  =head2 shuttle_url  =head2 shuttle_url
1657    
1658  This is method which uses C<IO::Socket::INET> to communicate with Hyper Estraier node  This is method which uses C<LWP::UserAgent> to communicate with Hyper Estraier node
1659  master.  master.
1660    
1661    my $rv = shuttle_url( $url, $content_type, $req_body, \$resbody );    my $rv = shuttle_url( $url, $content_type, $req_body, \$resbody );
# Line 1327  body will be saved within object. Line 1665  body will be saved within object.
1665    
1666  =cut  =cut
1667    
1668    use LWP::UserAgent;
1669    
1670  sub shuttle_url {  sub shuttle_url {
1671          my $self = shift;          my $self = shift;
1672    
1673          my ($url, $content_type, $reqbody, $resbody) = @_;          my ($url, $content_type, $reqbody, $resbody, $croak_on_error) = @_;
1674    
1675            $croak_on_error = $self->{croak_on_error} unless defined($croak_on_error);
1676    
1677          $self->{status} = -1;          $self->{status} = -1;
1678    
# Line 1345  sub shuttle_url { Line 1687  sub shuttle_url {
1687                  return -1;                  return -1;
1688          }          }
1689    
1690          my ($host,$port,$query) = ($url->host, $url->port, $url->path);          my $ua = LWP::UserAgent->new;
1691            $ua->agent( "Search-Estraier/$Search::Estraier::VERSION" );
         if ($self->{pxhost}) {  
                 ($host,$port) = ($self->{pxhost}, $self->{pxport});  
                 $query = "http://$host:$port/$query";  
         }  
   
         $query .= '?' . $url->query if ($url->query && ! $reqbody);  
   
         my $headers;  
1692    
1693            my $req;
1694          if ($reqbody) {          if ($reqbody) {
1695                  $headers .= "POST $query HTTP/1.0\r\n";                  $req = HTTP::Request->new(POST => $url);
1696          } else {          } else {
1697                  $headers .= "GET $query HTTP/1.0\r\n";                  $req = HTTP::Request->new(GET => $url);
1698          }          }
1699    
1700          $headers .= "Host: " . $url->host . ":" . $url->port . "\r\n";          $req->headers->header( 'Host' => $url->host . ":" . $url->port );
1701          $headers .= "Connection: close\r\n";          $req->headers->header( 'Connection', 'close' );
1702          $headers .= "User-Agent: Search-Estraier/$Search::Estraier::VERSION\r\n";          $req->headers->header( 'Authorization', 'Basic ' . $self->{auth} ) if ($self->{auth});
1703          $headers .= "Content-Type: $content_type\r\n";          $req->content_type( $content_type );
         $headers .= "Authorization: Basic $self->{auth}\r\n";  
         my $len = 0;  
         {  
                 use bytes;  
                 $len = length($reqbody) if ($reqbody);  
         }  
         $headers .= "Content-Length: $len\r\n";  
         $headers .= "\r\n";  
1704    
1705          my $sock = IO::Socket::INET->new(          warn $req->headers->as_string,"\n" if ($self->{debug});
                 PeerAddr        => $host,  
                 PeerPort        => $port,  
                 Proto           => 'tcp',  
                 Timeout         => $self->{timeout} || 90,  
         );  
1706    
1707          if (! $sock) {          if ($reqbody) {
1708                  carp "can't open socket to $host:$port";                  warn "$reqbody\n" if ($self->{debug});
1709                  return -1;                  $req->content( $reqbody );
1710          }          }
1711    
1712          warn $headers if ($self->{debug});          my $res = $ua->request($req) || croak "can't make request to $url: $!";
1713    
1714          print $sock $headers or          warn "## response status: ",$res->status_line,"\n" if ($self->{debug});
                 carp "can't send headers to network:\n$headers\n" and return -1;  
1715    
1716          if ($reqbody) {          ($self->{status}, $self->{status_message}) = split(/\s+/, $res->status_line, 2);
                 warn "$reqbody\n" if ($self->{debug});  
                 print $sock $reqbody or  
                         carp "can't send request body to network:\n$$reqbody\n" and return -1;  
         }  
1717    
1718          my $line = <$sock>;          if (! $res->is_success) {
1719          chomp($line);                  if ($croak_on_error) {
1720          my ($schema, $res_status, undef) = split(/  */, $line, 3);                          croak("can't get $url: ",$res->status_line);
1721          return if ($schema !~ /^HTTP/ || ! $res_status);                  } else {
1722                            return -1;
1723          $self->{status} = $res_status;                  }
1724          warn "## response status: $res_status\n" if ($self->{debug});          }
   
         # skip rest of headers  
         $line = <$sock>;  
         while ($line) {  
                 $line = <$sock>;  
                 $line =~ s/[\r\n]+$//;  
                 warn "## ", $line || 'NULL', " ##\n" if ($self->{debug});  
         };  
1725    
1726          # read body          $$resbody .= $res->content;
         $len = 0;  
         do {  
                 $len = read($sock, my $buf, 8192);  
                 $$resbody .= $buf if ($resbody);  
         } while ($len);  
1727    
1728          warn "## response body:\n$$resbody\n" if ($resbody && $self->{debug});          warn "## response body:\n$$resbody\n" if ($resbody && $self->{debug});
1729    
# Line 1490  sub set_user { Line 1794  sub set_user {
1794          croak "mode must be number, not '$mode'" unless ($mode =~ m/^\d+$/);          croak "mode must be number, not '$mode'" unless ($mode =~ m/^\d+$/);
1795    
1796          $self->shuttle_url( $self->{url} . '/_set_user',          $self->shuttle_url( $self->{url} . '/_set_user',
1797                  'text/plain',                  'application/x-www-form-urlencoded',
1798                  'name=' . uri_escape($name) . '&mode=' . $mode,                  'name=' . uri_escape($name) . '&mode=' . $mode,
1799                  undef                  undef
1800          ) == 200;          ) == 200;
# Line 1517  sub set_link { Line 1821  sub set_link {
1821          my $reqbody = 'url=' . uri_escape($url) . '&label=' . uri_escape($label);          my $reqbody = 'url=' . uri_escape($url) . '&label=' . uri_escape($label);
1822          $reqbody .= '&credit=' . $credit if ($credit > 0);          $reqbody .= '&credit=' . $credit if ($credit > 0);
1823    
1824          $self->shuttle_url( $self->{url} . '/_set_link',          if ($self->shuttle_url( $self->{url} . '/_set_link',
1825                  'text/plain',                  'application/x-www-form-urlencoded',
1826                  $reqbody,                  $reqbody,
1827                  undef                  undef
1828          ) == 200;          ) == 200) {
1829                    # refresh node info after adding link
1830                    $self->_clear_info;
1831                    return 1;
1832            }
1833            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            my $self = shift;
1846            $self->_set_info unless ($self->{inform}->{name});
1847            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            my $self = shift;
1860            $self->_set_info unless ($self->{inform}->{name});
1861            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            my $self = shift;
1874            $self->_set_info unless ($self->{inform}->{name});
1875            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            my $self = shift;
1888    
1889            return unless ($self->{url});
1890    
1891            my $resbody;
1892            my $rv = $self->shuttle_url( $self->{url} . '/cacheusage',
1893                    'text/plain',
1894                    undef,
1895                    \$resbody,
1896            );
1897    
1898            return if ($rv != 200 || !$resbody);
1899    
1900            return $resbody;
1901  }  }
1902    
1903    =head2 master
1904    
1905    Set actions on Hyper Estraier node master (C<estmaster> process)
1906    
1907      $node->master(
1908            action => 'sync'
1909      );
1910    
1911    All available actions are documented in
1912    L<http://hyperestraier.sourceforge.net/nguide-en.html#protocol>
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            my $self = shift;
1963    
1964            my $args = {@_};
1965    
1966            # have action?
1967            my $action = $args->{action} || croak "need action, available: ",
1968                    join(", ",keys %{ $estmaster_rest });
1969    
1970            # check if action is valid
1971            my $rest = $estmaster_rest->{$action};
1972            croak "action '$action' is not supported, available actions: ",
1973                    join(", ",keys %{ $estmaster_rest }) unless ($rest);
1974    
1975            croak "BUG: action '$action' needs return status" unless ($rest->{status});
1976    
1977            my @args;
1978    
1979            if ($rest->{required} || $rest->{optional}) {
1980    
1981                    map {
1982                            croak "need parametar '$_' for action '$action'" unless ($args->{$_});
1983                            push @args, $_ . '=' . uri_escape( $args->{$_} );
1984                    } ( @{ $rest->{required} } );
1985    
1986                    map {
1987                            push @args, $_ . '=' . uri_escape( $args->{$_} ) if ($args->{$_});
1988                    } ( @{ $rest->{optional} } );
1989    
1990            }
1991    
1992            my $uri = new URI( $self->{url} );
1993    
1994            my $resbody;
1995    
1996            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            if ($status == $rest->{status}) {
2005    
2006                    # refresh node info after sync
2007                    $self->_clear_info if ($action eq 'sync' || $action =~ m/^node(?:add|del|clr)$/);
2008    
2009                    if ($rest->{returns} && wantarray) {
2010    
2011                            my @results;
2012                            my $fields = $#{$rest->{returns}};
2013    
2014                            foreach my $line ( split(/[\r\n]/,$resbody) ) {
2015                                    my @e = split(/\t/, $line, $fields + 1);
2016                                    my $row;
2017                                    foreach my $i ( 0 .. $fields) {
2018                                            $row->{ $rest->{returns}->[$i] } = $e[ $i ];
2019                                    }
2020                                    push @results, $row;
2021                            }
2022    
2023                            return @results;
2024    
2025                    } elsif ($resbody) {
2026                            chomp $resbody;
2027                            return $resbody;
2028                    } else {
2029                            return 0E0;
2030                    }
2031            }
2032    
2033            carp "expected status $rest->{status}, but got $status";
2034            return undef;
2035    }
2036    
2037  =head1 PRIVATE METHODS  =head1 PRIVATE METHODS
2038    
# Line 1552  sub _set_info { Line 2061  sub _set_info {
2061    
2062          return if ($rv != 200 || !$resbody);          return if ($rv != 200 || !$resbody);
2063    
2064          # it seems that response can have multiple line endings          my @lines = split(/[\r\n]/,$resbody);
2065          $resbody =~ s/[\r\n]+$//;  
2066            $self->_clear_info;
2067    
2068          ( $self->{name}, $self->{label}, $self->{dnum}, $self->{wnum}, $self->{size} ) =          ( $self->{inform}->{name}, $self->{inform}->{label}, $self->{inform}->{dnum},
2069                  split(/\t/, $resbody, 5);                  $self->{inform}->{wnum}, $self->{inform}->{size} ) = split(/\t/, shift @lines, 5);
2070    
2071            return $resbody unless (@lines);
2072    
2073            shift @lines;
2074    
2075            while(my $admin = shift @lines) {
2076                    push @{$self->{inform}->{admins}}, $admin;
2077            }
2078    
2079            while(my $guest = shift @lines) {
2080                    push @{$self->{inform}->{guests}}, $guest;
2081            }
2082    
2083            while(my $link = shift @lines) {
2084                    push @{$self->{inform}->{links}}, $link;
2085            }
2086    
2087            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<name>, C<label>, C<doc_num>, C<word_num> or C<size> node
2098    info will be fetch again from Hyper Estraier.
2099    
2100    =cut
2101    sub _clear_info {
2102            my $self = shift;
2103            $self->{inform} = {
2104                    dnum => -1,
2105                    wnum => -1,
2106                    size => -1.0,
2107            };
2108  }  }
2109    
2110  ###  ###
# Line 1572  L<http://hyperestraier.sourceforge.net/> Line 2119  L<http://hyperestraier.sourceforge.net/>
2119    
2120  Hyper Estraier Ruby interface on which this module is based.  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<estmaster> P2P server.
2125    
2126  =head1 AUTHOR  =head1 AUTHOR
2127    
2128  Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>  Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
2129    
2130    Robert Klep E<lt>robert@klep.nameE<gt> contributed refactored search code
2131    
2132  =head1 COPYRIGHT AND LICENSE  =head1 COPYRIGHT AND LICENSE
2133    

Legend:
Removed from v.58  
changed lines
  Added in v.186

  ViewVC Help
Powered by ViewVC 1.1.26