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