File Coverage

File:C4/Search.pm
Coverage:4.2%

linestmtbrancondsubtimecode
1package 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
14
14
14
76
27
783
use strict;
19#use warnings; FIXME - Bug 2505
20require Exporter;
21
14
14
14
102
35
167
use C4::Context;
22
14
14
14
244
22
7311
use C4::Biblio; # GetMarcFromKohaField, GetBiblioData
23
14
14
14
95
26
4801
use C4::Koha; # getFacets
24
14
14
14
29730
105110
1283
use Lingua::Stem;
25
14
14
14
3458
64
808
use C4::Search::PazPar2;
26
14
14
14
96
42
221
use XML::Simple;
27
14
14
14
1021
60
822
use C4::Dates qw(format_date);
28
14
14
14
1587
77
1577
use C4::Members qw(GetHideLostItemsPreference);
29
14
14
14
2335
82
1680
use C4::XSLT;
30
14
14
14
126
67
2867
use C4::Branch;
31
14
14
14
522
71
2036
use C4::Reserves; # CheckReserves
32
14
14
14
114
62
1131
use C4::Debug;
33
14
14
14
116
55
873
use C4::Items;
34
14
14
14
119
52
2628
use C4::Charset;
35
14
14
14
3165
103326
1262
use YAML;
36
14
14
14
523
588
1255
use URI::Escape;
37
14
14
14
2860
175551
901
use Business::ISBN;
38
39
14
14
14
138
42
2060
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
40
41# set the version for version checking
42BEGIN {
43
14
67
    $VERSION = 3.01;
44
14
174773
    $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
45}
46
47 - 61
=head1 NAME

C4::Search - Functions for searching the Koha catalog.

=head1 SYNOPSIS

See opac/opac-search.pl or catalogue/search.pl for example of usage

=head1 DESCRIPTION

This module provides searching functions for Koha's bibliographic databases

=head1 FUNCTIONS

=cut
62
63@ISA = qw(Exporter);
64@EXPORT = qw(
65  &FindDuplicate
66  &SimpleSearch
67  &searchResults
68  &getRecords
69  &buildQuery
70  &NZgetRecords
71  &AddSearchHistory
72  &GetDistinctValues
73  &enabled_staff_search_views
74  &SimpleSearch
75);
76
77# make all your functions, whether exported or not;
78
79 - 85
=head2 FindDuplicate

($biblionumber,$biblionumber,$title) = FindDuplicate($record);

This function attempts to find duplicate records using a hard-coded, fairly simplistic algorithm

=cut
86
87sub FindDuplicate {
88
0
    my ($record) = @_;
89
0
    my $dbh = C4::Context->dbh;
90
0
    my $result = TransformMarcToKoha( $dbh, $record, '' );
91
0
    my $sth;
92
0
    my $query;
93
0
    my $search;
94
0
    my $type;
95
0
    my ( $biblionumber, $title );
96
97    # search duplicate on ISBN, easy and fast..
98    # ... normalize first
99
0
    if ( $result->{isbn} ) {
100
0
        $result->{isbn} =~ s/\(.*$//;
101
0
        $result->{isbn} =~ s/\s+$//;
102
0
        $query = "isbn=$result->{isbn}";
103    }
104    else {
105
0
        $result->{title} =~ s /\\//g;
106
0
        $result->{title} =~ s /\"//g;
107
0
        $result->{title} =~ s /\(//g;
108
0
        $result->{title} =~ s /\)//g;
109
110        # FIXME: instead of removing operators, could just do
111        # quotes around the value
112
0
        $result->{title} =~ s/(and|or|not)//g;
113
0
        $query = "ti,ext=$result->{title}";
114
0
        $query .= " and itemtype=$result->{itemtype}"
115          if ( $result->{itemtype} );
116
0
        if ( $result->{author} ) {
117
0
            $result->{author} =~ s /\\//g;
118
0
            $result->{author} =~ s /\"//g;
119
0
            $result->{author} =~ s /\(//g;
120
0
            $result->{author} =~ s /\)//g;
121
122            # remove valid operators
123
0
            $result->{author} =~ s/(and|or|not)//g;
124
0
            $query .= " and au,ext=$result->{author}";
125        }
126    }
127
128
0
    my ( $error, $searchresults, undef ) = SimpleSearch($query); # FIXME :: hardcoded !
129
0
    my @results;
130
0
    if (!defined $error) {
131
0
0
        foreach my $possible_duplicate_record (@{$searchresults}) {
132
0
            my $marcrecord =
133            MARC::Record->new_from_usmarc($possible_duplicate_record);
134
0
            my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
135
136            # FIXME :: why 2 $biblionumber ?
137
0
            if ($result) {
138
0
                push @results, $result->{'biblionumber'};
139
0
                push @results, $result->{'title'};
140            }
141        }
142    }
143
0
    return @results;
144}
145
146 - 206
=head2 SimpleSearch

( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers] );

This function provides a simple search API on the bibliographic catalog

=over 2

=item C<input arg:>

    * $query can be a simple keyword or a complete CCL query
    * @servers is optional. Defaults to biblioserver as found in koha-conf.xml
    * $offset - If present, represents the number of records at the beggining to omit. Defaults to 0
    * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.


=item C<Return:>

    Returns an array consisting of three elements
    * $error is undefined unless an error is detected
    * $results is a reference to an array of records.
    * $total_hits is the number of hits that would have been returned with no limit

    If an error is returned the two other return elements are undefined. If error itself is undefined
    the other two elements are always defined

=item C<usage in the script:>

=back

my ( $error, $marcresults, $total_hits ) = SimpleSearch($query);

if (defined $error) {
    $template->param(query_error => $error);
    warn "error: ".$error;
    output_html_with_http_headers $input, $cookie, $template->output;
    exit;
}

my $hits = @{$marcresults};
my @results;

for my $r ( @{$marcresults} ) {
    my $marcrecord = MARC::File::USMARC::decode($r);
    my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,q{});

    #build the iarray of hashs for the template.
    push @results, {
        title           => $biblio->{'title'},
        subtitle        => $biblio->{'subtitle'},
        biblionumber    => $biblio->{'biblionumber'},
        author          => $biblio->{'author'},
        publishercode   => $biblio->{'publishercode'},
        publicationyear => $biblio->{'publicationyear'},
        };

}

$template->param(result=>\@results);

=cut
207
208sub SimpleSearch {
209
0
    my ( $query, $offset, $max_results, $servers ) = @_;
210
211
0
    if ( C4::Context->preference('NoZebra') ) {
212
0
        my $result = NZorder( NZanalyse($query) )->{'biblioserver'};
213
0
        my $search_result =
214          ( $result->{hits}
215              && $result->{hits} > 0 ? $result->{'RECORDS'} : [] );
216
0
        return ( undef, $search_result, scalar($result->{hits}) );
217    }
218    else {
219
0
        return ( 'No query entered', undef, undef ) unless $query;
220        # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
221
0
        my @servers = defined ( $servers ) ? @$servers : ( 'biblioserver' );
222
0
        my @zoom_queries;
223
0
        my @tmpresults;
224
0
        my @zconns;
225
0
        my $results = [];
226
0
        my $total_hits = 0;
227
228        # Initialize & Search Zebra
229        for ( my $i = 0 ; $i < @servers ; $i++ ) {
230
0
            eval {
231
0
                $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
232
0
                $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
233
0
                $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
234
235                # error handling
236
0
                my $error =
237                    $zconns[$i]->errmsg() . " ("
238                  . $zconns[$i]->errcode() . ") "
239                  . $zconns[$i]->addinfo() . " "
240                  . $zconns[$i]->diagset();
241
242
0
                return ( $error, undef, undef ) if $zconns[$i]->errcode();
243            };
244
0
            if ($@) {
245
246                # caught a ZOOM::Exception
247
0
                my $error =
248                    $@->message() . " ("
249                  . $@->code() . ") "
250                  . $@->addinfo() . " "
251                  . $@->diagset();
252
0
                warn $error;
253
0
                return ( $error, undef, undef );
254            }
255
0
        }
256
0
        while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
257
0
            my $event = $zconns[ $i - 1 ]->last_event();
258
0
            if ( $event == ZOOM::Event::ZEND ) {
259
260
0
                my $first_record = defined( $offset ) ? $offset+1 : 1;
261
0
                my $hits = $tmpresults[ $i - 1 ]->size();
262
0
                $total_hits += $hits;
263
0
                my $last_record = $hits;
264
0
                if ( defined $max_results && $offset + $max_results < $hits ) {
265
0
                    $last_record = $offset + $max_results;
266                }
267
268
0
                for my $j ( $first_record..$last_record ) {
269
0
                    my $record = $tmpresults[ $i - 1 ]->record( $j-1 )->raw(); # 0 indexed
270
0
0
                    push @{$results}, $record;
271                }
272            }
273        }
274
275
0
        foreach my $result (@tmpresults) {
276
0
            $result->destroy();
277        }
278
0
        foreach my $zoom_query (@zoom_queries) {
279
0
            $zoom_query->destroy();
280        }
281
282
0
        return ( undef, $results, $total_hits );
283    }
284}
285
286 - 300
=head2 getRecords

( undef, $results_hashref, \@facets_loop ) = getRecords (

        $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
        $results_per_page, $offset,       $expanded_facet, $branches,
        $query_type,       $scan
    );

The all singing, all dancing, multi-server, asynchronous, scanning,
searching, record nabbing, facet-building

See verbse embedded documentation.

