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