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