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