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