=cut
301
302sub getRecords {
303    my (
304
0
        $koha_query, $simple_query, $sort_by_ref, $servers_ref,
305        $results_per_page, $offset, $expanded_facet, $branches,
306        $query_type, $scan
307    ) = @_;
308
309
0
    my @servers = @$servers_ref;
310
0
    my @sort_by = @$sort_by_ref;
311
312    # Initialize variables for the ZOOM connection and results object
313
0
    my $zconn;
314
0
    my @zconns;
315
0
    my @results;
316
0
    my $results_hashref = ();
317
318    # Initialize variables for the faceted results objects
319
0
    my $facets_counter = ();
320
0
    my $facets_info = ();
321
0
    my $facets = getFacets();
322
0
    my $facets_maxrecs = C4::Context->preference('maxRecordsForFacets')||20;
323
324
0
    my @facets_loop; # stores the ref to array of hashes for template facets loop
325
326    ### LOOP THROUGH THE SERVERS
327    for ( my $i = 0 ; $i < @servers ; $i++ ) {
328
0
        $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
329
330# perform the search, create the results objects
331# if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
332
0
        my $query_to_use = ($servers[$i] =~ /biblioserver/) ? $koha_query : $simple_query;
333
334        #$query_to_use = $simple_query if $scan;
335
0
        warn $simple_query if ( $scan and $DEBUG );
336
337        # Check if we've got a query_type defined, if so, use it
338
0
        eval {
339
0
            if ($query_type) {
340
0
                if ($query_type =~ /^ccl/) {
341
0
                    $query_to_use =~ s/\:/\=/g; # change : to = last minute (FIXME)
342
0
                    $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
343                } elsif ($query_type =~ /^cql/) {
344
0
                    $results[$i] = $zconns[$i]->search(new ZOOM::Query::CQL($query_to_use, $zconns[$i]));
345                } elsif ($query_type =~ /^pqf/) {
346
0
                    $results[$i] = $zconns[$i]->search(new ZOOM::Query::PQF($query_to_use, $zconns[$i]));
347                } else {
348
0
                    warn "Unknown query_type '$query_type'. Results undetermined.";
349                }
350            } elsif ($scan) {
351
0
                    $results[$i] = $zconns[$i]->scan( new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
352            } else {
353
0
                    $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
354            }
355        };
356
0
        if ($@) {
357
0
            warn "WARNING: query problem with $query_to_use " . $@;
358        }
359
360        # Concatenate the sort_by limits and pass them to the results object
361        # Note: sort will override rank
362
0
        my $sort_by;
363
0
        foreach my $sort (@sort_by) {
364
0
            if ( $sort eq "author_az" || $sort eq "author_asc" ) {
365
0
                $sort_by .= "1=1003 <i ";
366            }
367            elsif ( $sort eq "author_za" || $sort eq "author_dsc" ) {
368
0
                $sort_by .= "1=1003 >i ";
369            }
370            elsif ( $sort eq "popularity_asc" ) {
371
0
                $sort_by .= "1=9003 <i ";
372            }
373            elsif ( $sort eq "popularity_dsc" ) {
374
0
                $sort_by .= "1=9003 >i ";
375            }
376            elsif ( $sort eq "call_number_asc" ) {
377
0
                $sort_by .= "1=8007 <i ";
378            }
379            elsif ( $sort eq "call_number_dsc" ) {
380
0
                $sort_by .= "1=8007 >i ";
381            }
382            elsif ( $sort eq "pubdate_asc" ) {
383
0
                $sort_by .= "1=31 <i ";
384            }
385            elsif ( $sort eq "pubdate_dsc" ) {
386
0
                $sort_by .= "1=31 >i ";
387            }
388            elsif ( $sort eq "acqdate_asc" ) {
389
0
                $sort_by .= "1=32 <i ";
390            }
391            elsif ( $sort eq "acqdate_dsc" ) {
392
0
                $sort_by .= "1=32 >i ";
393            }
394            elsif ( $sort eq "title_az" || $sort eq "title_asc" ) {
395
0
                $sort_by .= "1=4 <i ";
396            }
397            elsif ( $sort eq "title_za" || $sort eq "title_dsc" ) {
398
0
                $sort_by .= "1=4 >i ";
399            }
400            else {
401
0
                warn "Ignoring unrecognized sort '$sort' requested" if $sort_by;
402            }
403        }
404
0
        if ($sort_by && !$scan) {
405
0
            if ( $results[$i]->sort( "yaz", $sort_by ) < 0 ) {
406
0
                warn "WARNING sort $sort_by failed";
407            }
408        }
409
0
    } # finished looping through servers
410
411    # The big moment: asynchronously retrieve results from all servers
412
0
    while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
413
0
        my $ev = $zconns[ $i - 1 ]->last_event();
414
0
        if ( $ev == ZOOM::Event::ZEND ) {
415
0
            next unless $results[ $i - 1 ];
416
0
            my $size = $results[ $i - 1 ]->size();
417
0
            if ( $size > 0 ) {
418
0
                my $results_hash;
419
420                # loop through the results
421
0
                $results_hash->{'hits'} = $size;
422
0
                my $times;
423
0
                if ( $offset + $results_per_page <= $size ) {
424
0
                    $times = $offset + $results_per_page;
425                }
426                else {
427
0
                    $times = $size;
428                }
429                for ( my $j = $offset ; $j < $times ; $j++ ) {
430
0
                    my $records_hash;
431
0
                    my $record;
432
433                    ## Check if it's an index scan
434
0
                    if ($scan) {
435
0
                        my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
436
437                 # here we create a minimal MARC record and hand it off to the
438                 # template just like a normal result ... perhaps not ideal, but
439                 # it works for now
440
0
                        my $tmprecord = MARC::Record->new();
441
0
                        $tmprecord->encoding('UTF-8');
442
0
                        my $tmptitle;
443
0
                        my $tmpauthor;
444
445                # the minimal record in author/title (depending on MARC flavour)
446
0
                        if (C4::Context->preference("marcflavour") eq "UNIMARC") {
447
0
                            $tmptitle = MARC::Field->new('200',' ',' ', a => $term, f => $occ);
448
0
                            $tmprecord->append_fields($tmptitle);
449                        } else {
450
0
                            $tmptitle = MARC::Field->new('245',' ',' ', a => $term,);
451
0
                            $tmpauthor = MARC::Field->new('100',' ',' ', a => $occ,);
452
0
                            $tmprecord->append_fields($tmptitle);
453
0
                            $tmprecord->append_fields($tmpauthor);
454                        }
455
0
                        $results_hash->{'RECORDS'}[$j] = $tmprecord->as_usmarc();
456                    }
457
458                    # not an index scan
459                    else {
460
0
                        $record = $results[ $i - 1 ]->record($j)->raw();
461
462                        # warn "RECORD $j:".$record;
463
0
                        $results_hash->{'RECORDS'}[$j] = $record;
464                    }
465
466
0
                }
467
0
                $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
468
469                # Fill the facets while we're looping, but only for the biblioserver and not for a scan
470
0
                if ( !$scan && $servers[ $i - 1 ] =~ /biblioserver/ ) {
471
472
0
                    my $jmax = $size>$facets_maxrecs? $facets_maxrecs: $size;
473
0
                    for my $facet ( @$facets ) {
474                                for ( my $j = 0 ; $j < $jmax ; $j++ ) {
475
0
                                    my $render_record = $results[ $i - 1 ]->record($j)->render();
476
0
                            my @used_datas = ();
477
0
0
                            foreach my $tag ( @{$facet->{tags}} ) {
478                                # avoid first line
479
0
                                my $tag_num = substr($tag, 0, 3);
480
0
                                my $letters = substr($tag, 3);
481
0
                                my $field_pattern = '\n' . $tag_num . ' ([^\n]+)';
482
0
                                my @field_tokens = ( $render_record =~ /$field_pattern/g ) ;
483
0
                                foreach my $field_token (@field_tokens) {
484
0
                                    my @subf = ( $field_token =~ /\$([a-zA-Z0-9]) ([^\$]+)/g );
485
0
                                    my @values;
486                                    for (my $i = 0; $i < @subf; $i += 2) {
487
0
                                        if ( $letters =~ $subf[$i] ) {
488
0
                                             my $value = $subf[$i+1];
489
0
                                             $value =~ s/^ *//;
490
0
                                             $value =~ s/ *$//;
491
0
                                             push @values, $value;
492                                        }
493
0
                                    }
494
0
                                    my $data = join($facet->{sep}, @values);
495
0
                                    unless ( $data ~~ @used_datas ) {
496
0
                                        $facets_counter->{ $facet->{idx} }->{$data}++;
497
0
                                        push @used_datas, $data;
498                                    }
499                                } # fields
500                            } # field codes
501
0
                        } # records
502
0
                        $facets_info->{ $facet->{idx} }->{label_value} = $facet->{label};
503
0
                        $facets_info->{ $facet->{idx} }->{expanded} = $facet->{expanded};
504                    } # facets
505                }
506            }
507
508            # warn "connection ", $i-1, ": $size hits";
509            # warn $results[$i-1]->record(0)->render() if $size > 0;
510
511            # BUILD FACETS
512
0
            if ( $servers[ $i - 1 ] =~ /biblioserver/ ) {
513
0
0
                for my $link_value (
514                    sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
515                        keys %$facets_counter )
516                {
517
0
                    my $expandable;
518
0
                    my $number_of_facets;
519
0
                    my @this_facets_array;
520
0
0
                    for my $one_facet (
521                        sort {
522
0
                             $facets_counter->{$link_value}->{$b}
523                         <=> $facets_counter->{$link_value}->{$a}
524                        } keys %{ $facets_counter->{$link_value} }
525                      )
526                    {
527
0
                        $number_of_facets++;
528
0
                        if ( ( $number_of_facets < 6 )
529                            || ( $expanded_facet eq $link_value )
530                            || ( $facets_info->{$link_value}->{'expanded'} ) )
531                        {
532
533                      # Sanitize the link value ), ( will cause errors with CCL,
534
0
                            my $facet_link_value = $one_facet;
535
0
                            $facet_link_value =~ s/(\(|\))/ /g;
536
537                            # fix the length that will display in the label,
538
0
                            my $facet_label_value = $one_facet;
539
0
                            my $facet_max_length =
540                                C4::Context->preference('FacetLabelTruncationLength') || 20;
541
0
                            $facet_label_value =
542                              substr( $one_facet, 0, $facet_max_length ) . "..."
543                                if length($facet_label_value) > $facet_max_length;
544
545                            # if it's a branch, label by the name, not the code,
546
0
                            if ( $link_value =~ /branch/ ) {
547
0
                                                                if (defined $branches
548                                                                        && ref($branches) eq "HASH"
549                                                                        && defined $branches->{$one_facet}
550                                                                        && ref ($branches->{$one_facet}) eq "HASH")
551                                                                {
552
0
                                 $facet_label_value =
553                                    $branches->{$one_facet}->{'branchname'};
554                                                                }
555                                                                else {
556
0
                                                                        $facet_label_value = "*";
557                                                                }
558                            }
559
560                            # but we're down with the whole label being in the link's title.
561
0
                            push @this_facets_array, {
562                                facet_count => $facets_counter->{$link_value}->{$one_facet},
563                                facet_label_value => $facet_label_value,
564                                facet_title_value => $one_facet,
565                                facet_link_value => $facet_link_value,
566                                type_link_value => $link_value,
567                            };
568                        }
569                    }
570
571                    # handle expanded option
572
0
                    unless ( $facets_info->{$link_value}->{'expanded'} ) {
573
0
                        $expandable = 1
574                          if ( ( $number_of_facets > 6 )
575                            && ( $expanded_facet ne $link_value ) );
576                    }
577
0
                    push @facets_loop, {
578                        type_link_value => $link_value,
579                        type_id => $link_value . "_id",
580                        "type_label_" . $facets_info->{$link_value}->{'label_value'} => 1,
581                        facets => \@this_facets_array,
582                        expandable => $expandable,
583                        expand => $link_value,
584                    } unless ( ($facets_info->{$link_value}->{'label_value'} =~ /Libraries/) and (C4::Context->preference('singleBranchMode')) );
585                }
586            }
587        }
588    }
589
0
    return ( undef, $results_hashref, \@facets_loop );
590}
591
592sub pazGetRecords {
593    my (
594
0
        $koha_query, $simple_query, $sort_by_ref, $servers_ref,
595        $results_per_page, $offset, $expanded_facet, $branches,
596        $query_type, $scan
597    ) = @_;
598
599
0
    my $paz = C4::Search::PazPar2->new(C4::Context->config('pazpar2url'));
600
0
    $paz->init();
601
0
    $paz->search($simple_query);
602
0
    sleep 1; # FIXME: WHY?
603
604    # do results
605
0
    my $results_hashref = {};
606
0
    my $stats = XMLin($paz->stat);
607
0
    my $results = XMLin($paz->show($offset, $results_per_page, 'work-title:1'), forcearray => 1);
608
609    # for a grouped search result, the number of hits
610    # is the number of groups returned; 'bib_hits' will have
611    # the total number of bibs.
612
0
    $results_hashref->{'biblioserver'}->{'hits'} = $results->{'merged'}->[0];
613
0
    $results_hashref->{'biblioserver'}->{'bib_hits'} = $stats->{'hits'};
614
615
0
0
    HIT: foreach my $hit (@{ $results->{'hit'} }) {
616
0
        my $recid = $hit->{recid}->[0];
617
618
0
        my $work_title = $hit->{'md-work-title'}->[0];
619
0
        my $work_author;
620
0
        if (exists $hit->{'md-work-author'}) {
621
0
            $work_author = $hit->{'md-work-author'}->[0];
622        }
623
0
        my $group_label = (defined $work_author) ? "$work_title / $work_author" : $work_title;
624
625
0
        my $result_group = {};
626
0
        $result_group->{'group_label'} = $group_label;
627
0
        $result_group->{'group_merge_key'} = $recid;
628
629
0
        my $count = 1;
630
0
        if (exists $hit->{count}) {
631
0
            $count = $hit->{count}->[0];
632        }
633
0
        $result_group->{'group_count'} = $count;
634
635        for (my $i = 0; $i < $count; $i++) {
636            # FIXME -- may need to worry about diacritics here
637
0
            my $rec = $paz->record($recid, $i);
638
0
0
            push @{ $result_group->{'RECORDS'} }, $rec;
639
0
        }
640
641
0
0
        push @{ $results_hashref->{'biblioserver'}->{'GROUPS'} }, $result_group;
642    }
643
644    # pass through facets
645
0
    my $termlist_xml = $paz->termlist('author,subject');
646
0
    my $terms = XMLin($termlist_xml, forcearray => 1);
647
0
    my @facets_loop = ();
648    #die Dumper($results);
649# foreach my $list (sort keys %{ $terms->{'list'} }) {
650# my @facets = ();
651# foreach my $facet (sort @{ $terms->{'list'}->{$list}->{'term'} } ) {
652# push @facets, {
653# facet_label_value => $facet->{'name'}->[0],
654# };
655# }
656# push @facets_loop, ( {
657# type_label => $list,
658# facets => \@facets,
659# } );
660# }
661
662
0
    return ( undef, $results_hashref, \@facets_loop );
663}
664
665# STOPWORDS
666sub _remove_stopwords {
667
0
    my ( $operand, $index ) = @_;
668
0
    my @stopwords_removed;
669
670    # phrase and exact-qualified indexes shouldn't have stopwords removed
671
0
    if ( $index !~ m/phr|ext/ ) {
672
673# remove stopwords from operand : parse all stopwords & remove them (case insensitive)
674# we use IsAlpha unicode definition, to deal correctly with diacritics.
675# otherwise, a French word like "leçon" woudl be split into "le" "çon", "le"
676# is a stopword, we'd get "çon" and wouldn't find anything...
677#
678
0
0
                foreach ( keys %{ C4::Context->stopwords } ) {
679
0
                        next if ( $_ =~ /(and|or|not)/ ); # don't remove operators
680
0
                        if ( my ($matched) = ($operand =~
681                                /([^\X\p{isAlnum}]\Q$_\E[^\X\p{isAlnum}]|[^\X\p{isAlnum}]\Q$_\E$|^\Q$_\E[^\X\p{isAlnum}])/gi))
682                        {
683
0
                                $operand =~ s/\Q$matched\E/ /gi;
684
0
                                push @stopwords_removed, $_;
685                        }
686                }
687        }
688
0
    return ( $operand, \@stopwords_removed );
689}
690
691# TRUNCATION
692sub _detect_truncation {
693
0
    my ( $operand, $index ) = @_;
694
0
    my ( @nontruncated, @righttruncated, @lefttruncated, @rightlefttruncated,
695        @regexpr );
696
0
    $operand =~ s/^ //g;
697
0
    my @wordlist = split( /\s/, $operand );
698
0
    foreach my $word (@wordlist) {
699
0
        if ( $word =~ s/^\*([^\*]+)\*$/$1/ ) {
700
0
            push @rightlefttruncated, $word;
701        }
702        elsif ( $word =~ s/^\*([^\*]+)$/$1/ ) {
703
0
            push @lefttruncated, $word;
704        }
705        elsif ( $word =~ s/^([^\*]+)\*$/$1/ ) {
706
0
            push @righttruncated, $word;
707        }
708        elsif ( index( $word, "*" ) < 0 ) {
709
0
            push @nontruncated, $word;
710        }
711        else {
712
0
            push @regexpr, $word;
713        }
714    }
715    return (
716
0
        \@nontruncated, \@righttruncated, \@lefttruncated,
717        \@rightlefttruncated, \@regexpr
718    );
719}
720
721# STEMMING
722sub _build_stemmed_operand {
723
0
    my ($operand,$lang) = @_;
724
0
    require Lingua::Stem::Snowball ;
725
0
    my $stemmed_operand;
726
727    # If operand contains a digit, it is almost certainly an identifier, and should
728    # not be stemmed. This is particularly relevant for ISBNs and ISSNs, which
729    # can contain the letter "X" - for example, _build_stemmend_operand would reduce
730    # "014100018X" to "x ", which for a MARC21 database would bring up irrelevant
731    # results (e.g., "23 x 29 cm." from the 300$c). Bug 2098.
732
0
    return $operand if $operand =~ /\d/;
733
734# FIXME: the locale should be set based on the user's language and/or search choice
735    #warn "$lang";
736
0
    my $stemmer = Lingua::Stem::Snowball->new( lang => $lang,
737                                               encoding => "UTF-8" );
738
739
0
    my @words = split( / /, $operand );
740
0
    my @stems = $stemmer->stem(\@words);
741
0
    for my $stem (@stems) {
742
0
        $stemmed_operand .= "$stem";
743
0
        $stemmed_operand .= "?"
744          unless ( $stem =~ /(and$|or$|not$)/ ) || ( length($stem) < 3 );
745
0
        $stemmed_operand .= " ";
746    }
747
0
    warn "STEMMED OPERAND: $stemmed_operand" if $DEBUG;
748
0
    return $stemmed_operand;
749}
750
751# FIELD WEIGHTING
752sub _build_weighted_query {
753
754# FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
755# pretty well but could work much better if we had a smarter query parser
756
0
    my ( $operand, $stemmed_operand, $index ) = @_;
757
0
    my $stemming = C4::Context->preference("QueryStemming") || 0;
758
0
    my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
759
0
    my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
760
761
0
    my $weighted_query .= "(rk=("; # Specifies that we're applying rank
762
763    # Keyword, or, no index specified
764
0
    if ( ( $index eq 'kw' ) || ( !$index ) ) {
765
0
        $weighted_query .=
766          "Title-cover,ext,r1=\"$operand\""; # exact title-cover
767
0
        $weighted_query .= " or ti,ext,r2=\"$operand\""; # exact title
768
0
        $weighted_query .= " or ti,phr,r3=\"$operand\""; # phrase title
769          #$weighted_query .= " or any,ext,r4=$operand"; # exact any
770          #$weighted_query .=" or kw,wrdl,r5=\"$operand\""; # word list any
771
0
        $weighted_query .= " or wrdl,fuzzy,r8=\"$operand\""
772          if $fuzzy_enabled; # add fuzzy, word list
773
0
        $weighted_query .= " or wrdl,right-Truncation,r9=\"$stemmed_operand\""
774          if ( $stemming and $stemmed_operand )
775          ; # add stemming, right truncation
776
0
        $weighted_query .= " or wrdl,r9=\"$operand\"";
777
778        # embedded sorting: 0 a-z; 1 z-a
779        # $weighted_query .= ") or (sort1,aut=1";
780    }
781
782    # Barcode searches should skip this process
783    elsif ( $index eq 'bc' ) {
784
0
        $weighted_query .= "bc=\"$operand\"";
785    }
786
787    # Authority-number searches should skip this process
788    elsif ( $index eq 'an' ) {
789
0
        $weighted_query .= "an=\"$operand\"";
790    }
791
792    # If the index already has more than one qualifier, wrap the operand
793    # in quotes and pass it back (assumption is that the user knows what they
794    # are doing and won't appreciate us mucking up their query
795    elsif ( $index =~ ',' ) {
796
0
        $weighted_query .= " $index=\"$operand\"";
797    }
798
799    #TODO: build better cases based on specific search indexes
800    else {
801
0
        $weighted_query .= " $index,ext,r1=\"$operand\""; # exact index
802          #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
803
0
        $weighted_query .= " or $index,phr,r3=\"$operand\""; # phrase index
804
0
        $weighted_query .=
805          " or $index,rt,wrdl,r3=\"$operand\""; # word list index
806    }
807
808
0
    $weighted_query .= "))"; # close rank specification
809
0
    return $weighted_query;
810}
811
812 - 816
=head2 getIndexes

