/[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 184 by dpavlin, Sat Nov 4 13:10:29 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});
430          $draft .= "\t" . join("\n\t", @{ $self->{htexts} }) . "\n" if ($self->{htexts});          $draft .= "\t" . join("\n\t", @{ $self->{htexts} }) . "\n" if ($self->{htexts});
431    
432            printf("[%s]\n", $draft);
433    
434          return $draft;          return $draft;
435  }  }
436    
# Line 316  sub delete { Line 463  sub delete {
463    
464  package Search::Estraier::Condition;  package Search::Estraier::Condition;
465    
466  use Carp qw/confess croak/;  use Carp qw/carp confess croak/;
467    
468  use Search::Estraier;  use Search::Estraier;
469  our @ISA = qw/Search::Estraier/;  our @ISA = qw/Search::Estraier/;
# Line 394  sub set_max { Line 541  sub set_max {
541    
542  =head2 set_options  =head2 set_options
543    
544    $cond->set_options( SURE => 1 );    $cond->set_options( 'SURE' );
545    
546      $cond->set_options( qw/AGITO NOIDF SIMPLE/ );
547    
548    Possible options are:
549    
550    =over 8
551    
552    =item SURE
553    
554    check every N-gram
555    
556    =item USUAL
557    
558    check every second N-gram
559    
560    =item FAST
561    
562    check every third N-gram
563    
564    =item AGITO
565    
566    check every fourth N-gram
567    
568    =item NOIDF
569    
570    don't perform TF-IDF tuning
571    
572    =item SIMPLE
573    
574    use simplified query phrase
575    
576    =back
577    
578    Skipping N-grams will speed up search, but reduce accuracy. Every call to C<set_options> will reset previous
579    options;
580    
581    This option changed in version C<0.04> of this module. It's backwards compatibile.
582    
583  =cut  =cut
584    
585  my $options = {  my $options = {
         # check N-gram keys skipping by three  
586          SURE => 1 << 0,          SURE => 1 << 0,
         # check N-gram keys skipping by two  
587          USUAL => 1 << 1,          USUAL => 1 << 1,
         # without TF-IDF tuning  
588          FAST => 1 << 2,          FAST => 1 << 2,
         # with the simplified phrase  
589          AGITO => 1 << 3,          AGITO => 1 << 3,
         # check every N-gram key  
590          NOIDF => 1 << 4,          NOIDF => 1 << 4,
         # check N-gram keys skipping by one  
591          SIMPLE => 1 << 10,          SIMPLE => 1 << 10,
592  };  };
593    
594  sub set_options {  sub set_options {
595          my $self = shift;          my $self = shift;
596          my $option = shift;          my $opt = 0;
597          confess "unknown option" unless ($options->{$option});          foreach my $option (@_) {
598          $self->{options} ||= $options->{$option};                  my $mask;
599                    unless ($mask = $options->{$option}) {
600                            if ($option eq '1') {
601                                    next;
602                            } else {
603                                    croak "unknown option $option";
604                            }
605                    }
606                    $opt += $mask;
607            }
608            $self->{options} = $opt;
609  }  }
610    
611    
# Line 460  Return search result attrs. Line 648  Return search result attrs.
648  sub attrs {  sub attrs {
649          my $self = shift;          my $self = shift;
650          #croak "attrs return array, not scalar" if (! wantarray);          #croak "attrs return array, not scalar" if (! wantarray);
651          return @{ $self->{attrs} };          return @{ $self->{attrs} } if ($self->{attrs});
652  }  }
653    
654    
# Line 496  sub options { Line 684  sub options {
684  }  }
685    
686    
687    =head2 set_skip
688    
689    Set number of skipped documents from beginning of results
690    
691      $cond->set_skip(42);
692    
693    Similar to C<offset> in RDBMS.
694    
695    =cut
696    
697    sub set_skip {
698            my $self = shift;
699            $self->{skip} = shift;
700    }
701    
702    =head2 skip
703    
704    Return skip for this condition.
705    
706      print $cond->skip;
707    
708    =cut
709    
710    sub skip {
711            my $self = shift;
712            return $self->{skip};
713    }
714    
715    
716    =head2 set_distinct
717    
718      $cond->set_distinct('@author');
719    
720    =cut
721    
722    sub set_distinct {
723            my $self = shift;
724            $self->{distinct} = shift;
725    }
726    
727    =head2 distinct
728    
729    Return distinct attribute
730    
731      print $cond->distinct;
732    
733    =cut
734    
735    sub distinct {
736            my $self = shift;
737            return $self->{distinct};
738    }
739    
740    =head2 set_mask
741    
742    Filter out some links when searching.
743    
744    Argument array of link numbers, starting with 0 (current node).
745    
746      $cond->set_mask(qw/0 1 4/);
747    
748    =cut
749    
750    sub set_mask {
751            my $self = shift;
752            return unless (@_);
753            $self->{mask} = \@_;
754    }
755    
756    
757  package Search::Estraier::ResultDocument;  package Search::Estraier::ResultDocument;
758    
759  use Carp qw/croak/;  use Carp qw/croak/;
# Line 524  sub new { Line 782  sub new {
782          my $self = {@_};          my $self = {@_};
783          bless($self, $class);          bless($self, $class);
784    
785          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});  
         }  
