Bug 1807 - Commenting out flawed nested queries implementation, now users can search...
[koha.git] / C4 / Search.pm
1 package C4::Search;
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
9 #
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
16 # Suite 330, Boston, MA  02111-1307 USA
17
18 use strict;
19 # use warnings; # FIXME
20 require Exporter;
21 use C4::Context;
22 use C4::Biblio;    # GetMarcFromKohaField, GetBiblioData
23 use C4::Koha;      # getFacets
24 use Lingua::Stem;
25 use C4::Search::PazPar2;
26 use XML::Simple;
27 use C4::Dates qw(format_date);
28 use C4::XSLT;
29 use C4::Branch;
30 use C4::Debug;
31 use YAML;
32 use URI::Escape;
33
34 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
35
36 # set the version for version checking
37 BEGIN {
38     $VERSION = 3.01;
39     $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
40 }
41
42 =head1 NAME
43
44 C4::Search - Functions for searching the Koha catalog.
45
46 =head1 SYNOPSIS
47
48 See opac/opac-search.pl or catalogue/search.pl for example of usage
49
50 =head1 DESCRIPTION
51
52 This module provides searching functions for Koha's bibliographic databases
53
54 =head1 FUNCTIONS
55
56 =cut
57
58 @ISA    = qw(Exporter);
59 @EXPORT = qw(
60   &FindDuplicate
61   &SimpleSearch
62   &searchResults
63   &getRecords
64   &buildQuery
65   &NZgetRecords
66   &AddSearchHistory
67   &GetDistinctValues
68   &BiblioAddAuthorities
69 );
70 #FIXME: i had to add BiblioAddAuthorities here because in Biblios.pm it caused circular dependencies (C4::Search uses C4::Biblio, and BiblioAddAuthorities uses SimpleSearch from C4::Search)
71
72 # make all your functions, whether exported or not;
73
74 =head2 FindDuplicate
75
76 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
77
78 This function attempts to find duplicate records using a hard-coded, fairly simplistic algorithm
79
80 =cut
81
82 sub FindDuplicate {
83     my ($record) = @_;
84     my $dbh = C4::Context->dbh;
85     my $result = TransformMarcToKoha( $dbh, $record, '' );
86     my $sth;
87     my $query;
88     my $search;
89     my $type;
90     my ( $biblionumber, $title );
91
92     # search duplicate on ISBN, easy and fast..
93     # ... normalize first
94     if ( $result->{isbn} ) {
95         $result->{isbn} =~ s/\(.*$//;
96         $result->{isbn} =~ s/\s+$//;
97         $query = "isbn=$result->{isbn}";
98     }
99     else {
100         $result->{title} =~ s /\\//g;
101         $result->{title} =~ s /\"//g;
102         $result->{title} =~ s /\(//g;
103         $result->{title} =~ s /\)//g;
104
105         # FIXME: instead of removing operators, could just do
106         # quotes around the value
107         $result->{title} =~ s/(and|or|not)//g;
108         $query = "ti,ext=$result->{title}";
109         $query .= " and itemtype=$result->{itemtype}"
110           if ( $result->{itemtype} );
111         if   ( $result->{author} ) {
112             $result->{author} =~ s /\\//g;
113             $result->{author} =~ s /\"//g;
114             $result->{author} =~ s /\(//g;
115             $result->{author} =~ s /\)//g;
116
117             # remove valid operators
118             $result->{author} =~ s/(and|or|not)//g;
119             $query .= " and au,ext=$result->{author}";
120         }
121     }
122
123     # FIXME: add error handling
124     my ( $error, $searchresults ) = SimpleSearch($query); # FIXME :: hardcoded !
125     my @results;
126     foreach my $possible_duplicate_record (@$searchresults) {
127         my $marcrecord =
128           MARC::Record->new_from_usmarc($possible_duplicate_record);
129         my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
130
131         # FIXME :: why 2 $biblionumber ?
132         if ($result) {
133             push @results, $result->{'biblionumber'};
134             push @results, $result->{'title'};
135         }
136     }
137     return @results;
138 }
139
140 =head2 SimpleSearch
141
142 ( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers] );
143
144 This function provides a simple search API on the bibliographic catalog
145
146 =over 2
147
148 =item C<input arg:>
149
150     * $query can be a simple keyword or a complete CCL query
151     * @servers is optional. Defaults to biblioserver as found in koha-conf.xml
152     * $offset - If present, represents the number of records at the beggining to omit. Defaults to 0
153     * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
154
155
156 =item C<Output:>
157
158     * $error is a empty unless an error is detected
159     * \@results is an array of records.
160     * $total_hits is the number of hits that would have been returned with no limit
161
162 =item C<usage in the script:>
163
164 =back
165
166 my ( $error, $marcresults, $total_hits ) = SimpleSearch($query);
167
168 if (defined $error) {
169     $template->param(query_error => $error);
170     warn "error: ".$error;
171     output_html_with_http_headers $input, $cookie, $template->output;
172     exit;
173 }
174
175 my $hits = scalar @$marcresults;
176 my @results;
177
178 for my $i (0..$hits) {
179     my %resultsloop;
180     my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
181     my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
182
183     #build the hash for the template.
184     $resultsloop{title}           = $biblio->{'title'};
185     $resultsloop{subtitle}        = $biblio->{'subtitle'};
186     $resultsloop{biblionumber}    = $biblio->{'biblionumber'};
187     $resultsloop{author}          = $biblio->{'author'};
188     $resultsloop{publishercode}   = $biblio->{'publishercode'};
189     $resultsloop{publicationyear} = $biblio->{'publicationyear'};
190
191     push @results, \%resultsloop;
192 }
193
194 $template->param(result=>\@results);
195
196 =cut
197
198 sub SimpleSearch {
199     my ( $query, $offset, $max_results, $servers )  = @_;
200     
201     if ( C4::Context->preference('NoZebra') ) {
202         my $result = NZorder( NZanalyse($query) )->{'biblioserver'};
203         my $search_result =
204           (      $result->{hits}
205               && $result->{hits} > 0 ? $result->{'RECORDS'} : [] );
206         return ( undef, $search_result, scalar($result->{hits}) );
207     }
208     else {
209         # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
210         my @servers = defined ( $servers ) ? @$servers : ( "biblioserver" );
211         my @results;
212         my @zoom_queries;
213         my @tmpresults;
214         my @zconns;
215         my $total_hits;
216         return ( "No query entered", undef, undef ) unless $query;
217
218         # Initialize & Search Zebra
219         for ( my $i = 0 ; $i < @servers ; $i++ ) {
220             eval {
221                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
222                 $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
223                 $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
224
225                 # error handling
226                 my $error =
227                     $zconns[$i]->errmsg() . " ("
228                   . $zconns[$i]->errcode() . ") "
229                   . $zconns[$i]->addinfo() . " "
230                   . $zconns[$i]->diagset();
231
232                 return ( $error, undef, undef ) if $zconns[$i]->errcode();
233             };
234             if ($@) {
235
236                 # caught a ZOOM::Exception
237                 my $error =
238                     $@->message() . " ("
239                   . $@->code() . ") "
240                   . $@->addinfo() . " "
241                   . $@->diagset();
242                 warn $error;
243                 return ( $error, undef, undef );
244             }
245         }
246         while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
247             my $event = $zconns[ $i - 1 ]->last_event();
248             if ( $event == ZOOM::Event::ZEND ) {
249
250                 my $first_record = defined( $offset ) ? $offset+1 : 1;
251                 my $hits = $tmpresults[ $i - 1 ]->size();
252                 $total_hits += $hits;
253                 my $last_record = $hits;
254                 if ( defined $max_results && $offset + $max_results < $hits ) {
255                     $last_record  = $offset + $max_results;
256                 }
257
258                 for my $j ( $first_record..$last_record ) {
259                     my $record = $tmpresults[ $i - 1 ]->record( $j-1 )->raw(); # 0 indexed
260                     push @results, $record;
261                 }
262             }
263         }
264
265         foreach my $result (@tmpresults) {
266             $result->destroy();
267         }
268         foreach my $zoom_query (@zoom_queries) {
269             $zoom_query->destroy();
270         }
271
272         return ( undef, \@results, $total_hits );
273     }
274 }
275
276 =head2 getRecords
277
278 ( undef, $results_hashref, \@facets_loop ) = getRecords (
279
280         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
281         $results_per_page, $offset,       $expanded_facet, $branches,
282         $query_type,       $scan
283     );
284
285 The all singing, all dancing, multi-server, asynchronous, scanning,
286 searching, record nabbing, facet-building 
287
288 See verbse embedded documentation.
289
290 =cut
291
292 sub getRecords {
293     my (
294         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
295         $results_per_page, $offset,       $expanded_facet, $branches,
296         $query_type,       $scan
297     ) = @_;
298
299     my @servers = @$servers_ref;
300     my @sort_by = @$sort_by_ref;
301
302     # Initialize variables for the ZOOM connection and results object
303     my $zconn;
304     my @zconns;
305     my @results;
306     my $results_hashref = ();
307
308     # Initialize variables for the faceted results objects
309     my $facets_counter = ();
310     my $facets_info    = ();
311     my $facets         = getFacets();
312
313     my @facets_loop;    # stores the ref to array of hashes for template facets loop
314
315     ### LOOP THROUGH THE SERVERS
316     for ( my $i = 0 ; $i < @servers ; $i++ ) {
317         $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
318
319 # perform the search, create the results objects
320 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
321         my $query_to_use = ($servers[$i] =~ /biblioserver/) ? $koha_query : $simple_query;
322
323         #$query_to_use = $simple_query if $scan;
324         warn $simple_query if ( $scan and $DEBUG );
325
326         # Check if we've got a query_type defined, if so, use it
327         eval {
328             if ($query_type) {
329                 if ($query_type =~ /^ccl/) {
330                     $query_to_use =~ s/\:/\=/g;    # change : to = last minute (FIXME)
331                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
332                 } elsif ($query_type =~ /^cql/) {
333                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CQL($query_to_use, $zconns[$i]));
334                 } elsif ($query_type =~ /^pqf/) {
335                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::PQF($query_to_use, $zconns[$i]));
336                 } else {
337                     warn "Unknown query_type '$query_type'.  Results undetermined.";
338                 }
339             } elsif ($scan) {
340                     $results[$i] = $zconns[$i]->scan(  new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
341             } else {
342                     $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
343             }
344         };
345         if ($@) {
346             warn "WARNING: query problem with $query_to_use " . $@;
347         }
348
349         # Concatenate the sort_by limits and pass them to the results object
350         # Note: sort will override rank
351         my $sort_by;
352         foreach my $sort (@sort_by) {
353             if ( $sort eq "author_az" ) {
354                 $sort_by .= "1=1003 <i ";
355             }
356             elsif ( $sort eq "author_za" ) {
357                 $sort_by .= "1=1003 >i ";
358             }
359             elsif ( $sort eq "popularity_asc" ) {
360                 $sort_by .= "1=9003 <i ";
361             }
362             elsif ( $sort eq "popularity_dsc" ) {
363                 $sort_by .= "1=9003 >i ";
364             }
365             elsif ( $sort eq "call_number_asc" ) {
366                 $sort_by .= "1=20  <i ";
367             }
368             elsif ( $sort eq "call_number_dsc" ) {
369                 $sort_by .= "1=20 >i ";
370             }
371             elsif ( $sort eq "pubdate_asc" ) {
372                 $sort_by .= "1=31 <i ";
373             }
374             elsif ( $sort eq "pubdate_dsc" ) {
375                 $sort_by .= "1=31 >i ";
376             }
377             elsif ( $sort eq "acqdate_asc" ) {
378                 $sort_by .= "1=32 <i ";
379             }
380             elsif ( $sort eq "acqdate_dsc" ) {
381                 $sort_by .= "1=32 >i ";
382             }
383             elsif ( $sort eq "title_az" ) {
384                 $sort_by .= "1=4 <i ";
385             }
386             elsif ( $sort eq "title_za" ) {
387                 $sort_by .= "1=4 >i ";
388             }
389             else {
390                 warn "Ignoring unrecognized sort '$sort' requested" if $sort_by;
391             }
392         }
393         if ($sort_by) {
394             if ( $results[$i]->sort( "yaz", $sort_by ) < 0 ) {
395                 warn "WARNING sort $sort_by failed";
396             }
397         }
398     }    # finished looping through servers
399
400     # The big moment: asynchronously retrieve results from all servers
401     while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
402         my $ev = $zconns[ $i - 1 ]->last_event();
403         if ( $ev == ZOOM::Event::ZEND ) {
404             next unless $results[ $i - 1 ];
405             my $size = $results[ $i - 1 ]->size();
406             if ( $size > 0 ) {
407                 my $results_hash;
408
409                 # loop through the results
410                 $results_hash->{'hits'} = $size;
411                 my $times;
412                 if ( $offset + $results_per_page <= $size ) {
413                     $times = $offset + $results_per_page;
414                 }
415                 else {
416                     $times = $size;
417                 }
418                 for ( my $j = $offset ; $j < $times ; $j++ ) {
419                     my $records_hash;
420                     my $record;
421                     my $facet_record;
422
423                     ## Check if it's an index scan
424                     if ($scan) {
425                         my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
426
427                  # here we create a minimal MARC record and hand it off to the
428                  # template just like a normal result ... perhaps not ideal, but
429                  # it works for now
430                         my $tmprecord = MARC::Record->new();
431                         $tmprecord->encoding('UTF-8');
432                         my $tmptitle;
433                         my $tmpauthor;
434
435                 # the minimal record in author/title (depending on MARC flavour)
436                         if (C4::Context->preference("marcflavour") eq "UNIMARC") {
437                             $tmptitle = MARC::Field->new('200',' ',' ', a => $term, f => $occ);
438                             $tmprecord->append_fields($tmptitle);
439                         } else {
440                             $tmptitle  = MARC::Field->new('245',' ',' ', a => $term,);
441                             $tmpauthor = MARC::Field->new('100',' ',' ', a => $occ,);
442                             $tmprecord->append_fields($tmptitle);
443                             $tmprecord->append_fields($tmpauthor);
444                         }
445                         $results_hash->{'RECORDS'}[$j] = $tmprecord->as_usmarc();
446                     }
447
448                     # not an index scan
449                     else {
450                         $record = $results[ $i - 1 ]->record($j)->raw();
451
452                         # warn "RECORD $j:".$record;
453                         $results_hash->{'RECORDS'}[$j] = $record;
454
455             # Fill the facets while we're looping, but only for the biblioserver
456                         $facet_record = MARC::Record->new_from_usmarc($record)
457                           if $servers[ $i - 1 ] =~ /biblioserver/;
458
459                     #warn $servers[$i-1]."\n".$record; #.$facet_record->title();
460                         if ($facet_record) {
461                             for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
462                                 ($facets->[$k]) or next;
463                                 my @fields = map {$facet_record->field($_)} @{$facets->[$k]->{'tags'}} ;
464                                 for my $field (@fields) {
465                                     my @subfields = $field->subfields();
466                                     for my $subfield (@subfields) {
467                                         my ( $code, $data ) = @$subfield;
468                                         ($code eq $facets->[$k]->{'subfield'}) or next;
469                                         $facets_counter->{ $facets->[$k]->{'link_value'} }->{$data}++;
470                                     }
471                                 }
472                                 $facets_info->{ $facets->[$k]->{'link_value'} }->{'label_value'} =
473                                     $facets->[$k]->{'label_value'};
474                                 $facets_info->{ $facets->[$k]->{'link_value'} }->{'expanded'} =
475                                     $facets->[$k]->{'expanded'};
476                             }
477                         }
478                     }
479                 }
480                 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
481             }
482
483             # warn "connection ", $i-1, ": $size hits";
484             # warn $results[$i-1]->record(0)->render() if $size > 0;
485
486             # BUILD FACETS
487             if ( $servers[ $i - 1 ] =~ /biblioserver/ ) {
488                 for my $link_value (
489                     sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
490                         keys %$facets_counter )
491                 {
492                     my $expandable;
493                     my $number_of_facets;
494                     my @this_facets_array;
495                     for my $one_facet (
496                         sort {
497                              $facets_counter->{$link_value}->{$b}
498                          <=> $facets_counter->{$link_value}->{$a}
499                         } keys %{ $facets_counter->{$link_value} }
500                       )
501                     {
502                         $number_of_facets++;
503                         if (   ( $number_of_facets < 6 )
504                             || ( $expanded_facet eq $link_value )
505                             || ( $facets_info->{$link_value}->{'expanded'} ) )
506                         {
507
508                       # Sanitize the link value ), ( will cause errors with CCL,
509                             my $facet_link_value = $one_facet;
510                             $facet_link_value =~ s/(\(|\))/ /g;
511
512                             # fix the length that will display in the label,
513                             my $facet_label_value = $one_facet;
514                             $facet_label_value =
515                               substr( $one_facet, 0, 20 ) . "..."
516                               unless length($facet_label_value) <= 20;
517
518                             # if it's a branch, label by the name, not the code,
519                             if ( $link_value =~ /branch/ ) {
520                                                                 if (defined $branches 
521                                                                         && ref($branches) eq "HASH" 
522                                                                         && defined $branches->{$one_facet} 
523                                                                         && ref ($branches->{$one_facet}) eq "HASH")
524                                                                 {
525                                         $facet_label_value =
526                                                 $branches->{$one_facet}->{'branchname'};
527                                                                 }
528                                                                 else {
529                                                                         $facet_label_value = "*";
530                                                                 }
531                             }
532
533                             # but we're down with the whole label being in the link's title.
534                             push @this_facets_array, {
535                                 facet_count       => $facets_counter->{$link_value}->{$one_facet},
536                                 facet_label_value => $facet_label_value,
537                                 facet_title_value => $one_facet,
538                                 facet_link_value  => $facet_link_value,
539                                 type_link_value   => $link_value,
540                             };
541                         }
542                     }
543
544                     # handle expanded option
545                     unless ( $facets_info->{$link_value}->{'expanded'} ) {
546                         $expandable = 1
547                           if ( ( $number_of_facets > 6 )
548                             && ( $expanded_facet ne $link_value ) );
549                     }
550                     push @facets_loop, {
551                         type_link_value => $link_value,
552                         type_id         => $link_value . "_id",
553                         "type_label_" . $facets_info->{$link_value}->{'label_value'} => 1, 
554                         facets     => \@this_facets_array,
555                         expandable => $expandable,
556                         expand     => $link_value,
557                     } unless ( ($facets_info->{$link_value}->{'label_value'} =~ /Libraries/) and (C4::Context->preference('singleBranchMode')) );
558                 }
559             }
560         }
561     }
562     return ( undef, $results_hashref, \@facets_loop );
563 }
564
565 sub pazGetRecords {
566     my (
567         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
568         $results_per_page, $offset,       $expanded_facet, $branches,
569         $query_type,       $scan
570     ) = @_;
571
572     my $paz = C4::Search::PazPar2->new(C4::Context->config('pazpar2url'));
573     $paz->init();
574     $paz->search($simple_query);
575     sleep 1;   # FIXME: WHY?
576
577     # do results
578     my $results_hashref = {};
579     my $stats = XMLin($paz->stat);
580     my $results = XMLin($paz->show($offset, $results_per_page, 'work-title:1'), forcearray => 1);
581    
582     # for a grouped search result, the number of hits
583     # is the number of groups returned; 'bib_hits' will have
584     # the total number of bibs. 
585     $results_hashref->{'biblioserver'}->{'hits'} = $results->{'merged'}->[0];
586     $results_hashref->{'biblioserver'}->{'bib_hits'} = $stats->{'hits'};
587
588     HIT: foreach my $hit (@{ $results->{'hit'} }) {
589         my $recid = $hit->{recid}->[0];
590
591         my $work_title = $hit->{'md-work-title'}->[0];
592         my $work_author;
593         if (exists $hit->{'md-work-author'}) {
594             $work_author = $hit->{'md-work-author'}->[0];
595         }
596         my $group_label = (defined $work_author) ? "$work_title / $work_author" : $work_title;
597
598         my $result_group = {};
599         $result_group->{'group_label'} = $group_label;
600         $result_group->{'group_merge_key'} = $recid;
601
602         my $count = 1;
603         if (exists $hit->{count}) {
604             $count = $hit->{count}->[0];
605         }
606         $result_group->{'group_count'} = $count;
607
608         for (my $i = 0; $i < $count; $i++) {
609             # FIXME -- may need to worry about diacritics here
610             my $rec = $paz->record($recid, $i);
611             push @{ $result_group->{'RECORDS'} }, $rec;
612         }
613
614         push @{ $results_hashref->{'biblioserver'}->{'GROUPS'} }, $result_group;
615     }
616     
617     # pass through facets
618     my $termlist_xml = $paz->termlist('author,subject');
619     my $terms = XMLin($termlist_xml, forcearray => 1);
620     my @facets_loop = ();
621     #die Dumper($results);
622 #    foreach my $list (sort keys %{ $terms->{'list'} }) {
623 #        my @facets = ();
624 #        foreach my $facet (sort @{ $terms->{'list'}->{$list}->{'term'} } ) {
625 #            push @facets, {
626 #                facet_label_value => $facet->{'name'}->[0],
627 #            };
628 #        }
629 #        push @facets_loop, ( {
630 #            type_label => $list,
631 #            facets => \@facets,
632 #        } );
633 #    }
634
635     return ( undef, $results_hashref, \@facets_loop );
636 }
637
638 # STOPWORDS
639 sub _remove_stopwords {
640     my ( $operand, $index ) = @_;
641     my @stopwords_removed;
642
643     # phrase and exact-qualified indexes shouldn't have stopwords removed
644     if ( $index !~ m/phr|ext/ ) {
645
646 # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
647 #       we use IsAlpha unicode definition, to deal correctly with diacritics.
648 #       otherwise, a French word like "leçon" woudl be split into "le" "çon", "le"
649 #       is a stopword, we'd get "çon" and wouldn't find anything...
650 #       
651                 foreach ( keys %{ C4::Context->stopwords } ) {
652                         next if ( $_ =~ /(and|or|not)/ );    # don't remove operators
653                         $debug && warn "$_ Dump($operand)";
654                         if ( my ($matched) = ($operand =~
655                                 /([^\X\p{isAlnum}]\Q$_\E[^\X\p{isAlnum}]|[^\X\p{isAlnum}]\Q$_\E$|^\Q$_\E[^\X\p{isAlnum}])/gi))
656                         {
657                                 $operand =~ s/\Q$matched\E/ /gi;
658                                 push @stopwords_removed, $_;
659                         }
660                 }
661         }
662     return ( $operand, \@stopwords_removed );
663 }
664
665 # TRUNCATION
666 sub _detect_truncation {
667     my ( $operand, $index ) = @_;
668     my ( @nontruncated, @righttruncated, @lefttruncated, @rightlefttruncated,
669         @regexpr );
670     $operand =~ s/^ //g;
671     my @wordlist = split( /\s/, $operand );
672     foreach my $word (@wordlist) {
673         if ( $word =~ s/^\*([^\*]+)\*$/$1/ ) {
674             push @rightlefttruncated, $word;
675         }
676         elsif ( $word =~ s/^\*([^\*]+)$/$1/ ) {
677             push @lefttruncated, $word;
678         }
679         elsif ( $word =~ s/^([^\*]+)\*$/$1/ ) {
680             push @righttruncated, $word;
681         }
682         elsif ( index( $word, "*" ) < 0 ) {
683             push @nontruncated, $word;
684         }
685         else {
686             push @regexpr, $word;
687         }
688     }
689     return (
690         \@nontruncated,       \@righttruncated, \@lefttruncated,
691         \@rightlefttruncated, \@regexpr
692     );
693 }
694
695 # STEMMING
696 sub _build_stemmed_operand {
697     my ($operand,$lang) = @_;
698     require Lingua::Stem::Snowball ;
699     my $stemmed_operand;
700
701     # If operand contains a digit, it is almost certainly an identifier, and should
702     # not be stemmed.  This is particularly relevant for ISBNs and ISSNs, which
703     # can contain the letter "X" - for example, _build_stemmend_operand would reduce 
704     # "014100018X" to "x ", which for a MARC21 database would bring up irrelevant
705     # results (e.g., "23 x 29 cm." from the 300$c).  Bug 2098.
706     return $operand if $operand =~ /\d/;
707
708 # FIXME: the locale should be set based on the user's language and/or search choice
709     warn "$lang";
710     my $stemmer = Lingua::Stem::Snowball->new( lang => $lang,
711                                                encoding => "UTF-8" );
712
713     my @words = split( / /, $operand );
714     my @stems = $stemmer->stem(\@words);
715     for my $stem (@stems) {
716         $stemmed_operand .= "$stem";
717         $stemmed_operand .= "?"
718           unless ( $stem =~ /(and$|or$|not$)/ ) || ( length($stem) < 3 );
719         $stemmed_operand .= " ";
720     }
721     warn "STEMMED OPERAND: $stemmed_operand" if $DEBUG;
722     return $stemmed_operand;
723 }
724
725 # FIELD WEIGHTING
726 sub _build_weighted_query {
727
728 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
729 # pretty well but could work much better if we had a smarter query parser
730     my ( $operand, $stemmed_operand, $index ) = @_;
731     my $stemming      = C4::Context->preference("QueryStemming")     || 0;
732     my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
733     my $fuzzy_enabled = C4::Context->preference("QueryFuzzy")        || 0;
734
735     my $weighted_query .= "(rk=(";    # Specifies that we're applying rank
736
737     # Keyword, or, no index specified
738     if ( ( $index eq 'kw' ) || ( !$index ) ) {
739         $weighted_query .=
740           "Title-cover,ext,r1=\"$operand\"";    # exact title-cover
741         $weighted_query .= " or ti,ext,r2=\"$operand\"";    # exact title
742         $weighted_query .= " or ti,phr,r3=\"$operand\"";    # phrase title
743           #$weighted_query .= " or any,ext,r4=$operand";               # exact any
744           #$weighted_query .=" or kw,wrdl,r5=\"$operand\"";            # word list any
745         $weighted_query .= " or wrdl,fuzzy,r8=\"$operand\""
746           if $fuzzy_enabled;    # add fuzzy, word list
747         $weighted_query .= " or wrdl,right-Truncation,r9=\"$stemmed_operand\""
748           if ( $stemming and $stemmed_operand )
749           ;                     # add stemming, right truncation
750         $weighted_query .= " or wrdl,r9=\"$operand\"";
751
752         # embedded sorting: 0 a-z; 1 z-a
753         # $weighted_query .= ") or (sort1,aut=1";
754     }
755
756     # Barcode searches should skip this process
757     elsif ( $index eq 'bc' ) {
758         $weighted_query .= "bc=\"$operand\"";
759     }
760
761     # Authority-number searches should skip this process
762     elsif ( $index eq 'an' ) {
763         $weighted_query .= "an=\"$operand\"";
764     }
765
766     # If the index already has more than one qualifier, wrap the operand
767     # in quotes and pass it back (assumption is that the user knows what they
768     # are doing and won't appreciate us mucking up their query
769     elsif ( $index =~ ',' ) {
770         $weighted_query .= " $index=\"$operand\"";
771     }
772
773     #TODO: build better cases based on specific search indexes
774     else {
775         $weighted_query .= " $index,ext,r1=\"$operand\"";    # exact index
776           #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
777         $weighted_query .= " or $index,phr,r3=\"$operand\"";    # phrase index
778         $weighted_query .=
779           " or $index,rt,wrdl,r3=\"$operand\"";    # word list index
780     }
781
782     $weighted_query .= "))";                       # close rank specification
783     return $weighted_query;
784 }
785
786 =head2 buildQuery
787
788 ( $error, $query,
789 $simple_query, $query_cgi,
790 $query_desc, $limit,
791 $limit_cgi, $limit_desc,
792 $stopwords_removed, $query_type ) = buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
793
794 Build queries and limits in CCL, CGI, Human,
795 handle truncation, stemming, field weighting, stopwords, fuzziness, etc.
796
797 See verbose embedded documentation.
798
799
800 =cut
801
802 sub buildQuery {
803     my ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang) = @_;
804
805     warn "---------\nEnter buildQuery\n---------" if $DEBUG;
806
807     # dereference
808     my @operators = $operators ? @$operators : ();
809     my @indexes   = $indexes   ? @$indexes   : ();
810     my @operands  = $operands  ? @$operands  : ();
811     my @limits    = $limits    ? @$limits    : ();
812     my @sort_by   = $sort_by   ? @$sort_by   : ();
813
814     my $stemming         = C4::Context->preference("QueryStemming")        || 0;
815     my $auto_truncation  = C4::Context->preference("QueryAutoTruncate")    || 0;
816     my $weight_fields    = C4::Context->preference("QueryWeightFields")    || 0;
817     my $fuzzy_enabled    = C4::Context->preference("QueryFuzzy")           || 0;
818     my $remove_stopwords = C4::Context->preference("QueryRemoveStopwords") || 0;
819
820     # no stemming/weight/fuzzy in NoZebra
821     if ( C4::Context->preference("NoZebra") ) {
822         $stemming      = 0;
823         $weight_fields = 0;
824         $fuzzy_enabled = 0;
825     }
826
827     my $query        = $operands[0];
828     my $simple_query = $operands[0];
829
830     # initialize the variables we're passing back
831     my $query_cgi;
832     my $query_desc;
833     my $query_type;
834
835     my $limit;
836     my $limit_cgi;
837     my $limit_desc;
838
839     my $stopwords_removed;    # flag to determine if stopwords have been removed
840
841 # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
842 # DIAGNOSTIC ONLY!!
843     if ( $query =~ /^ccl=/ ) {
844         return ( undef, $', $', "q=ccl=$'", $', '', '', '', '', 'ccl' );
845     }
846     if ( $query =~ /^cql=/ ) {
847         return ( undef, $', $', "q=cql=$'", $', '', '', '', '', 'cql' );
848     }
849     if ( $query =~ /^pqf=/ ) {
850         return ( undef, $', $', "q=pqf=$'", $', '', '', '', '', 'pqf' );
851     }
852
853     # pass nested queries directly
854     # FIXME: need better handling of some of these variables in this case
855     # Nested queries aren't handled well and this implementation is flawed and causes users to be
856     # unable to search for anything containing () commenting out, will be rewritten for 3.4.0
857 #    if ( $query =~ /(\(|\))/ ) {
858 #        return (
859 #            undef,              $query, $simple_query, $query_cgi,
860 #            $query,             $limit, $limit_cgi,    $limit_desc,
861 #            $stopwords_removed, 'ccl'
862 #        );
863 #    }
864
865 # Form-based queries are non-nested and fixed depth, so we can easily modify the incoming
866 # query operands and indexes and add stemming, truncation, field weighting, etc.
867 # Once we do so, we'll end up with a value in $query, just like if we had an
868 # incoming $query from the user
869     else {
870         $query = ""
871           ; # clear it out so we can populate properly with field-weighted, stemmed, etc. query
872         my $previous_operand
873           ;    # a flag used to keep track if there was a previous query
874                # if there was, we can apply the current operator
875                # for every operand
876         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
877
878             # COMBINE OPERANDS, INDEXES AND OPERATORS
879             if ( $operands[$i] ) {
880
881               # A flag to determine whether or not to add the index to the query
882                 my $indexes_set;
883
884 # If the user is sophisticated enough to specify an index, turn off field weighting, stemming, and stopword handling
885                 if ( $operands[$i] =~ /(:|=)/ || $scan ) {
886                     $weight_fields    = 0;
887                     $stemming         = 0;
888                     $remove_stopwords = 0;
889                 }
890                 my $operand = $operands[$i];
891                 my $index   = $indexes[$i];
892
893                 # Add index-specific attributes
894                 # Date of Publication
895                 if ( $index eq 'yr' ) {
896                     $index .= ",st-numeric";
897                     $indexes_set++;
898                                         $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
899                 }
900
901                 # Date of Acquisition
902                 elsif ( $index eq 'acqdate' ) {
903                     $index .= ",st-date-normalized";
904                     $indexes_set++;
905                                         $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
906                 }
907                 # ISBN,ISSN,Standard Number, don't need special treatment
908                 elsif ( $index eq 'nb' || $index eq 'ns' ) {
909                     $indexes_set++;
910                     (   
911                         $stemming,      $auto_truncation,
912                         $weight_fields, $fuzzy_enabled,
913                         $remove_stopwords
914                     ) = ( 0, 0, 0, 0, 0 );
915
916                 }
917                 # Set default structure attribute (word list)
918                 my $struct_attr;
919                 unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl)/ ) {
920                     $struct_attr = ",wrdl";
921                 }
922
923                 # Some helpful index variants
924                 my $index_plus       = $index . $struct_attr . ":" if $index;
925                 my $index_plus_comma = $index . $struct_attr . "," if $index;
926
927                 # Remove Stopwords
928                 if ($remove_stopwords) {
929                     ( $operand, $stopwords_removed ) =
930                       _remove_stopwords( $operand, $index );
931                     warn "OPERAND w/out STOPWORDS: >$operand<" if $DEBUG;
932                     warn "REMOVED STOPWORDS: @$stopwords_removed"
933                       if ( $stopwords_removed && $DEBUG );
934                 }
935
936                 if ($auto_truncation){
937                    # join throws an error if there is a leading space
938                    $operand =~ s/^\s+//;
939                    $operand=~join(" ",map{ "$_*" }split (/\s+/,$operand));
940                 }
941
942                 # Detect Truncation
943                 my $truncated_operand;
944                 my( $nontruncated, $righttruncated, $lefttruncated,
945                     $rightlefttruncated, $regexpr
946                 ) = _detect_truncation( $operand, $index );
947                 warn
948 "TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<"
949                   if $DEBUG;
950
951                 # Apply Truncation
952                 if (
953                     scalar(@$righttruncated) + scalar(@$lefttruncated) +
954                     scalar(@$rightlefttruncated) > 0 )
955                 {
956
957                # Don't field weight or add the index to the query, we do it here
958                     $indexes_set = 1;
959                     undef $weight_fields;
960                     my $previous_truncation_operand;
961                     if (scalar @$nontruncated) {
962                         $truncated_operand .= "$index_plus @$nontruncated ";
963                         $previous_truncation_operand = 1;
964                     }
965                     if (scalar @$righttruncated) {
966                         $truncated_operand .= "and " if $previous_truncation_operand;
967                         $truncated_operand .= $index_plus_comma . "rtrn:@$righttruncated ";
968                         $previous_truncation_operand = 1;
969                     }
970                     if (scalar @$lefttruncated) {
971                         $truncated_operand .= "and " if $previous_truncation_operand;
972                         $truncated_operand .= $index_plus_comma . "ltrn:@$lefttruncated ";
973                         $previous_truncation_operand = 1;
974                     }
975                     if (scalar @$rightlefttruncated) {
976                         $truncated_operand .= "and " if $previous_truncation_operand;
977                         $truncated_operand .= $index_plus_comma . "rltrn:@$rightlefttruncated ";
978                         $previous_truncation_operand = 1;
979                     }
980                 }
981                 $operand = $truncated_operand if $truncated_operand;
982                 warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
983
984                 # Handle Stemming
985                 my $stemmed_operand;
986                 $stemmed_operand = _build_stemmed_operand($operand, $lang)
987                                                                                 if $stemming;
988
989                 warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
990
991                 # Handle Field Weighting
992                 my $weighted_operand;
993                 if ($weight_fields) {
994                     $weighted_operand = _build_weighted_query( $operand, $stemmed_operand, $index );
995                     $operand = $weighted_operand;
996                     $indexes_set = 1;
997                 }
998
999                 warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
1000
1001                 # If there's a previous operand, we need to add an operator
1002                 if ($previous_operand) {
1003
1004                     # User-specified operator
1005                     if ( $operators[ $i - 1 ] ) {
1006                         $query     .= " $operators[$i-1] ";
1007                         $query     .= " $index_plus " unless $indexes_set;
1008                         $query     .= " $operand";
1009                         $query_cgi .= "&op=$operators[$i-1]";
1010                         $query_cgi .= "&idx=$index" if $index;
1011                         $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1012                         $query_desc .=
1013                           " $operators[$i-1] $index_plus $operands[$i]";
1014                     }
1015
1016                     # Default operator is and
1017                     else {
1018                         $query      .= " and ";
1019                         $query      .= "$index_plus " unless $indexes_set;
1020                         $query      .= "$operand";
1021                         $query_cgi  .= "&op=and&idx=$index" if $index;
1022                         $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
1023                         $query_desc .= " and $index_plus $operands[$i]";
1024                     }
1025                 }
1026
1027                 # There isn't a pervious operand, don't need an operator
1028                 else {
1029
1030                     # Field-weighted queries already have indexes set
1031                     $query .= " $index_plus " unless $indexes_set;
1032                     $query .= $operand;
1033                     $query_desc .= " $index_plus $operands[$i]";
1034                     $query_cgi  .= "&idx=$index" if $index;
1035                     $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
1036                     $previous_operand = 1;
1037                 }
1038             }    #/if $operands
1039         }    # /for
1040     }
1041     warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1042
1043     # add limits
1044     my $group_OR_limits;
1045     my $availability_limit;
1046     foreach my $this_limit (@limits) {
1047 #        if ( $this_limit =~ /available/ ) {
1048 #
1049 ## 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1050 ## In English:
1051 ## all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1052 #            $availability_limit .=
1053 #"( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and (lost,st-numeric=0) )"; #or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
1054 #            $limit_cgi  .= "&limit=available";
1055 #            $limit_desc .= "";
1056 #        }
1057 #
1058         # group_OR_limits, prefixed by mc-
1059         # OR every member of the group
1060 #        elsif ( $this_limit =~ /mc/ ) {
1061         if ( $this_limit =~ /mc/ ) {
1062             $group_OR_limits .= " or " if $group_OR_limits;
1063             $limit_desc      .= " or " if $group_OR_limits;
1064             $group_OR_limits .= "$this_limit";
1065             $limit_cgi       .= "&limit=$this_limit";
1066             $limit_desc      .= " $this_limit";
1067         }
1068
1069         # Regular old limits
1070         else {
1071             $limit .= " and " if $limit || $query;
1072             $limit      .= "$this_limit";
1073             $limit_cgi  .= "&limit=$this_limit";
1074             if ($this_limit =~ /^branch:(.+)/) {
1075                 my $branchcode = $1;
1076                 my $branchname = GetBranchName($branchcode);
1077                 if (defined $branchname) {
1078                     $limit_desc .= " branch:$branchname";
1079                 } else {
1080                     $limit_desc .= " $this_limit";
1081                 }
1082             } else {
1083                 $limit_desc .= " $this_limit";
1084             }
1085         }
1086     }
1087     if ($group_OR_limits) {
1088         $limit .= " and " if ( $query || $limit );
1089         $limit .= "($group_OR_limits)";
1090     }
1091     if ($availability_limit) {
1092         $limit .= " and " if ( $query || $limit );
1093         $limit .= "($availability_limit)";
1094     }
1095
1096     # Normalize the query and limit strings
1097     $query =~ s/:/=/g;
1098     $limit =~ s/:/=/g;
1099     for ( $query, $query_desc, $limit, $limit_desc ) {
1100         s/  / /g;    # remove extra spaces
1101         s/^ //g;     # remove any beginning spaces
1102         s/ $//g;     # remove any ending spaces
1103         s/==/=/g;    # remove double == from query
1104     }
1105     $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1106
1107     for ($query_cgi,$simple_query) {
1108         s/"//g;
1109     }
1110     # append the limit to the query
1111     $query .= " " . $limit;
1112
1113     # Warnings if DEBUG
1114     if ($DEBUG) {
1115         warn "QUERY:" . $query;
1116         warn "QUERY CGI:" . $query_cgi;
1117         warn "QUERY DESC:" . $query_desc;
1118         warn "LIMIT:" . $limit;
1119         warn "LIMIT CGI:" . $limit_cgi;
1120         warn "LIMIT DESC:" . $limit_desc;
1121         warn "---------\nLeave buildQuery\n---------";
1122     }
1123     return (
1124         undef,              $query, $simple_query, $query_cgi,
1125         $query_desc,        $limit, $limit_cgi,    $limit_desc,
1126         $stopwords_removed, $query_type
1127     );
1128 }
1129
1130 =head2 searchResults
1131
1132 Format results in a form suitable for passing to the template
1133
1134 =cut
1135
1136 # IMO this subroutine is pretty messy still -- it's responsible for
1137 # building the HTML output for the template
1138 sub searchResults {
1139     my ( $searchdesc, $hits, $results_per_page, $offset, $scan, @marcresults, $hidelostitems ) = @_;
1140     my $dbh = C4::Context->dbh;
1141     my @newresults;
1142
1143     #Build branchnames hash
1144     #find branchname
1145     #get branch information.....
1146     my %branches;
1147     my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
1148     $bsth->execute();
1149     while ( my $bdata = $bsth->fetchrow_hashref ) {
1150         $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
1151     }
1152 # FIXME - We build an authorised values hash here, using the default framework
1153 # though it is possible to have different authvals for different fws.
1154
1155     my $shelflocations =GetKohaAuthorisedValues('items.location','');
1156
1157     # get notforloan authorised value list (see $shelflocations  FIXME)
1158     my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
1159
1160     #Build itemtype hash
1161     #find itemtype & itemtype image
1162     my %itemtypes;
1163     $bsth =
1164       $dbh->prepare(
1165         "SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes"
1166       );
1167     $bsth->execute();
1168     while ( my $bdata = $bsth->fetchrow_hashref ) {
1169                 foreach (qw(description imageurl summary notforloan)) {
1170                 $itemtypes{ $bdata->{'itemtype'} }->{$_} = $bdata->{$_};
1171                 }
1172     }
1173
1174     #search item field code
1175     my $sth =
1176       $dbh->prepare(
1177 "SELECT tagfield FROM marc_subfield_structure WHERE kohafield LIKE 'items.itemnumber'"
1178       );
1179     $sth->execute;
1180     my ($itemtag) = $sth->fetchrow;
1181
1182     ## find column names of items related to MARC
1183     my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
1184     $sth2->execute;
1185     my %subfieldstosearch;
1186     while ( ( my $column ) = $sth2->fetchrow ) {
1187         my ( $tagfield, $tagsubfield ) =
1188           &GetMarcFromKohaField( "items." . $column, "" );
1189         $subfieldstosearch{$column} = $tagsubfield;
1190     }
1191
1192     # handle which records to actually retrieve
1193     my $times;
1194     if ( $hits && $offset + $results_per_page <= $hits ) {
1195         $times = $offset + $results_per_page;
1196     }
1197     else {
1198         $times = $hits;  # FIXME: if $hits is undefined, why do we want to equal it?
1199     }
1200
1201         my $marcflavour = C4::Context->preference("marcflavour");
1202     # We get the biblionumber position in MARC 
1203     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
1204     my $fw;
1205     
1206     # loop through all of the records we've retrieved
1207     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1208         my $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
1209         
1210         if ($bibliotag<10){
1211             $fw = GetFrameworkCode($marcrecord->field($bibliotag)->data);
1212         }else{
1213             $fw = GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
1214         }
1215         
1216         my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, $fw );
1217         $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
1218         $oldbiblio->{result_number} = $i + 1;
1219
1220         # add imageurl to itemtype if there is one
1221         $oldbiblio->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1222
1223         $oldbiblio->{'authorised_value_images'}  = C4::Items::get_authorised_value_images( C4::Biblio::get_biblio_authorised_values( $oldbiblio->{'biblionumber'}, $marcrecord ) );
1224                 $oldbiblio->{normalized_upc}  = GetNormalizedUPC(       $marcrecord,$marcflavour);
1225                 $oldbiblio->{normalized_ean}  = GetNormalizedEAN(       $marcrecord,$marcflavour);
1226                 $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
1227                 $oldbiblio->{normalized_isbn} = GetNormalizedISBN(undef,$marcrecord,$marcflavour);
1228                 $oldbiblio->{content_identifier_exists} = 1 if ($oldbiblio->{normalized_isbn} or $oldbiblio->{normalized_oclc} or $oldbiblio->{normalized_ean} or $oldbiblio->{normalized_upc});
1229
1230                 # edition information, if any
1231         $oldbiblio->{edition} = $oldbiblio->{editionstatement};
1232                 $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1233  # Build summary if there is one (the summary is defined in the itemtypes table)
1234  # FIXME: is this used anywhere, I think it can be commented out? -- JF
1235         if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1236             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1237             my @fields  = $marcrecord->fields();
1238             
1239             my $newsummary;
1240             foreach my $line ( "$summary\n" =~ /(.*)\n/g ){
1241                 my $tags = {};
1242                 foreach my $tag ( $line =~ /\[(\d{3}[\w|\d])\]/ ) {
1243                     $tag =~ /(.{3})(.)/;
1244                     if($marcrecord->field($1)){
1245                         my @abc = $marcrecord->field($1)->subfield($2);
1246                         $tags->{$tag} = $#abc + 1 ;
1247                     }
1248                 }
1249                 
1250                 # We catch how many times to repeat this line
1251                 my $max = 0;
1252                 foreach my $tag (keys(%$tags)){
1253                     $max = $tags->{$tag} if($tags->{$tag} > $max);
1254                  }
1255                 
1256                 # we replace, and repeat each line
1257                 for (my $i = 0 ; $i < $max ; $i++){
1258                     my $newline = $line;
1259
1260                     foreach my $tag ( $newline =~ /\[(\d{3}[\w|\d])\]/g ) {
1261                         $tag =~ /(.{3})(.)/;
1262                         
1263                         if($marcrecord->field($1)){
1264                             my @repl = $marcrecord->field($1)->subfield($2);
1265                             my $subfieldvalue = $repl[$i];
1266                             
1267                             if (! utf8::is_utf8($subfieldvalue)) {
1268                                 utf8::decode($subfieldvalue);
1269                             }
1270  
1271                              $newline =~ s/\[$tag\]/$subfieldvalue/g;
1272                         }
1273                     }
1274                     $newsummary .= "$newline\n";
1275                 }
1276             }
1277
1278             $newsummary =~ s/\[(.*?)]//g;
1279             $newsummary =~ s/\n/<br\/>/g;
1280             $oldbiblio->{summary} = $newsummary;
1281         }
1282
1283         # Pull out the items fields
1284         my @fields = $marcrecord->field($itemtag);
1285
1286         # Setting item statuses for display
1287         my @available_items_loop;
1288         my @onloan_items_loop;
1289         my @other_items_loop;
1290
1291         my $available_items;
1292         my $onloan_items;
1293         my $other_items;
1294
1295         my $ordered_count         = 0;
1296         my $available_count       = 0;
1297         my $onloan_count          = 0;
1298         my $longoverdue_count     = 0;
1299         my $other_count           = 0;
1300         my $wthdrawn_count        = 0;
1301         my $itemlost_count        = 0;
1302         my $itembinding_count     = 0;
1303         my $itemdamaged_count     = 0;
1304         my $item_in_transit_count = 0;
1305         my $can_place_holds       = 0;
1306         my $items_count           = scalar(@fields);
1307         my $maxitems =
1308           ( C4::Context->preference('maxItemsinSearchResults') )
1309           ? C4::Context->preference('maxItemsinSearchResults') - 1
1310           : 1;
1311
1312         # loop through every item
1313         foreach my $field (@fields) {
1314             my $item;
1315
1316             # populate the items hash
1317             foreach my $code ( keys %subfieldstosearch ) {
1318                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1319             }
1320             
1321                         my $hbranch     = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch'    : 'holdingbranch';
1322                         my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
1323             # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
1324             if ($item->{$hbranch}) {
1325                 $item->{'branchname'} = $branches{$item->{$hbranch}};
1326             }
1327             elsif ($item->{$otherbranch}) {     # Last resort
1328                 $item->{'branchname'} = $branches{$item->{$otherbranch}}; 
1329             }
1330
1331                         my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
1332 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
1333             if ( $item->{onloan} ) {
1334                 $onloan_count++;
1335                                 my $key = $prefix . $item->{onloan} . $item->{barcode};
1336                                 $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
1337                                 $onloan_items->{$key}->{count}++ if $item->{$hbranch};
1338                                 $onloan_items->{$key}->{branchname} = $item->{branchname};
1339                                 $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1340                                 $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
1341                                 $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1342                 # if something's checked out and lost, mark it as 'long overdue'
1343                 if ( $item->{itemlost} ) {
1344                     $onloan_items->{$prefix}->{longoverdue}++;
1345                     $longoverdue_count++;
1346                 } else {        # can place holds as long as item isn't lost
1347                     $can_place_holds = 1;
1348                 }
1349             }
1350
1351          # items not on loan, but still unavailable ( lost, withdrawn, damaged )
1352             else {
1353
1354                 # item is on order
1355                 if ( $item->{notforloan} == -1 ) {
1356                     $ordered_count++;
1357                 }
1358
1359                 # is item in transit?
1360                 my $transfertwhen = '';
1361                 my ($transfertfrom, $transfertto);
1362                 
1363                 unless ($item->{wthdrawn}
1364                         || $item->{itemlost}
1365                         || $item->{damaged}
1366                         || $item->{notforloan}
1367                         || $items_count > 20) {
1368
1369                     # A couple heuristics to limit how many times
1370                     # we query the database for item transfer information, sacrificing
1371                     # accuracy in some cases for speed;
1372                     #
1373                     # 1. don't query if item has one of the other statuses
1374                     # 2. don't check transit status if the bib has
1375                     #    more than 20 items
1376                     #
1377                     # FIXME: to avoid having the query the database like this, and to make
1378                     #        the in transit status count as unavailable for search limiting,
1379                     #        should map transit status to record indexed in Zebra.
1380                     #
1381                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
1382                 }
1383
1384                 # item is withdrawn, lost or damaged
1385                 if (   $item->{wthdrawn}
1386                     || $item->{itemlost}
1387                     || $item->{damaged}
1388                     || $item->{notforloan} 
1389                     || ($transfertwhen ne ''))
1390                 {
1391                     $wthdrawn_count++        if $item->{wthdrawn};
1392                     $itemlost_count++        if $item->{itemlost};
1393                     $itemdamaged_count++     if $item->{damaged};
1394                     $item_in_transit_count++ if $transfertwhen ne '';
1395                     $item->{status} = $item->{wthdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
1396                     $other_count++;
1397
1398                                         my $key = $prefix . $item->{status};
1399                                         foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber)) {
1400                         $other_items->{$key}->{$_} = $item->{$_};
1401                                         }
1402                     $other_items->{$key}->{intransit} = ($transfertwhen ne '') ? 1 : 0;
1403                                         $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
1404                                         $other_items->{$key}->{count}++ if $item->{$hbranch};
1405                                         $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1406                                         $other_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1407                 }
1408                 # item is available
1409                 else {
1410                     $can_place_holds = 1;
1411                     $available_count++;
1412                                         $available_items->{$prefix}->{count}++ if $item->{$hbranch};
1413                                         foreach (qw(branchname itemcallnumber)) {
1414                         $available_items->{$prefix}->{$_} = $item->{$_};
1415                                         }
1416                                         $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
1417                                         $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1418                 }
1419             }
1420         }    # notforloan, item level and biblioitem level
1421         my ( $availableitemscount, $onloanitemscount, $otheritemscount );
1422         $maxitems =
1423           ( C4::Context->preference('maxItemsinSearchResults') )
1424           ? C4::Context->preference('maxItemsinSearchResults') - 1
1425           : 1;
1426         for my $key ( sort keys %$onloan_items ) {
1427             (++$onloanitemscount > $maxitems) and last;
1428             push @onloan_items_loop, $onloan_items->{$key};
1429         }
1430         for my $key ( sort keys %$other_items ) {
1431             (++$otheritemscount > $maxitems) and last;
1432             push @other_items_loop, $other_items->{$key};
1433         }
1434         for my $key ( sort keys %$available_items ) {
1435             (++$availableitemscount > $maxitems) and last;
1436             push @available_items_loop, $available_items->{$key}
1437         }
1438
1439         # XSLT processing of some stuff
1440         if (C4::Context->preference("XSLTResultsDisplay") && !$scan) {
1441             $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display(
1442                 $oldbiblio->{biblionumber}, $marcrecord, 'Results' );
1443         }
1444
1445         # last check for norequest : if itemtype is notforloan, it can't be reserved either, whatever the items
1446         $can_place_holds = 0
1447           if $itemtypes{ $oldbiblio->{itemtype} }->{notforloan};
1448         $oldbiblio->{norequests} = 1 unless $can_place_holds;
1449         $oldbiblio->{itemsplural}          = 1 if $items_count > 1;
1450         $oldbiblio->{items_count}          = $items_count;
1451         $oldbiblio->{available_items_loop} = \@available_items_loop;
1452         $oldbiblio->{onloan_items_loop}    = \@onloan_items_loop;
1453         $oldbiblio->{other_items_loop}     = \@other_items_loop;
1454         $oldbiblio->{availablecount}       = $available_count;
1455         $oldbiblio->{availableplural}      = 1 if $available_count > 1;
1456         $oldbiblio->{onloancount}          = $onloan_count;
1457         $oldbiblio->{onloanplural}         = 1 if $onloan_count > 1;
1458         $oldbiblio->{othercount}           = $other_count;
1459         $oldbiblio->{otherplural}          = 1 if $other_count > 1;
1460         $oldbiblio->{wthdrawncount}        = $wthdrawn_count;
1461         $oldbiblio->{itemlostcount}        = $itemlost_count;
1462         $oldbiblio->{damagedcount}         = $itemdamaged_count;
1463         $oldbiblio->{intransitcount}       = $item_in_transit_count;
1464         $oldbiblio->{orderedcount}         = $ordered_count;
1465         $oldbiblio->{isbn} =~
1466           s/-//g;    # deleting - in isbn to enable amazon content
1467         push( @newresults, $oldbiblio ) 
1468             if(not $hidelostitems
1469                or (($items_count > $itemlost_count ) 
1470                     && $hidelostitems));
1471     }
1472     
1473     return @newresults;
1474 }
1475
1476 =head2 SearchAcquisitions
1477     Search for acquisitions 
1478 =cut
1479
1480 sub SearchAcquisitions{
1481     my ($datebegin, $dateend, $itemtypes,$criteria, $orderby) = @_;
1482     
1483     my $dbh=C4::Context->dbh;
1484     # Variable initialization
1485     my $str=qq|
1486     SELECT marcxml 
1487     FROM biblio 
1488     LEFT JOIN biblioitems ON biblioitems.biblionumber=biblio.biblionumber
1489     LEFT JOIN items ON items.biblionumber=biblio.biblionumber
1490     WHERE dateaccessioned BETWEEN ? AND ? 
1491     |;
1492     
1493     my (@params,@loopcriteria);
1494     
1495     push @params, $datebegin->output("iso");
1496     push @params, $dateend->output("iso");
1497
1498     if (scalar(@$itemtypes)>0 and $criteria ne "itemtype" ){
1499         if(C4::Context->preference("item-level_itypes")){
1500             $str .= "AND items.itype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1501         }else{
1502             $str .= "AND biblioitems.itemtype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1503         }    
1504         push @params, @$itemtypes;
1505     }
1506         
1507     if ($criteria =~/itemtype/){
1508         if(C4::Context->preference("item-level_itypes")){
1509             $str .= "AND items.itype=? ";
1510         }else{
1511             $str .= "AND biblioitems.itemtype=? ";
1512         }
1513         
1514         if(scalar(@$itemtypes) == 0){
1515             my $itypes = GetItemTypes();
1516             for my $key (keys %$itypes){
1517                 push @$itemtypes, $key;
1518             }
1519         }
1520         
1521         @loopcriteria= @$itemtypes;
1522     }elsif ($criteria=~/itemcallnumber/){
1523         $str .= "AND (items.itemcallnumber LIKE CONCAT(?,'%') 
1524                  OR items.itemcallnumber is NULL
1525                  OR items.itemcallnumber = '')";
1526
1527         @loopcriteria = ("AA".."ZZ", "") unless (scalar(@loopcriteria)>0);  
1528     }else {
1529         $str .= "AND biblio.title LIKE CONCAT(?,'%') ";
1530         @loopcriteria = ("A".."z") unless (scalar(@loopcriteria)>0);  
1531     }
1532         
1533     if ($orderby =~ /date_desc/){
1534         $str.=" ORDER BY dateaccessioned DESC";
1535     } else {
1536         $str.=" ORDER BY title";
1537     }
1538     
1539     my $qdataacquisitions=$dbh->prepare($str);
1540         
1541     my @loopacquisitions;
1542     foreach my $value(@loopcriteria){
1543         push @params,$value;
1544         my %cell;
1545         $cell{"title"}=$value;
1546         $cell{"titlecode"}=$value;
1547         
1548         eval{$qdataacquisitions->execute(@params);};
1549   
1550         if ($@){ warn "recentacquisitions Error :$@";}
1551         else {
1552             my @loopdata;
1553             while (my $data=$qdataacquisitions->fetchrow_hashref){
1554                 push @loopdata, {"summary"=>GetBiblioSummary( $data->{'marcxml'} ) };
1555             }
1556             $cell{"loopdata"}=\@loopdata;
1557         }
1558         push @loopacquisitions,\%cell if (scalar(@{$cell{loopdata}})>0);
1559         pop @params;
1560     }
1561     $qdataacquisitions->finish;
1562     return \@loopacquisitions;
1563 }
1564 #----------------------------------------------------------------------
1565 #
1566 # Non-Zebra GetRecords#
1567 #----------------------------------------------------------------------
1568
1569 =head2 NZgetRecords
1570
1571   NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1572
1573 =cut
1574
1575 sub NZgetRecords {
1576     my (
1577         $query,            $simple_query, $sort_by_ref,    $servers_ref,
1578         $results_per_page, $offset,       $expanded_facet, $branches,
1579         $query_type,       $scan
1580     ) = @_;
1581     warn "query =$query" if $DEBUG;
1582     my $result = NZanalyse($query);
1583     warn "results =$result" if $DEBUG;
1584     return ( undef,
1585         NZorder( $result, @$sort_by_ref[0], $results_per_page, $offset ),
1586         undef );
1587 }
1588
1589 =head2 NZanalyse
1590
1591   NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1592   the list is built from an inverted index in the nozebra SQL table
1593   note that title is here only for convenience : the sorting will be very fast when requested on title
1594   if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1595
1596 =cut
1597
1598 sub NZanalyse {
1599     my ( $string, $server ) = @_;
1600 #     warn "---------"       if $DEBUG;
1601     warn " NZanalyse" if $DEBUG;
1602 #     warn "---------"       if $DEBUG;
1603
1604  # $server contains biblioserver or authorities, depending on what we search on.
1605  #warn "querying : $string on $server";
1606     $server = 'biblioserver' unless $server;
1607
1608 # if we have a ", replace the content to discard temporarily any and/or/not inside
1609     my $commacontent;
1610     if ( $string =~ /"/ ) {
1611         $string =~ s/"(.*?)"/__X__/;
1612         $commacontent = $1;
1613         warn "commacontent : $commacontent" if $DEBUG;
1614     }
1615
1616 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1617 # then, call again NZanalyse with $left and $right
1618 # (recursive until we find a leaf (=> something without and/or/not)
1619 # delete repeated operator... Would then go in infinite loop
1620     while ( $string =~ s/( and| or| not| AND| OR| NOT)\1/$1/g ) {
1621     }
1622
1623     #process parenthesis before.
1624     if ( $string =~ /^\s*\((.*)\)(( and | or | not | AND | OR | NOT )(.*))?/ ) {
1625         my $left     = $1;
1626         my $right    = $4;
1627         my $operator = lc($3);   # FIXME: and/or/not are operators, not operands
1628         warn
1629 "dealing w/parenthesis before recursive sub call. left :$left operator:$operator right:$right"
1630           if $DEBUG;
1631         my $leftresult = NZanalyse( $left, $server );
1632         if ($operator) {
1633             my $rightresult = NZanalyse( $right, $server );
1634
1635             # OK, we have the results for right and left part of the query
1636             # depending of operand, intersect, union or exclude both lists
1637             # to get a result list
1638             if ( $operator eq ' and ' ) {
1639                 return NZoperatorAND($leftresult,$rightresult);      
1640             }
1641             elsif ( $operator eq ' or ' ) {
1642
1643                 # just merge the 2 strings
1644                 return $leftresult . $rightresult;
1645             }
1646             elsif ( $operator eq ' not ' ) {
1647                 return NZoperatorNOT($leftresult,$rightresult);      
1648             }
1649         }      
1650         else {
1651 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1652             return $leftresult;
1653         } 
1654     }
1655     warn "string :" . $string if $DEBUG;
1656     my $left = "";
1657     my $right = "";
1658     my $operator = "";
1659     if ($string =~ /(.*?)( and | or | not | AND | OR | NOT )(.*)/) {
1660         $left     = $1;
1661         $right    = $3;
1662         $operator = lc($2);    # FIXME: and/or/not are operators, not operands
1663     }
1664     warn "no parenthesis. left : $left operator: $operator right: $right"
1665       if $DEBUG;
1666
1667     # it's not a leaf, we have a and/or/not
1668     if ($operator) {
1669
1670         # reintroduce comma content if needed
1671         $right =~ s/__X__/"$commacontent"/ if $commacontent;
1672         $left  =~ s/__X__/"$commacontent"/ if $commacontent;
1673         warn "node : $left / $operator / $right\n" if $DEBUG;
1674         my $leftresult  = NZanalyse( $left,  $server );
1675         my $rightresult = NZanalyse( $right, $server );
1676         warn " leftresult : $leftresult" if $DEBUG;
1677         warn " rightresult : $rightresult" if $DEBUG;
1678         # OK, we have the results for right and left part of the query
1679         # depending of operand, intersect, union or exclude both lists
1680         # to get a result list
1681         if ( $operator eq ' and ' ) {
1682             warn "NZAND";
1683             return NZoperatorAND($leftresult,$rightresult);
1684         }
1685         elsif ( $operator eq ' or ' ) {
1686
1687             # just merge the 2 strings
1688             return $leftresult . $rightresult;
1689         }
1690         elsif ( $operator eq ' not ' ) {
1691             return NZoperatorNOT($leftresult,$rightresult);
1692         }
1693         else {
1694
1695 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1696             die "error : operand unknown : $operator for $string";
1697         }
1698
1699         # it's a leaf, do the real SQL query and return the result
1700     }
1701     else {
1702         $string =~ s/__X__/"$commacontent"/ if $commacontent;
1703         $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|&|\+|\*|\// /g;
1704         #remove trailing blank at the beginning
1705         $string =~ s/^ //g;
1706         warn "leaf:$string" if $DEBUG;
1707
1708         # parse the string in in operator/operand/value again
1709         my $left = "";
1710         my $operator = "";
1711         my $right = "";
1712         if ($string =~ /(.*)(>=|<=)(.*)/) {
1713             $left     = $1;
1714             $operator = $2;
1715             $right    = $3;
1716         } else {
1717             $left = $string;
1718         }
1719 #         warn "handling leaf... left:$left operator:$operator right:$right"
1720 #           if $DEBUG;
1721         unless ($operator) {
1722             if ($string =~ /(.*)(>|<|=)(.*)/) {
1723                 $left     = $1;
1724                 $operator = $2;
1725                 $right    = $3;
1726                 warn
1727     "handling unless (operator)... left:$left operator:$operator right:$right"
1728                 if $DEBUG;
1729             } else {
1730                 $left = $string;
1731             }
1732         }
1733         my $results;
1734
1735 # strip adv, zebra keywords, currently not handled in nozebra: wrdl, ext, phr...
1736         $left =~ s/ .*$//;
1737
1738         # automatic replace for short operators
1739         $left = 'title'            if $left =~ '^ti$';
1740         $left = 'author'           if $left =~ '^au$';
1741         $left = 'publisher'        if $left =~ '^pb$';
1742         $left = 'subject'          if $left =~ '^su$';
1743         $left = 'koha-Auth-Number' if $left =~ '^an$';
1744         $left = 'keyword'          if $left =~ '^kw$';
1745         $left = 'itemtype'         if $left =~ '^mc$'; # Fix for Bug 2599 - Search limits not working for NoZebra 
1746         warn "handling leaf... left:$left operator:$operator right:$right" if $DEBUG;
1747         my $dbh = C4::Context->dbh;
1748         if ( $operator && $left ne 'keyword' ) {
1749             #do a specific search
1750             $operator = 'LIKE' if $operator eq '=' and $right =~ /%/;
1751             my $sth = $dbh->prepare(
1752 "SELECT biblionumbers,value FROM nozebra WHERE server=? AND indexname=? AND value $operator ?"
1753             );
1754             warn "$left / $operator / $right\n" if $DEBUG;
1755
1756             # split each word, query the DB and build the biblionumbers result
1757             #sanitizing leftpart
1758             $left =~ s/^\s+|\s+$//;
1759             foreach ( split / /, $right ) {
1760                 my $biblionumbers;
1761                 $_ =~ s/^\s+|\s+$//;
1762                 next unless $_;
1763                 warn "EXECUTE : $server, $left, $_" if $DEBUG;
1764                 $sth->execute( $server, $left, $_ )
1765                   or warn "execute failed: $!";
1766                 while ( my ( $line, $value ) = $sth->fetchrow ) {
1767
1768 # if we are dealing with a numeric value, use only numeric results (in case of >=, <=, > or <)
1769 # otherwise, fill the result
1770                     $biblionumbers .= $line
1771                       unless ( $right =~ /^\d+$/ && $value =~ /\D/ );
1772                     warn "result : $value "
1773                       . ( $right  =~ /\d/ ) . "=="
1774                       . ( $value =~ /\D/?$line:"" ) if $DEBUG;         #= $line";
1775                 }
1776
1777 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1778                 if ($results) {
1779                     warn "NZAND" if $DEBUG;
1780                     $results = NZoperatorAND($biblionumbers,$results);
1781                 } else {
1782                     $results = $biblionumbers;
1783                 }
1784             }
1785         }
1786         else {
1787       #do a complete search (all indexes), if index='kw' do complete search too.
1788             my $sth = $dbh->prepare(
1789 "SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?"
1790             );
1791
1792             # split each word, query the DB and build the biblionumbers result
1793             foreach ( split / /, $string ) {
1794                 next if C4::Context->stopwords->{ uc($_) };   # skip if stopword
1795                 warn "search on all indexes on $_" if $DEBUG;
1796                 my $biblionumbers;
1797                 next unless $_;
1798                 $sth->execute( $server, $_ );
1799                 while ( my $line = $sth->fetchrow ) {
1800                     $biblionumbers .= $line;
1801                 }
1802
1803 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1804                 if ($results) {
1805                     $results = NZoperatorAND($biblionumbers,$results);
1806                 }
1807                 else {
1808                     warn "NEW RES for $_ = $biblionumbers" if $DEBUG;
1809                     $results = $biblionumbers;
1810                 }
1811             }
1812         }
1813         warn "return : $results for LEAF : $string" if $DEBUG;
1814         return $results;
1815     }
1816     warn "---------\nLeave NZanalyse\n---------" if $DEBUG;
1817 }
1818
1819 sub NZoperatorAND{
1820     my ($rightresult, $leftresult)=@_;
1821     
1822     my @leftresult = split /;/, $leftresult;
1823     warn " @leftresult / $rightresult \n" if $DEBUG;
1824     
1825     #             my @rightresult = split /;/,$leftresult;
1826     my $finalresult;
1827
1828 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1829 # the result is stored twice, to have the same weight for AND than OR.
1830 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1831 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1832     foreach (@leftresult) {
1833         my $value = $_;
1834         my $countvalue;
1835         ( $value, $countvalue ) = ( $1, $2 ) if ($value=~/(.*)-(\d+)$/);
1836         if ( $rightresult =~ /\Q$value\E-(\d+);/ ) {
1837             $countvalue = ( $1 > $countvalue ? $countvalue : $1 );
1838             $finalresult .=
1839                 "$value-$countvalue;$value-$countvalue;";
1840         }
1841     }
1842     warn "NZAND DONE : $finalresult \n" if $DEBUG;
1843     return $finalresult;
1844 }
1845       
1846 sub NZoperatorOR{
1847     my ($rightresult, $leftresult)=@_;
1848     return $rightresult.$leftresult;
1849 }
1850
1851 sub NZoperatorNOT{
1852     my ($leftresult, $rightresult)=@_;
1853     
1854     my @leftresult = split /;/, $leftresult;
1855
1856     #             my @rightresult = split /;/,$leftresult;
1857     my $finalresult;
1858     foreach (@leftresult) {
1859         my $value=$_;
1860         $value=$1 if $value=~m/(.*)-\d+$/;
1861         unless ($rightresult =~ "$value-") {
1862             $finalresult .= "$_;";
1863         }
1864     }
1865     return $finalresult;
1866 }
1867
1868 =head2 NZorder
1869
1870   $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
1871   
1872   TODO :: Description
1873
1874 =cut
1875
1876 sub NZorder {
1877     my ( $biblionumbers, $ordering, $results_per_page, $offset ) = @_;
1878     warn "biblionumbers = $biblionumbers and ordering = $ordering\n" if $DEBUG;
1879
1880     # order title asc by default
1881     #     $ordering = '1=36 <i' unless $ordering;
1882     $results_per_page = 20 unless $results_per_page;
1883     $offset           = 0  unless $offset;
1884     my $dbh = C4::Context->dbh;
1885
1886     #
1887     # order by POPULARITY
1888     #
1889     if ( $ordering =~ /popularity/ ) {
1890         my %result;
1891         my %popularity;
1892
1893         # popularity is not in MARC record, it's builded from a specific query
1894         my $sth =
1895           $dbh->prepare("select sum(issues) from items where biblionumber=?");
1896         foreach ( split /;/, $biblionumbers ) {
1897             my ( $biblionumber, $title ) = split /,/, $_;
1898             $result{$biblionumber} = GetMarcBiblio($biblionumber);
1899             $sth->execute($biblionumber);
1900             my $popularity = $sth->fetchrow || 0;
1901
1902 # hint : the key is popularity.title because we can have
1903 # many results with the same popularity. In this case, sub-ordering is done by title
1904 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
1905 # (un-frequent, I agree, but we won't forget anything that way ;-)
1906             $popularity{ sprintf( "%10d", $popularity ) . $title
1907                   . $biblionumber } = $biblionumber;
1908         }
1909
1910     # sort the hash and return the same structure as GetRecords (Zebra querying)
1911         my $result_hash;
1912         my $numbers = 0;
1913         if ( $ordering eq 'popularity_dsc' ) {    # sort popularity DESC
1914             foreach my $key ( sort { $b cmp $a } ( keys %popularity ) ) {
1915                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1916                   $result{ $popularity{$key} }->as_usmarc();
1917             }
1918         }
1919         else {                                    # sort popularity ASC
1920             foreach my $key ( sort ( keys %popularity ) ) {
1921                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1922                   $result{ $popularity{$key} }->as_usmarc();
1923             }
1924         }
1925         my $finalresult = ();
1926         $result_hash->{'hits'}         = $numbers;
1927         $finalresult->{'biblioserver'} = $result_hash;
1928         return $finalresult;
1929
1930         #
1931         # ORDER BY author
1932         #
1933     }
1934     elsif ( $ordering =~ /author/ ) {
1935         my %result;
1936         foreach ( split /;/, $biblionumbers ) {
1937             my ( $biblionumber, $title ) = split /,/, $_;
1938             my $record = GetMarcBiblio($biblionumber);
1939             my $author;
1940             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
1941                 $author = $record->subfield( '200', 'f' );
1942                 $author = $record->subfield( '700', 'a' ) unless $author;
1943             }
1944             else {
1945                 $author = $record->subfield( '100', 'a' );
1946             }
1947
1948 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1949 # and we don't want to get only 1 result for each of them !!!
1950             $result{ $author . $biblionumber } = $record;
1951         }
1952
1953     # sort the hash and return the same structure as GetRecords (Zebra querying)
1954         my $result_hash;
1955         my $numbers = 0;
1956         if ( $ordering eq 'author_za' ) {    # sort by author desc
1957             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
1958                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1959                   $result{$key}->as_usmarc();
1960             }
1961         }
1962         else {                               # sort by author ASC
1963             foreach my $key ( sort ( keys %result ) ) {
1964                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1965                   $result{$key}->as_usmarc();
1966             }
1967         }
1968         my $finalresult = ();
1969         $result_hash->{'hits'}         = $numbers;
1970         $finalresult->{'biblioserver'} = $result_hash;
1971         return $finalresult;
1972
1973         #
1974         # ORDER BY callnumber
1975         #
1976     }
1977     elsif ( $ordering =~ /callnumber/ ) {
1978         my %result;
1979         foreach ( split /;/, $biblionumbers ) {
1980             my ( $biblionumber, $title ) = split /,/, $_;
1981             my $record = GetMarcBiblio($biblionumber);
1982             my $callnumber;
1983             my $frameworkcode = GetFrameworkCode($biblionumber);
1984             my ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField(  'items.itemcallnumber', $frameworkcode);
1985                ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField('biblioitems.callnumber', $frameworkcode)
1986                 unless $callnumber_tag;
1987             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
1988                 $callnumber = $record->subfield( '200', 'f' );
1989             } else {
1990                 $callnumber = $record->subfield( '100', 'a' );
1991             }
1992
1993 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1994 # and we don't want to get only 1 result for each of them !!!
1995             $result{ $callnumber . $biblionumber } = $record;
1996         }
1997
1998     # sort the hash and return the same structure as GetRecords (Zebra querying)
1999         my $result_hash;
2000         my $numbers = 0;
2001         if ( $ordering eq 'call_number_dsc' ) {    # sort by title desc
2002             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2003                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2004                   $result{$key}->as_usmarc();
2005             }
2006         }
2007         else {                                     # sort by title ASC
2008             foreach my $key ( sort { $a cmp $b } ( keys %result ) ) {
2009                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2010                   $result{$key}->as_usmarc();
2011             }
2012         }
2013         my $finalresult = ();
2014         $result_hash->{'hits'}         = $numbers;
2015         $finalresult->{'biblioserver'} = $result_hash;
2016         return $finalresult;
2017     }
2018     elsif ( $ordering =~ /pubdate/ ) {             #pub year
2019         my %result;
2020         foreach ( split /;/, $biblionumbers ) {
2021             my ( $biblionumber, $title ) = split /,/, $_;
2022             my $record = GetMarcBiblio($biblionumber);
2023             my ( $publicationyear_tag, $publicationyear_subfield ) =
2024               GetMarcFromKohaField( 'biblioitems.publicationyear', '' );
2025             my $publicationyear =
2026               $record->subfield( $publicationyear_tag,
2027                 $publicationyear_subfield );
2028
2029 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2030 # and we don't want to get only 1 result for each of them !!!
2031             $result{ $publicationyear . $biblionumber } = $record;
2032         }
2033
2034     # sort the hash and return the same structure as GetRecords (Zebra querying)
2035         my $result_hash;
2036         my $numbers = 0;
2037         if ( $ordering eq 'pubdate_dsc' ) {    # sort by pubyear desc
2038             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2039                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2040                   $result{$key}->as_usmarc();
2041             }
2042         }
2043         else {                                 # sort by pub year ASC
2044             foreach my $key ( sort ( keys %result ) ) {
2045                 $result_hash->{'RECORDS'}[ $numbers++ ] =
2046                   $result{$key}->as_usmarc();
2047             }
2048         }
2049         my $finalresult = ();
2050         $result_hash->{'hits'}         = $numbers;
2051         $finalresult->{'biblioserver'} = $result_hash;
2052         return $finalresult;
2053
2054         #
2055         # ORDER BY title
2056         #
2057     }
2058     elsif ( $ordering =~ /title/ ) {
2059
2060 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
2061         my %result;
2062         foreach ( split /;/, $biblionumbers ) {
2063             my ( $biblionumber, $title ) = split /,/, $_;
2064
2065 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2066 # and we don't want to get only 1 result for each of them !!!
2067 # hint & speed improvement : we can order without reading the record
2068 # so order, and read records only for the requested page !
2069             $result{ $title . $biblionumber } = $biblionumber;
2070         }
2071
2072     # sort the hash and return the same structure as GetRecords (Zebra querying)
2073         my $result_hash;
2074         my $numbers = 0;
2075         if ( $ordering eq 'title_az' ) {    # sort by title desc
2076             foreach my $key ( sort ( keys %result ) ) {
2077                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2078             }
2079         }
2080         else {                              # sort by title ASC
2081             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2082                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2083             }
2084         }
2085
2086         # limit the $results_per_page to result size if it's more
2087         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2088
2089         # for the requested page, replace biblionumber by the complete record
2090         # speed improvement : avoid reading too much things
2091         for (
2092             my $counter = $offset ;
2093             $counter <= $offset + $results_per_page ;
2094             $counter++
2095           )
2096         {
2097             $result_hash->{'RECORDS'}[$counter] =
2098               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc;
2099         }
2100         my $finalresult = ();
2101         $result_hash->{'hits'}         = $numbers;
2102         $finalresult->{'biblioserver'} = $result_hash;
2103         return $finalresult;
2104     }
2105     else {
2106
2107 #
2108 # order by ranking
2109 #
2110 # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
2111         my %result;
2112         my %count_ranking;
2113         foreach ( split /;/, $biblionumbers ) {
2114             my ( $biblionumber, $title ) = split /,/, $_;
2115             $title =~ /(.*)-(\d)/;
2116
2117             # get weight
2118             my $ranking = $2;
2119
2120 # note that we + the ranking because ranking is calculated on weight of EACH term requested.
2121 # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
2122 # biblio N has ranking = 6
2123             $count_ranking{$biblionumber} += $ranking;
2124         }
2125
2126 # build the result by "inverting" the count_ranking hash
2127 # hing : as usual, we don't order by ranking only, to avoid having only 1 result for each rank. We build an hash on concat(ranking,biblionumber) instead
2128 #         warn "counting";
2129         foreach ( keys %count_ranking ) {
2130             $result{ sprintf( "%10d", $count_ranking{$_} ) . '-' . $_ } = $_;
2131         }
2132
2133     # sort the hash and return the same structure as GetRecords (Zebra querying)
2134         my $result_hash;
2135         my $numbers = 0;
2136         foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2137             $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2138         }
2139
2140         # limit the $results_per_page to result size if it's more
2141         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2142
2143         # for the requested page, replace biblionumber by the complete record
2144         # speed improvement : avoid reading too much things
2145         for (
2146             my $counter = $offset ;
2147             $counter <= $offset + $results_per_page ;
2148             $counter++
2149           )
2150         {
2151             $result_hash->{'RECORDS'}[$counter] =
2152               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc
2153               if $result_hash->{'RECORDS'}[$counter];
2154         }
2155         my $finalresult = ();
2156         $result_hash->{'hits'}         = $numbers;
2157         $finalresult->{'biblioserver'} = $result_hash;
2158         return $finalresult;
2159     }
2160 }
2161
2162 =head2 enabled_staff_search_views
2163
2164 %hash = enabled_staff_search_views()
2165
2166 This function returns a hash that contains three flags obtained from the system
2167 preferences, used to determine whether a particular staff search results view
2168 is enabled.
2169
2170 =over 2
2171
2172 =item C<Output arg:>
2173
2174     * $hash{can_view_MARC} is true only if the MARC view is enabled
2175     * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2176     * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2177
2178 =item C<usage in the script:>
2179
2180 =back
2181
2182 $template->param ( C4::Search::enabled_staff_search_views );
2183
2184 =cut
2185
2186 sub enabled_staff_search_views
2187 {
2188         return (
2189                 can_view_MARC                   => C4::Context->preference('viewMARC'),                 # 1 if the staff search allows the MARC view
2190                 can_view_ISBD                   => C4::Context->preference('viewISBD'),                 # 1 if the staff search allows the ISBD view
2191                 can_view_labeledMARC    => C4::Context->preference('viewLabeledMARC'),  # 1 if the staff search allows the Labeled MARC view
2192         );
2193 }
2194
2195 sub AddSearchHistory{
2196         my ($borrowernumber,$session,$query_desc,$query_cgi, $total)=@_;
2197     my $dbh = C4::Context->dbh;
2198
2199     # Add the request the user just made
2200     my $sql = "INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, total, time) VALUES(?, ?, ?, ?, ?, NOW())";
2201     my $sth   = $dbh->prepare($sql);
2202     $sth->execute($borrowernumber, $session, $query_desc, $query_cgi, $total);
2203         return $dbh->last_insert_id(undef, 'search_history', undef,undef,undef);
2204 }
2205
2206 sub GetSearchHistory{
2207         my ($borrowernumber,$session)=@_;
2208     my $dbh = C4::Context->dbh;
2209
2210     # Add the request the user just made
2211     my $query = "SELECT FROM search_history WHERE (userid=? OR sessionid=?)";
2212     my $sth   = $dbh->prepare($query);
2213         $sth->execute($borrowernumber, $session);
2214     return  $sth->fetchall_hashref({});
2215 }
2216
2217 =head2 z3950_search_args
2218
2219 $arrayref = z3950_search_args($matchpoints)
2220
2221 This function returns an array reference that contains the search parameters to be
2222 passed to the Z39.50 search script (z3950_search.pl). The array elements
2223 are hash refs whose keys are name, value and encvalue, and whose values are the
2224 name of a search parameter, the value of that search parameter and the URL encoded
2225 value of that parameter.
2226
2227 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2228
2229 The search parameter values are obtained from the bibliographic record whose
2230 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2231
2232 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2233 a general purpose search argument. In this case, the returned array contains only
2234 entry: the key is 'title' and the value and encvalue are derived from $matchpoints.
2235
2236 If a search parameter value is undefined or empty, it is not included in the returned
2237 array.
2238
2239 The returned array reference may be passed directly to the template parameters.
2240
2241 =over 2
2242
2243 =item C<Output arg:>
2244
2245     * $array containing hash refs as described above
2246
2247 =item C<usage in the script:>
2248
2249 =back
2250
2251 $data = Biblio::GetBiblioData($bibno);
2252 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2253
2254 *OR*
2255
2256 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2257
2258 =cut
2259
2260 sub z3950_search_args {
2261     my $bibrec = shift;
2262     $bibrec = { title => $bibrec } if !ref $bibrec;
2263     my $array = [];
2264     for my $field (qw/ lccn isbn issn title author dewey subject /)
2265     {
2266         my $encvalue = URI::Escape::uri_escape_utf8($bibrec->{$field});
2267         push @$array, { name=>$field, value=>$bibrec->{$field}, encvalue=>$encvalue } if defined $bibrec->{$field};
2268     }
2269     return $array;
2270 }
2271
2272 =head2 BiblioAddAuthorities
2273
2274 ( $countlinked, $countcreated ) = BiblioAddAuthorities($record, $frameworkcode);
2275
2276 this function finds the authorities linked to the biblio
2277     * search in the authority DB for the same authid (in $9 of the biblio)
2278     * search in the authority DB for the same 001 (in $3 of the biblio in UNIMARC)
2279     * search in the authority DB for the same values (exactly) (in all subfields of the biblio)
2280 OR adds a new authority record
2281
2282 =over 2
2283
2284 =item C<input arg:>
2285
2286     * $record is the MARC record in question (marc blob)
2287     * $frameworkcode is the bibliographic framework to use (if it is "" it uses the default framework)
2288
2289 =item C<Output arg:>
2290
2291     * $countlinked is the number of authorities records that are linked to this authority
2292     * $countcreated
2293
2294 =item C<BUGS>
2295     * I had to add this to Search.pm (instead of the logical Biblio.pm) because of a circular dependency (this sub uses SimpleSearch, and Search.pm uses Biblio.pm)
2296 =back
2297
2298 =cut
2299
2300
2301 sub BiblioAddAuthorities{
2302   my ( $record, $frameworkcode ) = @_;
2303   my $dbh=C4::Context->dbh;
2304   my $query=$dbh->prepare(qq|
2305 SELECT authtypecode,tagfield
2306 FROM marc_subfield_structure 
2307 WHERE frameworkcode=? 
2308 AND (authtypecode IS NOT NULL AND authtypecode<>\"\")|);
2309 # SELECT authtypecode,tagfield
2310 # FROM marc_subfield_structure 
2311 # WHERE frameworkcode=? 
2312 # AND (authtypecode IS NOT NULL OR authtypecode<>\"\")|);
2313   $query->execute($frameworkcode);
2314   my ($countcreated,$countlinked);
2315   while (my $data=$query->fetchrow_hashref){
2316     foreach my $field ($record->field($data->{tagfield})){
2317       next if ($field->subfield('3')||$field->subfield('9'));
2318       # No authorities id in the tag.
2319       # Search if there is any authorities to link to.
2320       my $query='at='.$data->{authtypecode}.' ';
2321       map {$query.= ' and he,ext="'.$_->[1].'"' if ($_->[0]=~/[A-z]/)}  $field->subfields();
2322       my ($error, $results, $total_hits)=SimpleSearch( $query, undef, undef, [ "authorityserver" ] );
2323     # there is only 1 result 
2324           if ( $error ) {
2325         warn "BIBLIOADDSAUTHORITIES: $error";
2326             return (0,0) ;
2327           }
2328       if ($results && scalar(@$results)==1) {
2329         my $marcrecord = MARC::File::USMARC::decode($results->[0]);
2330         $field->add_subfields('9'=>$marcrecord->field('001')->data);
2331         $countlinked++;
2332       } elsif (scalar(@$results)>1) {
2333    #More than One result 
2334    #This can comes out of a lack of a subfield.
2335 #         my $marcrecord = MARC::File::USMARC::decode($results->[0]);
2336 #         $record->field($data->{tagfield})->add_subfields('9'=>$marcrecord->field('001')->data);
2337   $countlinked++;
2338       } else {
2339   #There are no results, build authority record, add it to Authorities, get authid and add it to 9
2340   ###NOTICE : This is only valid if a subfield is linked to one and only one authtypecode     
2341   ###NOTICE : This can be a problem. We should also look into other types and rejected forms.
2342          my $authtypedata=C4::AuthoritiesMarc->GetAuthType($data->{authtypecode});
2343          next unless $authtypedata;
2344          my $marcrecordauth=MARC::Record->new();
2345          my $authfield=MARC::Field->new($authtypedata->{auth_tag_to_report},'','',"a"=>"".$field->subfield('a'));
2346          map { $authfield->add_subfields($_->[0]=>$_->[1]) if ($_->[0]=~/[A-z]/ && $_->[0] ne "a" )}  $field->subfields();
2347          $marcrecordauth->insert_fields_ordered($authfield);
2348
2349          # bug 2317: ensure new authority knows it's using UTF-8; currently
2350          # only need to do this for MARC21, as MARC::Record->as_xml_record() handles
2351          # automatically for UNIMARC (by not transcoding)
2352          # FIXME: AddAuthority() instead should simply explicitly require that the MARC::Record
2353          # use UTF-8, but as of 2008-08-05, did not want to introduce that kind
2354          # of change to a core API just before the 3.0 release.
2355          if (C4::Context->preference('marcflavour') eq 'MARC21') {
2356             SetMarcUnicodeFlag($marcrecordauth, 'MARC21');
2357          }
2358
2359 #          warn "AUTH RECORD ADDED : ".$marcrecordauth->as_formatted;
2360
2361          my $authid=AddAuthority($marcrecordauth,'',$data->{authtypecode});
2362          $countcreated++;
2363          $field->add_subfields('9'=>$authid);
2364       }
2365     }
2366   }
2367   return ($countlinked,$countcreated);
2368 }
2369
2370 =head2 GetDistinctValues($field);
2371
2372 C<$field> is a reference to the fields array
2373
2374 =cut
2375
2376 sub GetDistinctValues {
2377     my ($fieldname,$string)=@_;
2378     # returns a reference to a hash of references to branches...
2379     if ($fieldname=~/\./){
2380                         my ($table,$column)=split /\./, $fieldname;
2381                         my $dbh = C4::Context->dbh;
2382                         warn "select DISTINCT($column) as value, count(*) as cnt from $table group by lib order by $column ";
2383                         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 ");
2384                         $sth->execute;
2385                         my $elements=$sth->fetchall_arrayref({});
2386                         return $elements;
2387    }
2388    else {
2389                 $string||= qq("");
2390                 my @servers=qw<biblioserver authorityserver>;
2391                 my (@zconns,@results);
2392         for ( my $i = 0 ; $i < @servers ; $i++ ) {
2393                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
2394                         $results[$i] =
2395                       $zconns[$i]->scan(
2396                         ZOOM::Query::CCL2RPN->new( qq"$fieldname $string", $zconns[$i])
2397                       );
2398                 }
2399                 # The big moment: asynchronously retrieve results from all servers
2400                 my @elements;
2401                 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
2402                         my $ev = $zconns[ $i - 1 ]->last_event();
2403                         if ( $ev == ZOOM::Event::ZEND ) {
2404                                 next unless $results[ $i - 1 ];
2405                                 my $size = $results[ $i - 1 ]->size();
2406                                 if ( $size > 0 ) {
2407                       for (my $j=0;$j<$size;$j++){
2408                                                 my %hashscan;
2409                                                 @hashscan{qw(value cnt)}=$results[ $i - 1 ]->display_term($j);
2410                                                 push @elements, \%hashscan;
2411                                           }
2412                                 }
2413                         }
2414                 }
2415                 return \@elements;
2416    }
2417 }
2418
2419
2420 END { }    # module clean-up code here (global destructor)
2421
2422 1;
2423 __END__
2424
2425 =head1 AUTHOR
2426
2427 Koha Developement team <info@koha.org>
2428
2429 =cut