Merge remote-tracking branch 'origin/new/bug_8597'
[koha.git] / C4 / Search.pm
index cd970b7..2b78f75 100644 (file)
@@ -25,17 +25,23 @@ use Lingua::Stem;
 use C4::Search::PazPar2;
 use XML::Simple;
 use C4::Dates qw(format_date);
+use C4::Members qw(GetHideLostItemsPreference);
 use C4::XSLT;
 use C4::Branch;
 use C4::Reserves;    # CheckReserves
 use C4::Debug;
+use C4::Charset;
+use YAML;
 use URI::Escape;
+use Business::ISBN;
+use MARC::Record;
+use MARC::Field;
 
 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
 
 # set the version for version checking
 BEGIN {
-    $VERSION = 3.01;
+    $VERSION = 3.07.00.049;
     $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
 }
 
@@ -65,9 +71,9 @@ This module provides searching functions for Koha's bibliographic databases
   &NZgetRecords
   &AddSearchHistory
   &GetDistinctValues
-  &BiblioAddAuthorities
+  &enabled_staff_search_views
+  &SimpleSearch
 );
-#FIXME: i had to add BiblioAddAuthorities here because in Biblios.pm it caused circular dependencies (C4::Search uses C4::Biblio, and BiblioAddAuthorities uses SimpleSearch from C4::Search)
 
 # make all your functions, whether exported or not;
 
@@ -120,18 +126,19 @@ sub FindDuplicate {
         }
     }
 
-    # FIXME: add error handling
-    my ( $error, $searchresults ) = SimpleSearch($query); # FIXME :: hardcoded !
+    my ( $error, $searchresults, undef ) = SimpleSearch($query); # FIXME :: hardcoded !
     my @results;
-    foreach my $possible_duplicate_record (@$searchresults) {
-        my $marcrecord =
-          MARC::Record->new_from_usmarc($possible_duplicate_record);
-        my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
-
-        # FIXME :: why 2 $biblionumber ?
-        if ($result) {
-            push @results, $result->{'biblionumber'};
-            push @results, $result->{'title'};
+    if (!defined $error) {
+        foreach my $possible_duplicate_record (@{$searchresults}) {
+            my $marcrecord =
+            MARC::Record->new_from_usmarc($possible_duplicate_record);
+            my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
+
+            # FIXME :: why 2 $biblionumber ?
+            if ($result) {
+                push @results, $result->{'biblionumber'};
+                push @results, $result->{'title'};
+            }
         }
     }
     return @results;
@@ -153,12 +160,16 @@ This function provides a simple search API on the bibliographic catalog
     * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
 
 
-=item C<Output:>
+=item C<Return:>
 
-    * $error is a empty unless an error is detected
-    * \@results is an array of records.
+    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
@@ -172,23 +183,23 @@ if (defined $error) {
     exit;
 }
 
-my $hits = scalar @$marcresults;
+my $hits = @{$marcresults};
 my @results;
 
-for my $i (0..$hits) {
-    my %resultsloop;
-    my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
-    my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
-
-    #build the hash for the template.
-    $resultsloop{title}           = $biblio->{'title'};
-    $resultsloop{subtitle}        = $biblio->{'subtitle'};
-    $resultsloop{biblionumber}    = $biblio->{'biblionumber'};
-    $resultsloop{author}          = $biblio->{'author'};
-    $resultsloop{publishercode}   = $biblio->{'publishercode'};
-    $resultsloop{publicationyear} = $biblio->{'publicationyear'};
+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'},
+        };
 
-    push @results, \%resultsloop;
 }
 
 $template->param(result=>\@results);
@@ -206,14 +217,14 @@ sub SimpleSearch {
         return ( undef, $search_result, scalar($result->{hits}) );
     }
     else {
+        return ( 'No query entered', undef, undef ) unless $query;
         # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
-        my @servers = defined ( $servers ) ? @$servers : ( "biblioserver" );
-        my @results;
+        my @servers = defined ( $servers ) ? @$servers : ( 'biblioserver' );
         my @zoom_queries;
         my @tmpresults;
         my @zconns;
-        my $total_hits;
-        return ( "No query entered", undef, undef ) unless $query;
+        my $results = [];
+        my $total_hits = 0;
 
         # Initialize & Search Zebra
         for ( my $i = 0 ; $i < @servers ; $i++ ) {
@@ -239,7 +250,7 @@ sub SimpleSearch {
                   . $@->code() . ") "
                   . $@->addinfo() . " "
                   . $@->diagset();
-                warn $error;
+                warn $error." for query: $query";
                 return ( $error, undef, undef );
             }
         }
@@ -257,7 +268,7 @@ sub SimpleSearch {
 
                 for my $j ( $first_record..$last_record ) {
                     my $record = $tmpresults[ $i - 1 ]->record( $j-1 )->raw(); # 0 indexed
-                    push @results, $record;
+                    push @{$results}, $record;
                 }
             }
         }
@@ -269,7 +280,7 @@ sub SimpleSearch {
             $zoom_query->destroy();
         }
 
-        return ( undef, \@results, $total_hits );
+        return ( undef, $results, $total_hits );
     }
 }
 
