adding short keyword support (ti for title, au for author, some still to do)
[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
25 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
26
27 # set the version for version checking
28 $VERSION = do { my @v = '$Revision$' =~ /\d+/g;
29     shift(@v) . "." . join( "_", map { sprintf "%03d", $_ } @v );
30 };
31
32 =head1 NAME
33
34 C4::Search - Functions for searching the Koha catalog.
35
36 =head1 SYNOPSIS
37
38 see opac/opac-search.pl or catalogue/search.pl for example of usage
39
40 =head1 DESCRIPTION
41
42 This module provides the searching facilities for the Koha into a zebra catalog.
43
44 =head1 FUNCTIONS
45
46 =cut
47
48 @ISA    = qw(Exporter);
49 @EXPORT = qw(
50   &SimpleSearch
51   &findseealso
52   &FindDuplicate
53   &searchResults
54   &getRecords
55   &buildQuery
56   &NZgetRecords
57   &EditBiblios
58 );
59
60 # make all your functions, whether exported or not;
61
62 =head2 findseealso($dbh,$fields);
63
64 C<$dbh> is a link to the DB handler.
65
66 use C4::Context;
67 my $dbh =C4::Context->dbh;
68
69 C<$fields> is a reference to the fields array
70
71 This function modify the @$fields array and add related fields to search on.
72
73 =cut
74
75 sub findseealso {
76     my ( $dbh, $fields ) = @_;
77     my $tagslib = GetMarcStructure( $dbh, 1 );
78     for ( my $i = 0 ; $i <= $#{$fields} ; $i++ ) {
79         my ($tag)      = substr( @$fields[$i], 1, 3 );
80         my ($subfield) = substr( @$fields[$i], 4, 1 );
81         @$fields[$i] .= ',' . $tagslib->{$tag}->{$subfield}->{seealso}
82           if ( $tagslib->{$tag}->{$subfield}->{seealso} );
83     }
84 }
85
86 =head2 FindDuplicate
87
88 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
89
90 =cut
91
92 sub FindDuplicate {
93     my ($record) = @_;
94     return;
95     my $dbh = C4::Context->dbh;
96     my $result = TransformMarcToKoha( $dbh, $record, '' );
97     my $sth;
98     my $query;
99     my $search;
100     my $type;
101     my ( $biblionumber, $title );
102
103     # search duplicate on ISBN, easy and fast..
104     #$search->{'avoidquerylog'}=1;
105     if ( $result->{isbn} ) {
106         $query = "isbn=$result->{isbn}";
107     }
108     else {
109         $result->{title} =~ s /\\//g;
110         $result->{title} =~ s /\"//g;
111         $result->{title} =~ s /\(//g;
112         $result->{title} =~ s /\)//g;
113         $query = "ti,ext=$result->{title}";
114     }
115     my ($possible_duplicate_record) =
116       C4::Biblio::getRecord( "biblioserver", $query, "usmarc" ); # FIXME :: hardcoded !
117     if ($possible_duplicate_record) {
118         my $marcrecord =
119           MARC::Record->new_from_usmarc($possible_duplicate_record);
120         my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
121         
122         # FIXME :: why 2 $biblionumber ?
123         return $result->{'biblionumber'}, $result->{'biblionumber'},
124           $result->{'title'}
125           if $result;
126     }
127 }
128
129 =head2 SimpleSearch
130
131 ($error,$results) = SimpleSearch($query,@servers);
132
133 this function performs a simple search on the catalog using zoom.
134
135 =over 2
136
137 =item C<input arg:>
138
139     * $query could be a simple keyword or a complete CCL query wich is depending on your ccl file.
140     * @servers is optionnal. default one is read on koha.xml
141
142 =item C<Output arg:>
143     * $error is a string which containt the description error if there is one. Else it's empty.
144     * \@results is an array of marc record.
145
146 =item C<usage in the script:>
147
148 =back
149
150 my ($error, $marcresults) = SimpleSearch($query);
151
152 if (defined $error) {
153     $template->param(query_error => $error);
154     warn "error: ".$error;
155     output_html_with_http_headers $input, $cookie, $template->output;
156     exit;
157 }
158
159 my $hits = scalar @$marcresults;
160 my @results;
161
162 for(my $i=0;$i<$hits;$i++) {
163     my %resultsloop;
164     my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
165     my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
166
167     #build the hash for the template.
168     $resultsloop{highlight}       = ($i % 2)?(1):(0);
169     $resultsloop{title}           = $biblio->{'title'};
170     $resultsloop{subtitle}        = $biblio->{'subtitle'};
171     $resultsloop{biblionumber}    = $biblio->{'biblionumber'};
172     $resultsloop{author}          = $biblio->{'author'};
173     $resultsloop{publishercode}   = $biblio->{'publishercode'};
174     $resultsloop{publicationyear} = $biblio->{'publicationyear'};
175
176     push @results, \%resultsloop;
177 }
178 $template->param(result=>\@results);
179
180 =cut
181
182 sub SimpleSearch {
183     my $query   = shift;
184     if (C4::Context->preference('NoZebra')) {
185         my $result = NZorder(NZanalyse($query))->{'biblioserver'}->{'RECORDS'};
186         use Data::Dumper;
187         foreach (@$result) {
188             warn "$query :".@_;
189         }
190         return (undef,$result);
191     } else {
192         my @servers = @_;
193         my @results;
194         my @tmpresults;
195         my @zconns;
196         return ( "No query entered", undef ) unless $query;
197     
198         #@servers = (C4::Context->config("biblioserver")) unless @servers;
199         @servers =
200         ("biblioserver") unless @servers
201         ;    # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
202     
203         # Connect & Search
204         for ( my $i = 0 ; $i < @servers ; $i++ ) {
205             $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
206             $tmpresults[$i] =
207             $zconns[$i]
208             ->search( new ZOOM::Query::CCL2RPN( $query, $zconns[$i] ) );
209     
210             # getting error message if one occured.
211             my $error =
212                 $zconns[$i]->errmsg() . " ("
213             . $zconns[$i]->errcode() . ") "
214             . $zconns[$i]->addinfo() . " "
215             . $zconns[$i]->diagset();
216     
217             return ( $error, undef ) if $zconns[$i]->errcode();
218         }
219         my $hits;
220         my $ev;
221         while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
222             $ev = $zconns[ $i - 1 ]->last_event();
223             if ( $ev == ZOOM::Event::ZEND ) {
224                 $hits = $tmpresults[ $i - 1 ]->size();
225             }
226             if ( $hits > 0 ) {
227                 for ( my $j = 0 ; $j < $hits ; $j++ ) {
228                     my $record = $tmpresults[ $i - 1 ]->record($j)->raw();
229                     push @results, $record;
230                 }
231             }
232         }
233         return ( undef, \@results );
234     }
235 }
236
237 # performs the search
238 sub getRecords {
239     my (
240         $koha_query,     $federated_query,  $sort_by_ref,
241         $servers_ref,    $results_per_page, $offset,
242         $expanded_facet, $branches,         $query_type,
243         $scan
244     ) = @_;
245
246     my @servers = @$servers_ref;
247     my @sort_by = @$sort_by_ref;
248
249     # create the zoom connection and query object
250     my $zconn;
251     my @zconns;
252     my @results;
253     my $results_hashref = ();
254
255     ### FACETED RESULTS
256     my $facets_counter = ();
257     my $facets_info    = ();
258     my $facets         = getFacets();
259
260     #### INITIALIZE SOME VARS USED CREATE THE FACETED RESULTS
261     my @facets_loop;    # stores the ref to array of hashes for template
262     for ( my $i = 0 ; $i < @servers ; $i++ ) {
263         $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
264
265 # perform the search, create the results objects
266 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
267         my $query_to_use;
268         if ( $servers[$i] =~ /biblioserver/ ) {
269             $query_to_use = $koha_query;
270         }
271         else {
272             $query_to_use = $federated_query;
273         }
274
275         #          warn "HERE : $query_type => $query_to_use";
276         # check if we've got a query_type defined
277         eval {
278             if ($query_type)
279             {
280                 if ( $query_type =~ /^ccl/ ) {
281                     $query_to_use =~
282                       s/\:/\=/g;    # change : to = last minute (FIXME)
283
284                     #                 warn "CCL : $query_to_use";
285                     $results[$i] =
286                       $zconns[$i]->search(
287                         new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
288                       );
289                 }
290                 elsif ( $query_type =~ /^cql/ ) {
291
292                     #                 warn "CQL : $query_to_use";
293                     $results[$i] =
294                       $zconns[$i]->search(
295                         new ZOOM::Query::CQL( $query_to_use, $zconns[$i] ) );
296                 }
297                 elsif ( $query_type =~ /^pqf/ ) {
298
299                     #                 warn "PQF : $query_to_use";
300                     $results[$i] =
301                       $zconns[$i]->search(
302                         new ZOOM::Query::PQF( $query_to_use, $zconns[$i] ) );
303                 }
304             }
305             else {
306                 if ($scan) {
307
308                     #                 warn "preparing to scan";
309                     $results[$i] =
310                       $zconns[$i]->scan(
311                         new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
312                       );
313                 }
314                 else {
315
316                     #             warn "LAST : $query_to_use";
317                     $results[$i] =
318                       $zconns[$i]->search(
319                         new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
320                       );
321                 }
322             }
323         };
324         if ($@) {
325             warn "prob with query  toto $query_to_use " . $@;
326         }
327
328         # concatenate the sort_by limits and pass them to the results object
329         my $sort_by;
330         foreach my $sort (@sort_by) {
331             $sort_by .= $sort . " ";    # used to be $sort,
332         }
333         $results[$i]->sort( "yaz", $sort_by ) if $sort_by;
334     }
335     while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
336         my $ev = $zconns[ $i - 1 ]->last_event();
337         if ( $ev == ZOOM::Event::ZEND ) {
338             my $size = $results[ $i - 1 ]->size();
339             if ( $size > 0 ) {
340                 my $results_hash;
341                 #$results_hash->{'server'} = $servers[$i-1];
342                 # loop through the results
343                 $results_hash->{'hits'} = $size;
344                 my $times;
345                 if ( $offset + $results_per_page <= $size ) {
346                     $times = $offset + $results_per_page;
347                 }
348                 else {
349                     $times = $size;
350                 }
351                 for ( my $j = $offset ; $j < $times ; $j++ )
352                 {   #(($offset+$count<=$size) ? ($offset+$count):$size) ; $j++){
353                     my $records_hash;
354                     my $record;
355                     my $facet_record;
356                     ## This is just an index scan
357                     if ($scan) {
358                         my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
359
360                  # here we create a minimal MARC record and hand it off to the
361                  # template just like a normal result ... perhaps not ideal, but
362                  # it works for now
363                         my $tmprecord = MARC::Record->new();
364                         $tmprecord->encoding('UTF-8');
365                         my $tmptitle;
366
367           # srote the minimal record in author/title (depending on MARC flavour)
368                         if ( C4::Context->preference("marcflavour") eq
369                             "UNIMARC" )
370                         {
371                             $tmptitle = MARC::Field->new(
372                                 '200', ' ', ' ',
373                                 a => $term,
374                                 f => $occ
375                             );
376                         }
377                         else {
378                             $tmptitle = MARC::Field->new(
379                                 '245', ' ', ' ',
380                                 a => $term,
381                                 b => $occ
382                             );
383                         }
384                         $tmprecord->append_fields($tmptitle);
385                         $results_hash->{'RECORDS'}[$j] =
386                           $tmprecord->as_usmarc();
387                     }
388                     else {
389                         $record = $results[ $i - 1 ]->record($j)->raw();
390
391                         #warn "RECORD $j:".$record;
392                         $results_hash->{'RECORDS'}[$j] =
393                           $record;    # making a reference to a hash
394                                       # Fill the facets while we're looping
395                         $facet_record = MARC::Record->new_from_usmarc($record);
396
397                         #warn $servers[$i-1].$facet_record->title();
398                         for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
399                             if ( $facets->[$k] ) {
400                                 my @fields;
401                                 for my $tag ( @{ $facets->[$k]->{'tags'} } ) {
402                                     push @fields, $facet_record->field($tag);
403                                 }
404                                 for my $field (@fields) {
405                                     my @subfields = $field->subfields();
406                                     for my $subfield (@subfields) {
407                                         my ( $code, $data ) = @$subfield;
408                                         if ( $code eq
409                                             $facets->[$k]->{'subfield'} )
410                                         {
411                                             $facets_counter->{ $facets->[$k]
412                                                   ->{'link_value'} }->{$data}++;
413                                         }
414                                     }
415                                 }
416                                 $facets_info->{ $facets->[$k]->{'link_value'} }
417                                   ->{'label_value'} =
418                                   $facets->[$k]->{'label_value'};
419                                 $facets_info->{ $facets->[$k]->{'link_value'} }
420                                   ->{'expanded'} = $facets->[$k]->{'expanded'};
421                             }
422                         }
423                     }
424                 }
425                 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
426             }
427
428             #print "connection ", $i-1, ": $size hits";
429             #print $results[$i-1]->record(0)->render() if $size > 0;
430             # BUILD FACETS
431             for my $link_value (
432                 sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
433                 keys %$facets_counter
434               )
435             {
436                 my $expandable;
437                 my $number_of_facets;
438                 my @this_facets_array;
439                 for my $one_facet (
440                     sort {
441                         $facets_counter->{$link_value}
442                           ->{$b} <=> $facets_counter->{$link_value}->{$a}
443                     } keys %{ $facets_counter->{$link_value} }
444                   )
445                 {
446                     $number_of_facets++;
447                     if (   ( $number_of_facets < 6 )
448                         || ( $expanded_facet eq $link_value )
449                         || ( $facets_info->{$link_value}->{'expanded'} ) )
450                     {
451
452                        # sanitize the link value ), ( will cause errors with CCL
453                         my $facet_link_value = $one_facet;
454                         $facet_link_value =~ s/(\(|\))/ /g;
455
456                         # fix the length that will display in the label
457                         my $facet_label_value = $one_facet;
458                         $facet_label_value = substr( $one_facet, 0, 20 ) . "..."
459                           unless length($facet_label_value) <= 20;
460
461                        # well, if it's a branch, label by the name, not the code
462                         if ( $link_value =~ /branch/ ) {
463                             $facet_label_value =
464                               $branches->{$one_facet}->{'branchname'};
465                         }
466
467                  # but we're down with the whole label being in the link's title
468                         my $facet_title_value = $one_facet;
469
470                         push @this_facets_array,
471                           (
472                             {
473                                 facet_count =>
474                                   $facets_counter->{$link_value}->{$one_facet},
475                                 facet_label_value => $facet_label_value,
476                                 facet_title_value => $facet_title_value,
477                                 facet_link_value  => $facet_link_value,
478                                 type_link_value   => $link_value,
479                             },
480                           );
481                     }
482                 }
483                 unless ( $facets_info->{$link_value}->{'expanded'} ) {
484                     $expandable = 1
485                       if ( ( $number_of_facets > 6 )
486                         && ( $expanded_facet ne $link_value ) );
487                 }
488                 push @facets_loop,
489                   (
490                     {
491                         type_link_value => $link_value,
492                         type_id         => $link_value . "_id",
493                         type_label      =>
494                           $facets_info->{$link_value}->{'label_value'},
495                         facets     => \@this_facets_array,
496                         expandable => $expandable,
497                         expand     => $link_value,
498                     }
499                   );
500             }
501         }
502     }
503     use Data::Dumper;
504     warn Dumper($results_hashref);
505     return ( undef, $results_hashref, \@facets_loop );
506 }
507
508 # build the query itself
509 sub buildQuery {
510     my ( $query, $operators, $operands, $indexes, $limits, $sort_by ) = @_;
511
512     my @operators = @$operators if $operators;
513     my @indexes   = @$indexes   if $indexes;
514     my @operands  = @$operands  if $operands;
515     my @limits    = @$limits    if $limits;
516     my @sort_by   = @$sort_by   if $sort_by;
517
518     my $human_search_desc;      # a human-readable query
519     my $machine_search_desc;    #a machine-readable query
520         # FIXME: the locale should be set based on the syspref
521     my $stemmer = Lingua::Stem->new( -locale => 'EN-US' );
522
523 # FIXME: these should be stored in the db so the librarian can modify the behavior
524     $stemmer->add_exceptions(
525         {
526             'and' => 'and',
527             'or'  => 'or',
528             'not' => 'not',
529         }
530     );
531
532 # STEP I: determine if this is a form-based / simple query or if it's complex (if complex,
533 # we can't handle field weighting, stemming until a formal query parser is written
534 # I'll work on this soon -- JF
535 #if (!$query) { # form-based
536 # check if this is a known query language query, if it is, return immediately:
537     if ( $query =~ /^ccl=/ ) {
538         return ( undef, $', $', $', 'ccl' );
539     }
540     if ( $query =~ /^cql=/ ) {
541         return ( undef, $', $', $', 'cql' );
542     }
543     if ( $query =~ /^pqf=/ ) {
544         return ( undef, $', $', $', 'pqf' );
545     }
546     if ( $query =~ /(\(|\))/ ) {    # sorry, too complex
547         return ( undef, $query, $query, $query, 'ccl' );
548     }
549
550 # form-based queries are limited to non-nested a specific depth, so we can easily
551 # modify the incoming query operands and indexes to do stemming and field weighting
552 # Once we do so, we'll end up with a value in $query, just like if we had an
553 # incoming $query from the user
554     else {
555         $query = ""
556           ; # clear it out so we can populate properly with field-weighted stemmed query
557         my $previous_operand
558           ;    # a flag used to keep track if there was a previous query
559                # if there was, we can apply the current operator
560         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
561             my $operand = $operands[$i];
562             my $index   = $indexes[$i];
563             my $stemmed_operand;
564             my $stemming      = C4::Context->parameters("Stemming")     || 0;
565             my $weight_fields = C4::Context->parameters("WeightFields") || 0;
566
567             if ( $operands[$i] ) {
568
569 # STEMMING FIXME: need to refine the field weighting so stemmed operands don't disrupt the query ranking
570                 if ($stemming) {
571                     my @words = split( / /, $operands[$i] );
572                     my $stems = $stemmer->stem(@words);
573                     foreach my $stem (@$stems) {
574                         $stemmed_operand .= "$stem";
575                         $stemmed_operand .= "?"
576                           unless ( $stem =~ /(and$|or$|not$)/ )
577                           || ( length($stem) < 3 );
578                         $stemmed_operand .= " ";
579
580                         #warn "STEM: $stemmed_operand";
581                     }
582
583                     #$operand = $stemmed_operand;
584                 }
585
586 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
587 # pretty well but will work much better when we have an actual query parser
588                 my $weighted_query;
589                 if ($weight_fields) {
590                     $weighted_query .=
591                       " rk=(";    # Specifies that we're applying rank
592                                   # keyword has different weight properties
593                     if ( ( $index =~ /kw/ ) || ( !$index ) )
594                     { # FIXME: do I need to add right-truncation in the case of stemming?
595                           # a simple way to find out if this query uses an index
596                         if ( $operand =~ /(\=|\:)/ ) {
597                             $weighted_query .= " $operand";
598                         }
599                         else {
600                             $weighted_query .=
601                               " Title-cover,ext,r1=\"$operand\""
602                               ;    # index label as exact
603                             $weighted_query .=
604                               " or ti,ext,r2=$operand";    # index as exact
605                              #$weighted_query .= " or ti,phr,r3=$operand";              # index as  phrase
606                              #$weighted_query .= " or any,ext,r4=$operand";         # index as exact
607                             $weighted_query .=
608                               " or kw,wrdl,r5=$operand";    # index as exact
609                             $weighted_query .= " or wrd,fuzzy,r9=$operand";
610                             $weighted_query .= " or wrd=$stemmed_operand"
611                               if $stemming;
612                         }
613                     }
614                     elsif ( $index =~ /au/ ) {
615                         $weighted_query .=
616                           " $index,ext,r1=$operand";    # index label as exact
617                          #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
618                         $weighted_query .=
619                           " or $index,phr,r3=$operand";    # index as phrase
620                         $weighted_query .= " or $index,rt,wrd,r3=$operand";
621                     }
622                     elsif ( $index =~ /ti/ ) {
623                         $weighted_query .=
624                           " Title-cover,ext,r1=$operand"; # index label as exact
625                         $weighted_query .= " or Title-series,ext,r2=$operand";
626
627                         #$weighted_query .= " or ti,ext,r2=$operand";
628                         #$weighted_query .= " or ti,phr,r3=$operand";
629                         #$weighted_query .= " or ti,wrd,r3=$operand";
630                         $weighted_query .=
631 " or (title-sort-az=0 or Title-cover,startswithnt,st-word,r3=$operand #)";
632                         $weighted_query .=
633 " or (title-sort-az=0 or Title-cover,phr,r6=$operand)";
634
635                         #$weighted_query .= " or Title-cover,wrd,r5=$operand";
636                         #$weighted_query .= " or ti,ext,r6=$operand";
637                         #$weighted_query .= " or ti,startswith,phr,r7=$operand";
638                         #$weighted_query .= " or ti,phr,r8=$operand";
639                         #$weighted_query .= " or ti,wrd,r9=$operand";
640
641    #$weighted_query .= " or ti,ext,r2=$operand";         # index as exact
642    #$weighted_query .= " or ti,phr,r3=$operand";              # index as  phrase
643    #$weighted_query .= " or any,ext,r4=$operand";         # index as exact
644    #$weighted_query .= " or kw,wrd,r5=$operand";         # index as exact
645                     }
646                     else {
647                         $weighted_query .=
648                           " $index,ext,r1=$operand";    # index label as exact
649                          #$weighted_query .= " or $index,ext,r2=$operand";            # index as exact
650                         $weighted_query .=
651                           " or $index,phr,r3=$operand";    # index as phrase
652                         $weighted_query .= " or $index,rt,wrd,r3=$operand";
653                         $weighted_query .=
654                           " or $index,wrd,r5=$operand"
655                           ;    # index as word right-truncated
656                         $weighted_query .= " or $index,wrd,fuzzy,r8=$operand";
657                     }
658                     $weighted_query .= ")";    # close rank specification
659                     $operand = $weighted_query;
660                 }
661
662                 # only add an operator if there is a previous operand
663                 if ($previous_operand) {
664                     if ( $operators[ $i - 1 ] ) {
665                         $query .= " $operators[$i-1] $index: $operand";
666                         if ( !$index ) {
667                             $human_search_desc .=
668                               "  $operators[$i-1] $operands[$i]";
669                         }
670                         else {
671                             $human_search_desc .=
672                               "  $operators[$i-1] $index: $operands[$i]";
673                         }
674                     }
675
676                     # the default operator is and
677                     else {
678                         $query             .= " and $index: $operand";
679                         $human_search_desc .= "  and $index: $operands[$i]";
680                     }
681                 }
682                 else {
683                     if ( !$index ) {
684                         $query             .= " $operand";
685                         $human_search_desc .= "  $operands[$i]";
686                     }
687                     else {
688                         $query             .= " $index: $operand";
689                         $human_search_desc .= "  $index: $operands[$i]";
690                     }
691                     $previous_operand = 1;
692                 }
693             }    #/if $operands
694         }    # /for
695     }
696
697     # add limits
698     my $limit_query;
699     my $limit_search_desc;
700     foreach my $limit (@limits) {
701
702         # FIXME: not quite right yet ... will work on this soon -- JF
703         my $type = $1 if $limit =~ m/([^:]+):([^:]*)/;
704         if ( $limit =~ /available/ ) {
705             $limit_query .=
706 " (($query and datedue=0000-00-00) or ($query and datedue=0000-00-00 not lost=1) or ($query and datedue=0000-00-00 not lost=2))";
707
708             #$limit_search_desc.=" and available";
709         }
710         elsif ( ($limit_query) && ( index( $limit_query, $type, 0 ) > 0 ) ) {
711             if ( $limit_query !~ /\(/ ) {
712                 $limit_query =
713                     substr( $limit_query, 0, index( $limit_query, $type, 0 ) )
714                   . "("
715                   . substr( $limit_query, index( $limit_query, $type, 0 ) )
716                   . " or $limit )"
717                   if $limit;
718                 $limit_search_desc =
719                   substr( $limit_search_desc, 0,
720                     index( $limit_search_desc, $type, 0 ) )
721                   . "("
722                   . substr( $limit_search_desc,
723                     index( $limit_search_desc, $type, 0 ) )
724                   . " or $limit )"
725                   if $limit;
726             }
727             else {
728                 chop $limit_query;
729                 chop $limit_search_desc;
730                 $limit_query       .= " or $limit )" if $limit;
731                 $limit_search_desc .= " or $limit )" if $limit;
732             }
733         }
734         elsif ( ($limit_query) && ( $limit =~ /mc/ ) ) {
735             $limit_query       .= " or $limit" if $limit;
736             $limit_search_desc .= " or $limit" if $limit;
737         }
738
739         # these are treated as AND
740         elsif ($limit_query) {
741            if ($limit =~ /branch/){
742                         $limit_query       .= " ) and ( $limit" if $limit;
743                         $limit_search_desc .= " ) and ( $limit" if $limit;
744                 }else{
745                         $limit_query       .= " or $limit" if $limit;
746                         $limit_search_desc .= " or $limit" if $limit;
747                 }
748         }
749
750         # otherwise, there is nothing but the limit
751         else {
752             $limit_query       .= "$limit" if $limit;
753             $limit_search_desc .= "$limit" if $limit;
754         }
755     }
756
757     # if there's also a query, we need to AND the limits to it
758     if ( ($limit_query) && ($query) ) {
759         $limit_query       = " and (" . $limit_query . ")";
760         $limit_search_desc = " and ($limit_search_desc)" if $limit_search_desc;
761
762     }
763     $query             .= $limit_query;
764     $human_search_desc .= $limit_search_desc;
765
766     # now normalize the strings
767     $query =~ s/  / /g;    # remove extra spaces
768     $query =~ s/^ //g;     # remove any beginning spaces
769     $query =~ s/:/=/g;     # causes probs for server
770     $query =~ s/==/=/g;    # remove double == from query
771
772     my $federated_query = $human_search_desc;
773     $federated_query =~ s/  / /g;
774     $federated_query =~ s/^ //g;
775     $federated_query =~ s/:/=/g;
776     my $federated_query_opensearch = $federated_query;
777
778 #     my $federated_query_RPN = new ZOOM::Query::CCL2RPN( $query , C4::Context->ZConn('biblioserver'));
779
780     $human_search_desc =~ s/  / /g;
781     $human_search_desc =~ s/^ //g;
782     my $koha_query = $query;
783
784     #warn "QUERY:".$koha_query;
785     #warn "SEARCHDESC:".$human_search_desc;
786     #warn "FEDERATED QUERY:".$federated_query;
787     return ( undef, $human_search_desc, $koha_query, $federated_query );
788 }
789
790 # IMO this subroutine is pretty messy still -- it's responsible for
791 # building the HTML output for the template
792 sub searchResults {
793     my ( $searchdesc, $hits, $results_per_page, $offset, @marcresults ) = @_;
794
795     my $dbh = C4::Context->dbh;
796     my $toggle;
797     my $even = 1;
798     my @newresults;
799     my $span_terms_hashref;
800     for my $span_term ( split( / /, $searchdesc ) ) {
801         $span_term =~ s/(.*=|\)|\(|\+|\.)//g;
802         $span_terms_hashref->{$span_term}++;
803     }
804
805     #Build brancnames hash
806     #find branchname
807     #get branch information.....
808     my %branches;
809     my $bsth =
810       $dbh->prepare("SELECT branchcode,branchname FROM branches")
811       ;    # FIXME : use C4::Koha::GetBranches
812     $bsth->execute();
813     while ( my $bdata = $bsth->fetchrow_hashref ) {
814         $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
815     }
816
817     #Build itemtype hash
818     #find itemtype & itemtype image
819     my %itemtypes;
820     $bsth =
821       $dbh->prepare("SELECT itemtype,description,imageurl,summary FROM itemtypes");
822     $bsth->execute();
823     while ( my $bdata = $bsth->fetchrow_hashref ) {
824         $itemtypes{ $bdata->{'itemtype'} }->{description} =
825           $bdata->{'description'};
826         $itemtypes{ $bdata->{'itemtype'} }->{imageurl} = $bdata->{'imageurl'};
827         $itemtypes{ $bdata->{'itemtype'} }->{summary} = $bdata->{'summary'};
828     }
829
830     #search item field code
831     my $sth =
832       $dbh->prepare(
833 "select tagfield from marc_subfield_structure where kohafield like 'items.itemnumber'"
834       );
835     $sth->execute;
836     my ($itemtag) = $sth->fetchrow;
837
838     ## find column names of items related to MARC
839     my $sth2 = $dbh->prepare("SHOW COLUMNS from items");
840     $sth2->execute;
841     my %subfieldstosearch;
842     while ( ( my $column ) = $sth2->fetchrow ) {
843         my ( $tagfield, $tagsubfield ) =
844           &GetMarcFromKohaField( "items." . $column, "" );
845         $subfieldstosearch{$column} = $tagsubfield;
846     }
847     my $times;
848
849     if ( $hits && $offset + $results_per_page <= $hits ) {
850         $times = $offset + $results_per_page;
851     }
852     else {
853         $times = $hits;
854     }
855
856     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
857         my $marcrecord;
858         $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
859
860         my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, '' );
861
862         # add image url if there is one
863         if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} =~ /^http:/ ) {
864             $oldbiblio->{imageurl} =
865               $itemtypes{ $oldbiblio->{itemtype} }->{imageurl};
866             $oldbiblio->{description} =
867               $itemtypes{ $oldbiblio->{itemtype} }->{description};
868         }
869         else {
870             $oldbiblio->{imageurl} =
871               getitemtypeimagesrc() . "/"
872               . $itemtypes{ $oldbiblio->{itemtype} }->{imageurl}
873               if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
874             $oldbiblio->{description} =
875               $itemtypes{ $oldbiblio->{itemtype} }->{description};
876         }
877         #
878         # build summary if there is one (the summary is defined in itemtypes table
879         #
880         if ($itemtypes{ $oldbiblio->{itemtype} }->{summary}) {
881             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
882             my @fields = $marcrecord->fields();
883             foreach my $field (@fields) {
884                 my $tag = $field->tag();
885                 my $tagvalue = $field->as_string();
886                 $summary =~ s/\[(.?.?.?.?)$tag\*(.*?)]/$1$tagvalue$2\[$1$tag$2]/g;
887                 unless ($tag<10) {
888                     my @subf = $field->subfields;
889                     for my $i (0..$#subf) {
890                         my $subfieldcode = $subf[$i][0];
891                         my $subfieldvalue = $subf[$i][1];
892                         my $tagsubf = $tag.$subfieldcode;
893                         $summary =~ s/\[(.?.?.?.?)$tagsubf(.*?)]/$1$subfieldvalue$2\[$1$tagsubf$2]/g;
894                     }
895                 }
896             }
897             $summary =~ s/\[(.*?)]//g;
898             $summary =~ s/\n/<br>/g;
899             $oldbiblio->{summary} = $summary;
900         }
901         # add spans to search term in results
902         foreach my $term ( keys %$span_terms_hashref ) {
903
904             #warn "term: $term";
905             my $old_term = $term;
906             if ( length($term) > 3 ) {
907                 $term =~ s/(.*=|\)|\(|\+|\.|\?)//g;
908
909                 #FIXME: is there a better way to do this?
910                 $oldbiblio->{'title'} =~ s/$term/<span class=term>$&<\/span>/gi;
911                 $oldbiblio->{'subtitle'} =~
912                   s/$term/<span class=term>$&<\/span>/gi;
913
914                 $oldbiblio->{'author'} =~ s/$term/<span class=term>$&<\/span>/gi;
915                 $oldbiblio->{'publishercode'} =~ s/$term/<span class=term>$&<\/span>/gi;
916                 $oldbiblio->{'place'} =~ s/$term/<span class=term>$&<\/span>/gi;
917                 $oldbiblio->{'pages'} =~ s/$term/<span class=term>$&<\/span>/gi;
918                 $oldbiblio->{'notes'} =~ s/$term/<span class=term>$&<\/span>/gi;
919                 $oldbiblio->{'size'}  =~ s/$term/<span class=term>$&<\/span>/gi;
920             }
921         }
922
923         if ( $i % 2 ) {
924             $toggle = "#ffffcc";
925         }
926         else {
927             $toggle = "white";
928         }
929         $oldbiblio->{'toggle'} = $toggle;
930         my @fields = $marcrecord->field($itemtag);
931         my @items_loop;
932         my $items;
933         my $ordered_count     = 0;
934         my $onloan_count      = 0;
935         my $wthdrawn_count    = 0;
936         my $itemlost_count    = 0;
937         my $itembinding_count = 0;
938         my $norequests        = 1;
939
940         foreach my $field (@fields) {
941             my $item;
942             foreach my $code ( keys %subfieldstosearch ) {
943                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
944             }
945             if ( $item->{wthdrawn} ) {
946                 $wthdrawn_count++;
947             }
948             elsif ( $item->{notforloan} == -1 ) {
949                 $ordered_count++;
950                 $norequests = 0;
951             }
952             elsif ( $item->{itemlost} ) {
953                 $itemlost_count++;
954             }
955             elsif ( $item->{binding} ) {
956                 $itembinding_count++;
957             }
958             elsif ( ( $item->{onloan} ) && ( $item->{onloan} != '0000-00-00' ) )
959             {
960                 $onloan_count++;
961                 $norequests = 0;
962             }
963             else {
964                 $norequests = 0;
965                 if ( $item->{'homebranch'} ) {
966                     $items->{ $item->{'homebranch'} }->{count}++;
967                 }
968
969                 # Last resort
970                 elsif ( $item->{'holdingbranch'} ) {
971                     $items->{ $item->{'homebranch'} }->{count}++;
972                 }
973                 $items->{ $item->{homebranch} }->{itemcallnumber} =
974                 $item->{itemcallnumber};
975                 $items->{ $item->{homebranch} }->{location} =
976                 $item->{location};
977             }
978         }    # notforloan, item level and biblioitem level
979         for my $key ( keys %$items ) {
980
981             #warn "key: $key";
982             my $this_item = {
983                 branchname     => $branches{$key},
984                 branchcode     => $key,
985                 count          => $items->{$key}->{count},
986                 itemcallnumber => $items->{$key}->{itemcallnumber},
987                 location => $items->{$key}->{location},
988             };
989             push @items_loop, $this_item;
990         }
991         $oldbiblio->{norequests}    = $norequests;
992         $oldbiblio->{items_loop}    = \@items_loop;
993         $oldbiblio->{onloancount}   = $onloan_count;
994         $oldbiblio->{wthdrawncount} = $wthdrawn_count;
995         $oldbiblio->{itemlostcount} = $itemlost_count;
996         $oldbiblio->{bindingcount}  = $itembinding_count;
997         $oldbiblio->{orderedcount}  = $ordered_count;
998
999 # FIXME
1000 #  Ugh ... this is ugly, I'll re-write it better above then delete it
1001 #     my $norequests = 1;
1002 #     my $noitems    = 1;
1003 #     if (@items) {
1004 #         $noitems = 0;
1005 #         foreach my $itm (@items) {
1006 #             $norequests = 0 unless $itm->{'itemnotforloan'};
1007 #         }
1008 #     }
1009 #     $oldbiblio->{'noitems'} = $noitems;
1010 #     $oldbiblio->{'norequests'} = $norequests;
1011 #     $oldbiblio->{'even'} = $even = not $even;
1012 #     $oldbiblio->{'itemcount'} = $counts{'total'};
1013 #     my $totalitemcounts = 0;
1014 #     foreach my $key (keys %counts){
1015 #         if ($key ne 'total'){
1016 #             $totalitemcounts+= $counts{$key};
1017 #             $oldbiblio->{'locationhash'}->{$key}=$counts{$key};
1018 #         }
1019 #     }
1020 #     my ($locationtext, $locationtextonly, $notavailabletext) = ('','','');
1021 #     foreach (sort keys %{$oldbiblio->{'locationhash'}}) {
1022 #         if ($_ eq 'notavailable') {
1023 #             $notavailabletext="Not available";
1024 #             my $c=$oldbiblio->{'locationhash'}->{$_};
1025 #             $oldbiblio->{'not-available-p'}=$c;
1026 #         } else {
1027 #             $locationtext.="$_";
1028 #             my $c=$oldbiblio->{'locationhash'}->{$_};
1029 #             if ($_ eq 'Item Lost') {
1030 #                 $oldbiblio->{'lost-p'} = $c;
1031 #             } elsif ($_ eq 'Withdrawn') {
1032 #                 $oldbiblio->{'withdrawn-p'} = $c;
1033 #             } elsif ($_ eq 'On Loan') {
1034 #                 $oldbiblio->{'on-loan-p'} = $c;
1035 #             } else {
1036 #                 $locationtextonly.= $_;
1037 #                 $locationtextonly.= " ($c)<br/> " if $totalitemcounts > 1;
1038 #             }
1039 #             if ($totalitemcounts>1) {
1040 #                 $locationtext.=" ($c)<br/> ";
1041 #             }
1042 #         }
1043 #     }
1044 #     if ($notavailabletext) {
1045 #         $locationtext.= $notavailabletext;
1046 #     } else {
1047 #         $locationtext=~s/, $//;
1048 #     }
1049 #     $oldbiblio->{'location'} = $locationtext;
1050 #     $oldbiblio->{'location-only'} = $locationtextonly;
1051 #     $oldbiblio->{'use-location-flags-p'} = 1;
1052
1053         push( @newresults, $oldbiblio );
1054     }
1055     return @newresults;
1056 }
1057
1058
1059 =head2 EditBiblios
1060
1061 ($countchanged,$listunchanged) = EditBiblios($listbiblios, $tagsubfield,$initvalue,$targetvalue,$test);
1062
1063 this function changes all the values $initvalue in subfield $tag$subfield in any record in $listbiblios
1064 test parameter if set donot perform change to records in database.
1065
1066 =over 2
1067
1068 =item C<input arg:>
1069
1070     * $listbiblios is an array ref to marcrecords to be changed
1071     * $tagsubfield is the reference of the subfield to change.
1072     * $initvalue is the value to search the record for
1073     * $targetvalue is the value to set the subfield to
1074     * $test is to be set only not to perform changes in database.
1075
1076 =item C<Output arg:>
1077     * $countchanged counts all the changes performed.
1078     * $listunchanged contains the list of all the biblionumbers of records unchanged.
1079
1080 =item C<usage in the script:>
1081
1082 =back
1083
1084 my ($countchanged, $listunchanged) = EditBiblios($results->{RECORD}, $tagsubfield,$initvalue,$targetvalue);;
1085 #If one wants to display unchanged records, you should get biblios foreach @$listunchanged 
1086 $template->param(countchanged => $countchanged, loopunchanged=>$listunchanged);
1087
1088 =cut
1089 sub EditBiblios{
1090   my ($listbiblios,$tagsubfield,$initvalue,$targetvalue,$test)=@_;
1091   my $countmatched;
1092   my @unmatched;
1093   my ($tag,$subfield)=($1,$2) if ($tagsubfield=~/^(\d{1,3})(.)$/);
1094   my ($bntag,$bnsubf) = GetMarcFromKohaField('biblio.biblionumber');
1095
1096   foreach my $usmarc (@$listbiblios){
1097     my $record=MARC::Record->new_from_usmarc($usmarc);
1098     my $biblionumber;
1099     if ($bntag>10){
1100       $biblionumber = $record->subfield($bntag,$bnsubf);
1101     }else {
1102       $biblionumber=$record->field($bntag)->data;
1103     }
1104     #GetBiblionumber is to be written.
1105     #Could be replaced by TransformMarcToKoha (But Would be longer)
1106     if ($record->field($tag)){
1107       foreach my $field ($record->field($tag)){
1108         if ($field->delete_subfield('code' =>$subfield,'match'=>qr($initvalue))){
1109           $countmatched++;
1110           $field->update($subfield,$targetvalue) if ($targetvalue);
1111         }
1112       }
1113 #       warn $record->as_formatted;
1114       ModBiblio($record,$biblionumber,GetFrameworkCode($biblionumber)) unless ($test);
1115     } else {
1116       push @unmatched, $biblionumber;
1117     }
1118   }
1119   return ($countmatched,\@unmatched);
1120 }
1121
1122 #----------------------------------------------------------------------
1123 #
1124 # Non-Zebra GetRecords#
1125 #----------------------------------------------------------------------
1126
1127 =item
1128   NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1129 =cut
1130
1131 sub NZgetRecords {
1132     my (
1133         $koha_query,     $federated_query,  $sort_by_ref,
1134         $servers_ref,    $results_per_page, $offset,
1135         $expanded_facet, $branches,         $query_type,
1136         $scan
1137     ) = @_;
1138     my $result = NZanalyse($koha_query);
1139 #     use Data::Dumper;
1140 #     warn "==========".@$sort_by_ref[0];
1141     return (undef,NZorder($result,@$sort_by_ref[0],$results_per_page,$offset),undef);
1142 }
1143
1144 =item
1145
1146   NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1147   the list is builded from inverted index in nozebra SQL table
1148   note that title is here only for convenience : the sorting will be very fast when requested on title
1149   if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1150
1151 =cut
1152
1153 sub NZanalyse {
1154     my ($string) = @_;
1155     # if we have a ", replace the content to discard temporarily any and/or/not inside
1156     my $commacontent;
1157     if ($string =~/"/) {
1158         $string =~ s/"(.*?)"/__X__/;
1159         $commacontent = $1;
1160 #         print "commacontent : $commacontent\n";
1161     }
1162     # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1163     # then, call again NZanalyse with $left and $right
1164     # (recursive until we find a leaf (=> something without and/or/not)
1165     $string =~ /(.*)( and | or | not )(.*)/;
1166     my $left = $1;
1167     my $right = $3;
1168     my $operand = $2;
1169     # it's not a leaf, we have a and/or/not
1170     if ($operand) {
1171         # reintroduce comma content if needed
1172         $right =~ s/__X__/"$commacontent"/ if $commacontent;
1173         $left =~ s/__X__/"$commacontent"/ if $commacontent;
1174 #         print "noeud : $left / $operand / $right\n";
1175         my $leftresult = NZanalyse($left);
1176         my $rightresult = NZanalyse($right);
1177         # OK, we have the results for right and left part of the query
1178         # depending of operand, intersect, union or exclude both lists
1179         # to get a result list
1180         if ($operand eq ' and ') {
1181             my @leftresult = split /,/, $leftresult;
1182 #             my @rightresult = split /,/,$leftresult;
1183             my $finalresult;
1184             # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1185             # the result is stored twice, to have the same weight for AND than OR.
1186             # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1187             # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1188             foreach (@leftresult) {
1189                 if ($rightresult =~ "$_,") {
1190                     $finalresult .= "$_,$_,";
1191                 }
1192             }
1193             return $finalresult;
1194         } elsif ($operand eq ' or ') {
1195             # just merge the 2 strings
1196             return $leftresult.$rightresult;
1197         } elsif ($operand eq ' not ') {
1198             my @leftresult = split /,/, $leftresult;
1199 #             my @rightresult = split /,/,$leftresult;
1200             my $finalresult;
1201             foreach (@leftresult) {
1202                 unless ($rightresult =~ "$_,") {
1203                     $finalresult .= "$_,";
1204                 }
1205             }
1206             return $finalresult;
1207         } else {
1208             # this error is impossible, because of the regexp that isolate the operand, but just in case...
1209             die "error : operand unknown : $operand for $string";
1210         }
1211     # it's a leaf, do the real SQL query and return the result
1212     } else {
1213         $string =~  s/__X__/"$commacontent"/ if $commacontent;
1214         $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|<|>|&|\+|\*|\// /g;
1215 #         print "feuille : $string\n";
1216         # parse the string in in operator/operand/value again
1217         $string =~ /(.*)(=|>|>=|<|<=)(.*)/;
1218         my $left = $1;
1219         my $operator = $2;
1220         my $right = $3;
1221         my $results;
1222             # automatic replace for short operator
1223             $left='title' if $left eq 'ti';
1224             $left='author' if $left eq 'au';
1225         if ($operator) {
1226             #do a specific search
1227             my $dbh = C4::Context->dbh;
1228             $operator='LIKE' if $operator eq '=' and $right=~ /%/;
1229             my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE indexname=? AND value $operator ?");
1230 #             print "$left / $operator / $right\n";
1231             # split each word, query the DB and build the biblionumbers result
1232             foreach (split / /,$right) {
1233                 my $biblionumbers;
1234                 $sth->execute($left,$_);
1235                 while (my $line = $sth->fetchrow) {
1236                     $biblionumbers .= $line;
1237                 }
1238                 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1239                 if ($results) {
1240                     my @leftresult = split /;/, $biblionumbers;
1241                     my $temp;
1242                     foreach (@leftresult) {
1243                         if ($results =~ "$_;") {
1244                             $temp .= "$_;$_;";
1245                         }
1246                     }
1247                     $results = $temp;
1248                 } else {
1249                     $results = $biblionumbers;
1250                 }
1251             }
1252         } else {
1253             #do a complete search (all indexes)
1254             my $dbh = C4::Context->dbh;
1255             my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE value LIKE ?");
1256             # split each word, query the DB and build the biblionumbers result
1257             foreach (split / /,$string) {
1258                 my $biblionumbers;
1259                 $sth->execute($_);
1260                 while (my $line = $sth->fetchrow) {
1261                     $biblionumbers .= $line;
1262                 }
1263                 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1264                 if ($results) {
1265                     my @leftresult = split /,/, $biblionumbers;
1266                     my $temp;
1267                     foreach (@leftresult) {
1268                         if ($results =~ "$_;") {
1269                             $temp .= "$_;$_;";
1270                         }
1271                     }
1272                     $results = $temp;
1273                 } else {
1274                     $results = $biblionumbers;
1275                 }
1276             }
1277         }
1278         return $results;
1279     }
1280 }
1281
1282 sub NZorder {
1283     my ($biblionumbers, $ordering,$results_per_page,$offset) = @_;
1284     # order title asc by default
1285 #     $ordering = '1=36 <i' unless $ordering;
1286     $results_per_page=20 unless $results_per_page;
1287     $offset = 0 unless $offset;
1288     my $dbh = C4::Context->dbh;
1289     #
1290     # order by POPULARITY
1291     #
1292     if ($ordering =~ /1=9523/) {
1293         my %result;
1294         my %popularity;
1295         # popularity is not in MARC record, it's builded from a specific query
1296         my $sth = $dbh->prepare("select sum(issues) from items where biblionumber=?");
1297         foreach (split /;/,$biblionumbers) {
1298             my ($biblionumber,$title) = split /,/,$_;
1299             $result{$biblionumber}=GetMarcBiblio($biblionumber);
1300             $sth->execute($biblionumber);
1301             my $popularity= $sth->fetchrow ||0;
1302             # hint : the key is popularity.title because we can have
1303             # many results with the same popularity. In this cas, sub-ordering is done by title
1304             # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
1305             # (un-frequent, I agree, but we won't forget anything that way ;-)
1306             $popularity{sprintf("%10d",$popularity).$title.$biblionumber} = $biblionumber;
1307         }
1308         # sort the hash and return the same structure as GetRecords (Zebra querying)
1309         my $result_hash;
1310         my $numbers=0;
1311         if ($ordering eq '1=9523 >i') { # sort popularity DESC
1312             foreach my $key (sort {$b <=> $a} (keys %popularity)) {
1313                 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1314             }
1315         } else { # sort popularity ASC
1316             foreach my $key (sort (keys %popularity)) {
1317                 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1318             }
1319         }
1320         my $finalresult=();
1321         $result_hash->{'hits'} = $numbers;
1322         $finalresult->{'biblioserver'} = $result_hash;
1323         return $finalresult;
1324     #
1325     # ORDER BY author
1326     #
1327     } elsif ($ordering eq '1=1003 <i'){
1328         my %result;
1329         foreach (split /;/,$biblionumbers) {
1330             my ($biblionumber,$title) = split /,/,$_;
1331             my $record=GetMarcBiblio($biblionumber);
1332             my $author;
1333             if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1334                 $author=$record->subfield('200','f');
1335                 $author=$record->subfield('700','a') unless $author;
1336             } else {
1337                 $author=$record->subfield('100','a');
1338             }
1339             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1340             # and we don't want to get only 1 result for each of them !!!
1341             $result{$author.$biblionumber}=$record;
1342         }
1343         # sort the hash and return the same structure as GetRecords (Zebra querying)
1344         my $result_hash;
1345         my $numbers=0;
1346         if ($ordering eq '1=1003 <i') { # sort by title desc
1347             foreach my $key (sort (keys %result)) {
1348                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1349             }
1350         } else { # sort by title ASC
1351             foreach my $key (sort { $a <=> $b } (keys %result)) {
1352                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1353             }
1354         }
1355         my $finalresult=();
1356         $result_hash->{'hits'} = $numbers;
1357         $finalresult->{'biblioserver'} = $result_hash;
1358         return $finalresult;
1359     #
1360     # ORDER BY callnumber
1361     #
1362     } elsif ($ordering eq '1=20 <i'){
1363         my %result;
1364         foreach (split /;/,$biblionumbers) {
1365             my ($biblionumber,$title) = split /,/,$_;
1366             my $record=GetMarcBiblio($biblionumber);
1367             my $callnumber;
1368             my ($callnumber_tag,$callnumber_subfield)=GetMarcFromKohaField($dbh,'items.itemcallnumber');
1369             ($callnumber_tag,$callnumber_subfield)= GetMarcFromKohaField('biblioitems.callnumber') unless $callnumber_tag;
1370             if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1371                 $callnumber=$record->subfield('200','f');
1372             } else {
1373                 $callnumber=$record->subfield('100','a');
1374             }
1375             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1376             # and we don't want to get only 1 result for each of them !!!
1377             $result{$callnumber.$biblionumber}=$record;
1378         }
1379         # sort the hash and return the same structure as GetRecords (Zebra querying)
1380         my $result_hash;
1381         my $numbers=0;
1382         if ($ordering eq '1=1003 <i') { # sort by title desc
1383             foreach my $key (sort (keys %result)) {
1384                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1385             }
1386         } else { # sort by title ASC
1387             foreach my $key (sort { $a <=> $b } (keys %result)) {
1388                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1389             }
1390         }
1391         my $finalresult=();
1392         $result_hash->{'hits'} = $numbers;
1393         $finalresult->{'biblioserver'} = $result_hash;
1394         return $finalresult;
1395     } elsif ($ordering =~ /1=31/){ #pub year
1396         my %result;
1397         foreach (split /;/,$biblionumbers) {
1398             my ($biblionumber,$title) = split /,/,$_;
1399             my $record=GetMarcBiblio($biblionumber);
1400             my ($publicationyear_tag,$publicationyear_subfield)=GetMarcFromKohaField($dbh,'biblioitems.publicationyear');
1401             my $publicationyear=$record->subfield($publicationyear_tag,$publicationyear_subfield);
1402             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1403             # and we don't want to get only 1 result for each of them !!!
1404             $result{$publicationyear.$biblionumber}=$record;
1405         }
1406         # sort the hash and return the same structure as GetRecords (Zebra querying)
1407         my $result_hash;
1408         my $numbers=0;
1409         if ($ordering eq '1=31 <i') { # sort by title desc
1410             foreach my $key (sort (keys %result)) {
1411                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1412             }
1413         } else { # sort by title ASC
1414             foreach my $key (sort { $a <=> $b } (keys %result)) {
1415                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1416             }
1417         }
1418         my $finalresult=();
1419         $result_hash->{'hits'} = $numbers;
1420         $finalresult->{'biblioserver'} = $result_hash;
1421         return $finalresult;
1422     #
1423     # ORDER BY title
1424     #
1425     } elsif ($ordering =~ /1=36/) { 
1426         # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
1427         my %result;
1428         foreach (split /;/,$biblionumbers) {
1429             my ($biblionumber,$title) = split /,/,$_;
1430             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1431             # and we don't want to get only 1 result for each of them !!!
1432             # hint & speed improvement : we can order without reading the record
1433             # so order, and read records only for the requested page !
1434             $result{$title.$biblionumber}=$biblionumber;
1435         }
1436         # sort the hash and return the same structure as GetRecords (Zebra querying)
1437         my $result_hash;
1438         my $numbers=0;
1439         if ($ordering eq '1=36 <i') { # sort by title desc
1440             foreach my $key (sort (keys %result)) {
1441                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1442             }
1443         } else { # sort by title ASC
1444             foreach my $key (sort { $a <=> $b } (keys %result)) {
1445                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1446             }
1447         }
1448         # limit the $results_per_page to result size if it's more
1449         $results_per_page = $numbers-1 if $numbers < $results_per_page;
1450         # for the requested page, replace biblionumber by the complete record
1451         # speed improvement : avoid reading too much things
1452         for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1453             $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1454         }
1455         my $finalresult=();
1456         $result_hash->{'hits'} = $numbers;
1457         $finalresult->{'biblioserver'} = $result_hash;
1458         return $finalresult;
1459     } else {
1460     #
1461     # order by ranking
1462     #
1463         # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
1464         my %result;
1465         my %count_ranking;
1466         foreach (split /;/,$biblionumbers) {
1467             my ($biblionumber,$title) = split /,/,$_;
1468             $title =~ /(.*)-(\d)/;
1469             # get weight 
1470             my $ranking =$2;
1471             # note that we + the ranking because ranking is calculated on weight of EACH term requested.
1472             # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
1473             # biblio N has ranking = 6
1474             $count_ranking{$biblionumber} =+ $ranking;
1475         }
1476         # build the result by "inverting" the count_ranking hash
1477         # 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
1478 #         warn "counting";
1479         foreach (keys %count_ranking) {
1480             $result{sprintf("%10d",$count_ranking{$_}).'-'.$_} = $_;
1481         }
1482         # sort the hash and return the same structure as GetRecords (Zebra querying)
1483         my $result_hash;
1484         my $numbers=0;
1485             foreach my $key (sort {$b <=> $a} (keys %result)) {
1486                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1487             }
1488         # limit the $results_per_page to result size if it's more
1489         $results_per_page = $numbers-1 if $numbers < $results_per_page;
1490         # for the requested page, replace biblionumber by the complete record
1491         # speed improvement : avoid reading too much things
1492         for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1493             $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1494         }
1495         my $finalresult=();
1496         $result_hash->{'hits'} = $numbers;
1497         $finalresult->{'biblioserver'} = $result_hash;
1498         return $finalresult;
1499     }
1500 }
1501
1502 END { }    # module clean-up code here (global destructor)
1503
1504 1;
1505 __END__
1506
1507 =head1 AUTHOR
1508
1509 Koha Developement team <info@koha.org>
1510
1511 =cut