Perl filters now can optionally reach data via a virtual filehandle.
[idzebra-moved-to-github.git] / perl / lib / IDZebra / Session.pm
1 #!/usr/bin/perl
2 # ============================================================================
3 # Zebra perl API header
4 # =============================================================================
5 use strict;
6 # ============================================================================
7 package IDZebra::Session;
8 use IDZebra;
9 use IDZebra::Logger qw(:flags :calls);
10 #use IDZebra::Repository;
11 use IDZebra::Resultset;
12 use Scalar::Util;
13 use Carp;
14 use strict;
15 our @ISA = qw(IDZebra::Logger);
16
17 1;
18 # -----------------------------------------------------------------------------
19 # Class constructors, destructor
20 # -----------------------------------------------------------------------------
21 sub new {
22     my ($proto, %args) = @_;
23     my $class = ref($proto) || $proto;
24     my $self = {};
25     $self->{args} = \%args;
26     
27     bless ($self, $class);
28     $self->{cql_ct} = undef;
29     return ($self);
30
31     $self->{databases} = {};
32 }
33
34 sub start_service {
35     my ($self, %args) = @_;
36
37     my $zs;
38     unless (defined($self->{zs})) {
39         if (defined($args{'configFile'})) {
40             $self->{zs} = IDZebra::start($args{'configFile'});
41         } else {
42             $self->{zs} = IDZebra::start("zebra.cfg");
43         }
44     }
45 }
46
47 sub stop_service {
48     my ($self) = @_;
49     if (defined($self->{zs})) {
50         IDZebra::stop($self->{zs}) if ($self->{zs});    
51         $self->{zs} = undef;
52     }
53 }
54
55
56 sub open {
57     my ($proto,%args) = @_;
58     my $self = {};
59
60     if (ref($proto)) { $self = $proto; } else { 
61         $self = $proto->new(%args);
62     }
63
64     unless (%args) {
65         %args = %{$self->{args}};
66     }
67
68     $self->start_service(%args);
69
70     unless (defined($self->{zs})) {
71         croak ("Falied to open zebra service");
72     }    
73
74     unless (defined($self->{zh})) {
75         $self->{zh}=IDZebra::open($self->{zs}); 
76     }   
77
78     # Reset result set counter
79     $self->{rscount} = 0;
80
81     # This is needed in order to somehow initialize the service
82     $self->select_databases("Default");
83
84     # Load the default configuration
85     $self->group(%args);
86     
87     $self->{odr_input} = IDZebra::odr_createmem($IDZebra::ODR_DECODE);
88     $self->{odr_output} = IDZebra::odr_createmem($IDZebra::ODR_ENCODE);
89
90     return ($self);
91 }
92
93 sub close {
94     my ($self) = @_;
95
96     if ($self->{zh}) {
97         while (IDZebra::trans_no($self->{zh}) > 0) {
98             logf (LOG_WARN,"Explicitly closing transaction with session");
99             $self->end_trans;
100         }
101
102         IDZebra::close($self->{zh});
103         $self->{zh} = undef;
104     }
105     
106     if ($self->{odr_input}) {
107         IDZebra::odr_reset($self->{odr_input});
108         IDZebra::odr_destroy($self->{odr_input});
109         $self->{odr_input} = undef;  
110     }
111
112     if ($self->{odr_output}) {
113         IDZebra::odr_reset($self->{odr_output});
114         IDZebra::odr_destroy($self->{odr_output});
115         $self->{odr_output} = undef;  
116     }
117
118     $self->stop_service;
119 }
120
121 sub DESTROY {
122     my ($self) = @_;
123     logf (LOG_LOG,"DESTROY $self");
124     $self->close; 
125
126     if (defined ($self->{cql_ct})) {
127       IDZebra::cql_transform_close($self->{cql_ct});
128     }
129 }
130 # -----------------------------------------------------------------------------
131 # Record group selection  This is a bit nasty... but used at many places 
132 # -----------------------------------------------------------------------------
133 sub group {
134     my ($self,%args) = @_;
135     if ($#_ > 0) {
136         $self->{rg} = $self->_makeRecordGroup(%args);
137         $self->_selectRecordGroup($self->{rg});
138     }
139     return($self->{rg});
140 }
141
142 sub selectRecordGroup {
143     my ($self, $groupName) = @_;
144     $self->{rg} = $self->_getRecordGroup($groupName);
145     $self->_selectRecordGroup($self->{rg});
146 }
147
148 sub _displayRecordGroup {
149     my ($self, $rg) = @_;
150     print STDERR "-----\n";
151     foreach my $key qw (groupName 
152                         databaseName 
153                         path recordId 
154                         recordType 
155                         flagStoreData 
156                         flagStoreKeys 
157                         flagRw 
158                         fileVerboseLimit 
159                         databaseNamePath 
160                         explainDatabase 
161                         followLinks) {
162         print STDERR "$key:",$rg->{$key},"\n";
163     }
164 }
165
166 sub _cloneRecordGroup {
167     my ($self, $orig) = @_;
168     my $rg = IDZebra::recordGroup->new();
169     my $r = IDZebra::init_recordGroup($rg);
170     foreach my $key qw (groupName 
171                         databaseName 
172                         path 
173                         recordId 
174                         recordType 
175                         flagStoreData 
176                         flagStoreKeys 
177                         flagRw 
178                         fileVerboseLimit 
179                         databaseNamePath 
180                         explainDatabase 
181                         followLinks) {
182         $rg->{$key} = $orig->{$key} if ($orig->{$key});
183     }
184     return ($rg);
185 }
186
187 sub _getRecordGroup {
188     my ($self, $groupName, $ext) = @_;
189     my $rg = IDZebra::recordGroup->new();
190     my $r = IDZebra::init_recordGroup($rg);
191     $rg->{groupName} = $groupName if ($groupName ne "");  
192     $ext = "" unless ($ext);
193     my $r = IDZebra::res_get_recordGroup($self->{zh}, $rg, $ext);
194     return ($rg);
195 }
196
197 sub _makeRecordGroup {
198     my ($self, %args) = @_;
199     my $rg;
200
201     my @keys = keys(%args);
202     unless ($#keys >= 0) {
203         return ($self->{rg});
204     }
205
206     if ($args{groupName}) {
207         $rg = $self->_getRecordGroup($args{groupName});
208     } else {
209         $rg = $self->_cloneRecordGroup($self->{rg});
210     }
211     $self->_setRecordGroupOptions($rg, %args);
212     return ($rg);
213 }
214
215 sub _setRecordGroupOptions {
216     my ($self, $rg, %args) = @_;
217
218     foreach my $key qw (databaseName 
219                         path 
220                         recordId 
221                         recordType 
222                         flagStoreData 
223                         flagStoreKeys 
224                         flagRw 
225                         fileVerboseLimit 
226                         databaseNamePath 
227                         explainDatabase 
228                         followLinks) {
229         if (defined ($args{$key})) {
230             $rg->{$key} = $args{$key};
231         }
232     }
233 }
234 sub _selectRecordGroup {
235     my ($self, $rg) = @_;
236     my $r = IDZebra::set_group($self->{zh}, $rg);
237     my $dbName;
238     unless ($dbName = $rg->{databaseName}) {
239         $dbName = 'Default';
240     }
241     if ($self->select_databases($dbName)) {
242         croak("Fatal error selecting database $dbName");
243     }
244 }
245 # -----------------------------------------------------------------------------
246 # Selecting databases for search (and also for updating - internally)
247 # -----------------------------------------------------------------------------
248 sub select_databases {
249     my ($self, @databases) = @_;
250
251     my $changed = 0;
252     foreach my $db (@databases) {
253         next if ($self->{databases}{$db});
254         $changed++;
255     }
256
257     if ($changed) {
258
259         delete ($self->{databases});
260         foreach my $db (@databases) {
261             $self->{databases}{$db}++;
262         }
263
264         if (my $res = IDZebra::select_databases($self->{zh}, 
265                                                 ($#databases + 1), 
266                                                 \@databases)) {
267             logf(LOG_FATAL, 
268                  "Could not select database(s) %s errCode=%d",
269                  join(",",@databases),
270                  $self->errCode());
271             return ($res);
272         } else {
273             logf(LOG_LOG,"Database(s) selected: %s",join(",",@databases));
274         }
275     }
276     return (0);
277 }
278
279 # -----------------------------------------------------------------------------
280 # Error handling
281 # -----------------------------------------------------------------------------
282 sub errCode {
283     my ($self) = @_;
284     return(IDZebra::errCode($self->{zh}));
285 }
286
287 sub errString {
288     my ($self) = @_;
289     return(IDZebra::errString($self->{zh}));
290 }
291
292 sub errAdd {
293     my ($self) = @_;
294     return(IDZebra::errAdd($self->{zh}));
295 }
296
297 # -----------------------------------------------------------------------------
298 # Transaction stuff
299 # -----------------------------------------------------------------------------
300 sub begin_trans {
301     my ($self) = @_;
302     IDZebra::begin_trans($self->{zh});
303 }
304
305 sub end_trans {
306     my ($self) = @_;
307     my $stat = IDZebra::ZebraTransactionStatus->new();
308     IDZebra::end_trans($self->{zh}, $stat);
309     return ($stat);
310 }
311
312 sub begin_read {
313     my ($self) =@_;
314     return(IDZebra::begin_read($self->{zh}));
315 }
316
317 sub end_read {
318     my ($self) =@_;
319     IDZebra::end_read($self->{zh});
320 }
321
322 sub shadow_enable {
323     my ($self, $value) = @_;
324     if ($#_ > 0) { IDZebra::set_shadow_enable($self->{zh},$value); }
325     return (IDZebra::get_shadow_enable($self->{zh}));
326 }
327
328 sub commit {
329     my ($self) = @_;
330     if ($self->shadow_enable) {
331         return(IDZebra::commit($self->{zh}));
332     }
333 }
334
335 # -----------------------------------------------------------------------------
336 # We don't really need that...
337 # -----------------------------------------------------------------------------
338 sub odr_reset {
339     my ($self, $name) = @_;
340     if ($name !~/^(input|output)$/) {
341         croak("Undefined ODR '$name'");
342     }
343   IDZebra::odr_reset($self->{"odr_$name"});
344 }
345
346 # -----------------------------------------------------------------------------
347 # Init/compact
348 # -----------------------------------------------------------------------------
349 sub init {
350     my ($self) = @_;
351     return(IDZebra::init($self->{zh}));
352 }
353
354 sub compact {
355     my ($self) = @_;
356     return(IDZebra::compact($self->{zh}));
357 }
358
359 sub update {
360     my ($self, %args) = @_;
361     my $rg = $self->_update_args(%args);
362     $self->_selectRecordGroup($rg);
363     $self->begin_trans;
364     IDZebra::repository_update($self->{zh});
365     $self->_selectRecordGroup($self->{rg});
366     $self->end_trans;
367 }
368
369 sub delete {
370     my ($self, %args) = @_;
371     my $rg = $self->_update_args(%args);
372     $self->_selectRecordGroup($rg);
373     $self->begin_trans;
374     IDZebra::repository_delete($self->{zh});
375     $self->_selectRecordGroup($self->{rg});
376     $self->end_trans;
377 }
378
379 sub show {
380     my ($self, %args) = @_;
381     my $rg = $self->_update_args(%args);
382     $self->_selectRecordGroup($rg);
383     $self->begin_trans;
384     IDZebra::repository_show($self->{zh});
385     $self->_selectRecordGroup($self->{rg});
386     $self->end_trans;
387 }
388
389 sub _update_args {
390     my ($self, %args) = @_;
391     my $rg = $self->_makeRecordGroup(%args);
392     $self->_selectRecordGroup($rg);
393     return ($rg);
394 }
395
396 # -----------------------------------------------------------------------------
397 # Per record update
398 # -----------------------------------------------------------------------------
399
400 sub update_record {
401     my ($self, %args) = @_;
402     return(IDZebra::update_record($self->{zh},
403                                   $self->_record_update_args(%args)));
404 }
405
406 sub delete_record {
407     my ($self, %args) = @_;
408     return(IDZebra::delete_record($self->{zh},
409                                   $self->_record_update_args(%args)));
410 }
411 sub _record_update_args {
412     my ($self, %args) = @_;
413
414     my $sysno   = $args{sysno}      ? $args{sysno}      : 0;
415     my $match   = $args{match}      ? $args{match}      : "";
416     my $rectype = $args{recordType} ? $args{recordType} : "";
417     my $fname   = $args{file}       ? $args{file}       : "<no file>";
418
419     my $buff;
420
421     if ($args{data}) {
422         $buff = $args{data};
423     } 
424     elsif ($args{file}) {
425         open (F, $args{file}) || warn ("Cannot open $args{file}");
426         $buff = join('',(<F>));
427         close (F);
428     }
429     my $len = length($buff);
430
431     delete ($args{sysno});
432     delete ($args{match});
433     delete ($args{recordType});
434     delete ($args{file});
435     delete ($args{data});
436
437     my $rg = $self->_makeRecordGroup(%args);
438
439     # If no record type is given, then try to find it out from the
440     # file extension;
441     unless ($rectype) {
442         if (my ($ext) = $fname =~ /\.(\w+)$/) {
443             my $rg2 = $self->_getRecordGroup($rg->{groupName},$ext);
444             $rectype = $rg2->{recordType};
445         } 
446     }
447
448     $rg->{databaseName} = "Default" unless ($rg->{databaseName});
449
450 #    print STDERR "$rectype,$sysno,$match,$fname,$len\n";
451     unless ($rectype) {
452         $rectype="";
453     }
454     return ($rg, $rectype, $sysno, $match, $fname, $buff, $len);
455 }
456
457 # -----------------------------------------------------------------------------
458 # CQL stuff
459 sub cqlmap {
460     my ($self,$mapfile) = @_;
461     if ($#_ > 0) {
462         if ($self->{cql_mapfile} ne $mapfile) {
463             unless (-f $mapfile) {
464                 croak("Cannot find $mapfile");
465             }
466             if (defined ($self->{cql_ct})) {
467               IDZebra::cql_transform_close($self->{cql_ct});
468             }
469             $self->{cql_ct} = IDZebra::cql_transform_open_fname($mapfile);
470             $self->{cql_mapfile} = $mapfile;
471         }
472     }
473     return ($self->{cql_mapfile});
474 }
475
476 sub cql2pqf {
477     my ($self, $cqlquery) = @_;
478     unless (defined($self->{cql_ct})) {
479         croak("CQL map file is not specified yet.");
480     }
481     my $res = "\0" x 2048;
482     my $r = IDZebra::cql2pqf($self->{cql_ct}, $cqlquery, $res, 2048);
483     unless ($r) {return (undef)};
484     $res=~s/\0.+$//g;
485     return ($res); 
486 }
487
488
489 # -----------------------------------------------------------------------------
490 # Search 
491 # -----------------------------------------------------------------------------
492 sub search {
493     my ($self, %args) = @_;
494
495     if ($args{cqlmap}) { $self->cqlmap($args{cqlmap}); }
496
497     my $query;
498     if ($args{pqf}) {
499         $query = $args{pqf};
500     }
501     elsif ($args{cql}) {
502         unless ($query = $self->cql2pqf($args{cql})) {
503             croak ("Invalid CQL query: '$args{cql}'");
504         }
505     }
506     unless ($query) {
507         croak ("No query given to search");
508     }
509
510     my $rsname = $args{rsname} ? $args{rsname} : $self->_new_setname;
511
512     return ($self->_search_pqf($query, $rsname));
513 }
514
515 sub _new_setname {
516     my ($self) = @_;
517     return ("set_".$self->{rscount}++);
518 }
519
520 sub _search_pqf {
521     my ($self, $query, $setname) = @_;
522
523     my $hits = IDZebra::search_PQF($self->{zh},
524                                    $self->{odr_input},
525                                    $self->{odr_output},
526                                    $query,
527                                    $setname);
528
529     my $rs  = IDZebra::Resultset->new($self,
530                                       name        => $setname,
531                                       recordCount => $hits,
532                                       errCode     => $self->errCode,
533                                       errString   => $self->errString);
534     return($rs);
535 }
536
537 sub search_cql {
538     my ($self, $query, $transfile) = @_;
539 }
540
541
542 sub search_ccl {
543     my ($self, $query, $transfile) = @_;
544 }
545
546 # -----------------------------------------------------------------------------
547 # Sort
548 #
549 # Sorting of multiple result sets is not supported by zebra...
550 # -----------------------------------------------------------------------------
551
552 sub sortResultsets {
553     my ($self, $sortspec, $setname, @sets) = @_;
554
555     my @setnames;
556     my $count = 0;
557     foreach my $rs (@sets) {
558         push (@setnames, $rs->{name});
559         $count += $rs->{recordCount};  # is this really sure ??? It doesn't 
560                                        # matter now...
561     }
562
563     my $status = IDZebra::sort($self->{zh},
564                                $self->{odr_output},
565                                $sortspec,
566                                $setname,
567                                \@setnames);
568
569     my $errCode = $self->errCode;
570     my $errString = $self->errString;
571
572     if ($status || $errCode) {$count = 0;}
573
574     my $rs  = IDZebra::Resultset->new($self,
575                                       name        => $setname,
576                                       recordCount => $count,
577                                       errCode     => $errCode,
578                                       errString   => $errString);
579     
580     return ($rs);
581 }
582
583
584 __END__
585
586 =head1 NAME
587
588 IDZebra::Session - A Zebra database server session for update and retrieval
589
590 =head1 SYNOPSIS
591
592   $sess = IDZebra::Session->new(configFile => 'demo/zebra.cfg');
593   $sess->open();
594
595   $sess = IDZebra::Session->open(configFile => 'demo/zebra.cfg');
596
597   $sess->close;
598
599 =head1 DESCRIPTION
600
601 Zebra is a high-performance, general-purpose structured text indexing and retrieval engine. It reads structured records in a variety of input formats (eg. email, XML, MARC) and allows access to them through exact boolean search expressions and relevance-ranked free-text queries. 
602
603 Zebra supports large databases (more than ten gigabytes of data, tens of millions of records). It supports incremental, safe database updates on live systems. You can access data stored in Zebra using a variety of Index Data tools (eg. YAZ and PHP/YAZ) as well as commercial and freeware Z39.50 clients and toolkits. 
604
605 =head1 OPENING AND CLOSING A ZEBRA SESSIONS
606
607 For the time beeing only local database services are supported, the same way as calling zebraidx or zebrasrv from the command shell. In order to open a local Zebra database, with a specific configuration file, use
608
609   $sess = IDZebra::Session->new(configFile => 'demo/zebra.cfg');
610   $sess->open();
611
612 or
613
614   $sess = IDZebra::Session->open(configFile => 'demo/zebra.cfg');
615
616 where $sess is going to be the object representing a Zebra Session. Whenever this variable gets out of scope, the session is closed, together with all active transactions, etc... Anyway, if you'd like to close the session, just say:
617
618   $sess->close();
619
620 This will
621   - close all transactions
622   - destroy all result sets
623   - close the session
624
625 In the future different database access methods are going to be available, 
626 like:
627
628   $sess = IDZebra::Session->open(server => 'ostrich.technomat.hu:9999');
629
630 You can also use the B<record group> arguments described below directly when calling the constructor, or the open method:
631
632   $sess = IDZebra::Session->open(configFile => 'demo/zebra.cfg',
633                                  groupName  => 'demo');
634
635
636 =head1 RECORD GROUPS 
637
638 If you manage different sets of records that share common characteristics, you can organize the configuration settings for each type into "groups". See the Zebra manual on the configuration file (zebra.cfg). 
639
640 For each open session a default record group is assigned. You can configure it in the constructor, or by the B<set_group> method:
641
642   $sess->group(groupName => ..., ...)
643
644 The following options are available:
645
646 =over 4
647
648 =item B<groupName>
649
650 This will select the named record group, and load the corresponding settings from the configuration file. All subsequent values will overwrite those...
651
652 =item B<databaseName>
653
654 The name of the (logical) database the updated records will belong to.
655
656 =item B<path>
657
658 This path is used for directory updates (B<update>, B<delete> methods);
659  
660 =item B<recordId>
661
662 This option determines how to identify your records. See I<Zebra manual: Locating Records>
663
664 =item B<recordType>
665
666 The record type used for indexing. 
667
668 =item B<flagStoreData> 
669
670 Specifies whether the records should be stored internally in the Zebra system files. If you want to maintain the raw records yourself, this option should be false (0). If you want Zebra to take care of the records for you, it should be true(1). 
671
672 =item B<flagStoreKeys>
673
674 Specifies whether key information should be saved for a given group of records. If you plan to update/delete this type of records later this should be specified as 1; otherwise it should be 0 (default), to save register space. 
675
676 =item B<flagRw>
677
678 ?
679
680 =item B<fileVerboseLimit>
681
682 Skip log messages, when doing a directory update, and the specified number of files are processed...
683
684 =item B<databaseNamePath>
685
686 ?
687
688 =item B<explainDatabase>
689
690 The name of the explain database to be used
691
692 =item B<followLinks>              
693
694 Follow links when doing directory update.
695
696 =back
697
698 You can use the same parameters calling all update methods.
699
700 =head1 TRANSACTIONS (WRITE LOCKS)
701
702 A transaction is a block of record update (insert / modify / delete) procedures. So, all call to such function will implicitly start a transaction, unless one is started by
703
704   $sess->begin_trans;
705
706 For multiple per record updates it's efficient to start transactions explicitly: otherwise registers (system files, vocabularies, etc..) are updated one by one. After finishing all requested updates, use
707
708   $stat = $sess->end_trans;
709
710 The return value is a ZebraTransactionStatus object, containing the following members as a hash reference:
711
712   $stat->{processed} # Number of records processed
713   $stat->{updated}   # Number of records processed
714   $stat->{deleted}   # Number of records processed
715   $stat->{inserted}  # Number of records processed
716   $stat->{stime}     # System time used
717   $stat->{utime}     # User time used
718
719 =head1 UPDATING DATA
720
721 There are two ways to update data in a Zebra database using the perl API. You can update an entire directory structure just the way it's done by zebraidx:
722
723   $sess->update(path      =>  'lib');
724
725 This will update the database with the files in directory "lib", according to the current record group settings.
726
727   $sess->update();
728
729 This will update the database with the files, specified by the default record group setting. I<path> has to be specified there...
730
731   $sess->update(groupName => 'demo1',
732                 path      =>  'lib');
733
734 Update the database with files in "lib" according to the settings of group "demo1"
735
736   $sess->delete(groupName => 'demo1',
737                 path      =>  'lib');
738
739 Delete the records derived from the files in directory "lib", according to the "demo1" group settings. Sounds complex? Read zebra documentation about identifying records.
740
741 You can also update records one by one, even directly from the memory:
742
743   $sysno = $sess->update_record(data       => $rec1,
744                                 recordType => 'grs.perl.pod',
745                                 groupName  => "demo1");
746
747 This will update the database with the given record buffer. Note, that in this case recordType is explicitly specified, as there is no filename given, and for the demo1 group, no default record type is specified. The return value is the system assigned id of the record.
748
749 You can also index a single file:
750
751   $sysno = $sess->update_record(file => "lib/IDZebra/Data1.pm");
752
753 Or, provide a buffer, and a filename (where filename will only be used to identify the record, if configured that way, and possibly to find out it's record type):
754
755   $sysno = $sess->update_record(data => $rec1,
756                                 file => "lib/IDZebra/Data1.pm");
757
758 And some crazy stuff:
759
760   $sysno = $sess->delete_record(sysno => $sysno);
761
762 where sysno in itself is sufficient to identify the record
763
764   $sysno = $sess->delete_record(data => $rec1,
765                                 recordType => 'grs.perl.pod',
766                                 groupName  => "demo1");
767
768 This case the record is extracted, and if already exists, located in the database, then deleted... 
769
770   $sysno = $sess->delete_record(data       => $rec1,
771                                 match      => $myid,
772                                 recordType => 'grs.perl.pod',
773                                 groupName  => "demo1");
774
775 Don't try this at home! This case, the record identifier string (which is normally generated according to the rules set in recordId directive of zebra.cfg) is provided directly....
776
777
778 B<Important:> Note, that one record can be updated only once within a transaction - all subsequent updates are skipped. 
779
780 =head1 SEARCHING
781
782
783 =head1 COPYRIGHT
784
785 Fill in
786
787 =head1 AUTHOR
788
789 Peter Popovics, pop@technomat.hu
790
791 =head1 SEE ALSO
792
793 IDZebra, IDZebra::Data1, Zebra documentation
794
795 =cut