fixing re-sorting in staff client
[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::Date;
25
26 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
27
28 # set the version for version checking
29 $VERSION = 3.00;
30
31 =head1 NAME
32
33 C4::Search - Functions for searching the Koha catalog.
34
35 =head1 SYNOPSIS
36
37 see opac/opac-search.pl or catalogue/search.pl for example of usage
38
39 =head1 DESCRIPTION
40
41 This module provides the searching facilities for the Koha into a zebra catalog.
42
43 =head1 FUNCTIONS
44
45 =cut
46
47 @ISA    = qw(Exporter);
48 @EXPORT = qw(
49   &SimpleSearch
50   &findseealso
51   &FindDuplicate
52   &searchResults
53   &getRecords
54   &buildQuery
55   &NZgetRecords
56   &ModBiblios
57 );
58
59 # make all your functions, whether exported or not;
60
61 =head2 findseealso($dbh,$fields);
62
63 C<$dbh> is a link to the DB handler.
64
65 use C4::Context;
66 my $dbh =C4::Context->dbh;
67
68 C<$fields> is a reference to the fields array
69
70 This function modify the @$fields array and add related fields to search on.
71
72 =cut
73
74 sub findseealso {
75     my ( $dbh, $fields ) = @_;
76     my $tagslib = GetMarcStructure( 1 );
77     for ( my $i = 0 ; $i <= $#{$fields} ; $i++ ) {
78         my ($tag)      = substr( @$fields[$i], 1, 3 );
79         my ($subfield) = substr( @$fields[$i], 4, 1 );
80         @$fields[$i] .= ',' . $tagslib->{$tag}->{$subfield}->{seealso}
81           if ( $tagslib->{$tag}->{$subfield}->{seealso} );
82     }
83 }
84
85 =head2 FindDuplicate
86
87 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
88
89 =cut
90
91 sub FindDuplicate {
92     my ($record) = @_;
93     my $dbh = C4::Context->dbh;
94     my $result = TransformMarcToKoha( $dbh, $record, '' );
95     my $sth;
96     my $query;
97     my $search;
98     my $type;
99     my ( $biblionumber, $title );
100
101     # search duplicate on ISBN, easy and fast..
102     # ... normalize first
103     if ( $result->{isbn} ) {
104         $result->{isbn} =~ s/\(.*$//;
105         $result->{isbn} =~ s/\s+$//; 
106     }
107     #$search->{'avoidquerylog'}=1;
108     if ( $result->{isbn} ) {
109         $query = "isbn=$result->{isbn}";
110     }
111     else {
112         $result->{title} =~ s /\\//g;
113         $result->{title} =~ s /\"//g;
114         $result->{title} =~ s /\(//g;
115         $result->{title} =~ s /\)//g;
116                 # remove valid operators
117                 $result->{title} =~ s/(and|or|not)//g;
118         $query = "ti,ext=$result->{title}";
119         $query .= " and mt=$result->{itemtype}" if ($result->{itemtype});    
120         if ($result->{author}){
121           $result->{author} =~ s /\\//g;
122           $result->{author} =~ s /\"//g;
123           $result->{author} =~ s /\(//g;
124           $result->{author} =~ s /\)//g;
125                   # remove valid operators
126                   $result->{author} =~ s/(and|or|not)//g;
127           $query .= " and au,ext=$result->{author}";
128         }     
129     }
130     my ($error,$searchresults) =
131       SimpleSearch($query); # FIXME :: hardcoded !
132     my @results;
133     foreach my $possible_duplicate_record (@$searchresults) {
134         my $marcrecord =
135           MARC::Record->new_from_usmarc($possible_duplicate_record);
136         my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
137         
138         # FIXME :: why 2 $biblionumber ?
139         if ($result){
140           push @results, $result->{'biblionumber'};
141           push @results, $result->{'title'};
142         }
143     }
144     return @results;  
145 }
146
147 =head2 SimpleSearch
148
149 ($error,$results) = SimpleSearch($query,@servers);
150
151 this function performs a simple search on the catalog using zoom.
152
153 =over 2
154
155 =item C<input arg:>
156
157     * $query could be a simple keyword or a complete CCL query wich is depending on your ccl file.
158     * @servers is optionnal. default one is read on koha.xml
159
160 =item C<Output arg:>
161     * $error is a string which containt the description error if there is one. Else it's empty.
162     * \@results is an array of marc record.
163
164 =item C<usage in the script:>
165
166 =back
167
168 my ($error, $marcresults) = SimpleSearch($query);
169
170 if (defined $error) {
171     $template->param(query_error => $error);
172     warn "error: ".$error;
173     output_html_with_http_headers $input, $cookie, $template->output;
174     exit;
175 }
176
177 my $hits = scalar @$marcresults;
178 my @results;
179
180 for(my $i=0;$i<$hits;$i++) {
181     my %resultsloop;
182     my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
183     my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
184
185     #build the hash for the template.
186     $resultsloop{highlight}       = ($i % 2)?(1):(0);
187     $resultsloop{title}           = $biblio->{'title'};
188     $resultsloop{subtitle}        = $biblio->{'subtitle'};
189     $resultsloop{biblionumber}    = $biblio->{'biblionumber'};
190     $resultsloop{author}          = $biblio->{'author'};
191     $resultsloop{publishercode}   = $biblio->{'publishercode'};
192     $resultsloop{publicationyear} = $biblio->{'publicationyear'};
193
194     push @results, \%resultsloop;
195 }
196 $template->param(result=>\@results);
197
198 =cut
199
200 sub SimpleSearch {
201     my $query   = shift;
202     if (C4::Context->preference('NoZebra')) {
203         my $result = NZorder(NZanalyse($query))->{'biblioserver'}->{'RECORDS'};
204         return (undef,$result);
205     } else {
206         my @servers = @_;
207         my @results;
208         my @tmpresults;
209         my @zconns;
210         return ( "No query entered", undef ) unless $query;
211     
212         #@servers = (C4::Context->config("biblioserver")) unless @servers;
213         @servers =
214         ("biblioserver") unless @servers
215         ;    # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
216     
217         # Connect & Search
218         for ( my $i = 0 ; $i < @servers ; $i++ ) {
219             $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
220             $tmpresults[$i] =
221             $zconns[$i]
222             ->search( new ZOOM::Query::CCL2RPN( $query, $zconns[$i] ) );
223     
224             # getting error message if one occured.
225             my $error =
226                 $zconns[$i]->errmsg() . " ("
227             . $zconns[$i]->errcode() . ") "
228             . $zconns[$i]->addinfo() . " "
229             . $zconns[$i]->diagset();
230     
231             return ( $error, undef ) if $zconns[$i]->errcode();
232         }
233         my $hits;
234         my $ev;
235         while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
236             $ev = $zconns[ $i - 1 ]->last_event();
237             if ( $ev == ZOOM::Event::ZEND ) {
238                 $hits = $tmpresults[ $i - 1 ]->size();
239             }
240             if ( $hits > 0 ) {
241                 for ( my $j = 0 ; $j < $hits ; $j++ ) {
242                     my $record = $tmpresults[ $i - 1 ]->record($j)->raw();
243                     push @results, $record;
244                 }
245             }
246         }
247         return ( undef, \@results );
248     }
249 }
250
251 # performs the search
252 sub getRecords {
253     my (
254         $koha_query,     $federated_query,  $sort_by_ref,
255         $servers_ref,    $results_per_page, $offset,
256         $expanded_facet, $branches,         $query_type,
257         $scan
258     ) = @_;
259 #     warn "Query : $koha_query";
260     my @servers = @$servers_ref;
261     my @sort_by = @$sort_by_ref;
262
263     # create the zoom connection and query object
264     my $zconn;
265     my @zconns;
266     my @results;
267     my $results_hashref = ();
268
269     ### FACETED RESULTS
270     my $facets_counter = ();
271     my $facets_info    = ();
272     my $facets         = getFacets();
273
274     #### INITIALIZE SOME VARS USED CREATE THE FACETED RESULTS
275     my @facets_loop;    # stores the ref to array of hashes for template
276     for ( my $i = 0 ; $i < @servers ; $i++ ) {
277         $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
278
279 # perform the search, create the results objects
280 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
281         my $query_to_use;
282         if ( $servers[$i] =~ /biblioserver/ ) {
283             $query_to_use = $koha_query;
284         }
285         else {
286             $query_to_use = $federated_query;
287         }
288
289         # check if we've got a query_type defined
290         eval {
291             if ($query_type)
292             {
293                 if ( $query_type =~ /^ccl/ ) {
294                     $query_to_use =~
295                       s/\:/\=/g;    # change : to = last minute (FIXME)
296
297                     #                 warn "CCL : $query_to_use";
298                     $results[$i] =
299                       $zconns[$i]->search(
300                         new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
301                       );
302                 }
303                 elsif ( $query_type =~ /^cql/ ) {
304
305                     #                 warn "CQL : $query_to_use";
306                     $results[$i] =
307                       $zconns[$i]->search(
308                         new ZOOM::Query::CQL( $query_to_use, $zconns[$i] ) );
309                 }
310                 elsif ( $query_type =~ /^pqf/ ) {
311
312                     #                 warn "PQF : $query_to_use";
313                     $results[$i] =
314                       $zconns[$i]->search(
315                         new ZOOM::Query::PQF( $query_to_use, $zconns[$i] ) );
316                 }
317             }
318             else {
319                 if ($scan) {
320
321                     #                 warn "preparing to scan";
322                     $results[$i] =
323                       $zconns[$i]->scan(
324                         new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
325                       );
326                 }
327                 else {
328
329                     #             warn "LAST : $query_to_use";
330                     $results[$i] =
331                       $zconns[$i]->search(
332                         new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
333                       );
334                 }
335             }
336         };
337         if ($@) {
338             warn "WARNING: query problem with $query_to_use " . $@;
339         }
340
341         # concatenate the sort_by limits and pass them to the results object
342         my $sort_by;
343         foreach my $sort (@sort_by) {
344             if ($sort eq "author_az") {
345                 $sort_by.="1=1003 <i ";
346             }
347             elsif ($sort eq "author_za") {
348                 $sort_by.="1=1003 >i ";
349             }
350                         elsif ($sort eq "popularity_asc") {
351                                 $sort_by.="1=9003 <i ";
352                         }
353                         elsif ($sort eq "popularity_dsc") {
354                 $sort_by.="1=9003 >i ";
355             }
356                         elsif ($sort eq "call_number_asc") {
357                 $sort_by.="1=20  <i ";
358             }
359                         elsif ($sort eq "call_number_dsc") {
360                 $sort_by.="1=20 >i ";
361             }
362                         elsif ($sort eq "pubdate_asc") {
363                 $sort_by.="1=31 <i ";
364             }
365                         elsif ($sort eq "pubdate_dsc") {
366                 $sort_by.="1=31 >i ";
367             }
368                         elsif ($sort eq "acqdate_asc") {
369                 $sort_by.="1=32 <i ";
370             }
371                         elsif ($sort eq "acqdate_dsc") {
372                 $sort_by.="1=32 >i ";
373             }
374                         elsif ($sort eq "title_az") {
375                 $sort_by.="1=4 <i ";
376             }
377                         elsif ($sort eq "title_za") {
378                 $sort_by.="1=4 >i ";
379             }
380         }
381                 if ($sort_by) {
382                         if ( $results[$i]->sort( "yaz", $sort_by ) < 0) {
383                         warn "WARNING sort $sort_by failed";
384                         }
385                 }
386     }
387     while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
388         my $ev = $zconns[ $i - 1 ]->last_event();
389         if ( $ev == ZOOM::Event::ZEND ) {
390             my $size = $results[ $i - 1 ]->size();
391             if ( $size > 0 ) {
392                 my $results_hash;
393                 #$results_hash->{'server'} = $servers[$i-1];
394                 # loop through the results
395                 $results_hash->{'hits'} = $size;
396                 my $times;
397                 if ( $offset + $results_per_page <= $size ) {
398                     $times = $offset + $results_per_page;
399                 }
400                 else {
401                     $times = $size;
402                 }
403                 for ( my $j = $offset ; $j < $times ; $j++ )
404                 {   #(($offset+$count<=$size) ? ($offset+$count):$size) ; $j++){
405                     my $records_hash;
406                     my $record;
407                     my $facet_record;
408                     ## This is just an index scan
409                     if ($scan) {
410                         my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
411
412                  # here we create a minimal MARC record and hand it off to the
413                  # template just like a normal result ... perhaps not ideal, but
414                  # it works for now
415                         my $tmprecord = MARC::Record->new();
416                         $tmprecord->encoding('UTF-8');
417                         my $tmptitle;
418
419           # srote the minimal record in author/title (depending on MARC flavour)
420                         if ( C4::Context->preference("marcflavour") eq
421                             "UNIMARC" )
422                         {
423                             $tmptitle = MARC::Field->new(
424                                 '200', ' ', ' ',
425                                 a => $term,
426                                 f => $occ
427                             );
428                         }
429                         else {
430                             $tmptitle = MARC::Field->new(
431                                 '245', ' ', ' ',
432                                 a => $term,
433                                 b => $occ
434                             );
435                         }
436                         $tmprecord->append_fields($tmptitle);
437                         $results_hash->{'RECORDS'}[$j] =
438                           $tmprecord->as_usmarc();
439                     }
440                     else {
441                         $record = $results[ $i - 1 ]->record($j)->raw();
442
443                         #warn "RECORD $j:".$record;
444                         $results_hash->{'RECORDS'}[$j] =
445                           $record;    # making a reference to a hash
446                                       # Fill the facets while we're looping
447                         $facet_record = MARC::Record->new_from_usmarc($record);
448
449                         #warn $servers[$i-1].$facet_record->title();
450                         for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
451                             if ( $facets->[$k] ) {
452                                 my @fields;
453                                 for my $tag ( @{ $facets->[$k]->{'tags'} } ) {
454                                     push @fields, $facet_record->field($tag);
455                                 }
456                                 for my $field (@fields) {
457                                     my @subfields = $field->subfields();
458                                     for my $subfield (@subfields) {
459                                         my ( $code, $data ) = @$subfield;
460                                         if ( $code eq
461                                             $facets->[$k]->{'subfield'} )
462                                         {
463                                             $facets_counter->{ $facets->[$k]
464                                                   ->{'link_value'} }->{$data}++;
465                                         }
466                                     }
467                                 }
468                                 $facets_info->{ $facets->[$k]->{'link_value'} }
469                                   ->{'label_value'} =
470                                   $facets->[$k]->{'label_value'};
471                                 $facets_info->{ $facets->[$k]->{'link_value'} }
472                                   ->{'expanded'} = $facets->[$k]->{'expanded'};
473                             }
474                         }
475                     }
476                 }
477                 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
478             }
479
480             #print "connection ", $i-1, ": $size hits";
481             #print $results[$i-1]->record(0)->render() if $size > 0;
482             # BUILD FACETS
483             for my $link_value (
484                 sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
485                 keys %$facets_counter
486               )
487             {
488                 my $expandable;
489                 my $number_of_facets;
490                 my @this_facets_array;
491                 for my $one_facet (
492                     sort {
493                         $facets_counter->{$link_value}
494                           ->{$b} <=> $facets_counter->{$link_value}->{$a}
495                     } keys %{ $facets_counter->{$link_value} }
496                   )
497                 {
498                     $number_of_facets++;
499                     if (   ( $number_of_facets < 6 )
500                         || ( $expanded_facet eq $link_value )
501                         || ( $facets_info->{$link_value}->{'expanded'} ) )
502                     {
503
504                        # sanitize the link value ), ( will cause errors with CCL
505                         my $facet_link_value = $one_facet;
506                         $facet_link_value =~ s/(\(|\))/ /g;
507
508                         # fix the length that will display in the label
509                         my $facet_label_value = $one_facet;
510                         $facet_label_value = substr( $one_facet, 0, 20 ) . "..."
511                           unless length($facet_label_value) <= 20;
512
513                        # well, if it's a branch, label by the name, not the code
514                         if ( $link_value =~ /branch/ ) {
515                             $facet_label_value =
516                               $branches->{$one_facet}->{'branchname'};
517                         }
518
519                  # but we're down with the whole label being in the link's title
520                         my $facet_title_value = $one_facet;
521
522                         push @this_facets_array,
523                           (
524                             {
525                                 facet_count =>
526                                   $facets_counter->{$link_value}->{$one_facet},
527                                 facet_label_value => $facet_label_value,
528                                 facet_title_value => $facet_title_value,
529                                 facet_link_value  => $facet_link_value,
530                                 type_link_value   => $link_value,
531                             },
532                           );
533                     }
534                 }
535                 unless ( $facets_info->{$link_value}->{'expanded'} ) {
536                     $expandable = 1
537                       if ( ( $number_of_facets > 6 )
538                         && ( $expanded_facet ne $link_value ) );
539                 }
540                 push @facets_loop,
541                   (
542                     {
543                         type_link_value => $link_value,
544                         type_id         => $link_value . "_id",
545                         type_label      =>
546                           $facets_info->{$link_value}->{'label_value'},
547                         facets     => \@this_facets_array,
548                         expandable => $expandable,
549                         expand     => $link_value,
550                     }
551                   );
552             }
553         }
554     }
555     return ( undef, $results_hashref, \@facets_loop );
556 }
557
558 # build the query itself
559 sub buildQuery {
560     my ( $query, $operators, $operands, $indexes, $limits, $sort_by ) = @_;
561
562     my @operators = @$operators if $operators;
563     my @indexes   = @$indexes   if $indexes;
564     my @operands  = @$operands  if $operands;
565     my @limits    = @$limits    if $limits;
566     my @sort_by   = @$sort_by   if $sort_by;
567
568     my $human_search_desc;      # a human-readable query
569     my $machine_search_desc;    #a machine-readable query
570
571 # STEP I: determine if this is a form-based / simple query or if it's complex (if complex,
572 # we can't handle field weighting, stemming until a formal query parser is written
573 # I'll work on this soon -- JF
574 #if (!$query) { # form-based
575 # check if this is a known query language query, if it is, return immediately:
576     if ( $query =~ /^ccl=/ ) {
577         return ( undef, $', $', $', 'ccl' );
578     }
579     if ( $query =~ /^cql=/ ) {
580         return ( undef, $', $', $', 'cql' );
581     }
582     if ( $query =~ /^pqf=/ ) {
583         return ( undef, $', $', $', 'pqf' );
584     }
585     if ( $query =~ /(\(|\))/ ) {    # sorry, too complex
586         return ( undef, $query, $query, $query, 'ccl' );
587     }
588
589 # form-based queries are limited to non-nested a specific depth, so we can easily
590 # modify the incoming query operands and indexes to do stemming and field weighting
591 # Once we do so, we'll end up with a value in $query, just like if we had an
592 # incoming $query from the user
593     else {
594         $query = ""
595           ; # clear it out so we can populate properly with field-weighted stemmed query
596         my $previous_operand
597           ;    # a flag used to keep track if there was a previous query
598                # if there was, we can apply the current operator
599         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
600             my $operand = $operands[$i];
601             # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
602             # we use IsAlpha unicode definition, to deal correctly with diacritics.
603             # otherwise, a french word like "leçon" is splitted in "le" "çon", le is an empty word, we get "çon"
604             # and don't find anything...
605             my $stemmed_operand;
606             my $stemming      = C4::Context->preference("QueryStemming")     || 0;
607             my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
608                         my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
609                         
610             # We Have to do this more carefully.
611             #Since Phrase Search Is Phrase search.
612             #phrase "Physics In Collision" will not be found if we do it like that.
613             my $index   = $indexes[$i];
614             my (@nontruncated,@righttruncated,@lefttruncated,@rightlefttruncated,@regexpr);
615
616                         # if the operator contains more than one qualifier, but not phrase
617             if (index($index,"phr")<0 && index($index,",")>0){                  
618               #operand may be a wordlist deleting stopwords      
619               foreach (keys %{C4::Context->stopwords}) {
620                   $operand=~ s/\P{IsAlpha}$_\P{IsAlpha}/ /i;
621                   $operand=~ s/^$_\P{IsAlpha}/ /i;
622                   $operand=~ s/\P{IsAlpha}$_$/ /i;
623               }
624               #now coping with words      
625               my @wordlist= split (/\s/,$operand);
626               foreach my $word (@wordlist){
627                 if (index($word,"*")==0 && index($word,"*",1)==length($word)-2){
628                   $word=~s/\*//;
629                   push @rightlefttruncated,$word;
630                 } elsif(index($word,"*")==0 && index($word,"*",1)<0){        
631                   $word=~s/\*//;
632                   push @lefttruncated,$word;
633                 } elsif (index($word,"*")==length($word)-1){        
634                   $word=~s/\*//;
635                   push @righttruncated,$word;
636                 } elsif (index($word,"*")<0){        
637                   push @nontruncated,$word;
638                 } else {
639                   push @regexpr,$word;
640                 }        
641               }       
642             }      
643             
644             if ( $operands[$i] ) {
645                 $operand =~ s/^(and |or |not )//i;
646
647 # STEMMING FIXME: need to refine the field weighting so stemmed operands don't disrupt the query ranking
648                 if ($stemming) {
649                                 # FIXME: the locale should be set based on the user's language and/or search choice
650                                 my $stemmer = Lingua::Stem->new( -locale => 'EN-US' );
651                                 # FIXME: these should be stored in the db so the librarian can modify the behavior
652                                 $stemmer->add_exceptions(
653                                 {   
654                         'and' => 'and',
655                         'or'  => 'or',
656                         'not' => 'not',
657                                 }
658                                 );
659
660                     my @words = split( / /, $operands[$i] );
661                     my $stems = $stemmer->stem(@words);
662                     foreach my $stem (@$stems) {
663                         $stemmed_operand .= "$stem";
664                         $stemmed_operand .= "?"
665                           unless ( $stem =~ /(and$|or$|not$)/ )
666                           || ( length($stem) < 3 );
667                         $stemmed_operand .= " ";
668                                                 $stemmed_operand =~ s/(and|or|not)//g;
669                         #warn "STEM: $stemmed_operand";
670                     }
671
672                     #$operand = $stemmed_operand;
673                 }
674
675 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
676 # pretty well but will work much better when we have an actual query parser
677                 my $weighted_query;
678                 if ($weight_fields) {
679                     $weighted_query .=
680                       " rk=(";    # Specifies that we're applying rank
681                                   # keyword has different weight properties
682                     if ( ( $index =~ /kw/ ) || ( !$index ) )
683                     { # FIXME: do I need to add right-truncation in the case of stemming?
684                           # a simple way to find out if this query uses an index
685                         if ( $operand =~ /(\=|\:)/ ) {
686                             $weighted_query .= " $operand";
687                         }
688                         else {
689                             $weighted_query .=" Title-cover,ext,r1=\"$operand\"";       # title cover as exact
690                             $weighted_query .=" or ti,ext,r2=\"$operand\"";                     # exact title elsewhere
691                             #$weighted_query .= " or ti,phr,r3=$operand";          # index as phrase
692                             #$weighted_query .= " or any,ext,r4=$operand";         # index as exact
693                             $weighted_query .=" or kw,wrdl,r5=\"$operand\"";            # all the words in the query (wordlist)
694                             $weighted_query .= " or wrd,fuzzy,r9=$operand" if $fuzzy_enabled; # add fuzzy
695                             $weighted_query .= " or wrd,right-Truncation=$stemmed_operand" if $stemming; # add stemming
696                                                         # embedded sorting: 0 a-z; 1 z-a
697                                                         #$weighted_query .= ") or (sort1,aut=1";
698                         }
699                     }
700                     elsif ( $index =~ /au/ ) {
701                         $weighted_query .=
702                           " $index,ext,r1=$operand";    # index label as exact
703                          #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
704                         $weighted_query .=
705                           " or $index,phr,r3=$operand";    # index as phrase
706                         $weighted_query .= " or $index,rt,wrd,r3=$operand";
707                     }
708                     elsif ( $index =~ /ti/ ) {
709                         $weighted_query .=
710                           " Title-cover,ext,r1=$operand"; # index label as exact
711                         $weighted_query .= " or Title-series,ext,r2=$operand";
712
713                         #$weighted_query .= " or ti,ext,r2=$operand";
714                         #$weighted_query .= " or ti,phr,r3=$operand";
715                         #$weighted_query .= " or ti,wrd,r3=$operand";
716                         $weighted_query .=" or (title-sort-az=0 or Title-cover,startswithnt,st-word,r3=$operand #)";
717                         $weighted_query .=" or (title-sort-az=0 or Title-cover,phr,r6=$operand)";
718
719                         #$weighted_query .= " or Title-cover,wrd,r5=$operand";
720                         #$weighted_query .= " or ti,ext,r6=$operand";
721                         #$weighted_query .= " or ti,startswith,phr,r7=$operand";
722                         #$weighted_query .= " or ti,phr,r8=$operand";
723                         #$weighted_query .= " or ti,wrd,r9=$operand";
724
725                                                 #$weighted_query .= " or ti,ext,r2=$operand";         # index as exact
726                                                 #$weighted_query .= " or ti,phr,r3=$operand";              # index as  phrase
727                                                 #$weighted_query .= " or any,ext,r4=$operand";         # index as exact
728                                                 #$weighted_query .= " or kw,wrd,r5=$operand";         # index as exact
729                     }
730                     else { 
731                         $weighted_query .=
732                           " $index,ext,r1=$operand";    # index label as exact
733                          #$weighted_query .= " or $index,ext,r2=$operand";            # index as exact
734                         $weighted_query .=
735                           " or $index,phr,r3=$operand";    # index as phrase
736                         $weighted_query .= " or $index,rt,wrd,r3=$operand";
737                         $weighted_query .=
738                           " or $index,wrd,r5=$operand"
739                           ;    # index as word right-truncated
740                         $weighted_query .= " or $index,wrd,fuzzy,r8=$operand";
741                     }
742                     $weighted_query .= ")";    # close rank specification
743                     $operand = $weighted_query;
744                 }
745
746                 # only add an operator if there is a previous operand
747                 if ($previous_operand) {
748                     if ( $operators[ $i - 1 ] ) {
749                         $query .= " $operators[$i-1] $index: $operand";
750                         if ( !$index ) {
751                             $human_search_desc .=
752                               "  $operators[$i-1] $operands[$i]";
753                         }
754                         else {
755                             $human_search_desc .=
756                               "  $operators[$i-1] $index: $operands[$i]";
757                         }
758                     }
759
760                     # the default operator is and
761                     else {
762                         $query             .= " and $index: $operand";
763                         $human_search_desc .= "  and $index: $operands[$i]";
764                     }
765                 }
766                 else {
767                     if ( !$index ) {
768                         $query             .= " $operand";
769                         $human_search_desc .= "  $operands[$i]";
770                     }
771                     else {
772                       if (scalar(@righttruncated)+scalar(@lefttruncated)+scalar(@rightlefttruncated)>0){
773                          $query.= "$index: @nontruncated " if (scalar(@nontruncated)>0);
774                          if (scalar(@righttruncated)>0){
775                           $query .= "and $index,rtrn:@righttruncated ";
776                          }            
777                          if (scalar(@lefttruncated)>0){
778                           $query .= "and $index,ltrn:@lefttruncated ";
779                          }            
780                          if (scalar(@rightlefttruncated)>0){
781                           $query .= "and $index,rltrn:@rightlefttruncated ";
782                          }
783                         $query=~s/^and//;
784                         $human_search_desc .= $query;
785                       } else {           
786                         $query             .= " $index: $operand";
787                         $human_search_desc .= "  $index: $operands[$i]";
788                       }            
789                     }
790                     $previous_operand = 1;
791                 }
792             }    #/if $operands
793         }    # /for
794     }
795
796     # add limits
797     my $limit_query;
798     my $limit_search_desc;
799     foreach my $limit (@limits) {
800
801         # FIXME: not quite right yet ... will work on this soon -- JF
802         my $type = $1 if $limit =~ m/([^:]+):([^:]*)/;
803         if ( $limit =~ /available/ ) {
804             $limit_query .= " (($query and datedue=0000-00-00) or ($query and datedue=0000-00-00 not lost=1) or ($query and datedue=0000-00-00 not lost=2))";
805             #$limit_search_desc.=" and available";
806         }
807         elsif ( ($limit_query) && ( index( $limit_query, $type, 0 ) > 0 ) ) {
808             if ( $limit_query !~ /\(/ ) {
809                 $limit_query =
810                     substr( $limit_query, 0, index( $limit_query, $type, 0 ) )
811                   . "("
812                   . substr( $limit_query, index( $limit_query, $type, 0 ) )
813                   . " or $limit )"
814                   if $limit;
815                 $limit_search_desc =
816                   substr( $limit_search_desc, 0,
817                     index( $limit_search_desc, $type, 0 ) )
818                   . "("
819                   . substr( $limit_search_desc,
820                     index( $limit_search_desc, $type, 0 ) )
821                   . " or $limit )"
822                   if $limit;
823             }
824             else {
825                 chop $limit_query;
826                 chop $limit_search_desc;
827                 $limit_query       .= " or $limit )" if $limit;
828                 $limit_search_desc .= " or $limit )" if $limit;
829             }
830         }
831         elsif ( ($limit_query) && ( $limit =~ /mc/ ) ) {
832             $limit_query       .= " or $limit" if $limit;
833             $limit_search_desc .= " or $limit" if $limit;
834         }
835
836         # these are treated as AND
837         elsif ($limit_query) {
838            if ($limit =~ /branch/){
839                         $limit_query       .= " ) and ( $limit" if $limit;
840                         $limit_search_desc .= " ) and ( $limit" if $limit;
841                 }else{
842                         $limit_query       .= " or $limit" if $limit;
843                         $limit_search_desc .= " or $limit" if $limit;
844                 }
845         }
846
847         # otherwise, there is nothing but the limit
848         else {
849             $limit_query       .= "$limit" if $limit;
850             $limit_search_desc .= "$limit" if $limit;
851         }
852     }
853
854     # if there's also a query, we need to AND the limits to it
855     if ( ($limit_query) && ($query) ) {
856         $limit_query       = " and (" . $limit_query . ")";
857         $limit_search_desc = " and ($limit_search_desc)" if $limit_search_desc;
858
859     }
860         warn "LIMIT: $limit_query";
861     $query             .= $limit_query;
862     $human_search_desc .= $limit_search_desc;
863
864     # now normalize the strings
865     $query =~ s/  / /g;    # remove extra spaces
866     $query =~ s/^ //g;     # remove any beginning spaces
867     $query =~ s/:/=/g;     # causes probs for server
868     $query =~ s/==/=/g;    # remove double == from query
869
870     my $federated_query = $human_search_desc;
871     $federated_query =~ s/  / /g;
872     $federated_query =~ s/^ //g;
873     $federated_query =~ s/:/=/g;
874     my $federated_query_opensearch = $federated_query;
875
876 #     my $federated_query_RPN = new ZOOM::Query::CCL2RPN( $query , C4::Context->ZConn('biblioserver'));
877
878     $human_search_desc =~ s/  / /g;
879     $human_search_desc =~ s/^ //g;
880     my $koha_query = $query;
881
882     #warn "QUERY:".$koha_query;
883     #warn "SEARCHDESC:".$human_search_desc;
884     #warn "FEDERATED QUERY:".$federated_query;
885     return ( undef, $human_search_desc, $koha_query, $federated_query );
886 }
887
888 # IMO this subroutine is pretty messy still -- it's responsible for
889 # building the HTML output for the template
890 sub searchResults {
891     my ( $searchdesc, $hits, $results_per_page, $offset, @marcresults ) = @_;
892
893     my $dbh = C4::Context->dbh;
894     my $toggle;
895     my $even = 1;
896     my @newresults;
897     my $span_terms_hashref;
898     for my $span_term ( split( / /, $searchdesc ) ) {
899         $span_term =~ s/(.*=|\)|\(|\+|\.)//g;
900         $span_terms_hashref->{$span_term}++;
901     }
902
903     #Build brancnames hash
904     #find branchname
905     #get branch information.....
906     my %branches;
907     my $bsth =
908       $dbh->prepare("SELECT branchcode,branchname FROM branches")
909       ;    # FIXME : use C4::Koha::GetBranches
910     $bsth->execute();
911     while ( my $bdata = $bsth->fetchrow_hashref ) {
912         $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
913     }
914
915     #Build itemtype hash
916     #find itemtype & itemtype image
917     my %itemtypes;
918     $bsth =
919       $dbh->prepare("SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes");
920     $bsth->execute();
921     while ( my $bdata = $bsth->fetchrow_hashref ) {
922         $itemtypes{ $bdata->{'itemtype'} }->{description} =
923           $bdata->{'description'};
924         $itemtypes{ $bdata->{'itemtype'} }->{imageurl} = $bdata->{'imageurl'};
925         $itemtypes{ $bdata->{'itemtype'} }->{summary} = $bdata->{'summary'};
926         $itemtypes{ $bdata->{'itemtype'} }->{notforloan} = $bdata->{'notforloan'};
927     }
928
929     #search item field code
930     my $sth =
931       $dbh->prepare(
932 "select tagfield from marc_subfield_structure where kohafield like 'items.itemnumber'"
933       );
934     $sth->execute;
935     my ($itemtag) = $sth->fetchrow;
936
937     ## find column names of items related to MARC
938     my $sth2 = $dbh->prepare("SHOW COLUMNS from items");
939     $sth2->execute;
940     my %subfieldstosearch;
941     while ( ( my $column ) = $sth2->fetchrow ) {
942         my ( $tagfield, $tagsubfield ) =
943           &GetMarcFromKohaField( "items." . $column, "" );
944         $subfieldstosearch{$column} = $tagsubfield;
945     }
946     my $times;
947
948     if ( $hits && $offset + $results_per_page <= $hits ) {
949         $times = $offset + $results_per_page;
950     }
951     else {
952         $times = $hits;
953     }
954
955     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
956         my $marcrecord;
957         $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
958         my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, '' );
959         # add image url if there is one
960         if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} =~ /^http:/ ) {
961             $oldbiblio->{imageurl} =
962               $itemtypes{ $oldbiblio->{itemtype} }->{imageurl};
963             $oldbiblio->{description} =
964               $itemtypes{ $oldbiblio->{itemtype} }->{description};
965         }
966         else {
967             $oldbiblio->{imageurl} =
968               getitemtypeimagesrc() . "/"
969               . $itemtypes{ $oldbiblio->{itemtype} }->{imageurl}
970               if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
971             $oldbiblio->{description} =
972               $itemtypes{ $oldbiblio->{itemtype} }->{description};
973         }
974         #
975         # build summary if there is one (the summary is defined in itemtypes table
976         #
977         if ($itemtypes{ $oldbiblio->{itemtype} }->{summary}) {
978             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
979             my @fields = $marcrecord->fields();
980             foreach my $field (@fields) {
981                 my $tag = $field->tag();
982                 my $tagvalue = $field->as_string();
983                 $summary =~ s/\[(.?.?.?.?)$tag\*(.*?)]/$1$tagvalue$2\[$1$tag$2]/g;
984                 unless ($tag<10) {
985                     my @subf = $field->subfields;
986                     for my $i (0..$#subf) {
987                         my $subfieldcode = $subf[$i][0];
988                         my $subfieldvalue = $subf[$i][1];
989                         my $tagsubf = $tag.$subfieldcode;
990                         $summary =~ s/\[(.?.?.?.?)$tagsubf(.*?)]/$1$subfieldvalue$2\[$1$tagsubf$2]/g;
991                     }
992                 }
993             }
994             $summary =~ s/\[(.*?)]//g;
995             $summary =~ s/\n/<br>/g;
996             $oldbiblio->{summary} = $summary;
997         }
998         # add spans to search term in results
999         foreach my $term ( keys %$span_terms_hashref ) {
1000
1001             #warn "term: $term";
1002             my $old_term = $term;
1003             if ( length($term) > 3 ) {
1004                 $term =~ s/(.*=|\)|\(|\+|\.|\?|\[|\])//g;
1005                                 $term =~ s/\\//g;
1006
1007                 #FIXME: is there a better way to do this?
1008                 $oldbiblio->{'title'} =~ s/$term/<span class=term>$&<\/span>/gi;
1009                 $oldbiblio->{'subtitle'} =~
1010                   s/$term/<span class=term>$&<\/span>/gi;
1011
1012                 $oldbiblio->{'author'} =~ s/$term/<span class=term>$&<\/span>/gi;
1013                 $oldbiblio->{'publishercode'} =~ s/$term/<span class=term>$&<\/span>/gi;
1014                 $oldbiblio->{'place'} =~ s/$term/<span class=term>$&<\/span>/gi;
1015                 $oldbiblio->{'pages'} =~ s/$term/<span class=term>$&<\/span>/gi;
1016                 $oldbiblio->{'notes'} =~ s/$term/<span class=term>$&<\/span>/gi;
1017                 $oldbiblio->{'size'}  =~ s/$term/<span class=term>$&<\/span>/gi;
1018             }
1019         }
1020
1021         if ( $i % 2 ) {
1022             $toggle = "#ffffcc";
1023         }
1024         else {
1025             $toggle = "white";
1026         }
1027         $oldbiblio->{'toggle'} = $toggle;
1028         my @fields = $marcrecord->field($itemtag);
1029         my @items_loop;
1030         my $items;
1031         my $ordered_count     = 0;
1032         my $onloan_count      = 0;
1033         my $wthdrawn_count    = 0;
1034         my $itemlost_count    = 0;
1035         my $norequests        = 1;
1036
1037         #
1038         # check the loan status of the item : 
1039         # it is not stored in the MARC record, for pref (zebra reindexing)
1040         # reason. Thus, we have to get the status from a specific SQL query
1041         #
1042         my $sth_issue = $dbh->prepare("
1043             SELECT date_due,returndate 
1044             FROM issues 
1045             WHERE itemnumber=? AND returndate IS NULL");
1046         my $items_count=scalar(@fields);
1047         foreach my $field (@fields) {
1048             my $item;
1049             foreach my $code ( keys %subfieldstosearch ) {
1050                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1051             }
1052             $sth_issue->execute($item->{itemnumber});
1053             $item->{due_date} = format_date($sth_issue->fetchrow);
1054             $item->{onloan} = 1 if $item->{due_date};
1055             # at least one item can be reserved : suppose no
1056             $norequests = 1;
1057             if ( $item->{wthdrawn} ) {
1058                 $wthdrawn_count++;
1059                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1060                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{wthdrawn}=1;
1061             }
1062             elsif ( $item->{itemlost} ) {
1063                 $itemlost_count++;
1064                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1065                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{itemlost}=1;
1066             }
1067             unless ( $item->{notforloan}) {
1068                 # OK, this one can be issued, so at least one can be reserved
1069                 $norequests = 0;
1070             }
1071             if ( ( $item->{onloan} ) && ( $item->{onloan} != '0000-00-00' ) )
1072             {
1073                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1074                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{onloancount} = 1;
1075                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{due_date} = $item->{due_date};
1076                 $onloan_count++;
1077             }
1078             if ( $item->{'homebranch'} ) {
1079                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{count}++;
1080             }
1081
1082             # Last resort
1083             elsif ( $item->{'holdingbranch'} ) {
1084                 $items->{ $item->{'holdingbranch'} }->{count}++;
1085             }
1086             $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{itemcallnumber} =                $item->{itemcallnumber};
1087             $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{location} =                $item->{location};
1088             $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{branchcode} =               $item->{homebranch};
1089         }    # notforloan, item level and biblioitem level
1090
1091         # last check for norequest : if itemtype is notforloan, it can't be reserved either, whatever the items
1092         $norequests = 1 if $itemtypes{$oldbiblio->{itemtype}}->{notforloan};
1093
1094         for my $key ( sort keys %$items ) {
1095             my $this_item = {
1096                 branchname     => $branches{$items->{$key}->{branchcode}},
1097                 branchcode     => $items->{$key}->{branchcode},
1098                 count          => $items->{$key}->{count},
1099                 itemcallnumber => $items->{$key}->{itemcallnumber},
1100                 location => $items->{$key}->{location},
1101                 onloancount      => $items->{$key}->{onloancount},
1102                 due_date         => $items->{$key}->{due_date},
1103                 wthdrawn      => $items->{$key}->{wthdrawn},
1104                 lost         => $items->{$key}->{itemlost},
1105             };
1106             push @items_loop, $this_item;
1107         }
1108         $oldbiblio->{norequests}    = $norequests;
1109         $oldbiblio->{items_count}    = $items_count;
1110         $oldbiblio->{items_loop}    = \@items_loop;
1111         $oldbiblio->{onloancount}   = $onloan_count;
1112         $oldbiblio->{wthdrawncount} = $wthdrawn_count;
1113         $oldbiblio->{itemlostcount} = $itemlost_count;
1114         $oldbiblio->{orderedcount}  = $ordered_count;
1115         $oldbiblio->{isbn}          =~ s/-//g; # deleting - in isbn to enable amazon content 
1116         push( @newresults, $oldbiblio );
1117     }
1118     return @newresults;
1119 }
1120
1121
1122
1123 #----------------------------------------------------------------------
1124 #
1125 # Non-Zebra GetRecords#
1126 #----------------------------------------------------------------------
1127
1128 =head2 NZgetRecords
1129
1130   NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1131
1132 =cut
1133
1134 sub NZgetRecords {
1135     my (
1136         $koha_query,     $federated_query,  $sort_by_ref,
1137         $servers_ref,    $results_per_page, $offset,
1138         $expanded_facet, $branches,         $query_type,
1139         $scan
1140     ) = @_;
1141     my $result = NZanalyse($koha_query);
1142     return (undef,NZorder($result,@$sort_by_ref[0],$results_per_page,$offset),undef);
1143 }
1144
1145 =head2 NZanalyse
1146
1147   NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1148   the list is builded from inverted index in nozebra SQL table
1149   note that title is here only for convenience : the sorting will be very fast when requested on title
1150   if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1151
1152 =cut
1153
1154 sub NZanalyse {
1155     my ($string,$server) = @_;
1156     # $server contains biblioserver or authorities, depending on what we search on.
1157     #warn "querying : $string on $server";
1158     $server='biblioserver' unless $server;
1159     # if we have a ", replace the content to discard temporarily any and/or/not inside
1160     my $commacontent;
1161     if ($string =~/"/) {
1162         $string =~ s/"(.*?)"/__X__/;
1163         $commacontent = $1;
1164 #         print "commacontent : $commacontent\n";
1165     }
1166     # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1167     # then, call again NZanalyse with $left and $right
1168     # (recursive until we find a leaf (=> something without and/or/not)
1169     $string =~ /(.*)( and | or | not | AND | OR | NOT )(.*)/;
1170     my $left = $1;
1171     my $right = $3;
1172     my $operand = lc($2);
1173     # it's not a leaf, we have a and/or/not
1174     if ($operand) {
1175         # reintroduce comma content if needed
1176         $right =~ s/__X__/"$commacontent"/ if $commacontent;
1177         $left =~ s/__X__/"$commacontent"/ if $commacontent;
1178 #         warn "node : $left / $operand / $right\n";
1179         my $leftresult = NZanalyse($left,$server);
1180         my $rightresult = NZanalyse($right,$server);
1181         # OK, we have the results for right and left part of the query
1182         # depending of operand, intersect, union or exclude both lists
1183         # to get a result list
1184         if ($operand eq ' and ') {
1185             my @leftresult = split /;/, $leftresult;
1186 #             my @rightresult = split /;/,$leftresult;
1187             my $finalresult;
1188             # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1189             # the result is stored twice, to have the same weight for AND than OR.
1190             # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1191             # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1192             foreach (@leftresult) {
1193                 if ($rightresult =~ "$_;") {
1194                     $finalresult .= "$_;$_;";
1195                 }
1196             }
1197             return $finalresult;
1198         } elsif ($operand eq ' or ') {
1199             # just merge the 2 strings
1200             return $leftresult.$rightresult;
1201         } elsif ($operand eq ' not ') {
1202             my @leftresult = split /;/, $leftresult;
1203 #             my @rightresult = split /;/,$leftresult;
1204             my $finalresult;
1205             foreach (@leftresult) {
1206                 unless ($rightresult =~ "$_;") {
1207                     $finalresult .= "$_;";
1208                 }
1209             }
1210             return $finalresult;
1211         } else {
1212             # this error is impossible, because of the regexp that isolate the operand, but just in case...
1213             die "error : operand unknown : $operand for $string";
1214         }
1215     # it's a leaf, do the real SQL query and return the result
1216     } else {
1217         $string =~  s/__X__/"$commacontent"/ if $commacontent;
1218         $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|<|>|&|\+|\*|\// /g;
1219 #         warn "leaf : $string\n";
1220         # parse the string in in operator/operand/value again
1221         $string =~ /(.*)(=|>|>=|<|<=)(.*)/;
1222         my $left = $1;
1223         my $operator = $2;
1224         my $right = $3;
1225         my $results;
1226         # automatic replace for short operators
1227         $left='title' if $left eq 'ti';
1228         $left='author' if $left eq 'au';
1229         $left='publisher' if $left eq 'pb';
1230         $left='subject' if $left eq 'su';
1231         $left='koha-Auth-Number' if $left eq 'an';
1232         $left='keyword' if $left eq 'kw';
1233         if ($operator) {
1234             #do a specific search
1235             my $dbh = C4::Context->dbh;
1236             $operator='LIKE' if $operator eq '=' and $right=~ /%/;
1237             my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE server=? AND indexname=? AND value $operator ?");
1238             # warn "$left / $operator / $right\n";
1239             # split each word, query the DB and build the biblionumbers result
1240             foreach (split / /,$right) {
1241                 my $biblionumbers;
1242                 next unless $_;
1243 #                 warn "EXECUTE : $server, $left, $_";
1244                 $sth->execute($server, $left, $_);
1245                 while (my $line = $sth->fetchrow) {
1246                     $biblionumbers .= $line;
1247 #                     warn "result : $line";
1248                 }
1249                 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1250                 if ($results) {
1251                     my @leftresult = split /;/, $biblionumbers;
1252                     my $temp;
1253                     foreach (@leftresult) {
1254                         if ($results =~ "$_;") {
1255                             $temp .= "$_;$_;";
1256                         }
1257                     }
1258                     $results = $temp;
1259                 } else {
1260                     $results = $biblionumbers;
1261                 }
1262             }
1263         } else {
1264             #do a complete search (all indexes)
1265             my $dbh = C4::Context->dbh;
1266             my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?");
1267             # split each word, query the DB and build the biblionumbers result
1268             foreach (split / /,$string) {
1269                 next if C4::Context->stopwords->{uc($_)}; # skip if stopword
1270                 #warn "search on all indexes on $_";
1271                 my $biblionumbers;
1272                 next unless $_;
1273                 $sth->execute($server, $_);
1274                 while (my $line = $sth->fetchrow) {
1275                     $biblionumbers .= $line;
1276                 }
1277                 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1278                 if ($results) {
1279                     my @leftresult = split /;/, $biblionumbers;
1280                     my $temp;
1281                     foreach (@leftresult) {
1282                         if ($results =~ "$_;") {
1283                             $temp .= "$_;$_;";
1284                         }
1285                     }
1286                     $results = $temp;
1287                 } else {
1288                     $results = $biblionumbers;
1289                 }
1290             }
1291         }
1292 #         warn "return : $results for LEAF : $string";
1293         return $results;
1294     }
1295 }
1296
1297 =head2 NZorder
1298
1299   $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
1300   
1301   TODO :: Description
1302
1303 =cut
1304
1305
1306 sub NZorder {
1307     my ($biblionumbers, $ordering,$results_per_page,$offset) = @_;
1308     # order title asc by default
1309 #     $ordering = '1=36 <i' unless $ordering;
1310     $results_per_page=20 unless $results_per_page;
1311     $offset = 0 unless $offset;
1312     my $dbh = C4::Context->dbh;
1313     #
1314     # order by POPULARITY
1315     #
1316     if ($ordering =~ /1=9523/) {
1317         my %result;
1318         my %popularity;
1319         # popularity is not in MARC record, it's builded from a specific query
1320         my $sth = $dbh->prepare("select sum(issues) from items where biblionumber=?");
1321         foreach (split /;/,$biblionumbers) {
1322             my ($biblionumber,$title) = split /,/,$_;
1323             $result{$biblionumber}=GetMarcBiblio($biblionumber);
1324             $sth->execute($biblionumber);
1325             my $popularity= $sth->fetchrow ||0;
1326             # hint : the key is popularity.title because we can have
1327             # many results with the same popularity. In this cas, sub-ordering is done by title
1328             # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
1329             # (un-frequent, I agree, but we won't forget anything that way ;-)
1330             $popularity{sprintf("%10d",$popularity).$title.$biblionumber} = $biblionumber;
1331         }
1332         # sort the hash and return the same structure as GetRecords (Zebra querying)
1333         my $result_hash;
1334         my $numbers=0;
1335         if ($ordering eq '1=9523 >i') { # sort popularity DESC
1336             foreach my $key (sort {$b cmp $a} (keys %popularity)) {
1337                 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1338             }
1339         } else { # sort popularity ASC
1340             foreach my $key (sort (keys %popularity)) {
1341                 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1342             }
1343         }
1344         my $finalresult=();
1345         $result_hash->{'hits'} = $numbers;
1346         $finalresult->{'biblioserver'} = $result_hash;
1347         return $finalresult;
1348     #
1349     # ORDER BY author
1350     #
1351     } elsif ($ordering eq '1=1003 <i'){
1352         my %result;
1353         foreach (split /;/,$biblionumbers) {
1354             my ($biblionumber,$title) = split /,/,$_;
1355             my $record=GetMarcBiblio($biblionumber);
1356             my $author;
1357             if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1358                 $author=$record->subfield('200','f');
1359                 $author=$record->subfield('700','a') unless $author;
1360             } else {
1361                 $author=$record->subfield('100','a');
1362             }
1363             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1364             # and we don't want to get only 1 result for each of them !!!
1365             $result{$author.$biblionumber}=$record;
1366         }
1367         # sort the hash and return the same structure as GetRecords (Zebra querying)
1368         my $result_hash;
1369         my $numbers=0;
1370         if ($ordering eq '1=1003 <i') { # sort by author desc
1371             foreach my $key (sort (keys %result)) {
1372                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1373             }
1374         } else { # sort by author ASC
1375             foreach my $key (sort { $a cmp $b } (keys %result)) {
1376                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1377             }
1378         }
1379         my $finalresult=();
1380         $result_hash->{'hits'} = $numbers;
1381         $finalresult->{'biblioserver'} = $result_hash;
1382         return $finalresult;
1383     #
1384     # ORDER BY callnumber
1385     #
1386     } elsif ($ordering eq '1=20 <i'){
1387         my %result;
1388         foreach (split /;/,$biblionumbers) {
1389             my ($biblionumber,$title) = split /,/,$_;
1390             my $record=GetMarcBiblio($biblionumber);
1391             my $callnumber;
1392             my ($callnumber_tag,$callnumber_subfield)=GetMarcFromKohaField($dbh,'items.itemcallnumber');
1393             ($callnumber_tag,$callnumber_subfield)= GetMarcFromKohaField('biblioitems.callnumber') unless $callnumber_tag;
1394             if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1395                 $callnumber=$record->subfield('200','f');
1396             } else {
1397                 $callnumber=$record->subfield('100','a');
1398             }
1399             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1400             # and we don't want to get only 1 result for each of them !!!
1401             $result{$callnumber.$biblionumber}=$record;
1402         }
1403         # sort the hash and return the same structure as GetRecords (Zebra querying)
1404         my $result_hash;
1405         my $numbers=0;
1406         if ($ordering eq '1=1003 <i') { # sort by title desc
1407             foreach my $key (sort (keys %result)) {
1408                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1409             }
1410         } else { # sort by title ASC
1411             foreach my $key (sort { $a cmp $b } (keys %result)) {
1412                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1413             }
1414         }
1415         my $finalresult=();
1416         $result_hash->{'hits'} = $numbers;
1417         $finalresult->{'biblioserver'} = $result_hash;
1418         return $finalresult;
1419     } elsif ($ordering =~ /1=31/){ #pub year
1420         my %result;
1421         foreach (split /;/,$biblionumbers) {
1422             my ($biblionumber,$title) = split /,/,$_;
1423             my $record=GetMarcBiblio($biblionumber);
1424             my ($publicationyear_tag,$publicationyear_subfield)=GetMarcFromKohaField($dbh,'biblioitems.publicationyear');
1425             my $publicationyear=$record->subfield($publicationyear_tag,$publicationyear_subfield);
1426             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1427             # and we don't want to get only 1 result for each of them !!!
1428             $result{$publicationyear.$biblionumber}=$record;
1429         }
1430         # sort the hash and return the same structure as GetRecords (Zebra querying)
1431         my $result_hash;
1432         my $numbers=0;
1433         if ($ordering eq '1=31 <i') { # sort by pubyear desc
1434             foreach my $key (sort (keys %result)) {
1435                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1436             }
1437         } else { # sort by pub year ASC
1438             foreach my $key (sort { $b cmp $a } (keys %result)) {
1439                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1440             }
1441         }
1442         my $finalresult=();
1443         $result_hash->{'hits'} = $numbers;
1444         $finalresult->{'biblioserver'} = $result_hash;
1445         return $finalresult;
1446     #
1447     # ORDER BY title
1448     #
1449     } elsif ($ordering =~ /1=4/) { 
1450         # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
1451         my %result;
1452         foreach (split /;/,$biblionumbers) {
1453             my ($biblionumber,$title) = split /,/,$_;
1454             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1455             # and we don't want to get only 1 result for each of them !!!
1456             # hint & speed improvement : we can order without reading the record
1457             # so order, and read records only for the requested page !
1458             $result{$title.$biblionumber}=$biblionumber;
1459         }
1460         # sort the hash and return the same structure as GetRecords (Zebra querying)
1461         my $result_hash;
1462         my $numbers=0;
1463         if ($ordering eq '1=4 <i') { # sort by title desc
1464             foreach my $key (sort (keys %result)) {
1465                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1466             }
1467         } else { # sort by title ASC
1468             foreach my $key (sort { $b cmp $a } (keys %result)) {
1469                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1470             }
1471         }
1472         # limit the $results_per_page to result size if it's more
1473         $results_per_page = $numbers-1 if $numbers < $results_per_page;
1474         # for the requested page, replace biblionumber by the complete record
1475         # speed improvement : avoid reading too much things
1476         for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1477             $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1478         }
1479         my $finalresult=();
1480         $result_hash->{'hits'} = $numbers;
1481         $finalresult->{'biblioserver'} = $result_hash;
1482         return $finalresult;
1483     } else {
1484     #
1485     # order by ranking
1486     #
1487         # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
1488         my %result;
1489         my %count_ranking;
1490         foreach (split /;/,$biblionumbers) {
1491             my ($biblionumber,$title) = split /,/,$_;
1492             $title =~ /(.*)-(\d)/;
1493             # get weight 
1494             my $ranking =$2;
1495             # note that we + the ranking because ranking is calculated on weight of EACH term requested.
1496             # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
1497             # biblio N has ranking = 6
1498             $count_ranking{$biblionumber} += $ranking;
1499         }
1500         # build the result by "inverting" the count_ranking hash
1501         # 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
1502 #         warn "counting";
1503         foreach (keys %count_ranking) {
1504             $result{sprintf("%10d",$count_ranking{$_}).'-'.$_} = $_;
1505         }
1506         # sort the hash and return the same structure as GetRecords (Zebra querying)
1507         my $result_hash;
1508         my $numbers=0;
1509             foreach my $key (sort {$b cmp $a} (keys %result)) {
1510                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1511             }
1512         # limit the $results_per_page to result size if it's more
1513         $results_per_page = $numbers-1 if $numbers < $results_per_page;
1514         # for the requested page, replace biblionumber by the complete record
1515         # speed improvement : avoid reading too much things
1516         for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1517             $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1518         }
1519         my $finalresult=();
1520         $result_hash->{'hits'} = $numbers;
1521         $finalresult->{'biblioserver'} = $result_hash;
1522         return $finalresult;
1523     }
1524 }
1525 =head2 ModBiblios
1526
1527 ($countchanged,$listunchanged) = ModBiblios($listbiblios, $tagsubfield,$initvalue,$targetvalue,$test);
1528
1529 this function changes all the values $initvalue in subfield $tag$subfield in any record in $listbiblios
1530 test parameter if set donot perform change to records in database.
1531
1532 =over 2
1533
1534 =item C<input arg:>
1535
1536     * $listbiblios is an array ref to marcrecords to be changed
1537     * $tagsubfield is the reference of the subfield to change.
1538     * $initvalue is the value to search the record for
1539     * $targetvalue is the value to set the subfield to
1540     * $test is to be set only not to perform changes in database.
1541
1542 =item C<Output arg:>
1543     * $countchanged counts all the changes performed.
1544     * $listunchanged contains the list of all the biblionumbers of records unchanged.
1545
1546 =item C<usage in the script:>
1547
1548 =back
1549
1550 my ($countchanged, $listunchanged) = EditBiblios($results->{RECORD}, $tagsubfield,$initvalue,$targetvalue);;
1551 #If one wants to display unchanged records, you should get biblios foreach @$listunchanged 
1552 $template->param(countchanged => $countchanged, loopunchanged=>$listunchanged);
1553
1554 =cut
1555
1556 sub ModBiblios{
1557   my ($listbiblios,$tagsubfield,$initvalue,$targetvalue,$test)=@_;
1558   my $countmatched;
1559   my @unmatched;
1560   my ($tag,$subfield)=($1,$2) if ($tagsubfield=~/^(\d{1,3})([a-z0-9A-Z@])?$/); 
1561   if ((length($tag)<3)&& $subfield=~/0-9/){
1562     $tag=$tag.$subfield;
1563     undef $subfield;
1564   } 
1565   my ($bntag,$bnsubf) = GetMarcFromKohaField('biblio.biblionumber');
1566   my ($itemtag,$itemsubf) = GetMarcFromKohaField('items.itemnumber');
1567   foreach my $usmarc (@$listbiblios){
1568     my $record; 
1569     $record=eval{MARC::Record->new_from_usmarc($usmarc)};
1570     my $biblionumber;
1571     if ($@){
1572       # usmarc is not a valid usmarc May be a biblionumber
1573       if ($tag eq $itemtag){
1574         my $bib=GetBiblioFromItemNumber($usmarc);   
1575         $record=GetMarcItem($bib->{'biblionumber'},$usmarc) ;   
1576         $biblionumber=$bib->{'biblionumber'};
1577       } else {   
1578         $record=GetMarcBiblio($usmarc);   
1579         $biblionumber=$usmarc;
1580       }   
1581     }  else {
1582       if ($bntag >= 010){
1583         $biblionumber = $record->subfield($bntag,$bnsubf);
1584       }else {
1585         $biblionumber=$record->field($bntag)->data;
1586       }
1587     }  
1588     #GetBiblionumber is to be written.
1589     #Could be replaced by TransformMarcToKoha (But Would be longer)
1590     if ($record->field($tag)){
1591       my $modify=0;  
1592       foreach my $field ($record->field($tag)){
1593         if ($subfield){
1594           if ($field->delete_subfield('code' =>$subfield,'match'=>qr($initvalue))){
1595             $countmatched++;
1596             $modify=1;      
1597             $field->update($subfield,$targetvalue) if ($targetvalue);
1598           }
1599         } else {
1600           if ($tag >= 010){
1601             if ($field->delete_field($field)){
1602               $countmatched++;
1603               $modify=1;      
1604             }
1605           } else {
1606             $field->data=$targetvalue if ($field->data=~qr($initvalue));
1607           }     
1608         }    
1609       }
1610 #       warn $record->as_formatted;
1611       if ($modify){
1612         ModBiblio($record,$biblionumber,GetFrameworkCode($biblionumber)) unless ($test);
1613       } else {
1614         push @unmatched, $biblionumber;   
1615       }      
1616     } else {
1617       push @unmatched, $biblionumber;
1618     }
1619   }
1620   return ($countmatched,\@unmatched);
1621 }
1622
1623 END { }    # module clean-up code here (global destructor)
1624
1625 1;
1626 __END__
1627
1628 =head1 AUTHOR
1629
1630 Koha Developement team <info@koha.org>
1631
1632 =cut