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