Return an array with available indexes.

=cut
817
818sub getIndexes{
819
0
    my @indexes = (
820                    # biblio indexes
821                    'ab',
822                    'Abstract',
823                    'acqdate',
824                    'allrecords',
825                    'an',
826                    'Any',
827                    'at',
828                    'au',
829                    'aub',
830                    'aud',
831                    'audience',
832                    'auo',
833                    'aut',
834                    'Author',
835                    'Author-in-order ',
836                    'Author-personal-bibliography',
837                    'Authority-Number',
838                    'authtype',
839                    'bc',
840                    'Bib-level',
841                    'biblionumber',
842                    'bio',
843                    'biography',
844                    'callnum',
845                    'cfn',
846                    'Chronological-subdivision',
847                    'cn-bib-source',
848                    'cn-bib-sort',
849                    'cn-class',
850                    'cn-item',
851                    'cn-prefix',
852                    'cn-suffix',
853                    'cpn',
854                    'Code-institution',
855                    'Conference-name',
856                    'Conference-name-heading',
857                    'Conference-name-see',
858                    'Conference-name-seealso',
859                    'Content-type',
860                    'Control-number',
861                    'copydate',
862                    'Corporate-name',
863                    'Corporate-name-heading',
864                    'Corporate-name-see',
865                    'Corporate-name-seealso',
866                    'ctype',
867                    'date-entered-on-file',
868                    'Date-of-acquisition',
869                    'Date-of-publication',
870                    'Dewey-classification',
871                    'EAN',
872                    'extent',
873                    'fic',
874                    'fiction',
875                    'Form-subdivision',
876                    'format',
877                    'Geographic-subdivision',
878                    'he',
879                    'Heading',
880                    'Heading-use-main-or-added-entry',
881                    'Heading-use-series-added-entry ',
882                    'Heading-use-subject-added-entry',
883                    'Host-item',
884                    'id-other',
885                    'Illustration-code',
886                    'ISBN',
887                    'isbn',
888                    'ISSN',
889                    'issn',
890                    'itemtype',
891                    'kw',
892                    'Koha-Auth-Number',
893                    'l-format',
894                    'language',
895                    'lc-card',
896                    'LC-card-number',
897                    'lcn',
898                    'llength',
899                    'ln',
900                    'Local-classification',
901                    'Local-number',
902                    'Match-heading',
903                    'Match-heading-see-from',
904                    'Material-type',
905                    'mc-itemtype',
906                    'mc-rtype',
907                    'mus',
908                    'name',
909                    'Music-number',
910                    'Name-geographic',
911                    'Name-geographic-heading',
912                    'Name-geographic-see',
913                    'Name-geographic-seealso',
914                    'nb',
915                    'Note',
916                    'notes',
917                    'ns',
918                    'nt',
919                    'pb',
920                    'Personal-name',
921                    'Personal-name-heading',
922                    'Personal-name-see',
923                    'Personal-name-seealso',
924                    'pl',
925                    'Place-publication',
926                    'pn',
927                    'popularity',
928                    'pubdate',
929                    'Publisher',
930                    'Record-control-number',
931                    'rcn',
932                    'Record-type',
933                    'rtype',
934                    'se',
935                    'See',
936                    'See-also',
937                    'sn',
938                    'Stock-number',
939                    'su',
940                    'Subject',
941                    'Subject-heading-thesaurus',
942                    'Subject-name-personal',
943                    'Subject-subdivision',
944                    'Summary',
945                    'Suppress',
946                    'su-geo',
947                    'su-na',
948                    'su-to',
949                    'su-ut',
950                    'ut',
951                    'UPC',
952                    'Term-genre-form',
953                    'Term-genre-form-heading',
954                    'Term-genre-form-see',
955                    'Term-genre-form-seealso',
956                    'ti',
957                    'Title',
958                    'Title-cover',
959                    'Title-series',
960                    'Title-host',
961                    'Title-uniform',
962                    'Title-uniform-heading',
963                    'Title-uniform-see',
964                    'Title-uniform-seealso',
965                    'totalissues',
966                    'yr',
967
968                    # items indexes
969                    'acqsource',
970                    'barcode',
971                    'bc',
972                    'branch',
973                    'ccode',
974                    'classification-source',
975                    'cn-sort',
976                    'coded-location-qualifier',
977                    'copynumber',
978                    'damaged',
979                    'datelastborrowed',
980                    'datelastseen',
981                    'holdingbranch',
982                    'homebranch',
983                    'issues',
984                    'item',
985                    'itemnumber',
986                    'itype',
987                    'Local-classification',
988                    'location',
989                    'lost',
990                    'materials-specified',
991                    'mc-ccode',
992                    'mc-itype',
993                    'mc-loc',
994                    'notforloan',
995                    'onloan',
996                    'price',
997                    'renewals',
998                    'replacementprice',
999                    'replacementpricedate',
1000                    'reserves',
1001                    'restricted',
1002                    'stack',
1003                    'stocknumber',
1004                    'inv',
1005                    'uri',
1006                    'withdrawn',
1007
1008                    # subject related
1009                  );
1010
1011
0
    return \@indexes;
1012}
1013
1014 - 1028
=head2 buildQuery

( $error, $query,
$simple_query, $query_cgi,
$query_desc, $limit,
$limit_cgi, $limit_desc,
$stopwords_removed, $query_type ) = buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);

Build queries and limits in CCL, CGI, Human,
handle truncation, stemming, field weighting, stopwords, fuzziness, etc.

See verbose embedded documentation.


=cut
1029
1030sub buildQuery {
1031
0
    my ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang) = @_;
1032
1033
0
    warn "---------\nEnter buildQuery\n---------" if $DEBUG;
1034
1035    # dereference
1036
0
    my @operators = $operators ? @$operators : ();
1037
0
    my @indexes = $indexes ? @$indexes : ();
1038
0
    my @operands = $operands ? @$operands : ();
1039
0
    my @limits = $limits ? @$limits : ();
1040
0
    my @sort_by = $sort_by ? @$sort_by : ();
1041
1042
0
    my $stemming = C4::Context->preference("QueryStemming") || 0;
1043
0
    my $auto_truncation = C4::Context->preference("QueryAutoTruncate") || 0;
1044
0
    my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
1045
0
    my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
1046
0
    my $remove_stopwords = C4::Context->preference("QueryRemoveStopwords") || 0;
1047
1048    # no stemming/weight/fuzzy in NoZebra
1049
0
    if ( C4::Context->preference("NoZebra") ) {
1050
0
        $stemming = 0;
1051
0
        $weight_fields = 0;
1052
0
        $fuzzy_enabled = 0;
1053
0
     $auto_truncation = 0;
1054    }
1055
1056
0
    my $query = $operands[0];
1057
0
    my $simple_query = $operands[0];
1058
1059    # initialize the variables we're passing back
1060
0
    my $query_cgi;
1061
0
    my $query_desc;
1062
0
    my $query_type;
1063
1064
0
    my $limit;
1065
0
    my $limit_cgi;
1066
0
    my $limit_desc;
1067
1068
0
    my $stopwords_removed; # flag to determine if stopwords have been removed
1069
1070
0
    my $cclq = 0;
1071
0
    my $cclindexes = getIndexes();
