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