Merge remote-tracking branch 'origin/new/bug_7849'
[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 #use warnings; FIXME - Bug 2505
20 require Exporter;
21 use C4::Context;
22 use C4::Biblio;    # GetMarcFromKohaField, GetBiblioData
23 use C4::Koha;      # getFacets
24 use Lingua::Stem;
25 use C4::Search::PazPar2;
26 use XML::Simple;
27 use C4::Dates qw(format_date);
28 use C4::Members qw(GetHideLostItemsPreference);
29 use C4::XSLT;
30 use C4::Branch;
31 use C4::Reserves;    # CheckReserves
32 use C4::Debug;
33 use C4::Items;
34 use C4::Charset;
35 use YAML;
36 use URI::Escape;
37 use Business::ISBN;
38
39 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
40
41 # set the version for version checking
42 BEGIN {
43     $VERSION = 3.01;
44     $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
45 }
46
47 =head1 NAME
48
49 C4::Search - Functions for searching the Koha catalog.
50
51 =head1 SYNOPSIS
52
53 See opac/opac-search.pl or catalogue/search.pl for example of usage
54
55 =head1 DESCRIPTION
56
57 This module provides searching functions for Koha's bibliographic databases
58
59 =head1 FUNCTIONS
60
61 =cut
62
63 @ISA    = qw(Exporter);
64 @EXPORT = qw(
65   &FindDuplicate
66   &SimpleSearch
67   &searchResults
68   &getRecords
69   &buildQuery
70   &NZgetRecords
71   &AddSearchHistory
72   &GetDistinctValues
73   &enabled_staff_search_views
74   &SimpleSearch
75 );
76
77 # make all your functions, whether exported or not;
78
79 =head2 FindDuplicate
80
81 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
82
83 This function attempts to find duplicate records using a hard-coded, fairly simplistic algorithm
84
85 =cut
86
87 sub FindDuplicate {
88     my ($record) = @_;
89     my $dbh = C4::Context->dbh;
90     my $result = TransformMarcToKoha( $dbh, $record, '' );
91     my $sth;
92     my $query;
93     my $search;
94     my $type;
95     my ( $biblionumber, $title );
96
97     # search duplicate on ISBN, easy and fast..
98     # ... normalize first
99     if ( $result->{isbn} ) {
100         $result->{isbn} =~ s/\(.*$//;
101         $result->{isbn} =~ s/\s+$//;
102         $query = "isbn=$result->{isbn}";
103     }
104     else {
105         $result->{title} =~ s /\\//g;
106         $result->{title} =~ s /\"//g;
107         $result->{title} =~ s /\(//g;
108         $result->{title} =~ s /\)//g;
109
110         # FIXME: instead of removing operators, could just do
111         # quotes around the value
112         $result->{title} =~ s/(and|or|not)//g;
113         $query = "ti,ext=$result->{title}";
114         $query .= " and itemtype=$result->{itemtype}"
115           if ( $result->{itemtype} );
116         if   ( $result->{author} ) {
117             $result->{author} =~ s /\\//g;
118             $result->{author} =~ s /\"//g;
119             $result->{author} =~ s /\(//g;
120             $result->{author} =~ s /\)//g;
121
122             # remove valid operators
123             $result->{author} =~ s/(and|or|not)//g;
124             $query .= " and au,ext=$result->{author}";
125         }
126     }
127
128     my ( $error, $searchresults, undef ) = SimpleSearch($query); # FIXME :: hardcoded !
129     my @results;
130     if (!defined $error) {
131         foreach my $possible_duplicate_record (@{$searchresults}) {
132             my $marcrecord =
133             MARC::Record->new_from_usmarc($possible_duplicate_record);
134             my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
135
136             # FIXME :: why 2 $biblionumber ?
137             if ($result) {
138                 push @results, $result->{'biblionumber'};
139                 push @results, $result->{'title'};
140             }
141         }
142     }
143     return @results;
144 }
145
146 =head2 SimpleSearch
147
148 ( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers] );
149
150 This function provides a simple search API on the bibliographic catalog
151
152 =over 2
153
154 =item C<input arg:>
155
156     * $query can be a simple keyword or a complete CCL query
157     * @servers is optional. Defaults to biblioserver as found in koha-conf.xml
158     * $offset - If present, represents the number of records at the beggining to omit. Defaults to 0
159     * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
160
161
162 =item C<Return:>
163
164     Returns an array consisting of three elements
165     * $error is undefined unless an error is detected
166     * $results is a reference to an array of records.
167     * $total_hits is the number of hits that would have been returned with no limit
168
169     If an error is returned the two other return elements are undefined. If error itself is undefined
170     the other two elements are always defined
171
172 =item C<usage in the script:>
173
174 =back
175
176 my ( $error, $marcresults, $total_hits ) = SimpleSearch($query);
177
178 if (defined $error) {
179     $template->param(query_error => $error);
180     warn "error: ".$error;
181     output_html_with_http_headers $input, $cookie, $template->output;
182     exit;
183 }
184
185 my $hits = @{$marcresults};
186 my @results;
187
188 for my $r ( @{$marcresults} ) {
189     my $marcrecord = MARC::File::USMARC::decode($r);
190     my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,q{});
191
192     #build the iarray of hashs for the template.
193     push @results, {
194         title           => $biblio->{'title'},
195         subtitle        => $biblio->{'subtitle'},
196         biblionumber    => $biblio->{'biblionumber'},
197         author          => $biblio->{'author'},
198         publishercode   => $biblio->{'publishercode'},
199         publicationyear => $biblio->{'publicationyear'},
200         };
201
202 }
203
204 $template->param(result=>\@results);
205
206 =cut
207
208 sub SimpleSearch {
209     my ( $query, $offset, $max_results, $servers )  = @_;
210
211     if ( C4::Context->preference('NoZebra') ) {
212         my $result = NZorder( NZanalyse($query) )->{'biblioserver'};
213         my $search_result =
214           (      $result->{hits}
215               && $result->{hits} > 0 ? $result->{'RECORDS'} : [] );
216         return ( undef, $search_result, scalar($result->{hits}) );
217     }
218     else {
219         return ( 'No query entered', undef, undef ) unless $query;
220         # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
221         my @servers = defined ( $servers ) ? @$servers : ( 'biblioserver' );
222         my @zoom_queries;
223         my @tmpresults;
224         my @zconns;
225         my $results = [];
226         my $total_hits = 0;
227
228         # Initialize & Search Zebra
229         for ( my $i = 0 ; $i < @servers ; $i++ ) {
230             eval {
231                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
232                 $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
233                 $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
234
235                 # error handling
236                 my $error =
237                     $zconns[$i]->errmsg() . " ("
238                   . $zconns[$i]->errcode() . ") "
239                   . $zconns[$i]->addinfo() . " "
240                   . $zconns[$i]->diagset();
241
242                 return ( $error, undef, undef ) if $zconns[$i]->errcode();
243             };
244             if ($@) {
245
246                 # caught a ZOOM::Exception
247                 my $error =
248                     $@->message() . " ("
249                   . $@->code() . ") "
250                   . $@->addinfo() . " "
251                   . $@->diagset();
252                 warn $error;
253                 return ( $error, undef, undef );
254             }
255         }
256         while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
257             my $event = $zconns[ $i - 1 ]->last_event();
258             if ( $event == ZOOM::Event::ZEND ) {
259
260                 my $first_record = defined( $offset ) ? $offset+1 : 1;
261                 my $hits = $tmpresults[ $i - 1 ]->size();
262                 $total_hits += $hits;
263                 my $last_record = $hits;
264                 if ( defined $max_results && $offset + $max_results < $hits ) {
265                     $last_record  = $offset + $max_results;
266                 }
267
268                 for my $j ( $first_record..$last_record ) {
269                     my $record = $tmpresults[ $i - 1 ]->record( $j-1 )->raw(); # 0 indexed
270                     push @{$results}, $record;
271                 }
272             }
273         }
274
275         foreach my $result (@tmpresults) {
276             $result->destroy();
277         }
278         foreach my $zoom_query (@zoom_queries) {
279             $zoom_query->destroy();
280         }
281
282         return ( undef, $results, $total_hits );
283     }
284 }
285
286 =head2 getRecords
287
288 ( undef, $results_hashref, \@facets_loop ) = getRecords (
289
290         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
291         $results_per_page, $offset,       $expanded_facet, $branches,
292         $query_type,       $scan
293     );
294
295 The all singing, all dancing, multi-server, asynchronous, scanning,
296 searching, record nabbing, facet-building
297
298 See verbse embedded documentation.
299
300 =cut
301
302 sub getRecords {
303     my (
304         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
305         $results_per_page, $offset,       $expanded_facet, $branches,
306         $query_type,       $scan
307     ) = @_;
308
309     my @servers = @$servers_ref;
310     my @sort_by = @$sort_by_ref;
311
312     # Initialize variables for the ZOOM connection and results object
313     my $zconn;
314     my @zconns;
315     my @results;
316     my $results_hashref = ();
317
318     # Initialize variables for the faceted results objects
319     my $facets_counter = ();
320     my $facets_info    = ();
321     my $facets         = getFacets();
322     my $facets_maxrecs = C4::Context->preference('maxRecordsForFacets')||20;
323
324     my @facets_loop;    # stores the ref to array of hashes for template facets loop
325
326     ### LOOP THROUGH THE SERVERS
327     for ( my $i = 0 ; $i < @servers ; $i++ ) {
328         $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
329
330 # perform the search, create the results objects
331 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
332         my $query_to_use = ($servers[$i] =~ /biblioserver/) ? $koha_query : $simple_query;
333
334         #$query_to_use = $simple_query if $scan;
335         warn $simple_query if ( $scan and $DEBUG );
336
337         # Check if we've got a query_type defined, if so, use it
338         eval {
339             if ($query_type) {
340                 if ($query_type =~ /^ccl/) {
341                     $query_to_use =~ s/\:/\=/g;    # change : to = last minute (FIXME)
342                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
343                 } elsif ($query_type =~ /^cql/) {
344                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CQL($query_to_use, $zconns[$i]));
345                 } elsif ($query_type =~ /^pqf/) {
346                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::PQF($query_to_use, $zconns[$i]));
347                 } else {
348                     warn "Unknown query_type '$query_type'.  Results undetermined.";
349                 }
350             } elsif ($scan) {
351                     $results[$i] = $zconns[$i]->scan(  new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
352             } else {
353                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
354             }
355         };
356         if ($@) {
357             warn "WARNING: query problem with $query_to_use " . $@;
358         }
359
360         # Concatenate the sort_by limits and pass them to the results object
361         # Note: sort will override rank
362         my $sort_by;
363         foreach my $sort (@sort_by) {
364             if ( $sort eq "author_az" || $sort eq "author_asc" ) {
365                 $sort_by .= "1=1003 <i ";
366             }
367             elsif ( $sort eq "author_za" || $sort eq "author_dsc" ) {
368                 $sort_by .= "1=1003 >i ";
369             }
370             elsif ( $sort eq "popularity_asc" ) {
371                 $sort_by .= "1=9003 <i ";
372             }
373             elsif ( $sort eq "popularity_dsc" ) {
374                 $sort_by .= "1=9003 >i ";
375             }
376             elsif ( $sort eq "call_number_asc" ) {
377                 $sort_by .= "1=8007  <i ";
378             }
379             elsif ( $sort eq "call_number_dsc" ) {
380                 $sort_by .= "1=8007 >i ";
381             }
382             elsif ( $sort eq "pubdate_asc" ) {
383                 $sort_by .= "1=31 <i ";
384             }
385             elsif ( $sort eq "pubdate_dsc" ) {
386                 $sort_by .= "1=31 >i ";
387             }
388             elsif ( $sort eq "acqdate_asc" ) {
389                 $sort_by .= "1=32 <i ";
390             }
391             elsif ( $sort eq "acqdate_dsc" ) {
392                 $sort_by .= "1=32 >i ";
393             }
394             elsif ( $sort eq "title_az" || $sort eq "title_asc" ) {
395                 $sort_by .= "1=4 <i ";
396             }
397             elsif ( $sort eq "title_za" || $sort eq "title_dsc" ) {
398                 $sort_by .= "1=4 >i ";
399             }
400             else {
401                 warn "Ignoring unrecognized sort '$sort' requested" if $sort_by;
402             }
403         }
404         if ($sort_by && !$scan) {
405             if ( $results[$i]->sort( "yaz", $sort_by ) < 0 ) {
406                 warn "WARNING sort $sort_by failed";
407             }
408         }
409     }    # finished looping through servers
410
411     # The big moment: asynchronously retrieve results from all servers
412     while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
413         my $ev = $zconns[ $i - 1 ]->last_event();
414         if ( $ev == ZOOM::Event::ZEND ) {
415             next unless $results[ $i - 1 ];
416             my $size = $results[ $i - 1 ]->size();
417             if ( $size > 0 ) {
418                 my $results_hash;
419
420                 # loop through the results
421                 $results_hash->{'hits'} = $size;
422                 my $times;
423                 if ( $offset + $results_per_page <= $size ) {
424                     $times = $offset + $results_per_page;
425                 }
426                 else {
427                     $times = $size;
428                 }
429                 for ( my $j = $offset ; $j < $times ; $j++ ) {
430                     my $records_hash;
431                     my $record;
432
433                     ## Check if it's an index scan
434                     if ($scan) {
435                         my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
436
437                  # here we create a minimal MARC record and hand it off to the
438                  # template just like a normal result ... perhaps not ideal, but
439                  # it works for now
440                         my $tmprecord = MARC::Record->new();
441                         $tmprecord->encoding('UTF-8');
442                         my $tmptitle;
443                         my $tmpauthor;
444
445                 # the minimal record in author/title (depending on MARC flavour)
446                         if (C4::Context->preference("marcflavour") eq "UNIMARC") {
447                             $tmptitle = MARC::Field->new('200',' ',' ', a => $term, f => $occ);
448                             $tmprecord->append_fields($tmptitle);
449                         } else {
450                             $tmptitle  = MARC::Field->new('245',' ',' ', a => $term,);
451                             $tmpauthor = MARC::Field->new('100',' ',' ', a => $occ,);
452                             $tmprecord->append_fields($tmptitle);
453                             $tmprecord->append_fields($tmpauthor);
454                         }
455                         $results_hash->{'RECORDS'}[$j] = $tmprecord->as_usmarc();
456                     }
457
458                     # not an index scan
459                     else {
460                         $record = $results[ $i - 1 ]->record($j)->raw();
461
462                         # warn "RECORD $j:".$record;
463                         $results_hash->{'RECORDS'}[$j] = $record;
464                     }
465
466                 }
467                 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
468
469                 # Fill the facets while we're looping, but only for the biblioserver and not for a scan
470                 if ( !$scan && $servers[ $i - 1 ] =~ /biblioserver/ ) {
471
472                     my $jmax = $size>$facets_maxrecs? $facets_maxrecs: $size;
473                     for my $facet ( @$facets ) {
474                                 for ( my $j = 0 ; $j < $jmax ; $j++ ) {
475                                     my $render_record = $results[ $i - 1 ]->record($j)->render();
476                             my @used_datas = ();
477                             foreach my $tag ( @{$facet->{tags}} ) {
478                                 # avoid first line
479                                 my $tag_num = substr($tag, 0, 3);
480                                 my $letters = substr($tag, 3);
481                                 my $field_pattern = '\n' . $tag_num . ' ([^\n]+)';
482                                 my @field_tokens = ( $render_record =~ /$field_pattern/g ) ;
483                                 foreach my $field_token (@field_tokens) {
484                                     my @subf = ( $field_token =~ /\$([a-zA-Z0-9]) ([^\$]+)/g );
485                                     my @values;
486                                     for (my $i = 0; $i < @subf; $i += 2) {
487                                         if ( $letters =~ $subf[$i] ) {
488                                              my $value = $subf[$i+1];
489                                              $value =~ s/^ *//;
490                                              $value =~ s/ *$//;
491                                              push @values, $value;
492                                         }
493                                     }
494                                     my $data = join($facet->{sep}, @values);
495                                     unless ( $data ~~ @used_datas ) {
496                                         $facets_counter->{ $facet->{idx} }->{$data}++;
497                                         push @used_datas, $data;
498                                     }
499                                 } # fields
500                             } # field codes
501                         } # records
502                         $facets_info->{ $facet->{idx} }->{label_value} = $facet->{label};
503                         $facets_info->{ $facet->{idx} }->{expanded} = $facet->{expanded};
504                     } # facets
505                 }
506             }
507
508             # warn "connection ", $i-1, ": $size hits";
509             # warn $results[$i-1]->record(0)->render() if $size > 0;
510
511             # BUILD FACETS
512             if ( $servers[ $i - 1 ] =~ /biblioserver/ ) {
513                 for my $link_value (
514                     sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
515                         keys %$facets_counter )
516                 {
517                     my $expandable;
518                     my $number_of_facets;
519                     my @this_facets_array;
520                     for my $one_facet (
521                         sort {
522                              $facets_counter->{$link_value}->{$b}
523                          <=> $facets_counter->{$link_value}->{$a}
524                         } keys %{ $facets_counter->{$link_value} }
525                       )
526                     {
527                         $number_of_facets++;
528                         if (   ( $number_of_facets < 6 )
529                             || ( $expanded_facet eq $link_value )
530                             || ( $facets_info->{$link_value}->{'expanded'} ) )
531                         {
532
533                       # Sanitize the link value ), ( will cause errors with CCL,
534                             my $facet_link_value = $one_facet;
535                             $facet_link_value =~ s/(\(|\))/ /g;
536
537                             # fix the length that will display in the label,
538                             my $facet_label_value = $one_facet;
539                             my $facet_max_length =
540                                 C4::Context->preference('FacetLabelTruncationLength') || 20;
541                             $facet_label_value =
542                               substr( $one_facet, 0, $facet_max_length ) . "..."
543                                 if length($facet_label_value) > $facet_max_length;
544
545                             # if it's a branch, label by the name, not the code,
546                             if ( $link_value =~ /branch/ ) {
547                                                                 if (defined $branches
548                                                                         && ref($branches) eq "HASH"
549                                                                         && defined $branches->{$one_facet}
550                                                                         && ref ($branches->{$one_facet}) eq "HASH")
551                                                                 {
552                                         $facet_label_value =
553                                                 $branches->{$one_facet}->{'branchname'};
554                                                                 }
555                                                                 else {
556                                                                         $facet_label_value = "*";
557                                                                 }
558                             }
559
560                             # but we're down with the whole label being in the link's title.
561                             push @this_facets_array, {
562                                 facet_count       => $facets_counter->{$link_value}->{$one_facet},
563                                 facet_label_value => $facet_label_value,
564                                 facet_title_value => $one_facet,
565                                 facet_link_value  => $facet_link_value,
566                                 type_link_value   => $link_value,
567                             };
568                         }
569                     }
570
571                     # handle expanded option
572                     unless ( $facets_info->{$link_value}->{'expanded'} ) {
573                         $expandable = 1
574                           if ( ( $number_of_facets > 6 )
575                             && ( $expanded_facet ne $link_value ) );
576                     }
577                     push @facets_loop, {
578                         type_link_value => $link_value,
579                         type_id         => $link_value . "_id",
580                         "type_label_" . $facets_info->{$link_value}->{'label_value'} => 1,
581                         facets     => \@this_facets_array,
582                         expandable => $expandable,
583                         expand     => $link_value,
584                     } unless ( ($facets_info->{$link_value}->{'label_value'} =~ /Libraries/) and (C4::Context->preference('singleBranchMode')) );
585                 }
586             }
587         }
588     }
589     return ( undef, $results_hashref, \@facets_loop );
590 }
591
592 sub pazGetRecords {
593     my (
594         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
595         $results_per_page, $offset,       $expanded_facet, $branches,
596         $query_type,       $scan
597     ) = @_;
598
599     my $paz = C4::Search::PazPar2->new(C4::Context->config('pazpar2url'));
600     $paz->init();
601     $paz->search($simple_query);
602     sleep 1;   # FIXME: WHY?
603
604     # do results
605     my $results_hashref = {};
606     my $stats = XMLin($paz->stat);
607     my $results = XMLin($paz->show($offset, $results_per_page, 'work-title:1'), forcearray => 1);
608
609     # for a grouped search result, the number of hits
610     # is the number of groups returned; 'bib_hits' will have
611     # the total number of bibs.
612     $results_hashref->{'biblioserver'}->{'hits'} = $results->{'merged'}->[0];
613     $results_hashref->{'biblioserver'}->{'bib_hits'} = $stats->{'hits'};
614
615     HIT: foreach my $hit (@{ $results->{'hit'} }) {
616         my $recid = $hit->{recid}->[0];
617
618         my $work_title = $hit->{'md-work-title'}->[0];
619         my $work_author;
620         if (exists $hit->{'md-work-author'}) {
621             $work_author = $hit->{'md-work-author'}->[0];
622         }
623         my $group_label = (defined $work_author) ? "$work_title / $work_author" : $work_title;
624
625         my $result_group = {};
626         $result_group->{'group_label'} = $group_label;
627         $result_group->{'group_merge_key'} = $recid;
628
629         my $count = 1;
630         if (exists $hit->{count}) {
631             $count = $hit->{count}->[0];
632         }
633         $result_group->{'group_count'} = $count;
634
635         for (my $i = 0; $i < $count; $i++) {
636             # FIXME -- may need to worry about diacritics here
637             my $rec = $paz->record($recid, $i);
638             push @{ $result_group->{'RECORDS'} }, $rec;
639         }
640
641         push @{ $results_hashref->{'biblioserver'}->{'GROUPS'} }, $result_group;
642     }
643
644     # pass through facets
645     my $termlist_xml = $paz->termlist('author,subject');
646     my $terms = XMLin($termlist_xml, forcearray => 1);
647     my @facets_loop = ();
648     #die Dumper($results);
649 #    foreach my $list (sort keys %{ $terms->{'list'} }) {
650 #        my @facets = ();
651 #        foreach my $facet (sort @{ $terms->{'list'}->{$list}->{'term'} } ) {
652 #            push @facets, {
653 #                facet_label_value => $facet->{'name'}->[0],
654 #            };
655 #        }
656 #        push @facets_loop, ( {
657 #            type_label => $list,
658 #            facets => \@facets,
659 #        } );
660 #    }
661
662     return ( undef, $results_hashref, \@facets_loop );
663 }
664
665 # STOPWORDS
666 sub _remove_stopwords {
667     my ( $operand, $index ) = @_;
668     my @stopwords_removed;
669
670     # phrase and exact-qualified indexes shouldn't have stopwords removed
671     if ( $index !~ m/phr|ext/ ) {
672
673 # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
674 #       we use IsAlpha unicode definition, to deal correctly with diacritics.
675 #       otherwise, a French word like "leçon" woudl be split into "le" "çon", "le"
676 #       is a stopword, we'd get "çon" and wouldn't find anything...
677 #
678                 foreach ( keys %{ C4::Context->stopwords } ) {
679                         next if ( $_ =~ /(and|or|not)/ );    # don't remove operators
680                         if ( my ($matched) = ($operand =~
681                                 /([^\X\p{isAlnum}]\Q$_\E[^\X\p{isAlnum}]|[^\X\p{isAlnum}]\Q$_\E$|^\Q$_\E[^\X\p{isAlnum}])/gi))
682                         {
683                                 $operand =~ s/\Q$matched\E/ /gi;
684                                 push @stopwords_removed, $_;
685                         }
686                 }
687         }
688     return ( $operand, \@stopwords_removed );
689 }
690
691 # TRUNCATION
692 sub _detect_truncation {
693     my ( $operand, $index ) = @_;
694     my ( @nontruncated, @righttruncated, @lefttruncated, @rightlefttruncated,
695         @regexpr );
696     $operand =~ s/^ //g;
697     my @wordlist = split( /\s/, $operand );
698     foreach my $word (@wordlist) {
699         if ( $word =~ s/^\*([^\*]+)\*$/$1/ ) {
700             push @rightlefttruncated, $word;
701         }
702         elsif ( $word =~ s/^\*([^\*]+)$/$1/ ) {
703             push @lefttruncated, $word;
704         }
705         elsif ( $word =~ s/^([^\*]+)\*$/$1/ ) {
706             push @righttruncated, $word;
707         }
708         elsif ( index( $word, "*" ) < 0 ) {
709             push @nontruncated, $word;
710         }
711         else {
712             push @regexpr, $word;
713         }
714     }
715     return (
716         \@nontruncated,       \@righttruncated, \@lefttruncated,
717         \@rightlefttruncated, \@regexpr
718     );
719 }
720
721 # STEMMING
722 sub _build_stemmed_operand {
723     my ($operand,$lang) = @_;
724     require Lingua::Stem::Snowball ;
725     my $stemmed_operand;
726
727     # If operand contains a digit, it is almost certainly an identifier, and should
728     # not be stemmed.  This is particularly relevant for ISBNs and ISSNs, which
729     # can contain the letter "X" - for example, _build_stemmend_operand would reduce
730     # "014100018X" to "x ", which for a MARC21 database would bring up irrelevant
731     # results (e.g., "23 x 29 cm." from the 300$c).  Bug 2098.
732     return $operand if $operand =~ /\d/;
733
734 # FIXME: the locale should be set based on the user's language and/or search choice
735     #warn "$lang";
736     # Make sure we only use the first two letters from the language code
737     $lang = lc(substr($lang, 0, 2));
738     # The language codes for the two variants of Norwegian will now be "nb" and "nn",
739     # none of which Lingua::Stem::Snowball can use, so we need to "translate" them
740     if ($lang eq 'nb' || $lang eq 'nn') {
741       $lang = 'no';
742     }
743     my $stemmer = Lingua::Stem::Snowball->new( lang => $lang,
744                                                encoding => "UTF-8" );
745
746     my @words = split( / /, $operand );
747     my @stems = $stemmer->stem(\@words);
748     for my $stem (@stems) {
749         $stemmed_operand .= "$stem";
750         $stemmed_operand .= "?"
751           unless ( $stem =~ /(and$|or$|not$)/ ) || ( length($stem) < 3 );
752         $stemmed_operand .= " ";
753     }
754     warn "STEMMED OPERAND: $stemmed_operand" if $DEBUG;
755     return $stemmed_operand;
756 }
757
758 # FIELD WEIGHTING
759 sub _build_weighted_query {
760
761 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
762 # pretty well but could work much better if we had a smarter query parser
763     my ( $operand, $stemmed_operand, $index ) = @_;
764     my $stemming      = C4::Context->preference("QueryStemming")     || 0;
765     my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
766     my $fuzzy_enabled = C4::Context->preference("QueryFuzzy")        || 0;
767
768     my $weighted_query .= "(rk=(";    # Specifies that we're applying rank
769
770     # Keyword, or, no index specified
771     if ( ( $index eq 'kw' ) || ( !$index ) ) {
772         $weighted_query .=
773           "Title-cover,ext,r1=\"$operand\"";    # exact title-cover
774         $weighted_query .= " or ti,ext,r2=\"$operand\"";    # exact title
775         $weighted_query .= " or ti,phr,r3=\"$operand\"";    # phrase title
776           #$weighted_query .= " or any,ext,r4=$operand";               # exact any
777           #$weighted_query .=" or kw,wrdl,r5=\"$operand\"";            # word list any
778         $weighted_query .= " or wrdl,fuzzy,r8=\"$operand\""
779           if $fuzzy_enabled;    # add fuzzy, word list
780         $weighted_query .= " or wrdl,right-Truncation,r9=\"$stemmed_operand\""
781           if ( $stemming and $stemmed_operand )
782           ;                     # add stemming, right truncation
783         $weighted_query .= " or wrdl,r9=\"$operand\"";
784
785         # embedded sorting: 0 a-z; 1 z-a
786         # $weighted_query .= ") or (sort1,aut=1";
787     }
788
789     # Barcode searches should skip this process
790     elsif ( $index eq 'bc' ) {
791         $weighted_query .= "bc=\"$operand\"";
792     }
793
794     # Authority-number searches should skip this process
795     elsif ( $index eq 'an' ) {
796         $weighted_query .= "an=\"$operand\"";
797     }
798
799     # If the index already has more than one qualifier, wrap the operand
800     # in quotes and pass it back (assumption is that the user knows what they
801     # are doing and won't appreciate us mucking up their query
802     elsif ( $index =~ ',' ) {
803         $weighted_query .= " $index=\"$operand\"";
804     }
805
806     #TODO: build better cases based on specific search indexes
807     else {
808         $weighted_query .= " $index,ext,r1=\"$operand\"";    # exact index
809           #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
810         $weighted_query .= " or $index,phr,r3=\"$operand\"";    # phrase index
811         $weighted_query .=
812           " or $index,rt,wrdl,r3=\"$operand\"";    # word list index
813     }
814
815     $weighted_query .= "))";                       # close rank specification
816     return $weighted_query;
817 }
818
819 =head2 getIndexes
820
821 Return an array with available indexes.
822
823 =cut
824
825 sub getIndexes{
826     my @indexes = (
827                     # biblio indexes
828                     'ab',
829                     'Abstract',
830                     'acqdate',
831                     'allrecords',
832                     'an',
833                     'Any',
834                     'at',
835                     'au',
836                     'aub',
837                     'aud',
838                     'audience',
839                     'auo',
840                     'aut',
841                     'Author',
842                     'Author-in-order ',
843                     'Author-personal-bibliography',
844                     'Authority-Number',
845                     'authtype',
846                     'bc',
847                     'Bib-level',
848                     'biblionumber',
849                     'bio',
850                     'biography',
851                     'callnum',
852                     'cfn',
853                     'Chronological-subdivision',
854                     'cn-bib-source',
855                     'cn-bib-sort',
856                     'cn-class',
857                     'cn-item',
858                     'cn-prefix',
859                     'cn-suffix',
860                     'cpn',
861                     'Code-institution',
862                     'Conference-name',
863                     'Conference-name-heading',
864                     'Conference-name-see',
865                     'Conference-name-seealso',
866                     'Content-type',
867                     'Control-number',
868                     'copydate',
869                     'Corporate-name',
870                     'Corporate-name-heading',
871                     'Corporate-name-see',
872                     'Corporate-name-seealso',
873                     'ctype',
874                     'date-entered-on-file',
875                     'Date-of-acquisition',
876                     'Date-of-publication',
877                     'Dewey-classification',
878                     'EAN',
879                     'extent',
880                     'fic',
881                     'fiction',
882                     'Form-subdivision',
883                     'format',
884                     'Geographic-subdivision',
885                     'he',
886                     'Heading',
887                     'Heading-use-main-or-added-entry',
888                     'Heading-use-series-added-entry ',
889                     'Heading-use-subject-added-entry',
890                     'Host-item',
891                     'id-other',
892                     'Illustration-code',
893                     'ISBN',
894                     'isbn',
895                     'ISSN',
896                     'issn',
897                     'itemtype',
898                     'kw',
899                     'Koha-Auth-Number',
900                     'l-format',
901                     'language',
902                     'lc-card',
903                     'LC-card-number',
904                     'lcn',
905                     'llength',
906                     'ln',
907                     'Local-classification',
908                     'Local-number',
909                     'Match-heading',
910                     'Match-heading-see-from',
911                     'Material-type',
912                     'mc-itemtype',
913                     'mc-rtype',
914                     'mus',
915                     'name',
916                     'Music-number',
917                     'Name-geographic',
918                     'Name-geographic-heading',
919                     'Name-geographic-see',
920                     'Name-geographic-seealso',
921                     'nb',
922                     'Note',
923                     'notes',
924                     'ns',
925                     'nt',
926                     'pb',
927                     'Personal-name',
928                     'Personal-name-heading',
929                     'Personal-name-see',
930                     'Personal-name-seealso',
931                     'pl',
932                     'Place-publication',
933                     'pn',
934                     'popularity',
935                     'pubdate',
936                     'Publisher',
937                     'Record-control-number',
938                     'rcn',
939                     'Record-type',
940                     'rtype',
941                     'se',
942                     'See',
943                     'See-also',
944                     'sn',
945                     'Stock-number',
946                     'su',
947                     'Subject',
948                     'Subject-heading-thesaurus',
949                     'Subject-name-personal',
950                     'Subject-subdivision',
951                     'Summary',
952                     'Suppress',
953                     'su-geo',
954                     'su-na',
955                     'su-to',
956                     'su-ut',
957                     'ut',
958                     'UPC',
959                     'Term-genre-form',
960                     'Term-genre-form-heading',
961                     'Term-genre-form-see',
962                     'Term-genre-form-seealso',
963                     'ti',
964                     'Title',
965                     'Title-cover',
966                     'Title-series',
967                     'Title-host',
968                     'Title-uniform',
969                     'Title-uniform-heading',
970                     'Title-uniform-see',
971                     'Title-uniform-seealso',
972                     'totalissues',
973                     'yr',
974
975                     # items indexes
976                     'acqsource',
977                     'barcode',
978                     'bc',
979                     'branch',
980                     'ccode',
981                     'classification-source',
982                     'cn-sort',
983                     'coded-location-qualifier',
984                     'copynumber',
985                     'damaged',
986                     'datelastborrowed',
987                     'datelastseen',
988                     'holdingbranch',
989                     'homebranch',
990                     'issues',
991                     'item',
992                     'itemnumber',
993                     'itype',
994                     'Local-classification',
995                     'location',
996                     'lost',
997                     'materials-specified',
998                     'mc-ccode',
999                     'mc-itype',
1000                     'mc-loc',
1001                     'notforloan',
1002                     'onloan',
1003                     'price',
1004                     'renewals',
1005                     'replacementprice',
1006                     'replacementpricedate',
1007                     'reserves',
1008                     'restricted',
1009                     'stack',
1010                     'stocknumber',
1011                     'inv',
1012                     'uri',
1013                     'withdrawn',
1014
1015                     # subject related
1016                   );
1017
1018     return \@indexes;
1019 }
1020
1021 =head2 buildQuery
1022
1023 ( $error, $query,
1024 $simple_query, $query_cgi,
1025 $query_desc, $limit,
1026 $limit_cgi, $limit_desc,
1027 $stopwords_removed, $query_type ) = buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
1028
1029 Build queries and limits in CCL, CGI, Human,
1030 handle truncation, stemming, field weighting, stopwords, fuzziness, etc.
1031
1032 See verbose embedded documentation.
1033
1034
1035 =cut
1036
1037 sub buildQuery {
1038     my ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang) = @_;
1039
1040     warn "---------\nEnter buildQuery\n---------" if $DEBUG;
1041
1042     # dereference
1043     my @operators = $operators ? @$operators : ();
1044     my @indexes   = $indexes   ? @$indexes   : ();
1045     my @operands  = $operands  ? @$operands  : ();
1046     my @limits    = $limits    ? @$limits    : ();
1047     my @sort_by   = $sort_by   ? @$sort_by   : ();
1048
1049     my $stemming         = C4::Context->preference("QueryStemming")        || 0;
1050     my $auto_truncation  = C4::Context->preference("QueryAutoTruncate")    || 0;
1051     my $weight_fields    = C4::Context->preference("QueryWeightFields")    || 0;
1052     my $fuzzy_enabled    = C4::Context->preference("QueryFuzzy")           || 0;
1053     my $remove_stopwords = C4::Context->preference("QueryRemoveStopwords") || 0;
1054
1055     # no stemming/weight/fuzzy in NoZebra
1056     if ( C4::Context->preference("NoZebra") ) {
1057         $stemming         = 0;
1058         $weight_fields    = 0;
1059         $fuzzy_enabled    = 0;
1060         $auto_truncation  = 0;
1061     }
1062
1063     my $query        = $operands[0];
1064     my $simple_query = $operands[0];
1065
1066     # initialize the variables we're passing back
1067     my $query_cgi;
1068     my $query_desc;
1069     my $query_type;
1070
1071     my $limit;
1072     my $limit_cgi;
1073     my $limit_desc;
1074
1075     my $stopwords_removed;    # flag to determine if stopwords have been removed
1076
1077     my $cclq       = 0;
1078     my $cclindexes = getIndexes();
1079     if ( $query !~ /\s*ccl=/ ) {
1080         while ( !$cclq && $query =~ /(?:^|\W)([\w-]+)(,[\w-]+)*[:=]/g ) {
1081             my $dx = lc($1);
1082             $cclq = grep { lc($_) eq $dx } @$cclindexes;
1083         }
1084         $query = "ccl=$query" if $cclq;
1085     }
1086
1087 # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
1088 # DIAGNOSTIC ONLY!!
1089     if ( $query =~ /^ccl=/ ) {
1090         my $q=$';
1091         # This is needed otherwise ccl= and &limit won't work together, and
1092         # this happens when selecting a subject on the opac-detail page
1093         if (@limits) {
1094             $q .= ' and '.join(' and ', @limits);
1095         }
1096         return ( undef, $q, $q, "q=ccl=$q", $q, '', '', '', '', 'ccl' );
1097     }
1098     if ( $query =~ /^cql=/ ) {
1099         return ( undef, $', $', "q=cql=$'", $', '', '', '', '', 'cql' );
1100     }
1101     if ( $query =~ /^pqf=/ ) {
1102         return ( undef, $', $', "q=pqf=$'", $', '', '', '', '', 'pqf' );
1103     }
1104
1105     # pass nested queries directly
1106     # FIXME: need better handling of some of these variables in this case
1107     # Nested queries aren't handled well and this implementation is flawed and causes users to be
1108     # unable to search for anything containing () commenting out, will be rewritten for 3.4.0
1109 #    if ( $query =~ /(\(|\))/ ) {
1110 #        return (
1111 #            undef,              $query, $simple_query, $query_cgi,
1112 #            $query,             $limit, $limit_cgi,    $limit_desc,
1113 #            $stopwords_removed, 'ccl'
1114 #        );
1115 #    }
1116
1117 # Form-based queries are non-nested and fixed depth, so we can easily modify the incoming
1118 # query operands and indexes and add stemming, truncation, field weighting, etc.
1119 # Once we do so, we'll end up with a value in $query, just like if we had an
1120 # incoming $query from the user
1121     else {
1122         $query = ""
1123           ; # clear it out so we can populate properly with field-weighted, stemmed, etc. query
1124         my $previous_operand
1125           ;    # a flag used to keep track if there was a previous query
1126                # if there was, we can apply the current operator
1127                # for every operand
1128         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
1129
1130             # COMBINE OPERANDS, INDEXES AND OPERATORS
1131             if ( $operands[$i] ) {
1132                 $operands[$i]=~s/^\s+//;
1133
1134               # A flag to determine whether or not to add the index to the query
1135                 my $indexes_set;
1136
1137 # If the user is sophisticated enough to specify an index, turn off field weighting, stemming, and stopword handling
1138                 if ( $operands[$i] =~ /\w(:|=)/ || $scan ) {
1139                     $weight_fields    = 0;
1140                     $stemming         = 0;
1141                     $remove_stopwords = 0;
1142                 } else {
1143                     $operands[$i] =~ s/\?/{?}/g; # need to escape question marks
1144                 }
1145                 my $operand = $operands[$i];
1146                 my $index   = $indexes[$i];
1147
1148                 # Add index-specific attributes
1149                 # Date of Publication
1150                 if ( $index eq 'yr' ) {
1151                     $index .= ",st-numeric";
1152                     $indexes_set++;
1153                                         $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1154                 }
1155
1156                 # Date of Acquisition
1157                 elsif ( $index eq 'acqdate' ) {
1158                     $index .= ",st-date-normalized";
1159                     $indexes_set++;
1160                                         $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1161                 }
1162                 # ISBN,ISSN,Standard Number, don't need special treatment
1163                 elsif ( $index eq 'nb' || $index eq 'ns' ) {
1164                     (
1165                         $stemming,      $auto_truncation,
1166                         $weight_fields, $fuzzy_enabled,
1167                         $remove_stopwords
1168                     ) = ( 0, 0, 0, 0, 0 );
1169
1170                 }
1171
1172                 if(not $index){
1173                     $index = 'kw';
1174                 }
1175
1176                 # Set default structure attribute (word list)
1177                 my $struct_attr = q{};
1178                 unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl|nb|ns)/ ) {
1179                     $struct_attr = ",wrdl";
1180                 }
1181
1182                 # Some helpful index variants
1183                 my $index_plus       = $index . $struct_attr . ':';
1184                 my $index_plus_comma = $index . $struct_attr . ',';
1185
1186                 # Remove Stopwords
1187                 if ($remove_stopwords) {
1188                     ( $operand, $stopwords_removed ) =
1189                       _remove_stopwords( $operand, $index );
1190                     warn "OPERAND w/out STOPWORDS: >$operand<" if $DEBUG;
1191                     warn "REMOVED STOPWORDS: @$stopwords_removed"
1192                       if ( $stopwords_removed && $DEBUG );
1193                 }
1194
1195                 if ($auto_truncation){
1196                                         unless ( $index =~ /(st-|phr|ext)/ ) {
1197                                                 #FIXME only valid with LTR scripts
1198                                                 $operand=join(" ",map{
1199                                                                                         (index($_,"*")>0?"$_":"$_*")
1200                                                                                          }split (/\s+/,$operand));
1201                                                 warn $operand if $DEBUG;
1202                                         }
1203                                 }
1204
1205                 # Detect Truncation
1206                 my $truncated_operand;
1207                 my( $nontruncated, $righttruncated, $lefttruncated,
1208                     $rightlefttruncated, $regexpr
1209                 ) = _detect_truncation( $operand, $index );
1210                 warn
1211 "TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<"
1212                   if $DEBUG;
1213
1214                 # Apply Truncation
1215                 if (
1216                     scalar(@$righttruncated) + scalar(@$lefttruncated) +
1217                     scalar(@$rightlefttruncated) > 0 )
1218                 {
1219
1220                # Don't field weight or add the index to the query, we do it here
1221                     $indexes_set = 1;
1222                     undef $weight_fields;
1223                     my $previous_truncation_operand;
1224                     if (scalar @$nontruncated) {
1225                         $truncated_operand .= "$index_plus @$nontruncated ";
1226                         $previous_truncation_operand = 1;
1227                     }
1228                     if (scalar @$righttruncated) {
1229                         $truncated_operand .= "and " if $previous_truncation_operand;
1230                         $truncated_operand .= $index_plus_comma . "rtrn:@$righttruncated ";
1231                         $previous_truncation_operand = 1;
1232                     }
1233                     if (scalar @$lefttruncated) {
1234                         $truncated_operand .= "and " if $previous_truncation_operand;
1235                         $truncated_operand .= $index_plus_comma . "ltrn:@$lefttruncated ";
1236                         $previous_truncation_operand = 1;
1237                     }
1238                     if (scalar @$rightlefttruncated) {
1239                         $truncated_operand .= "and " if $previous_truncation_operand;
1240                         $truncated_operand .= $index_plus_comma . "rltrn:@$rightlefttruncated ";
1241                         $previous_truncation_operand = 1;
1242                     }
1243                 }
1244                 $operand = $truncated_operand if $truncated_operand;
1245                 warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
1246
1247                 # Handle Stemming
1248                 my $stemmed_operand;
1249                 $stemmed_operand = _build_stemmed_operand($operand, $lang)
1250                                                                                 if $stemming;
1251
1252                 warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
1253
1254                 # Handle Field Weighting
1255                 my $weighted_operand;
1256                 if ($weight_fields) {
1257                     $weighted_operand = _build_weighted_query( $operand, $stemmed_operand, $index );
1258                     $operand = $weighted_operand;
1259                     $indexes_set = 1;
1260                 }
1261
1262                 warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
1263
1264                 # If there's a previous operand, we need to add an operator
1265                 if ($previous_operand) {
1266
1267                     # User-specified operator
1268                     if ( $operators[ $i - 1 ] ) {
1269                         $query     .= " $operators[$i-1] ";
1270                         $query     .= " $index_plus " unless $indexes_set;
1271                         $query     .= " $operand";
1272                         $query_cgi .= "&op=$operators[$i-1]";
1273                         $query_cgi .= "&idx=$index" if $index;
1274                         $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1275                         $query_desc .=
1276                           " $operators[$i-1] $index_plus $operands[$i]";
1277                     }
1278
1279                     # Default operator is and
1280                     else {
1281                         $query      .= " and ";
1282                         $query      .= "$index_plus " unless $indexes_set;
1283                         $query      .= "$operand";
1284                         $query_cgi  .= "&op=and&idx=$index" if $index;
1285                         $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
1286                         $query_desc .= " and $index_plus $operands[$i]";
1287                     }
1288                 }
1289
1290                 # There isn't a pervious operand, don't need an operator
1291                 else {
1292
1293                     # Field-weighted queries already have indexes set
1294                     $query .= " $index_plus " unless $indexes_set;
1295                     $query .= $operand;
1296                     $query_desc .= " $index_plus $operands[$i]";
1297                     $query_cgi  .= "&idx=$index" if $index;
1298                     $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
1299                     $previous_operand = 1;
1300                 }
1301             }    #/if $operands
1302         }    # /for
1303     }
1304     warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1305
1306     # add limits
1307     my %group_OR_limits;
1308     my $availability_limit;
1309     foreach my $this_limit (@limits) {
1310         if ( $this_limit =~ /available/ ) {
1311 #
1312 ## 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1313 ## In English:
1314 ## all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1315             $availability_limit .=
1316 "( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and (lost,st-numeric=0) )"; #or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
1317             $limit_cgi  .= "&limit=available";
1318             $limit_desc .= "";
1319         }
1320
1321         # group_OR_limits, prefixed by mc-
1322         # OR every member of the group
1323         elsif ( $this_limit =~ /mc/ ) {
1324             my ($k,$v) = split(/:/, $this_limit,2);
1325             if ( $k !~ /mc-i(tem)?type/ ) {
1326                 # in case the mc-ccode value has complicating chars like ()'s inside it we wrap in quotes
1327                 $this_limit =~ tr/"//d;
1328                 $this_limit = $k.":\"".$v."\"";
1329             }
1330
1331             $group_OR_limits{$k} .= " or " if $group_OR_limits{$k};
1332             $limit_desc      .= " or " if $group_OR_limits{$k};
1333             $group_OR_limits{$k} .= "$this_limit";
1334             $limit_cgi       .= "&limit=$this_limit";
1335             $limit_desc      .= " $this_limit";
1336         }
1337
1338         # Regular old limits
1339         else {
1340             $limit .= " and " if $limit || $query;
1341             $limit      .= "$this_limit";
1342             $limit_cgi  .= "&limit=$this_limit";
1343             if ($this_limit =~ /^branch:(.+)/) {
1344                 my $branchcode = $1;
1345                 my $branchname = GetBranchName($branchcode);
1346                 if (defined $branchname) {
1347                     $limit_desc .= " branch:$branchname";
1348                 } else {
1349                     $limit_desc .= " $this_limit";
1350                 }
1351             } else {
1352                 $limit_desc .= " $this_limit";
1353             }
1354         }
1355     }
1356     foreach my $k (keys (%group_OR_limits)) {
1357         $limit .= " and " if ( $query || $limit );
1358         $limit .= "($group_OR_limits{$k})";
1359     }
1360     if ($availability_limit) {
1361         $limit .= " and " if ( $query || $limit );
1362         $limit .= "($availability_limit)";
1363     }
1364
1365     # Normalize the query and limit strings
1366     # This is flawed , means we can't search anything with : in it
1367     # if user wants to do ccl or cql, start the query with that
1368 #    $query =~ s/:/=/g;
1369     $query =~ s/(?<=(ti|au|pb|su|an|kw|mc|nb|ns)):/=/g;
1370     $query =~ s/(?<=(wrdl)):/=/g;
1371     $query =~ s/(?<=(trn|phr)):/=/g;
1372     $limit =~ s/:/=/g;
1373     for ( $query, $query_desc, $limit, $limit_desc ) {
1374         s/  +/ /g;    # remove extra spaces
1375         s/^ //g;     # remove any beginning spaces
1376         s/ $//g;     # remove any ending spaces
1377         s/==/=/g;    # remove double == from query
1378     }
1379     $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1380
1381     for ($query_cgi,$simple_query) {
1382         s/"//g;
1383     }
1384     # append the limit to the query
1385     $query .= " " . $limit;
1386
1387     # Warnings if DEBUG
1388     if ($DEBUG) {
1389         warn "QUERY:" . $query;
1390         warn "QUERY CGI:" . $query_cgi;
1391         warn "QUERY DESC:" . $query_desc;
1392         warn "LIMIT:" . $limit;
1393         warn "LIMIT CGI:" . $limit_cgi;
1394         warn "LIMIT DESC:" . $limit_desc;
1395         warn "---------\nLeave buildQuery\n---------";
1396     }
1397     return (
1398         undef,              $query, $simple_query, $query_cgi,
1399         $query_desc,        $limit, $limit_cgi,    $limit_desc,
1400         $stopwords_removed, $query_type
1401     );
1402 }
1403
1404 =head2 searchResults
1405
1406   my @search_results = searchResults($search_context, $searchdesc, $hits, 
1407                                      $results_per_page, $offset, $scan, 
1408                                      @marcresults, $hidelostitems);
1409
1410 Format results in a form suitable for passing to the template
1411
1412 =cut
1413
1414 # IMO this subroutine is pretty messy still -- it's responsible for
1415 # building the HTML output for the template
1416 sub searchResults {
1417     my ( $search_context, $searchdesc, $hits, $results_per_page, $offset, $scan, $marcresults ) = @_;
1418     my $dbh = C4::Context->dbh;
1419     my @newresults;
1420
1421     $search_context = 'opac' if !$search_context || $search_context ne 'intranet';
1422     my ($is_opac, $hidelostitems);
1423     if ($search_context eq 'opac') {
1424         $hidelostitems = C4::Context->preference('hidelostitems');
1425         $is_opac       = 1;
1426     }
1427
1428     #Build branchnames hash
1429     #find branchname
1430     #get branch information.....
1431     my %branches;
1432     my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
1433     $bsth->execute();
1434     while ( my $bdata = $bsth->fetchrow_hashref ) {
1435         $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
1436     }
1437 # FIXME - We build an authorised values hash here, using the default framework
1438 # though it is possible to have different authvals for different fws.
1439
1440     my $shelflocations =GetKohaAuthorisedValues('items.location','');
1441
1442     # get notforloan authorised value list (see $shelflocations  FIXME)
1443     my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
1444
1445     #Build itemtype hash
1446     #find itemtype & itemtype image
1447     my %itemtypes;
1448     $bsth =
1449       $dbh->prepare(
1450         "SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes"
1451       );
1452     $bsth->execute();
1453     while ( my $bdata = $bsth->fetchrow_hashref ) {
1454                 foreach (qw(description imageurl summary notforloan)) {
1455                 $itemtypes{ $bdata->{'itemtype'} }->{$_} = $bdata->{$_};
1456                 }
1457     }
1458
1459     #search item field code
1460     my $sth =
1461       $dbh->prepare(
1462 "SELECT tagfield FROM marc_subfield_structure WHERE kohafield LIKE 'items.itemnumber'"
1463       );
1464     $sth->execute;
1465     my ($itemtag) = $sth->fetchrow;
1466
1467     ## find column names of items related to MARC
1468     my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
1469     $sth2->execute;
1470     my %subfieldstosearch;
1471     while ( ( my $column ) = $sth2->fetchrow ) {
1472         my ( $tagfield, $tagsubfield ) =
1473           &GetMarcFromKohaField( "items." . $column, "" );
1474         $subfieldstosearch{$column} = $tagsubfield;
1475     }
1476
1477     # handle which records to actually retrieve
1478     my $times;
1479     if ( $hits && $offset + $results_per_page <= $hits ) {
1480         $times = $offset + $results_per_page;
1481     }
1482     else {
1483         $times = $hits;  # FIXME: if $hits is undefined, why do we want to equal it?
1484     }
1485
1486         my $marcflavour = C4::Context->preference("marcflavour");
1487     # We get the biblionumber position in MARC
1488     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
1489
1490     # loop through all of the records we've retrieved
1491     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1492         my $marcrecord = MARC::File::USMARC::decode( $marcresults->[$i] );
1493         my $fw = $scan
1494              ? undef
1495              : $bibliotag < 10
1496                ? GetFrameworkCode($marcrecord->field($bibliotag)->data)
1497                : GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
1498         my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, $fw );
1499         $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
1500         $oldbiblio->{result_number} = $i + 1;
1501
1502         # add imageurl to itemtype if there is one
1503         $oldbiblio->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1504
1505         $oldbiblio->{'authorised_value_images'}  = ($search_context eq 'opac' && C4::Context->preference('AuthorisedValueImages')) || ($search_context eq 'intranet' && C4::Context->preference('StaffAuthorisedValueImages')) ? C4::Items::get_authorised_value_images( C4::Biblio::get_biblio_authorised_values( $oldbiblio->{'biblionumber'}, $marcrecord ) ) : [];
1506                 $oldbiblio->{normalized_upc}  = GetNormalizedUPC(       $marcrecord,$marcflavour);
1507                 $oldbiblio->{normalized_ean}  = GetNormalizedEAN(       $marcrecord,$marcflavour);
1508                 $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
1509                 $oldbiblio->{normalized_isbn} = GetNormalizedISBN(undef,$marcrecord,$marcflavour);
1510                 $oldbiblio->{content_identifier_exists} = 1 if ($oldbiblio->{normalized_isbn} or $oldbiblio->{normalized_oclc} or $oldbiblio->{normalized_ean} or $oldbiblio->{normalized_upc});
1511
1512                 # edition information, if any
1513         $oldbiblio->{edition} = $oldbiblio->{editionstatement};
1514                 $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1515  # Build summary if there is one (the summary is defined in the itemtypes table)
1516  # FIXME: is this used anywhere, I think it can be commented out? -- JF
1517         if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1518             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1519             my @fields  = $marcrecord->fields();
1520
1521             my $newsummary;
1522             foreach my $line ( "$summary\n" =~ /(.*)\n/g ){
1523                 my $tags = {};
1524                 foreach my $tag ( $line =~ /\[(\d{3}[\w|\d])\]/ ) {
1525                     $tag =~ /(.{3})(.)/;
1526                     if($marcrecord->field($1)){
1527                         my @abc = $marcrecord->field($1)->subfield($2);
1528                         $tags->{$tag} = $#abc + 1 ;
1529                     }
1530                 }
1531
1532                 # We catch how many times to repeat this line
1533                 my $max = 0;
1534                 foreach my $tag (keys(%$tags)){
1535                     $max = $tags->{$tag} if($tags->{$tag} > $max);
1536                  }
1537
1538                 # we replace, and repeat each line
1539                 for (my $i = 0 ; $i < $max ; $i++){
1540                     my $newline = $line;
1541
1542                     foreach my $tag ( $newline =~ /\[(\d{3}[\w|\d])\]/g ) {
1543                         $tag =~ /(.{3})(.)/;
1544
1545                         if($marcrecord->field($1)){
1546                             my @repl = $marcrecord->field($1)->subfield($2);
1547                             my $subfieldvalue = $repl[$i];
1548
1549                             if (! utf8::is_utf8($subfieldvalue)) {
1550                                 utf8::decode($subfieldvalue);
1551                             }
1552
1553                              $newline =~ s/\[$tag\]/$subfieldvalue/g;
1554                         }
1555                     }
1556                     $newsummary .= "$newline\n";
1557                 }
1558             }
1559
1560             $newsummary =~ s/\[(.*?)]//g;
1561             $newsummary =~ s/\n/<br\/>/g;
1562             $oldbiblio->{summary} = $newsummary;
1563         }
1564
1565         # Pull out the items fields
1566         my @fields = $marcrecord->field($itemtag);
1567         my $marcflavor = C4::Context->preference("marcflavour");
1568         # adding linked items that belong to host records
1569         my $analyticsfield = '773';
1570         if ($marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC') {
1571             $analyticsfield = '773';
1572         } elsif ($marcflavor eq 'UNIMARC') {
1573             $analyticsfield = '461';
1574         }
1575         foreach my $hostfield ( $marcrecord->field($analyticsfield)) {
1576             my $hostbiblionumber = $hostfield->subfield("0");
1577             my $linkeditemnumber = $hostfield->subfield("9");
1578             if(!$hostbiblionumber eq undef){
1579                 my $hostbiblio = GetMarcBiblio($hostbiblionumber, 1);
1580                 my ($itemfield, undef) = GetMarcFromKohaField( 'items.itemnumber', GetFrameworkCode($hostbiblionumber) );
1581                 if(!$hostbiblio eq undef){
1582                     my @hostitems = $hostbiblio->field($itemfield);
1583                     foreach my $hostitem (@hostitems){
1584                         if ($hostitem->subfield("9") eq $linkeditemnumber){
1585                             my $linkeditem =$hostitem;
1586                             # append linked items if they exist
1587                             if (!$linkeditem eq undef){
1588                                 push (@fields, $linkeditem);}
1589                         }
1590                     }
1591                 }
1592             }
1593         }
1594
1595         # Setting item statuses for display
1596         my @available_items_loop;
1597         my @onloan_items_loop;
1598         my @other_items_loop;
1599
1600         my $available_items;
1601         my $onloan_items;
1602         my $other_items;
1603
1604         my $ordered_count         = 0;
1605         my $available_count       = 0;
1606         my $onloan_count          = 0;
1607         my $longoverdue_count     = 0;
1608         my $other_count           = 0;
1609         my $wthdrawn_count        = 0;
1610         my $itemlost_count        = 0;
1611         my $hideatopac_count      = 0;
1612         my $itembinding_count     = 0;
1613         my $itemdamaged_count     = 0;
1614         my $item_in_transit_count = 0;
1615         my $can_place_holds       = 0;
1616         my $item_onhold_count     = 0;
1617         my $items_count           = scalar(@fields);
1618         my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults');
1619         my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1;
1620
1621         # loop through every item
1622               my @hiddenitems;
1623         foreach my $field (@fields) {
1624             my $item;
1625
1626             # populate the items hash
1627             foreach my $code ( keys %subfieldstosearch ) {
1628                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1629             }
1630             $item->{description} = $itemtypes{ $item->{itype} }{description};
1631
1632                 # Hidden items
1633             if ($is_opac) {
1634                     my @hi = GetHiddenItemnumbers($item);
1635                 $item->{'hideatopac'} = @hi;
1636               push @hiddenitems, @hi;
1637             }
1638
1639             my $hbranch     = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch'    : 'holdingbranch';
1640             my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
1641
1642             # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
1643             if ($item->{$hbranch}) {
1644                 $item->{'branchname'} = $branches{$item->{$hbranch}};
1645             }
1646             elsif ($item->{$otherbranch}) {     # Last resort
1647                 $item->{'branchname'} = $branches{$item->{$otherbranch}};
1648             }
1649
1650                         my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
1651 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
1652             my $userenv = C4::Context->userenv;
1653             if ( $item->{onloan} && !(C4::Members::GetHideLostItemsPreference($userenv->{'number'}) && $item->{itemlost}) ) {
1654                 $onloan_count++;
1655                                 my $key = $prefix . $item->{onloan} . $item->{barcode};
1656                                 $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
1657                                 $onloan_items->{$key}->{count}++ if $item->{$hbranch};
1658                                 $onloan_items->{$key}->{branchname} = $item->{branchname};
1659                                 $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1660                                 $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
1661                                 $onloan_items->{$key}->{description} = $item->{description};
1662                                 $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
1663                 # if something's checked out and lost, mark it as 'long overdue'
1664                 if ( $item->{itemlost} ) {
1665                     $onloan_items->{$prefix}->{longoverdue}++;
1666                     $longoverdue_count++;
1667                 } else {        # can place holds as long as item isn't lost
1668                     $can_place_holds = 1;
1669                 }
1670             }
1671
1672          # items not on loan, but still unavailable ( lost, withdrawn, damaged )
1673             else {
1674
1675                 # item is on order
1676                 if ( $item->{notforloan} == -1 ) {
1677                     $ordered_count++;
1678                 }
1679
1680                 # is item in transit?
1681                 my $transfertwhen = '';
1682                 my ($transfertfrom, $transfertto);
1683
1684                 # is item on the reserve shelf?
1685                 my $reservestatus = '';
1686                 my $reserveitem;
1687
1688                 unless ($item->{wthdrawn}
1689                         || $item->{itemlost}
1690                         || $item->{damaged}
1691                         || $item->{notforloan}
1692                         || $items_count > 20) {
1693
1694                     # A couple heuristics to limit how many times
1695                     # we query the database for item transfer information, sacrificing
1696                     # accuracy in some cases for speed;
1697                     #
1698                     # 1. don't query if item has one of the other statuses
1699                     # 2. don't check transit status if the bib has
1700                     #    more than 20 items
1701                     #
1702                     # FIXME: to avoid having the query the database like this, and to make
1703                     #        the in transit status count as unavailable for search limiting,
1704                     #        should map transit status to record indexed in Zebra.
1705                     #
1706                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
1707                     ($reservestatus, $reserveitem, undef) = C4::Reserves::CheckReserves($item->{itemnumber});
1708                 }
1709
1710                 # item is withdrawn, lost, damaged, not for loan, reserved or in transit
1711                 if (   $item->{wthdrawn}
1712                     || $item->{itemlost}
1713                     || $item->{damaged}
1714                     || $item->{notforloan} > 0
1715                     || $item->{hideatopac}
1716                     || $reservestatus eq 'Waiting'
1717                     || ($transfertwhen ne ''))
1718                 {
1719                     $wthdrawn_count++        if $item->{wthdrawn};
1720                     $itemlost_count++        if $item->{itemlost};
1721                     $itemdamaged_count++     if $item->{damaged};
1722                     $hideatopac_count++      if $item->{hideatopac};
1723                     $item_in_transit_count++ if $transfertwhen ne '';
1724                     $item_onhold_count++     if $reservestatus eq 'Waiting';
1725                     $item->{status} = $item->{wthdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
1726
1727                     # can place hold on item ?
1728                     if ((!$item->{damaged} || C4::Context->preference('AllowHoldsOnDamagedItems'))
1729                       && !$item->{itemlost}
1730                       && !$item->{withdrawn}
1731                     ) {
1732                         $can_place_holds = 1;
1733                     }
1734                     
1735                     $other_count++;
1736
1737                     my $key = $prefix . $item->{status};
1738                     foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber hideatopac)) {
1739                         $other_items->{$key}->{$_} = $item->{$_};
1740                     }
1741                     $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
1742                     $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
1743                                         $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
1744                                         $other_items->{$key}->{count}++ if $item->{$hbranch};
1745                                         $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1746                                         $other_items->{$key}->{description} = $item->{description};
1747                                         $other_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
1748                 }
1749                 # item is available
1750                 else {
1751                     $can_place_holds = 1;
1752                     $available_count++;
1753                                         $available_items->{$prefix}->{count}++ if $item->{$hbranch};
1754                                         foreach (qw(branchname itemcallnumber hideatopac description)) {
1755                         $available_items->{$prefix}->{$_} = $item->{$_};
1756                                         }
1757                                         $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
1758                                         $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
1759                 }
1760             }
1761         }    # notforloan, item level and biblioitem level
1762         if ($items_count > 0) {
1763         next if $is_opac       && $hideatopac_count >= $items_count;
1764         next if $hidelostitems && $itemlost_count   >= $items_count;
1765         }
1766         my ( $availableitemscount, $onloanitemscount, $otheritemscount );
1767         for my $key ( sort keys %$onloan_items ) {
1768             (++$onloanitemscount > $maxitems) and last;
1769             push @onloan_items_loop, $onloan_items->{$key};
1770         }
1771         for my $key ( sort keys %$other_items ) {
1772             (++$otheritemscount > $maxitems) and last;
1773             push @other_items_loop, $other_items->{$key};
1774         }
1775         for my $key ( sort keys %$available_items ) {
1776             (++$availableitemscount > $maxitems) and last;
1777             push @available_items_loop, $available_items->{$key}
1778         }
1779
1780         # XSLT processing of some stuff
1781         use C4::Charset;
1782         SetUTF8Flag($marcrecord);
1783         $debug && warn $marcrecord->as_formatted;
1784         my $interface = $search_context eq 'opac' ? 'OPAC' : '';
1785         if (!$scan && C4::Context->preference($interface . "XSLTResultsDisplay")) {
1786             $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display($oldbiblio->{biblionumber}, $marcrecord, $interface."XSLTResultsDisplay", 1, \@hiddenitems);
1787             # the last parameter tells Koha to clean up the problematic ampersand entities that Zebra outputs
1788         }
1789
1790         # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
1791         if (!C4::Context->preference("item-level_itypes")) {
1792             if ($itemtypes{ $oldbiblio->{itemtype} }->{notforloan}) {
1793                 $can_place_holds = 0;
1794             }
1795         }
1796         $oldbiblio->{norequests} = 1 unless $can_place_holds;
1797         $oldbiblio->{itemsplural}          = 1 if $items_count > 1;
1798         $oldbiblio->{items_count}          = $items_count;
1799         $oldbiblio->{available_items_loop} = \@available_items_loop;
1800         $oldbiblio->{onloan_items_loop}    = \@onloan_items_loop;
1801         $oldbiblio->{other_items_loop}     = \@other_items_loop;
1802         $oldbiblio->{availablecount}       = $available_count;
1803         $oldbiblio->{availableplural}      = 1 if $available_count > 1;
1804         $oldbiblio->{onloancount}          = $onloan_count;
1805         $oldbiblio->{onloanplural}         = 1 if $onloan_count > 1;
1806         $oldbiblio->{othercount}           = $other_count;
1807         $oldbiblio->{otherplural}          = 1 if $other_count > 1;
1808         $oldbiblio->{wthdrawncount}        = $wthdrawn_count;
1809         $oldbiblio->{itemlostcount}        = $itemlost_count;
1810         $oldbiblio->{damagedcount}         = $itemdamaged_count;
1811         $oldbiblio->{intransitcount}       = $item_in_transit_count;
1812         $oldbiblio->{onholdcount}          = $item_onhold_count;
1813         $oldbiblio->{orderedcount}         = $ordered_count;
1814         # deleting - in isbn to enable amazon content
1815         $oldbiblio->{isbn} =~ s/-//g;
1816
1817         if (C4::Context->preference("AlternateHoldingsField") && $items_count == 0) {
1818             my $fieldspec = C4::Context->preference("AlternateHoldingsField");
1819             my $subfields = substr $fieldspec, 3;
1820             my $holdingsep = C4::Context->preference("AlternateHoldingsSeparator") || ' ';
1821             my @alternateholdingsinfo = ();
1822             my @holdingsfields = $marcrecord->field(substr $fieldspec, 0, 3);
1823             my $alternateholdingscount = 0;
1824
1825             for my $field (@holdingsfields) {
1826                 my %holding = ( holding => '' );
1827                 my $havesubfield = 0;
1828                 for my $subfield ($field->subfields()) {
1829                     if ((index $subfields, $$subfield[0]) >= 0) {
1830                         $holding{'holding'} .= $holdingsep if (length $holding{'holding'} > 0);
1831                         $holding{'holding'} .= $$subfield[1];
1832                         $havesubfield++;
1833                     }
1834                 }
1835                 if ($havesubfield) {
1836                     push(@alternateholdingsinfo, \%holding);
1837                     $alternateholdingscount++;
1838                 }
1839             }
1840
1841             $oldbiblio->{'ALTERNATEHOLDINGS'} = \@alternateholdingsinfo;
1842             $oldbiblio->{'alternateholdings_count'} = $alternateholdingscount;
1843         }
1844
1845         push( @newresults, $oldbiblio );
1846     }
1847
1848     return @newresults;
1849 }
1850
1851 =head2 SearchAcquisitions
1852     Search for acquisitions
1853 =cut
1854
1855 sub SearchAcquisitions{
1856     my ($datebegin, $dateend, $itemtypes,$criteria, $orderby) = @_;
1857
1858     my $dbh=C4::Context->dbh;
1859     # Variable initialization
1860     my $str=qq|
1861     SELECT marcxml
1862     FROM biblio
1863     LEFT JOIN biblioitems ON biblioitems.biblionumber=biblio.biblionumber
1864     LEFT JOIN items ON items.biblionumber=biblio.biblionumber
1865     WHERE dateaccessioned BETWEEN ? AND ?
1866     |;
1867
1868     my (@params,@loopcriteria);
1869
1870     push @params, $datebegin->output("iso");
1871     push @params, $dateend->output("iso");
1872
1873     if (scalar(@$itemtypes)>0 and $criteria ne "itemtype" ){
1874         if(C4::Context->preference("item-level_itypes")){
1875             $str .= "AND items.itype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1876         }else{
1877             $str .= "AND biblioitems.itemtype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1878         }
1879         push @params, @$itemtypes;
1880     }
1881
1882     if ($criteria =~/itemtype/){
1883         if(C4::Context->preference("item-level_itypes")){
1884             $str .= "AND items.itype=? ";
1885         }else{
1886             $str .= "AND biblioitems.itemtype=? ";
1887         }
1888
1889         if(scalar(@$itemtypes) == 0){
1890             my $itypes = GetItemTypes();
1891             for my $key (keys %$itypes){
1892                 push @$itemtypes, $key;
1893             }
1894         }
1895
1896         @loopcriteria= @$itemtypes;
1897     }elsif ($criteria=~/itemcallnumber/){
1898         $str .= "AND (items.itemcallnumber LIKE CONCAT(?,'%')
1899                  OR items.itemcallnumber is NULL
1900                  OR items.itemcallnumber = '')";
1901
1902         @loopcriteria = ("AA".."ZZ", "") unless (scalar(@loopcriteria)>0);
1903     }else {
1904         $str .= "AND biblio.title LIKE CONCAT(?,'%') ";
1905         @loopcriteria = ("A".."z") unless (scalar(@loopcriteria)>0);
1906     }
1907
1908     if ($orderby =~ /date_desc/){
1909         $str.=" ORDER BY dateaccessioned DESC";
1910     } else {
1911         $str.=" ORDER BY title";
1912     }
1913
1914     my $qdataacquisitions=$dbh->prepare($str);
1915
1916     my @loopacquisitions;
1917     foreach my $value(@loopcriteria){
1918         push @params,$value;
1919         my %cell;
1920         $cell{"title"}=$value;
1921         $cell{"titlecode"}=$value;
1922
1923         eval{$qdataacquisitions->execute(@params);};
1924
1925         if ($@){ warn "recentacquisitions Error :$@";}
1926         else {
1927             my @loopdata;
1928             while (my $data=$qdataacquisitions->fetchrow_hashref){
1929                 push @loopdata, {"summary"=>GetBiblioSummary( $data->{'marcxml'} ) };
1930             }
1931             $cell{"loopdata"}=\@loopdata;
1932         }
1933         push @loopacquisitions,\%cell if (scalar(@{$cell{loopdata}})>0);
1934         pop @params;
1935     }
1936     $qdataacquisitions->finish;
1937     return \@loopacquisitions;
1938 }
1939 #----------------------------------------------------------------------
1940 #
1941 # Non-Zebra GetRecords#
1942 #----------------------------------------------------------------------
1943
1944 =head2 NZgetRecords
1945
1946   NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1947
1948 =cut
1949
1950 sub NZgetRecords {
1951     my (
1952         $query,            $simple_query, $sort_by_ref,    $servers_ref,
1953         $results_per_page, $offset,       $expanded_facet, $branches,
1954         $query_type,       $scan
1955     ) = @_;
1956     warn "query =$query" if $DEBUG;
1957     my $result = NZanalyse($query);
1958     warn "results =$result" if $DEBUG;
1959     return ( undef,
1960         NZorder( $result, @$sort_by_ref[0], $results_per_page, $offset ),
1961         undef );
1962 }
1963
1964 =head2 NZanalyse
1965
1966   NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1967   the list is built from an inverted index in the nozebra SQL table
1968   note that title is here only for convenience : the sorting will be very fast when requested on title
1969   if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1970
1971 =cut
1972
1973 sub NZanalyse {
1974     my ( $string, $server ) = @_;
1975 #     warn "---------"       if $DEBUG;
1976     warn " NZanalyse" if $DEBUG;
1977 #     warn "---------"       if $DEBUG;
1978
1979  # $server contains biblioserver or authorities, depending on what we search on.
1980  #warn "querying : $string on $server";
1981     $server = 'biblioserver' unless $server;
1982
1983 # if we have a ", replace the content to discard temporarily any and/or/not inside
1984     my $commacontent;
1985     if ( $string =~ /"/ ) {
1986         $string =~ s/"(.*?)"/__X__/;
1987         $commacontent = $1;
1988         warn "commacontent : $commacontent" if $DEBUG;
1989     }
1990
1991 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1992 # then, call again NZanalyse with $left and $right
1993 # (recursive until we find a leaf (=> something without and/or/not)
1994 # delete repeated operator... Would then go in infinite loop
1995     while ( $string =~ s/( and| or| not| AND| OR| NOT)\1/$1/g ) {
1996     }
1997
1998     #process parenthesis before.
1999     if ( $string =~ /^\s*\((.*)\)(( and | or | not | AND | OR | NOT )(.*))?/ ) {
2000         my $left     = $1;
2001         my $right    = $4;
2002         my $operator = lc($3);   # FIXME: and/or/not are operators, not operands
2003         warn
2004 "dealing w/parenthesis before recursive sub call. left :$left operator:$operator right:$right"
2005           if $DEBUG;
2006         my $leftresult = NZanalyse( $left, $server );
2007         if ($operator) {
2008             my $rightresult = NZanalyse( $right, $server );
2009
2010             # OK, we have the results for right and left part of the query
2011             # depending of operand, intersect, union or exclude both lists
2012             # to get a result list
2013             if ( $operator eq ' and ' ) {
2014                 return NZoperatorAND($leftresult,$rightresult);
2015             }
2016             elsif ( $operator eq ' or ' ) {
2017
2018                 # just merge the 2 strings
2019                 return $leftresult . $rightresult;
2020             }
2021             elsif ( $operator eq ' not ' ) {
2022                 return NZoperatorNOT($leftresult,$rightresult);
2023             }
2024         }
2025         else {
2026 # this error is impossible, because of the regexp that isolate the operand, but just in case...
2027             return $leftresult;
2028         }
2029     }
2030     warn "string :" . $string if $DEBUG;
2031     my $left = "";
2032     my $right = "";
2033     my $operator = "";
2034     if ($string =~ /(.*?)( and | or | not | AND | OR | NOT )(.*)/) {
2035         $left     = $1;
2036         $right    = $3;
2037         $operator = lc($2);    # FIXME: and/or/not are operators, not operands
2038     }
2039     warn "no parenthesis. left : $left operator: $operator right: $right"
2040       if $DEBUG;
2041
2042     # it's not a leaf, we have a and/or/not
2043     if ($operator) {
2044
2045         # reintroduce comma content if needed
2046         $right =~ s/__X__/"$commacontent"/ if $commacontent;
2047         $left  =~ s/__X__/"$commacontent"/ if $commacontent;
2048         warn "node : $left / $operator / $right\n" if $DEBUG;
2049         my $leftresult  = NZanalyse( $left,  $server );
2050         my $rightresult = NZanalyse( $right, $server );
2051         warn " leftresult : $leftresult" if $DEBUG;
2052         warn " rightresult : $rightresult" if $DEBUG;
2053         # OK, we have the results for right and left part of the query
2054         # depending of operand, intersect, union or exclude both lists
2055         # to get a result list
2056         if ( $operator eq ' and ' ) {
2057             return NZoperatorAND($leftresult,$rightresult);
2058         }
2059         elsif ( $operator eq ' or ' ) {
2060
2061             # just merge the 2 strings
2062             return $leftresult . $rightresult;
2063         }
2064         elsif ( $operator eq ' not ' ) {
2065             return NZoperatorNOT($leftresult,$rightresult);
2066         }
2067         else {
2068
2069 # this error is impossible, because of the regexp that isolate the operand, but just in case...
2070             die "error : operand unknown : $operator for $string";
2071         }
2072
2073         # it's a leaf, do the real SQL query and return the result
2074     }
2075     else {
2076         $string =~ s/__X__/"$commacontent"/ if $commacontent;
2077         $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|&|\+|\*|\// /g;
2078         #remove trailing blank at the beginning
2079         $string =~ s/^ //g;
2080         warn "leaf:$string" if $DEBUG;
2081
2082         # parse the string in in operator/operand/value again
2083         my $left = "";
2084         my $operator = "";
2085         my $right = "";
2086         if ($string =~ /(.*)(>=|<=)(.*)/) {
2087             $left     = $1;
2088             $operator = $2;
2089             $right    = $3;
2090         } else {
2091             $left = $string;
2092         }
2093 #         warn "handling leaf... left:$left operator:$operator right:$right"
2094 #           if $DEBUG;
2095         unless ($operator) {
2096             if ($string =~ /(.*)(>|<|=)(.*)/) {
2097                 $left     = $1;
2098                 $operator = $2;
2099                 $right    = $3;
2100                 warn
2101     "handling unless (operator)... left:$left operator:$operator right:$right"
2102                 if $DEBUG;
2103             } else {
2104                 $left = $string;
2105             }
2106         }
2107         my $results;
2108
2109 # strip adv, zebra keywords, currently not handled in nozebra: wrdl, ext, phr...
2110         $left =~ s/ .*$//;
2111
2112         # automatic replace for short operators
2113         $left = 'title'            if $left =~ '^ti$';
2114         $left = 'author'           if $left =~ '^au$';
2115         $left = 'publisher'        if $left =~ '^pb$';
2116         $left = 'subject'          if $left =~ '^su$';
2117         $left = 'koha-Auth-Number' if $left =~ '^an$';
2118         $left = 'keyword'          if $left =~ '^kw$';
2119         $left = 'itemtype'         if $left =~ '^mc$'; # Fix for Bug 2599 - Search limits not working for NoZebra
2120         warn "handling leaf... left:$left operator:$operator right:$right" if $DEBUG;
2121         my $dbh = C4::Context->dbh;
2122         if ( $operator && $left ne 'keyword' ) {
2123             #do a specific search
2124             $operator = 'LIKE' if $operator eq '=' and $right =~ /%/;
2125             my $sth = $dbh->prepare(
2126 "SELECT biblionumbers,value FROM nozebra WHERE server=? AND indexname=? AND value $operator ?"
2127             );
2128             warn "$left / $operator / $right\n" if $DEBUG;
2129
2130             # split each word, query the DB and build the biblionumbers result
2131             #sanitizing leftpart
2132             $left =~ s/^\s+|\s+$//;
2133             foreach ( split / /, $right ) {
2134                 my $biblionumbers;
2135                 $_ =~ s/^\s+|\s+$//;
2136                 next unless $_;
2137                 warn "EXECUTE : $server, $left, $_" if $DEBUG;
2138                 $sth->execute( $server, $left, $_ )
2139                   or warn "execute failed: $!";
2140                 while ( my ( $line, $value ) = $sth->fetchrow ) {
2141
2142 # if we are dealing with a numeric value, use only numeric results (in case of >=, <=, > or <)
2143 # otherwise, fill the result
2144                     $biblionumbers .= $line
2145                       unless ( $right =~ /^\d+$/ && $value =~ /\D/ );
2146                     warn "result : $value "
2147                       . ( $right  =~ /\d/ ) . "=="
2148                       . ( $value =~ /\D/?$line:"" ) if $DEBUG;         #= $line";
2149                 }
2150
2151 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2152                 if ($results) {
2153                     warn "NZAND" if $DEBUG;
2154                     $results = NZoperatorAND($biblionumbers,$results);
2155                 } else {
2156                     $results = $biblionumbers;
2157                 }
2158             }
2159         }
2160         else {
2161       #do a complete search (all indexes), if index='kw' do complete search too.
2162             my $sth = $dbh->prepare(
2163 "SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?"
2164             );
2165
2166             # split each word, query the DB and build the biblionumbers result
2167             foreach ( split / /, $string ) {
2168                 next if C4::Context->stopwords->{ uc($_) };   # skip if stopword
2169                 warn "search on all indexes on $_" if $DEBUG;
2170                 my $biblionumbers;
2171                 next unless $_;
2172                 $sth->execute( $server, $_ );
2173                 while ( my $line = $sth->fetchrow ) {
2174                     $biblionumbers .= $line;
2175                 }
2176
2177 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2178                 if ($results) {
2179                     $results = NZoperatorAND($biblionumbers,$results);
2180                 }
2181                 else {
2182                     warn "NEW RES for $_ = $biblionumbers" if $DEBUG;
2183                     $results = $biblionumbers;
2184                 }
2185             }
2186         }
2187         warn "return : $results for LEAF : $string" if $DEBUG;
2188         return $results;
2189     }
2190     warn "---------\nLeave NZanalyse\n---------" if $DEBUG;
2191 }
2192
2193 sub NZoperatorAND{
2194     my ($rightresult, $leftresult)=@_;
2195
2196     my @leftresult = split /;/, $leftresult;
2197     warn " @leftresult / $rightresult \n" if $DEBUG;
2198
2199     #             my @rightresult = split /;/,$leftresult;
2200     my $finalresult;
2201
2202 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
2203 # the result is stored twice, to have the same weight for AND than OR.
2204 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
2205 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
2206     foreach (@leftresult) {
2207         my $value = $_;
2208         my $countvalue;
2209         ( $value, $countvalue ) = ( $1, $2 ) if ($value=~/(.*)-(\d+)$/);
2210         if ( $rightresult =~ /\Q$value\E-(\d+);/ ) {
2211             $countvalue = ( $1 > $countvalue ? $countvalue : $1 );
2212             $finalresult .=
2213                 "$value-$countvalue;$value-$countvalue;";
2214         }
2215     }
2216     warn "NZAND DONE : $finalresult \n" if $DEBUG;
2217     return $finalresult;
2218 }
2219
2220 sub NZoperatorOR{
2221     my ($rightresult, $leftresult)=@_;
2222     return $rightresult.$leftresult;
2223 }
2224
2225 sub NZoperatorNOT{
2226     my ($leftresult, $rightresult)=@_;
2227
2228     my @leftresult = split /;/, $leftresult;
2229
2230     #             my @rightresult = split /;/,$leftresult;
2231     my $finalresult;
2232     foreach (@leftresult) {
2233         my $value=$_;
2234         $value=$1 if $value=~m/(.*)-\d+$/;
2235         unless ($rightresult =~ "$value-") {
2236             $finalresult .= "$_;";
2237         }
2238     }
2239     return $finalresult;
2240 }
2241
2242 =head2 NZorder
2243
2244   $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
2245
2246   TODO :: Description
2247
2248 =cut
2249
2250 sub NZorder {
2251     my ( $biblionumbers, $ordering, $results_per_page, $offset ) = @_;
2252     warn "biblionumbers = $biblionumbers and ordering = $ordering\n" if $DEBUG;
2253
2254     # order title asc by default
2255     #     $ordering = '1=36 <i' unless $ordering;
2256     $results_per_page = 20 unless $results_per_page;
2257     $offset           = 0  unless $offset;
2258     my $dbh = C4::Context->dbh;
2259
2260     #
2261     # order by POPULARITY
2262     #
2263     if ( $ordering =~ /popularity/ ) {
2264         my %result;
2265         my %popularity;
2266
2267         # popularity is not in MARC record, it's builded from a specific query
2268         my $sth =
2269           $dbh->prepare("select sum(issues) from items where biblionumber=?");
2270         foreach ( split /;/, $biblionumbers ) {
2271             my ( $biblionumber, $title ) = split /,/, $_;
2272             $result{$biblionumber} = GetMarcBiblio($biblionumber);
2273             $sth->execute($biblionumber);
2274             my $popularity = $sth->fetchrow || 0;
2275
2276 # hint : the key is popularity.title because we can have
2277 # many results with the same popularity. In this case, sub-ordering is done by title
2278 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
2279 # (un-frequent, I agree, but we won't forget anything that way ;-)
2280             $popularity{ sprintf( "%10d", $popularity ) . $title
2281                   . $biblionumber } = $biblionumber;
2282         }
2283
2284     # sort the hash and return the same structure as GetRecords (Zebra querying)
2285         my $result_hash;
2286         my $numbers = 0;
2287         if ( $ordering eq 'popularity_dsc' ) {    # sort popularity DESC
2288             foreach my $key ( sort { $b cmp $a } ( keys %popularity ) ) {
2289                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2290                   $result{ $popularity{$key} }->as_usmarc();
2291             }
2292         }
2293         else {                                    # sort popularity ASC
2294             foreach my $key ( sort ( keys %popularity ) ) {
2295                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2296                   $result{ $popularity{$key} }->as_usmarc();
2297             }
2298         }
2299         my $finalresult = ();
2300         $result_hash->{'hits'}         = $numbers;
2301         $finalresult->{'biblioserver'} = $result_hash;
2302         return $finalresult;
2303
2304         #
2305         # ORDER BY author
2306         #
2307     }
2308     elsif ( $ordering =~ /author/ ) {
2309         my %result;
2310         foreach ( split /;/, $biblionumbers ) {
2311             my ( $biblionumber, $title ) = split /,/, $_;
2312             my $record = GetMarcBiblio($biblionumber);
2313             my $author;
2314             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2315                 $author = $record->subfield( '200', 'f' );
2316                 $author = $record->subfield( '700', 'a' ) unless $author;
2317             }
2318             else {
2319                 $author = $record->subfield( '100', 'a' );
2320             }
2321
2322 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2323 # and we don't want to get only 1 result for each of them !!!
2324             $result{ $author . $biblionumber } = $record;
2325         }
2326
2327     # sort the hash and return the same structure as GetRecords (Zebra querying)
2328         my $result_hash;
2329         my $numbers = 0;
2330         if ( $ordering eq 'author_za' || $ordering eq 'author_dsc' ) {    # sort by author desc
2331             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2332                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2333                   $result{$key}->as_usmarc();
2334             }
2335         }
2336         else {                               # sort by author ASC
2337             foreach my $key ( sort ( keys %result ) ) {
2338                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2339                   $result{$key}->as_usmarc();
2340             }
2341         }
2342         my $finalresult = ();
2343         $result_hash->{'hits'}         = $numbers;
2344         $finalresult->{'biblioserver'} = $result_hash;
2345         return $finalresult;
2346
2347         #
2348         # ORDER BY callnumber
2349         #
2350     }
2351     elsif ( $ordering =~ /callnumber/ ) {
2352         my %result;
2353         foreach ( split /;/, $biblionumbers ) {
2354             my ( $biblionumber, $title ) = split /,/, $_;
2355             my $record = GetMarcBiblio($biblionumber);
2356             my $callnumber;
2357             my $frameworkcode = GetFrameworkCode($biblionumber);
2358             my ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField(  'items.itemcallnumber', $frameworkcode);
2359                ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField('biblioitems.callnumber', $frameworkcode)
2360                 unless $callnumber_tag;
2361             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2362                 $callnumber = $record->subfield( '200', 'f' );
2363             } else {
2364                 $callnumber = $record->subfield( '100', 'a' );
2365             }
2366
2367 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2368 # and we don't want to get only 1 result for each of them !!!
2369             $result{ $callnumber . $biblionumber } = $record;
2370         }
2371
2372     # sort the hash and return the same structure as GetRecords (Zebra querying)
2373         my $result_hash;
2374         my $numbers = 0;
2375         if ( $ordering eq 'call_number_dsc' ) {    # sort by title desc
2376             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2377                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2378                   $result{$key}->as_usmarc();
2379             }
2380         }
2381         else {                                     # sort by title ASC
2382             foreach my $key ( sort { $a cmp $b } ( keys %result ) ) {
2383                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2384                   $result{$key}->as_usmarc();
2385             }
2386         }
2387         my $finalresult = ();
2388         $result_hash->{'hits'}         = $numbers;
2389         $finalresult->{'biblioserver'} = $result_hash;
2390         return $finalresult;
2391     }
2392     elsif ( $ordering =~ /pubdate/ ) {             #pub year
2393         my %result;
2394         foreach ( split /;/, $biblionumbers ) {
2395             my ( $biblionumber, $title ) = split /,/, $_;
2396             my $record = GetMarcBiblio($biblionumber);
2397             my ( $publicationyear_tag, $publicationyear_subfield ) =
2398               GetMarcFromKohaField( 'biblioitems.publicationyear', '' );
2399             my $publicationyear =
2400               $record->subfield( $publicationyear_tag,
2401                 $publicationyear_subfield );
2402
2403 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2404 # and we don't want to get only 1 result for each of them !!!
2405             $result{ $publicationyear . $biblionumber } = $record;
2406         }
2407
2408     # sort the hash and return the same structure as GetRecords (Zebra querying)
2409         my $result_hash;
2410         my $numbers = 0;
2411         if ( $ordering eq 'pubdate_dsc' ) {    # sort by pubyear desc
2412             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2413                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2414                   $result{$key}->as_usmarc();
2415             }
2416         }
2417         else {                                 # sort by pub year ASC
2418             foreach my $key ( sort ( keys %result ) ) {
2419                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2420                   $result{$key}->as_usmarc();
2421             }
2422         }
2423         my $finalresult = ();
2424         $result_hash->{'hits'}         = $numbers;
2425         $finalresult->{'biblioserver'} = $result_hash;
2426         return $finalresult;
2427
2428         #
2429         # ORDER BY title
2430         #
2431     }
2432     elsif ( $ordering =~ /title/ ) {
2433
2434 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
2435         my %result;
2436         foreach ( split /;/, $biblionumbers ) {
2437             my ( $biblionumber, $title ) = split /,/, $_;
2438
2439 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2440 # and we don't want to get only 1 result for each of them !!!
2441 # hint & speed improvement : we can order without reading the record
2442 # so order, and read records only for the requested page !
2443             $result{ $title . $biblionumber } = $biblionumber;
2444         }
2445
2446     # sort the hash and return the same structure as GetRecords (Zebra querying)
2447         my $result_hash;
2448         my $numbers = 0;
2449         if ( $ordering eq 'title_az' ) {    # sort by title desc
2450             foreach my $key ( sort ( keys %result ) ) {
2451                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2452             }
2453         }
2454         else {                              # sort by title ASC
2455             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2456                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2457             }
2458         }
2459
2460         # limit the $results_per_page to result size if it's more
2461         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2462
2463         # for the requested page, replace biblionumber by the complete record
2464         # speed improvement : avoid reading too much things
2465         for (
2466             my $counter = $offset ;
2467             $counter <= $offset + $results_per_page ;
2468             $counter++
2469           )
2470         {
2471             $result_hash->{'RECORDS'}[$counter] =
2472               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc;
2473         }
2474         my $finalresult = ();
2475         $result_hash->{'hits'}         = $numbers;
2476         $finalresult->{'biblioserver'} = $result_hash;
2477         return $finalresult;
2478     }
2479     else {
2480
2481 #
2482 # order by ranking
2483 #
2484 # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
2485         my %result;
2486         my %count_ranking;
2487         foreach ( split /;/, $biblionumbers ) {
2488             my ( $biblionumber, $title ) = split /,/, $_;
2489             $title =~ /(.*)-(\d)/;
2490
2491             # get weight
2492             my $ranking = $2;
2493
2494 # note that we + the ranking because ranking is calculated on weight of EACH term requested.
2495 # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
2496 # biblio N has ranking = 6
2497             $count_ranking{$biblionumber} += $ranking;
2498         }
2499
2500 # build the result by "inverting" the count_ranking hash
2501 # 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
2502 #         warn "counting";
2503         foreach ( keys %count_ranking ) {
2504             $result{ sprintf( "%10d", $count_ranking{$_} ) . '-' . $_ } = $_;
2505         }
2506
2507     # sort the hash and return the same structure as GetRecords (Zebra querying)
2508         my $result_hash;
2509         my $numbers = 0;
2510         foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2511             $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2512         }
2513
2514         # limit the $results_per_page to result size if it's more
2515         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2516
2517         # for the requested page, replace biblionumber by the complete record
2518         # speed improvement : avoid reading too much things
2519         for (
2520             my $counter = $offset ;
2521             $counter <= $offset + $results_per_page ;
2522             $counter++
2523           )
2524         {
2525             $result_hash->{'RECORDS'}[$counter] =
2526               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc
2527               if $result_hash->{'RECORDS'}[$counter];
2528         }
2529         my $finalresult = ();
2530         $result_hash->{'hits'}         = $numbers;
2531         $finalresult->{'biblioserver'} = $result_hash;
2532         return $finalresult;
2533     }
2534 }
2535
2536 =head2 enabled_staff_search_views
2537
2538 %hash = enabled_staff_search_views()
2539
2540 This function returns a hash that contains three flags obtained from the system
2541 preferences, used to determine whether a particular staff search results view
2542 is enabled.
2543
2544 =over 2
2545
2546 =item C<Output arg:>
2547
2548     * $hash{can_view_MARC} is true only if the MARC view is enabled
2549     * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2550     * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2551
2552 =item C<usage in the script:>
2553
2554 =back
2555
2556 $template->param ( C4::Search::enabled_staff_search_views );
2557
2558 =cut
2559
2560 sub enabled_staff_search_views
2561 {
2562         return (
2563                 can_view_MARC                   => C4::Context->preference('viewMARC'),                 # 1 if the staff search allows the MARC view
2564                 can_view_ISBD                   => C4::Context->preference('viewISBD'),                 # 1 if the staff search allows the ISBD view
2565                 can_view_labeledMARC    => C4::Context->preference('viewLabeledMARC'),  # 1 if the staff search allows the Labeled MARC view
2566         );
2567 }
2568
2569 sub AddSearchHistory{
2570         my ($borrowernumber,$session,$query_desc,$query_cgi, $total)=@_;
2571     my $dbh = C4::Context->dbh;
2572
2573     # Add the request the user just made
2574     my $sql = "INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, total, time) VALUES(?, ?, ?, ?, ?, NOW())";
2575     my $sth   = $dbh->prepare($sql);
2576     $sth->execute($borrowernumber, $session, $query_desc, $query_cgi, $total);
2577         return $dbh->last_insert_id(undef, 'search_history', undef,undef,undef);
2578 }
2579
2580 sub GetSearchHistory{
2581         my ($borrowernumber,$session)=@_;
2582     my $dbh = C4::Context->dbh;
2583
2584     # Add the request the user just made
2585     my $query = "SELECT FROM search_history WHERE (userid=? OR sessionid=?)";
2586     my $sth   = $dbh->prepare($query);
2587         $sth->execute($borrowernumber, $session);
2588     return  $sth->fetchall_hashref({});
2589 }
2590
2591 =head2 z3950_search_args
2592
2593 $arrayref = z3950_search_args($matchpoints)
2594
2595 This function returns an array reference that contains the search parameters to be
2596 passed to the Z39.50 search script (z3950_search.pl). The array elements
2597 are hash refs whose keys are name, value and encvalue, and whose values are the
2598 name of a search parameter, the value of that search parameter and the URL encoded
2599 value of that parameter.
2600
2601 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2602
2603 The search parameter values are obtained from the bibliographic record whose
2604 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2605
2606 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2607 a general purpose search argument. In this case, the returned array contains only
2608 entry: the key is 'title' and the value and encvalue are derived from $matchpoints.
2609
2610 If a search parameter value is undefined or empty, it is not included in the returned
2611 array.
2612
2613 The returned array reference may be passed directly to the template parameters.
2614
2615 =over 2
2616
2617 =item C<Output arg:>
2618
2619     * $array containing hash refs as described above
2620
2621 =item C<usage in the script:>
2622
2623 =back
2624
2625 $data = Biblio::GetBiblioData($bibno);
2626 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2627
2628 *OR*
2629
2630 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2631
2632 =cut
2633
2634 sub z3950_search_args {
2635     my $bibrec = shift;
2636     my $isbn = Business::ISBN->new($bibrec);
2637
2638     if (defined $isbn && $isbn->is_valid)
2639     {
2640         $bibrec = { isbn => $bibrec } if !ref $bibrec;
2641     }
2642     else {
2643         $bibrec = { title => $bibrec } if !ref $bibrec;
2644     }
2645     my $array = [];
2646     for my $field (qw/ lccn isbn issn title author dewey subject /)
2647     {
2648         my $encvalue = URI::Escape::uri_escape_utf8($bibrec->{$field});
2649         push @$array, { name=>$field, value=>$bibrec->{$field}, encvalue=>$encvalue } if defined $bibrec->{$field};
2650     }
2651     return $array;
2652 }
2653
2654 =head2 GetDistinctValues($field);
2655
2656 C<$field> is a reference to the fields array
2657
2658 =cut
2659
2660 sub GetDistinctValues {
2661     my ($fieldname,$string)=@_;
2662     # returns a reference to a hash of references to branches...
2663     if ($fieldname=~/\./){
2664                         my ($table,$column)=split /\./, $fieldname;
2665                         my $dbh = C4::Context->dbh;
2666                         warn "select DISTINCT($column) as value, count(*) as cnt from $table group by lib order by $column " if $DEBUG;
2667                         my $sth = $dbh->prepare("select DISTINCT($column) as value, count(*) as cnt from $table ".($string?" where $column like \"$string%\"":"")."group by value order by $column ");
2668                         $sth->execute;
2669                         my $elements=$sth->fetchall_arrayref({});
2670                         return $elements;
2671    }
2672    else {
2673                 $string||= qq("");
2674                 my @servers=qw<biblioserver authorityserver>;
2675                 my (@zconns,@results);
2676         for ( my $i = 0 ; $i < @servers ; $i++ ) {
2677                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
2678                         $results[$i] =
2679                       $zconns[$i]->scan(
2680                         ZOOM::Query::CCL2RPN->new( qq"$fieldname $string", $zconns[$i])
2681                       );
2682                 }
2683                 # The big moment: asynchronously retrieve results from all servers
2684                 my @elements;
2685                 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
2686                         my $ev = $zconns[ $i - 1 ]->last_event();
2687                         if ( $ev == ZOOM::Event::ZEND ) {
2688                                 next unless $results[ $i - 1 ];
2689                                 my $size = $results[ $i - 1 ]->size();
2690                                 if ( $size > 0 ) {
2691                       for (my $j=0;$j<$size;$j++){
2692                                                 my %hashscan;
2693                                                 @hashscan{qw(value cnt)}=$results[ $i - 1 ]->display_term($j);
2694                                                 push @elements, \%hashscan;
2695                                           }
2696                                 }
2697                         }
2698                 }
2699                 return \@elements;
2700    }
2701 }
2702
2703
2704 END { }    # module clean-up code here (global destructor)
2705
2706 1;
2707 __END__
2708
2709 =head1 AUTHOR
2710
2711 Koha Development Team <http://koha-community.org/>
2712
2713 =cut