786    
787          $self ? return $self : return undef;          $self ? return $self : return undef;
788  }  }
# Line 641  Return number of documents Line 897  Return number of documents
897    
898    print $res->doc_num;    print $res->doc_num;
899    
900    This will return real number of documents (limited by C<max>).
901    If you want to get total number of hits, see C<hits>.
902    
903  =cut  =cut
904    
905  sub doc_num {  sub doc_num {
# Line 672  sub get_doc { Line 931  sub get_doc {
931    
932  Return specific hint from results.  Return specific hint from results.
933    
934    print $rec->hint( 'VERSION' );    print $res->hint( 'VERSION' );
935    
936  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>,
937  C<TIME>, C<LINK#n>, C<VIEW>.  C<TIME>, C<LINK#n>, C<VIEW>.
# Line 685  sub hint { Line 944  sub hint {
944          return $self->{hints}->{$key};          return $self->{hints}->{$key};
945  }  }
946    
947    =head2 hints
948    
949    More perlish version of C<hint>. This one returns hash.
950    
951      my %hints = $res->hints;
952    
953    =cut
954    
955    sub hints {
956            my $self = shift;
957            return $self->{hints};
958    }
959    
960    =head2 hits
961    
962    Syntaxtic sugar for total number of hits for this query
963    
964      print $res->hits;
965    
966    It's same as
967    
968      print $res->hint('HIT');
969    
970    but shorter.
971    
972    =cut
973    
974    sub hits {
975            my $self = shift;
976            return $self->{hints}->{'HIT'} || 0;
977    }
978    
979  package Search::Estraier::Node;  package Search::Estraier::Node;
980    
# Line 700  use URI::Escape qw/uri_escape/; Line 990  use URI::Escape qw/uri_escape/;
990    
991    my $node = new Search::HyperEstraier::Node;    my $node = new Search::HyperEstraier::Node;
992    
993    or optionally with C<url> as parametar
994    
995      my $node = new Search::HyperEstraier::Node( 'http://localhost:1978/node/test' );
996    
997    or in more verbose form
998    
999      my $node = new Search::HyperEstraier::Node(
1000            url => 'http://localhost:1978/node/test',
1001            user => 'admin',
1002            passwd => 'admin'
1003            create => 1,
1004            label => 'optional node label',
1005            debug => 1,
1006            croak_on_error => 1
1007      );
1008    
1009    with following arguments:
1010    
1011    =over 4
1012    
1013    =item url
1014    
1015    URL to node
1016    
1017    =item user
1018    
1019    specify username for node server authentication
1020    
1021    =item passwd
1022    
1023    password for authentication
1024    
1025    =item create
1026    
1027    create node if it doesn't exists
1028    
1029    =item label
1030    
1031    optional label for new node if C<create> is used
1032    
1033    =item debug
1034    
1035    dumps a B<lot> of debugging output
1036    
1037    =item croak_on_error
1038    
1039    very helpful during development. It will croak on all errors instead of
1040    silently returning C<-1> (which is convention of Hyper Estraier API in other
1041    languages).
1042    
1043    =back
1044    
1045  =cut  =cut
1046    
1047  sub new {  sub new {
# Line 707  sub new { Line 1049  sub new {
1049          my $self = {          my $self = {
1050                  pxport => -1,                  pxport => -1,
1051                  timeout => 0,   # this used to be -1                  timeout => 0,   # this used to be -1
                 dnum => -1,  
                 wnum => -1,  
                 size => -1.0,  
1052                  wwidth => 480,                  wwidth => 480,
1053                  hwidth => 96,                  hwidth => 96,
1054                  awidth => 96,                  awidth => 96,
1055                  status => -1,                  status => -1,
1056          };          };
1057    
1058          bless($self, $class);          bless($self, $class);
1059    
1060          my $args = {@_};          if ($#_ == 0) {
1061                    $self->{url} = shift;
1062            } else {
1063                    %$self = ( %$self, @_ );
1064    
1065          $self->{debug} = $args->{debug};                  $self->set_auth( $self->{user}, $self->{passwd} ) if ($self->{user});
1066          warn "## Node debug on\n" if ($self->{debug});  
1067                    warn "## Node debug on\n" if ($self->{debug});
1068            }
1069    
1070            $self->{inform} = {
1071                    dnum => -1,
1072                    wnum => -1,
1073                    size => -1.0,
1074            };
1075    
1076            if ($self->{create}) {
1077                    if (! eval { $self->name } || $@) {
1078                            my $name = $1 if ($self->{url} =~ m#/node/([^/]+)/*#);
1079                            croak "can't find node name in '$self->{url}'" unless ($name);
1080                            my $label = $self->{label} || $name;
1081                            $self->master(
1082                                    action => 'nodeadd',
1083                                    name => $name,
1084                                    label => $label,
1085                            ) || croak "can't create node $name ($label)";
1086                    }
1087            }
1088    
1089          $self ? return $self : return undef;          $self ? return $self : return undef;
1090  }  }
# Line 812  Add a document Line 1176  Add a document
1176    
1177    $node->put_doc( $document_draft ) or die "can't add document";    $node->put_doc( $document_draft ) or die "can't add document";
1178    
1179  Return true on success or false on failture.  Return true on success or false on failure.
1180    
1181  =cut  =cut
1182    
# Line 820  sub put_doc { Line 1184  sub put_doc {
1184          my $self = shift;          my $self = shift;
1185          my $doc = shift || return;          my $doc = shift || return;
1186          return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));          return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));
1187          $self->shuttle_url( $self->{url} . '/put_doc',          if ($self->shuttle_url( $self->{url} . '/put_doc',
1188                  'text/x-estraier-draft',                  'text/x-estraier-draft',
1189                  $doc->dump_draft,                  $doc->dump_draft,
1190                  undef                  undef
1191          ) == 200;          ) == 200) {
1192                    $self->_clear_info;
1193                    return 1;
1194            }
1195            return undef;
1196  }  }
1197    
1198    
# Line 843  sub out_doc { Line 1211  sub out_doc {
1211          my $id = shift || return;          my $id = shift || return;
1212          return unless ($self->{url});          return unless ($self->{url});
1213          croak "id must be number, not '$id'" unless ($id =~ m/^\d+$/);          croak "id must be number, not '$id'" unless ($id =~ m/^\d+$/);
1214          $self->shuttle_url( $self->{url} . '/out_doc',          if ($self->shuttle_url( $self->{url} . '/out_doc',
1215                  'application/x-www-form-urlencoded',                  'application/x-www-form-urlencoded',
1216                  "id=$id",                  "id=$id",
1217                  undef                  undef
1218          ) == 200;          ) == 200) {
1219                    $self->_clear_info;
1220                    return 1;
1221            }
1222            return undef;
1223  }  }
1224    
1225    
# Line 865  sub out_doc_by_uri { Line 1237  sub out_doc_by_uri {
1237          my $self = shift;          my $self = shift;
1238          my $uri = shift || return;          my $uri = shift || return;
1239          return unless ($self->{url});          return unless ($self->{url});
1240          $self->shuttle_url( $self->{url} . '/out_doc',          if ($self->shuttle_url( $self->{url} . '/out_doc',
1241                  'application/x-www-form-urlencoded',                  'application/x-www-form-urlencoded',
1242                  "uri=" . uri_escape($uri),                  "uri=" . uri_escape($uri),
1243                  undef                  undef
1244          ) == 200;          ) == 200) {
1245                    $self->_clear_info;
1246                    return 1;
1247            }
1248            return undef;
1249  }  }
1250    
1251    
# Line 887  sub edit_doc { Line 1263  sub edit_doc {
1263          my $self = shift;          my $self = shift;
1264          my $doc = shift || return;          my $doc = shift || return;
1265          return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));          return unless ($self->{url} && $doc->isa('Search::Estraier::Document'));
1266          $self->shuttle_url( $self->{url} . '/edit_doc',          if ($self->shuttle_url( $self->{url} . '/edit_doc',
1267                  'text/x-estraier-draft',                  'text/x-estraier-draft',
1268                  $doc->dump_draft,                  $doc->dump_draft,
1269                  undef                  undef
1270          ) == 200;          ) == 200) {
1271                    $self->_clear_info;
1272                    return 1;
1273            }
1274            return undef;
1275  }  }
1276    
1277    
# Line 1000  Get ID of document specified by URI Line 1380  Get ID of document specified by URI
1380    
1381    my $id = $node->uri_to_id( 'file:///document/uri/42' );    my $id = $node->uri_to_id( 'file:///document/uri/42' );
1382    
1383    This method won't croak, even if using C<croak_on_error>.
1384    
1385  =cut  =cut
1386    
1387  sub uri_to_id {  sub uri_to_id {
1388          my $self = shift;          my $self = shift;
1389          my $uri = shift || return;          my $uri = shift || return;
1390          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 );
1391  }  }
1392    
1393    
# Line 1047  sub _fetch_doc { Line 1429  sub _fetch_doc {
1429          $path = '/etch_doc' if ($a->{etch});          $path = '/etch_doc' if ($a->{etch});
1430    
1431          if ($a->{id}) {          if ($a->{id}) {
1432                  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+$/);
1433                  $arg = 'id=' . $a->{id};                  $arg = 'id=' . $a->{id};
1434          } elsif ($a->{uri}) {          } elsif ($a->{uri}) {
1435                  $arg = 'uri=' . uri_escape($a->{uri});                  $arg = 'uri=' . uri_escape($a->{uri});
# Line 1065  sub _fetch_doc { Line 1447  sub _fetch_doc {
1447                  'application/x-www-form-urlencoded',                  'application/x-www-form-urlencoded',
1448                  $arg,                  $arg,
1449                  \$resbody,                  \$resbody,
1450                    $a->{croak_on_error},
1451          );          );
1452    
1453          return if ($rv != 200);          return if ($rv != 200);
# Line 1095  sub _fetch_doc { Line 1478  sub _fetch_doc {
1478    
1479  sub name {  sub name {
1480          my $self = shift;          my $self = shift;
1481          $self->_set_info unless ($self->{name});          $self->_set_info unless ($self->{inform}->{name});
1482          return $self->{name};          return $self->{inform}->{name};
1483  }  }
1484    
1485    
# Line 1108  sub name { Line 1491  sub name {
1491    
1492  sub label {  sub label {
1493          my $self = shift;          my $self = shift;
1494          $self->_set_info unless ($self->{label});          $self->_set_info unless ($self->{inform}->{label});
1495          return $self->{label};          return $self->{inform}->{label};
1496  }  }
1497    
1498    
# Line 1121  sub label { Line 1504  sub label {
1504    
1505  sub doc_num {  sub doc_num {
1506          my $self = shift;          my $self = shift;
1507          $self->_set_info if ($self->{dnum} < 0);          $self->_set_info if ($self->{inform}->{dnum} < 0);
1508          return $self->{dnum};          return $self->{inform}->{dnum};
1509  }  }
1510    
1511    
# Line 1134  sub doc_num { Line 1517  sub doc_num {
1517    
1518  sub word_num {  sub word_num {
1519          my $self = shift;          my $self = shift;
1520          $self->_set_info if ($self->{wnum} < 0);          $self->_set_info if ($self->{inform}->{wnum} < 0);
1521          return $self->{wnum};          return $self->{inform}->{wnum};
1522  }  }
1523    
1524    
# Line 1147  sub word_num { Line 1530  sub word_num {
1530    
1531  sub size {  sub size {
1532          my $self = shift;          my $self = shift;
1533          $self->_set_info if ($self->{size} < 0);          $self->_set_info if ($self->{inform}->{size} < 0);
1534          return $self->{size};          return $self->{inform}->{size};
1535  }  }
1536    
1537    
# Line 1176  sub search { Line 1559  sub search {
1559    
1560          my $rv = $self->shuttle_url( $self->{url} . '/search',          my $rv = $self->shuttle_url( $self->{url} . '/search',
1561                  'application/x-www-form-urlencoded',                  'application/x-www-form-urlencoded',
1562                  $self->cond_to_query( $cond ),                  $self->cond_to_query( $cond, $depth ),
1563                  \$resbody,                  \$resbody,
1564          );          );
1565          return if ($rv != 200);          return if ($rv != 200);
1566    
1567          my (@docs, $hints);          my @records     = split /--------\[.*?\]--------(?::END)?\r?\n/, $resbody;
1568            my $hintsText   = splice @records, 0, 2; # starts with empty record
1569          my @lines = split(/\n/, $resbody);          my $hints               = { $hintsText =~ m/^(.*?)\t(.*?)$/gsm };
1570          return unless (@lines);  
1571            # process records
1572          my $border = $lines[0];          my $docs = [];
1573          my $isend = 0;          foreach my $record (@records)
1574          my $lnum = 1;          {
1575                    # split into keys and snippets
1576          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$/);  
                 }  
   
         }  