1072
0
    if ( $query !~ /\s*ccl=/ ) {
1073
0
        while ( !$cclq && $query =~ /(?:^|\W)([\w-]+)(,[\w-]+)*[:=]/g ) {
1074
0
            my $dx = lc($1);
1075
0
0
            $cclq = grep { lc($_) eq $dx } @$cclindexes;
1076        }
1077
0
        $query = "ccl=$query" if $cclq;
1078    }
1079
1080# for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
1081# DIAGNOSTIC ONLY!!
1082
0
    if ( $query =~ /^ccl=/ ) {
1083
0
        my $q=$';
1084        # This is needed otherwise ccl= and &limit won't work together, and
1085        # this happens when selecting a subject on the opac-detail page
1086
0
        if (@limits) {
1087
0
            $q .= ' and '.join(' and ', @limits);
1088        }
1089
0
        return ( undef, $q, $q, "q=ccl=$q", $q, '', '', '', '', 'ccl' );
1090    }
1091
0
    if ( $query =~ /^cql=/ ) {
1092
0
        return ( undef, $', $', "q=cql=$'", $', '', '', '', '', 'cql' );
1093    }
1094
0
    if ( $query =~ /^pqf=/ ) {
1095
0
        return ( undef, $', $', "q=pqf=$'", $', '', '', '', '', 'pqf' );
1096    }
1097
1098    # pass nested queries directly
1099    # FIXME: need better handling of some of these variables in this case
1100    # Nested queries aren't handled well and this implementation is flawed and causes users to be
1101    # unable to search for anything containing () commenting out, will be rewritten for 3.4.0
1102# if ( $query =~ /(\(|\))/ ) {
1103# return (
1104# undef, $query, $simple_query, $query_cgi,
1105# $query, $limit, $limit_cgi, $limit_desc,
1106# $stopwords_removed, 'ccl'
1107# );
1108# }
1109
1110# Form-based queries are non-nested and fixed depth, so we can easily modify the incoming
1111# query operands and indexes and add stemming, truncation, field weighting, etc.
1112# Once we do so, we'll end up with a value in $query, just like if we had an
1113# incoming $query from the user
1114    else {
1115
0
        $query = ""
1116          ; # clear it out so we can populate properly with field-weighted, stemmed, etc. query
1117
0
        my $previous_operand
1118          ; # a flag used to keep track if there was a previous query
1119               # if there was, we can apply the current operator
1120               # for every operand
1121        for ( my $i = 0 ; $i <= @operands ; $i++ ) {
1122
1123            # COMBINE OPERANDS, INDEXES AND OPERATORS
1124
0
            if ( $operands[$i] ) {
1125
0
                $operands[$i]=~s/^\s+//;
1126
1127              # A flag to determine whether or not to add the index to the query
1128
0
                my $indexes_set;
1129
1130# If the user is sophisticated enough to specify an index, turn off field weighting, stemming, and stopword handling
1131
0
                if ( $operands[$i] =~ /\w(:|=)/ || $scan ) {
1132
0
                    $weight_fields = 0;
1133
0
                    $stemming = 0;
1134
0
                    $remove_stopwords = 0;
1135                } else {
1136
0
                    $operands[$i] =~ s/\?/{?}/g; # need to escape question marks
1137                }
1138
0
                my $operand = $operands[$i];
1139
0
                my $index = $indexes[$i];
1140
1141                # Add index-specific attributes
1142                # Date of Publication
1143
0
                if ( $index eq 'yr' ) {
1144
0
                    $index .= ",st-numeric";
1145
0
                    $indexes_set++;
1146
0
                                        $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1147                }
1148
1149                # Date of Acquisition
1150                elsif ( $index eq 'acqdate' ) {
1151
0
                    $index .= ",st-date-normalized";
1152
0
                    $indexes_set++;
1153
0
                                        $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1154                }
1155                # ISBN,ISSN,Standard Number, don't need special treatment
1156                elsif ( $index eq 'nb' || $index eq 'ns' ) {
1157                    (
1158
0
                        $stemming, $auto_truncation,
1159                        $weight_fields, $fuzzy_enabled,
1160                        $remove_stopwords
1161                    ) = ( 0, 0, 0, 0, 0 );
1162
1163                }
1164
1165
0
                if(not $index){
1166
0
                    $index = 'kw';
1167                }
1168
1169                # Set default structure attribute (word list)
1170
0
                my $struct_attr = q{};
1171
0
                unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl|nb|ns)/ ) {
1172
0
                    $struct_attr = ",wrdl";
1173                }
1174
1175                # Some helpful index variants
1176
0
                my $index_plus = $index . $struct_attr . ':';
1177
0
                my $index_plus_comma = $index . $struct_attr . ',';
1178
1179                # Remove Stopwords
1180
0
                if ($remove_stopwords) {
1181
0
                    ( $operand, $stopwords_removed ) =
1182                      _remove_stopwords( $operand, $index );
1183
0
                    warn "OPERAND w/out STOPWORDS: >$operand<" if $DEBUG;
1184
0
                    warn "REMOVED STOPWORDS: @$stopwords_removed"
1185                      if ( $stopwords_removed && $DEBUG );
1186                }
1187
1188
0
                if ($auto_truncation){
1189
0
                                        unless ( $index =~ /(st-|phr|ext)/ ) {
1190                                                #FIXME only valid with LTR scripts
1191
0
                                                $operand=join(" ",map{
1192
0
                                                                                        (index($_,"*")>0?"$_":"$_*")
1193                                                                                         }split (/\s+/,$operand));
1194
0
                                                warn $operand if $DEBUG;
1195                                        }
1196                                }
1197
1198                # Detect Truncation
1199
0
                my $truncated_operand;
1200
0
                my( $nontruncated, $righttruncated, $lefttruncated,
1201                    $rightlefttruncated, $regexpr
1202                ) = _detect_truncation( $operand, $index );
1203
0
                warn
1204"TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<"
1205                  if $DEBUG;
1206
1207                # Apply Truncation
1208
0
                if (
1209                    scalar(@$righttruncated) + scalar(@$lefttruncated) +
1210                    scalar(@$rightlefttruncated) > 0 )
1211                {
1212
1213               # Don't field weight or add the index to the query, we do it here
1214
0
                    $indexes_set = 1;
1215
0
                    undef $weight_fields;
1216
0
                    my $previous_truncation_operand;
1217
0
                    if (scalar @$nontruncated) {
1218
0
                        $truncated_operand .= "$index_plus @$nontruncated ";
1219
0
                        $previous_truncation_operand = 1;
1220                    }
1221
0
                    if (scalar @$righttruncated) {
1222
0
                        $truncated_operand .= "and " if $previous_truncation_operand;
1223
0
                        $truncated_operand .= $index_plus_comma . "rtrn:@$righttruncated ";
1224
0
                        $previous_truncation_operand = 1;
1225                    }
1226
0
                    if (scalar @$lefttruncated) {
1227
0
                        $truncated_operand .= "and " if $previous_truncation_operand;
1228
0
                        $truncated_operand .= $index_plus_comma . "ltrn:@$lefttruncated ";
1229
0
                        $previous_truncation_operand = 1;
1230                    }
1231
0
                    if (scalar @$rightlefttruncated) {
1232
0
                        $truncated_operand .= "and " if $previous_truncation_operand;
1233
0
                        $truncated_operand .= $index_plus_comma . "rltrn:@$rightlefttruncated ";
1234
0
                        $previous_truncation_operand = 1;
1235                    }
1236                }
1237
0
                $operand = $truncated_operand if $truncated_operand;
1238
0
                warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
1239
1240                # Handle Stemming
1241
0
                my $stemmed_operand;
1242
0
                $stemmed_operand = _build_stemmed_operand($operand, $lang)
1243                                                                                if $stemming;
1244
1245
0
                warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
1246
1247                # Handle Field Weighting
1248
0
                my $weighted_operand;
1249
0
                if ($weight_fields) {
1250
0
                    $weighted_operand = _build_weighted_query( $operand, $stemmed_operand, $index );
1251
0
                    $operand = $weighted_operand;
1252
0
                    $indexes_set = 1;
1253                }
1254
1255
0
                warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
1256
1257                # If there's a previous operand, we need to add an operator
1258
0
                if ($previous_operand) {
1259
1260                    # User-specified operator
1261
0
                    if ( $operators[ $i - 1 ] ) {
1262
0
                        $query .= " $operators[$i-1] ";
1263
0
                        $query .= " $index_plus " unless $indexes_set;
1264
0
                        $query .= " $operand";
1265
0
                        $query_cgi .= "&op=$operators[$i-1]";
1266
0
                        $query_cgi .= "&idx=$index" if $index;
1267
0
                        $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1268
0
                        $query_desc .=
1269                          " $operators[$i-1] $index_plus $operands[$i]";
1270                    }
1271
1272                    # Default operator is and
1273                    else {
1274
0
                        $query .= " and ";
1275
0
                        $query .= "$index_plus " unless $indexes_set;
1276
0
                        $query .= "$operand";
1277
0
                        $query_cgi .= "&op=and&idx=$index" if $index;
1278
0
                        $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1279
0
                        $query_desc .= " and $index_plus $operands[$i]";
1280                    }
1281                }
1282
1283                # There isn't a pervious operand, don't need an operator
1284                else {
1285
1286                    # Field-weighted queries already have indexes set
1287
0
                    $query .= " $index_plus " unless $indexes_set;
1288
0
                    $query .= $operand;
1289
0
                    $query_desc .= " $index_plus $operands[$i]";
1290
0
                    $query_cgi .= "&idx=$index" if $index;
1291
0
                    $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1292
0
                    $previous_operand = 1;
1293                }
1294            } #/if $operands
1295
0
        } # /for
1296    }
1297
0
    warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1298
1299    # add limits
1300
0
    my %group_OR_limits;
1301
0
    my $availability_limit;
1302
0
    foreach my $this_limit (@limits) {
1303
0
        if ( $this_limit =~ /available/ ) {
1304#
1305## 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1306## In English:
1307## all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1308
0
            $availability_limit .=
1309"( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and (lost,st-numeric=0) )"; #or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
1310
0
            $limit_cgi .= "&limit=available";
1311
0
            $limit_desc .= "";
1312        }
1313
1314        # group_OR_limits, prefixed by mc-
1315        # OR every member of the group
1316        elsif ( $this_limit =~ /mc/ ) {
1317
0
            my ($k,$v) = split(/:/, $this_limit,2);
1318
0
            if ( $k !~ /mc-i(tem)?type/ ) {
1319                # in case the mc-ccode value has complicating chars like ()'s inside it we wrap in quotes
1320
0
                $this_limit =~ tr/"//d;
1321
0
                $this_limit = $k.":\"".$v."\"";
1322            }
1323
1324
0
            $group_OR_limits{$k} .= " or " if $group_OR_limits{$k};
1325
0
            $limit_desc .= " or " if $group_OR_limits{$k};
1326
0
            $group_OR_limits{$k} .= "$this_limit";
1327
0
            $limit_cgi .= "&limit=$this_limit";
1328
0
            $limit_desc .= " $this_limit";
1329        }
1330
1331        # Regular old limits
1332        else {
1333
0
            $limit .= " and " if $limit || $query;
1334
0
            $limit .= "$this_limit";
1335
0
            $limit_cgi .= "&limit=$this_limit";
1336
0
            if ($this_limit =~ /^branch:(.+)/) {
1337
0
                my $branchcode = $1;
1338
0
                my $branchname = GetBranchName($branchcode);
1339
0
                if (defined $branchname) {
1340
0
                    $limit_desc .= " branch:$branchname";
1341                } else {
1342
0
                    $limit_desc .= " $this_limit";
1343                }
1344            } else {
1345
0
                $limit_desc .= " $this_limit";
1346            }
1347        }
1348    }
1349
0
    foreach my $k (keys (%group_OR_limits)) {
1350
0
        $limit .= " and " if ( $query || $limit );
1351
0
        $limit .= "($group_OR_limits{$k})";
1352    }
1353
0
    if ($availability_limit) {
1354
0
        $limit .= " and " if ( $query || $limit );
1355
0
        $limit .= "($availability_limit)";
1356    }
1357
1358    # Normalize the query and limit strings
1359    # This is flawed , means we can't search anything with : in it
1360    # if user wants to do ccl or cql, start the query with that
1361# $query =~ s/:/=/g;
1362
0
    $query =~ s/(?<=(ti|au|pb|su|an|kw|mc|nb|ns)):/=/g;
1363
0
    $query =~ s/(?<=(wrdl)):/=/g;
1364
0
    $query =~ s/(?<=(trn|phr)):/=/g;
1365
0
    $limit =~ s/:/=/g;
1366
0
    for ( $query, $query_desc, $limit, $limit_desc ) {
1367
0
        s/ +/ /g; # remove extra spaces
1368
0
        s/^ //g; # remove any beginning spaces
1369
0
        s/ $//g; # remove any ending spaces
1370
0
        s/==/=/g; # remove double == from query
1371    }
1372
0
    $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1373
1374
0
    for ($query_cgi,$simple_query) {
1375
0
        s/"//g;
1376    }
1377    # append the limit to the query
1378
0
    $query .= " " . $limit;
1379
1380    # Warnings if DEBUG
1381
0
    if ($DEBUG) {
1382
0
        warn "QUERY:" . $query;
1383
0
        warn "QUERY CGI:" . $query_cgi;
1384
0
        warn "QUERY DESC:" . $query_desc;
1385
0
        warn "LIMIT:" . $limit;
1386
0
        warn "LIMIT CGI:" . $limit_cgi;
1387
0
        warn "LIMIT DESC:" . $limit_desc;
1388
0
        warn "---------\nLeave buildQuery\n---------";
1389    }
1390    return (
1391
0
        undef, $query, $simple_query, $query_cgi,
1392        $query_desc, $limit, $limit_cgi, $limit_desc,
1393        $stopwords_removed, $query_type
1394    );
1395}
1396
1397 - 1405
=head2 searchResults

  my @search_results = searchResults($search_context, $searchdesc, $hits, 
                                     $results_per_page, $offset, $scan, 
                                     @marcresults, $hidelostitems);

Format results in a form suitable for passing to the template

