fixing subject link on detail pages and searching API
[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 sub _remove_stopwords {
559         my ($operand,$index) = @_;
560         # if the index contains more than one qualifier, but not phrase:    
561         if ($index!~m/phr|ext/){
562         # operand may be a wordlist deleting stopwords
563         # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
564         #       we use IsAlpha unicode definition, to deal correctly with diacritics.
565         #       otherwise, a french word like "leçon" is splitted in "le" "çon", le is an empty word, we get "çon"
566         #       and don't find anything...
567                 foreach (keys %{C4::Context->stopwords}) {
568                         next if ($_ =~/(and|or|not)/); # don't remove operators
569                         $operand=~ s/\P{IsAlpha}$_\P{IsAlpha}/ /i;
570                         $operand=~ s/^$_\P{IsAlpha}/ /i;
571                         $operand=~ s/\P{IsAlpha}$_$/ /i;
572                     
573                 }
574         }
575         return $operand;
576 }
577
578 sub _add_truncation {
579         my ($operand,$index) = @_;
580         my (@nontruncated,@righttruncated,@lefttruncated,@rightlefttruncated,@regexpr);
581         # if the index contains more than one qualifier, but not phrase, add truncation qualifiers
582         #if (index($index,"phr")<0 && index($index,",")>0){
583         # warn "ADDING TRUNCATION QUALIFIERS";
584                 $operand =~s/^ //g;
585                 my @wordlist= split (/\s/,$operand);
586                 foreach my $word (@wordlist){
587                         if ($word=~s/^\*([^\*]+)\*$/$1/){
588         push @rightlefttruncated,$word;
589                         } 
590                         elsif($word=~s/^\*([^\*]+)$/$1/){
591                                 push @lefttruncated,$word;
592                         
593                         } 
594                         elsif ($word=~s/^([^\*]+)\*$/$1/){
595                                 push @righttruncated,$word;
596                         } 
597                         elsif (index($word,"*")<0){
598                                 push @nontruncated,$word;
599                         }
600                         else {
601                                 push @regexpr,$word;
602                         
603                         }
604                 }
605         #}
606         return (\@nontruncated,\@righttruncated,\@lefttruncated,\@rightlefttruncated,\@regexpr);
607 }
608
609 sub _build_stemmed_operand {
610         my ($operand) = @_;
611         my $stemmed_operand;
612         #$operand =~ s/^(and |or |not )//i;
613         # STEMMING FIXME: may need to refine the field weighting so stemmed operands don't 
614         # disrupt the query ranking, this needs more testing
615         # FIXME: the locale should be set based on the user's language and/or search choice
616         my $stemmer = Lingua::Stem->new( -locale => 'EN-US' );
617         # FIXME: these should be stored in the db so the librarian can modify the behavior
618         $stemmer->add_exceptions(
619                         {
620                                 'and' => 'and',
621                 'or'  => 'or',
622                 'not' => 'not',
623                         }
624                     
625                 );
626         my @words = split( / /, $operand );
627         my $stems = $stemmer->stem(@words);
628         foreach my $stem (@$stems) {
629                         $stemmed_operand .= "$stem";
630                         $stemmed_operand .= "?" unless ( $stem =~ /(and$|or$|not$)/ ) || ( length($stem) < 3 );
631                         $stemmed_operand .= " ";
632         }
633         #warn "STEMMED OPERAND: $stemmed_operand";
634         return $stemmed_operand;
635 }
636
637 sub _build_weighted_query {
638         # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
639         # pretty well but will work much better when we have an actual query parser
640         my ($operand,$stemmed_operand,$index) = @_;
641     my $stemming      = C4::Context->preference("QueryStemming")     || 0;
642     my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
643     my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
644
645     my $weighted_query .= " (rk=(";     # Specifies that we're applying rank
646         # keyword has different weight properties
647         if ( ( $index =~ /kw/ ) || ( !$index ) ) {
648         # a simple way to find out if this query uses an index
649                 if ( $operand =~ /(\=|\:)/ ) {
650                         $weighted_query .= " $operand";
651                 }
652                 else {
653                         $weighted_query .=" Title-cover,ext,r1=\"$operand\"";   # title cover as exact
654                         $weighted_query .=" or ti,ext,r2=\"$operand\"";             # exact title elsewhere
655                         $weighted_query .= " or ti,phr,r3=\"$operand\"";          # index as phrase
656                         #$weighted_query .= " or any,ext,r4=$operand";         # index as exact
657                         #$weighted_query .=" or kw,wrdl,r5=\"$operand\"";            # all the words in the query (wordlist)
658                         $weighted_query .= " or wrd,fuzzy,r8=\"$operand\"" if $fuzzy_enabled; # add fuzzy
659                         $weighted_query .= " or wrd,right-Truncation,r9=\"$stemmed_operand\"" if ($stemming and $stemmed_operand); # add stemming
660                         # embedded sorting: 0 a-z; 1 z-a
661                         #$weighted_query .= ") or (sort1,aut=1";
662                 }
663                     
664         }
665         #TODO: build better cases based on specific search indexes
666         #elsif ( $index =~ /au/ ) {
667         #       $weighted_query .=" $index,ext,r1=$operand";    # index label as exact
668         #       #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
669         #       $weighted_query .=" or $index,phr,r3=$operand";    # index as phrase
670         #       $weighted_query .= " or $index,rt,wrd,r3=$operand";
671         #}
672         #elsif ( $index =~ /ti/ ) {
673         #       $weighted_query .=" Title-cover,ext,r1=$operand"; # index label as exact
674         #       $weighted_query .= " or Title-series,ext,r2=$operand";
675         #       #$weighted_query .= " or ti,ext,r2=$operand";
676         #       #$weighted_query .= " or ti,phr,r3=$operand";
677         #       #$weighted_query .= " or ti,wrd,r3=$operand";
678         #       $weighted_query .=" or (title-sort-az=0 or Title-cover,startswithnt,st-word,r3=$operand #)";
679         #       $weighted_query .=" or (title-sort-az=0 or Title-cover,phr,r6=$operand)";
680                 #$weighted_query .= " or Title-cover,wrd,r5=$operand";
681                 #$weighted_query .= " or ti,ext,r6=$operand";
682                 #$weighted_query .= " or ti,startswith,phr,r7=$operand";
683                 #$weighted_query .= " or ti,phr,r8=$operand";
684                 #$weighted_query .= " or ti,wrd,r9=$operand";
685                 #$weighted_query .= " or ti,ext,r2=$operand";         # index as exact
686                 #$weighted_query .= " or ti,phr,r3=$operand";              # index as  phrase
687                 #$weighted_query .= " or any,ext,r4=$operand";         # index as exact
688                 #$weighted_query .= " or kw,wrd,r5=$operand";         # index as exact
689         #}
690         else {
691                 $weighted_query .=" $index,ext,r1=$operand";    # index label as exact
692                 #$weighted_query .= " or $index,ext,r2=$operand";            # index as exact
693                 $weighted_query .=" or $index,phr,r3=$operand";    # index as phrase
694                 $weighted_query .= " or $index,rt,wrd,r3=$operand";
695                 $weighted_query .=" or $index,wrd,r5=$operand";    # index as word right-truncated
696                 $weighted_query .= " or $index,wrd,fuzzy,r8=$operand" if $fuzzy_enabled;
697         }
698         $weighted_query .= "))";    # close rank specification
699         return $weighted_query;
700 }
701
702 # build the query itself
703 sub buildQuery {
704     my ( $operators, $operands, $indexes, $limits, $sort_by ) = @_;
705
706     my @operators = @$operators if $operators;
707     my @indexes   = @$indexes   if $indexes;
708     my @operands  = @$operands  if $operands;
709     my @limits    = @$limits    if $limits;
710     my @sort_by   = @$sort_by   if $sort_by;
711
712         my $stemming      = C4::Context->preference("QueryStemming")     || 0;
713         my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
714         my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
715
716     my $human_search_desc;      # a human-readable query
717     my $machine_search_desc;    #a machine-readable query
718         #warn "OPERATORS: >@operators< INDEXES: >@indexes< OPERANDS: >@operands< LIMITS: >@limits< SORTS: >@sort_by<";
719         my $query = $operands[0];
720 # STEP I: determine if this is a form-based / simple query or if it's complex (if complex,
721 # we can't handle field weighting, stemming until a formal query parser is written
722
723 # check if this is a known query language query, if it is, return immediately,
724 # the user is responsible for constructing valid syntax:
725     if ( $query =~ /^ccl=/ ) {
726         return ( undef, $', $', $', 'ccl' );
727     }
728     if ( $query =~ /^cql=/ ) {
729         return ( undef, $', $', $', 'cql' );
730     }
731     if ( $query =~ /^pqf=/ ) {
732         return ( undef, $', $', $', 'pqf' );
733     }
734     if ( $query =~ /(\(|\))/ ) {    # sorry, too complex, assume CCL
735         return ( undef, $query, $query, $query, 'ccl' );
736     }
737
738 # form-based queries are limited to non-nested at a specific depth, so we can easily
739 # modify the incoming query operands and indexes to do stemming and field weighting
740 # Once we do so, we'll end up with a value in $query, just like if we had an
741 # incoming $query from the user
742     else {
743         $query = ""; # clear it out so we can populate properly with field-weighted stemmed query
744         my $previous_operand;    # a flag used to keep track if there was a previous query
745                                                 # if there was, we can apply the current operator
746                 # for every operand
747         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
748
749                         # COMBINE OPERANDS, INDEXES AND OPERATORS
750                         if ( $operands[$i] ) {
751                                 warn "OP: $operands[$i]";
752                 my $operand = $operands[$i];
753                 my $index   = $indexes[$i];
754                                 # if there's no index, don't use one, it will throw a CCL error
755                                 my $index_plus; $index_plus = "$index:" if $index;
756                                 my $index_plus_comma; $index_plus_comma="$index," if $index;
757
758                                 # Remove Stopwords      
759                                 $operand = _remove_stopwords($operand,$index);
760                                 warn "OP_SW: $operand";
761                                 # Handle Truncation
762                                 my ($nontruncated,$righttruncated,$lefttruncated,$rightlefttruncated,$regexpr);
763                                 ($nontruncated,$righttruncated,$lefttruncated,$rightlefttruncated,$regexpr) = _add_truncation($operand,$index);
764                                 warn "TRUNCATION: NON:@$nontruncated RIGHT:@$righttruncated LEFT:@$lefttruncated RIGHTLEFT:@$rightlefttruncated REGEX:@$regexpr";
765
766                                 # Handle Stemming
767                         my $stemmed_operand;
768                                 $stemmed_operand = _build_stemmed_operand($operand) if $stemming;
769
770                                 # Handle Field Weighting
771                                 my $weighted_operand;
772                 $weighted_operand = _build_weighted_query($operand,$stemmed_operand,$index) if $weight_fields;
773
774                                 # proves we're operating in multi-leaf mode
775                                 # $weighted_operand = "$weighted_operand and $weighted_operand";
776                                 $operand = $weighted_operand if $weight_fields;
777
778                 # If there's a previous operand, we need to add an operator
779                 if ($previous_operand) {
780                     if ( $operators[ $i - 1 ] ) {
781                                                 $human_search_desc .="  $operators[$i-1] $index_plus $operands[$i]";
782                                                 $query .= " $operators[$i-1] $index_plus $operand";
783                     }
784                     # the default operator is and
785                     else {
786                         $query             .= " and $index_plus $operand";
787                         $human_search_desc .= " and $index_plus $operands[$i]";
788                     }
789                 }
790                                 # There's no previous operand - FIXME: completely ignoring our $query, no field weighting, no stemming
791                                 # FIXME: also, doesn't preserve original order
792                 else { 
793                                         # if there are terms to fit with truncation
794                                         if (scalar(@$righttruncated)+scalar(@$lefttruncated)+scalar(@$rightlefttruncated)>0){
795                                                 # add the non-truncated ones first
796                                                 $query.= "$index_plus @$nontruncated " if (scalar(@$nontruncated)>0);
797                                                 if (scalar(@$righttruncated)>0){
798                                                         $query .= "and $index_plus_comma"."rtrn:@$righttruncated ";
799                                                 }            
800                                                 if (scalar(@$lefttruncated)>0){
801                                                         $query .= "and $index_plus_comma"."ltrn:@$lefttruncated ";
802                                                 }            
803                                                 if (scalar(@$rightlefttruncated)>0){
804                                                         $query .= "and $index_plus_comma"."rltrn:@$rightlefttruncated ";
805                                                 }
806                         $query=~s/^and//; # FIXME: this is cheating :-)
807                         $human_search_desc .= $query;
808                                         } else {           
809                         $query             .= " $index_plus $operand";
810                         $human_search_desc .= " $index_plus $operands[$i]";
811                                         }            
812                     $previous_operand = 1;
813                 }
814             }    #/if $operands
815         }    # /for
816     }
817
818     # add limits
819     my $limit_query;
820     my $limit_search_desc;
821     foreach my $limit (@limits) {
822
823         # FIXME: not quite right yet ... will work on this soon -- JF
824         my $type = $1 if $limit =~ m/([^:]+):([^:]*)/;
825         if ( $limit =~ /available/ ) {
826             $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))";
827             #$limit_search_desc.=" and available";
828         }
829         elsif ( ($limit_query) && ( index( $limit_query, $type, 0 ) > 0 ) ) {
830             if ( $limit_query !~ /\(/ ) {
831                 $limit_query =
832                     substr( $limit_query, 0, index( $limit_query, $type, 0 ) )
833                   . "("
834                   . substr( $limit_query, index( $limit_query, $type, 0 ) )
835                   . " or $limit )"
836                   if $limit;
837                 $limit_search_desc =
838                   substr( $limit_search_desc, 0,
839                     index( $limit_search_desc, $type, 0 ) )
840                   . "("
841                   . substr( $limit_search_desc,
842                     index( $limit_search_desc, $type, 0 ) )
843                   . " or $limit )"
844                   if $limit;
845             }
846             else {
847                 chop $limit_query;
848                 chop $limit_search_desc;
849                 $limit_query       .= " or $limit )" if $limit;
850                 $limit_search_desc .= " or $limit )" if $limit;
851             }
852         }
853         elsif ( ($limit_query) && ( $limit =~ /mc/ ) ) {
854             $limit_query       .= " or $limit" if $limit;
855             $limit_search_desc .= " or $limit" if $limit;
856         }
857
858         # these are treated as AND
859         elsif ($limit_query) {
860            if ($limit =~ /branch/){
861                         $limit_query       .= " ) and ( $limit" if $limit;
862                         $limit_search_desc .= " ) and ( $limit" if $limit;
863                 }else{
864                         $limit_query       .= " or $limit" if $limit;
865                         $limit_search_desc .= " or $limit" if $limit;
866                 }
867         }
868
869         # otherwise, there is nothing but the limit
870         else {
871             $limit_query       .= "$limit" if $limit;
872             $limit_search_desc .= "$limit" if $limit;
873         }
874     }
875
876     # if there's also a query, we need to AND the limits to it
877     if ( ($limit_query) && ($query) ) {
878         $limit_query       = " and (" . $limit_query . ")";
879         $limit_search_desc = " and ($limit_search_desc)" if $limit_search_desc;
880
881     }
882         #warn "LIMIT: $limit_query";
883     $query             .= $limit_query;
884     $human_search_desc .= $limit_search_desc;
885
886     # now normalize the strings
887     $query =~ s/  / /g;    # remove extra spaces
888     $query =~ s/^ //g;     # remove any beginning spaces
889     $query =~ s/:/=/g;     # causes probs for server
890     $query =~ s/==/=/g;    # remove double == from query
891
892     my $federated_query = $human_search_desc;
893     $federated_query =~ s/  / /g;
894     $federated_query =~ s/^ //g;
895     $federated_query =~ s/:/=/g;
896     my $federated_query_opensearch = $federated_query;
897
898 #     my $federated_query_RPN = new ZOOM::Query::CCL2RPN( $query , C4::Context->ZConn('biblioserver'));
899
900     $human_search_desc =~ s/  / /g;
901     $human_search_desc =~ s/^ //g;
902     my $koha_query = $query;
903
904     #warn "QUERY:".$koha_query;
905     #warn "SEARCHDESC:".$human_search_desc;
906     #warn "FEDERATED QUERY:".$federated_query;
907     return ( undef, $human_search_desc, $koha_query, $federated_query );
908 }
909
910 # IMO this subroutine is pretty messy still -- it's responsible for
911 # building the HTML output for the template
912 sub searchResults {
913     my ( $searchdesc, $hits, $results_per_page, $offset, @marcresults ) = @_;
914
915     my $dbh = C4::Context->dbh;
916     my $toggle;
917     my $even = 1;
918     my @newresults;
919     my $span_terms_hashref;
920     for my $span_term ( split( / /, $searchdesc ) ) {
921         $span_term =~ s/(.*=|\)|\(|\+|\.)//g;
922         $span_terms_hashref->{$span_term}++;
923     }
924
925     #Build brancnames hash
926     #find branchname
927     #get branch information.....
928     my %branches;
929     my $bsth =
930       $dbh->prepare("SELECT branchcode,branchname FROM branches")
931       ;    # FIXME : use C4::Koha::GetBranches
932     $bsth->execute();
933     while ( my $bdata = $bsth->fetchrow_hashref ) {
934         $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
935     }
936
937     #Build itemtype hash
938     #find itemtype & itemtype image
939     my %itemtypes;
940     $bsth =
941       $dbh->prepare("SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes");
942     $bsth->execute();
943     while ( my $bdata = $bsth->fetchrow_hashref ) {
944         $itemtypes{ $bdata->{'itemtype'} }->{description} =
945           $bdata->{'description'};
946         $itemtypes{ $bdata->{'itemtype'} }->{imageurl} = $bdata->{'imageurl'};
947         $itemtypes{ $bdata->{'itemtype'} }->{summary} = $bdata->{'summary'};
948         $itemtypes{ $bdata->{'itemtype'} }->{notforloan} = $bdata->{'notforloan'};
949     }
950
951     #search item field code
952     my $sth =
953       $dbh->prepare(
954 "select tagfield from marc_subfield_structure where kohafield like 'items.itemnumber'"
955       );
956     $sth->execute;
957     my ($itemtag) = $sth->fetchrow;
958
959     ## find column names of items related to MARC
960     my $sth2 = $dbh->prepare("SHOW COLUMNS from items");
961     $sth2->execute;
962     my %subfieldstosearch;
963     while ( ( my $column ) = $sth2->fetchrow ) {
964         my ( $tagfield, $tagsubfield ) =
965           &GetMarcFromKohaField( "items." . $column, "" );
966         $subfieldstosearch{$column} = $tagsubfield;
967     }
968     my $times;
969
970     if ( $hits && $offset + $results_per_page <= $hits ) {
971         $times = $offset + $results_per_page;
972     }
973     else {
974         $times = $hits;
975     }
976
977     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
978         my $marcrecord;
979         $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
980         my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, '' );
981         # add image url if there is one
982         if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} =~ /^http:/ ) {
983             $oldbiblio->{imageurl} =
984               $itemtypes{ $oldbiblio->{itemtype} }->{imageurl};
985             $oldbiblio->{description} =
986               $itemtypes{ $oldbiblio->{itemtype} }->{description};
987         }
988         else {
989             $oldbiblio->{imageurl} =
990               getitemtypeimagesrc() . "/"
991               . $itemtypes{ $oldbiblio->{itemtype} }->{imageurl}
992               if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
993             $oldbiblio->{description} =
994               $itemtypes{ $oldbiblio->{itemtype} }->{description};
995         }
996         #
997         # build summary if there is one (the summary is defined in itemtypes table
998         #
999         if ($itemtypes{ $oldbiblio->{itemtype} }->{summary}) {
1000             my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1001             my @fields = $marcrecord->fields();
1002             foreach my $field (@fields) {
1003                 my $tag = $field->tag();
1004                 my $tagvalue = $field->as_string();
1005                 $summary =~ s/\[(.?.?.?.?)$tag\*(.*?)]/$1$tagvalue$2\[$1$tag$2]/g;
1006                 unless ($tag<10) {
1007                     my @subf = $field->subfields;
1008                     for my $i (0..$#subf) {
1009                         my $subfieldcode = $subf[$i][0];
1010                         my $subfieldvalue = $subf[$i][1];
1011                         my $tagsubf = $tag.$subfieldcode;
1012                         $summary =~ s/\[(.?.?.?.?)$tagsubf(.*?)]/$1$subfieldvalue$2\[$1$tagsubf$2]/g;
1013                     }
1014                 }
1015             }
1016             $summary =~ s/\[(.*?)]//g;
1017             $summary =~ s/\n/<br>/g;
1018             $oldbiblio->{summary} = $summary;
1019         }
1020         # add spans to search term in results for search term highlighting
1021         foreach my $term ( keys %$span_terms_hashref ) {
1022             my $old_term = $term;
1023             if ( length($term) > 3 ) {
1024                 $term =~ s/(.*=|\)|\(|\+|\.|\?|\[|\])//g;
1025                                 $term =~ s/\\//g;
1026                                 $term =~ s/\*//g;
1027
1028                 #FIXME: is there a better way to do this?
1029                 $oldbiblio->{'title'} =~ s/$term/<span class=term>$&<\/span>/gi;
1030                 $oldbiblio->{'subtitle'} =~
1031                   s/$term/<span class=term>$&<\/span>/gi;
1032
1033                 $oldbiblio->{'author'} =~ s/$term/<span class=term>$&<\/span>/gi;
1034                 $oldbiblio->{'publishercode'} =~ s/$term/<span class=term>$&<\/span>/gi;
1035                 $oldbiblio->{'place'} =~ s/$term/<span class=term>$&<\/span>/gi;
1036                 $oldbiblio->{'pages'} =~ s/$term/<span class=term>$&<\/span>/gi;
1037                 $oldbiblio->{'notes'} =~ s/$term/<span class=term>$&<\/span>/gi;
1038                 $oldbiblio->{'size'}  =~ s/$term/<span class=term>$&<\/span>/gi;
1039             }
1040         }
1041
1042         if ( $i % 2 ) {
1043             $toggle = "#ffffcc";
1044         }
1045         else {
1046             $toggle = "white";
1047         }
1048         $oldbiblio->{'toggle'} = $toggle;
1049         my @fields = $marcrecord->field($itemtag);
1050         my @items_loop;
1051         my $items;
1052         my $ordered_count     = 0;
1053         my $onloan_count      = 0;
1054         my $wthdrawn_count    = 0;
1055         my $itemlost_count    = 0;
1056         my $norequests        = 1;
1057
1058         #
1059         # check the loan status of the item : 
1060         # it is not stored in the MARC record, for pref (zebra reindexing)
1061         # reason. Thus, we have to get the status from a specific SQL query
1062         #
1063         my $sth_issue = $dbh->prepare("
1064             SELECT date_due,returndate 
1065             FROM issues 
1066             WHERE itemnumber=? AND returndate IS NULL");
1067         my $items_count=scalar(@fields);
1068         foreach my $field (@fields) {
1069             my $item;
1070             foreach my $code ( keys %subfieldstosearch ) {
1071                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1072             }
1073             $sth_issue->execute($item->{itemnumber});
1074             $item->{due_date} = format_date($sth_issue->fetchrow);
1075             $item->{onloan} = 1 if $item->{due_date};
1076             # at least one item can be reserved : suppose no
1077             $norequests = 1;
1078             if ( $item->{wthdrawn} ) {
1079                 $wthdrawn_count++;
1080                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1081                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{wthdrawn}=1;
1082             }
1083             elsif ( $item->{itemlost} ) {
1084                 $itemlost_count++;
1085                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1086                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{itemlost}=1;
1087             }
1088             unless ( $item->{notforloan}) {
1089                 # OK, this one can be issued, so at least one can be reserved
1090                 $norequests = 0;
1091             }
1092             if ( ( $item->{onloan} ) && ( $item->{onloan} != '0000-00-00' ) )
1093             {
1094                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1095                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{onloancount} = 1;
1096                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{due_date} = $item->{due_date};
1097                 $onloan_count++;
1098             }
1099             if ( $item->{'homebranch'} ) {
1100                 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{count}++;
1101             }
1102
1103             # Last resort
1104             elsif ( $item->{'holdingbranch'} ) {
1105                 $items->{ $item->{'holdingbranch'} }->{count}++;
1106             }
1107             $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{itemcallnumber} =                $item->{itemcallnumber};
1108             $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{location} =                $item->{location};
1109             $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{branchcode} =               $item->{homebranch};
1110         }    # notforloan, item level and biblioitem level
1111
1112         # last check for norequest : if itemtype is notforloan, it can't be reserved either, whatever the items
1113         $norequests = 1 if $itemtypes{$oldbiblio->{itemtype}}->{notforloan};
1114
1115         for my $key ( sort keys %$items ) {
1116             my $this_item = {
1117                 branchname     => $branches{$items->{$key}->{branchcode}},
1118                 branchcode     => $items->{$key}->{branchcode},
1119                 count          => $items->{$key}->{count},
1120                 itemcallnumber => $items->{$key}->{itemcallnumber},
1121                 location => $items->{$key}->{location},
1122                 onloancount      => $items->{$key}->{onloancount},
1123                 due_date         => $items->{$key}->{due_date},
1124                 wthdrawn      => $items->{$key}->{wthdrawn},
1125                 lost         => $items->{$key}->{itemlost},
1126             };
1127             push @items_loop, $this_item;
1128         }
1129         $oldbiblio->{norequests}    = $norequests;
1130         $oldbiblio->{items_count}    = $items_count;
1131         $oldbiblio->{items_loop}    = \@items_loop;
1132         $oldbiblio->{onloancount}   = $onloan_count;
1133         $oldbiblio->{wthdrawncount} = $wthdrawn_count;
1134         $oldbiblio->{itemlostcount} = $itemlost_count;
1135         $oldbiblio->{orderedcount}  = $ordered_count;
1136         $oldbiblio->{isbn}          =~ s/-//g; # deleting - in isbn to enable amazon content 
1137         push( @newresults, $oldbiblio );
1138     }
1139     return @newresults;
1140 }
1141
1142
1143
1144 #----------------------------------------------------------------------
1145 #
1146 # Non-Zebra GetRecords#
1147 #----------------------------------------------------------------------
1148
1149 =head2 NZgetRecords
1150
1151   NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1152
1153 =cut
1154
1155 sub NZgetRecords {
1156     my (
1157         $koha_query,     $federated_query,  $sort_by_ref,
1158         $servers_ref,    $results_per_page, $offset,
1159         $expanded_facet, $branches,         $query_type,
1160         $scan
1161     ) = @_;
1162     my $result = NZanalyse($koha_query);
1163     return (undef,NZorder($result,@$sort_by_ref[0],$results_per_page,$offset),undef);
1164 }
1165
1166 =head2 NZanalyse
1167
1168   NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1169   the list is builded from inverted index in nozebra SQL table
1170   note that title is here only for convenience : the sorting will be very fast when requested on title
1171   if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1172
1173 =cut
1174
1175 sub NZanalyse {
1176     my ($string,$server) = @_;
1177     # $server contains biblioserver or authorities, depending on what we search on.
1178     warn "querying : $string on $server";
1179     $server='biblioserver' unless $server;
1180     # if we have a ", replace the content to discard temporarily any and/or/not inside
1181     my $commacontent;
1182     if ($string =~/"/) {
1183         $string =~ s/"(.*?)"/__X__/;
1184         $commacontent = $1;
1185 #         print "commacontent : $commacontent\n";
1186     }
1187     # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1188     # then, call again NZanalyse with $left and $right
1189     # (recursive until we find a leaf (=> something without and/or/not)
1190     $string =~ /(.*)( and | or | not | AND | OR | NOT )(.*)/;
1191     my $left = $1;
1192     my $right = $3;
1193     my $operand = lc($2);
1194     # it's not a leaf, we have a and/or/not
1195     if ($operand) {
1196         # reintroduce comma content if needed
1197         $right =~ s/__X__/"$commacontent"/ if $commacontent;
1198         $left =~ s/__X__/"$commacontent"/ if $commacontent;
1199 #         warn "node : $left / $operand / $right\n";
1200         my $leftresult = NZanalyse($left,$server);
1201         my $rightresult = NZanalyse($right,$server);
1202         # OK, we have the results for right and left part of the query
1203         # depending of operand, intersect, union or exclude both lists
1204         # to get a result list
1205         if ($operand eq ' and ') {
1206             my @leftresult = split /;/, $leftresult;
1207 #             my @rightresult = split /;/,$leftresult;
1208             my $finalresult;
1209             # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1210             # the result is stored twice, to have the same weight for AND than OR.
1211             # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1212             # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1213             foreach (@leftresult) {
1214                 if ($rightresult =~ "$_;") {
1215                     $finalresult .= "$_;$_;";
1216                 }
1217             }
1218             return $finalresult;
1219         } elsif ($operand eq ' or ') {
1220             # just merge the 2 strings
1221             return $leftresult.$rightresult;
1222         } elsif ($operand eq ' not ') {
1223             my @leftresult = split /;/, $leftresult;
1224 #             my @rightresult = split /;/,$leftresult;
1225             my $finalresult;
1226             foreach (@leftresult) {
1227                 unless ($rightresult =~ "$_;") {
1228                     $finalresult .= "$_;";
1229                 }
1230             }
1231             return $finalresult;
1232         } else {
1233             # this error is impossible, because of the regexp that isolate the operand, but just in case...
1234             die "error : operand unknown : $operand for $string";
1235         }
1236     # it's a leaf, do the real SQL query and return the result
1237     } else {
1238         $string =~  s/__X__/"$commacontent"/ if $commacontent;
1239         $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|<|>|&|\+|\*|\// /g;
1240 #         warn "leaf : $string\n";
1241         # parse the string in in operator/operand/value again
1242         $string =~ /(.*)(=|>|>=|<|<=)(.*)/;
1243         my $left = $1;
1244         my $operator = $2;
1245         my $right = $3;
1246         my $results;
1247         # automatic replace for short operators
1248         $left='title' if $left =~ '^ti';
1249         $left='author' if $left =~ '^au';
1250         $left='publisher' if $left =~ '^pb';
1251         $left='subject' if $left =~ '^su';
1252         $left='koha-Auth-Number' if $left =~ '^an';
1253         $left='keyword' if $left =~ '^kw';
1254         if ($operator) {
1255             #do a specific search
1256             my $dbh = C4::Context->dbh;
1257             $operator='LIKE' if $operator eq '=' and $right=~ /%/;
1258             my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE server=? AND indexname=? AND value $operator ?");
1259             # warn "$left / $operator / $right\n";
1260             # split each word, query the DB and build the biblionumbers result
1261             foreach (split / /,$right) {
1262                 my $biblionumbers;
1263                 next unless $_;
1264 #                 warn "EXECUTE : $server, $left, $_";
1265                 $sth->execute($server, $left, $_);
1266                 while (my $line = $sth->fetchrow) {
1267                     $biblionumbers .= $line;
1268 #                     warn "result : $line";
1269                 }
1270                 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1271                 if ($results) {
1272                     my @leftresult = split /;/, $biblionumbers;
1273                     my $temp;
1274                     foreach my $entry (@leftresult) { # $_ contains biblionumber,title-weight
1275                         # remove weight at the end
1276                         my $cleaned = $entry;
1277                         $cleaned =~ s/-\d*$//;
1278                         # if the entry already in the hash, take it & increase weight
1279 #                         warn "===== $cleaned =====";
1280                         if ($results =~ "$cleaned") {
1281                             $temp .= "$entry;$entry;";
1282 #                             warn "INCLUDING $entry";
1283                         }
1284                     }
1285                     $results = $temp;
1286                 } else {
1287                     $results = $biblionumbers;
1288                 }
1289             }
1290         } else {
1291             #do a complete search (all indexes)
1292             my $dbh = C4::Context->dbh;
1293             my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?");
1294             # split each word, query the DB and build the biblionumbers result
1295             foreach (split / /,$string) {
1296                 next if C4::Context->stopwords->{uc($_)}; # skip if stopword
1297                 #warn "search on all indexes on $_";
1298                 my $biblionumbers;
1299                 next unless $_;
1300                 $sth->execute($server, $_);
1301                 while (my $line = $sth->fetchrow) {
1302                     $biblionumbers .= $line;
1303                 }
1304                 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1305                 if ($results) {
1306 #                 warn "RES for $_ = $biblionumbers";
1307                     my @leftresult = split /;/, $biblionumbers;
1308                     my $temp;
1309                     foreach my $entry (@leftresult) { # $_ contains biblionumber,title-weight
1310                         # remove weight at the end
1311                         my $cleaned = $entry;
1312                         $cleaned =~ s/-\d*$//;
1313                         # if the entry already in the hash, take it & increase weight
1314 #                         warn "===== $cleaned =====";
1315                         if ($results =~ "$cleaned") {
1316                             $temp .= "$entry;$entry;";
1317 #                             warn "INCLUDING $entry";
1318                         }
1319                     }
1320                     $results = $temp;
1321                 } else {
1322 #                 warn "NEW RES for $_ = $biblionumbers";
1323                     $results = $biblionumbers;
1324                 }
1325             }
1326         }
1327 #         warn "return : $results for LEAF : $string";
1328         return $results;
1329     }
1330 }
1331
1332 =head2 NZorder
1333
1334   $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
1335   
1336   TODO :: Description
1337
1338 =cut
1339
1340
1341 sub NZorder {
1342     my ($biblionumbers, $ordering,$results_per_page,$offset) = @_;
1343     # order title asc by default
1344 #     $ordering = '1=36 <i' unless $ordering;
1345     $results_per_page=20 unless $results_per_page;
1346     $offset = 0 unless $offset;
1347     my $dbh = C4::Context->dbh;
1348     #
1349     # order by POPULARITY
1350     #
1351     if ($ordering =~ /1=9523/) {
1352         my %result;
1353         my %popularity;
1354         # popularity is not in MARC record, it's builded from a specific query
1355         my $sth = $dbh->prepare("select sum(issues) from items where biblionumber=?");
1356         foreach (split /;/,$biblionumbers) {
1357             my ($biblionumber,$title) = split /,/,$_;
1358             $result{$biblionumber}=GetMarcBiblio($biblionumber);
1359             $sth->execute($biblionumber);
1360             my $popularity= $sth->fetchrow ||0;
1361             # hint : the key is popularity.title because we can have
1362             # many results with the same popularity. In this cas, sub-ordering is done by title
1363             # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
1364             # (un-frequent, I agree, but we won't forget anything that way ;-)
1365             $popularity{sprintf("%10d",$popularity).$title.$biblionumber} = $biblionumber;
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=9523 >i') { # sort popularity DESC
1371             foreach my $key (sort {$b cmp $a} (keys %popularity)) {
1372                 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1373             }
1374         } else { # sort popularity ASC
1375             foreach my $key (sort (keys %popularity)) {
1376                 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$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 author
1385     #
1386     } elsif ($ordering eq '1=1003 <i'){
1387         my %result;
1388         foreach (split /;/,$biblionumbers) {
1389             my ($biblionumber,$title) = split /,/,$_;
1390             my $record=GetMarcBiblio($biblionumber);
1391             my $author;
1392             if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1393                 $author=$record->subfield('200','f');
1394                 $author=$record->subfield('700','a') unless $author;
1395             } else {
1396                 $author=$record->subfield('100','a');
1397             }
1398             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1399             # and we don't want to get only 1 result for each of them !!!
1400             $result{$author.$biblionumber}=$record;
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=1003 <i') { # sort by author desc
1406             foreach my $key (sort (keys %result)) {
1407                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1408             }
1409         } else { # sort by author ASC
1410             foreach my $key (sort { $a cmp $b } (keys %result)) {
1411                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1412             }
1413         }
1414         my $finalresult=();
1415         $result_hash->{'hits'} = $numbers;
1416         $finalresult->{'biblioserver'} = $result_hash;
1417         return $finalresult;
1418     #
1419     # ORDER BY callnumber
1420     #
1421     } elsif ($ordering eq '1=20 <i'){
1422         my %result;
1423         foreach (split /;/,$biblionumbers) {
1424             my ($biblionumber,$title) = split /,/,$_;
1425             my $record=GetMarcBiblio($biblionumber);
1426             my $callnumber;
1427             my ($callnumber_tag,$callnumber_subfield)=GetMarcFromKohaField($dbh,'items.itemcallnumber');
1428             ($callnumber_tag,$callnumber_subfield)= GetMarcFromKohaField('biblioitems.callnumber') unless $callnumber_tag;
1429             if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1430                 $callnumber=$record->subfield('200','f');
1431             } else {
1432                 $callnumber=$record->subfield('100','a');
1433             }
1434             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1435             # and we don't want to get only 1 result for each of them !!!
1436             $result{$callnumber.$biblionumber}=$record;
1437         }
1438         # sort the hash and return the same structure as GetRecords (Zebra querying)
1439         my $result_hash;
1440         my $numbers=0;
1441         if ($ordering eq '1=1003 <i') { # sort by title desc
1442             foreach my $key (sort (keys %result)) {
1443                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1444             }
1445         } else { # sort by title ASC
1446             foreach my $key (sort { $a cmp $b } (keys %result)) {
1447                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1448             }
1449         }
1450         my $finalresult=();
1451         $result_hash->{'hits'} = $numbers;
1452         $finalresult->{'biblioserver'} = $result_hash;
1453         return $finalresult;
1454     } elsif ($ordering =~ /1=31/){ #pub year
1455         my %result;
1456         foreach (split /;/,$biblionumbers) {
1457             my ($biblionumber,$title) = split /,/,$_;
1458             my $record=GetMarcBiblio($biblionumber);
1459             my ($publicationyear_tag,$publicationyear_subfield)=GetMarcFromKohaField($dbh,'biblioitems.publicationyear');
1460             my $publicationyear=$record->subfield($publicationyear_tag,$publicationyear_subfield);
1461             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1462             # and we don't want to get only 1 result for each of them !!!
1463             $result{$publicationyear.$biblionumber}=$record;
1464         }
1465         # sort the hash and return the same structure as GetRecords (Zebra querying)
1466         my $result_hash;
1467         my $numbers=0;
1468         if ($ordering eq '1=31 <i') { # sort by pubyear desc
1469             foreach my $key (sort (keys %result)) {
1470                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1471             }
1472         } else { # sort by pub year ASC
1473             foreach my $key (sort { $b cmp $a } (keys %result)) {
1474                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1475             }
1476         }
1477         my $finalresult=();
1478         $result_hash->{'hits'} = $numbers;
1479         $finalresult->{'biblioserver'} = $result_hash;
1480         return $finalresult;
1481     #
1482     # ORDER BY title
1483     #
1484     } elsif ($ordering =~ /1=4/) { 
1485         # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
1486         my %result;
1487         foreach (split /;/,$biblionumbers) {
1488             my ($biblionumber,$title) = split /,/,$_;
1489             # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1490             # and we don't want to get only 1 result for each of them !!!
1491             # hint & speed improvement : we can order without reading the record
1492             # so order, and read records only for the requested page !
1493             $result{$title.$biblionumber}=$biblionumber;
1494         }
1495         # sort the hash and return the same structure as GetRecords (Zebra querying)
1496         my $result_hash;
1497         my $numbers=0;
1498         if ($ordering eq '1=4 <i') { # sort by title desc
1499             foreach my $key (sort (keys %result)) {
1500                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1501             }
1502         } else { # sort by title ASC
1503             foreach my $key (sort { $b cmp $a } (keys %result)) {
1504                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1505             }
1506         }
1507         # limit the $results_per_page to result size if it's more
1508         $results_per_page = $numbers-1 if $numbers < $results_per_page;
1509         # for the requested page, replace biblionumber by the complete record
1510         # speed improvement : avoid reading too much things
1511         for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1512             $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1513         }
1514         my $finalresult=();
1515         $result_hash->{'hits'} = $numbers;
1516         $finalresult->{'biblioserver'} = $result_hash;
1517         return $finalresult;
1518     } else {
1519     #
1520     # order by ranking
1521     #
1522         # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
1523         my %result;
1524         my %count_ranking;
1525         foreach (split /;/,$biblionumbers) {
1526             my ($biblionumber,$title) = split /,/,$_;
1527             $title =~ /(.*)-(\d)/;
1528             # get weight 
1529             my $ranking =$2;
1530             # note that we + the ranking because ranking is calculated on weight of EACH term requested.
1531             # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
1532             # biblio N has ranking = 6
1533             $count_ranking{$biblionumber} += $ranking;
1534         }
1535         # build the result by "inverting" the count_ranking hash
1536         # 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
1537 #         warn "counting";
1538         foreach (keys %count_ranking) {
1539             $result{sprintf("%10d",$count_ranking{$_}).'-'.$_} = $_;
1540         }
1541         # sort the hash and return the same structure as GetRecords (Zebra querying)
1542         my $result_hash;
1543         my $numbers=0;
1544             foreach my $key (sort {$b cmp $a} (keys %result)) {
1545                 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1546             }
1547         # limit the $results_per_page to result size if it's more
1548         $results_per_page = $numbers-1 if $numbers < $results_per_page;
1549         # for the requested page, replace biblionumber by the complete record
1550         # speed improvement : avoid reading too much things
1551         for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1552             $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1553         }
1554         my $finalresult=();
1555         $result_hash->{'hits'} = $numbers;
1556         $finalresult->{'biblioserver'} = $result_hash;
1557         return $finalresult;
1558     }
1559 }
1560 =head2 ModBiblios
1561
1562 ($countchanged,$listunchanged) = ModBiblios($listbiblios, $tagsubfield,$initvalue,$targetvalue,$test);
1563
1564 this function changes all the values $initvalue in subfield $tag$subfield in any record in $listbiblios
1565 test parameter if set donot perform change to records in database.
1566
1567 =over 2
1568
1569 =item C<input arg:>
1570
1571     * $listbiblios is an array ref to marcrecords to be changed
1572     * $tagsubfield is the reference of the subfield to change.
1573     * $initvalue is the value to search the record for
1574     * $targetvalue is the value to set the subfield to
1575     * $test is to be set only not to perform changes in database.
1576
1577 =item C<Output arg:>
1578     * $countchanged counts all the changes performed.
1579     * $listunchanged contains the list of all the biblionumbers of records unchanged.
1580
1581 =item C<usage in the script:>
1582
1583 =back
1584
1585 my ($countchanged, $listunchanged) = EditBiblios($results->{RECORD}, $tagsubfield,$initvalue,$targetvalue);;
1586 #If one wants to display unchanged records, you should get biblios foreach @$listunchanged 
1587 $template->param(countchanged => $countchanged, loopunchanged=>$listunchanged);
1588
1589 =cut
1590
1591 sub ModBiblios{
1592   my ($listbiblios,$tagsubfield,$initvalue,$targetvalue,$test)=@_;
1593   my $countmatched;
1594   my @unmatched;
1595   my ($tag,$subfield)=($1,$2) if ($tagsubfield=~/^(\d{1,3})([a-z0-9A-Z@])?$/); 
1596   if ((length($tag)<3)&& $subfield=~/0-9/){
1597     $tag=$tag.$subfield;
1598     undef $subfield;
1599   } 
1600   my ($bntag,$bnsubf) = GetMarcFromKohaField('biblio.biblionumber');
1601   my ($itemtag,$itemsubf) = GetMarcFromKohaField('items.itemnumber');
1602   foreach my $usmarc (@$listbiblios){
1603     my $record; 
1604     $record=eval{MARC::Record->new_from_usmarc($usmarc)};
1605     my $biblionumber;
1606     if ($@){
1607       # usmarc is not a valid usmarc May be a biblionumber
1608       if ($tag eq $itemtag){
1609         my $bib=GetBiblioFromItemNumber($usmarc);   
1610         $record=GetMarcItem($bib->{'biblionumber'},$usmarc) ;   
1611         $biblionumber=$bib->{'biblionumber'};
1612       } else {   
1613         $record=GetMarcBiblio($usmarc);   
1614         $biblionumber=$usmarc;
1615       }   
1616     }  else {
1617       if ($bntag >= 010){
1618         $biblionumber = $record->subfield($bntag,$bnsubf);
1619       }else {
1620         $biblionumber=$record->field($bntag)->data;
1621       }
1622     }  
1623     #GetBiblionumber is to be written.
1624     #Could be replaced by TransformMarcToKoha (But Would be longer)
1625     if ($record->field($tag)){
1626       my $modify=0;  
1627       foreach my $field ($record->field($tag)){
1628         if ($subfield){
1629           if ($field->delete_subfield('code' =>$subfield,'match'=>qr($initvalue))){
1630             $countmatched++;
1631             $modify=1;      
1632             $field->update($subfield,$targetvalue) if ($targetvalue);
1633           }
1634         } else {
1635           if ($tag >= 010){
1636             if ($field->delete_field($field)){
1637               $countmatched++;
1638               $modify=1;      
1639             }
1640           } else {
1641             $field->data=$targetvalue if ($field->data=~qr($initvalue));
1642           }     
1643         }    
1644       }
1645 #       warn $record->as_formatted;
1646       if ($modify){
1647         ModBiblio($record,$biblionumber,GetFrameworkCode($biblionumber)) unless ($test);
1648       } else {
1649         push @unmatched, $biblionumber;   
1650       }      
1651     } else {
1652       push @unmatched, $biblionumber;
1653     }
1654   }
1655   return ($countmatched,\@unmatched);
1656 }
1657
1658 END { }    # module clean-up code here (global destructor)
1659
1660 1;
1661 __END__
1662
1663 =head1 AUTHOR
1664
1665 Koha Developement team <info@koha.org>
1666
1667 =cut