installer: location of koha-conf.xml
[koha.git] / C4 / Search.pm
1 package C4::Search;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
16 # Suite 330, Boston, MA  02111-1307 USA
17
18 use strict;
19 require Exporter;
20 use C4::Context;
21 use C4::Biblio;    # GetMarcFromKohaField
22 use C4::Koha;      # getFacets
23 use Lingua::Stem;
24 use C4::Dates qw(format_date);
25
26 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
27
28 # set the version for version checking
29 BEGIN {
30         $VERSION = 3.01;
31         $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
32 }
33
34 =head1 NAME
35
36 C4::Search - Functions for searching the Koha catalog.
37
38 =head1 SYNOPSIS
39
40 see opac/opac-search.pl or catalogue/search.pl for example of usage
41
42 =head1 DESCRIPTION
43
44 This module provides the searching facilities for the Koha into a zebra catalog.
45
46 =head1 FUNCTIONS
47
48 =cut
49
50 @ISA    = qw(Exporter);
51 @EXPORT = qw(
52   &SimpleSearch
53   &findseealso
54   &FindDuplicate
55   &searchResults
56   &getRecords
57   &buildQuery
58   &NZgetRecords
59   &ModBiblios
60 );
61
62 # make all your functions, whether exported or not;
63
64 =head2 findseealso($dbh,$fields);
65
66 C<$dbh> is a link to the DB handler.
67
68 use C4::Context;
69 my $dbh =C4::Context->dbh;
70
71 C<$fields> is a reference to the fields array
72
73 This function modify the @$fields array and add related fields to search on.
74
75 =cut
76
77 sub findseealso {
78     my ( $dbh, $fields ) = @_;
79     my $tagslib = GetMarcStructure( 1 );
80     for ( my $i = 0 ; $i <= $#{$fields} ; $i++ ) {
81         my ($tag)      = substr( @$fields[$i], 1, 3 );
82         my ($subfield) = substr( @$fields[$i], 4, 1 );
83         @$fields[$i] .= ',' . $tagslib->{$tag}->{$subfield}->{seealso}
84           if ( $tagslib->{$tag}->{$subfield}->{seealso} );
85     }
86 }
87
88 =head2 FindDuplicate
89
90 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
91
92 =cut
93
94 sub FindDuplicate {
95     my ($record) = @_;
96     my $dbh = C4::Context->dbh;
97     my $result = TransformMarcToKoha( $dbh, $record, '' );
98     my $sth;
99     my $query;
100     my $search;
101     my $type;
102     my ( $biblionumber, $title );
103
104     # search duplicate on ISBN, easy and fast..
105     # ... normalize first
106     if ( $result->{isbn} ) {
107         $result->{isbn} =~ s/\(.*$//;
108         $result->{isbn} =~ s/\s+$//; 
109     }
110     #$search->{'avoidquerylog'}=1;
111     if ( $result->{isbn} ) {
112         $query = "isbn=$result->{isbn}";
113     }
114     else {
115         $result->{title} =~ s /\\//g;
116         $result->{title} =~ s /\"//g;
117         $result->{title} =~ s /\(//g;
118         $result->{title} =~ s /\)//g;
119         # remove valid operators
120         $result->{title} =~ s/(and|or|not)//g;
121         $query = "ti,ext=$result->{title}";
122         $query .= " and itemtype=$result->{itemtype}" if ($result->{itemtype});    
123         if ($result->{author}){
124           $result->{author} =~ s /\\//g;
125           $result->{author} =~ s /\"//g;
126           $result->{author} =~ s /\(//g;
127           $result->{author} =~ s /\)//g;
128           # remove valid operators
129           $result->{author} =~ s/(and|or|not)//g;
130           $query .= " and au,ext=$result->{author}";
131         }     
132     }
133     my ($error,$searchresults) =
134       SimpleSearch($query); # FIXME :: hardcoded !
135     my @results;
136     foreach my $possible_duplicate_record (@$searchresults) {
137         my $marcrecord =
138           MARC::Record->new_from_usmarc($possible_duplicate_record);
139         my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
140         
141         # FIXME :: why 2 $biblionumber ?
142         if ($result){
143           push @results, $result->{'biblionumber'};
144           push @results, $result->{'title'};
145         }
146     }
147     return @results;  
148 }
149
150 =head2 SimpleSearch
151
152 ($error,$results) = SimpleSearch($query,@servers);
153
154 this function performs a simple search on the catalog using zoom.
155
156 =over 2
157
158 =item C<input arg:>
159
160     * $query could be a simple keyword or a complete CCL query wich is depending on your ccl file.
161     * @servers is optionnal. default one is read on koha-conf.xml
162
163 =item C<Output arg:>
164     * $error is a string which containt the description error if there is one. Else it's empty.
165     * \@results is an array of marc record.
166
167 =item C<usage in the script:>
168
169 =back
170
171 my ($error, $marcresults) = SimpleSearch($query);
172
173 if (defined $error) {
174     $template->param(query_error => $error);
175     warn "error: ".$error;
176     output_html_with_http_headers $input, $cookie, $template->output;
177     exit;
178 }
179
180 my $hits = scalar @$marcresults;
181 my @results;
182
183 for(my $i=0;$i<$hits;$i++) {
184     my %resultsloop;
185     my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
186     my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
187
188     #build the hash for the template.
189     $resultsloop{highlight}       = ($i % 2)?(1):(0);
190     $resultsloop{title}           = $biblio->{'title'};
191     $resultsloop{subtitle}        = $biblio->{'subtitle'};
192     $resultsloop{biblionumber}    = $biblio->{'biblionumber'};
193     $resultsloop{author}          = $biblio->{'author'};
194     $resultsloop{publishercode}   = $biblio->{'publishercode'};
195     $resultsloop{publicationyear} = $biblio->{'publicationyear'};
196
197     push @results, \%resultsloop;
198 }
199 $template->param(result=>\@results);
200
201 =cut
202
203 sub SimpleSearch {
204     my $query   = shift;
205     if (C4::Context->preference('NoZebra')) {
206         my $result = NZorder(NZanalyse($query))->{'biblioserver'}->{'RECORDS'};
207         return (undef,$result);
208     } else {
209         my @servers = @_;
210         my @results;
211         my @tmpresults;
212         my @zconns;
213         return ( "No query entered", undef ) unless $query;
214     
215         #@servers = (C4::Context->config("biblioserver")) unless @servers;
216         @servers =
217         ("biblioserver") unless @servers
218         ;    # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
219     
220         # Connect & Search
221         for ( my $i = 0 ; $i < @servers ; $i++ ) {
222             eval {
223                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
224                 $tmpresults[$i] =
225                 $zconns[$i]
226                 ->search( new ZOOM::Query::CCL2RPN( $query, $zconns[$i] ) );
227         
228                 # getting error message if one occured.
229                 my $error =
230                   $zconns[$i]->errmsg() . " ("
231                 . $zconns[$i]->errcode() . ") "
232                 . $zconns[$i]->addinfo() . " "
233                 . $zconns[$i]->diagset();
234     
235                 return ( $error, undef ) if $zconns[$i]->errcode();
236             };
237             if ($@) {
238                 # caught a ZOOM::Exception
239                 my $error = 
240                   $@->message() . " ("
241                 . $@->code() . ") "
242                 . $@->addinfo() . " "
243                 . $@->diagset();
244                 warn $error;
245                 return ( $error, undef );
246             }
247         }
248         my $hits;
249         my $ev;
250         while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
251             $ev = $zconns[ $i - 1 ]->last_event();
252             if ( $ev == ZOOM::Event::ZEND ) {
253                 $hits = $tmpresults[ $i - 1 ]->size();
254             }
255             if ( $hits > 0 ) {
256                 for ( my $j = 0 ; $j < $hits ; $j++ ) {
257                     my $record = $tmpresults[ $i - 1 ]->record($j)->raw();
258                     push @results, $record;
259                 }
260             }
261         }
262         return ( undef, \@results );
263     }
264 }
265
266 # performs the search
267 sub getRecords {
268     my (
269         $koha_query,     $simple_query,  $sort_by_ref,
270         $servers_ref,    $results_per_page, $offset,
271         $expanded_facet, $branches,         $query_type,
272         $scan
273     ) = @_;
274 #     warn "Query : $koha_query";
275     my @servers = @$servers_ref;
276     my @sort_by = @$sort_by_ref;
277
278     # create the zoom connection and query object
279     my $zconn;
280     my @zconns;
281     my @results;
282     my $results_hashref = ();
283
284     ### FACETED RESULTS
285     my $facets_counter = ();
286     my $facets_info    = ();
287     my $facets         = getFacets();
288
289     #### INITIALIZE SOME VARS USED CREATE THE FACETED RESULTS
290     my @facets_loop;    # stores the ref to array of hashes for template
291     for ( my $i = 0 ; $i < @servers ; $i++ ) {
292         $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
293
294 # perform the search, create the results objects
295 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
296         my $query_to_use;
297         if ( $servers[$i] =~ /biblioserver/ ) {
298             $query_to_use = $koha_query;
299         }
300         else {
301             $query_to_use = $simple_query;
302         }
303
304                 #$query_to_use = $simple_query if $scan;
305                 #warn $simple_query if ($scan && $DEBUG);
306         # check if we've got a query_type defined
307         eval {
308             if ($query_type)
309             {
310                 if ( $query_type =~ /^ccl/ ) {
311                     $query_to_use =~
312                       s/\:/\=/g;    # change : to = last minute (FIXME)
313
314                     #                 warn "CCL : $query_to_use";
315                     $results[$i] =
316                       $zconns[$i]->search(
317                         new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
318                       );
319                 }
320                 elsif ( $query_type =~ /^cql/ ) {
321
322                     #                 warn "CQL : $query_to_use";
323                     $results[$i] =
324                       $zconns[$i]->search(
325                         new ZOOM::Query::CQL( $query_to_use, $zconns[$i] ) );
326                 }
327                 elsif ( $query_type =~ /^pqf/ ) {
328
329                     #                 warn "PQF : $query_to_use";
330                     $results[$i] =
331                       $zconns[$i]->search(
332                         new ZOOM::Query::PQF( $query_to_use, $zconns[$i] ) );
333                 }
334             }
335             else {
336                 if ($scan) {
337                      #               warn "preparing to scan:$query_to_use";
338                     $results[$i] =
339                       $zconns[$i]->scan(
340                         new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
341                       );
342                 }
343                 else {
344                     #             warn "LAST : $query_to_use";
345                     $results[$i] =
346                       $zconns[$i]->search(
347                         new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
348                       );
349                 }
350             }
351         };
352         if ($@) {
353             warn "WARNING: query problem with $query_to_use " . $@;
354         }
355
356         # concatenate the sort_by limits and pass them to the results object
357         my $sort_by;
358         foreach my $sort (@sort_by) {
359             if ($sort eq "author_az") {
360                 $sort_by.="1=1003 <i ";
361             }
362             elsif ($sort eq "author_za") {
363                 $sort_by.="1=1003 >i ";
364             }
365             elsif ($sort eq "popularity_asc") {
366                 $sort_by.="1=9003 <i ";
367             }
368             elsif ($sort eq "popularity_dsc") {
369                 $sort_by.="1=9003 >i ";
370             }
371             elsif ($sort eq "call_number_asc") {
372                 $sort_by.="1=20  <i ";
373             }
374             elsif ($sort eq "call_number_dsc") {
375                 $sort_by.="1=20 >i ";
376             }
377             elsif ($sort eq "pubdate_asc") {
378                 $sort_by.="1=31 <i ";
379             }
380             elsif ($sort eq "pubdate_dsc") {
381                 $sort_by.="1=31 >i ";
382             }
383             elsif ($sort eq "acqdate_asc") {
384                 $sort_by.="1=32 <i ";
385             }
386             elsif ($sort eq "acqdate_dsc") {
387                 $sort_by.="1=32 >i ";
388             }
389             elsif ($sort eq "title_az") {
390                 $sort_by.="1=4 <i ";
391             }
392             elsif ($sort eq "title_za") {
393                 $sort_by.="1=4 >i ";
394             }
395         }
396         if ($sort_by) {
397             if ( $results[$i]->sort( "yaz", $sort_by ) < 0) {
398                 warn "WARNING sort $sort_by failed";
399             }
400         }
401     }
402     while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
403         my $ev = $zconns[ $i - 1 ]->last_event();
404         if ( $ev == ZOOM::Event::ZEND ) {
405             my $size = $results[ $i - 1 ]->size();
406             if ( $size > 0 ) {
407                 my $results_hash;
408                 #$results_hash->{'server'} = $servers[$i-1];
409                 # loop through the results
410                 $results_hash->{'hits'} = $size;
411                 my $times;
412                 if ( $offset + $results_per_page <= $size ) {
413                     $times = $offset + $results_per_page;
414                 }
415                 else {
416                     $times = $size;
417                 }
418                 for ( my $j = $offset ; $j < $times ; $j++ )
419                 {   #(($offset+$count<=$size) ? ($offset+$count):$size) ; $j++){
420                     my $records_hash;
421                     my $record;
422                     my $facet_record;
423                     ## This is just an index scan
424                     if ($scan) {
425                         my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
426                  # here we create a minimal MARC record and hand it off to the
427                  # template just like a normal result ... perhaps not ideal, but
428                  # it works for now
429                         my $tmprecord = MARC::Record->new();
430                         $tmprecord->encoding('UTF-8');
431                         my $tmptitle;
432
433                         # srote the minimal record in author/title (depending on MARC flavour)
434                         if ( C4::Context->preference("marcflavour") eq
435                             "UNIMARC" )
436                         {
437                             $tmptitle = MARC::Field->new(
438                                 '200', ' ', ' ',
439                                 a => $term,
440                                 f => $occ
441                             );
442                         }
443                         else {
444                             $tmptitle = MARC::Field->new(
445                                 '245', ' ', ' ',
446                                 a => $term,
447                                 b => $occ
448                             );
449                         }
450                         $tmprecord->append_fields($tmptitle);
451                         $results_hash->{'RECORDS'}[$j] =
452                           $tmprecord->as_usmarc();
453                     }
454                     else {
455                         $record = $results[ $i - 1 ]->record($j)->raw();
456
457                         #warn "RECORD $j:".$record;
458                         $results_hash->{'RECORDS'}[$j] =
459                           $record;    # making a reference to a hash
460                                       # Fill the facets while we're looping
461                         $facet_record = MARC::Record->new_from_usmarc($record);
462
463                         #warn $servers[$i-1].$facet_record->title();
464                         for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
465                             if ( $facets->[$k] ) {
466                                 my @fields;
467                                 for my $tag ( @{ $facets->[$k]->{'tags'} } ) {
468                                     push @fields, $facet_record->field($tag);
469                                 }
470                                 for my $field (@fields) {
471                                     my @subfields = $field->subfields();
472                                     for my $subfield (@subfields) {
473                                         my ( $code, $data ) = @$subfield;
474                                         if ( $code eq
475                                             $facets->[$k]->{'subfield'} )
476                                         {
477                                             $facets_counter->{ $facets->[$k]
478                                                   ->{'link_value'} }->{$data}++;
479                                         }
480                                     }
481                                 }
482                                 $facets_info->{ $facets->[$k]->{'link_value'} }
483                                   ->{'label_value'} =
484                                   $facets->[$k]->{'label_value'};
485                                 $facets_info->{ $facets->[$k]->{'link_value'} }
486                                   ->{'expanded'} = $facets->[$k]->{'expanded'};
487                             }
488                         }
489                     }
490                 }
491                 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
492             }
493
494             #print "connection ", $i-1, ": $size hits";
495             #print $results[$i-1]->record(0)->render() if $size > 0;
496             # BUILD FACETS
497             for my $link_value (
498                 sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
499                 keys %$facets_counter
500               )
501             {
502                 my $expandable;
503                 my $number_of_facets;
504                 my @this_facets_array;
505                 for my $one_facet (
506                     sort {
507                         $facets_counter->{$link_value}
508                           ->{$b} <=> $facets_counter->{$link_value}->{$a}
509                     } keys %{ $facets_counter->{$link_value} }
510                   )
511                 {
512                     $number_of_facets++;
513                     if (   ( $number_of_facets < 6 )
514                         || ( $expanded_facet eq $link_value )
515                         || ( $facets_info->{$link_value}->{'expanded'} ) )
516                     {
517
518                        # sanitize the link value ), ( will cause errors with CCL
519                         my $facet_link_value = $one_facet;
520                         $facet_link_value =~ s/(\(|\))/ /g;
521
522                         # fix the length that will display in the label
523                         my $facet_label_value = $one_facet;
524                         $facet_label_value = substr( $one_facet, 0, 20 ) . "..."
525                           unless length($facet_label_value) <= 20;
526
527                        # well, if it's a branch, label by the name, not the code
528                         if ( $link_value =~ /branch/ ) {
529                             $facet_label_value =
530                               $branches->{$one_facet}->{'branchname'};
531                         }
532
533                  # but we're down with the whole label being in the link's title
534                         my $facet_title_value = $one_facet;
535
536                         push @this_facets_array,
537                           (
538                             {
539                                 facet_count =>
540                                   $facets_counter->{$link_value}->{$one_facet},
541                                 facet_label_value => $facet_label_value,
542                                 facet_title_value => $facet_title_value,
543                                 facet_link_value  => $facet_link_value,
544                                 type_link_value   => $link_value,
545                             },
546                           );
547                     }
548                 }
549                 unless ( $facets_info->{$link_value}->{'expanded'} ) {
550                     $expandable = 1
551                       if ( ( $number_of_facets > 6 )
552                         && ( $expanded_facet ne $link_value ) );
553                 }
554                 push @facets_loop,
555                   (
556                     {
557                         type_link_value => $link_value,
558                         type_id         => $link_value . "_id",
559                         type_label      =>
560                           $facets_info->{$link_value}->{'label_value'},
561                         facets     => \@this_facets_array,
562                         expandable => $expandable,
563                         expand     => $link_value,
564                     }
565                   );
566             }
567         }
568     }
569     return ( undef, $results_hashref, \@facets_loop );
570 }
571
572 # STOPWORDS
573 sub _remove_stopwords {
574     my ($operand,$index) = @_;
575         my @stopwords_removed;
576     # phrase and exact-qualified indexes shouldn't have stopwords removed
577     if ($index!~m/phr|ext/){
578     # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
579     #       we use IsAlpha unicode definition, to deal correctly with diacritics.
580     #       otherwise, a French word like "leçon" woudl be split into "le" "çon", le 
581     #       is an empty word, we'd get "çon" and wouldn't find anything...
582         foreach (keys %{C4::Context->stopwords}) {
583             next if ($_ =~/(and|or|not)/); # don't remove operators
584                         if ($operand =~ /(\P{IsAlpha}$_\P{IsAlpha}|^$_\P{IsAlpha}|\P{IsAlpha}$_$)/) {
585                 $operand=~ s/\P{IsAlpha}$_\P{IsAlpha}/ /gi;
586                 $operand=~ s/^$_\P{IsAlpha}/ /gi;
587                 $operand=~ s/\P{IsAlpha}$_$/ /gi;
588                                 push @stopwords_removed, $_;
589                         }
590         }
591     }
592     return ($operand, \@stopwords_removed);
593 }
594
595 # TRUNCATION
596 sub _detect_truncation {
597     my ($operand,$index) = @_;
598     my (@nontruncated,@righttruncated,@lefttruncated,@rightlefttruncated,@regexpr);
599     $operand =~s/^ //g;
600     my @wordlist= split (/\s/,$operand);
601     foreach my $word (@wordlist){
602         if ($word=~s/^\*([^\*]+)\*$/$1/){
603             push @rightlefttruncated,$word;
604         } 
605         elsif($word=~s/^\*([^\*]+)$/$1/){
606             push @lefttruncated,$word;
607         } 
608         elsif ($word=~s/^([^\*]+)\*$/$1/){
609             push @righttruncated,$word;
610         } 
611         elsif (index($word,"*")<0){
612             push @nontruncated,$word;
613         }
614         else {
615             push @regexpr,$word;
616         }
617     }
618     return (\@nontruncated,\@righttruncated,\@lefttruncated,\@rightlefttruncated,\@regexpr);
619 }
620
621 sub _build_stemmed_operand {
622     my ($operand) = @_;
623     my $stemmed_operand;
624     # FIXME: the locale should be set based on the user's language and/or search choice
625     my $stemmer = Lingua::Stem->new( -locale => 'EN-US' );
626     # FIXME: these should be stored in the db so the librarian can modify the behavior
627     $stemmer->add_exceptions(
628             {
629                 'and' => 'and',
630                 'or'  => 'or',
631                 'not' => 'not',
632             }
633                     
634         );
635     my @words = split( / /, $operand );
636     my $stems = $stemmer->stem(@words);
637     for my $stem (@$stems) {
638             $stemmed_operand .= "$stem";
639             $stemmed_operand .= "?" unless ( $stem =~ /(and$|or$|not$)/ ) || ( length($stem) < 3 );
640             $stemmed_operand .= " ";
641     }
642     #warn "STEMMED OPERAND: $stemmed_operand";
643     return $stemmed_operand;
644 }
645
646 sub _build_weighted_query {
647     # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
648     # pretty well but will work much better when we have an actual query parser
649     my ($operand,$stemmed_operand,$index) = @_;
650     my $stemming      = C4::Context->preference("QueryStemming")     || 0;
651     my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
652     my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
653
654     my $weighted_query .= "(rk=(";     # Specifies that we're applying rank
655
656     # Keyword, or, no index specified
657     if ( ( $index eq 'kw' ) || ( !$index ) ) {
658         $weighted_query .= "Title-cover,ext,r1=\"$operand\"";       # exact title-cover
659         $weighted_query .= " or ti,ext,r2=\"$operand\"";            # exact title
660         $weighted_query .= " or ti,phr,r3=\"$operand\"";            # phrase title
661        #$weighted_query .= " or any,ext,r4=$operand";               # exact any
662        #$weighted_query .=" or kw,wrdl,r5=\"$operand\"";            # word list any
663         $weighted_query .= " or wrd,fuzzy,r8=\"$operand\"" if $fuzzy_enabled; # add fuzzy, word list
664         $weighted_query .= " or wrd,right-Truncation,r9=\"$stemmed_operand\"" if ($stemming and $stemmed_operand); # add stemming, right truncation
665        # embedded sorting: 0 a-z; 1 z-a
666        # $weighted_query .= ") or (sort1,aut=1";
667     }
668         elsif ( $index eq 'bc' ) {
669                 $weighted_query .= "bc=\"$operand\"";
670         }
671     # if the index already has more than one qualifier, just wrap the operand 
672     # in quotes and pass it back
673     elsif ($index =~ ',') {
674         $weighted_query .=" $index=\"$operand\"";
675     }
676     #TODO: build better cases based on specific search indexes
677     else {
678        $weighted_query .= " $index,ext,r1=\"$operand\"";            # exact index
679        #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
680        $weighted_query .= " or $index,phr,r3=\"$operand\"";         # phrase index
681        $weighted_query .= " or $index,rt,wrd,r3=\"$operand\"";      # word list index
682     }
683     $weighted_query .= "))";    # close rank specification
684     return $weighted_query;
685 }
686
687 # build the query itself
688 sub buildQuery {
689     my ( $operators, $operands, $indexes, $limits, $sort_by, $scan) = @_;
690
691     my @operators = @$operators if $operators;
692     my @indexes   = @$indexes   if $indexes;
693     my @operands  = @$operands  if $operands;
694     my @limits    = @$limits    if $limits;
695     my @sort_by   = @$sort_by   if $sort_by;
696
697     my $stemming      = C4::Context->preference("QueryStemming")                || 0;
698         my $auto_truncation = C4::Context->preference("QueryAutoTruncate")              || 0;
699     my $weight_fields = C4::Context->preference("QueryWeightFields")            || 0;
700     my $fuzzy_enabled = C4::Context->preference("QueryFuzzy")                           || 0;
701     # no stemming/weight/fuzzy in NoZebra
702     if (C4::Context->preference("NoZebra")) {
703         $stemming =0;
704         $weight_fields=0;
705         $fuzzy_enabled=0;
706     }
707         my $remove_stopwords = C4::Context->preference("QueryRemoveStopwords")  || 0;
708
709     my $query = $operands[0];
710         my $simple_query = $operands[0];
711         my $query_cgi;
712         my $query_desc;
713         my $query_type;
714
715         my $limit;
716         my $limit_cgi;
717         my $limit_desc;
718
719         my $stopwords_removed;
720
721         # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
722         # DIAGNOSTIC ONLY!!
723     if ( $query =~ /^ccl=/ ) {
724         return ( undef, $', $', $', $', '', '', '', '', 'ccl' );
725     }
726     if ( $query =~ /^cql=/ ) {
727         return ( undef, $', $', $', $', '', '', '', '', 'cql' );
728     }
729     if ( $query =~ /^pqf=/ ) {
730         return ( undef, $', $', $', $', '', '', '', '', 'pqf' );
731     }
732
733         # pass nested queries directly
734     if ( $query =~ /(\(|\))/ ) {
735         return ( undef, $query, $simple_query, $query_cgi, $query, $limit, $limit_cgi, $limit_desc, $stopwords_removed, 'ccl' );
736     }
737
738 # form-based queries are limited to non-nested at a specific depth, so we can easily
739 # modify the incoming query operands and indexes to do stemming and field weighting
740 # Once we do so, we'll end up with a value in $query, just like if we had an
741 # incoming $query from the user
742     else {
743         $query = ""; # clear it out so we can populate properly with field-weighted stemmed query
744         my $previous_operand;    # a flag used to keep track if there was a previous query
745                                 # if there was, we can apply the current operator
746         # for every operand
747         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
748
749             # COMBINE OPERANDS, INDEXES AND OPERATORS
750             if ( $operands[$i] ) {
751
752                                 # a flag to determine whether or not to add the index to the query
753                                 my $indexes_set;
754                                 # if the user is sophisticated enough to specify an index, turn off some defaults
755                                 if ($operands[$i] =~ /(:|=)/ || $scan) {
756                                         $weight_fields = 0;
757                                         $stemming = 0;
758                                         $remove_stopwords = 0;
759                                 }
760                 my $operand = $operands[$i];
761                 my $index   = $indexes[$i];
762
763                                 # some helpful index modifs
764                 my $index_plus = "$index:" if $index;
765                 my $index_plus_comma="$index," if $index;
766
767                 # Remove Stopwords
768                                 if ($remove_stopwords) {
769                 ($operand, $stopwords_removed) = _remove_stopwords($operand,$index);
770                         warn "OPERAND w/out STOPWORDS: >$operand<" if $DEBUG;
771                                         warn "REMOVED STOPWORDS: @$stopwords_removed" if ($stopwords_removed && $DEBUG);
772                                 }
773
774                 # Detect Truncation
775                 my ($nontruncated,$righttruncated,$lefttruncated,$rightlefttruncated,$regexpr);
776                 my $truncated_operand;
777                 ($nontruncated,$righttruncated,$lefttruncated,$rightlefttruncated,$regexpr) = _detect_truncation($operand,$index);
778                 warn "TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<" if $DEBUG;
779
780                 # Apply Truncation
781                 if (scalar(@$righttruncated)+scalar(@$lefttruncated)+scalar(@$rightlefttruncated)>0){
782                                         # don't field weight or add the index to the query, we do it here
783                     $indexes_set = 1;
784                     undef $weight_fields;
785                     my $previous_truncation_operand;
786                     if (scalar(@$nontruncated)>0) {
787                         $truncated_operand.= "$index_plus @$nontruncated ";
788                         $previous_truncation_operand = 1;
789                     }
790                     if (scalar(@$righttruncated)>0){
791                         $truncated_operand .= "and " if $previous_truncation_operand;
792                         $truncated_operand .= "$index_plus_comma"."rtrn:@$righttruncated ";
793                         $previous_truncation_operand = 1;
794                     }
795                     if (scalar(@$lefttruncated)>0){
796                         $truncated_operand .= "and " if $previous_truncation_operand;
797                         $truncated_operand .= "$index_plus_comma"."ltrn:@$lefttruncated ";
798                         $previous_truncation_operand = 1;
799                     }
800                     if (scalar(@$rightlefttruncated)>0){
801                         $truncated_operand .= "and " if $previous_truncation_operand;
802                         $truncated_operand .= "$index_plus_comma"."rltrn:@$rightlefttruncated ";
803                         $previous_truncation_operand = 1;
804                     }
805                 }
806                 $operand = $truncated_operand if $truncated_operand;
807                 warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
808
809                 # Handle Stemming
810                 my $stemmed_operand;
811                 $stemmed_operand = _build_stemmed_operand($operand) if $stemming;
812                 warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
813
814                 # Handle Field Weighting
815                 my $weighted_operand;
816                 $weighted_operand = _build_weighted_query($operand,$stemmed_operand,$index) if $weight_fields;
817                 warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
818                 $operand = $weighted_operand if $weight_fields;
819                 $indexes_set = 1 if $weight_fields;
820
821                 # If there's a previous operand, we need to add an operator
822                 if ($previous_operand) {
823
824                     # user-specified operator
825                     if ( $operators[$i-1] ) {
826                         $query .= " $operators[$i-1] ";
827                         $query .= " $index_plus " unless $indexes_set;
828                         $query .= " $operand";
829                                                 $query_cgi .="&op=$operators[$i-1]";
830                                                 $query_cgi .="&idx=$index" if $index;
831                                                 $query_cgi .="&q=$operands[$i]" if $operands[$i];
832                                                 $query_desc .=" $operators[$i-1] $index_plus $operands[$i]";
833                     }
834
835                     # the default operator is and
836                     else {
837                         $query .= " and ";
838                         $query .= "$index_plus " unless $indexes_set;
839                         $query .= "$operand";
840                                                 $query_cgi .="&op=and&idx=$index" if $index;
841                                                 $query_cgi .="&q=$operands[$i]" if $operands[$i];
842                         $query_desc .= " and $index_plus $operands[$i]";
843                     }
844                 }
845
846                                 # there isn't a pervious operand, don't need an operator
847                 else { 
848                                         # field-weighted queries already have indexes set
849                                         $query .=" $index_plus " unless $indexes_set;
850                                         $query .= $operand;
851                                         $query_desc .= " $index_plus $operands[$i]";
852                                         $query_cgi.="&idx=$index" if $index;
853                                         $query_cgi.="&q=$operands[$i]" if $operands[$i];
854
855                     $previous_operand = 1;
856                 }
857             }    #/if $operands
858         }    # /for
859     }
860     warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
861
862     # add limits
863         my $group_OR_limits;
864         my $availability_limit;
865     foreach my $this_limit (@limits) {
866         if ( $this_limit =~ /available/ ) {
867                         # available is defined as (items.notloan is NULL) and (items.itemlost > 0 or NULL) (last clause handles NULL values for lost in zebra)
868                         $availability_limit .="( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and ((lost,st-numeric gt 0) or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
869                         $limit_cgi .= "&limit=available";
870                         $limit_desc .="";
871         }
872
873                 # these are treated as OR
874         elsif ( $this_limit =~ /mc/ ) {
875             $group_OR_limits .= " or " if $group_OR_limits;
876                         $limit_desc .=" or " if $group_OR_limits;
877                         $group_OR_limits .= "$this_limit";
878                         $limit_cgi .="&limit=$this_limit";
879                         $limit_desc .= "$this_limit";
880         }
881
882                 # regular old limits
883                 else {
884                         $limit .= " and " if $limit || $query;
885                         $limit .= "$this_limit";
886                         $limit_cgi .="&limit=$this_limit";
887                         $limit_desc .=" and $this_limit";
888                 }
889     }
890         if ($group_OR_limits) {
891                 $limit.=" and " if ($query || $limit );
892                 $limit.="($group_OR_limits)";
893         }
894         if ($availability_limit) {
895                 $limit.=" not " if ($query || $limit );
896                 $limit.="$availability_limit";
897         }
898         # normalize the strings
899         $query =~ s/:/=/g;
900         $limit =~ s/:/=/g;
901         for ($query, $query_desc, $limit, $limit_desc) {
902                 $_ =~ s/  / /g;    # remove extra spaces
903         $_ =~ s/^ //g;     # remove any beginning spaces
904                 $_ =~ s/ $//g;     # remove any ending spaces
905         $_ =~ s/==/=/g;    # remove double == from query
906
907         }
908         $query_cgi =~ s/^&//;
909
910         # append the limit to the query
911         $query .= " ".$limit;
912
913     warn "QUERY:".$query if $DEBUG;
914         warn "QUERY CGI:".$query_cgi if $DEBUG;
915     warn "QUERY DESC:".$query_desc if $DEBUG;
916     warn "LIMIT:".$limit if $DEBUG;
917     warn "LIMIT CGI:".$limit_cgi if $DEBUG;
918     warn "LIMIT DESC:".$limit_desc if $DEBUG;
919
920         return ( undef, $query,$simple_query,$query_cgi,$query_desc,$limit,$limit_cgi,$limit_desc,$stopwords_removed,$query_type );
921 }
922
923 # IMO this subroutine is pretty messy still -- it's responsible for
924 # building the HTML output for the template
925 sub searchResults {
926     my ( $searchdesc, $hits, $results_per_page, $offset, @marcresults ) = @_;
927
928     my $dbh = C4::Context->dbh;
929     my $toggle;
930     my $even = 1;
931     my @newresults;
932     my $span_terms_hashref;
933     for my $span_term ( split( / /, $searchdesc ) ) {
934         $span_term =~ s/(.*=|\)|\(|\+|\.)//g;
935         $span_terms_hashref->{$span_term}++;
936     }
937
938     #Build brancnames hash
939     #find branchname
940     #get branch information.....
941     my %branches;
942     my $bsth =
943       $dbh->prepare("SELECT branchcode,branchname FROM branches")
944       ;    # FIXME : use C4::Koha::GetBranches
945     $bsth->execute();
946     while ( my $bdata = $bsth->fetchrow_hashref ) {
947         $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
948     }
949
950     #Build itemtype hash
951     #find itemtype & itemtype image
952     my %itemtypes;
953     $bsth =
954       $dbh->prepare("SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes");
955     $bsth->execute();
956     while ( my $bdata = $bsth->fetchrow_hashref ) {
957         $itemtypes{ $bdata->{'itemtype'} }->{description} =
958           $bdata->{'description'};
959         $itemtypes{ $bdata->{'itemtype'} }->{imageurl} = $bdata->{'imageurl'};
960         $itemtypes{ $bdata->{'itemtype'} }->{summary} = $bdata->{'summary'};
961         $itemtypes{ $bdata->{'itemtype'} }->{notforloan} = $bdata->{'notforloan'};
962     }
963
964     #search item field code
965     my $sth =
966       $dbh->prepare(
967 "select tagfield from marc_subfield_structure where kohafield like 'items.itemnumber'"
968       );
969     $sth->execute;
970     my ($itemtag) = $sth->fetchrow;
971
972     ## find column names of items related to MARC
973     my $sth2 = $dbh->prepare("SHOW COLUMNS from items");
974     $sth2->execute;
975     my %subfieldstosearch;
976     while ( ( my $column ) = $sth2->fetchrow ) {
977         my ( $tagfield, $tagsubfield ) =
978           &GetMarcFromKohaField( "items." . $column, "" );
979         $subfieldstosearch{$column} = $tagsubfield;
980     }
981     my $times;
982
983     if ( $hits && $offset + $results_per_page <= $hits ) {
984         $times = $offset + $results_per_page;
985     }
986     else {
987         $times = $hits;
988     }
989
990     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
991         my $marcrecord;
992         $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
993         my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, '' );
994                 $oldbiblio->{result_number} = $i+1;
995         # add image url if there is one
996         if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} =~ /^http:/ ) {
997             $oldbiblio->{imageurl} =
998               $itemtypes{ $oldbiblio->{itemtype} }->{imageurl};
999             $oldbiblio->{description} =
1000               $itemtypes{ $oldbiblio->{itemtype} }->{description};
1001         }
1002         else {
1003             $oldbiblio->{imageurl} =
1004               getitemtypeimagesrc() . "/"
1005               . $itemtypes{ $oldbiblio->{itemtype} }->{imageurl}
1006               if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1007             $oldbiblio->{description} =
1008               $itemtypes{ $oldbiblio->{itemtype} }->{description};
1009         }
1010         #
1011         # build summary if there is one (the summary is defined in itemtypes table
1012         #
1013         if ($itemtypes{ $oldbiblio->{itemtype} }->{summary}) {
1014             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1015             my @fields = $marcrecord->fields();
1016             foreach my $field (@fields) {
1017                 my $tag = $field->tag();
1018                 my $tagvalue = $field->as_string();
1019                 $summary =~ s/\[(.?.?.?.?)$tag\*(.*?)]/$1$tagvalue$2\[$1$tag$2]/g;
1020                 unless ($tag<10) {
1021                     my @subf = $field->subfields;
1022                     for my $i (0..$#subf) {
1023                         my $subfieldcode = $subf[$i][0];
1024                         my $subfieldvalue = $subf[$i][1];
1025                         my $tagsubf = $tag.$subfieldcode;
1026                         $summary =~ s/\[(.?.?.?.?)$tagsubf(.*?)]/$1$subfieldvalue$2\[$1$tagsubf$2]/g;
1027                     }
1028                 }
1029             }
1030             $summary =~ s/\[(.*?)]//g;
1031             $summary =~ s/\n/<br>/g;
1032             $oldbiblio->{summary} = $summary;
1033         }
1034         # add spans to search term in results for search term highlighting
1035         # save a native author, for the <a href=search.lq=<!--tmpl_var name="author"-->> link
1036         $oldbiblio->{'author_nospan'} = $oldbiblio->{'author'};
1037         foreach my $term ( keys %$span_terms_hashref ) {
1038             my $old_term = $term;
1039             if ( length($term) > 3 ) {
1040                 $term =~ s/(.*=|\)|\(|\+|\.|\?|\[|\])//g;
1041                 $term =~ s/\\//g;
1042                 $term =~ s/\*//g;
1043
1044                 #FIXME: is there a better way to do this?
1045                 $oldbiblio->{'title'} =~ s/$term/<span class=\"term\">$&<\/span>/gi;
1046                 $oldbiblio->{'subtitle'} =~
1047                   s/$term/<span class=\"term\">$&<\/span>/gi;
1048
1049                 $oldbiblio->{'author'} =~ s/$term/<span class=\"term\">$&<\/span>/gi;
1050                 $oldbiblio->{'publishercode'} =~ s/$term/<span class=\"term\">$&<\/span>/gi;
1051                 $oldbiblio->{'place'} =~ s/$term/<span class=\"term\">$&<\/span>/gi;
1052                 $oldbiblio->{'pages'} =~ s/$term/<span class=\"term\">$&<\/span>/gi;
1053                 $oldbiblio->{'notes'} =~ s/$term/<span class=\"term\">$&<\/span>/gi;
1054                 $oldbiblio->{'size'}  =~ s/$term/<span class=\"term\">$&<\/span>/gi;
1055             }
1056         }
1057
1058         if ( $i % 2 ) {
1059             $toggle = "#ffffcc";
1060         }
1061         else {
1062             $toggle = "white";
1063         }
1064         $oldbiblio->{'toggle'} = $toggle;
1065         my @fields = $marcrecord->field($itemtag);
1066         my @items_loop;
1067         my $items;
1068         my $ordered_count     = 0;
1069         my $onloan_count      = 0;
1070         my $wthdrawn_count    = 0;
1071         my $itemlost_count    = 0;
1072         my $norequests        = 1;
1073
1074         #
1075         # check the loan status of the item : 
1076         # it is not stored in the MARC record, for pref (zebra reindexing)
1077         # reason. Thus, we have to get the status from a specific SQL query
1078         #
1079         my $sth_issue = $dbh->prepare("
1080             SELECT date_due,returndate 
1081             FROM issues 
1082             WHERE itemnumber=? AND returndate IS NULL");
1083         my $items_count=scalar(@fields);
1084         foreach my $field (@fields) {
1085             my $item;
1086             foreach my $code ( keys %subfieldstosearch ) {
1087                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1088             }
1089             $sth_issue->execute($item->{itemnumber});
1090             $item->{due_date} = format_date($sth_issue->fetchrow) if $sth_issue->fetchrow;
1091             $item->{onloan} = 1 if $item->{due_date};
1092             # at least one item can be reserved : suppose no
1093             $norequests = 1;
1094             if ( $item->{wthdrawn} ) {
1095                 $wthdrawn_count++;
1096                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1097                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{wthdrawn}=1;
1098             }
1099             elsif ( $item->{itemlost} ) {
1100                 $itemlost_count++;
1101                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1102                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{itemlost}=1;
1103             }
1104             unless ( $item->{notforloan}) {
1105                 # OK, this one can be issued, so at least one can be reserved
1106                 $norequests = 0;
1107             }
1108             if ( ( $item->{onloan} ) && ( $item->{onloan} != '0000-00-00' ) )
1109             {
1110                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1111                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{onloancount} = 1;
1112                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{due_date} = $item->{due_date};
1113                 $onloan_count++;
1114             }
1115             if ( $item->{'homebranch'} ) {
1116                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{count}++;
1117             }
1118
1119             # Last resort
1120             elsif ( $item->{'holdingbranch'} ) {
1121                 $items->{ $item->{'holdingbranch'} }->{count}++;
1122             }
1123             $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{itemcallnumber} =                $item->{itemcallnumber};
1124             $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{location} =                $item->{location};
1125             $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{branchcode} =               $item->{homebranch};
1126         }    # notforloan, item level and biblioitem level
1127
1128         # last check for norequest : if itemtype is notforloan, it can't be reserved either, whatever the items
1129         $norequests = 1 if $itemtypes{$oldbiblio->{itemtype}}->{notforloan};
1130                 my $itemscount;
1131         for my $key ( sort keys %$items ) {
1132                         $itemscount++;
1133             my $this_item = {
1134                 branchname     => $branches{$items->{$key}->{branchcode}},
1135                 branchcode     => $items->{$key}->{branchcode},
1136                 count          => $items->{$key}->{count},
1137                 itemcallnumber => $items->{$key}->{itemcallnumber},
1138                 location => $items->{$key}->{location},
1139                 onloancount      => $items->{$key}->{onloancount},
1140                 due_date         => $items->{$key}->{due_date},
1141                 wthdrawn      => $items->{$key}->{wthdrawn},
1142                 lost         => $items->{$key}->{itemlost},
1143             };
1144                         # only show the number specified by the user
1145                         my $maxitems = (C4::Context->preference('maxItemsinSearchResults')) ? C4::Context->preference('maxItemsinSearchResults')- 1 : 1;
1146             push @items_loop, $this_item unless $itemscount > $maxitems;;
1147         }
1148         $oldbiblio->{norequests}    = $norequests;
1149         $oldbiblio->{items_count}    = $items_count;
1150         $oldbiblio->{items_loop}    = \@items_loop;
1151         $oldbiblio->{onloancount}   = $onloan_count;
1152         $oldbiblio->{wthdrawncount} = $wthdrawn_count;
1153         $oldbiblio->{itemlostcount} = $itemlost_count;
1154         $oldbiblio->{orderedcount}  = $ordered_count;
1155         $oldbiblio->{isbn}          =~ s/-//g; # deleting - in isbn to enable amazon content 
1156         push( @newresults, $oldbiblio );
1157     }
1158     return @newresults;
1159 }
1160
1161
1162
1163 #----------------------------------------------------------------------
1164 #
1165 # Non-Zebra GetRecords#
1166 #----------------------------------------------------------------------
1167
1168 =head2 NZgetRecords
1169
1170   NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1171
1172 =cut
1173 sub NZgetRecords {
1174     my ($query,$simple_query,$sort_by_ref,$servers_ref,$results_per_page,$offset,$expanded_facet,$branches,$query_type,$scan) = @_;
1175     my $result = NZanalyse($query);
1176     return (undef,NZorder($result,@$sort_by_ref[0],$results_per_page,$offset),undef);
1177 }
1178
1179 =head2 NZanalyse
1180
1181   NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1182   the list is built from an inverted index in the nozebra SQL table
1183   note that title is here only for convenience : the sorting will be very fast when requested on title
1184   if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1185
1186 =cut
1187
1188 sub NZanalyse {
1189     my ($string,$server) = @_;
1190     # $server contains biblioserver or authorities, depending on what we search on.
1191     #warn "querying : $string on $server";
1192     $server='biblioserver' unless $server;
1193
1194     # if we have a ", replace the content to discard temporarily any and/or/not inside
1195     my $commacontent;
1196     if ($string =~/"/) {
1197         $string =~ s/"(.*?)"/__X__/;
1198         $commacontent = $1;
1199                 warn "commacontent : $commacontent" if $DEBUG;
1200     }
1201     # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1202     # then, call again NZanalyse with $left and $right
1203     # (recursive until we find a leaf (=> something without and/or/not)
1204     # delete repeated operator... Would then go in infinite loop
1205     while ($string =~s/( and| or| not| AND| OR| NOT)\1/$1/g){
1206     }
1207     #process parenthesis before.   
1208     if ($string =~ /^\s*\((.*)\)(( and | or | not | AND | OR | NOT )(.*))?/){
1209       my $left = $1;
1210 #       warn "left :".$left;   
1211       my $right = $4;
1212       my $operator = lc($3); # FIXME: and/or/not are operators, not operands
1213       my $leftresult = NZanalyse($left,$server);
1214       if ($operator) {
1215         my $rightresult = NZanalyse($right,$server);
1216         # OK, we have the results for right and left part of the query
1217         # depending of operand, intersect, union or exclude both lists
1218         # to get a result list
1219         if ($operator eq ' and ') {
1220             my @leftresult = split /;/, $leftresult;
1221 #             my @rightresult = split /;/,$leftresult;
1222             my $finalresult;
1223             # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1224             # the result is stored twice, to have the same weight for AND than OR.
1225             # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1226             # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1227             foreach (@leftresult) {
1228                 if ($rightresult =~ "$_;") {
1229                     $finalresult .= "$_;$_;";
1230                 }
1231             }
1232             return $finalresult;
1233         } elsif ($operator eq ' or ') {
1234             # just merge the 2 strings
1235             return $leftresult.$rightresult;
1236         } elsif ($operator eq ' not ') {
1237             my @leftresult = split /;/, $leftresult;
1238 #             my @rightresult = split /;/,$leftresult;
1239             my $finalresult;
1240             foreach (@leftresult) {
1241                 unless ($rightresult =~ "$_;") {
1242                     $finalresult .= "$_;";
1243                 }
1244             }
1245             return $finalresult;
1246         } else {
1247             # this error is impossible, because of the regexp that isolate the operand, but just in case...
1248             return $leftresult;
1249             exit;        
1250         }
1251       }   
1252     }  
1253     warn "string :".$string if $DEBUG;
1254     $string =~ /(.*?)( and | or | not | AND | OR | NOT )(.*)/;
1255     my $left = $1;   
1256     my $right = $3;
1257     my $operand = lc($2); # FIXME: and/or/not are operators, not operands
1258     # it's not a leaf, we have a and/or/not
1259     if ($operand) {
1260         # reintroduce comma content if needed
1261         $right =~ s/__X__/"$commacontent"/ if $commacontent;
1262         $left =~ s/__X__/"$commacontent"/ if $commacontent;
1263         warn "node : $left / $operand / $right\n" if $DEBUG;
1264         my $leftresult = NZanalyse($left,$server);
1265         my $rightresult = NZanalyse($right,$server);
1266         # OK, we have the results for right and left part of the query
1267         # depending of operand, intersect, union or exclude both lists
1268         # to get a result list
1269         if ($operand eq ' and ') {
1270             my @leftresult = split /;/, $leftresult;
1271 #             my @rightresult = split /;/,$leftresult;
1272             my $finalresult;
1273             # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1274             # the result is stored twice, to have the same weight for AND than OR.
1275             # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1276             # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1277             foreach (@leftresult) {
1278                 if ($rightresult =~ "$_;") {
1279                     $finalresult .= "$_;$_;";
1280                 }
1281             }
1282             return $finalresult;
1283         } elsif ($operand eq ' or ') {
1284             # just merge the 2 strings
1285             return $leftresult.$rightresult;
1286         } elsif ($operand eq ' not ') {
1287             my @leftresult = split /;/, $leftresult;
1288 #             my @rightresult = split /;/,$leftresult;
1289             my $finalresult;
1290             foreach (@leftresult) {
1291                 unless ($rightresult =~ "$_;") {
1292                     $finalresult .= "$_;";
1293                 }
1294             }
1295             return $finalresult;
1296         } else {
1297             # this error is impossible, because of the regexp that isolate the operand, but just in case...
1298             die "error : operand unknown : $operand for $string";
1299         }
1300     # it's a leaf, do the real SQL query and return the result
1301     } else {
1302         $string =~  s/__X__/"$commacontent"/ if $commacontent;
1303         $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|&|\+|\*|\// /g;
1304         warn "leaf : $string\n" if $DEBUG;
1305         # parse the string in in operator/operand/value again
1306         $string =~ /(.*)(>=|<=)(.*)/;
1307         my $left = $1;
1308         my $operator = $2;
1309         my $right = $3;
1310         unless ($operator) {
1311             $string =~ /(.*)(>|<|=)(.*)/;
1312             $left = $1;
1313             $operator = $2;
1314             $right = $3;
1315         }
1316         my $results;
1317         # strip adv, zebra keywords, currently not handled in nozebra: wrdl, ext, phr...
1318         $left =~ s/[ ,].*$//;
1319         # automatic replace for short operators
1320         $left='title' if $left =~ '^ti$';
1321         $left='author' if $left =~ '^au$';
1322         $left='publisher' if $left =~ '^pb$';
1323         $left='subject' if $left =~ '^su$';
1324         $left='koha-Auth-Number' if $left =~ '^an$';
1325         $left='keyword' if $left =~ '^kw$';
1326         if ($operator) {
1327             #do a specific search
1328             my $dbh = C4::Context->dbh;
1329             $operator='LIKE' if $operator eq '=' and $right=~ /%/;
1330             my $sth = $dbh->prepare("SELECT biblionumbers,value FROM nozebra WHERE server=? AND indexname=? AND value $operator ?");
1331             warn "$left / $operator / $right\n";
1332             # split each word, query the DB and build the biblionumbers result
1333             #sanitizing leftpart      
1334             $left=~s/^\s+|\s+$//;
1335             my ($biblionumbers,$value);
1336             foreach (split / /,$right) {
1337                 next unless $_;
1338                 warn "EXECUTE : $server, $left, $_";
1339                 $sth->execute($server, $left, $_) or warn "execute failed: $!";
1340                 while (my ($line,$value) = $sth->fetchrow) {
1341                     # if we are dealing with a numeric value, use only numeric results (in case of >=, <=, > or <)
1342                     # otherwise, fill the result
1343                     $biblionumbers .= $line unless ($right =~ /\d/ && $value =~ /\D/);
1344 #                     warn "result : $value ". ($right =~ /\d/) . "==".(!$value =~ /\d/) ;#= $line";
1345                 }
1346                 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1347                 if ($results) {
1348                     my @leftresult = split /;/, $biblionumbers;
1349                     my $temp;
1350                     foreach my $entry (@leftresult) { # $_ contains biblionumber,title-weight
1351                         # remove weight at the end
1352                         my $cleaned = $entry;
1353                         $cleaned =~ s/-\d*$//;
1354                         # if the entry already in the hash, take it & increase weight
1355                          warn "===== $cleaned =====" if $DEBUG;
1356                         if ($results =~ "$cleaned") {
1357                             $temp .= "$entry;$entry;";
1358                              warn "INCLUDING $entry" if $DEBUG;
1359                         }
1360                     }
1361                     $results = $temp;
1362                 } else {
1363                     $results = $biblionumbers;
1364                 }
1365             }
1366         } else {
1367             #do a complete search (all indexes)
1368             my $dbh = C4::Context->dbh;
1369             my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?");
1370             # split each word, query the DB and build the biblionumbers result
1371             foreach (split / /,$string) {
1372                 next if C4::Context->stopwords->{uc($_)}; # skip if stopword
1373                 warn "search on all indexes on $_" if $DEBUG;
1374                 my $biblionumbers;
1375                 next unless $_;
1376                 $sth->execute($server, $_);
1377                 while (my $line = $sth->fetchrow) {
1378                     $biblionumbers .= $line;
1379                 }
1380                 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1381                 if ($results) {
1382                  warn "RES for $_ = $biblionumbers" if $DEBUG;
1383                     my @leftresult = split /;/, $biblionumbers;
1384                     my $temp;
1385                     foreach my $entry (@leftresult) { # $_ contains biblionumber,title-weight
1386                         # remove weight at the end
1387                         my $cleaned = $entry;
1388                         $cleaned =~ s/-\d*$//;
1389                         # if the entry already in the hash, take it & increase weight
1390                          warn "===== $cleaned =====" if $DEBUG;
1391                         if ($results =~ "$cleaned") {
1392                             $temp .= "$entry;$entry;";
1393                              warn "INCLUDING $entry" if $DEBUG;
1394                         }
1395                     }
1396                     $results = $temp;
1397                 } else {
1398                  warn "NEW RES for $_ = $biblionumbers" if $DEBUG;
1399                     $results = $biblionumbers;
1400                 }
1401             }
1402         }
1403 #         warn "return : $results for LEAF : $string" if $DEBUG;
1404         return $results;
1405     }
1406 }
1407
1408 =head2 NZorder
1409
1410   $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
1411   
1412   TODO :: Description
1413
1414 =cut
1415
1416
1417 sub NZorder {
1418     my ($biblionumbers, $ordering,$results_per_page,$offset) = @_;
1419     # order title asc by default
1420 #     $ordering = '1=36 <i' unless $ordering;
1421     $results_per_page=20 unless $results_per_page;
1422     $offset = 0 unless $offset;
1423     my $dbh = C4::Context->dbh;
1424     #
1425     # order by POPULARITY
1426     #
1427     if ($ordering =~ /popularity/) {
1428         my %result;
1429         my %popularity;
1430         # popularity is not in MARC record, it's builded from a specific query
1431         my $sth = $dbh->prepare("select sum(issues) from items where biblionumber=?");
1432         foreach (split /;/,$biblionumbers) {
1433             my ($biblionumber,$title) = split /,/,$_;
1434             $result{$biblionumber}=GetMarcBiblio($biblionumber);
1435             $sth->execute($biblionumber);
1436             my $popularity= $sth->fetchrow ||0;
1437             # hint : the key is popularity.title because we can have
1438             # many results with the same popularity. In this cas, sub-ordering is done by title
1439             # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
1440             # (un-frequent, I agree, but we won't forget anything that way ;-)
1441             $popularity{sprintf("%10d",$popularity).$title.$biblionumber} = $biblionumber;
1442         }
1443         # sort the hash and return the same structure as GetRecords (Zebra querying)
1444         my $result_hash;
1445         my $numbers=0;
1446         if ($ordering eq 'popularity_dsc') { # sort popularity DESC
1447             foreach my $key (sort {$b cmp $a} (keys %popularity)) {
1448                 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1449             }
1450         } else { # sort popularity ASC
1451             foreach my $key (sort (keys %popularity)) {
1452                 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1453             }
1454         }
1455         my $finalresult=();
1456         $result_hash->{'hits'} = $numbers;
1457         $finalresult->{'biblioserver'} = $result_hash;
1458         return $finalresult;
1459     #
1460     # ORDER BY author
1461     #
1462     } elsif ($ordering =~/author/){
1463         my %result;
1464         foreach (split /;/,$biblionumbers) {
1465             my ($biblionumber,$title) = split /,/,$_;
1466             my $record=GetMarcBiblio($biblionumber);
1467             my $author;
1468             if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1469                 $author=$record->subfield('200','f');
1470                 $author=$record->subfield('700','a') unless $author;
1471             } else {
1472                 $author=$record->subfield('100','a');
1473             }
1474             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1475             # and we don't want to get only 1 result for each of them !!!
1476             $result{$author.$biblionumber}=$record;
1477         }
1478         # sort the hash and return the same structure as GetRecords (Zebra querying)
1479         my $result_hash;
1480         my $numbers=0;
1481         if ($ordering eq 'author_za') { # sort by author desc
1482             foreach my $key (sort { $b cmp $a } (keys %result)) {
1483                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1484             }
1485         } else { # sort by author ASC
1486             foreach my $key (sort (keys %result)) {
1487                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1488             }
1489         }
1490         my $finalresult=();
1491         $result_hash->{'hits'} = $numbers;
1492         $finalresult->{'biblioserver'} = $result_hash;
1493         return $finalresult;
1494     #
1495     # ORDER BY callnumber
1496     #
1497     } elsif ($ordering =~/callnumber/){
1498         my %result;
1499         foreach (split /;/,$biblionumbers) {
1500             my ($biblionumber,$title) = split /,/,$_;
1501             my $record=GetMarcBiblio($biblionumber);
1502             my $callnumber;
1503             my ($callnumber_tag,$callnumber_subfield)=GetMarcFromKohaField($dbh,'items.itemcallnumber');
1504             ($callnumber_tag,$callnumber_subfield)= GetMarcFromKohaField('biblioitems.callnumber') unless $callnumber_tag;
1505             if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1506                 $callnumber=$record->subfield('200','f');
1507             } else {
1508                 $callnumber=$record->subfield('100','a');
1509             }
1510             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1511             # and we don't want to get only 1 result for each of them !!!
1512             $result{$callnumber.$biblionumber}=$record;
1513         }
1514         # sort the hash and return the same structure as GetRecords (Zebra querying)
1515         my $result_hash;
1516         my $numbers=0;
1517         if ($ordering eq 'call_number_dsc') { # sort by title desc
1518             foreach my $key (sort { $b cmp $a } (keys %result)) {
1519                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1520             }
1521         } else { # sort by title ASC
1522             foreach my $key (sort { $a cmp $b } (keys %result)) {
1523                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1524             }
1525         }
1526         my $finalresult=();
1527         $result_hash->{'hits'} = $numbers;
1528         $finalresult->{'biblioserver'} = $result_hash;
1529         return $finalresult;
1530     } elsif ($ordering =~ /pubdate/){ #pub year
1531         my %result;
1532         foreach (split /;/,$biblionumbers) {
1533             my ($biblionumber,$title) = split /,/,$_;
1534             my $record=GetMarcBiblio($biblionumber);
1535             my ($publicationyear_tag,$publicationyear_subfield)=GetMarcFromKohaField('biblioitems.publicationyear','');
1536             my $publicationyear=$record->subfield($publicationyear_tag,$publicationyear_subfield);
1537             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1538             # and we don't want to get only 1 result for each of them !!!
1539             $result{$publicationyear.$biblionumber}=$record;
1540         }
1541         # sort the hash and return the same structure as GetRecords (Zebra querying)
1542         my $result_hash;
1543         my $numbers=0;
1544         if ($ordering eq 'pubdate_dsc') { # sort by pubyear desc
1545             foreach my $key (sort { $b cmp $a } (keys %result)) {
1546                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1547             }
1548         } else { # sort by pub year ASC
1549             foreach my $key (sort (keys %result)) {
1550                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1551             }
1552         }
1553         my $finalresult=();
1554         $result_hash->{'hits'} = $numbers;
1555         $finalresult->{'biblioserver'} = $result_hash;
1556         return $finalresult;
1557     #
1558     # ORDER BY title
1559     #
1560     } elsif ($ordering =~ /title/) { 
1561         # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
1562         my %result;
1563         foreach (split /;/,$biblionumbers) {
1564             my ($biblionumber,$title) = split /,/,$_;
1565             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1566             # and we don't want to get only 1 result for each of them !!!
1567             # hint & speed improvement : we can order without reading the record
1568             # so order, and read records only for the requested page !
1569             $result{$title.$biblionumber}=$biblionumber;
1570         }
1571         # sort the hash and return the same structure as GetRecords (Zebra querying)
1572         my $result_hash;
1573         my $numbers=0;
1574         if ($ordering eq 'title_az') { # sort by title desc
1575             foreach my $key (sort (keys %result)) {
1576                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1577             }
1578         } else { # sort by title ASC
1579             foreach my $key (sort { $b cmp $a } (keys %result)) {
1580                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1581             }
1582         }
1583         # limit the $results_per_page to result size if it's more
1584         $results_per_page = $numbers-1 if $numbers < $results_per_page;
1585         # for the requested page, replace biblionumber by the complete record
1586         # speed improvement : avoid reading too much things
1587         for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1588             $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1589         }
1590         my $finalresult=();
1591         $result_hash->{'hits'} = $numbers;
1592         $finalresult->{'biblioserver'} = $result_hash;
1593         return $finalresult;
1594     } else {
1595     #
1596     # order by ranking
1597     #
1598         # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
1599         my %result;
1600         my %count_ranking;
1601         foreach (split /;/,$biblionumbers) {
1602             my ($biblionumber,$title) = split /,/,$_;
1603             $title =~ /(.*)-(\d)/;
1604             # get weight 
1605             my $ranking =$2;
1606             # note that we + the ranking because ranking is calculated on weight of EACH term requested.
1607             # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
1608             # biblio N has ranking = 6
1609             $count_ranking{$biblionumber} += $ranking;
1610         }
1611         # build the result by "inverting" the count_ranking hash
1612         # hing : as usual, we don't order by ranking only, to avoid having only 1 result for each rank. We build an hash on concat(ranking,biblionumber) instead
1613 #         warn "counting";
1614         foreach (keys %count_ranking) {
1615             $result{sprintf("%10d",$count_ranking{$_}).'-'.$_} = $_;
1616         }
1617         # sort the hash and return the same structure as GetRecords (Zebra querying)
1618         my $result_hash;
1619         my $numbers=0;
1620             foreach my $key (sort {$b cmp $a} (keys %result)) {
1621                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1622             }
1623         # limit the $results_per_page to result size if it's more
1624         $results_per_page = $numbers-1 if $numbers < $results_per_page;
1625         # for the requested page, replace biblionumber by the complete record
1626         # speed improvement : avoid reading too much things
1627         for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1628             $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc if $result_hash->{'RECORDS'}[$counter];
1629         }
1630         my $finalresult=();
1631         $result_hash->{'hits'} = $numbers;
1632         $finalresult->{'biblioserver'} = $result_hash;
1633         return $finalresult;
1634     }
1635 }
1636 =head2 ModBiblios
1637
1638 ($countchanged,$listunchanged) = ModBiblios($listbiblios, $tagsubfield,$initvalue,$targetvalue,$test);
1639
1640 this function changes all the values $initvalue in subfield $tag$subfield in any record in $listbiblios
1641 test parameter if set donot perform change to records in database.
1642
1643 =over 2
1644
1645 =item C<input arg:>
1646
1647     * $listbiblios is an array ref to marcrecords to be changed
1648     * $tagsubfield is the reference of the subfield to change.
1649     * $initvalue is the value to search the record for
1650     * $targetvalue is the value to set the subfield to
1651     * $test is to be set only not to perform changes in database.
1652
1653 =item C<Output arg:>
1654     * $countchanged counts all the changes performed.
1655     * $listunchanged contains the list of all the biblionumbers of records unchanged.
1656
1657 =item C<usage in the script:>
1658
1659 =back
1660
1661 my ($countchanged, $listunchanged) = EditBiblios($results->{RECORD}, $tagsubfield,$initvalue,$targetvalue);;
1662 #If one wants to display unchanged records, you should get biblios foreach @$listunchanged 
1663 $template->param(countchanged => $countchanged, loopunchanged=>$listunchanged);
1664
1665 =cut
1666
1667 sub ModBiblios{
1668   my ($listbiblios,$tagsubfield,$initvalue,$targetvalue,$test)=@_;
1669   my $countmatched;
1670   my @unmatched;
1671   my ($tag,$subfield)=($1,$2) if ($tagsubfield=~/^(\d{1,3})([a-z0-9A-Z@])?$/); 
1672   if ((length($tag)<3)&& $subfield=~/0-9/){
1673     $tag=$tag.$subfield;
1674     undef $subfield;
1675   } 
1676   my ($bntag,$bnsubf) = GetMarcFromKohaField('biblio.biblionumber');
1677   my ($itemtag,$itemsubf) = GetMarcFromKohaField('items.itemnumber');
1678   foreach my $usmarc (@$listbiblios){
1679     my $record; 
1680     $record=eval{MARC::Record->new_from_usmarc($usmarc)};
1681     my $biblionumber;
1682     if ($@){
1683       # usmarc is not a valid usmarc May be a biblionumber
1684       if ($tag eq $itemtag){
1685         my $bib=GetBiblioFromItemNumber($usmarc);   
1686         $record=GetMarcItem($bib->{'biblionumber'},$usmarc) ;   
1687         $biblionumber=$bib->{'biblionumber'};
1688       } else {   
1689         $record=GetMarcBiblio($usmarc);   
1690         $biblionumber=$usmarc;
1691       }   
1692     }  else {
1693       if ($bntag >= 010){
1694         $biblionumber = $record->subfield($bntag,$bnsubf);
1695       }else {
1696         $biblionumber=$record->field($bntag)->data;
1697       }
1698     }  
1699     #GetBiblionumber is to be written.
1700     #Could be replaced by TransformMarcToKoha (But Would be longer)
1701     if ($record->field($tag)){
1702       my $modify=0;  
1703       foreach my $field ($record->field($tag)){
1704         if ($subfield){
1705           if ($field->delete_subfield('code' =>$subfield,'match'=>qr($initvalue))){
1706             $countmatched++;
1707             $modify=1;      
1708             $field->update($subfield,$targetvalue) if ($targetvalue);
1709           }
1710         } else {
1711           if ($tag >= 010){
1712             if ($field->delete_field($field)){
1713               $countmatched++;
1714               $modify=1;      
1715             }
1716           } else {
1717             $field->data=$targetvalue if ($field->data=~qr($initvalue));
1718           }     
1719         }    
1720       }
1721 #       warn $record->as_formatted;
1722       if ($modify){
1723         ModBiblio($record,$biblionumber,GetFrameworkCode($biblionumber)) unless ($test);
1724       } else {
1725         push @unmatched, $biblionumber;   
1726       }      
1727     } else {
1728       push @unmatched, $biblionumber;
1729     }
1730   }
1731   return ($countmatched,\@unmatched);
1732 }
1733
1734 END { }    # module clean-up code here (global destructor)
1735
1736 1;
1737 __END__
1738
1739 =head1 AUTHOR
1740
1741 Koha Developement team <info@koha.org>
1742
1743 =cut