=cut
1406
1407# IMO this subroutine is pretty messy still -- it's responsible for
1408# building the HTML output for the template
1409sub searchResults {
1410
0
    my ( $search_context, $searchdesc, $hits, $results_per_page, $offset, $scan, $marcresults ) = @_;
1411
0
    my $dbh = C4::Context->dbh;
1412
0
    my @newresults;
1413
1414
0
    $search_context = 'opac' if !$search_context || $search_context ne 'intranet';
1415
0
    my ($is_opac, $hidelostitems);
1416
0
    if ($search_context eq 'opac') {
1417
0
        $hidelostitems = C4::Context->preference('hidelostitems');
1418
0
        $is_opac = 1;
1419    }
1420
1421    #Build branchnames hash
1422    #find branchname
1423    #get branch information.....
1424
0
    my %branches;
1425
0
    my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
1426
0
    $bsth->execute();
1427
0
    while ( my $bdata = $bsth->fetchrow_hashref ) {
1428
0
        $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
1429    }
1430# FIXME - We build an authorised values hash here, using the default framework
1431# though it is possible to have different authvals for different fws.
1432
1433
0
    my $shelflocations =GetKohaAuthorisedValues('items.location','');
1434
1435    # get notforloan authorised value list (see $shelflocations FIXME)
1436
0
    my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
1437
1438    #Build itemtype hash
1439    #find itemtype & itemtype image
1440
0
    my %itemtypes;
1441
0
    $bsth =
1442      $dbh->prepare(
1443        "SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes"
1444      );
1445
0
    $bsth->execute();
1446
0
    while ( my $bdata = $bsth->fetchrow_hashref ) {
1447
0
                foreach (qw(description imageurl summary notforloan)) {
1448
0
         $itemtypes{ $bdata->{'itemtype'} }->{$_} = $bdata->{$_};
1449                }
1450    }
1451
1452    #search item field code
1453
0
    my $sth =
1454      $dbh->prepare(
1455"SELECT tagfield FROM marc_subfield_structure WHERE kohafield LIKE 'items.itemnumber'"
1456      );
1457
0
    $sth->execute;
1458
0
    my ($itemtag) = $sth->fetchrow;
1459
1460    ## find column names of items related to MARC
1461
0
    my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
1462
0
    $sth2->execute;
1463
0
    my %subfieldstosearch;
1464
0
    while ( ( my $column ) = $sth2->fetchrow ) {
1465
0
        my ( $tagfield, $tagsubfield ) =
1466          &GetMarcFromKohaField( "items." . $column, "" );
1467
0
        $subfieldstosearch{$column} = $tagsubfield;
1468    }
1469
1470    # handle which records to actually retrieve
1471
0
    my $times;
1472
0
    if ( $hits && $offset + $results_per_page <= $hits ) {
1473
0
        $times = $offset + $results_per_page;
1474    }
1475    else {
1476
0
        $times = $hits; # FIXME: if $hits is undefined, why do we want to equal it?
1477    }
1478
1479
0
        my $marcflavour = C4::Context->preference("marcflavour");
1480    # We get the biblionumber position in MARC
1481
0
    my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
1482
1483    # loop through all of the records we've retrieved
1484    for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1485
0
        my $marcrecord = MARC::File::USMARC::decode( $marcresults->[$i] );
1486
0
        my $fw = $scan
1487             ? undef
1488             : $bibliotag < 10
1489               ? GetFrameworkCode($marcrecord->field($bibliotag)->data)
1490               : GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
1491
0
        my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, $fw );
1492
0
        $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
1493
0
        $oldbiblio->{result_number} = $i + 1;
1494
1495        # add imageurl to itemtype if there is one
1496
0
        $oldbiblio->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1497
1498
0
        $oldbiblio->{'authorised_value_images'} = ($search_context eq 'opac' && C4::Context->preference('AuthorisedValueImages')) || ($search_context eq 'intranet' && C4::Context->preference('StaffAuthorisedValueImages')) ? C4::Items::get_authorised_value_images( C4::Biblio::get_biblio_authorised_values( $oldbiblio->{'biblionumber'}, $marcrecord ) ) : [];
1499
0
                $oldbiblio->{normalized_upc} = GetNormalizedUPC( $marcrecord,$marcflavour);
1500
0
                $oldbiblio->{normalized_ean} = GetNormalizedEAN( $marcrecord,$marcflavour);
1501
0
                $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
1502
0
                $oldbiblio->{normalized_isbn} = GetNormalizedISBN(undef,$marcrecord,$marcflavour);
1503
0
                $oldbiblio->{content_identifier_exists} = 1 if ($oldbiblio->{normalized_isbn} or $oldbiblio->{normalized_oclc} or $oldbiblio->{normalized_ean} or $oldbiblio->{normalized_upc});
1504
1505                # edition information, if any
1506
0
        $oldbiblio->{edition} = $oldbiblio->{editionstatement};
1507
0
                $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1508 # Build summary if there is one (the summary is defined in the itemtypes table)
1509 # FIXME: is this used anywhere, I think it can be commented out? -- JF
1510
0
        if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1511
0
            my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1512
0
            my @fields = $marcrecord->fields();
1513
1514
0
            my $newsummary;
1515
0
            foreach my $line ( "$summary\n" =~ /(.*)\n/g ){
1516
0
                my $tags = {};
1517
0
                foreach my $tag ( $line =~ /\[(\d{3}[\w|\d])\]/ ) {
1518
0
                    $tag =~ /(.{3})(.)/;
1519
0
                    if($marcrecord->field($1)){
1520
0
                        my @abc = $marcrecord->field($1)->subfield($2);
1521
0
                        $tags->{$tag} = $#abc + 1 ;
1522                    }
1523                }
1524
1525                # We catch how many times to repeat this line
1526
0
                my $max = 0;
1527
0
                foreach my $tag (keys(%$tags)){
1528
0
                    $max = $tags->{$tag} if($tags->{$tag} > $max);
1529                 }
1530
1531                # we replace, and repeat each line
1532                for (my $i = 0 ; $i < $max ; $i++){
1533
0
                    my $newline = $line;
1534
1535
0
                    foreach my $tag ( $newline =~ /\[(\d{3}[\w|\d])\]/g ) {
1536
0
                        $tag =~ /(.{3})(.)/;
1537
1538
0
                        if($marcrecord->field($1)){
1539
0
                            my @repl = $marcrecord->field($1)->subfield($2);
1540
0
                            my $subfieldvalue = $repl[$i];
1541
1542
0
                            if (! utf8::is_utf8($subfieldvalue)) {
1543
0
                                utf8::decode($subfieldvalue);
1544                            }
1545
1546
0
                             $newline =~ s/\[$tag\]/$subfieldvalue/g;
1547                        }
1548                    }
1549
0
                    $newsummary .= "$newline\n";
1550
0
                }
1551            }
1552
1553
0
            $newsummary =~ s/\[(.*?)]//g;
1554
0
            $newsummary =~ s/\n/<br\/>/g;
1555
0
            $oldbiblio->{summary} = $newsummary;
1556        }
1557
1558        # Pull out the items fields
1559
0
        my @fields = $marcrecord->field($itemtag);
1560
0
        my $marcflavor = C4::Context->preference("marcflavour");
1561        # adding linked items that belong to host records
1562
0
        my $analyticsfield = '773';
1563
0
        if ($marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC') {
1564
0
            $analyticsfield = '773';
1565        } elsif ($marcflavor eq 'UNIMARC') {
1566
0
            $analyticsfield = '461';
1567        }
1568
0
        foreach my $hostfield ( $marcrecord->field($analyticsfield)) {
1569
0
            my $hostbiblionumber = $hostfield->subfield("0");
1570
0
            my $linkeditemnumber = $hostfield->subfield("9");
1571
0
            if(!$hostbiblionumber eq undef){
1572
0
                my $hostbiblio = GetMarcBiblio($hostbiblionumber, 1);
1573
0
                my ($itemfield, undef) = GetMarcFromKohaField( 'items.itemnumber', GetFrameworkCode($hostbiblionumber) );
1574
0
                if(!$hostbiblio eq undef){
1575
0
                    my @hostitems = $hostbiblio->field($itemfield);
1576
0
                    foreach my $hostitem (@hostitems){
1577
0
                        if ($hostitem->subfield("9") eq $linkeditemnumber){
1578
0
                            my $linkeditem =$hostitem;
1579                            # append linked items if they exist
1580
0
                            if (!$linkeditem eq undef){
1581
0
                                push (@fields, $linkeditem);}
1582                        }
1583                    }
1584                }
1585            }
1586        }
1587
1588        # Setting item statuses for display
1589
0
        my @available_items_loop;
1590
0
        my @onloan_items_loop;
1591
0
        my @other_items_loop;
1592
1593
0
        my $available_items;
1594
0
        my $onloan_items;
1595
0
        my $other_items;
1596
1597
0
        my $ordered_count = 0;
1598
0
        my $available_count = 0;
1599
0
        my $onloan_count = 0;
1600
0
        my $longoverdue_count = 0;
1601
0
        my $other_count = 0;
1602
0
        my $wthdrawn_count = 0;
1603
0
        my $itemlost_count = 0;
1604
0
        my $hideatopac_count = 0;
1605
0
        my $itembinding_count = 0;
1606
0
        my $itemdamaged_count = 0;
1607
0
        my $item_in_transit_count = 0;
1608
0
        my $can_place_holds = 0;
1609
0
        my $item_onhold_count = 0;
1610
0
        my $items_count = scalar(@fields);
1611
0
        my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults');
1612
0
        my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1;
1613
1614        # loop through every item
1615
0
              my @hiddenitems;
1616
0
        foreach my $field (@fields) {
1617
0
            my $item;
1618
1619            # populate the items hash
1620
0
            foreach my $code ( keys %subfieldstosearch ) {
1621
0
                $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1622            }
1623
0
            $item->{description} = $itemtypes{ $item->{itype} }{description};
1624
1625                # Hidden items
1626
0
            if ($is_opac) {
1627
0
                    my @hi = GetHiddenItemnumbers($item);
1628
0
                $item->{'hideatopac'} = @hi;
1629
0
              push @hiddenitems, @hi;
1630            }
1631
1632
0
            my $hbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch' : 'holdingbranch';
1633
0
            my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
1634
1635            # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
1636
0
            if ($item->{$hbranch}) {
1637
0
                $item->{'branchname'} = $branches{$item->{$hbranch}};
1638            }
1639            elsif ($item->{$otherbranch}) { # Last resort
1640
0
                $item->{'branchname'} = $branches{$item->{$otherbranch}};
1641            }
1642
1643
0
                        my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
1644# For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
1645
0
            my $userenv = C4::Context->userenv;
1646
0
            if ( $item->{onloan} && !(C4::Members::GetHideLostItemsPreference($userenv->{'number'}) && $item->{itemlost}) ) {
1647
0
                $onloan_count++;
1648
0
                                my $key = $prefix . $item->{onloan} . $item->{barcode};
1649
0
                                $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
1650
0
                                $onloan_items->{$key}->{count}++ if $item->{$hbranch};
1651
0
                                $onloan_items->{$key}->{branchname} = $item->{branchname};
1652
0
                                $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1653
0
                                $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
1654
0
                                $onloan_items->{$key}->{description} = $item->{description};
1655
0
                                $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
1656                # if something's checked out and lost, mark it as 'long overdue'
1657
0
                if ( $item->{itemlost} ) {
1658
0
                    $onloan_items->{$prefix}->{longoverdue}++;
1659
0
                    $longoverdue_count++;
1660                } else { # can place holds as long as item isn't lost
1661
0
                    $can_place_holds = 1;
1662                }
1663            }
1664
1665         # items not on loan, but still unavailable ( lost, withdrawn, damaged )
1666            else {
1667
1668                # item is on order
1669
0
                if ( $item->{notforloan} == -1 ) {
1670
0
                    $ordered_count++;
1671                }
1672
1673                # is item in transit?
1674
0
                my $transfertwhen = '';
1675
0
                my ($transfertfrom, $transfertto);
1676
1677                # is item on the reserve shelf?
1678
0
                my $reservestatus = '';
1679
0
                my $reserveitem;
1680
1681
0
                unless ($item->{wthdrawn}
1682                        || $item->{itemlost}
1683                        || $item->{damaged}
1684                        || $item->{notforloan}
1685                        || $items_count > 20) {
1686
1687                    # A couple heuristics to limit how many times
1688                    # we query the database for item transfer information, sacrificing
1689                    # accuracy in some cases for speed;
1690                    #
1691                    # 1. don't query if item has one of the other statuses
1692                    # 2. don't check transit status if the bib has
1693                    # more than 20 items
1694                    #
1695                    # FIXME: to avoid having the query the database like this, and to make
1696                    # the in transit status count as unavailable for search limiting,
1697                    # should map transit status to record indexed in Zebra.
1698                    #
1699
0
                    ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
1700
0
                    ($reservestatus, $reserveitem, undef) = C4::Reserves::CheckReserves($item->{itemnumber});
1701                }
1702
1703                # item is withdrawn, lost, damaged, not for loan, reserved or in transit
1704
0
                if ( $item->{wthdrawn}
1705                    || $item->{itemlost}
1706                    || $item->{damaged}
1707                    || $item->{notforloan} > 0
1708                    || $item->{hideatopac}
1709                    || $reservestatus eq 'Waiting'
1710                    || ($transfertwhen ne ''))
1711                {
1712
0
                    $wthdrawn_count++ if $item->{wthdrawn};
1713
0
                    $itemlost_count++ if $item->{itemlost};
1714
0
                    $itemdamaged_count++ if $item->{damaged};
1715
0
                    $hideatopac_count++ if $item->{hideatopac};
1716
0
                    $item_in_transit_count++ if $transfertwhen ne '';
1717
0
                    $item_onhold_count++ if $reservestatus eq 'Waiting';
1718
0
                    $item->{status} = $item->{wthdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
1719
1720                    # can place hold on item ?
1721
0
                    if ((!$item->{damaged} || C4::Context->preference('AllowHoldsOnDamagedItems'))
1722                      && !$item->{itemlost}
1723                      && !$item->{withdrawn}
1724                    ) {
1725
0
                        $can_place_holds = 1;
1726                    }
1727
1728
0
                    $other_count++;
1729
1730
0
                    my $key = $prefix . $item->{status};
1731
0
                    foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber hideatopac)) {
1732
0
                        $other_items->{$key}->{$_} = $item->{$_};
1733                    }
1734
0
                    $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
1735
0
                    $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
1736
0
                                        $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
1737
0
                                        $other_items->{$key}->{count}++ if $item->{$hbranch};
1738
0
                                        $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1739
0
                                        $other_items->{$key}->{description} = $item->{description};
1740
0
                                        $other_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
1741                }
1742                # item is available
1743                else {
1744
0
                    $can_place_holds = 1;
1745
0
                    $available_count++;
1746
0
                                        $available_items->{$prefix}->{count}++ if $item->{$hbranch};
1747
0
                                        foreach (qw(branchname itemcallnumber hideatopac description)) {
1748
0
                     $available_items->{$prefix}->{$_} = $item->{$_};
1749                                        }
1750
0
                                        $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
1751
0
                                        $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
1752                }
1753            }
1754        } # notforloan, item level and biblioitem level
1755
0
        if ($items_count > 0) {
1756
0
        next if $is_opac && $hideatopac_count >= $items_count;
1757
0
        next if $hidelostitems && $itemlost_count >= $items_count;
1758        }
1759
0
        my ( $availableitemscount, $onloanitemscount, $otheritemscount );
