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