1577    
1578          if (! $isend) {                  # create document hash
1579                  warn "received result doesn't have :END\n$resbody";                  my $doc                         = { $keys =~ m/^(.*?)=(.*?)$/gsm };
1580                  return;                  $doc->{'@keywords'}     = $doc->{keywords};
1581                    ($doc->{keywords})      = $keys =~ m/^%VECTOR\t(.*?)$/gm;
1582                    $doc->{snippet}         = $snippet;
1583    
1584                    push @$docs, new Search::Estraier::ResultDocument(
1585                            attrs           => $doc,
1586                            uri             => $doc->{'@uri'},
1587                            snippet         => $snippet,
1588                            keywords        => $doc->{'keywords'},
1589                    );
1590          }          }
1591    
1592          #warn Dumper(\@docs, $hints);          return new Search::Estraier::NodeResult( docs => $docs, hints => $hints );
   
         return new Search::Estraier::NodeResult( docs => \@docs, hints => $hints );  
1593  }  }
1594    
1595    
# Line 1270  sub search { Line 1597  sub search {
1597    
1598  Return URI encoded string generated from Search::Estraier::Condition  Return URI encoded string generated from Search::Estraier::Condition
1599    
1600    my $args = $node->cond_to_query( $cond );    my $args = $node->cond_to_query( $cond, $depth );
1601    
1602  =cut  =cut
1603    
# Line 1279  sub cond_to_query { Line 1606  sub cond_to_query {
1606    
1607          my $cond = shift || return;          my $cond = shift || return;
1608          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'));
1609            my $depth = shift;
1610    
1611          my @args;          my @args;
1612    
# Line 1288  sub cond_to_query { Line 1616  sub cond_to_query {
1616    
1617          if (my @attrs = $cond->attrs) {          if (my @attrs = $cond->attrs) {
1618                  for my $i ( 0 .. $#attrs ) {                  for my $i ( 0 .. $#attrs ) {
1619                          push @args,'attr' . ($i+1) . '=' . uri_escape( $attrs[$i] );                          push @args,'attr' . ($i+1) . '=' . uri_escape( $attrs[$i] ) if ($attrs[$i]);
1620                  }                  }
1621          }          }
1622    
# Line 1306  sub cond_to_query { Line 1634  sub cond_to_query {
1634                  push @args, 'options=' . $options;                  push @args, 'options=' . $options;
1635          }          }
1636    
1637          push @args, 'depth=' . $self->{depth} if ($self->{depth});          push @args, 'depth=' . $depth if ($depth);
1638          push @args, 'wwidth=' . $self->{wwidth};          push @args, 'wwidth=' . $self->{wwidth};
1639          push @args, 'hwidth=' . $self->{hwidth};          push @args, 'hwidth=' . $self->{hwidth};
1640          push @args, 'awidth=' . $self->{awidth};          push @args, 'awidth=' . $self->{awidth};
1641            push @args, 'skip=' . $cond->{skip} if ($cond->{skip});
1642    
1643            if (my $distinct = $cond->distinct) {
1644                    push @args, 'distinct=' . uri_escape($distinct);
1645            }
1646    
1647            if ($cond->{mask}) {
1648                    my $mask = 0;
1649                    map { $mask += ( 2 ** $_ ) } @{ $cond->{mask} };
1650    
1651                    push @args, 'mask=' . $mask if ($mask);
1652            }
1653    
1654          return join('&', @args);          return join('&', @args);
1655  }  }
# Line 1317  sub cond_to_query { Line 1657  sub cond_to_query {
1657    
1658  =head2 shuttle_url  =head2 shuttle_url
1659    
1660  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
1661  master.  master.
1662    
1663    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 1667  body will be saved within object.
1667    
1668  =cut  =cut
1669    
1670    use LWP::UserAgent;
1671    
1672  sub shuttle_url {  sub shuttle_url {
1673          my $self = shift;          my $self = shift;
1674    
1675          my ($url, $content_type, $reqbody, $resbody) = @_;          my ($url, $content_type, $reqbody, $resbody, $croak_on_error) = @_;
1676    
1677            $croak_on_error = $self->{croak_on_error} unless defined($croak_on_error);
1678    
1679          $self->{status} = -1;          $self->{status} = -1;
1680    
# Line 1345  sub shuttle_url { Line 1689  sub shuttle_url {
1689                  return -1;                  return -1;
1690          }          }
1691    
1692          my ($host,$port,$query) = ($url->host, $url->port, $url->path);          my $ua = LWP::UserAgent->new;
1693            $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;  
1694    
1695            my $req;
1696          if ($reqbody) {          if ($reqbody) {
1697                  $headers .= "POST $query HTTP/1.0\r\n";                  $req = HTTP::Request->new(POST => $url);
1698          } else {          } else {
1699                  $headers .= "GET $query HTTP/1.0\r\n";                  $req = HTTP::Request->new(GET => $url);
1700          }          }
1701    
1702          $headers .= "Host: " . $url->host . ":" . $url->port . "\r\n";          $req->headers->header( 'Host' => $url->host . ":" . $url->port );
1703          $headers .= "Connection: close\r\n";          $req->headers->header( 'Connection', 'close' );
1704          $headers .= "User-Agent: Search-Estraier/$Search::Estraier::VERSION\r\n";          $req->headers->header( 'Authorization', 'Basic ' . $self->{auth} ) if ($self->{auth});
1705          $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";  
1706    
1707          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,  
         );  