1760
0
        for my $key ( sort keys %$onloan_items ) {
1761
0
            (++$onloanitemscount > $maxitems) and last;
1762
0
            push @onloan_items_loop, $onloan_items->{$key};
1763        }
1764
0
        for my $key ( sort keys %$other_items ) {
1765
0
            (++$otheritemscount > $maxitems) and last;
1766
0
            push @other_items_loop, $other_items->{$key};
1767        }
1768
0
        for my $key ( sort keys %$available_items ) {
1769
0
            (++$availableitemscount > $maxitems) and last;
1770
0
            push @available_items_loop, $available_items->{$key}
1771        }
1772
1773        # XSLT processing of some stuff
1774
14
14
14
402
171
117318
        use C4::Charset;
1775
0
        SetUTF8Flag($marcrecord);
1776
0
        $debug && warn $marcrecord->as_formatted;
1777
0
        my $interface = $search_context eq 'opac' ? 'OPAC' : '';
1778
0
        if (!$scan && C4::Context->preference($interface . "XSLTResultsDisplay")) {
1779
0
            $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display($oldbiblio->{biblionumber}, $marcrecord, 'Results',
1780                                                                $search_context, 1, \@hiddenitems);
1781            # the last parameter tells Koha to clean up the problematic ampersand entities that Zebra outputs
1782        }
1783
1784        # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
1785
0
        if (!C4::Context->preference("item-level_itypes")) {
1786
0
            if ($itemtypes{ $oldbiblio->{itemtype} }->{notforloan}) {
1787
0
                $can_place_holds = 0;
1788            }
1789        }
1790
0
        $oldbiblio->{norequests} = 1 unless $can_place_holds;
1791
0
        $oldbiblio->{itemsplural} = 1 if $items_count > 1;
1792
0
        $oldbiblio->{items_count} = $items_count;
1793
0
        $oldbiblio->{available_items_loop} = \@available_items_loop;
1794
0
        $oldbiblio->{onloan_items_loop} = \@onloan_items_loop;
1795
0
        $oldbiblio->{other_items_loop} = \@other_items_loop;
1796
0
        $oldbiblio->{availablecount} = $available_count;
1797
0
        $oldbiblio->{availableplural} = 1 if $available_count > 1;
1798
0
        $oldbiblio->{onloancount} = $onloan_count;
1799
0
        $oldbiblio->{onloanplural} = 1 if $onloan_count > 1;
1800
0
        $oldbiblio->{othercount} = $other_count;
1801
0
        $oldbiblio->{otherplural} = 1 if $other_count > 1;
1802
0
        $oldbiblio->{wthdrawncount} = $wthdrawn_count;
1803
0
        $oldbiblio->{itemlostcount} = $itemlost_count;
1804
0
        $oldbiblio->{damagedcount} = $itemdamaged_count;
1805
0
        $oldbiblio->{intransitcount} = $item_in_transit_count;
1806
0
        $oldbiblio->{onholdcount} = $item_onhold_count;
1807
0
        $oldbiblio->{orderedcount} = $ordered_count;
1808        # deleting - in isbn to enable amazon content
1809
0
        $oldbiblio->{isbn} =~ s/-//g;
1810
1811
0
        if (C4::Context->preference("AlternateHoldingsField") && $items_count == 0) {
1812
0
            my $fieldspec = C4::Context->preference("AlternateHoldingsField");
1813
0
            my $subfields = substr $fieldspec, 3;
1814
0
            my $holdingsep = C4::Context->preference("AlternateHoldingsSeparator") || ' ';
1815
0
            my @alternateholdingsinfo = ();
1816
0
            my @holdingsfields = $marcrecord->field(substr $fieldspec, 0, 3);
1817
0
            my $alternateholdingscount = 0;
1818
1819
0
            for my $field (@holdingsfields) {
1820
0
                my %holding = ( holding => '' );
1821
0
                my $havesubfield = 0;
1822
0
                for my $subfield ($field->subfields()) {
1823
0
                    if ((index $subfields, $$subfield[0]) >= 0) {
1824
0
                        $holding{'holding'} .= $holdingsep if (length $holding{'holding'} > 0);
1825
0
                        $holding{'holding'} .= $$subfield[1];
1826
0
                        $havesubfield++;
1827                    }
1828                }
1829
0
                if ($havesubfield) {
1830
0
                    push(@alternateholdingsinfo, \%holding);
1831
0
                    $alternateholdingscount++;
1832                }
1833            }
1834
1835
0
            $oldbiblio->{'ALTERNATEHOLDINGS'} = \@alternateholdingsinfo;
1836
0
            $oldbiblio->{'alternateholdings_count'} = $alternateholdingscount;
1837        }
1838
1839
0
        push( @newresults, $oldbiblio );
1840
0
    }
1841
1842
0
    return @newresults;
1843}
1844
1845 - 1847
=head2 SearchAcquisitions
    Search for acquisitions
=cut
1848
1849sub SearchAcquisitions{
1850
0
    my ($datebegin, $dateend, $itemtypes,$criteria, $orderby) = @_;
1851
1852
0
    my $dbh=C4::Context->dbh;
1853    # Variable initialization
1854
0
    my $str=qq|
1855    SELECT marcxml
1856    FROM biblio
1857    LEFT JOIN biblioitems ON biblioitems.biblionumber=biblio.biblionumber
1858    LEFT JOIN items ON items.biblionumber=biblio.biblionumber
1859    WHERE dateaccessioned BETWEEN ? AND ?
1860    |;
1861
1862
0
    my (@params,@loopcriteria);
1863
1864
0
    push @params, $datebegin->output("iso");
1865
0
    push @params, $dateend->output("iso");
1866
1867
0
    if (scalar(@$itemtypes)>0 and $criteria ne "itemtype" ){
1868
0
        if(C4::Context->preference("item-level_itypes")){
1869
0
            $str .= "AND items.itype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1870        }else{
1871
0
            $str .= "AND biblioitems.itemtype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1872        }
1873
0
        push @params, @$itemtypes;
1874    }
1875
1876
0
    if ($criteria =~/itemtype/){
1877
0
        if(C4::Context->preference("item-level_itypes")){
1878
0
            $str .= "AND items.itype=? ";
1879        }else{
1880
0
            $str .= "AND biblioitems.itemtype=? ";
1881        }
1882
1883
0
        if(scalar(@$itemtypes) == 0){
1884
0
            my $itypes = GetItemTypes();
1885
0
            for my $key (keys %$itypes){
1886
0
                push @$itemtypes, $key;
1887            }
1888        }
1889
1890
0
        @loopcriteria= @$itemtypes;
1891    }elsif ($criteria=~/itemcallnumber/){
1892
0
        $str .= "AND (items.itemcallnumber LIKE CONCAT(?,'%')
1893                 OR items.itemcallnumber is NULL
1894                 OR items.itemcallnumber = '')";
1895
1896
0
        @loopcriteria = ("AA".."ZZ", "") unless (scalar(@loopcriteria)>0);
1897    }else {
1898
0
        $str .= "AND biblio.title LIKE CONCAT(?,'%') ";
1899
0
        @loopcriteria = ("A".."z") unless (scalar(@loopcriteria)>0);
1900    }
1901
1902
0
    if ($orderby =~ /date_desc/){
1903
0
        $str.=" ORDER BY dateaccessioned DESC";
1904    } else {
1905
0
        $str.=" ORDER BY title";
1906    }
1907
1908
0
    my $qdataacquisitions=$dbh->prepare($str);
1909
1910
0
    my @loopacquisitions;
1911
0
    foreach my $value(@loopcriteria){
1912
0
        push @params,$value;
1913
0
        my %cell;
1914
0
        $cell{"title"}=$value;
1915
0
        $cell{"titlecode"}=$value;
1916
1917
0
0
        eval{$qdataacquisitions->execute(@params);};
1918
1919
0
0
        if ($@){ warn "recentacquisitions Error :$@";}
1920        else {
1921
0
            my @loopdata;
1922
0
            while (my $data=$qdataacquisitions->fetchrow_hashref){
1923
0
                push @loopdata, {"summary"=>GetBiblioSummary( $data->{'marcxml'} ) };
1924            }
1925
0
            $cell{"loopdata"}=\@loopdata;
1926        }
1927
0
0
        push @loopacquisitions,\%cell if (scalar(@{$cell{loopdata}})>0);
1928
0
        pop @params;
1929    }
1930
0
    $qdataacquisitions->finish;
1931
0
    return \@loopacquisitions;
1932}
1933#----------------------------------------------------------------------
1934#
1935# Non-Zebra GetRecords#
1936#----------------------------------------------------------------------
1937
1938 - 1942
=head2 NZgetRecords

  NZgetRecords has the same API as zera getRecords, even if some parameters are not managed

=cut
1943
1944sub NZgetRecords {
1945    my (
1946
0
        $query, $simple_query, $sort_by_ref, $servers_ref,
1947        $results_per_page, $offset, $expanded_facet, $branches,
1948        $query_type, $scan
1949    ) = @_;
1950
0
    warn "query =$query" if $DEBUG;
1951
0
    my $result = NZanalyse($query);
1952
0
    warn "results =$result" if $DEBUG;
1953    return ( undef,
1954
0
        NZorder( $result, @$sort_by_ref[0], $results_per_page, $offset ),
1955        undef );
1956}
1957
1958 - 1965
=head2 NZanalyse

  NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
  the list is built from an inverted index in the nozebra SQL table
  note that title is here only for convenience : the sorting will be very fast when requested on title
  if the sorting is requested on something else, we will have to reread all results, and that may be longer.

