Bug Fixing MT 2095 : Add_excepti.al Search broken on QueryStemming
[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     if ( $query =~ /(\(|\))/ ) {
856         return (
857             undef,              $query, $simple_query, $query_cgi,
858             $query,             $limit, $limit_cgi,    $limit_desc,
859             $stopwords_removed, 'ccl'
860         );
861     }
862
863 # Form-based queries are non-nested and fixed depth, so we can easily modify the incoming
864 # query operands and indexes and add stemming, truncation, field weighting, etc.
865 # Once we do so, we'll end up with a value in $query, just like if we had an
866 # incoming $query from the user
867     else {
868         $query = ""
869           ; # clear it out so we can populate properly with field-weighted, stemmed, etc. query
870         my $previous_operand
871           ;    # a flag used to keep track if there was a previous query
872                # if there was, we can apply the current operator
873                # for every operand
874         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
875
876             # COMBINE OPERANDS, INDEXES AND OPERATORS
877             if ( $operands[$i] ) {
878
879               # A flag to determine whether or not to add the index to the query
880                 my $indexes_set;
881
882 # If the user is sophisticated enough to specify an index, turn off field weighting, stemming, and stopword handling
883                 if ( $operands[$i] =~ /(:|=)/ || $scan ) {
884                     $weight_fields    = 0;
885                     $stemming         = 0;
886                     $remove_stopwords = 0;
887                 }
888                 my $operand = $operands[$i];
889                 my $index   = $indexes[$i];
890
891                 # Add index-specific attributes
892                 # Date of Publication
893                 if ( $index eq 'yr' ) {
894                     $index .= ",st-numeric";
895                     $indexes_set++;
896                                         $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
897                 }
898
899                 # Date of Acquisition
900                 elsif ( $index eq 'acqdate' ) {
901                     $index .= ",st-date-normalized";
902                     $indexes_set++;
903                                         $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
904                 }
905                 # ISBN,ISSN,Standard Number, don't need special treatment
906                 elsif ( $index eq 'nb' || $index eq 'ns' ) {
907                     $indexes_set++;
908                     (   
909                         $stemming,      $auto_truncation,
910                         $weight_fields, $fuzzy_enabled,
911                         $remove_stopwords
912                     ) = ( 0, 0, 0, 0, 0 );
913
914                 }
915                 # Set default structure attribute (word list)
916                 my $struct_attr;
917                 unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl)/ ) {
918                     $struct_attr = ",wrdl";
919                 }
920
921                 # Some helpful index variants
922                 my $index_plus       = $index . $struct_attr . ":" if $index;
923                 my $index_plus_comma = $index . $struct_attr . "," if $index;
924
925                 # Remove Stopwords
926                 if ($remove_stopwords) {
927                     ( $operand, $stopwords_removed ) =
928                       _remove_stopwords( $operand, $index );
929                     warn "OPERAND w/out STOPWORDS: >$operand<" if $DEBUG;
930                     warn "REMOVED STOPWORDS: @$stopwords_removed"
931                       if ( $stopwords_removed && $DEBUG );
932                 }
933
934                 if ($auto_truncation){
935                                         $operand=~join(" ",map{ "$_*" }split (/\s+/,$operand));
936                                 }
937
938                 # Detect Truncation
939                 my $truncated_operand;
940                 my( $nontruncated, $righttruncated, $lefttruncated,
941                     $rightlefttruncated, $regexpr
942                 ) = _detect_truncation( $operand, $index );
943                 warn
944 "TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<"
945                   if $DEBUG;
946
947                 # Apply Truncation
948                 if (
949                     scalar(@$righttruncated) + scalar(@$lefttruncated) +
950                     scalar(@$rightlefttruncated) > 0 )
951                 {
952
953                # Don't field weight or add the index to the query, we do it here
954                     $indexes_set = 1;
955                     undef $weight_fields;
956                     my $previous_truncation_operand;
957                     if (scalar @$nontruncated) {
958                         $truncated_operand .= "$index_plus @$nontruncated ";
959                         $previous_truncation_operand = 1;
960                     }
961                     if (scalar @$righttruncated) {
962                         $truncated_operand .= "and " if $previous_truncation_operand;
963                         $truncated_operand .= $index_plus_comma . "rtrn:@$righttruncated ";
964                         $previous_truncation_operand = 1;
965                     }
966                     if (scalar @$lefttruncated) {
967                         $truncated_operand .= "and " if $previous_truncation_operand;
968                         $truncated_operand .= $index_plus_comma . "ltrn:@$lefttruncated ";
969                         $previous_truncation_operand = 1;
970                     }
971                     if (scalar @$rightlefttruncated) {
972                         $truncated_operand .= "and " if $previous_truncation_operand;
973                         $truncated_operand .= $index_plus_comma . "rltrn:@$rightlefttruncated ";
974                         $previous_truncation_operand = 1;
975                     }
976                 }
977                 $operand = $truncated_operand if $truncated_operand;
978                 warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
979
980                 # Handle Stemming
981                 my $stemmed_operand;
982                 $stemmed_operand = _build_stemmed_operand($operand, $lang)
983                                                                                 if $stemming;
984
985                 warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
986
987                 # Handle Field Weighting
988                 my $weighted_operand;
989                 if ($weight_fields) {
990                     $weighted_operand = _build_weighted_query( $operand, $stemmed_operand, $index );
991                     $operand = $weighted_operand;
992                     $indexes_set = 1;
993                 }
994
995                 warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
996
997                 # If there's a previous operand, we need to add an operator
998                 if ($previous_operand) {
999
1000                     # User-specified operator
1001                     if ( $operators[ $i - 1 ] ) {
1002                         $query     .= " $operators[$i-1] ";
1003                         $query     .= " $index_plus " unless $indexes_set;
1004                         $query     .= " $operand";
1005                         $query_cgi .= "&op=$operators[$i-1]";
1006                         $query_cgi .= "&idx=$index" if $index;
1007                         $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1008                         $query_desc .=
1009                           " $operators[$i-1] $index_plus $operands[$i]";
1010                     }
1011
1012                     # Default operator is and
1013                     else {
1014                         $query      .= " and ";
1015                         $query      .= "$index_plus " unless $indexes_set;
1016                         $query      .= "$operand";
1017                         $query_cgi  .= "&op=and&idx=$index" if $index;
1018                         $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
1019                         $query_desc .= " and $index_plus $operands[$i]";
1020                     }
1021                 }
1022
1023                 # There isn't a pervious operand, don't need an operator
1024                 else {
1025
1026                     # Field-weighted queries already have indexes set
1027                     $query .= " $index_plus " unless $indexes_set;
1028                     $query .= $operand;
1029                     $query_desc .= " $index_plus $operands[$i]";
1030                     $query_cgi  .= "&idx=$index" if $index;
1031                     $query_cgi  .= "&q=$operands[$i]" if $operands[$i];
1032                     $previous_operand = 1;
1033                 }
1034             }    #/if $operands
1035         }    # /for
1036     }
1037     warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1038
1039     # add limits
1040     my $group_OR_limits;
1041     my $availability_limit;
1042     foreach my $this_limit (@limits) {
1043         if ( $this_limit =~ /available/ ) {
1044
1045 # 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1046 # In English:
1047 # all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1048             $availability_limit .=
1049 "( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and (lost,st-numeric=0) )"; #or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
1050             $limit_cgi  .= "&limit=available";
1051             $limit_desc .= "";
1052         }
1053
1054         # group_OR_limits, prefixed by mc-
1055         # OR every member of the group
1056         elsif ( $this_limit =~ /mc/ ) {
1057             $group_OR_limits .= " or " if $group_OR_limits;
1058             $limit_desc      .= " or " if $group_OR_limits;
1059             $group_OR_limits .= "$this_limit";
1060             $limit_cgi       .= "&limit=$this_limit";
1061             $limit_desc      .= " $this_limit";
1062         }
1063
1064         # Regular old limits
1065         else {
1066             $limit .= " and " if $limit || $query;
1067             $limit      .= "$this_limit";
1068             $limit_cgi  .= "&limit=$this_limit";
1069             if ($this_limit =~ /^branch:(.+)/) {
1070                 my $branchcode = $1;
1071                 my $branchname = GetBranchName($branchcode);
1072                 if (defined $branchname) {
1073                     $limit_desc .= " branch:$branchname";
1074                 } else {
1075                     $limit_desc .= " $this_limit";
1076                 }
1077             } else {
1078                 $limit_desc .= " $this_limit";
1079             }
1080         }
1081     }
1082     if ($group_OR_limits) {
1083         $limit .= " and " if ( $query || $limit );
1084         $limit .= "($group_OR_limits)";
1085     }
1086     if ($availability_limit) {
1087         $limit .= " and " if ( $query || $limit );
1088         $limit .= "($availability_limit)";
1089     }
1090
1091     # Normalize the query and limit strings
1092     $query =~ s/:/=/g;
1093     $limit =~ s/:/=/g;
1094     for ( $query, $query_desc, $limit, $limit_desc ) {
1095         s/  / /g;    # remove extra spaces
1096         s/^ //g;     # remove any beginning spaces
1097         s/ $//g;     # remove any ending spaces
1098         s/==/=/g;    # remove double == from query
1099     }
1100     $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1101
1102     for ($query_cgi,$simple_query) {
1103         s/"//g;
1104     }
1105     # append the limit to the query
1106     $query .= " " . $limit;
1107
1108     # Warnings if DEBUG
1109     if ($DEBUG) {
1110         warn "QUERY:" . $query;
1111         warn "QUERY CGI:" . $query_cgi;
1112         warn "QUERY DESC:" . $query_desc;
1113         warn "LIMIT:" . $limit;
1114         warn "LIMIT CGI:" . $limit_cgi;
1115         warn "LIMIT DESC:" . $limit_desc;
1116         warn "---------\nLeave buildQuery\n---------";
1117     }
1118     return (
1119         undef,              $query, $simple_query, $query_cgi,
1120         $query_desc,        $limit, $limit_cgi,    $limit_desc,
1121         $stopwords_removed, $query_type
1122     );
1123 }
1124
1125 =head2 searchResults
1126
1127 Format results in a form suitable for passing to the template
1128
1129 =cut
1130
1131 # IMO this subroutine is pretty messy still -- it's responsible for
1132 # building the HTML output for the template
1133 sub searchResults {
1134     my ( $searchdesc, $hits, $results_per_page, $offset, $scan, @marcresults ) = @_;
1135     my $dbh = C4::Context->dbh;
1136     my @newresults;
1137
1138     #Build branchnames hash
1139     #find branchname
1140     #get branch information.....
1141     my %branches;
1142     my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
1143     $bsth->execute();
1144     while ( my $bdata = $bsth->fetchrow_hashref ) {
1145         $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
1146     }
1147 # FIXME - We build an authorised values hash here, using the default framework
1148 # though it is possible to have different authvals for different fws.
1149
1150     my $shelflocations =GetKohaAuthorisedValues('items.location','');
1151
1152     # get notforloan authorised value list (see $shelflocations  FIXME)
1153     my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
1154
1155     #Build itemtype hash
1156     #find itemtype & itemtype image
1157     my %itemtypes;
1158     $bsth =
1159       $dbh->prepare(
1160         "SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes"
1161       );
1162     $bsth->execute();
1163     while ( my $bdata = $bsth->fetchrow_hashref ) {
1164                 foreach (qw(description imageurl summary notforloan)) {
1165                 $itemtypes{ $bdata->{'itemtype'} }->{$_} = $bdata->{$_};
1166                 }
1167     }
1168
1169     #search item field code
1170     my $sth =
1171       $dbh->prepare(
1172 "SELECT tagfield FROM marc_subfield_structure WHERE kohafield LIKE 'items.itemnumber'"
1173       );
1174     $sth->execute;
1175     my ($itemtag) = $sth->fetchrow;
1176
1177     ## find column names of items related to MARC
1178     my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
1179     $sth2->execute;
1180     my %subfieldstosearch;
1181     while ( ( my $column ) = $sth2->fetchrow ) {
1182         my ( $tagfield, $tagsubfield ) =
1183           &GetMarcFromKohaField( "items." . $column, "" );
1184         $subfieldstosearch{$column} = $tagsubfield;
1185     }
1186
1187     # handle which records to actually retrieve
1188     my $times;
1189     if ( $hits && $offset + $results_per_page <= $hits ) {
1190         $times = $offset + $results_per_page;
1191     }
1192     else {
1193         $times = $hits;  # FIXME: if $hits is undefined, why do we want to equal it?
1194     }
1195
1196         my $marcflavour = C4::Context->preference("marcflavour");
1197     # We get the biblionumber position in MARC 
1198     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
1199     my $fw;
1200     
1201     # loop through all of the records we've retrieved
1202     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1203         my $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
1204         
1205         if ($bibliotag<10){
1206             $fw = GetFrameworkCode($marcrecord->field($bibliotag)->data);
1207         }else{
1208             $fw = GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
1209         }
1210         
1211         my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, $fw );
1212         $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
1213         $oldbiblio->{result_number} = $i + 1;
1214
1215         # add imageurl to itemtype if there is one
1216         $oldbiblio->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1217
1218         $oldbiblio->{'authorised_value_images'}  = C4::Items::get_authorised_value_images( C4::Biblio::get_biblio_authorised_values( $oldbiblio->{'biblionumber'}, $marcrecord ) );
1219                 $oldbiblio->{normalized_upc}  = GetNormalizedUPC(       $marcrecord,$marcflavour);
1220                 $oldbiblio->{normalized_ean}  = GetNormalizedEAN(       $marcrecord,$marcflavour);
1221                 $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
1222                 $oldbiblio->{normalized_isbn} = GetNormalizedISBN(undef,$marcrecord,$marcflavour);
1223                 $oldbiblio->{content_identifier_exists} = 1 if ($oldbiblio->{normalized_isbn} or $oldbiblio->{normalized_oclc} or $oldbiblio->{normalized_ean} or $oldbiblio->{normalized_upc});
1224
1225                 # edition information, if any
1226         $oldbiblio->{edition} = $oldbiblio->{editionstatement};
1227                 $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1228  # Build summary if there is one (the summary is defined in the itemtypes table)
1229  # FIXME: is this used anywhere, I think it can be commented out? -- JF
1230         if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1231             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1232             my @fields  = $marcrecord->fields();
1233             
1234             my $newsummary;
1235             foreach my $line ( "$summary\n" =~ /(.*)\n/g ){
1236                 my $tags = {};
1237                 foreach my $tag ( $line =~ /\[(\d{3}[\w|\d])\]/ ) {
1238                     $tag =~ /(.{3})(.)/;
1239                     if($marcrecord->field($1)){
1240                         my @abc = $marcrecord->field($1)->subfield($2);
1241                         $tags->{$tag} = $#abc + 1 ;
1242                     }
1243                 }
1244                 
1245                 # We catch how many times to repeat this line
1246                 my $max = 0;
1247                 foreach my $tag (keys(%$tags)){
1248                     $max = $tags->{$tag} if($tags->{$tag} > $max);
1249                  }
1250                 
1251                 # we replace, and repeat each line
1252                 for (my $i = 0 ; $i < $max ; $i++){
1253                     my $newline = $line;
1254
1255                     foreach my $tag ( $newline =~ /\[(\d{3}[\w|\d])\]/g ) {
1256                         $tag =~ /(.{3})(.)/;
1257                         
1258                         if($marcrecord->field($1)){
1259                             my @repl = $marcrecord->field($1)->subfield($2);
1260                             my $subfieldvalue = $repl[$i];
1261                             
1262                             if (! utf8::is_utf8($subfieldvalue)) {
1263                                 utf8::decode($subfieldvalue);
1264                             }
1265  
1266                              $newline =~ s/\[$tag\]/$subfieldvalue/g;
1267                         }
1268                     }
1269                     $newsummary .= "$newline\n";
1270                 }
1271             }
1272
1273             $newsummary =~ s/\[(.*?)]//g;
1274             $newsummary =~ s/\n/<br\/>/g;
1275             $oldbiblio->{summary} = $newsummary;
1276         }
1277
1278         # Pull out the items fields
1279         my @fields = $marcrecord->field($itemtag);
1280
1281         # Setting item statuses for display
1282         my @available_items_loop;
1283         my @onloan_items_loop;
1284         my @other_items_loop;
1285
1286         my $available_items;
1287         my $onloan_items;
1288         my $other_items;
1289
1290         my $ordered_count         = 0;
1291         my $available_count       = 0;
1292         my $onloan_count          = 0;
1293         my $longoverdue_count     = 0;
1294         my $other_count           = 0;
1295         my $wthdrawn_count        = 0;
1296         my $itemlost_count        = 0;
1297         my $itembinding_count     = 0;
1298         my $itemdamaged_count     = 0;
1299         my $item_in_transit_count = 0;
1300         my $can_place_holds       = 0;
1301         my $items_count           = scalar(@fields);
1302         my $maxitems =
1303           ( C4::Context->preference('maxItemsinSearchResults') )
1304           ? C4::Context->preference('maxItemsinSearchResults') - 1
1305           : 1;
1306
1307         # loop through every item
1308         foreach my $field (@fields) {
1309             my $item;
1310
1311             # populate the items hash
1312             foreach my $code ( keys %subfieldstosearch ) {
1313                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1314             }
1315                         my $hbranch     = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch'    : 'holdingbranch';
1316                         my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
1317             # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
1318             if ($item->{$hbranch}) {
1319                 $item->{'branchname'} = $branches{$item->{$hbranch}};
1320             }
1321             elsif ($item->{$otherbranch}) {     # Last resort
1322                 $item->{'branchname'} = $branches{$item->{$otherbranch}}; 
1323             }
1324
1325                         my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
1326 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
1327             if ( $item->{onloan} ) {
1328                 $onloan_count++;
1329                                 my $key = $prefix . $item->{onloan} . $item->{barcode};
1330                                 $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
1331                                 $onloan_items->{$key}->{count}++ if $item->{$hbranch};
1332                                 $onloan_items->{$key}->{branchname} = $item->{branchname};
1333                                 $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1334                                 $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
1335                                 $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1336                 # if something's checked out and lost, mark it as 'long overdue'
1337                 if ( $item->{itemlost} ) {
1338                     $onloan_items->{$prefix}->{longoverdue}++;
1339                     $longoverdue_count++;
1340                 } else {        # can place holds as long as item isn't lost
1341                     $can_place_holds = 1;
1342                 }
1343             }
1344
1345          # items not on loan, but still unavailable ( lost, withdrawn, damaged )
1346             else {
1347
1348                 # item is on order
1349                 if ( $item->{notforloan} == -1 ) {
1350                     $ordered_count++;
1351                 }
1352
1353                 # is item in transit?
1354                 my $transfertwhen = '';
1355                 my ($transfertfrom, $transfertto);
1356                 
1357                 unless ($item->{wthdrawn}
1358                         || $item->{itemlost}
1359                         || $item->{damaged}
1360                         || $item->{notforloan}
1361                         || $items_count > 20) {
1362
1363                     # A couple heuristics to limit how many times
1364                     # we query the database for item transfer information, sacrificing
1365                     # accuracy in some cases for speed;
1366                     #
1367                     # 1. don't query if item has one of the other statuses
1368                     # 2. don't check transit status if the bib has
1369                     #    more than 20 items
1370                     #
1371                     # FIXME: to avoid having the query the database like this, and to make
1372                     #        the in transit status count as unavailable for search limiting,
1373                     #        should map transit status to record indexed in Zebra.
1374                     #
1375                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
1376                 }
1377
1378                 # item is withdrawn, lost or damaged
1379                 if (   $item->{wthdrawn}
1380                     || $item->{itemlost}
1381                     || $item->{damaged}
1382                     || $item->{notforloan} 
1383                     || ($transfertwhen ne ''))
1384                 {
1385                     $wthdrawn_count++        if $item->{wthdrawn};
1386                     $itemlost_count++        if $item->{itemlost};
1387                     $itemdamaged_count++     if $item->{damaged};
1388                     $item_in_transit_count++ if $transfertwhen ne '';
1389                     $item->{status} = $item->{wthdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
1390                     $other_count++;
1391
1392                                         my $key = $prefix . $item->{status};
1393                                         foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber)) {
1394                         $other_items->{$key}->{$_} = $item->{$_};
1395                                         }
1396                     $other_items->{$key}->{intransit} = ($transfertwhen ne '') ? 1 : 0;
1397                                         $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
1398                                         $other_items->{$key}->{count}++ if $item->{$hbranch};
1399                                         $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1400                                         $other_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1401                 }
1402                 # item is available
1403                 else {
1404                     $can_place_holds = 1;
1405                     $available_count++;
1406                                         $available_items->{$prefix}->{count}++ if $item->{$hbranch};
1407                                         foreach (qw(branchname itemcallnumber)) {
1408                         $available_items->{$prefix}->{$_} = $item->{$_};
1409                                         }
1410                                         $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
1411                                         $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1412                 }
1413             }
1414         }    # notforloan, item level and biblioitem level
1415         my ( $availableitemscount, $onloanitemscount, $otheritemscount );
1416         $maxitems =
1417           ( C4::Context->preference('maxItemsinSearchResults') )
1418           ? C4::Context->preference('maxItemsinSearchResults') - 1
1419           : 1;
1420         for my $key ( sort keys %$onloan_items ) {
1421             (++$onloanitemscount > $maxitems) and last;
1422             push @onloan_items_loop, $onloan_items->{$key};
1423         }
1424         for my $key ( sort keys %$other_items ) {
1425             (++$otheritemscount > $maxitems) and last;
1426             push @other_items_loop, $other_items->{$key};
1427         }
1428         for my $key ( sort keys %$available_items ) {
1429             (++$availableitemscount > $maxitems) and last;
1430             push @available_items_loop, $available_items->{$key}
1431         }
1432
1433         # XSLT processing of some stuff
1434         if (C4::Context->preference("XSLTResultsDisplay") && !$scan) {
1435             $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display(
1436                 $oldbiblio->{biblionumber}, $marcrecord, 'Results' );
1437         }
1438
1439         # last check for norequest : if itemtype is notforloan, it can't be reserved either, whatever the items
1440         $can_place_holds = 0
1441           if $itemtypes{ $oldbiblio->{itemtype} }->{notforloan};
1442         $oldbiblio->{norequests} = 1 unless $can_place_holds;
1443         $oldbiblio->{itemsplural}          = 1 if $items_count > 1;
1444         $oldbiblio->{items_count}          = $items_count;
1445         $oldbiblio->{available_items_loop} = \@available_items_loop;
1446         $oldbiblio->{onloan_items_loop}    = \@onloan_items_loop;
1447         $oldbiblio->{other_items_loop}     = \@other_items_loop;
1448         $oldbiblio->{availablecount}       = $available_count;
1449         $oldbiblio->{availableplural}      = 1 if $available_count > 1;
1450         $oldbiblio->{onloancount}          = $onloan_count;
1451         $oldbiblio->{onloanplural}         = 1 if $onloan_count > 1;
1452         $oldbiblio->{othercount}           = $other_count;
1453         $oldbiblio->{otherplural}          = 1 if $other_count > 1;
1454         $oldbiblio->{wthdrawncount}        = $wthdrawn_count;
1455         $oldbiblio->{itemlostcount}        = $itemlost_count;
1456         $oldbiblio->{damagedcount}         = $itemdamaged_count;
1457         $oldbiblio->{intransitcount}       = $item_in_transit_count;
1458         $oldbiblio->{orderedcount}         = $ordered_count;
1459         push( @newresults, $oldbiblio );
1460     }
1461     return @newresults;
1462 }
1463
1464 #----------------------------------------------------------------------
1465 #
1466 # Non-Zebra GetRecords#
1467 #----------------------------------------------------------------------
1468
1469 =head2 NZgetRecords
1470
1471   NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1472
1473 =cut
1474
1475 sub NZgetRecords {
1476     my (
1477         $query,            $simple_query, $sort_by_ref,    $servers_ref,
1478         $results_per_page, $offset,       $expanded_facet, $branches,
1479         $query_type,       $scan
1480     ) = @_;
1481     warn "query =$query" if $DEBUG;
1482     my $result = NZanalyse($query);
1483     warn "results =$result" if $DEBUG;
1484     return ( undef,
1485         NZorder( $result, @$sort_by_ref[0], $results_per_page, $offset ),
1486         undef );
1487 }
1488
1489 =head2 NZanalyse
1490
1491   NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1492   the list is built from an inverted index in the nozebra SQL table
1493   note that title is here only for convenience : the sorting will be very fast when requested on title
1494   if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1495
1496 =cut
1497
1498 sub NZanalyse {
1499     my ( $string, $server ) = @_;
1500 #     warn "---------"       if $DEBUG;
1501     warn " NZanalyse" if $DEBUG;
1502 #     warn "---------"       if $DEBUG;
1503
1504  # $server contains biblioserver or authorities, depending on what we search on.
1505  #warn "querying : $string on $server";
1506     $server = 'biblioserver' unless $server;
1507
1508 # if we have a ", replace the content to discard temporarily any and/or/not inside
1509     my $commacontent;
1510     if ( $string =~ /"/ ) {
1511         $string =~ s/"(.*?)"/__X__/;
1512         $commacontent = $1;
1513         warn "commacontent : $commacontent" if $DEBUG;
1514     }
1515
1516 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1517 # then, call again NZanalyse with $left and $right
1518 # (recursive until we find a leaf (=> something without and/or/not)
1519 # delete repeated operator... Would then go in infinite loop
1520     while ( $string =~ s/( and| or| not| AND| OR| NOT)\1/$1/g ) {
1521     }
1522
1523     #process parenthesis before.
1524     if ( $string =~ /^\s*\((.*)\)(( and | or | not | AND | OR | NOT )(.*))?/ ) {
1525         my $left     = $1;
1526         my $right    = $4;
1527         my $operator = lc($3);   # FIXME: and/or/not are operators, not operands
1528         warn
1529 "dealing w/parenthesis before recursive sub call. left :$left operator:$operator right:$right"
1530           if $DEBUG;
1531         my $leftresult = NZanalyse( $left, $server );
1532         if ($operator) {
1533             my $rightresult = NZanalyse( $right, $server );
1534
1535             # OK, we have the results for right and left part of the query
1536             # depending of operand, intersect, union or exclude both lists
1537             # to get a result list
1538             if ( $operator eq ' and ' ) {
1539                 return NZoperatorAND($leftresult,$rightresult);      
1540             }
1541             elsif ( $operator eq ' or ' ) {
1542
1543                 # just merge the 2 strings
1544                 return $leftresult . $rightresult;
1545             }
1546             elsif ( $operator eq ' not ' ) {
1547                 return NZoperatorNOT($leftresult,$rightresult);      
1548             }
1549         }      
1550         else {
1551 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1552             return $leftresult;
1553         } 
1554     }
1555     warn "string :" . $string if $DEBUG;
1556     my $left = "";
1557     my $right = "";
1558     my $operator = "";
1559     if ($string =~ /(.*?)( and | or | not | AND | OR | NOT )(.*)/) {
1560         $left     = $1;
1561         $right    = $3;
1562         $operator = lc($2);    # FIXME: and/or/not are operators, not operands
1563     }
1564     warn "no parenthesis. left : $left operator: $operator right: $right"
1565       if $DEBUG;
1566
1567     # it's not a leaf, we have a and/or/not
1568     if ($operator) {
1569
1570         # reintroduce comma content if needed
1571         $right =~ s/__X__/"$commacontent"/ if $commacontent;
1572         $left  =~ s/__X__/"$commacontent"/ if $commacontent;
1573         warn "node : $left / $operator / $right\n" if $DEBUG;
1574         my $leftresult  = NZanalyse( $left,  $server );
1575         my $rightresult = NZanalyse( $right, $server );
1576         warn " leftresult : $leftresult" if $DEBUG;
1577         warn " rightresult : $rightresult" if $DEBUG;
1578         # OK, we have the results for right and left part of the query
1579         # depending of operand, intersect, union or exclude both lists
1580         # to get a result list
1581         if ( $operator eq ' and ' ) {
1582             warn "NZAND";
1583             return NZoperatorAND($leftresult,$rightresult);
1584         }
1585         elsif ( $operator eq ' or ' ) {
1586
1587             # just merge the 2 strings
1588             return $leftresult . $rightresult;
1589         }
1590         elsif ( $operator eq ' not ' ) {
1591             return NZoperatorNOT($leftresult,$rightresult);
1592         }
1593         else {
1594
1595 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1596             die "error : operand unknown : $operator for $string";
1597         }
1598
1599         # it's a leaf, do the real SQL query and return the result
1600     }
1601     else {
1602         $string =~ s/__X__/"$commacontent"/ if $commacontent;
1603         $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|&|\+|\*|\// /g;
1604         #remove trailing blank at the beginning
1605         $string =~ s/^ //g;
1606         warn "leaf:$string" if $DEBUG;
1607
1608         # parse the string in in operator/operand/value again
1609         my $left = "";
1610         my $operator = "";
1611         my $right = "";
1612         if ($string =~ /(.*)(>=|<=)(.*)/) {
1613             $left     = $1;
1614             $operator = $2;
1615             $right    = $3;
1616         } else {
1617             $left = $string;
1618         }
1619 #         warn "handling leaf... left:$left operator:$operator right:$right"
1620 #           if $DEBUG;
1621         unless ($operator) {
1622             if ($string =~ /(.*)(>|<|=)(.*)/) {
1623                 $left     = $1;
1624                 $operator = $2;
1625                 $right    = $3;
1626                 warn
1627     "handling unless (operator)... left:$left operator:$operator right:$right"
1628                 if $DEBUG;
1629             } else {
1630                 $left = $string;
1631             }
1632         }
1633         my $results;
1634
1635 # strip adv, zebra keywords, currently not handled in nozebra: wrdl, ext, phr...
1636         $left =~ s/ .*$//;
1637
1638         # automatic replace for short operators
1639         $left = 'title'            if $left =~ '^ti$';
1640         $left = 'author'           if $left =~ '^au$';
1641         $left = 'publisher'        if $left =~ '^pb$';
1642         $left = 'subject'          if $left =~ '^su$';
1643         $left = 'koha-Auth-Number' if $left =~ '^an$';
1644         $left = 'keyword'          if $left =~ '^kw$';
1645         $left = 'itemtype'         if $left =~ '^mc$'; # Fix for Bug 2599 - Search limits not working for NoZebra 
1646         warn "handling leaf... left:$left operator:$operator right:$right" if $DEBUG;
1647         my $dbh = C4::Context->dbh;
1648         if ( $operator && $left ne 'keyword' ) {
1649             #do a specific search
1650             $operator = 'LIKE' if $operator eq '=' and $right =~ /%/;
1651             my $sth = $dbh->prepare(
1652 "SELECT biblionumbers,value FROM nozebra WHERE server=? AND indexname=? AND value $operator ?"
1653             );
1654             warn "$left / $operator / $right\n" if $DEBUG;
1655
1656             # split each word, query the DB and build the biblionumbers result
1657             #sanitizing leftpart
1658             $left =~ s/^\s+|\s+$//;
1659             foreach ( split / /, $right ) {
1660                 my $biblionumbers;
1661                 $_ =~ s/^\s+|\s+$//;
1662                 next unless $_;
1663                 warn "EXECUTE : $server, $left, $_" if $DEBUG;
1664                 $sth->execute( $server, $left, $_ )
1665                   or warn "execute failed: $!";
1666                 while ( my ( $line, $value ) = $sth->fetchrow ) {
1667
1668 # if we are dealing with a numeric value, use only numeric results (in case of >=, <=, > or <)
1669 # otherwise, fill the result
1670                     $biblionumbers .= $line
1671                       unless ( $right =~ /^\d+$/ && $value =~ /\D/ );
1672                     warn "result : $value "
1673                       . ( $right  =~ /\d/ ) . "=="
1674                       . ( $value =~ /\D/?$line:"" ) if $DEBUG;         #= $line";
1675                 }
1676
1677 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1678                 if ($results) {
1679                     warn "NZAND" if $DEBUG;
1680                     $results = NZoperatorAND($biblionumbers,$results);
1681                 } else {
1682                     $results = $biblionumbers;
1683                 }
1684             }
1685         }
1686         else {
1687       #do a complete search (all indexes), if index='kw' do complete search too.
1688             my $sth = $dbh->prepare(
1689 "SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?"
1690             );
1691
1692             # split each word, query the DB and build the biblionumbers result
1693             foreach ( split / /, $string ) {
1694                 next if C4::Context->stopwords->{ uc($_) };   # skip if stopword
1695                 warn "search on all indexes on $_" if $DEBUG;
1696                 my $biblionumbers;
1697                 next unless $_;
1698                 $sth->execute( $server, $_ );
1699                 while ( my $line = $sth->fetchrow ) {
1700                     $biblionumbers .= $line;
1701                 }
1702
1703 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1704                 if ($results) {
1705                     $results = NZoperatorAND($biblionumbers,$results);
1706                 }
1707                 else {
1708                     warn "NEW RES for $_ = $biblionumbers" if $DEBUG;
1709                     $results = $biblionumbers;
1710                 }
1711             }
1712         }
1713         warn "return : $results for LEAF : $string" if $DEBUG;
1714         return $results;
1715     }
1716     warn "---------\nLeave NZanalyse\n---------" if $DEBUG;
1717 }
1718
1719 sub NZoperatorAND{
1720     my ($rightresult, $leftresult)=@_;
1721     
1722     my @leftresult = split /;/, $leftresult;
1723     warn " @leftresult / $rightresult \n" if $DEBUG;
1724     
1725     #             my @rightresult = split /;/,$leftresult;
1726     my $finalresult;
1727
1728 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1729 # the result is stored twice, to have the same weight for AND than OR.
1730 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1731 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1732     foreach (@leftresult) {
1733         my $value = $_;
1734         my $countvalue;
1735         ( $value, $countvalue ) = ( $1, $2 ) if ($value=~/(.*)-(\d+)$/);
1736         if ( $rightresult =~ /\Q$value\E-(\d+);/ ) {
1737             $countvalue = ( $1 > $countvalue ? $countvalue : $1 );
1738             $finalresult .=
1739                 "$value-$countvalue;$value-$countvalue;";
1740         }
1741     }
1742     warn "NZAND DONE : $finalresult \n" if $DEBUG;
1743     return $finalresult;
1744 }
1745       
1746 sub NZoperatorOR{
1747     my ($rightresult, $leftresult)=@_;
1748     return $rightresult.$leftresult;
1749 }
1750
1751 sub NZoperatorNOT{
1752     my ($leftresult, $rightresult)=@_;
1753     
1754     my @leftresult = split /;/, $leftresult;
1755
1756     #             my @rightresult = split /;/,$leftresult;
1757     my $finalresult;
1758     foreach (@leftresult) {
1759         my $value=$_;
1760         $value=$1 if $value=~m/(.*)-\d+$/;
1761         unless ($rightresult =~ "$value-") {
1762             $finalresult .= "$_;";
1763         }
1764     }
1765     return $finalresult;
1766 }
1767
1768 =head2 NZorder
1769
1770   $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
1771   
1772   TODO :: Description
1773
1774 =cut
1775
1776 sub NZorder {
1777     my ( $biblionumbers, $ordering, $results_per_page, $offset ) = @_;
1778     warn "biblionumbers = $biblionumbers and ordering = $ordering\n" if $DEBUG;
1779
1780     # order title asc by default
1781     #     $ordering = '1=36 <i' unless $ordering;
1782     $results_per_page = 20 unless $results_per_page;
1783     $offset           = 0  unless $offset;
1784     my $dbh = C4::Context->dbh;
1785
1786     #
1787     # order by POPULARITY
1788     #
1789     if ( $ordering =~ /popularity/ ) {
1790         my %result;
1791         my %popularity;
1792
1793         # popularity is not in MARC record, it's builded from a specific query
1794         my $sth =
1795           $dbh->prepare("select sum(issues) from items where biblionumber=?");
1796         foreach ( split /;/, $biblionumbers ) {
1797             my ( $biblionumber, $title ) = split /,/, $_;
1798             $result{$biblionumber} = GetMarcBiblio($biblionumber);
1799             $sth->execute($biblionumber);
1800             my $popularity = $sth->fetchrow || 0;
1801
1802 # hint : the key is popularity.title because we can have
1803 # many results with the same popularity. In this case, sub-ordering is done by title
1804 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
1805 # (un-frequent, I agree, but we won't forget anything that way ;-)
1806             $popularity{ sprintf( "%10d", $popularity ) . $title
1807                   . $biblionumber } = $biblionumber;
1808         }
1809
1810     # sort the hash and return the same structure as GetRecords (Zebra querying)
1811         my $result_hash;
1812         my $numbers = 0;
1813         if ( $ordering eq 'popularity_dsc' ) {    # sort popularity DESC
1814             foreach my $key ( sort { $b cmp $a } ( keys %popularity ) ) {
1815                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1816                   $result{ $popularity{$key} }->as_usmarc();
1817             }
1818         }
1819         else {                                    # sort popularity ASC
1820             foreach my $key ( sort ( keys %popularity ) ) {
1821                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1822                   $result{ $popularity{$key} }->as_usmarc();
1823             }
1824         }
1825         my $finalresult = ();
1826         $result_hash->{'hits'}         = $numbers;
1827         $finalresult->{'biblioserver'} = $result_hash;
1828         return $finalresult;
1829
1830         #
1831         # ORDER BY author
1832         #
1833     }
1834     elsif ( $ordering =~ /author/ ) {
1835         my %result;
1836         foreach ( split /;/, $biblionumbers ) {
1837             my ( $biblionumber, $title ) = split /,/, $_;
1838             my $record = GetMarcBiblio($biblionumber);
1839             my $author;
1840             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
1841                 $author = $record->subfield( '200', 'f' );
1842                 $author = $record->subfield( '700', 'a' ) unless $author;
1843             }
1844             else {
1845                 $author = $record->subfield( '100', 'a' );
1846             }
1847
1848 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1849 # and we don't want to get only 1 result for each of them !!!
1850             $result{ $author . $biblionumber } = $record;
1851         }
1852
1853     # sort the hash and return the same structure as GetRecords (Zebra querying)
1854         my $result_hash;
1855         my $numbers = 0;
1856         if ( $ordering eq 'author_za' ) {    # sort by author desc
1857             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
1858                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1859                   $result{$key}->as_usmarc();
1860             }
1861         }
1862         else {                               # sort by author ASC
1863             foreach my $key ( sort ( keys %result ) ) {
1864                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1865                   $result{$key}->as_usmarc();
1866             }
1867         }
1868         my $finalresult = ();
1869         $result_hash->{'hits'}         = $numbers;
1870         $finalresult->{'biblioserver'} = $result_hash;
1871         return $finalresult;
1872
1873         #
1874         # ORDER BY callnumber
1875         #
1876     }
1877     elsif ( $ordering =~ /callnumber/ ) {
1878         my %result;
1879         foreach ( split /;/, $biblionumbers ) {
1880             my ( $biblionumber, $title ) = split /,/, $_;
1881             my $record = GetMarcBiblio($biblionumber);
1882             my $callnumber;
1883             my $frameworkcode = GetFrameworkCode($biblionumber);
1884             my ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField(  'items.itemcallnumber', $frameworkcode);
1885                ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField('biblioitems.callnumber', $frameworkcode)
1886                 unless $callnumber_tag;
1887             if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
1888                 $callnumber = $record->subfield( '200', 'f' );
1889             } else {
1890                 $callnumber = $record->subfield( '100', 'a' );
1891             }
1892
1893 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1894 # and we don't want to get only 1 result for each of them !!!
1895             $result{ $callnumber . $biblionumber } = $record;
1896         }
1897
1898     # sort the hash and return the same structure as GetRecords (Zebra querying)
1899         my $result_hash;
1900         my $numbers = 0;
1901         if ( $ordering eq 'call_number_dsc' ) {    # sort by title desc
1902             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
1903                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1904                   $result{$key}->as_usmarc();
1905             }
1906         }
1907         else {                                     # sort by title ASC
1908             foreach my $key ( sort { $a cmp $b } ( keys %result ) ) {
1909                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1910                   $result{$key}->as_usmarc();
1911             }
1912         }
1913         my $finalresult = ();
1914         $result_hash->{'hits'}         = $numbers;
1915         $finalresult->{'biblioserver'} = $result_hash;
1916         return $finalresult;
1917     }
1918     elsif ( $ordering =~ /pubdate/ ) {             #pub year
1919         my %result;
1920         foreach ( split /;/, $biblionumbers ) {
1921             my ( $biblionumber, $title ) = split /,/, $_;
1922             my $record = GetMarcBiblio($biblionumber);
1923             my ( $publicationyear_tag, $publicationyear_subfield ) =
1924               GetMarcFromKohaField( 'biblioitems.publicationyear', '' );
1925             my $publicationyear =
1926               $record->subfield( $publicationyear_tag,
1927                 $publicationyear_subfield );
1928
1929 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1930 # and we don't want to get only 1 result for each of them !!!
1931             $result{ $publicationyear . $biblionumber } = $record;
1932         }
1933
1934     # sort the hash and return the same structure as GetRecords (Zebra querying)
1935         my $result_hash;
1936         my $numbers = 0;
1937         if ( $ordering eq 'pubdate_dsc' ) {    # sort by pubyear desc
1938             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
1939                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1940                   $result{$key}->as_usmarc();
1941             }
1942         }
1943         else {                                 # sort by pub year ASC
1944             foreach my $key ( sort ( keys %result ) ) {
1945                 $result_hash->{'RECORDS'}[ $numbers++ ] =
1946                   $result{$key}->as_usmarc();
1947             }
1948         }
1949         my $finalresult = ();
1950         $result_hash->{'hits'}         = $numbers;
1951         $finalresult->{'biblioserver'} = $result_hash;
1952         return $finalresult;
1953
1954         #
1955         # ORDER BY title
1956         #
1957     }
1958     elsif ( $ordering =~ /title/ ) {
1959
1960 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
1961         my %result;
1962         foreach ( split /;/, $biblionumbers ) {
1963             my ( $biblionumber, $title ) = split /,/, $_;
1964
1965 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1966 # and we don't want to get only 1 result for each of them !!!
1967 # hint & speed improvement : we can order without reading the record
1968 # so order, and read records only for the requested page !
1969             $result{ $title . $biblionumber } = $biblionumber;
1970         }
1971
1972     # sort the hash and return the same structure as GetRecords (Zebra querying)
1973         my $result_hash;
1974         my $numbers = 0;
1975         if ( $ordering eq 'title_az' ) {    # sort by title desc
1976             foreach my $key ( sort ( keys %result ) ) {
1977                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
1978             }
1979         }
1980         else {                              # sort by title ASC
1981             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
1982                 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
1983             }
1984         }
1985
1986         # limit the $results_per_page to result size if it's more
1987         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
1988
1989         # for the requested page, replace biblionumber by the complete record
1990         # speed improvement : avoid reading too much things
1991         for (
1992             my $counter = $offset ;
1993             $counter <= $offset + $results_per_page ;
1994             $counter++
1995           )
1996         {
1997             $result_hash->{'RECORDS'}[$counter] =
1998               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc;
1999         }
2000         my $finalresult = ();
2001         $result_hash->{'hits'}         = $numbers;
2002         $finalresult->{'biblioserver'} = $result_hash;
2003         return $finalresult;
2004     }
2005     else {
2006
2007 #
2008 # order by ranking
2009 #
2010 # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
2011         my %result;
2012         my %count_ranking;
2013         foreach ( split /;/, $biblionumbers ) {
2014             my ( $biblionumber, $title ) = split /,/, $_;
2015             $title =~ /(.*)-(\d)/;
2016
2017             # get weight
2018             my $ranking = $2;
2019
2020 # note that we + the ranking because ranking is calculated on weight of EACH term requested.
2021 # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
2022 # biblio N has ranking = 6
2023             $count_ranking{$biblionumber} += $ranking;
2024         }
2025
2026 # build the result by "inverting" the count_ranking hash
2027 # 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
2028 #         warn "counting";
2029         foreach ( keys %count_ranking ) {
2030             $result{ sprintf( "%10d", $count_ranking{$_} ) . '-' . $_ } = $_;
2031         }
2032
2033     # sort the hash and return the same structure as GetRecords (Zebra querying)
2034         my $result_hash;
2035         my $numbers = 0;
2036         foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2037             $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2038         }
2039
2040         # limit the $results_per_page to result size if it's more
2041         $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2042
2043         # for the requested page, replace biblionumber by the complete record
2044         # speed improvement : avoid reading too much things
2045         for (
2046             my $counter = $offset ;
2047             $counter <= $offset + $results_per_page ;
2048             $counter++
2049           )
2050         {
2051             $result_hash->{'RECORDS'}[$counter] =
2052               GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc
2053               if $result_hash->{'RECORDS'}[$counter];
2054         }
2055         my $finalresult = ();
2056         $result_hash->{'hits'}         = $numbers;
2057         $finalresult->{'biblioserver'} = $result_hash;
2058         return $finalresult;
2059     }
2060 }
2061
2062 =head2 enabled_staff_search_views
2063
2064 %hash = enabled_staff_search_views()
2065
2066 This function returns a hash that contains three flags obtained from the system
2067 preferences, used to determine whether a particular staff search results view
2068 is enabled.
2069
2070 =over 2
2071
2072 =item C<Output arg:>
2073
2074     * $hash{can_view_MARC} is true only if the MARC view is enabled
2075     * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2076     * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2077
2078 =item C<usage in the script:>
2079
2080 =back
2081
2082 $template->param ( C4::Search::enabled_staff_search_views );
2083
2084 =cut
2085
2086 sub enabled_staff_search_views
2087 {
2088         return (
2089                 can_view_MARC                   => C4::Context->preference('viewMARC'),                 # 1 if the staff search allows the MARC view
2090                 can_view_ISBD                   => C4::Context->preference('viewISBD'),                 # 1 if the staff search allows the ISBD view
2091                 can_view_labeledMARC    => C4::Context->preference('viewLabeledMARC'),  # 1 if the staff search allows the Labeled MARC view
2092         );
2093 }
2094
2095 sub AddSearchHistory{
2096         my ($borrowernumber,$session,$query_desc,$query_cgi, $total)=@_;
2097     my $dbh = C4::Context->dbh;
2098
2099     # Add the request the user just made
2100     my $sql = "INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, total, time) VALUES(?, ?, ?, ?, ?, NOW())";
2101     my $sth   = $dbh->prepare($sql);
2102     $sth->execute($borrowernumber, $session, $query_desc, $query_cgi, $total);
2103         return $dbh->last_insert_id(undef, 'search_history', undef,undef,undef);
2104 }
2105
2106 sub GetSearchHistory{
2107         my ($borrowernumber,$session)=@_;
2108     my $dbh = C4::Context->dbh;
2109
2110     # Add the request the user just made
2111     my $query = "SELECT FROM search_history WHERE (userid=? OR sessionid=?)";
2112     my $sth   = $dbh->prepare($query);
2113         $sth->execute($borrowernumber, $session);
2114     return  $sth->fetchall_hashref({});
2115 }
2116
2117 =head2 z3950_search_args
2118
2119 $arrayref = z3950_search_args($matchpoints)
2120
2121 This function returns an array reference that contains the search parameters to be
2122 passed to the Z39.50 search script (z3950_search.pl). The array elements
2123 are hash refs whose keys are name, value and encvalue, and whose values are the
2124 name of a search parameter, the value of that search parameter and the URL encoded
2125 value of that parameter.
2126
2127 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2128
2129 The search parameter values are obtained from the bibliographic record whose
2130 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2131
2132 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2133 a general purpose search argument. In this case, the returned array contains only
2134 entry: the key is 'title' and the value and encvalue are derived from $matchpoints.
2135
2136 If a search parameter value is undefined or empty, it is not included in the returned
2137 array.
2138
2139 The returned array reference may be passed directly to the template parameters.
2140
2141 =over 2
2142
2143 =item C<Output arg:>
2144
2145     * $array containing hash refs as described above
2146
2147 =item C<usage in the script:>
2148
2149 =back
2150
2151 $data = Biblio::GetBiblioData($bibno);
2152 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2153
2154 *OR*
2155
2156 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2157
2158 =cut
2159
2160 sub z3950_search_args {
2161     my $bibrec = shift;
2162     $bibrec = { title => $bibrec } if !ref $bibrec;
2163     my $array = [];
2164     for my $field (qw/ lccn isbn issn title author dewey subject /)
2165     {
2166         my $encvalue = URI::Escape::uri_escape_utf8($bibrec->{$field});
2167         push @$array, { name=>$field, value=>$bibrec->{$field}, encvalue=>$encvalue } if defined $bibrec->{$field};
2168     }
2169     return $array;
2170 }
2171
2172 =head2 BiblioAddAuthorities
2173
2174 ( $countlinked, $countcreated ) = BiblioAddAuthorities($record, $frameworkcode);
2175
2176 this function finds the authorities linked to the biblio
2177     * search in the authority DB for the same authid (in $9 of the biblio)
2178     * search in the authority DB for the same 001 (in $3 of the biblio in UNIMARC)
2179     * search in the authority DB for the same values (exactly) (in all subfields of the biblio)
2180 OR adds a new authority record
2181
2182 =over 2
2183
2184 =item C<input arg:>
2185
2186     * $record is the MARC record in question (marc blob)
2187     * $frameworkcode is the bibliographic framework to use (if it is "" it uses the default framework)
2188
2189 =item C<Output arg:>
2190
2191     * $countlinked is the number of authorities records that are linked to this authority
2192     * $countcreated
2193
2194 =item C<BUGS>
2195     * 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)
2196 =back
2197
2198 =cut
2199
2200
2201 sub BiblioAddAuthorities{
2202   my ( $record, $frameworkcode ) = @_;
2203   my $dbh=C4::Context->dbh;
2204   my $query=$dbh->prepare(qq|
2205 SELECT authtypecode,tagfield
2206 FROM marc_subfield_structure 
2207 WHERE frameworkcode=? 
2208 AND (authtypecode IS NOT NULL AND authtypecode<>\"\")|);
2209 # SELECT authtypecode,tagfield
2210 # FROM marc_subfield_structure 
2211 # WHERE frameworkcode=? 
2212 # AND (authtypecode IS NOT NULL OR authtypecode<>\"\")|);
2213   $query->execute($frameworkcode);
2214   my ($countcreated,$countlinked);
2215   while (my $data=$query->fetchrow_hashref){
2216     foreach my $field ($record->field($data->{tagfield})){
2217       next if ($field->subfield('3')||$field->subfield('9'));
2218       # No authorities id in the tag.
2219       # Search if there is any authorities to link to.
2220       my $query='at='.$data->{authtypecode}.' ';
2221       map {$query.= ' and he,ext="'.$_->[1].'"' if ($_->[0]=~/[A-z]/)}  $field->subfields();
2222       my ($error, $results, $total_hits)=SimpleSearch( $query, undef, undef, [ "authorityserver" ] );
2223     # there is only 1 result 
2224           if ( $error ) {
2225         warn "BIBLIOADDSAUTHORITIES: $error";
2226             return (0,0) ;
2227           }
2228       if ($results && scalar(@$results)==1) {
2229         my $marcrecord = MARC::File::USMARC::decode($results->[0]);
2230         $field->add_subfields('9'=>$marcrecord->field('001')->data);
2231         $countlinked++;
2232       } elsif (scalar(@$results)>1) {
2233    #More than One result 
2234    #This can comes out of a lack of a subfield.
2235 #         my $marcrecord = MARC::File::USMARC::decode($results->[0]);
2236 #         $record->field($data->{tagfield})->add_subfields('9'=>$marcrecord->field('001')->data);
2237   $countlinked++;
2238       } else {
2239   #There are no results, build authority record, add it to Authorities, get authid and add it to 9
2240   ###NOTICE : This is only valid if a subfield is linked to one and only one authtypecode     
2241   ###NOTICE : This can be a problem. We should also look into other types and rejected forms.
2242          my $authtypedata=C4::AuthoritiesMarc->GetAuthType($data->{authtypecode});
2243          next unless $authtypedata;
2244          my $marcrecordauth=MARC::Record->new();
2245          my $authfield=MARC::Field->new($authtypedata->{auth_tag_to_report},'','',"a"=>"".$field->subfield('a'));
2246          map { $authfield->add_subfields($_->[0]=>$_->[1]) if ($_->[0]=~/[A-z]/ && $_->[0] ne "a" )}  $field->subfields();
2247          $marcrecordauth->insert_fields_ordered($authfield);
2248
2249          # bug 2317: ensure new authority knows it's using UTF-8; currently
2250          # only need to do this for MARC21, as MARC::Record->as_xml_record() handles
2251          # automatically for UNIMARC (by not transcoding)
2252          # FIXME: AddAuthority() instead should simply explicitly require that the MARC::Record
2253          # use UTF-8, but as of 2008-08-05, did not want to introduce that kind
2254          # of change to a core API just before the 3.0 release.
2255          if (C4::Context->preference('marcflavour') eq 'MARC21') {
2256             SetMarcUnicodeFlag($marcrecordauth, 'MARC21');
2257          }
2258
2259 #          warn "AUTH RECORD ADDED : ".$marcrecordauth->as_formatted;
2260
2261          my $authid=AddAuthority($marcrecordauth,'',$data->{authtypecode});
2262          $countcreated++;
2263          $field->add_subfields('9'=>$authid);
2264       }
2265     }
2266   }
2267   return ($countlinked,$countcreated);
2268 }
2269
2270 =head2 GetDistinctValues($field);
2271
2272 C<$field> is a reference to the fields array
2273
2274 =cut
2275
2276 sub GetDistinctValues {
2277     my ($fieldname,$string)=@_;
2278     # returns a reference to a hash of references to branches...
2279     if ($fieldname=~/\./){
2280                         my ($table,$column)=split /\./, $fieldname;
2281                         my $dbh = C4::Context->dbh;
2282                         warn "select DISTINCT($column) as value, count(*) as cnt from $table group by lib order by $column ";
2283                         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 ");
2284                         $sth->execute;
2285                         my $elements=$sth->fetchall_arrayref({});
2286                         return $elements;
2287    }
2288    else {
2289                 $string||= qq("");
2290                 my @servers=qw<biblioserver authorityserver>;
2291                 my (@zconns,@results);
2292         for ( my $i = 0 ; $i < @servers ; $i++ ) {
2293                 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
2294                         $results[$i] =
2295                       $zconns[$i]->scan(
2296                         ZOOM::Query::CCL2RPN->new( qq"$fieldname $string", $zconns[$i])
2297                       );
2298                 }
2299                 # The big moment: asynchronously retrieve results from all servers
2300                 my @elements;
2301                 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
2302                         my $ev = $zconns[ $i - 1 ]->last_event();
2303                         if ( $ev == ZOOM::Event::ZEND ) {
2304                                 next unless $results[ $i - 1 ];
2305                                 my $size = $results[ $i - 1 ]->size();
2306                                 if ( $size > 0 ) {
2307                       for (my $j=0;$j<$size;$j++){
2308                                                 my %hashscan;
2309                                                 @hashscan{qw(value cnt)}=$results[ $i - 1 ]->display_term($j);
2310                                                 push @elements, \%hashscan;
2311                                           }
2312                                 }
2313                         }
2314                 }
2315                 return \@elements;
2316    }
2317 }
2318
2319 END { }    # module clean-up code here (global destructor)
2320
2321 1;
2322 __END__
2323
2324 =head1 AUTHOR
2325
2326 Koha Developement team <info@koha.org>
2327
2328 =cut