Bug 23084: Replace grep {^$var$} with grep {$_ eq $var}
[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 {$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 ] eq 'or' ? ' || ' : ' && '
1363               if ($query);
1364             if ( $operands[$ii] =~ /^[^"]\W*[-|_\w]*:\w.*[^"]$/ ) {
1365                 $query .= $operands[$ii];
1366             }
1367             elsif ( $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                 delete $limits['available'];
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];
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 = '';
1577                         }
1578                     }
1579                 }
1580
1581                 if(not $index){
1582                     $index = 'kw';
1583                 }
1584
1585                 # Set default structure attribute (word list)
1586                 my $struct_attr = q{};
1587                 unless ( $indexes_set || !$index || $index =~ /,(st-|phr|ext|wrdl)/ || $index =~ /^(nb|ns)$/ ) {
1588                     $struct_attr = ",wrdl";
1589                 }
1590
1591                 # Some helpful index variants
1592                 my $index_plus       = $index . $struct_attr . ':';
1593                 my $index_plus_comma = $index . $struct_attr . ',';
1594
1595                 if ($auto_truncation){
1596                         unless ( $index =~ /,(st-|phr|ext)/ ) {
1597                                                 #FIXME only valid with LTR scripts
1598                                                 $operand=join(" ",map{
1599                                                                                         (index($_,"*")>0?"$_":"$_*")
1600                                                                                          }split (/\s+/,$operand));
1601                                                 warn $operand if $DEBUG;
1602                                         }
1603                                 }
1604
1605                 # Detect Truncation
1606                 my $truncated_operand;
1607                 my( $nontruncated, $righttruncated, $lefttruncated,
1608                     $rightlefttruncated, $regexpr
1609                 ) = _detect_truncation( $operand, $index );
1610                 warn
1611 "TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<"
1612                   if $DEBUG;
1613
1614                 # Apply Truncation
1615                 if (
1616                     scalar(@$righttruncated) + scalar(@$lefttruncated) +
1617                     scalar(@$rightlefttruncated) > 0 )
1618                 {
1619
1620                # Don't field weight or add the index to the query, we do it here
1621                     $indexes_set = 1;
1622                     undef $weight_fields;
1623                     my $previous_truncation_operand;
1624                     if (scalar @$nontruncated) {
1625                         $truncated_operand .= "$index_plus @$nontruncated ";
1626                         $previous_truncation_operand = 1;
1627                     }
1628                     if (scalar @$righttruncated) {
1629                         $truncated_operand .= "and " if $previous_truncation_operand;
1630                         $truncated_operand .= $index_plus_comma . "rtrn:@$righttruncated ";
1631                         $previous_truncation_operand = 1;
1632                     }
1633                     if (scalar @$lefttruncated) {
1634                         $truncated_operand .= "and " if $previous_truncation_operand;
1635                         $truncated_operand .= $index_plus_comma . "ltrn:@$lefttruncated ";
1636                         $previous_truncation_operand = 1;
1637                     }
1638                     if (scalar @$rightlefttruncated) {
1639                         $truncated_operand .= "and " if $previous_truncation_operand;
1640                         $truncated_operand .= $index_plus_comma . "rltrn:@$rightlefttruncated ";
1641                         $previous_truncation_operand = 1;
1642                     }
1643                 }
1644                 $operand = $truncated_operand if $truncated_operand;
1645                 warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
1646
1647                 # Handle Stemming
1648                 my $stemmed_operand;
1649                 $stemmed_operand = _build_stemmed_operand($operand, $lang)
1650                                                                                 if $stemming;
1651
1652                 warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
1653
1654                 # Handle Field Weighting
1655                 my $weighted_operand;
1656                 if ($weight_fields) {
1657                     $weighted_operand = _build_weighted_query( $operand, $stemmed_operand, $index );
1658                     $operand = $weighted_operand;
1659                     $indexes_set = 1;
1660                 }
1661
1662                 warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
1663
1664                 ($query,$query_cgi,$query_desc,$previous_operand) = _build_initial_query({
1665                     query => $query,
1666                     query_cgi => $query_cgi,
1667                     query_desc => $query_desc,
1668                     operator => ($operators[ $i - 1 ]) ? $operators[ $i - 1 ] : '',
1669                     parsed_operand => $operand,
1670                     original_operand => $operands[$i] // '',
1671                     index => $index,
1672                     index_plus => $index_plus,
1673                     indexes_set => $indexes_set,
1674                     previous_operand => $previous_operand,
1675                 });
1676
1677             }    #/if $operands
1678         }    # /for
1679     }
1680     warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1681
1682     # add limits
1683     my %group_OR_limits;
1684     my $availability_limit;
1685     foreach my $this_limit (@limits) {
1686         next unless $this_limit;
1687         if ( $this_limit =~ /available/ ) {
1688 #
1689 ## 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1690 ## In English:
1691 ## all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1692             $availability_limit .=
1693 "( (allrecords,AlwaysMatches='') and (not-onloan-count,st-numeric >= 1) and (lost,st-numeric=0) )";
1694             $limit_cgi  .= "&limit=available";
1695             $limit_desc .= "";
1696         }
1697
1698         # group_OR_limits, prefixed by mc-
1699         # OR every member of the group
1700         elsif ( $this_limit =~ /mc/ ) {
1701             my ($k,$v) = split(/:/, $this_limit,2);
1702             if ( $k !~ /mc-i(tem)?type/ ) {
1703                 # in case the mc-ccode value has complicating chars like ()'s inside it we wrap in quotes
1704                 $this_limit =~ tr/"//d;
1705                 $this_limit = $k.":'".$v."'";
1706             }
1707
1708             $group_OR_limits{$k} .= " or " if $group_OR_limits{$k};
1709             $limit_desc      .= " or " if $group_OR_limits{$k};
1710             $group_OR_limits{$k} .= "$this_limit";
1711             $limit_cgi       .= "&limit=" . uri_escape_utf8($this_limit);
1712             $limit_desc      .= " $this_limit";
1713         }
1714
1715         # Regular old limits
1716         else {
1717             $limit .= " and " if $limit || $query;
1718             $limit      .= "$this_limit";
1719             $limit_cgi  .= "&limit=" . uri_escape_utf8($this_limit);
1720             if ($this_limit =~ /^branch:(.+)/) {
1721                 my $branchcode = $1;
1722                 my $library = Koha::Libraries->find( $branchcode );
1723                 if (defined $library) {
1724                     $limit_desc .= " branch:" . $library->branchname;
1725                 } else {
1726                     $limit_desc .= " $this_limit";
1727                 }
1728             } else {
1729                 $limit_desc .= " $this_limit";
1730             }
1731         }
1732     }
1733     foreach my $k (keys (%group_OR_limits)) {
1734         $limit .= " and " if ( $query || $limit );
1735         $limit .= "($group_OR_limits{$k})";
1736     }
1737     if ($availability_limit) {
1738         $limit .= " and " if ( $query || $limit );
1739         $limit .= "($availability_limit)";
1740     }
1741
1742     # Normalize the query and limit strings
1743     # This is flawed , means we can't search anything with : in it
1744     # if user wants to do ccl or cql, start the query with that
1745 #    $query =~ s/:/=/g;
1746     #NOTE: We use several several different regexps here as you can't have variable length lookback assertions
1747     $query =~ s/(?<=(ti|au|pb|su|an|kw|mc|nb|ns)):/=/g;
1748     $query =~ s/(?<=(wrdl)):/=/g;
1749     $query =~ s/(?<=(trn|phr)):/=/g;
1750     $query =~ s/(?<=(st-numeric)):/=/g;
1751     $query =~ s/(?<=(st-year)):/=/g;
1752     $query =~ s/(?<=(st-date-normalized)):/=/g;
1753     $limit =~ s/:/=/g;
1754     for ( $query, $query_desc, $limit, $limit_desc ) {
1755         s/  +/ /g;    # remove extra spaces
1756         s/^ //g;     # remove any beginning spaces
1757         s/ $//g;     # remove any ending spaces
1758         s/==/=/g;    # remove double == from query
1759     }
1760     $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1761
1762     for ($query_cgi,$simple_query) {
1763         s/"//g;
1764     }
1765     # append the limit to the query
1766     $query .= " " . $limit;
1767
1768     # Warnings if DEBUG
1769     if ($DEBUG) {
1770         warn "QUERY:" . $query;
1771         warn "QUERY CGI:" . $query_cgi;
1772         warn "QUERY DESC:" . $query_desc;
1773         warn "LIMIT:" . $limit;
1774         warn "LIMIT CGI:" . $limit_cgi;
1775         warn "LIMIT DESC:" . $limit_desc;
1776         warn "---------\nLeave buildQuery\n---------";
1777     }
1778
1779     return (
1780         undef,              $query, $simple_query, $query_cgi,
1781         $query_desc,        $limit, $limit_cgi,    $limit_desc,
1782         $query_type
1783     );
1784 }
1785
1786 =head2 _build_initial_query
1787
1788   ($query, $query_cgi, $query_desc, $previous_operand) = _build_initial_query($initial_query_params);
1789
1790   Build a section of the initial query containing indexes, operators, and operands.
1791
1792 =cut
1793
1794 sub _build_initial_query {
1795     my ($params) = @_;
1796
1797     my $operator = "";
1798     if ($params->{previous_operand}){
1799         #If there is a previous operand, add a supplied operator or the default 'and'
1800         $operator = ($params->{operator}) ? " ".($params->{operator})." " : ' and ';
1801     }
1802
1803     #NOTE: indexes_set is typically set when doing truncation or field weighting
1804     my $operand = ($params->{indexes_set}) ? $params->{parsed_operand} : $params->{index_plus}.$params->{parsed_operand};
1805
1806     #e.g. "kw,wrdl:test"
1807     #e.g. " and kw,wrdl:test"
1808     $params->{query} .= $operator . $operand;
1809
1810     $params->{query_cgi} .= "&op=".uri_escape_utf8($operator) if $operator;
1811     $params->{query_cgi} .= "&idx=".uri_escape_utf8($params->{index}) if $params->{index};
1812     $params->{query_cgi} .= "&q=".uri_escape_utf8($params->{original_operand}) if $params->{original_operand};
1813
1814     #e.g. " and kw,wrdl: test"
1815     $params->{query_desc} .= $operator . $params->{index_plus} . " " . $params->{original_operand};
1816
1817     $params->{previous_operand} = 1 unless $params->{previous_operand}; #If there is no previous operand, mark this as one
1818
1819     return ($params->{query}, $params->{query_cgi}, $params->{query_desc}, $params->{previous_operand});
1820 }
1821
1822 =head2 searchResults
1823
1824   my @search_results = searchResults($search_context, $searchdesc, $hits, 
1825                                      $results_per_page, $offset, $scan, 
1826                                      @marcresults);
1827
1828 Format results in a form suitable for passing to the template
1829
1830 =cut
1831
1832 # IMO this subroutine is pretty messy still -- it's responsible for
1833 # building the HTML output for the template
1834 sub searchResults {
1835     my ( $search_context, $searchdesc, $hits, $results_per_page, $offset, $scan, $marcresults ) = @_;
1836     my $dbh = C4::Context->dbh;
1837     my @newresults;
1838
1839     require C4::Items;
1840
1841     $search_context->{'interface'} = 'opac' if !$search_context->{'interface'} || $search_context->{'interface'} ne 'intranet';
1842     my ($is_opac, $hidelostitems);
1843     if ($search_context->{'interface'} eq 'opac') {
1844         $hidelostitems = C4::Context->preference('hidelostitems');
1845         $is_opac       = 1;
1846     }
1847
1848     #Build branchnames hash
1849     my %branches = map { $_->branchcode => $_->branchname } Koha::Libraries->search({}, { order_by => 'branchname' });
1850
1851 # FIXME - We build an authorised values hash here, using the default framework
1852 # though it is possible to have different authvals for different fws.
1853
1854     my $shelflocations =
1855       { map { $_->{authorised_value} => $_->{lib} } Koha::AuthorisedValues->get_descriptions_by_koha_field( { frameworkcode => '', kohafield => 'items.location' } ) };
1856
1857     # get notforloan authorised value list (see $shelflocations  FIXME)
1858     my $av = Koha::MarcSubfieldStructures->search({ frameworkcode => '', kohafield => 'items.notforloan', authorised_value => [ -and => {'!=' => undef }, {'!=' => ''}] });
1859     my $notforloan_authorised_value = $av->count ? $av->next->authorised_value : undef;
1860
1861     #Get itemtype hash
1862     my $itemtypes = Koha::ItemTypes->search_with_localization;
1863     my %itemtypes = map { $_->{itemtype} => $_ } @{ $itemtypes->unblessed };
1864
1865     #search item field code
1866     my ($itemtag, undef) = &GetMarcFromKohaField( "items.itemnumber" );
1867
1868     ## find column names of items related to MARC
1869     my %subfieldstosearch;
1870     my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
1871     for my $column ( @columns ) {
1872         my ( $tagfield, $tagsubfield ) =
1873           &GetMarcFromKohaField( "items." . $column );
1874         if ( defined $tagsubfield ) {
1875             $subfieldstosearch{$column} = $tagsubfield;
1876         }
1877     }
1878
1879     # handle which records to actually retrieve
1880     my $times;
1881     if ( $hits && $offset + $results_per_page <= $hits ) {
1882         $times = $offset + $results_per_page;
1883     }
1884     else {
1885         $times = $hits;  # FIXME: if $hits is undefined, why do we want to equal it?
1886     }
1887
1888     my $marcflavour = C4::Context->preference("marcflavour");
1889     # We get the biblionumber position in MARC
1890     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField( 'biblio.biblionumber' );
1891
1892     # set stuff for XSLT processing here once, not later again for every record we retrieved
1893     my $xslfile;
1894     my $xslsyspref;
1895     if( $is_opac ){
1896         $xslsyspref = "OPACXSLTResultsDisplay";
1897         $xslfile = C4::Context->preference( $xslsyspref );
1898     } else {
1899         $xslsyspref = "XSLTResultsDisplay";
1900         $xslfile = C4::Context->preference( $xslsyspref ) || "default";
1901     }
1902     my $lang   = $xslfile ? C4::Languages::getlanguage()  : undef;
1903     my $sysxml = $xslfile ? C4::XSLT::get_xslt_sysprefs() : undef;
1904
1905     my $userenv = C4::Context->userenv;
1906     my $logged_in_user
1907         = ( defined $userenv and $userenv->{number} )
1908         ? Koha::Patrons->find( $userenv->{number} )
1909         : undef;
1910     my $patron_category_hide_lost_items = ($logged_in_user) ? $logged_in_user->category->hidelostitems : 0;
1911
1912     # loop through all of the records we've retrieved
1913     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1914
1915         my $marcrecord;
1916         if ($scan) {
1917             # For Scan searches we built USMARC data
1918             $marcrecord = MARC::Record->new_from_usmarc( $marcresults->[$i]);
1919         } else {
1920             # Normal search, render from Zebra's output
1921             $marcrecord = new_record_from_zebra(
1922                 'biblioserver',
1923                 $marcresults->[$i]
1924             );
1925
1926             if ( ! defined $marcrecord ) {
1927                 warn "ERROR DECODING RECORD - $@: " . $marcresults->[$i];
1928                 next;
1929             }
1930         }
1931
1932         my $fw = $scan
1933              ? undef
1934              : $bibliotag < 10
1935                ? GetFrameworkCode($marcrecord->field($bibliotag)->data)
1936                : GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
1937
1938         SetUTF8Flag($marcrecord);
1939         my $oldbiblio = TransformMarcToKoha( $marcrecord, $fw );
1940         $oldbiblio->{result_number} = $i + 1;
1941
1942         # add imageurl to itemtype if there is one
1943         $oldbiblio->{imageurl} = getitemtypeimagelocation( $search_context->{'interface'}, $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
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         $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{translated_description};
1954  # Build summary if there is one (the summary is defined in the itemtypes table)
1955
1956         # FIXME: this is only used in the deprecated non-XLST opac results
1957         if ( !$xslfile && $is_opac && $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1958             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1959             my @fields  = $marcrecord->fields();
1960
1961             my $newsummary;
1962             foreach my $line ( "$summary\n" =~ /(.*)\n/g ){
1963                 my $tags = {};
1964                 foreach my $tag ( $line =~ /\[(\d{3}[\w|\d])\]/ ) {
1965                     $tag =~ /(.{3})(.)/;
1966                     if($marcrecord->field($1)){
1967                         my @abc = $marcrecord->field($1)->subfield($2);
1968                         $tags->{$tag} = $#abc + 1 ;
1969                     }
1970                 }
1971
1972                 # We catch how many times to repeat this line
1973                 my $max = 0;
1974                 foreach my $tag (keys(%$tags)){
1975                     $max = $tags->{$tag} if($tags->{$tag} > $max);
1976                  }
1977
1978                 # we replace, and repeat each line
1979                 for (my $i = 0 ; $i < $max ; $i++){
1980                     my $newline = $line;
1981
1982                     foreach my $tag ( $newline =~ /\[(\d{3}[\w|\d])\]/g ) {
1983                         $tag =~ /(.{3})(.)/;
1984
1985                         if($marcrecord->field($1)){
1986                             my @repl = $marcrecord->field($1)->subfield($2);
1987                             my $subfieldvalue = $repl[$i];
1988                             $newline =~ s/\[$tag\]/$subfieldvalue/g;
1989                         }
1990                     }
1991                     $newsummary .= "$newline\n";
1992                 }
1993             }
1994
1995             $newsummary =~ s/\[(.*?)]//g;
1996             $newsummary =~ s/\n/<br\/>/g;
1997             $oldbiblio->{summary} = $newsummary;
1998         }
1999
2000         # Pull out the items fields
2001         my @fields = $marcrecord->field($itemtag);
2002         my $marcflavor = C4::Context->preference("marcflavour");
2003
2004         # adding linked items that belong to host records
2005         if ( C4::Context->preference('EasyAnalyticalRecords') ) {
2006             my $analyticsfield = '773';
2007             if ($marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC') {
2008                 $analyticsfield = '773';
2009             } elsif ($marcflavor eq 'UNIMARC') {
2010                 $analyticsfield = '461';
2011             }
2012             foreach my $hostfield ( $marcrecord->field($analyticsfield)) {
2013                 my $hostbiblionumber = $hostfield->subfield("0");
2014                 my $linkeditemnumber = $hostfield->subfield("9");
2015                 if( $hostbiblionumber ) {
2016                     my $hostbiblio = GetMarcBiblio({
2017                         biblionumber => $hostbiblionumber,
2018                         embed_items  => 1 });
2019                     my ($itemfield, undef) = GetMarcFromKohaField( 'items.itemnumber' );
2020                     if( $hostbiblio ) {
2021                         my @hostitems = $hostbiblio->field($itemfield);
2022                         foreach my $hostitem (@hostitems){
2023                             if ($hostitem->subfield("9") eq $linkeditemnumber){
2024                                 my $linkeditem =$hostitem;
2025                                 # append linked items if they exist
2026                                 push @fields, $linkeditem if $linkeditem;
2027                             }
2028                         }
2029                     }
2030                 }
2031             }
2032         }
2033
2034         # Setting item statuses for display
2035         my @available_items_loop;
2036         my @onloan_items_loop;
2037         my @other_items_loop;
2038
2039         my $available_items;
2040         my $onloan_items;
2041         my $other_items;
2042
2043         my $ordered_count         = 0;
2044         my $available_count       = 0;
2045         my $onloan_count          = 0;
2046         my $longoverdue_count     = 0;
2047         my $other_count           = 0;
2048         my $withdrawn_count        = 0;
2049         my $itemlost_count        = 0;
2050         my $hideatopac_count      = 0;
2051         my $itembinding_count     = 0;
2052         my $itemdamaged_count     = 0;
2053         my $item_in_transit_count = 0;
2054         my $can_place_holds       = 0;
2055         my $item_onhold_count     = 0;
2056         my $notforloan_count      = 0;
2057         my $items_count           = scalar(@fields);
2058         my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults');
2059         my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1;
2060         my @hiddenitems; # hidden itemnumbers based on OpacHiddenItems syspref
2061
2062         # loop through every item
2063         foreach my $field (@fields) {
2064             my $item;
2065
2066             # populate the items hash
2067             foreach my $code ( keys %subfieldstosearch ) {
2068                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
2069             }
2070             $item->{description} = $itemtypes{ $item->{itype} }{translated_description};
2071
2072                 # OPAC hidden items
2073             if ($is_opac) {
2074                 # hidden because lost
2075                 if ($hidelostitems && $item->{itemlost}) {
2076                     $hideatopac_count++;
2077                     next;
2078                 }
2079                 # hidden based on OpacHiddenItems syspref
2080                 my @hi = C4::Items::GetHiddenItemnumbers({ items=> [ $item ], borcat => $search_context->{category} });
2081                 if (scalar @hi) {
2082                     push @hiddenitems, @hi;
2083                     $hideatopac_count++;
2084                     next;
2085                 }
2086             }
2087
2088             my $hbranch     = C4::Context->preference('StaffSearchResultsDisplayBranch');
2089             my $otherbranch = $hbranch eq 'homebranch' ? 'holdingbranch' : 'homebranch';
2090
2091             # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
2092             if ($item->{$hbranch}) {
2093                 $item->{'branchname'} = $branches{$item->{$hbranch}};
2094             }
2095             elsif ($item->{$otherbranch}) {     # Last resort
2096                 $item->{'branchname'} = $branches{$item->{$otherbranch}};
2097             }
2098
2099                         my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
2100 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
2101             if ( $item->{onloan}
2102                 and $logged_in_user
2103                 and !( $patron_category_hide_lost_items and $item->{itemlost} ) )
2104             {
2105                 $onloan_count++;
2106                 my $key = $prefix . $item->{onloan} . $item->{barcode};
2107                 $onloan_items->{$key}->{due_date} = $item->{onloan};
2108                 $onloan_items->{$key}->{count}++ if $item->{$hbranch};
2109                 $onloan_items->{$key}->{branchname}     = $item->{branchname};
2110                 $onloan_items->{$key}->{location}       = $shelflocations->{ $item->{location} };
2111                 $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
2112                 $onloan_items->{$key}->{description}    = $item->{description};
2113                 $onloan_items->{$key}->{imageurl} =
2114                   getitemtypeimagelocation( $search_context->{'interface'}, $itemtypes{ $item->{itype} }->{imageurl} );
2115
2116                 # if something's checked out and lost, mark it as 'long overdue'
2117                 if ( $item->{itemlost} ) {
2118                     $onloan_items->{$key}->{longoverdue}++;
2119                     $longoverdue_count++;
2120                 }
2121                 else {    # can place holds as long as item isn't lost
2122                     $can_place_holds = 1;
2123                 }
2124             }
2125
2126          # items not on loan, but still unavailable ( lost, withdrawn, damaged )
2127             else {
2128
2129                 $item->{notforloan}=1 if !$item->{notforloan}  && $itemtypes{ C4::Context->preference("item-level_itypes")? $item->{itype}: $oldbiblio->{itemtype} }->{notforloan};
2130
2131                 # item is on order
2132                 if ( $item->{notforloan} < 0 ) {
2133                     $ordered_count++;
2134                 } elsif ( $item->{notforloan} > 0 ) {
2135                     $notforloan_count++;
2136                 }
2137
2138                 # is item in transit?
2139                 my $transfertwhen = '';
2140                 my ($transfertfrom, $transfertto);
2141
2142                 # is item on the reserve shelf?
2143                 my $reservestatus = '';
2144
2145                 unless ($item->{withdrawn}
2146                         || $item->{itemlost}
2147                         || $item->{damaged}
2148                         || $item->{notforloan}
2149                         || ( C4::Context->preference('MaxSearchResultsItemsPerRecordStatusCheck')
2150                         && $items_count > C4::Context->preference('MaxSearchResultsItemsPerRecordStatusCheck') ) ) {
2151
2152                     # A couple heuristics to limit how many times
2153                     # we query the database for item transfer information, sacrificing
2154                     # accuracy in some cases for speed;
2155                     #
2156                     # 1. don't query if item has one of the other statuses
2157                     # 2. don't check transit status if the bib has
2158                     #    more than 20 items
2159                     #
2160                     # FIXME: to avoid having the query the database like this, and to make
2161                     #        the in transit status count as unavailable for search limiting,
2162                     #        should map transit status to record indexed in Zebra.
2163                     #
2164                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
2165                     $reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber} );
2166                 }
2167
2168                 # item is withdrawn, lost, damaged, not for loan, reserved or in transit
2169                 if (   $item->{withdrawn}
2170                     || $item->{itemlost}
2171                     || $item->{damaged}
2172                     || $item->{notforloan}
2173                     || $reservestatus eq 'Waiting'
2174                     || ($transfertwhen ne ''))
2175                 {
2176                     $withdrawn_count++        if $item->{withdrawn};
2177                     $itemlost_count++        if $item->{itemlost};
2178                     $itemdamaged_count++     if $item->{damaged};
2179                     $item_in_transit_count++ if $transfertwhen ne '';
2180                     $item_onhold_count++     if $reservestatus eq 'Waiting';
2181                     $item->{status} = $item->{withdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
2182
2183                     # can place a hold on a item if
2184                     # not lost nor withdrawn
2185                     # not damaged unless AllowHoldsOnDamagedItems is true
2186                     # item is either for loan or on order (notforloan < 0)
2187                     $can_place_holds = 1
2188                       if (
2189                            !$item->{itemlost}
2190                         && !$item->{withdrawn}
2191                         && ( !$item->{damaged} || C4::Context->preference('AllowHoldsOnDamagedItems') )
2192                         && ( !$item->{notforloan} || $item->{notforloan} < 0 )
2193                       );
2194
2195                     $other_count++;
2196
2197                     my $key = $prefix . $item->{status};
2198                     foreach (qw(withdrawn itemlost damaged branchname itemcallnumber)) {
2199                         $other_items->{$key}->{$_} = $item->{$_};
2200                     }
2201                     $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
2202                     $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
2203                     $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value and $item->{notforloan};
2204                     $other_items->{$key}->{count}++ if $item->{$hbranch};
2205                     $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
2206                     $other_items->{$key}->{description} = $item->{description};
2207                     $other_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context->{'interface'}, $itemtypes{ $item->{itype} }->{imageurl} );
2208                 }
2209                 # item is available
2210                 else {
2211                     $can_place_holds = 1;
2212                     $available_count++;
2213                     $available_items->{$prefix}->{count}++ if $item->{$hbranch};
2214                     foreach (qw(branchname itemcallnumber description)) {
2215                         $available_items->{$prefix}->{$_} = $item->{$_};
2216                     }
2217                     $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
2218                     $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( $search_context->{'interface'}, $itemtypes{ $item->{itype} }->{imageurl} );
2219                 }
2220             }
2221         }    # notforloan, item level and biblioitem level
2222
2223         # if all items are hidden, do not show the record
2224         if ($items_count > 0 && $hideatopac_count == $items_count) {
2225             next;
2226         }
2227
2228         my ( $availableitemscount, $onloanitemscount, $otheritemscount );
2229         for my $key ( sort keys %$onloan_items ) {
2230             (++$onloanitemscount > $maxitems) and last;
2231             push @onloan_items_loop, $onloan_items->{$key};
2232         }
2233         for my $key ( sort keys %$other_items ) {
2234             (++$otheritemscount > $maxitems) and last;
2235             push @other_items_loop, $other_items->{$key};
2236         }
2237         for my $key ( sort keys %$available_items ) {
2238             (++$availableitemscount > $maxitems) and last;
2239             push @available_items_loop, $available_items->{$key}
2240         }
2241
2242         # XSLT processing of some stuff
2243         # we fetched the sysprefs already before the loop through all retrieved record!
2244         if (!$scan && $xslfile) {
2245             $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display($oldbiblio->{biblionumber}, $marcrecord, $xslsyspref, 1, \@hiddenitems, $sysxml, $xslfile, $lang);
2246         }
2247
2248         # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
2249         if (!C4::Context->preference("item-level_itypes")) {
2250             if ($itemtypes{ $oldbiblio->{itemtype} }->{notforloan}) {
2251                 $can_place_holds = 0;
2252             }
2253         }
2254         $oldbiblio->{norequests} = 1 unless $can_place_holds;
2255         $oldbiblio->{items_count}          = $items_count;
2256         $oldbiblio->{available_items_loop} = \@available_items_loop;
2257         $oldbiblio->{onloan_items_loop}    = \@onloan_items_loop;
2258         $oldbiblio->{other_items_loop}     = \@other_items_loop;
2259         $oldbiblio->{availablecount}       = $available_count;
2260         $oldbiblio->{availableplural}      = 1 if $available_count > 1;
2261         $oldbiblio->{onloancount}          = $onloan_count;
2262         $oldbiblio->{onloanplural}         = 1 if $onloan_count > 1;
2263         $oldbiblio->{othercount}           = $other_count;
2264         $oldbiblio->{otherplural}          = 1 if $other_count > 1;
2265         $oldbiblio->{withdrawncount}        = $withdrawn_count;
2266         $oldbiblio->{itemlostcount}        = $itemlost_count;
2267         $oldbiblio->{damagedcount}         = $itemdamaged_count;
2268         $oldbiblio->{intransitcount}       = $item_in_transit_count;
2269         $oldbiblio->{onholdcount}          = $item_onhold_count;
2270         $oldbiblio->{orderedcount}         = $ordered_count;
2271         $oldbiblio->{notforloancount}      = $notforloan_count;
2272
2273         if (C4::Context->preference("AlternateHoldingsField") && $items_count == 0) {
2274             my $fieldspec = C4::Context->preference("AlternateHoldingsField");
2275             my $subfields = substr $fieldspec, 3;
2276             my $holdingsep = C4::Context->preference("AlternateHoldingsSeparator") || ' ';
2277             my @alternateholdingsinfo = ();
2278             my @holdingsfields = $marcrecord->field(substr $fieldspec, 0, 3);
2279             my $alternateholdingscount = 0;
2280
2281             for my $field (@holdingsfields) {
2282                 my %holding = ( holding => '' );
2283                 my $havesubfield = 0;
2284                 for my $subfield ($field->subfields()) {
2285                     if ((index $subfields, $$subfield[0]) >= 0) {
2286                         $holding{'holding'} .= $holdingsep if (length $holding{'holding'} > 0);
2287                         $holding{'holding'} .= $$subfield[1];
2288                         $havesubfield++;
2289                     }
2290                 }
2291                 if ($havesubfield) {
2292                     push(@alternateholdingsinfo, \%holding);
2293                     $alternateholdingscount++;
2294                 }
2295             }
2296
2297             $oldbiblio->{'ALTERNATEHOLDINGS'} = \@alternateholdingsinfo;
2298             $oldbiblio->{'alternateholdings_count'} = $alternateholdingscount;
2299         }
2300
2301         $oldbiblio->{biblio_object} = Koha::Biblios->find( $oldbiblio->{biblionumber} );
2302
2303         push( @newresults, $oldbiblio );
2304     }
2305
2306     return @newresults;
2307 }
2308
2309 =head2 enabled_staff_search_views
2310
2311 %hash = enabled_staff_search_views()
2312
2313 This function returns a hash that contains three flags obtained from the system
2314 preferences, used to determine whether a particular staff search results view
2315 is enabled.
2316
2317 =over 2
2318
2319 =item C<Output arg:>
2320
2321     * $hash{can_view_MARC} is true only if the MARC view is enabled
2322     * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2323     * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2324
2325 =item C<usage in the script:>
2326
2327 =back
2328
2329 $template->param ( C4::Search::enabled_staff_search_views );
2330
2331 =cut
2332
2333 sub enabled_staff_search_views
2334 {
2335         return (
2336                 can_view_MARC                   => C4::Context->preference('viewMARC'),                 # 1 if the staff search allows the MARC view
2337                 can_view_ISBD                   => C4::Context->preference('viewISBD'),                 # 1 if the staff search allows the ISBD view
2338                 can_view_labeledMARC    => C4::Context->preference('viewLabeledMARC'),  # 1 if the staff search allows the Labeled MARC view
2339         );
2340 }
2341
2342 =head2 z3950_search_args
2343
2344 $arrayref = z3950_search_args($matchpoints)
2345
2346 This function returns an array reference that contains the search parameters to be
2347 passed to the Z39.50 search script (z3950_search.pl). The array elements
2348 are hash refs whose keys are name and value, and whose values are the
2349 name of a search parameter, the value of that search parameter and the URL encoded
2350 value of that parameter.
2351
2352 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2353
2354 The search parameter values are obtained from the bibliographic record whose
2355 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2356
2357 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2358 a general purpose search argument. In this case, the returned array contains only
2359 entry: the key is 'title' and the value is derived from $matchpoints.
2360
2361 If a search parameter value is undefined or empty, it is not included in the returned
2362 array.
2363
2364 The returned array reference may be passed directly to the template parameters.
2365
2366 =over 2
2367
2368 =item C<Output arg:>
2369
2370     * $array containing hash refs as described above
2371
2372 =item C<usage in the script:>
2373
2374 =back
2375
2376 $data = Biblio::GetBiblioData($bibno);
2377 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2378
2379 *OR*
2380
2381 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2382
2383 =cut
2384
2385 sub z3950_search_args {
2386     my $bibrec = shift;
2387
2388     my $isbn_string = ref( $bibrec ) ? $bibrec->{title} : $bibrec;
2389     my $isbn = Business::ISBN->new( $isbn_string );
2390
2391     if (defined $isbn && $isbn->is_valid)
2392     {
2393         if ( ref($bibrec) ) {
2394             $bibrec->{isbn} = $isbn_string;
2395             $bibrec->{title} = undef;
2396         } else {
2397             $bibrec = { isbn => $isbn_string };
2398         }
2399     }
2400     else {
2401         $bibrec = { title => $bibrec } if !ref $bibrec;
2402     }
2403     my $array = [];
2404     for my $field (qw/ lccn isbn issn title author dewey subject /)
2405     {
2406         push @$array, { name => $field, value => $bibrec->{$field} }
2407           if defined $bibrec->{$field};
2408     }
2409     return $array;
2410 }
2411
2412 =head2 GetDistinctValues($field);
2413
2414 C<$field> is a reference to the fields array
2415
2416 =cut
2417
2418 sub GetDistinctValues {
2419     my ($fieldname,$string)=@_;
2420     # returns a reference to a hash of references to branches...
2421     if ($fieldname=~/\./){
2422                         my ($table,$column)=split /\./, $fieldname;
2423                         my $dbh = C4::Context->dbh;
2424                         warn "select DISTINCT($column) as value, count(*) as cnt from $table group by lib order by $column " if $DEBUG;
2425                         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 ");
2426                         $sth->execute;
2427                         my $elements=$sth->fetchall_arrayref({});
2428                         return $elements;
2429    }
2430    else {
2431                 $string||= qq("");
2432                 my @servers=qw<biblioserver authorityserver>;
2433                 my (@zconns,@results);
2434         for ( my $i = 0 ; $i < @servers ; $i++ ) {
2435                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
2436                         $results[$i] =
2437                       $zconns[$i]->scan(
2438                         ZOOM::Query::CCL2RPN->new( qq"$fieldname $string", $zconns[$i])
2439                       );
2440                 }
2441                 # The big moment: asynchronously retrieve results from all servers
2442                 my @elements;
2443         _ZOOM_event_loop(
2444             \@zconns,
2445             \@results,
2446             sub {
2447                 my ( $i, $size ) = @_;
2448                 for ( my $j = 0 ; $j < $size ; $j++ ) {
2449                     my %hashscan;
2450                     @hashscan{qw(value cnt)} =
2451                       $results[ $i - 1 ]->display_term($j);
2452                     push @elements, \%hashscan;
2453                 }
2454             }
2455         );
2456                 return \@elements;
2457    }
2458 }
2459
2460 =head2 _ZOOM_event_loop
2461
2462     _ZOOM_event_loop(\@zconns, \@results, sub {
2463         my ( $i, $size ) = @_;
2464         ....
2465     } );
2466
2467 Processes a ZOOM event loop and passes control to a closure for
2468 processing the results, and destroying the resultsets.
2469
2470 =cut
2471
2472 sub _ZOOM_event_loop {
2473     my ($zconns, $results, $callback) = @_;
2474     while ( ( my $i = ZOOM::event( $zconns ) ) != 0 ) {
2475         my $ev = $zconns->[ $i - 1 ]->last_event();
2476         if ( $ev == ZOOM::Event::ZEND ) {
2477             next unless $results->[ $i - 1 ];
2478             my $size = $results->[ $i - 1 ]->size();
2479             if ( $size > 0 ) {
2480                 $callback->($i, $size);
2481             }
2482         }
2483     }
2484
2485     foreach my $result (@$results) {
2486         $result->destroy();
2487     }
2488 }
2489
2490 =head2 new_record_from_zebra
2491
2492 Given raw data from a searchengine result set, return a MARC::Record object
2493
2494 This helper function is needed to take into account all the involved
2495 system preferences and configuration variables to properly create the
2496 MARC::Record object.
2497
2498 If we are using GRS-1, then the raw data we get from Zebra should be USMARC
2499 data. If we are using DOM, then it has to be MARCXML.
2500
2501 If we are using elasticsearch, it'll already be a MARC::Record and this
2502 function needs a new name.
2503
2504 =cut
2505
2506 sub new_record_from_zebra {
2507
2508     my $server   = shift;
2509     my $raw_data = shift;
2510     # Set the default indexing modes
2511     my $search_engine = C4::Context->preference("SearchEngine");
2512     if ($search_engine eq 'Elasticsearch') {
2513         return ref $raw_data eq 'MARC::Record' ? $raw_data : MARC::Record->new_from_xml( $raw_data, 'UTF-8' );
2514     }
2515     my $index_mode = ( $server eq 'biblioserver' )
2516                         ? C4::Context->config('zebra_bib_index_mode') // 'dom'
2517                         : C4::Context->config('zebra_auth_index_mode') // 'dom';
2518
2519     my $marc_record =  eval {
2520         if ( $index_mode eq 'dom' ) {
2521             MARC::Record->new_from_xml( $raw_data, 'UTF-8' );
2522         } else {
2523             MARC::Record->new_from_usmarc( $raw_data );
2524         }
2525     };
2526
2527     if ($@) {
2528         return;
2529     } else {
2530         return $marc_record;
2531     }
2532
2533 }
2534
2535 END { }    # module clean-up code here (global destructor)
2536
2537 1;
2538 __END__
2539
2540 =head1 AUTHOR
2541
2542 Koha Development Team <http://koha-community.org/>
2543
2544 =cut