=cut
1966
1967sub NZanalyse {
1968
0
    my ( $string, $server ) = @_;
1969# warn "---------" if $DEBUG;
1970
0
    warn " NZanalyse" if $DEBUG;
1971# warn "---------" if $DEBUG;
1972
1973 # $server contains biblioserver or authorities, depending on what we search on.
1974 #warn "querying : $string on $server";
1975
0
    $server = 'biblioserver' unless $server;
1976
1977# if we have a ", replace the content to discard temporarily any and/or/not inside
1978
0
    my $commacontent;
1979
0
    if ( $string =~ /"/ ) {
1980
0
        $string =~ s/"(.*?)"/__X__/;
1981
0
        $commacontent = $1;
1982
0
        warn "commacontent : $commacontent" if $DEBUG;
1983    }
1984
1985# split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1986# then, call again NZanalyse with $left and $right
1987# (recursive until we find a leaf (=> something without and/or/not)
1988# delete repeated operator... Would then go in infinite loop
1989
0
    while ( $string =~ s/( and| or| not| AND| OR| NOT)\1/$1/g ) {
1990    }
1991
1992    #process parenthesis before.
1993
0
    if ( $string =~ /^\s*\((.*)\)(( and | or | not | AND | OR | NOT )(.*))?/ ) {
1994
0
        my $left = $1;
1995
0
        my $right = $4;
1996
0
        my $operator = lc($3); # FIXME: and/or/not are operators, not operands
1997
0
        warn
1998"dealing w/parenthesis before recursive sub call. left :$left operator:$operator right:$right"
1999          if $DEBUG;
2000
0
        my $leftresult = NZanalyse( $left, $server );
2001
0
        if ($operator) {
2002
0
            my $rightresult = NZanalyse( $right, $server );
2003
2004            # OK, we have the results for right and left part of the query
2005            # depending of operand, intersect, union or exclude both lists
2006            # to get a result list
2007
0
            if ( $operator eq ' and ' ) {
2008
0
                return NZoperatorAND($leftresult,$rightresult);
2009            }
2010            elsif ( $operator eq ' or ' ) {
2011
2012                # just merge the 2 strings
2013
0
                return $leftresult . $rightresult;
2014            }
2015            elsif ( $operator eq ' not ' ) {
2016
0
                return NZoperatorNOT($leftresult,$rightresult);
2017            }
2018        }
2019        else {
2020# this error is impossible, because of the regexp that isolate the operand, but just in case...
2021
0
            return $leftresult;
2022        }
2023    }
2024
0
    warn "string :" . $string if $DEBUG;
2025
0
    my $left = "";
2026
0
    my $right = "";
2027
0
    my $operator = "";
2028
0
    if ($string =~ /(.*?)( and | or | not | AND | OR | NOT )(.*)/) {
2029
0
        $left = $1;
2030
0
        $right = $3;
2031
0
        $operator = lc($2); # FIXME: and/or/not are operators, not operands
2032    }
2033
0
    warn "no parenthesis. left : $left operator: $operator right: $right"
2034      if $DEBUG;
2035
2036    # it's not a leaf, we have a and/or/not
2037
0
    if ($operator) {
2038
2039        # reintroduce comma content if needed
2040
0
        $right =~ s/__X__/"$commacontent"/ if $commacontent;
2041
0
        $left =~ s/__X__/"$commacontent"/ if $commacontent;
2042
0
        warn "node : $left / $operator / $right\n" if $DEBUG;
2043
0
        my $leftresult = NZanalyse( $left, $server );
2044
0
        my $rightresult = NZanalyse( $right, $server );
2045
0
        warn " leftresult : $leftresult" if $DEBUG;
2046
0
        warn " rightresult : $rightresult" if $DEBUG;
2047        # OK, we have the results for right and left part of the query
2048        # depending of operand, intersect, union or exclude both lists
2049        # to get a result list
2050
0
        if ( $operator eq ' and ' ) {
2051
0
            return NZoperatorAND($leftresult,$rightresult);
2052        }
2053        elsif ( $operator eq ' or ' ) {
2054
2055            # just merge the 2 strings
2056
0
            return $leftresult . $rightresult;
2057        }
2058        elsif ( $operator eq ' not ' ) {
2059
0
            return NZoperatorNOT($leftresult,$rightresult);
2060        }
2061        else {
2062
2063# this error is impossible, because of the regexp that isolate the operand, but just in case...
2064
0
            die "error : operand unknown : $operator for $string";
2065        }
2066
2067        # it's a leaf, do the real SQL query and return the result
2068    }
2069    else {
2070
0
        $string =~ s/__X__/"$commacontent"/ if $commacontent;
2071
0
        $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|&|\+|\*|\// /g;
2072        #remove trailing blank at the beginning
2073
0
        $string =~ s/^ //g;
2074
0
        warn "leaf:$string" if $DEBUG;
2075
2076        # parse the string in in operator/operand/value again
2077
0
        my $left = "";
2078
0
        my $operator = "";
2079
0
        my $right = "";
2080
0
        if ($string =~ /(.*)(>=|<=)(.*)/) {
2081
0
            $left = $1;
2082
0
            $operator = $2;
2083
0
            $right = $3;
2084        } else {
2085
0
            $left = $string;
2086        }
2087# warn "handling leaf... left:$left operator:$operator right:$right"
2088# if $DEBUG;
2089
0
        unless ($operator) {
2090
0
            if ($string =~ /(.*)(>|<|=)(.*)/) {
2091
0
                $left = $1;
2092
0
                $operator = $2;
2093
0
                $right = $3;
2094
0
                warn
2095    "handling unless (operator)... left:$left operator:$operator right:$right"
2096                if $DEBUG;
2097            } else {
2098
0
                $left = $string;
2099            }
2100        }
2101
0
        my $results;
2102
2103# strip adv, zebra keywords, currently not handled in nozebra: wrdl, ext, phr...
2104
0
        $left =~ s/ .*$//;
2105
2106        # automatic replace for short operators
2107
0
        $left = 'title' if $left =~ '^ti$';
2108
0
        $left = 'author' if $left =~ '^au$';
2109
0
        $left = 'publisher' if $left =~ '^pb$';
2110
0
        $left = 'subject' if $left =~ '^su$';
2111
0
        $left = 'koha-Auth-Number' if $left =~ '^an$';
2112
0
        $left = 'keyword' if $left =~ '^kw$';
2113
0
        $left = 'itemtype' if $left =~ '^mc$'; # Fix for Bug 2599 - Search limits not working for NoZebra
2114
0
        warn "handling leaf... left:$left operator:$operator right:$right" if $DEBUG;
2115
0
        my $dbh = C4::Context->dbh;
2116
0
        if ( $operator && $left ne 'keyword' ) {
2117            #do a specific search
2118
0
            $operator = 'LIKE' if $operator eq '=' and $right =~ /%/;
2119
0
            my $sth = $dbh->prepare(
2120"SELECT biblionumbers,value FROM nozebra WHERE server=? AND indexname=? AND value $operator ?"
2121            );
2122
0
            warn "$left / $operator / $right\n" if $DEBUG;
2123
2124            # split each word, query the DB and build the biblionumbers result
2125            #sanitizing leftpart
2126
0
            $left =~ s/^\s+|\s+$//;
2127
0
            foreach ( split / /, $right ) {
2128
0
                my $biblionumbers;
2129
0
                $_ =~ s/^\s+|\s+$//;
2130
0
                next unless $_;
2131
0
                warn "EXECUTE : $server, $left, $_" if $DEBUG;
2132
0
                $sth->execute( $server, $left, $_ )
2133                  or warn "execute failed: $!";
2134
0
                while ( my ( $line, $value ) = $sth->fetchrow ) {
2135
2136# if we are dealing with a numeric value, use only numeric results (in case of >=, <=, > or <)
2137# otherwise, fill the result
2138
0
                    $biblionumbers .= $line
2139                      unless ( $right =~ /^\d+$/ && $value =~ /\D/ );
2140
0
                    warn "result : $value "
2141                      . ( $right =~ /\d/ ) . "=="
2142                      . ( $value =~ /\D/?$line:"" ) if $DEBUG; #= $line";
2143                }
2144
2145# do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2146
0
                if ($results) {
2147
0
                    warn "NZAND" if $DEBUG;
2148
0
                    $results = NZoperatorAND($biblionumbers,$results);
2149                } else {
2150
0
                    $results = $biblionumbers;
2151                }
2152            }
2153        }
2154        else {
2155      #do a complete search (all indexes), if index='kw' do complete search too.
2156
0
            my $sth = $dbh->prepare(
2157"SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?"
2158            );
2159
2160            # split each word, query the DB and build the biblionumbers result
2161
0
            foreach ( split / /, $string ) {
2162
0
                next if C4::Context->stopwords->{ uc($_) }; # skip if stopword
2163
0
                warn "search on all indexes on $_" if $DEBUG;
2164
0
                my $biblionumbers;
2165
0
                next unless $_;
2166
0
                $sth->execute( $server, $_ );
2167
0
                while ( my $line = $sth->fetchrow ) {
2168
0
                    $biblionumbers .= $line;
2169                }
2170
2171# do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2172
0
                if ($results) {
2173
0
                    $results = NZoperatorAND($biblionumbers,$results);
2174                }
2175                else {
2176
0
                    warn "NEW RES for $_ = $biblionumbers" if $DEBUG;
2177
0
                    $results = $biblionumbers;
2178                }
2179            }
2180        }
2181
0
        warn "return : $results for LEAF : $string" if $DEBUG;
2182
0
        return $results;
2183    }
2184
0
    warn "---------\nLeave NZanalyse\n---------" if $DEBUG;
2185}
2186
2187sub NZoperatorAND{
2188
0
    my ($rightresult, $leftresult)=@_;
2189
2190
0
    my @leftresult = split /;/, $leftresult;
2191
0
    warn " @leftresult / $rightresult \n" if $DEBUG;
2192
2193    # my @rightresult = split /;/,$leftresult;
2194
0
    my $finalresult;
2195
2196# parse the left results, and if the biblionumber exist in the right result, save it in finalresult
2197# the result is stored twice, to have the same weight for AND than OR.
2198# example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
2199# result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
2200
0
    foreach (@leftresult) {
2201
0
        my $value = $_;
2202
0
        my $countvalue;
2203
0
        ( $value, $countvalue ) = ( $1, $2 ) if ($value=~/(.*)-(\d+)$/);
2204
0
        if ( $rightresult =~ /\Q$value\E-(\d+);/ ) {
2205
0
            $countvalue = ( $1 > $countvalue ? $countvalue : $1 );
2206
0
            $finalresult .=
2207                "$value-$countvalue;$value-$countvalue;";
2208        }
2209    }
2210
0
    warn "NZAND DONE : $finalresult \n" if $DEBUG;
2211
0
    return $finalresult;
2212}
2213
2214sub NZoperatorOR{
2215
0
    my ($rightresult, $leftresult)=@_;
2216
0
    return $rightresult.$leftresult;
2217}
2218
2219sub NZoperatorNOT{
2220
0
    my ($leftresult, $rightresult)=@_;
2221
2222
0
    my @leftresult = split /;/, $leftresult;
2223
2224    # my @rightresult = split /;/,$leftresult;
2225
0
    my $finalresult;
2226
0
    foreach (@leftresult) {
2227
0
        my $value=$_;
2228
0
        $value=$1 if $value=~m/(.*)-\d+$/;
2229
0
        unless ($rightresult =~ "$value-") {
2230
0
            $finalresult .= "$_;";
2231        }
2232    }
2233
0
    return $finalresult;
2234}
2235
2236 - 2242
=head2 NZorder

  $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);

  TODO :: Description

=cut
2243
2244sub NZorder {
2245
0
    my ( $biblionumbers, $ordering, $results_per_page, $offset ) = @_;
2246
0
    warn "biblionumbers = $biblionumbers and ordering = $ordering\n" if $DEBUG;
2247
2248    # order title asc by default
2249    # $ordering = '1=36 <i' unless $ordering;
2250
0
    $results_per_page = 20 unless $results_per_page;
2251
0
    $offset = 0 unless $offset;
2252
0
    my $dbh = C4::Context->dbh;
2253
2254    #
2255    # order by POPULARITY
2256    #
2257
0
    if ( $ordering =~ /popularity/ ) {
2258
0
        my %result;
2259
0
        my %popularity;
2260
2261        # popularity is not in MARC record, it's builded from a specific query
2262
0
        my $sth =
2263          $dbh->prepare("select sum(issues) from items where biblionumber=?");
2264
0
        foreach ( split /;/, $biblionumbers ) {
2265
0
            my ( $biblionumber, $title ) = split /,/, $_;
2266
0
            $result{$biblionumber} = GetMarcBiblio($biblionumber);
2267
0
            $sth->execute($biblionumber);
2268
0
            my $popularity = $sth->fetchrow || 0;
2269
2270# hint : the key is popularity.title because we can have
2271# many results with the same popularity. In this case, sub-ordering is done by title
2272# we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
2273# (un-frequent, I agree, but we won't forget anything that way ;-)
2274
0
            $popularity{ sprintf( "%10d", $popularity ) . $title
2275                  . $biblionumber } = $biblionumber;
2276        }
2277
2278    # sort the hash and return the same structure as GetRecords (Zebra querying)
2279
0
        my $result_hash;
2280
0
        my $numbers = 0;
2281
0
        if ( $ordering eq 'popularity_dsc' ) { # sort popularity DESC
2282
0
0
            foreach my $key ( sort { $b cmp $a } ( keys %popularity ) ) {
2283
0
                $result_hash->{'RECORDS'}[ $numbers++ ] =
2284                  $result{ $popularity{$key} }->as_usmarc();
2285            }
2286        }
2287        else { # sort popularity ASC
2288
0
            foreach my $key ( sort ( keys %popularity ) ) {
2289
0
                $result_hash->{'RECORDS'}[ $numbers++ ] =
2290                  $result{ $popularity{$key} }->as_usmarc();
2291            }
2292        }
2293
0
        my $finalresult = ();
2294
0
        $result_hash->{'hits'} = $numbers;
2295
0
        $finalresult->{'biblioserver'} = $result_hash;
2296
0
        return $finalresult;
2297
2298        #
2299        # ORDER BY author
2300        #
2301    }
2302    elsif ( $ordering =~ /author/ ) {
2303
0
        my %result;
2304
0
        foreach ( split /;/, $biblionumbers ) {
2305
0
            my ( $biblionumber, $title ) = split /,/, $_;
2306
0
            my $record = GetMarcBiblio($biblionumber);
2307
0
            my $author;
2308
0
            if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2309
0
                $author = $record->subfield( '200', 'f' );
2310
0
                $author = $record->subfield( '700', 'a' ) unless $author;
2311            }
2312            else {
2313
0
                $author = $record->subfield( '100', 'a' );
2314            }
2315
2316# hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2317# and we don't want to get only 1 result for each of them !!!
2318
0
            $result{ $author . $biblionumber } = $record;
2319        }
2320
2321    # sort the hash and return the same structure as GetRecords (Zebra querying)
2322
0
        my $result_hash;
2323
0
        my $numbers = 0;
2324
0
        if ( $ordering eq 'author_za' || $ordering eq 'author_dsc' ) { # sort by author desc
2325
0
0
            foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2326
0
                $result_hash->{'RECORDS'}[ $numbers++ ] =
2327                  $result{$key}->as_usmarc();
2328            }
2329        }
2330        else { # sort by author ASC
2331
0
            foreach my $key ( sort ( keys %result ) ) {
2332
0
                $result_hash->{'RECORDS'}[ $numbers++ ] =
2333                  $result{$key}->as_usmarc();
2334            }
2335        }
2336
0
        my $finalresult = ();
2337
0
        $result_hash->{'hits'} = $numbers;
2338
0
        $finalresult->{'biblioserver'} = $result_hash;
2339
0
        return $finalresult;
