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