1708    
1709          if (! $sock) {          if ($reqbody) {
1710                  carp "can't open socket to $host:$port";                  warn "$reqbody\n" if ($self->{debug});
1711                  return -1;                  $req->content( $reqbody );
1712          }          }
1713    
1714          warn $headers if ($self->{debug});          my $res = $ua->request($req) || croak "can't make request to $url: $!";
1715    
1716          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;  
1717    
1718          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;  
         }  
1719    
1720          my $line = <$sock>;          if (! $res->is_success) {
1721          chomp($line);                  if ($croak_on_error) {
1722          my ($schema, $res_status, undef) = split(/  */, $line, 3);                          croak("can't get $url: ",$res->status_line);
1723          return if ($schema !~ /^HTTP/ || ! $res_status);                  } else {
1724                            return -1;
1725          $self->{status} = $res_status;                  }
1726          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});  
         };  
1727    
1728          # read body          $$resbody .= $res->content;
         $len = 0;  
         do {  
                 $len = read($sock, my $buf, 8192);  
                 $$resbody .= $buf if ($resbody);  
         } while ($len);  
1729    
1730          warn "## response body:\n$$resbody\n" if ($resbody && $self->{debug});          warn "## response body:\n$$resbody\n" if ($resbody && $self->{debug});
1731    
# Line 1490  sub set_user { Line 1796  sub set_user {
1796          croak "mode must be number, not '$mode'" unless ($mode =~ m/^\d+$/);          croak "mode must be number, not '$mode'" unless ($mode =~ m/^\d+$/);
1797    
1798          $self->shuttle_url( $self->{url} . '/_set_user',          $self->shuttle_url( $self->{url} . '/_set_user',
1799                  'text/plain',                  'application/x-www-form-urlencoded',
1800                  'name=' . uri_escape($name) . '&mode=' . $mode,                  'name=' . uri_escape($name) . '&mode=' . $mode,
1801                  undef                  undef
1802          ) == 200;          ) == 200;
# Line 1517  sub set_link { Line 1823  sub set_link {
1823          my $reqbody = 'url=' . uri_escape($url) . '&label=' . uri_escape($label);          my $reqbody = 'url=' . uri_escape($url) . '&label=' . uri_escape($label);
1824          $reqbody .= '&credit=' . $credit if ($credit > 0);          $reqbody .= '&credit=' . $credit if ($credit > 0);
1825    
1826          $self->shuttle_url( $self->{url} . '/_set_link',          if ($self->shuttle_url( $self->{url} . '/_set_link',
1827                  'text/plain',                  'application/x-www-form-urlencoded',
1828                  $reqbody,                  $reqbody,
1829                  undef                  undef
1830          ) == 200;          ) == 200) {
1831                    # refresh node info after adding link
1832                    $self->_clear_info;
1833                    return 1;
1834            }
1835            return undef;
1836    }
1837    
1838    =head2 admins
1839    
1840     my @admins = @{ $node->admins };
1841    
1842    Return array of users with admin rights on node
1843    
1844    =cut
1845    
1846    sub admins {
1847            my $self = shift;
1848            $self->_set_info unless ($self->{inform}->{name});
1849            return $self->{inform}->{admins};
1850    }
1851    
1852    =head2 guests
1853    
1854     my @guests = @{ $node->guests };
1855    
1856    Return array of users with guest rights on node
1857    
1858    =cut
1859    
1860    sub guests {
1861            my $self = shift;
1862            $self->_set_info unless ($self->{inform}->{name});
1863            return $self->{inform}->{guests};
1864    }
1865    
1866    =head2 links
1867    
1868     my $links = @{ $node->links };
1869    
1870    Return array of links for this node
1871    
1872    =cut
1873    
1874    sub links {
1875            my $self = shift;
1876            $self->_set_info unless ($self->{inform}->{name});
1877            return $self->{inform}->{links};
1878    }
1879    
1880    =head2 cacheusage
1881    
1882    Return cache usage for a node
1883    
1884      my $cache = $node->cacheusage;
1885    
1886    =cut
1887    
1888    sub cacheusage {
1889            my $self = shift;
1890    
1891            return unless ($self->{url});
1892    
1893            my $resbody;
1894            my $rv = $self->shuttle_url( $self->{url} . '/cacheusage',
1895                    'text/plain',
1896                    undef,
1897                    \$resbody,
1898            );
1899    
1900            return if ($rv != 200 || !$resbody);
1901    
1902            return $resbody;
1903  }  }
1904    
1905    =head2 master
1906    
1907    Set actions on Hyper Estraier node master (C<estmaster> process)
1908    
1909      $node->master(
1910            action => 'sync'
1911      );
1912    
1913    All available actions are documented in
1914    L<http://hyperestraier.sourceforge.net/nguide-en.html#protocol>
1915    
1916    =cut
1917    
1918    my $estmaster_rest = {
1919            shutdown => {
1920                    status => 202,
1921            },
1922            sync => {
1923                    status => 202,
1924            },
1925            backup => {
1926                    status => 202,
1927            },
1928            userlist => {
1929                    status => 200,
1930                    returns => [ qw/name passwd flags fname misc/ ],
1931            },
1932            useradd => {
1933                    required => [ qw/name passwd flags/ ],
1934                    optional => [ qw/fname misc/ ],
1935                    status => 200,
1936            },
1937            userdel => {
1938                    required => [ qw/name/ ],
1939                    status => 200,
1940            },
1941            nodelist => {
1942                    status => 200,
1943                    returns => [ qw/name label doc_num word_num size/ ],
1944            },
1945            nodeadd => {
1946                    required => [ qw/name/ ],
1947                    optional => [ qw/label/ ],
1948                    status => 200,
1949            },
1950            nodedel => {
1951                    required => [ qw/name/ ],
1952                    status => 200,
1953            },
1954            nodeclr => {
1955                    required => [ qw/name/ ],
1956                    status => 200,
1957            },
1958            nodertt => {
1959                    status => 200,  
1960            },
1961    };
1962    
1963    sub master {
1964            my $self = shift;
1965    
1966            my $args = {@_};
1967    
1968            # have action?
1969            my $action = $args->{action} || croak "need action, available: ",
1970                    join(", ",keys %{ $estmaster_rest });
1971    
1972            # check if action is valid
1973            my $rest = $estmaster_rest->{$action};
1974            croak "action '$action' is not supported, available actions: ",
1975                    join(", ",keys %{ $estmaster_rest }) unless ($rest);
1976    
1977            croak "BUG: action '$action' needs return status" unless ($rest->{status});
1978    
1979            my @args;
1980    
1981            if ($rest->{required} || $rest->{optional}) {
1982    
1983                    map {
1984                            croak "need parametar '$_' for action '$action'" unless ($args->{$_});
1985                            push @args, $_ . '=' . uri_escape( $args->{$_} );
1986                    } ( @{ $rest->{required} } );
1987    
1988                    map {
1989                            push @args, $_ . '=' . uri_escape( $args->{$_} ) if ($args->{$_});
1990                    } ( @{ $rest->{optional} } );
1991    
1992            }
1993    
1994            my $uri = new URI( $self->{url} );
1995    
1996            my $resbody;
1997    
1998            my $status = $self->shuttle_url(
1999                    'http://' . $uri->host_port . '/master?action=' . $action ,
2000                    'application/x-www-form-urlencoded',
2001                    join('&', @args),
2002                    \$resbody,
2003                    1,
2004            ) or confess "shuttle_url failed";
2005    
2006            if ($status == $rest->{status}) {
2007    
2008                    # refresh node info after sync
2009                    $self->_clear_info if ($action eq 'sync' || $action =~ m/^node(?:add|del|clr)$/);
2010    
2011                    if ($rest->{returns} && wantarray) {
2012    
2013                            my @results;
2014                            my $fields = $#{$rest->{returns}};
2015    
2016                            foreach my $line ( split(/[\r\n]/,$resbody) ) {
2017                                    my @e = split(/\t/, $line, $fields + 1);
2018                                    my $row;
2019                                    foreach my $i ( 0 .. $fields) {
2020                                            $row->{ $rest->{returns}->[$i] } = $e[ $i ];
2021                                    }
2022                                    push @results, $row;
2023                            }
2024    
2025                            return @results;
2026    
2027                    } elsif ($resbody) {
2028                            chomp $resbody;
2029                            return $resbody;
2030                    } else {
2031                            return 0E0;
2032                    }
2033            }
2034    
2035            carp "expected status $rest->{status}, but got $status";
2036            return undef;
2037    }
2038    
2039  =head1 PRIVATE METHODS  =head1 PRIVATE METHODS
2040    
# Line 1552  sub _set_info { Line 2063  sub _set_info {
2063    
2064          return if ($rv != 200 || !$resbody);          return if ($rv != 200 || !$resbody);
2065    
2066          # it seems that response can have multiple line endings          my @lines = split(/[\r\n]/,$resbody);
2067          $resbody =~ s/[\r\n]+$//;  
2068            $self->_clear_info;
2069    
2070          ( $self->{name}, $self->{label}, $self->{dnum}, $self->{wnum}, $self->{size} ) =          ( $self->{inform}->{name}, $self->{inform}->{label}, $self->{inform}->{dnum},
2071                  split(/\t/, $resbody, 5);                  $self->{inform}->{wnum}, $self->{inform}->{size} ) = split(/\t/, shift @lines, 5);
2072    
2073            return $resbody unless (@lines);
2074    
2075            shift @lines;
2076    
2077            while(my $admin = shift @lines) {
2078                    push @{$self->{inform}->{admins}}, $admin;
2079            }
2080    
2081            while(my $guest = shift @lines) {
2082                    push @{$self->{inform}->{guests}}, $guest;
2083            }
2084    
2085            while(my $link = shift @lines) {
2086                    push @{$self->{inform}->{links}}, $link;
2087            }
2088    
2089            return $resbody;
2090    
2091    }
2092    
2093    =head2 _clear_info
2094    
2095    Clear information for node
2096    
2097      $node->_clear_info;
2098    
2099    On next call to C<name>, C<label>, C<doc_num>, C<word_num> or C<size> node
2100    info will be fetch again from Hyper Estraier.
2101    
2102    =cut
2103    sub _clear_info {
2104            my $self = shift;
2105            $self->{inform} = {
2106                    dnum => -1,
2107                    wnum => -1,
2108                    size => -1.0,
2109            };
2110  }  }
2111    
2112  ###  ###
# Line 1572  L<http://hyperestraier.sourceforge.net/> Line 2121  L<http://hyperestraier.sourceforge.net/>
2121    
2122  Hyper Estraier Ruby interface on which this module is based.  Hyper Estraier Ruby interface on which this module is based.
2123    
2124    Hyper Estraier now also has pure-perl binding included in distribution. It's
2125    a faster way to access databases directly if you are not running
2126    C<estmaster> P2P server.
2127    
2128  =head1 AUTHOR  =head1 AUTHOR
2129    
2130  Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>  Dobrica Pavlinusic, E<lt>dpavlin@rot13.orgE<gt>
2131    
2132    Robert Klep E<lt>robert@klep.nameE<gt> contributed refactored search code
2133    
2134  =head1 COPYRIGHT AND LICENSE  =head1 COPYRIGHT AND LICENSE
2135    

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

  ViewVC Help
Powered by ViewVC 1.1.26