2340
2341        #
2342        # ORDER BY callnumber
2343        #
2344    }
2345    elsif ( $ordering =~ /callnumber/ ) {
2346
0
        my %result;
2347
0
        foreach ( split /;/, $biblionumbers ) {
2348
0
            my ( $biblionumber, $title ) = split /,/, $_;
2349
0
            my $record = GetMarcBiblio($biblionumber);
2350
0
            my $callnumber;
2351
0
            my $frameworkcode = GetFrameworkCode($biblionumber);
2352
0
            my ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField( 'items.itemcallnumber', $frameworkcode);
2353
0
               ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField('biblioitems.callnumber', $frameworkcode)
2354                unless $callnumber_tag;
2355
0
            if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2356
0
                $callnumber = $record->subfield( '200', 'f' );
2357            } else {
2358
0
                $callnumber = $record->subfield( '100', 'a' );
2359            }
2360
2361# hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2362# and we don't want to get only 1 result for each of them !!!
2363
0
            $result{ $callnumber . $biblionumber } = $record;
2364        }
2365
2366    # sort the hash and return the same structure as GetRecords (Zebra querying)
2367
0
        my $result_hash;
2368
0
        my $numbers = 0;
2369
0
        if ( $ordering eq 'call_number_dsc' ) { # sort by title desc
2370
0
0
            foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2371
0
                $result_hash->{'RECORDS'}[ $numbers++ ] =
2372                  $result{$key}->as_usmarc();
2373            }
2374        }
2375        else { # sort by title ASC
2376
0
0
            foreach my $key ( sort { $a cmp $b } ( keys %result ) ) {
2377
0
                $result_hash->{'RECORDS'}[ $numbers++ ] =
2378                  $result{$key}->as_usmarc();
2379            }
2380        }
2381
0
        my $finalresult = ();
2382
0
        $result_hash->{'hits'} = $numbers;
2383
0
        $finalresult->{'biblioserver'} = $result_hash;
2384
0
        return $finalresult;
2385    }
2386    elsif ( $ordering =~ /pubdate/ ) { #pub year
2387
0
        my %result;
2388
0
        foreach ( split /;/, $biblionumbers ) {
2389
0
            my ( $biblionumber, $title ) = split /,/, $_;
2390
0
            my $record = GetMarcBiblio($biblionumber);
2391
0
            my ( $publicationyear_tag, $publicationyear_subfield ) =
2392              GetMarcFromKohaField( 'biblioitems.publicationyear', '' );
2393
0
            my $publicationyear =
2394              $record->subfield( $publicationyear_tag,
2395                $publicationyear_subfield );
2396
2397# hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2398# and we don't want to get only 1 result for each of them !!!
2399
0
            $result{ $publicationyear . $biblionumber } = $record;
2400        }
2401
2402    # sort the hash and return the same structure as GetRecords (Zebra querying)
2403
0
        my $result_hash;
2404
0
        my $numbers = 0;
2405
0
        if ( $ordering eq 'pubdate_dsc' ) { # sort by pubyear desc
2406
0
0
            foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2407
0
                $result_hash->{'RECORDS'}[ $numbers++ ] =
2408                  $result{$key}->as_usmarc();
2409            }
2410        }
2411        else { # sort by pub year ASC
2412
0
            foreach my $key ( sort ( keys %result ) ) {
2413
0
                $result_hash->{'RECORDS'}[ $numbers++ ] =
2414                  $result{$key}->as_usmarc();
2415            }
2416        }
2417
0
        my $finalresult = ();
2418
0
        $result_hash->{'hits'} = $numbers;
2419
0
        $finalresult->{'biblioserver'} = $result_hash;
2420
0
        return $finalresult;
2421
2422        #
2423        # ORDER BY title
2424        #
2425    }
2426    elsif ( $ordering =~ /title/ ) {
2427
2428# the title is in the biblionumbers string, so we just need to build a hash, sort it and return
2429
0
        my %result;
2430
0
        foreach ( split /;/, $biblionumbers ) {
2431
0
            my ( $biblionumber, $title ) = split /,/, $_;
2432
2433# hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2434# and we don't want to get only 1 result for each of them !!!
2435# hint & speed improvement : we can order without reading the record
2436# so order, and read records only for the requested page !
2437
0
            $result{ $title . $biblionumber } = $biblionumber;
2438        }
2439
2440    # sort the hash and return the same structure as GetRecords (Zebra querying)
2441
0
        my $result_hash;
2442
0
        my $numbers = 0;
2443
0
        if ( $ordering eq 'title_az' ) { # sort by title desc
2444
0
            foreach my $key ( sort ( keys %result ) ) {
2445
0
                $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2446            }
2447        }
2448        else { # sort by title ASC
2449
0
0
            foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2450
0
                $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2451            }
2452        }
2453
2454        # limit the $results_per_page to result size if it's more
2455
0
        $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2456
2457        # for the requested page, replace biblionumber by the complete record
2458        # speed improvement : avoid reading too much things
2459        for (
2460            my $counter = $offset ;
2461            $counter <= $offset + $results_per_page ;
2462            $counter++
2463          )
2464        {
2465
0
            $result_hash->{'RECORDS'}[$counter] =
2466              GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc;
2467
0
        }
2468
0
        my $finalresult = ();
2469
0
        $result_hash->{'hits'} = $numbers;
2470
0
        $finalresult->{'biblioserver'} = $result_hash;
2471
0
        return $finalresult;
2472    }
2473    else {
2474
2475#
2476# order by ranking
2477#
2478# we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
2479
0
        my %result;
2480
0
        my %count_ranking;
2481
0
        foreach ( split /;/, $biblionumbers ) {
2482
0
            my ( $biblionumber, $title ) = split /,/, $_;
2483
0
            $title =~ /(.*)-(\d)/;
2484
2485            # get weight
2486
0
            my $ranking = $2;
2487
2488# note that we + the ranking because ranking is calculated on weight of EACH term requested.
2489# if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
2490# biblio N has ranking = 6
2491
0
            $count_ranking{$biblionumber} += $ranking;
2492        }
2493
2494# build the result by "inverting" the count_ranking hash
2495# 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
2496# warn "counting";
2497
0
        foreach ( keys %count_ranking ) {
2498
0
            $result{ sprintf( "%10d", $count_ranking{$_} ) . '-' . $_ } = $_;
2499        }
2500
2501    # sort the hash and return the same structure as GetRecords (Zebra querying)
2502
0
        my $result_hash;
2503
0
        my $numbers = 0;
2504
0
0
        foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2505
0
            $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2506        }
2507
2508        # limit the $results_per_page to result size if it's more
2509
0
        $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2510
2511        # for the requested page, replace biblionumber by the complete record
2512        # speed improvement : avoid reading too much things
2513        for (
2514            my $counter = $offset ;
2515            $counter <= $offset + $results_per_page ;
2516            $counter++
2517          )
2518        {
2519
0
            $result_hash->{'RECORDS'}[$counter] =
2520              GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc
2521              if $result_hash->{'RECORDS'}[$counter];
2522
0
        }
2523
0
        my $finalresult = ();
2524
0
        $result_hash->{'hits'} = $numbers;
2525
0
        $finalresult->{'biblioserver'} = $result_hash;
2526
0
        return $finalresult;
2527    }
2528}
2529
2530 - 2552
=head2 enabled_staff_search_views

%hash = enabled_staff_search_views()

This function returns a hash that contains three flags obtained from the system
preferences, used to determine whether a particular staff search results view
is enabled.

=over 2

=item C<Output arg:>

    * $hash{can_view_MARC} is true only if the MARC view is enabled
    * $hash{can_view_ISBD} is true only if the ISBD view is enabled
    * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled

=item C<usage in the script:>

=back

$template->param ( C4::Search::enabled_staff_search_views );

=cut
2553
2554sub enabled_staff_search_views
2555{
2556        return (
2557
0
                can_view_MARC => C4::Context->preference('viewMARC'), # 1 if the staff search allows the MARC view
2558                can_view_ISBD => C4::Context->preference('viewISBD'), # 1 if the staff search allows the ISBD view
2559                can_view_labeledMARC => C4::Context->preference('viewLabeledMARC'), # 1 if the staff search allows the Labeled MARC view
2560        );
2561}
2562
2563sub AddSearchHistory{
2564
0
        my ($borrowernumber,$session,$query_desc,$query_cgi, $total)=@_;
2565
0
    my $dbh = C4::Context->dbh;
2566
2567    # Add the request the user just made
2568
0
    my $sql = "INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, total, time) VALUES(?, ?, ?, ?, ?, NOW())";
2569
0
    my $sth = $dbh->prepare($sql);
2570
0
    $sth->execute($borrowernumber, $session, $query_desc, $query_cgi, $total);
2571
0
        return $dbh->last_insert_id(undef, 'search_history', undef,undef,undef);
2572}
2573
2574sub GetSearchHistory{
2575
0
        my ($borrowernumber,$session)=@_;
2576
0
    my $dbh = C4::Context->dbh;
2577
2578    # Add the request the user just made
2579
0
    my $query = "SELECT FROM search_history WHERE (userid=? OR sessionid=?)";
2580
0
    my $sth = $dbh->prepare($query);
2581
0
        $sth->execute($borrowernumber, $session);
2582
0
    return $sth->fetchall_hashref({});
2583}
2584
2585 - 2626
=head2 z3950_search_args

$arrayref = z3950_search_args($matchpoints)

This function returns an array reference that contains the search parameters to be
passed to the Z39.50 search script (z3950_search.pl). The array elements
are hash refs whose keys are name, value and encvalue, and whose values are the
name of a search parameter, the value of that search parameter and the URL encoded
value of that parameter.

The search parameter names are lccn, isbn, issn, title, author, dewey and subject.

The search parameter values are obtained from the bibliographic record whose
data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().

If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
a general purpose search argument. In this case, the returned array contains only
entry: the key is 'title' and the value and encvalue are derived from $matchpoints.

If a search parameter value is undefined or empty, it is not included in the returned
array.

The returned array reference may be passed directly to the template parameters.

=over 2

=item C<Output arg:>

    * $array containing hash refs as described above

=item C<usage in the script:>

=back

$data = Biblio::GetBiblioData($bibno);
$template->param ( MYLOOP => C4::Search::z3950_search_args($data) )

*OR*

$template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )

=cut
2627
2628sub z3950_search_args {
2629
0
    my $bibrec = shift;
2630
0
    my $isbn = Business::ISBN->new($bibrec);
2631
2632
0
    if (defined $isbn && $isbn->is_valid)
2633    {
2634
0
        $bibrec = { isbn => $bibrec } if !ref $bibrec;
2635    }
2636    else {
2637
0
        $bibrec = { title => $bibrec } if !ref $bibrec;
2638    }
2639
0
    my $array = [];
2640
0
    for my $field (qw/ lccn isbn issn title author dewey subject /)
2641    {
2642
0
        my $encvalue = URI::Escape::uri_escape_utf8($bibrec->{$field});
2643
0
        push @$array, { name=>$field, value=>$bibrec->{$field}, encvalue=>$encvalue } if defined $bibrec->{$field};
2644    }
2645
0
    return $array;
2646}
2647
2648 - 2652
=head2 GetDistinctValues($field);

C<$field> is a reference to the fields array

=cut
2653
2654sub GetDistinctValues {
2655
0
    my ($fieldname,$string)=@_;
2656    # returns a reference to a hash of references to branches...
2657
0
    if ($fieldname=~/\./){
2658
0
                        my ($table,$column)=split /\./, $fieldname;
2659
0
                        my $dbh = C4::Context->dbh;
2660
0
                        warn "select DISTINCT($column) as value, count(*) as cnt from $table group by lib order by $column " if $DEBUG;
2661
0
                        my $sth = $dbh->prepare("select DISTINCT($column) as value, count(*) as cnt from $table ".($string?" where $column like \"$string%\"":"")."group by value order by $column ");
2662
0
                        $sth->execute;
2663
0
                        my $elements=$sth->fetchall_arrayref({});
2664
0
                        return $elements;
2665   }
2666   else {
2667
0
                $string||= qq("");
2668
0
                my @servers=qw<biblioserver authorityserver>;
2669
0
                my (@zconns,@results);
2670        for ( my $i = 0 ; $i < @servers ; $i++ ) {
2671
0
         $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
2672
0
                        $results[$i] =
2673                      $zconns[$i]->scan(
2674                        ZOOM::Query::CCL2RPN->new( qq"$fieldname $string", $zconns[$i])
2675                      );
2676
0
                }
2677                # The big moment: asynchronously retrieve results from all servers
2678
0
                my @elements;
2679
0
                while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
2680
0
                        my $ev = $zconns[ $i - 1 ]->last_event();
2681
0
                        if ( $ev == ZOOM::Event::ZEND ) {
2682
0
                                next unless $results[ $i - 1 ];
2683
0
                                my $size = $results[ $i - 1 ]->size();
2684
0
                                if ( $size > 0 ) {
2685                      for (my $j=0;$j<$size;$j++){
2686
0
                                                my %hashscan;
2687
0
                                                @hashscan{qw(value cnt)}=$results[ $i - 1 ]->display_term($j);
2688
0
                                                push @elements, \%hashscan;
2689
0
                                          }
2690                                }
2691                        }
2692                }
2693
0
                return \@elements;
2694   }
2695}
2696
2697
2698
14
8022569
END { } # module clean-up code here (global destructor)
2699
27001;