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