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