@@ -278,7 +289,7 @@ sub SimpleSearch {
 ( undef, $results_hashref, \@facets_loop ) = getRecords (
 
         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
-        $results_per_page, $offset,       $expanded_facet, $branches,
+        $results_per_page, $offset,       $expanded_facet, $branches,$itemtypes,
         $query_type,       $scan
     );
 
@@ -292,7 +303,7 @@ See verbse embedded documentation.
 sub getRecords {
     my (
         $koha_query,       $simple_query, $sort_by_ref,    $servers_ref,
-        $results_per_page, $offset,       $expanded_facet, $branches,
+        $results_per_page, $offset,       $expanded_facet, $branches,$itemtypes,
         $query_type,       $scan
     ) = @_;
 
@@ -351,10 +362,10 @@ sub getRecords {
         # Note: sort will override rank
         my $sort_by;
         foreach my $sort (@sort_by) {
-            if ( $sort eq "author_az" ) {
+            if ( $sort eq "author_az" || $sort eq "author_asc" ) {
                 $sort_by .= "1=1003 <i ";
             }
-            elsif ( $sort eq "author_za" ) {
+            elsif ( $sort eq "author_za" || $sort eq "author_dsc" ) {
                 $sort_by .= "1=1003 >i ";
             }
             elsif ( $sort eq "popularity_asc" ) {
@@ -364,10 +375,10 @@ sub getRecords {
                 $sort_by .= "1=9003 >i ";
             }
             elsif ( $sort eq "call_number_asc" ) {
-                $sort_by .= "1=20  <i ";
+                $sort_by .= "1=8007  <i ";
             }
             elsif ( $sort eq "call_number_dsc" ) {
-                $sort_by .= "1=20 >i ";
+                $sort_by .= "1=8007 >i ";
             }
             elsif ( $sort eq "pubdate_asc" ) {
                 $sort_by .= "1=31 <i ";
@@ -381,17 +392,17 @@ sub getRecords {
             elsif ( $sort eq "acqdate_dsc" ) {
                 $sort_by .= "1=32 >i ";
             }
-            elsif ( $sort eq "title_az" ) {
+            elsif ( $sort eq "title_az" || $sort eq "title_asc" ) {
                 $sort_by .= "1=4 <i ";
             }
-            elsif ( $sort eq "title_za" ) {
+            elsif ( $sort eq "title_za" || $sort eq "title_dsc" ) {
                 $sort_by .= "1=4 >i ";
             }
             else {
                 warn "Ignoring unrecognized sort '$sort' requested" if $sort_by;
             }
         }
-        if ($sort_by) {
+        if ($sort_by && !$scan) {
             if ( $results[$i]->sort( "yaz", $sort_by ) < 0 ) {
                 warn "WARNING sort $sort_by failed";
             }
@@ -460,46 +471,40 @@ sub getRecords {
                 if ( !$scan && $servers[ $i - 1 ] =~ /biblioserver/ ) {
 
                     my $jmax = $size>$facets_maxrecs? $facets_maxrecs: $size;
-
-                    for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
-                        ($facets->[$k]) or next;
-                        my @fcodes = @{$facets->[$k]->{'tags'}};
-                        my $sfcode = $facets->[$k]->{'subfield'};
-
+                    for my $facet ( @$facets ) {
                                for ( my $j = 0 ; $j < $jmax ; $j++ ) {
                                    my $render_record = $results[ $i - 1 ]->record($j)->render();
                             my @used_datas = ();
-
-                            foreach my $fcode (@fcodes) {
-
+                            foreach my $tag ( @{$facet->{tags}} ) {
                                 # avoid first line
-                                my $field_pattern = '\n'.$fcode.' ([^\n]+)';
+                                my $tag_num = substr($tag, 0, 3);
+                                my $letters = substr($tag, 3);
+                                my $field_pattern = '\n' . $tag_num . ' ([^z][^\n]+)';
+                                $field_pattern = '\n' . $tag_num . ' ([^\n]+)' if (int($tag_num) < 10);
                                 my @field_tokens = ( $render_record =~ /$field_pattern/g ) ;
-
                                 foreach my $field_token (@field_tokens) {
-                                    my $subfield_pattern = '\$'.$sfcode.' ([^\$]+)';
-                                    my @subfield_values = ( $field_token =~ /$subfield_pattern/g );
-
-                                    foreach my $subfield_value (@subfield_values) {
-
-                                        my $data = $subfield_value;
-                                        $data =~ s/^\s+//; # trim left
-                                        $data =~ s/\s+$//; # trim right
-
-                                        unless ( $data ~~ @used_datas ) {
-                                            $facets_counter->{ $facets->[$k]->{'link_value'} }->{$data}++;
-                                            push @used_datas, $data;
+                                    my @subf = ( $field_token =~ /\$([a-zA-Z0-9]) ([^\$]+)/g );
+                                    my @values;
+                                    for (my $i = 0; $i < @subf; $i += 2) {
+                                        if ( $letters =~ $subf[$i] ) {
+                                             my $value = $subf[$i+1];
+                                             $value =~ s/^ *//;
+                                             $value =~ s/ *$//;
+                                             push @values, $value;
                                         }
-                                    } # subfields
+                                    }
+                                    my $data = join($facet->{sep}, @values);
+                                    unless ( $data ~~ @used_datas ) {
+                                        $facets_counter->{ $facet->{idx} }->{$data}++;
+                                        push @used_datas, $data;
+                                    }
                                 } # fields
                             } # field codes
                         } # records
-
-                        $facets_info->{ $facets->[$k]->{'link_value'} }->{'label_value'} = $facets->[$k]->{'label_value'};
-                        $facets_info->{ $facets->[$k]->{'link_value'} }->{'expanded'} = $facets->[$k]->{'expanded'};
+                        $facets_info->{ $facet->{idx} }->{label_value} = $facet->{label};
+                        $facets_info->{ $facet->{idx} }->{expanded} = $facet->{expanded};
                     } # facets
                 }
-                # End PROGILONE
             }
 
             # warn "connection ", $i-1, ": $size hits";
@@ -553,6 +558,17 @@ sub getRecords {
                                                                        $facet_label_value = "*";
                                                                }
                             }
+                            # if it's a itemtype, label by the name, not the code,
+                            if ( $link_value =~ /itype/ ) {
+                                if (defined $itemtypes
+                                    && ref($itemtypes) eq "HASH"
+                                    && defined $itemtypes->{$one_facet}
+                                    && ref ($itemtypes->{$one_facet}) eq "HASH")
+                                {
+                                    $facet_label_value =
+                                        $itemtypes->{$one_facet}->{'description'};
+                                }
+                            }
 
                             # but we're down with the whole label being in the link's title.
                             push @this_facets_array, {
@@ -719,7 +735,7 @@ sub _detect_truncation {
 sub _build_stemmed_operand {
     my ($operand,$lang) = @_;
     require Lingua::Stem::Snowball ;
-    my $stemmed_operand;
+    my $stemmed_operand=q{};
 
     # If operand contains a digit, it is almost certainly an identifier, and should
     # not be stemmed.  This is particularly relevant for ISBNs and ISSNs, which
@@ -730,6 +746,13 @@ sub _build_stemmed_operand {
 
 # FIXME: the locale should be set based on the user's language and/or search choice
     #warn "$lang";
+    # Make sure we only use the first two letters from the language code
+    $lang = lc(substr($lang, 0, 2));
+    # The language codes for the two variants of Norwegian will now be "nb" and "nn",
+    # none of which Lingua::Stem::Snowball can use, so we need to "translate" them
+    if ($lang eq 'nb' || $lang eq 'nn') {
+      $lang = 'no';
+    }
     my $stemmer = Lingua::Stem::Snowball->new( lang => $lang,
                                                encoding => "UTF-8" );
 
@@ -762,7 +785,7 @@ sub _build_weighted_query {
         $weighted_query .=
           "Title-cover,ext,r1=\"$operand\"";    # exact title-cover
         $weighted_query .= " or ti,ext,r2=\"$operand\"";    # exact title
-        $weighted_query .= " or ti,phr,r3=\"$operand\"";    # phrase title
+        $weighted_query .= " or Title-cover,phr,r3=\"$operand\"";    # phrase title
           #$weighted_query .= " or any,ext,r4=$operand";               # exact any
           #$weighted_query .=" or kw,wrdl,r5=\"$operand\"";            # word list any
         $weighted_query .= " or wrdl,fuzzy,r8=\"$operand\""
@@ -834,6 +857,7 @@ sub getIndexes{
                     'Authority-Number',
                     'authtype',
                     'bc',
+                   'Bib-level',
                     'biblionumber',
                     'bio',
                     'biography',
@@ -864,6 +888,7 @@ sub getIndexes{
                     'Date-of-acquisition',
                     'Date-of-publication',
                     'Dewey-classification',
+                    'EAN',
                     'extent',
                     'fic',
                     'fiction',
@@ -901,6 +926,7 @@ sub getIndexes{
                     'mc-rtype',
                     'mus',
                     'name',
+                    'Music-number',
                     'Name-geographic',
                     'Name-geographic-heading',
                     'Name-geographic-see',
@@ -942,6 +968,7 @@ sub getIndexes{
                     'su-to',
                     'su-ut',
                     'ut',
+                    'UPC',
                     'Term-genre-form',
                     'Term-genre-form-heading',
                     'Term-genre-form-see',
@@ -950,6 +977,7 @@ sub getIndexes{
                     'Title',
                     'Title-cover',
                     'Title-series',
+                    'Title-host',
                     'Title-uniform',
                     'Title-uniform-heading',
                     'Title-uniform-see',
@@ -992,6 +1020,8 @@ sub getIndexes{
                     'reserves',
                     'restricted',
                     'stack',
+                    'stocknumber',
+                    'inv',
                     'uri',
                     'withdrawn',
 
@@ -1001,6 +1031,104 @@ sub getIndexes{
     return \@indexes;
 }
 
+=head2 _handle_exploding_index
+
+    my $query = _handle_exploding_index($index, $term)
+
+Callback routine to generate the search for "exploding" indexes (i.e.
+those indexes which are turned into multiple or-connected searches based
+on authority data).
+
+=cut
+
+sub _handle_exploding_index {
+    my ( $index, $term ) = @_;
+
+    return unless ($index =~ m/(su-br|su-na|su-rl)/ && $term);
+
+    my $marcflavour = C4::Context->preference('marcflavour');
+
+    my $codesubfield = $marcflavour eq 'UNIMARC' ? '5' : 'w';
+    my $wantedcodes = '';
+    my @subqueries = ( "(su=\"$term\")");
+    my ($error, $results, $total_hits) = SimpleSearch( "Heading,wrdl=$term", undef, undef, [ "authorityserver" ] );
+    foreach my $auth (@$results) {
+        my $record = MARC::Record->new_from_usmarc($auth);
+        my @references = $record->field('5..');
+        if (@references) {
+            if ($index eq 'su-br') {
+                $wantedcodes = 'g';
+            } elsif ($index eq 'su-na') {
+                $wantedcodes = 'h';
+            } elsif ($index eq 'su-rl') {
+                $wantedcodes = '';
+            }
+            foreach my $reference (@references) {
+                my $codes = $reference->subfield($codesubfield);
+                push @subqueries, '(su="' . $reference->as_string('abcdefghijlmnopqrstuvxyz') . '")' if (($codes && $codes eq $wantedcodes) || !$wantedcodes);
+            }
+        }
+    }
+    return join(' or ', @subqueries);
+}
+
+=head2 parseQuery
+
+    ( $operators, $operands, $indexes, $limits,
+      $sort_by, $scan, $lang ) =
+            buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
+
+Shim function to ease the transition from buildQuery to a new QueryParser.
+This function is called at the beginning of buildQuery, and modifies
+buildQuery's input. If it can handle the input, it returns a query that
+buildQuery will not try to parse.
+=cut
+
+sub parseQuery {
+    my ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang) = @_;
+
+    my @operators = $operators ? @$operators : ();
+    my @indexes   = $indexes   ? @$indexes   : ();
+    my @operands  = $operands  ? @$operands  : ();
+    my @limits    = $limits    ? @$limits    : ();
+    my @sort_by   = $sort_by   ? @$sort_by   : ();
+
+    my $query = $operands[0];
+    my $index;
+    my $term;
+
+# TODO: once we are using QueryParser, all this special case code for
+#       exploded search indexes will be replaced by a callback to
+#       _handle_exploding_index
+    if ( $query =~ m/^(.*)\b(su-br|su-na|su-rl)[:=](\w.*)$/ ) {
+        $query = $1;
+        $index = $2;
+        $term  = $3;
+    } else {
+        $query = '';
+        for ( my $i = 0 ; $i <= @operands ; $i++ ) {
+            if ($operands[$i] && $indexes[$i] =~ m/(su-br|su-na|su-rl)/) {
+                $index = $indexes[$i];
+                $term = $operands[$i];
+            } elsif ($operands[$i]) {
+                $query .= $operators[$i] eq 'or' ? ' or ' : ' and ' if ($query);
+                $query .= "($indexes[$i]:$operands[$i])";
+            }
+        }
+    }
+
+    if ($index) {
+        my $queryPart = _handle_exploding_index($index, $term);
+        if ($queryPart) {
+            $query .= "($queryPart)";
+        }
+        $operators = ();
+        $operands[0] = "ccl=$query";
+    }
+
+    return ( $operators, \@operands, $indexes, $limits, $sort_by, $scan, $lang);
+}
+
 =head2 buildQuery
 
 ( $error, $query,
@@ -1022,6 +1150,8 @@ sub buildQuery {
 
     warn "---------\nEnter buildQuery\n---------" if $DEBUG;
 
+    ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang) = parseQuery($operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
+
     # dereference
     my @operators = $operators ? @$operators : ();
     my @indexes   = $indexes   ? @$indexes   : ();
@@ -1057,15 +1187,14 @@ sub buildQuery {
 
     my $stopwords_removed;    # flag to determine if stopwords have been removed
 
-    my $cclq;
+    my $cclq       = 0;
     my $cclindexes = getIndexes();
-    if( $query !~ /\s*ccl=/ ){
-        for my $index (@$cclindexes){
-            if($query =~ /($index)(,?\w)*[:=]/){
-                $cclq = 1;
-            }
+    if ( $query !~ /\s*ccl=/ ) {
+        while ( !$cclq && $query =~ /(?:^|\W)([\w-]+)(,[\w-]+)*[:=]/g ) {
+            my $dx = lc($1);
+            $cclq = grep { lc($_) eq $dx } @$cclindexes;
         }
-        $query = "ccl=$query" if($cclq);
+        $query = "ccl=$query" if $cclq;
     }
 
 # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
@@ -1074,7 +1203,8 @@ sub buildQuery {
         my $q=$';
         # This is needed otherwise ccl= and &limit won't work together, and
         # this happens when selecting a subject on the opac-detail page
-        if (@limits) {
+        @limits = grep {!/^$/} @limits;
+        if ( @limits ) {
             $q .= ' and '.join(' and ', @limits);
         }
         return ( undef, $q, $q, "q=ccl=$q", $q, '', '', '', '', 'ccl' );
@@ -1119,10 +1249,12 @@ sub buildQuery {
                 my $indexes_set;
 
 # If the user is sophisticated enough to specify an index, turn off field weighting, stemming, and stopword handling
-                if ( $operands[$i] =~ /(:|=)/ || $scan ) {
+                if ( $operands[$i] =~ /\w(:|=)/ || $scan ) {
                     $weight_fields    = 0;
                     $stemming         = 0;
                     $remove_stopwords = 0;
+                } else {
+                    $operands[$i] =~ s/\?/{?}/g; # need to escape question marks
                 }
                 my $operand = $operands[$i];
                 my $index   = $indexes[$i];
@@ -1143,7 +1275,6 @@ sub buildQuery {
                 }
                 # ISBN,ISSN,Standard Number, don't need special treatment
                 elsif ( $index eq 'nb' || $index eq 'ns' ) {
-                    $indexes_set++;
                     (
                         $stemming,      $auto_truncation,
                         $weight_fields, $fuzzy_enabled,
@@ -1158,7 +1289,7 @@ sub buildQuery {
 
                 # Set default structure attribute (word list)
                 my $struct_attr = q{};
-                unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl)/ ) {
+                unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl|nb|ns)/ ) {
                     $struct_attr = ",wrdl";
                 }
 
@@ -1287,9 +1418,10 @@ sub buildQuery {
     warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
 
     # add limits
-    my $group_OR_limits;
+    my %group_OR_limits;
     my $availability_limit;
     foreach my $this_limit (@limits) {
+        next unless $this_limit;
         if ( $this_limit =~ /available/ ) {
 #
 ## 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
@@ -1304,17 +1436,16 @@ sub buildQuery {
         # group_OR_limits, prefixed by mc-
         # OR every member of the group
         elsif ( $this_limit =~ /mc/ ) {
-        
-            if ( $this_limit =~ /mc-ccode:/ ) {
+            my ($k,$v) = split(/:/, $this_limit,2);
+            if ( $k !~ /mc-i(tem)?type/ ) {
                 # in case the mc-ccode value has complicating chars like ()'s inside it we wrap in quotes
                 $this_limit =~ tr/"//d;
-                my ($k,$v) = split(/:/, $this_limit,2);
                 $this_limit = $k.":\"".$v."\"";
             }
 
-            $group_OR_limits .= " or " if $group_OR_limits;
-            $limit_desc      .= " or " if $group_OR_limits;
-            $group_OR_limits .= "$this_limit";
+            $group_OR_limits{$k} .= " or " if $group_OR_limits{$k};
+            $limit_desc      .= " or " if $group_OR_limits{$k};
+            $group_OR_limits{$k} .= "$this_limit";
             $limit_cgi       .= "&limit=$this_limit";
             $limit_desc      .= " $this_limit";
         }
@@ -1337,9 +1468,9 @@ sub buildQuery {
             }
         }
     }
-    if ($group_OR_limits) {
+    foreach my $k (keys (%group_OR_limits)) {
         $limit .= " and " if ( $query || $limit );
-        $limit .= "($group_OR_limits)";
+        $limit .= "($group_OR_limits{$k})";
     }
     if ($availability_limit) {
         $limit .= " and " if ( $query || $limit );
@@ -1350,7 +1481,7 @@ sub buildQuery {
     # This is flawed , means we can't search anything with : in it
     # if user wants to do ccl or cql, start the query with that
 #    $query =~ s/:/=/g;
-    $query =~ s/(?<=(ti|au|pb|su|an|kw|mc)):/=/g;
+    $query =~ s/(?<=(ti|au|pb|su|an|kw|mc|nb|ns)):/=/g;
     $query =~ s/(?<=(wrdl)):/=/g;
     $query =~ s/(?<=(trn|phr)):/=/g;
     $limit =~ s/:/=/g;
@@ -1389,7 +1520,7 @@ sub buildQuery {
 
   my @search_results = searchResults($search_context, $searchdesc, $hits, 
                                      $results_per_page, $offset, $scan, 
-                                     @marcresults, $hidelostitems);
+                                     @marcresults);
 
 Format results in a form suitable for passing to the template
 
@@ -1398,11 +1529,18 @@ Format results in a form suitable for passing to the template
 # IMO this subroutine is pretty messy still -- it's responsible for
 # building the HTML output for the template
 sub searchResults {
-    my ( $search_context, $searchdesc, $hits, $results_per_page, $offset, $scan, @marcresults, $hidelostitems ) = @_;
+    my ( $search_context, $searchdesc, $hits, $results_per_page, $offset, $scan, $marcresults ) = @_;
     my $dbh = C4::Context->dbh;
     my @newresults;
 
-    $search_context = 'opac' unless $search_context eq 'opac' or $search_context eq 'intranet';
+    require C4::Items;
+
+    $search_context = 'opac' if !$search_context || $search_context ne 'intranet';
+    my ($is_opac, $hidelostitems);
+    if ($search_context eq 'opac') {
+        $hidelostitems = C4::Context->preference('hidelostitems');
+        $is_opac       = 1;
+    }
 
     #Build branchnames hash
     #find branchname
@@ -1436,12 +1574,7 @@ sub searchResults {
     }
 
     #search item field code
-    my $sth =
-      $dbh->prepare(
-"SELECT tagfield FROM marc_subfield_structure WHERE kohafield LIKE 'items.itemnumber'"
-      );
-    $sth->execute;
-    my ($itemtag) = $sth->fetchrow;
+    my ($itemtag, undef) = &GetMarcFromKohaField( "items.itemnumber", "" );
 
     ## find column names of items related to MARC
     my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
@@ -1465,12 +1598,11 @@ sub searchResults {
        my $marcflavour = C4::Context->preference("marcflavour");
     # We get the biblionumber position in MARC
     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
-    my $fw;
 
     # loop through all of the records we've retrieved
     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
-        my $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
-        $fw = $scan
+        my $marcrecord = MARC::File::USMARC::decode( $marcresults->[$i] );
+        my $fw = $scan
              ? undef
              : $bibliotag < 10
                ? GetFrameworkCode($marcrecord->field($bibliotag)->data)
@@ -1480,9 +1612,9 @@ sub searchResults {
         $oldbiblio->{result_number} = $i + 1;
 
         # add imageurl to itemtype if there is one
-        $oldbiblio->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
+        $oldbiblio->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
 
-        $oldbiblio->{'authorised_value_images'}  = C4::Items::get_authorised_value_images( C4::Biblio::get_biblio_authorised_values( $oldbiblio->{'biblionumber'}, $marcrecord ) );
+        $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 ) ) : [];
                $oldbiblio->{normalized_upc}  = GetNormalizedUPC(       $marcrecord,$marcflavour);
                $oldbiblio->{normalized_ean}  = GetNormalizedEAN(       $marcrecord,$marcflavour);
                $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
@@ -1544,6 +1676,33 @@ sub searchResults {
 
         # Pull out the items fields
         my @fields = $marcrecord->field($itemtag);
+        my $marcflavor = C4::Context->preference("marcflavour");
+        # adding linked items that belong to host records
+        my $analyticsfield = '773';
+        if ($marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC') {
+            $analyticsfield = '773';
+        } elsif ($marcflavor eq 'UNIMARC') {
+            $analyticsfield = '461';
+        }
+        foreach my $hostfield ( $marcrecord->field($analyticsfield)) {
+            my $hostbiblionumber = $hostfield->subfield("0");
+            my $linkeditemnumber = $hostfield->subfield("9");
+            if(!$hostbiblionumber eq undef){
+                my $hostbiblio = GetMarcBiblio($hostbiblionumber, 1);
+                my ($itemfield, undef) = GetMarcFromKohaField( 'items.itemnumber', GetFrameworkCode($hostbiblionumber) );
+                if(!$hostbiblio eq undef){
+                    my @hostitems = $hostbiblio->field($itemfield);
+                    foreach my $hostitem (@hostitems){
+                        if ($hostitem->subfield("9") eq $linkeditemnumber){
+                            my $linkeditem =$hostitem;
+                            # append linked items if they exist
+                            if (!$linkeditem eq undef){
+                                push (@fields, $linkeditem);}
+                        }
+                    }
+                }
+            }
+        }
 
         # Setting item statuses for display
         my @available_items_loop;
@@ -1561,16 +1720,16 @@ sub searchResults {
         my $other_count           = 0;
         my $wthdrawn_count        = 0;
         my $itemlost_count        = 0;
+        my $hideatopac_count      = 0;
         my $itembinding_count     = 0;
         my $itemdamaged_count     = 0;
         my $item_in_transit_count = 0;
         my $can_place_holds       = 0;
-       my $item_onhold_count     = 0;
+        my $item_onhold_count     = 0;
         my $items_count           = scalar(@fields);
-        my $maxitems =
-          ( C4::Context->preference('maxItemsinSearchResults') )
-          ? C4::Context->preference('maxItemsinSearchResults') - 1
-          : 1;
+        my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults');
+        my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1;
+        my @hiddenitems; # hidden itemnumbers based on OpacHiddenItems syspref
 
         # loop through every item
         foreach my $field (@fields) {
@@ -1580,9 +1739,27 @@ sub searchResults {
             foreach my $code ( keys %subfieldstosearch ) {
                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
             }
+            $item->{description} = $itemtypes{ $item->{itype} }{description};
+
+               # OPAC hidden items
+            if ($is_opac) {
+                # hidden because lost
+                if ($hidelostitems && $item->{itemlost}) {
+                    $hideatopac_count++;
+                    next;
+                }
+                # hidden based on OpacHiddenItems syspref
+                my @hi = C4::Items::GetHiddenItemnumbers($item);
+                if (scalar @hi) {
+                    push @hiddenitems, @hi;
+                    $hideatopac_count++;
+                    next;
+                }
+            }
+
+            my $hbranch     = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch'    : 'holdingbranch';
+            my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
 
-                       my $hbranch     = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch'    : 'holdingbranch';
-                       my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
             # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
             if ($item->{$hbranch}) {
                 $item->{'branchname'} = $branches{$item->{$hbranch}};
@@ -1593,7 +1770,8 @@ sub searchResults {
 
                        my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
-            if ( $item->{onloan} ) {
+            my $userenv = C4::Context->userenv;
+            if ( $item->{onloan} && !(C4::Members::GetHideLostItemsPreference($userenv->{'number'}) && $item->{itemlost}) ) {
                 $onloan_count++;
                                my $key = $prefix . $item->{onloan} . $item->{barcode};
                                $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
@@ -1601,7 +1779,8 @@ sub searchResults {
                                $onloan_items->{$key}->{branchname} = $item->{branchname};
                                $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
                                $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
-                               $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
+                               $onloan_items->{$key}->{description} = $item->{description};
+                               $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
                 # if something's checked out and lost, mark it as 'long overdue'
                 if ( $item->{itemlost} ) {
                     $onloan_items->{$prefix}->{longoverdue}++;
@@ -1624,7 +1803,7 @@ sub searchResults {
                 my ($transfertfrom, $transfertto);
 
                 # is item on the reserve shelf?
-               my $reservestatus = 0;
+               my $reservestatus = '';
                my $reserveitem;
 
                 unless ($item->{wthdrawn}
@@ -1646,10 +1825,10 @@ sub searchResults {
                     #        should map transit status to record indexed in Zebra.
                     #
                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
-                   ($reservestatus, $reserveitem) = C4::Reserves::CheckReserves($item->{itemnumber});
+                   ($reservestatus, $reserveitem, undef) = C4::Reserves::CheckReserves($item->{itemnumber});
                 }
 
-                # item is withdrawn, lost or damaged
+                # item is withdrawn, lost, damaged, not for loan, reserved or in transit
                 if (   $item->{wthdrawn}
                     || $item->{itemlost}
                     || $item->{damaged}
@@ -1663,37 +1842,49 @@ sub searchResults {
                     $item_in_transit_count++ if $transfertwhen ne '';
                    $item_onhold_count++     if $reservestatus eq 'Waiting';
                     $item->{status} = $item->{wthdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
+
+                    # can place hold on item ?
+                    if ((!$item->{damaged} || C4::Context->preference('AllowHoldsOnDamagedItems'))
+                      && !$item->{itemlost}
+                      && !$item->{withdrawn}
+                    ) {
+                        $can_place_holds = 1;
+                    }
+                    
                     $other_count++;
 
-                                       my $key = $prefix . $item->{status};
-                                       foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber)) {
-                       $other_items->{$key}->{$_} = $item->{$_};
-                                       }
-                    $other_items->{$key}->{intransit} = ($transfertwhen ne '') ? 1 : 0;
+                    my $key = $prefix . $item->{status};
+                    foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber)) {
+                        $other_items->{$key}->{$_} = $item->{$_};
+                    }
+                    $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
                     $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
-                                       $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
+                    $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value and $item->{notforloan};
                                        $other_items->{$key}->{count}++ if $item->{$hbranch};
                                        $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
-                                       $other_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
+                                       $other_items->{$key}->{description} = $item->{description};
+                                       $other_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
                 }
                 # item is available
                 else {
                     $can_place_holds = 1;
                     $available_count++;
                                        $available_items->{$prefix}->{count}++ if $item->{$hbranch};
-                                       foreach (qw(branchname itemcallnumber)) {
+                                       foreach (qw(branchname itemcallnumber description)) {
                        $available_items->{$prefix}->{$_} = $item->{$_};
                                        }
                                        $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
-                                       $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
+                                       $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
                 }
             }
         }    # notforloan, item level and biblioitem level
+
+        # if all items are hidden, do not show the record
+        if ($items_count > 0 && $hideatopac_count == $items_count) {
+            next;
+        }
+
         my ( $availableitemscount, $onloanitemscount, $otheritemscount );
-        $maxitems =
-          ( C4::Context->preference('maxItemsinSearchResults') )
-          ? C4::Context->preference('maxItemsinSearchResults') - 1
-          : 1;
         for my $key ( sort keys %$onloan_items ) {
             (++$onloanitemscount > $maxitems) and last;
             push @onloan_items_loop, $onloan_items->{$key};
@@ -1710,14 +1901,11 @@ sub searchResults {
         # XSLT processing of some stuff
        use C4::Charset;
        SetUTF8Flag($marcrecord);
-       $debug && warn $marcrecord->as_formatted;
-        if (!$scan && $search_context eq 'opac' && C4::Context->preference("OPACXSLTResultsDisplay")) {
-            # FIXME note that XSLTResultsDisplay (use of XSLT to format staff interface bib search results)
-            # is not implemented yet
-            $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display($oldbiblio->{biblionumber}, $marcrecord, 'Results', 
-                                                                $search_context, 1);
-                # the last parameter tells Koha to clean up the problematic ampersand entities that Zebra outputs
-
+        warn $marcrecord->as_formatted if $DEBUG;
+       my $interface = $search_context eq 'opac' ? 'OPAC' : '';
+       if (!$scan && C4::Context->preference($interface . "XSLTResultsDisplay")) {
+            $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display($oldbiblio->{biblionumber}, $marcrecord, $interface."XSLTResultsDisplay", 1, \@hiddenitems);
+           # the last parameter tells Koha to clean up the problematic ampersand entities that Zebra outputs
         }
 
         # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
@@ -1744,12 +1932,36 @@ sub searchResults {
         $oldbiblio->{intransitcount}       = $item_in_transit_count;
         $oldbiblio->{onholdcount}          = $item_onhold_count;
         $oldbiblio->{orderedcount}         = $ordered_count;
-        $oldbiblio->{isbn} =~
-          s/-//g;    # deleting - in isbn to enable amazon content
-        push( @newresults, $oldbiblio )
-            if(not $hidelostitems
-               or (($items_count > $itemlost_count )
-                    && $hidelostitems));
+
+        if (C4::Context->preference("AlternateHoldingsField") && $items_count == 0) {
+            my $fieldspec = C4::Context->preference("AlternateHoldingsField");
+            my $subfields = substr $fieldspec, 3;
+            my $holdingsep = C4::Context->preference("AlternateHoldingsSeparator") || ' ';
+            my @alternateholdingsinfo = ();
+            my @holdingsfields = $marcrecord->field(substr $fieldspec, 0, 3);
+            my $alternateholdingscount = 0;
+
+            for my $field (@holdingsfields) {
+                my %holding = ( holding => '' );
+                my $havesubfield = 0;
+                for my $subfield ($field->subfields()) {
+                    if ((index $subfields, $$subfield[0]) >= 0) {
+                        $holding{'holding'} .= $holdingsep if (length $holding{'holding'} > 0);
+                        $holding{'holding'} .= $$subfield[1];
+                        $havesubfield++;
+                    }
+                }
+                if ($havesubfield) {
+                    push(@alternateholdingsinfo, \%holding);
+                    $alternateholdingscount++;
+                }
+            }
+
+            $oldbiblio->{'ALTERNATEHOLDINGS'} = \@alternateholdingsinfo;
+            $oldbiblio->{'alternateholdings_count'} = $alternateholdingscount;
+        }
+
+        push( @newresults, $oldbiblio );
     }
 
     return @newresults;
@@ -2234,7 +2446,7 @@ sub NZorder {
     # sort the hash and return the same structure as GetRecords (Zebra querying)
         my $result_hash;
         my $numbers = 0;
-        if ( $ordering eq 'author_za' ) {    # sort by author desc
+        if ( $ordering eq 'author_za' || $ordering eq 'author_dsc' ) {    # sort by author desc
             foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
                 $result_hash->{'RECORDS'}[ $numbers++ ] =
                   $result{$key}->as_usmarc();
@@ -2540,7 +2752,15 @@ $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
 
 sub z3950_search_args {
     my $bibrec = shift;
-    $bibrec = { title => $bibrec } if !ref $bibrec;
+    my $isbn = Business::ISBN->new($bibrec);
+
+    if (defined $isbn && $isbn->is_valid)
+    {
+        $bibrec = { isbn => $bibrec } if !ref $bibrec;
+    }
+    else {
+        $bibrec = { title => $bibrec } if !ref $bibrec;
+    }
     my $array = [];
     for my $field (qw/ lccn isbn issn title author dewey subject /)
     {
@@ -2550,105 +2770,6 @@ sub z3950_search_args {
     return $array;
 }
 
-=head2 BiblioAddAuthorities
-
-( $countlinked, $countcreated ) = BiblioAddAuthorities($record, $frameworkcode);
-
-this function finds the authorities linked to the biblio
-    * search in the authority DB for the same authid (in $9 of the biblio)
-    * search in the authority DB for the same 001 (in $3 of the biblio in UNIMARC)
-    * search in the authority DB for the same values (exactly) (in all subfields of the biblio)
-OR adds a new authority record
-
-=over 2
-
-=item C<input arg:>
-
-    * $record is the MARC record in question (marc blob)
-    * $frameworkcode is the bibliographic framework to use (if it is "" it uses the default framework)
-
-=item C<Output arg:>
-
-    * $countlinked is the number of authorities records that are linked to this authority
-    * $countcreated
-
-=item C<BUGS>
-    * I had to add this to Search.pm (instead of the logical Biblio.pm) because of a circular dependency (this sub uses SimpleSearch, and Search.pm uses Biblio.pm)
-
-=back
-
-=cut
-
-
-sub BiblioAddAuthorities{
-  my ( $record, $frameworkcode ) = @_;
-  my $dbh=C4::Context->dbh;
-  my $query=$dbh->prepare(qq|
-SELECT authtypecode,tagfield
-FROM marc_subfield_structure
-WHERE frameworkcode=?
-AND (authtypecode IS NOT NULL AND authtypecode<>\"\")|);
-# SELECT authtypecode,tagfield
-# FROM marc_subfield_structure
-# WHERE frameworkcode=?
-# AND (authtypecode IS NOT NULL OR authtypecode<>\"\")|);
-  $query->execute($frameworkcode);
-  my ($countcreated,$countlinked);
-  while (my $data=$query->fetchrow_hashref){
-    foreach my $field ($record->field($data->{tagfield})){
-      next if ($field->subfield('3')||$field->subfield('9'));
-      # No authorities id in the tag.
-      # Search if there is any authorities to link to.
-      my $query='at='.$data->{authtypecode}.' ';
-      map {$query.= ' and he,ext="'.$_->[1].'"' if ($_->[0]=~/[A-z]/)}  $field->subfields();
-      my ($error, $results, $total_hits)=SimpleSearch( $query, undef, undef, [ "authorityserver" ] );
-    # there is only 1 result
-          if ( $error ) {
-        warn "BIBLIOADDSAUTHORITIES: $error";
-            return (0,0) ;
-          }
-      if ($results && scalar(@$results)==1) {
-        my $marcrecord = MARC::File::USMARC::decode($results->[0]);
-        $field->add_subfields('9'=>$marcrecord->field('001')->data);
-        $countlinked++;
-      } elsif (scalar(@$results)>1) {
-   #More than One result
-   #This can comes out of a lack of a subfield.
-#         my $marcrecord = MARC::File::USMARC::decode($results->[0]);
-#         $record->field($data->{tagfield})->add_subfields('9'=>$marcrecord->field('001')->data);
-  $countlinked++;
-      } else {
-  #There are no results, build authority record, add it to Authorities, get authid and add it to 9
-  ###NOTICE : This is only valid if a subfield is linked to one and only one authtypecode
-  ###NOTICE : This can be a problem. We should also look into other types and rejected forms.
-         my $authtypedata=C4::AuthoritiesMarc::GetAuthType($data->{authtypecode});
-         next unless $authtypedata;
-         my $marcrecordauth=MARC::Record->new();
-         my $authfield=MARC::Field->new($authtypedata->{auth_tag_to_report},'','',"a"=>"".$field->subfield('a'));
-         map { $authfield->add_subfields($_->[0]=>$_->[1]) if ($_->[0]=~/[A-z]/ && $_->[0] ne "a" )}  $field->subfields();
-         $marcrecordauth->insert_fields_ordered($authfield);
-
-         # bug 2317: ensure new authority knows it's using UTF-8; currently
-         # only need to do this for MARC21, as MARC::Record->as_xml_record() handles
-         # automatically for UNIMARC (by not transcoding)
-         # FIXME: AddAuthority() instead should simply explicitly require that the MARC::Record
-         # use UTF-8, but as of 2008-08-05, did not want to introduce that kind
-         # of change to a core API just before the 3.0 release.
-         if (C4::Context->preference('marcflavour') eq 'MARC21') {
-            SetMarcUnicodeFlag($marcrecordauth, 'MARC21');
-         }
-
-#          warn "AUTH RECORD ADDED : ".$marcrecordauth->as_formatted;
-
-         my $authid=AddAuthority($marcrecordauth,'',$data->{authtypecode});
-         $countcreated++;
-         $field->add_subfields('9'=>$authid);
-      }
-    }
-  }
-  return ($countlinked,$countcreated);
-}
-
 =head2 GetDistinctValues($field);
 
 C<$field> is a reference to the fields array