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