Bug 22043: (QA follow-up) Add parameter to control behavior
[koha.git] / C4 / Search.pm
index 7abe096..ef81e19 100644 (file)
@@ -2,18 +2,18 @@ package C4::Search;
 
 # This file is part of Koha.
 #
 
 # This file is part of Koha.
 #
-# Koha is free software; you can redistribute it and/or modify it under the
-# terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 2 of the License, or (at your option) any later
-# version.
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
 #
 #
-# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
 #
 #
-# You should have received a copy of the GNU General Public License along with
-# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
-# Suite 330, Boston, MA  02111-1307 USA
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
 
 use strict;
 #use warnings; FIXME - Bug 2505
 
 use strict;
 #use warnings; FIXME - Bug 2505
@@ -21,27 +21,27 @@ require Exporter;
 use C4::Context;
 use C4::Biblio;    # GetMarcFromKohaField, GetBiblioData
 use C4::Koha;      # getFacets
 use C4::Context;
 use C4::Biblio;    # GetMarcFromKohaField, GetBiblioData
 use C4::Koha;      # getFacets
+use Koha::DateUtils;
+use Koha::Libraries;
 use Lingua::Stem;
 use C4::Search::PazPar2;
 use XML::Simple;
 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::XSLT;
-use C4::Branch;
 use C4::Reserves;    # GetReserveStatus
 use C4::Debug;
 use C4::Charset;
 use C4::Reserves;    # GetReserveStatus
 use C4::Debug;
 use C4::Charset;
+use Koha::AuthorisedValues;
+use Koha::ItemTypes;
+use Koha::Libraries;
+use Koha::Patrons;
 use YAML;
 use URI::Escape;
 use Business::ISBN;
 use MARC::Record;
 use MARC::Field;
 use YAML;
 use URI::Escape;
 use Business::ISBN;
 use MARC::Record;
 use MARC::Field;
-use utf8;
-use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
+use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
 
 
-# set the version for version checking
 BEGIN {
 BEGIN {
-    $VERSION = 3.07.00.049;
     $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
 }
 
     $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
 }
 
@@ -70,7 +70,6 @@ This module provides searching functions for Koha's bibliographic databases
   &buildQuery
   &GetDistinctValues
   &enabled_staff_search_views
   &buildQuery
   &GetDistinctValues
   &enabled_staff_search_views
-  &PurgeSearchHistory
 );
 
 # make all your functions, whether exported or not;
 );
 
 # make all your functions, whether exported or not;
@@ -86,7 +85,7 @@ This function attempts to find duplicate records using a hard-coded, fairly simp
 sub FindDuplicate {
     my ($record) = @_;
     my $dbh = C4::Context->dbh;
 sub FindDuplicate {
     my ($record) = @_;
     my $dbh = C4::Context->dbh;
-    my $result = TransformMarcToKoha( $dbh, $record, '' );
+    my $result = TransformMarcToKoha( $record, '' );
     my $sth;
     my $query;
     my $search;
     my $sth;
     my $query;
     my $search;
@@ -148,7 +147,7 @@ sub FindDuplicate {
                 $possible_duplicate_record
             );
 
                 $possible_duplicate_record
             );
 
-            my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
+            my $result = TransformMarcToKoha( $marcrecord, '' );
 
             # FIXME :: why 2 $biblionumber ?
             if ($result) {
 
             # FIXME :: why 2 $biblionumber ?
             if ($result) {
@@ -162,7 +161,7 @@ sub FindDuplicate {
 
 =head2 SimpleSearch
 
 
 =head2 SimpleSearch
 
-( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers] );
+( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers], [%options] );
 
 This function provides a simple search API on the bibliographic catalog
 
 
 This function provides a simple search API on the bibliographic catalog
 
@@ -172,8 +171,9 @@ This function provides a simple search API on the bibliographic catalog
 
     * $query can be a simple keyword or a complete CCL query
     * @servers is optional. Defaults to biblioserver as found in koha-conf.xml
 
     * $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
+    * $offset - If present, represents the number of records at the beginning to omit. Defaults to 0
     * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
     * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
+    * %options is optional. (e.g. "skip_normalize" allows you to skip changing : to = )
 
 
 =item C<Return:>
 
 
 =item C<Return:>
@@ -204,7 +204,7 @@ my @results;
 
 for my $r ( @{$marcresults} ) {
     my $marcrecord = MARC::File::USMARC::decode($r);
 
 for my $r ( @{$marcresults} ) {
     my $marcrecord = MARC::File::USMARC::decode($r);
-    my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,q{});
+    my $biblio = TransformMarcToKoha($marcrecord,q{});
 
     #build the iarray of hashs for the template.
     push @results, {
 
     #build the iarray of hashs for the template.
     push @results, {
@@ -223,7 +223,7 @@ $template->param(result=>\@results);
 =cut
 
 sub SimpleSearch {
 =cut
 
 sub SimpleSearch {
-    my ( $query, $offset, $max_results, $servers )  = @_;
+    my ( $query, $offset, $max_results, $servers, %options )  = @_;
 
     return ( 'No query entered', undef, undef ) unless $query;
     # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
 
     return ( 'No query entered', undef, undef ) unless $query;
     # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
@@ -245,12 +245,12 @@ sub SimpleSearch {
         eval {
             $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
             if ($QParser) {
         eval {
             $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
             if ($QParser) {
-                $query =~ s/=/:/g;
+                $query =~ s/=/:/g unless $options{skip_normalize};
                 $QParser->parse( $query );
                 $query = $QParser->target_syntax($servers[$i]);
                 $zoom_queries[$i] = new ZOOM::Query::PQF( $query, $zconns[$i]);
             } else {
                 $QParser->parse( $query );
                 $query = $QParser->target_syntax($servers[$i]);
                 $zoom_queries[$i] = new ZOOM::Query::PQF( $query, $zconns[$i]);
             } else {
-                $query =~ s/:/=/g;
+                $query =~ s/:/=/g unless $options{skip_normalize};
                 $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
             }
             $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
                 $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
             }
             $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
@@ -332,6 +332,7 @@ sub getRecords {
 
     my @servers = @$servers_ref;
     my @sort_by = @$sort_by_ref;
 
     my @servers = @$servers_ref;
     my @sort_by = @$sort_by_ref;
+    $offset = 0 if $offset < 0;
 
     # Initialize variables for the ZOOM connection and results object
     my $zconn;
 
     # Initialize variables for the ZOOM connection and results object
     my $zconn;
@@ -339,6 +340,9 @@ sub getRecords {
     my @results;
     my $results_hashref = ();
 
     my @results;
     my $results_hashref = ();
 
+    # TODO simplify this structure ( { branchcode => $branchname } is enought) and remove this parameter
+    $branches ||= { map { $_->branchcode => { branchname => $_->branchname } } Koha::Libraries->search };
+
     # Initialize variables for the faceted results objects
     my $facets_counter = {};
     my $facets_info    = {};
     # Initialize variables for the faceted results objects
     my $facets_counter = {};
     my $facets_info    = {};
@@ -572,15 +576,22 @@ sub getRecords {
                                     {
                                         $facet_label_value =
                                           $itemtypes->{$one_facet}
                                     {
                                         $facet_label_value =
                                           $itemtypes->{$one_facet}
-                                          ->{'description'};
+                                          ->{translated_description};
                                     }
                                 }
 
                # also, if it's a location code, use the name instead of the code
                                 if ( $link_value =~ /location/ ) {
                                     }
                                 }
 
                # also, if it's a location code, use the name instead of the code
                                 if ( $link_value =~ /location/ ) {
-                                    $facet_label_value =
-                                      GetKohaAuthorisedValueLib( 'LOC',
-                                        $one_facet, $opac );
+                                    # TODO Retrieve all authorised values at once, instead of 1 query per entry
+                                    my $av = Koha::AuthorisedValues->search({ category => 'LOC', authorised_value => $one_facet });
+                                    $facet_label_value = $av->count ? $av->next->opac_description : '';
+                                }
+
+                                # also, if it's a collection code, use the name instead of the code
+                                if ( $link_value =~ /ccode/ ) {
+                                    # TODO Retrieve all authorised values at once, instead of 1 query per entry
+                                    my $av = Koha::AuthorisedValues->search({ category => 'CCODE', authorised_value => $one_facet });
+                                    $facet_label_value = $av->count ? $av->next->opac_description : '';
                                 }
 
                 # but we're down with the whole label being in the link's title.
                                 }
 
                 # but we're down with the whole label being in the link's title.
@@ -620,12 +631,21 @@ sub getRecords {
                                 $facets_info->{$link_value}->{'label_value'} =~
                                 /Libraries/
                             )
                                 $facets_info->{$link_value}->{'label_value'} =~
                                 /Libraries/
                             )
-                            and ( C4::Context->preference('singleBranchMode') )
+                            and ( Koha::Libraries->search->count == 1 )
                           );
                     }
                 }
             }
         );
                           );
                     }
                 }
             }
         );
+
+    # This sorts the facets into alphabetical order
+    if (@facets_loop) {
+        foreach my $f (@facets_loop) {
+            $f->{facets} = [ sort { uc($a->{facet_label_value}) cmp uc($b->{facet_label_value}) } @{ $f->{facets} } ];
+        }
+        @facets_loop = sort {$a->{expand} cmp $b->{expand}} @facets_loop;
+    }
+
     return ( undef, $results_hashref, \@facets_loop );
 }
 
     return ( undef, $results_hashref, \@facets_loop );
 }
 
@@ -634,9 +654,9 @@ sub GetFacets {
     my $rs = shift;
     my $facets;
 
     my $rs = shift;
     my $facets;
 
-    my $index_mode = C4::Context->config('zebra_auth_index_mode') // 'dom';
+    my $use_zebra_facets = C4::Context->config('use_zebra_facets') // 0;
 
 
-    if ( $index_mode eq 'dom' ) {
+    if ( $use_zebra_facets ) {
         $facets = _get_facets_from_zebra( $rs );
     } else {
         $facets = _get_facets_from_records( $rs );
         $facets = _get_facets_from_zebra( $rs );
     } else {
         $facets = _get_facets_from_records( $rs );
@@ -786,11 +806,12 @@ sub _get_facet_from_result_set {
     my $rs        = shift;
     my $sep       = shift;
 
     my $rs        = shift;
     my $sep       = shift;
 
-    my $internal_sep = '<*>';
+    my $internal_sep  = '<*>';
+    my $facetMaxCount = C4::Context->preference('FacetMaxCount') // 20;
 
     return if ( ! defined $facet_idx || ! defined $rs );
     # zebra's facet element, untokenized index
 
     return if ( ! defined $facet_idx || ! defined $rs );
     # zebra's facet element, untokenized index
-    my $facet_element = 'zebra::facet::' . $facet_idx . ':0:100';
+    my $facet_element = 'zebra::facet::' . $facet_idx . ':0:' . $facetMaxCount;
     # configure zebra results for retrieving the desired facet
     $rs->option( elementSetName => $facet_element );
     # get the facet record from result set
     # configure zebra results for retrieving the desired facet
     $rs->option( elementSetName => $facet_element );
     # get the facet record from result set
@@ -843,6 +864,7 @@ sub pazGetRecords {
         $results_per_page, $offset,       $expanded_facet, $branches,
         $query_type,       $scan
     ) = @_;
         $results_per_page, $offset,       $expanded_facet, $branches,
         $query_type,       $scan
     ) = @_;
+    #NOTE: Parameter $branches is not used here !
 
     my $paz = C4::Search::PazPar2->new(C4::Context->config('pazpar2url'));
     $paz->init();
 
     my $paz = C4::Search::PazPar2->new(C4::Context->config('pazpar2url'));
     $paz->init();
@@ -910,32 +932,6 @@ sub pazGetRecords {
     return ( undef, $results_hashref, \@facets_loop );
 }
 
     return ( undef, $results_hashref, \@facets_loop );
 }
 
-# STOPWORDS
-sub _remove_stopwords {
-    my ( $operand, $index ) = @_;
-    my @stopwords_removed;
-
-    # phrase and exact-qualified indexes shouldn't have stopwords removed
-    if ( $index !~ m/,(phr|ext)/ ) {
-
-# remove stopwords from operand : parse all stopwords & remove them (case insensitive)
-#       we use IsAlpha unicode definition, to deal correctly with diacritics.
-#       otherwise, a French word like "leçon" woudl be split into "le" "çon", "le"
-#       is a stopword, we'd get "çon" and wouldn't find anything...
-#
-               foreach ( keys %{ C4::Context->stopwords } ) {
-                       next if ( $_ =~ /(and|or|not)/ );    # don't remove operators
-                       if ( my ($matched) = ($operand =~
-                               /([^\X\p{isAlnum}]\Q$_\E[^\X\p{isAlnum}]|[^\X\p{isAlnum}]\Q$_\E$|^\Q$_\E[^\X\p{isAlnum}])/gi))
-                       {
-                               $operand =~ s/\Q$matched\E/ /gi;
-                               push @stopwords_removed, $_;
-                       }
-               }
-       }
-    return ( $operand, \@stopwords_removed );
-}
-
 # TRUNCATION
 sub _detect_truncation {
     my ( $operand, $index ) = @_;
 # TRUNCATION
 sub _detect_truncation {
     my ( $operand, $index ) = @_;
@@ -972,6 +968,9 @@ sub _build_stemmed_operand {
     require Lingua::Stem::Snowball ;
     my $stemmed_operand=q{};
 
     require Lingua::Stem::Snowball ;
     my $stemmed_operand=q{};
 
+    # Stemmer needs language
+    return $operand unless $lang;
+
     # 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
     # can contain the letter "X" - for example, _build_stemmend_operand would reduce
     # 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
     # can contain the letter "X" - for example, _build_stemmend_operand would reduce
@@ -1046,6 +1045,11 @@ sub _build_weighted_query {
         $weighted_query .= "an=\"$operand\"";
     }
 
         $weighted_query .= "an=\"$operand\"";
     }
 
+    # If the index is numeric, don't autoquote it.
+    elsif ( $index =~ /,st-numeric$/ ) {
+        $weighted_query .= " $index=$operand";
+    }
+
     # If the index already has more than one qualifier, wrap the operand
     # in quotes and pass it back (assumption is that the user knows what they
     # are doing and won't appreciate us mucking up their query
     # If the index already has more than one qualifier, wrap the operand
     # in quotes and pass it back (assumption is that the user knows what they
     # are doing and won't appreciate us mucking up their query
@@ -1085,6 +1089,8 @@ sub getIndexes{
                     'an',
                     'Any',
                     'at',
                     'an',
                     'Any',
                     'at',
+                    'arl',
+                    'arp',
                     'au',
                     'aub',
                     'aud',
                     'au',
                     'aub',
                     'aud',
@@ -1129,8 +1135,11 @@ sub getIndexes{
                     'date-entered-on-file',
                     'Date-of-acquisition',
                     'Date-of-publication',
                     'date-entered-on-file',
                     'Date-of-acquisition',
                     'Date-of-publication',
+                    'Date-time-last-modified',
                     'Dewey-classification',
                     'Dissertation-information',
                     'Dewey-classification',
                     'Dissertation-information',
+                    'diss',
+                    'dtlm',
                     'EAN',
                     'extent',
                     'fic',
                     'EAN',
                     'extent',
                     'fic',
@@ -1145,9 +1154,13 @@ sub getIndexes{
                     'Heading-use-subject-added-entry',
                     'Host-item',
                     'id-other',
                     'Heading-use-subject-added-entry',
                     'Host-item',
                     'id-other',
+                    'ident',
+                    'Identifier-standard',
                     'Illustration-code',
                     'Index-term-genre',
                     'Index-term-uncontrolled',
                     'Illustration-code',
                     'Index-term-genre',
                     'Index-term-uncontrolled',
+                    'Interest-age-level',
+                    'Interest-grade-level',
                     'ISBN',
                     'isbn',
                     'ISSN',
                     'ISBN',
                     'isbn',
                     'ISSN',
@@ -1162,6 +1175,7 @@ sub getIndexes{
                     'LC-card-number',
                     'lcn',
                     'lex',
                     'LC-card-number',
                     'lcn',
                     'lex',
+                    'lexile-number',
                     'llength',
                     'ln',
                     'ln-audio',
                     'llength',
                     'ln',
                     'ln-audio',
@@ -1185,6 +1199,7 @@ sub getIndexes{
                     'notes',
                     'ns',
                     'nt',
                     'notes',
                     'ns',
                     'nt',
+                    'Other-control-number',
                     'pb',
                     'Personal-name',
                     'Personal-name-heading',
                     'pb',
                     'Personal-name',
                     'Personal-name-heading',
@@ -1196,6 +1211,9 @@ sub getIndexes{
                     'popularity',
                     'pubdate',
                     'Publisher',
                     'popularity',
                     'pubdate',
                     'Publisher',
+                    'Provider',
+                    'pv',
+                    'Reading-grade-level',
                     'Record-control-number',
                     'rcn',
                     'Record-type',
                     'Record-control-number',
                     'rcn',
                     'Record-type',
@@ -1327,12 +1345,13 @@ sub _handle_exploding_index {
 
     ( $operators, $operands, $indexes, $limits,
       $sort_by, $scan, $lang ) =
 
     ( $operators, $operands, $indexes, $limits,
       $sort_by, $scan, $lang ) =
-            buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
+            parseQuery ( $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.
 
 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 {
 =cut
 
 sub parseQuery {
@@ -1412,10 +1431,10 @@ sub parseQuery {
 $simple_query, $query_cgi,
 $query_desc, $limit,
 $limit_cgi, $limit_desc,
 $simple_query, $query_cgi,
 $query_desc, $limit,
 $limit_cgi, $limit_desc,
-$stopwords_removed, $query_type ) = buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
+$query_type ) = buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
 
 Build queries and limits in CCL, CGI, Human,
 
 Build queries and limits in CCL, CGI, Human,
-handle truncation, stemming, field weighting, stopwords, fuzziness, etc.
+handle truncation, stemming, field weighting, fuzziness, etc.
 
 See verbose embedded documentation.
 
 
 See verbose embedded documentation.
 
@@ -1441,7 +1460,6 @@ sub buildQuery {
     my $auto_truncation  = C4::Context->preference("QueryAutoTruncate")    || 0;
     my $weight_fields    = C4::Context->preference("QueryWeightFields")    || 0;
     my $fuzzy_enabled    = C4::Context->preference("QueryFuzzy")           || 0;
     my $auto_truncation  = C4::Context->preference("QueryAutoTruncate")    || 0;
     my $weight_fields    = C4::Context->preference("QueryWeightFields")    || 0;
     my $fuzzy_enabled    = C4::Context->preference("QueryFuzzy")           || 0;
-    my $remove_stopwords = C4::Context->preference("QueryRemoveStopwords") || 0;
 
     my $query        = $operands[0];
     my $simple_query = $operands[0];
 
     my $query        = $operands[0];
     my $simple_query = $operands[0];
@@ -1454,8 +1472,6 @@ sub buildQuery {
     my $limit_cgi;
     my $limit_desc;
 
     my $limit_cgi;
     my $limit_desc;
 
-    my $stopwords_removed;    # flag to determine if stopwords have been removed
-
     my $cclq       = 0;
     my $cclindexes = getIndexes();
     if ( $query !~ /\s*(ccl=|pqf=|cql=)/ ) {
     my $cclq       = 0;
     my $cclindexes = getIndexes();
     if ( $query !~ /\s*(ccl=|pqf=|cql=)/ ) {
@@ -1473,22 +1489,31 @@ sub buildQuery {
         # This is needed otherwise ccl= and &limit won't work together, and
         # this happens when selecting a subject on the opac-detail page
         @limits = grep {!/^$/} @limits;
         # This is needed otherwise ccl= and &limit won't work together, and
         # this happens when selecting a subject on the opac-detail page
         @limits = grep {!/^$/} @limits;
+        my $original_q = $q; # without available part
+        unless ( grep { /^available$/ } @limits ) {
+            $q =~ s| and \( \( allrecords,AlwaysMatches:'' not onloan,AlwaysMatches:''\) and \(lost,st-numeric=0\) \)||;
+            $original_q = $q;
+        }
         if ( @limits ) {
         if ( @limits ) {
-            $q .= ' and '.join(' and ', @limits);
+            if ( grep { /^available$/ } @limits ) {
+                $q .= q| and ( ( allrecords,AlwaysMatches:'' not onloan,AlwaysMatches:'') and (lost,st-numeric=0) )|;
+                delete $limits['available'];
+            }
+            $q .= ' and '.join(' and ', @limits) if @limits;
         }
         }
-        return ( undef, $q, $q, "q=ccl=".uri_escape($q), $q, '', '', '', '', 'ccl' );
+        return ( undef, $q, $q, "q=ccl=".uri_escape_utf8($q), $original_q, '', '', '', 'ccl' );
     }
     if ( $query =~ /^cql=/ ) {
     }
     if ( $query =~ /^cql=/ ) {
-        return ( undef, $', $', "q=cql=".uri_escape($'), $', '', '', '', '', 'cql' );
+        return ( undef, $', $', "q=cql=".uri_escape_utf8($'), $', '', '', '', 'cql' );
     }
     if ( $query =~ /^pqf=/ ) {
         if ($query_desc) {
     }
     if ( $query =~ /^pqf=/ ) {
         if ($query_desc) {
-            $query_cgi = "q=".uri_escape($query_desc);
+            $query_cgi = "q=".uri_escape_utf8($query_desc);
         } else {
             $query_desc = $';
         } else {
             $query_desc = $';
-            $query_cgi = "q=pqf=".uri_escape($');
+            $query_cgi = "q=pqf=".uri_escape_utf8($');
         }
         }
-        return ( undef, $', $', $query_cgi, $query_desc, '', '', '', '', 'pqf' );
+        return ( undef, $', $', $query_cgi, $query_desc, '', '', '', 'pqf' );
     }
 
     # pass nested queries directly
     }
 
     # pass nested queries directly
@@ -1499,7 +1524,7 @@ sub buildQuery {
 #        return (
 #            undef,              $query, $simple_query, $query_cgi,
 #            $query,             $limit, $limit_cgi,    $limit_desc,
 #        return (
 #            undef,              $query, $simple_query, $query_cgi,
 #            $query,             $limit, $limit_cgi,    $limit_desc,
-#            $stopwords_removed, 'ccl'
+#            'ccl'
 #        );
 #    }
 
 #        );
 #    }
 
@@ -1517,17 +1542,16 @@ sub buildQuery {
         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
 
             # COMBINE OPERANDS, INDEXES AND OPERATORS
         for ( my $i = 0 ; $i <= @operands ; $i++ ) {
 
             # COMBINE OPERANDS, INDEXES AND OPERATORS
-            if ( $operands[$i] ) {
+            if ( ($operands[$i] // '') ne '' ) {
                $operands[$i]=~s/^\s+//;
 
               # A flag to determine whether or not to add the index to the query
                 my $indexes_set;
 
                $operands[$i]=~s/^\s+//;
 
               # A flag to determine whether or not to add the index to the query
                 my $indexes_set;
 
-# If the user is sophisticated enough to specify an index, turn off field weighting, stemming, and stopword handling
+# If the user is sophisticated enough to specify an index, turn off field weighting, and stemming handling
                 if ( $operands[$i] =~ /\w(:|=)/ || $scan ) {
                     $weight_fields    = 0;
                     $stemming         = 0;
                 if ( $operands[$i] =~ /\w(:|=)/ || $scan ) {
                     $weight_fields    = 0;
                     $stemming         = 0;
-                    $remove_stopwords = 0;
                 } else {
                     $operands[$i] =~ s/\?/{?}/g; # need to escape question marks
                 }
                 } else {
                     $operands[$i] =~ s/\?/{?}/g; # need to escape question marks
                 }
@@ -1535,27 +1559,43 @@ sub buildQuery {
                 my $index   = $indexes[$i];
 
                 # Add index-specific attributes
                 my $index   = $indexes[$i];
 
                 # Add index-specific attributes
-                # Date of Publication
-                if ( $index eq 'yr' ) {
-                    $index .= ",st-numeric";
-                    $indexes_set++;
-                                       $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
+
+                #Afaik, this 'yr' condition will only ever be met in the staff client advanced search
+                #for "Publication date", since typing 'yr:YYYY' into the search box produces a CCL query,
+                #which is processed higher up in this sub. Other than that, year searches are typically
+                #handled as limits which are not processed her either.
+
+                # Search ranges: Date of Publication, st-numeric
+                if ( $index =~ /(yr|st-numeric)/ ) {
+                    #weight_fields/relevance search causes errors with date ranges
+                    #In the case of YYYY-, it will only return records with a 'yr' of YYYY (not the range)
+                    #In the case of YYYY-YYYY, it will return no results
+                    $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = 0;
                 }
 
                 # Date of Acquisition
                 }
 
                 # Date of Acquisition
-                elsif ( $index eq 'acqdate' ) {
-                    $index .= ",st-date-normalized";
-                    $indexes_set++;
-                                       $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
+                elsif ( $index =~ /acqdate/ ) {
+                    #stemming and auto_truncation would have zero impact since it already is YYYY-MM-DD format
+                    #Weight_fields probably SHOULD be turned OFF, otherwise you'll get records floating to the
+                      #top of the results just because they have lots of item records matching that date.
+                    #Fuzzy actually only applies during _build_weighted_query, and is reset there anyway, so
+                      #irrelevant here
+                    $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = 0;
                 }
                 # ISBN,ISSN,Standard Number, don't need special treatment
                 }
                 # ISBN,ISSN,Standard Number, don't need special treatment
-                elsif ( $index eq 'nb' || $index eq 'ns' ) {
+                elsif ( $index eq 'nb' || $index eq 'ns' || $index eq 'hi' ) {
                     (
                         $stemming,      $auto_truncation,
                     (
                         $stemming,      $auto_truncation,
-                        $weight_fields, $fuzzy_enabled,
-                        $remove_stopwords
-                    ) = ( 0, 0, 0, 0, 0 );
-
+                        $weight_fields, $fuzzy_enabled
+                    ) = ( 0, 0, 0, 0 );
+
+                    if ( $index eq 'nb' ) {
+                        if ( C4::Context->preference("SearchWithISBNVariations") ) {
+                            my @isbns = C4::Koha::GetVariationsOfISBN( $operand );
+                            $operands[$i] = $operand =  '(nb=' . join(' OR nb=', @isbns) . ')';
+                            $indexes[$i] = $index = '';
+                        }
+                    }
                 }
 
                 if(not $index){
                 }
 
                 if(not $index){
@@ -1572,15 +1612,6 @@ sub buildQuery {
                 my $index_plus       = $index . $struct_attr . ':';
                 my $index_plus_comma = $index . $struct_attr . ',';
 
                 my $index_plus       = $index . $struct_attr . ':';
                 my $index_plus_comma = $index . $struct_attr . ',';
 
-                # Remove Stopwords
-                if ($remove_stopwords) {
-                    ( $operand, $stopwords_removed ) =
-                      _remove_stopwords( $operand, $index );
-                    warn "OPERAND w/out STOPWORDS: >$operand<" if $DEBUG;
-                    warn "REMOVED STOPWORDS: @$stopwords_removed"
-                      if ( $stopwords_removed && $DEBUG );
-                }
-
                 if ($auto_truncation){
                         unless ( $index =~ /,(st-|phr|ext)/ ) {
                                                #FIXME only valid with LTR scripts
                 if ($auto_truncation){
                         unless ( $index =~ /,(st-|phr|ext)/ ) {
                                                #FIXME only valid with LTR scripts
@@ -1656,7 +1687,7 @@ sub buildQuery {
                     query_desc => $query_desc,
                     operator => ($operators[ $i - 1 ]) ? $operators[ $i - 1 ] : '',
                     parsed_operand => $operand,
                     query_desc => $query_desc,
                     operator => ($operators[ $i - 1 ]) ? $operators[ $i - 1 ] : '',
                     parsed_operand => $operand,
-                    original_operand => ($operands[$i]) ? $operands[$i] : '',
+                    original_operand => $operands[$i] // '',
                     index => $index,
                     index_plus => $index_plus,
                     indexes_set => $indexes_set,
                     index => $index,
                     index_plus => $index_plus,
                     indexes_set => $indexes_set,
@@ -1691,13 +1722,13 @@ sub buildQuery {
             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;
             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;
-                $this_limit = $k.":\"".$v."\"";
+                $this_limit = $k.":'".$v."'";
             }
 
             $group_OR_limits{$k} .= " or " if $group_OR_limits{$k};
             $limit_desc      .= " or " if $group_OR_limits{$k};
             $group_OR_limits{$k} .= "$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=" . uri_escape($this_limit);
+            $limit_cgi       .= "&limit=" . uri_escape_utf8($this_limit);
             $limit_desc      .= " $this_limit";
         }
 
             $limit_desc      .= " $this_limit";
         }
 
@@ -1705,12 +1736,12 @@ sub buildQuery {
         else {
             $limit .= " and " if $limit || $query;
             $limit      .= "$this_limit";
         else {
             $limit .= " and " if $limit || $query;
             $limit      .= "$this_limit";
-            $limit_cgi  .= "&limit=" . uri_escape($this_limit);
+            $limit_cgi  .= "&limit=" . uri_escape_utf8($this_limit);
             if ($this_limit =~ /^branch:(.+)/) {
                 my $branchcode = $1;
             if ($this_limit =~ /^branch:(.+)/) {
                 my $branchcode = $1;
-                my $branchname = GetBranchName($branchcode);
-                if (defined $branchname) {
-                    $limit_desc .= " branch:$branchname";
+                my $library = Koha::Libraries->find( $branchcode );
+                if (defined $library) {
+                    $limit_desc .= " branch:" . $library->branchname;
                 } else {
                     $limit_desc .= " $this_limit";
                 }
                 } else {
                     $limit_desc .= " $this_limit";
                 }
@@ -1732,9 +1763,13 @@ 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;
     # 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;
+    #NOTE: We use several several different regexps here as you can't have variable length lookback assertions
     $query =~ s/(?<=(ti|au|pb|su|an|kw|mc|nb|ns)):/=/g;
     $query =~ s/(?<=(wrdl)):/=/g;
     $query =~ s/(?<=(trn|phr)):/=/g;
     $query =~ s/(?<=(ti|au|pb|su|an|kw|mc|nb|ns)):/=/g;
     $query =~ s/(?<=(wrdl)):/=/g;
     $query =~ s/(?<=(trn|phr)):/=/g;
+    $query =~ s/(?<=(st-numeric)):/=/g;
+    $query =~ s/(?<=(st-year)):/=/g;
+    $query =~ s/(?<=(st-date-normalized)):/=/g;
     $limit =~ s/:/=/g;
     for ( $query, $query_desc, $limit, $limit_desc ) {
         s/  +/ /g;    # remove extra spaces
     $limit =~ s/:/=/g;
     for ( $query, $query_desc, $limit, $limit_desc ) {
         s/  +/ /g;    # remove extra spaces
@@ -1760,10 +1795,11 @@ sub buildQuery {
         warn "LIMIT DESC:" . $limit_desc;
         warn "---------\nLeave buildQuery\n---------";
     }
         warn "LIMIT DESC:" . $limit_desc;
         warn "---------\nLeave buildQuery\n---------";
     }
+
     return (
         undef,              $query, $simple_query, $query_cgi,
         $query_desc,        $limit, $limit_cgi,    $limit_desc,
     return (
         undef,              $query, $simple_query, $query_cgi,
         $query_desc,        $limit, $limit_cgi,    $limit_desc,
-        $stopwords_removed, $query_type
+        $query_type
     );
 }
 
     );
 }
 
@@ -1791,9 +1827,9 @@ sub _build_initial_query {
     #e.g. " and kw,wrdl:test"
     $params->{query} .= $operator . $operand;
 
     #e.g. " and kw,wrdl:test"
     $params->{query} .= $operator . $operand;
 
-    $params->{query_cgi} .= "&op=".uri_escape($operator) if $operator;
-    $params->{query_cgi} .= "&idx=".uri_escape($params->{index}) if $params->{index};
-    $params->{query_cgi} .= "&q=".uri_escape($params->{original_operand}) if $params->{original_operand};
+    $params->{query_cgi} .= "&op=".uri_escape_utf8($operator) if $operator;
+    $params->{query_cgi} .= "&idx=".uri_escape_utf8($params->{index}) if $params->{index};
+    $params->{query_cgi} .= "&q=".uri_escape_utf8($params->{original_operand}) if $params->{original_operand};
 
     #e.g. " and kw,wrdl: test"
     $params->{query_desc} .= $operator . $params->{index_plus} . " " . $params->{original_operand};
 
     #e.g. " and kw,wrdl: test"
     $params->{query_desc} .= $operator . $params->{index_plus} . " " . $params->{original_operand};
@@ -1822,52 +1858,37 @@ sub searchResults {
 
     require C4::Items;
 
 
     require C4::Items;
 
-    $search_context = 'opac' if !$search_context || $search_context ne 'intranet';
+    $search_context->{'interface'} = 'opac' if !$search_context->{'interface'} || $search_context->{'interface'} ne 'intranet';
     my ($is_opac, $hidelostitems);
     my ($is_opac, $hidelostitems);
-    if ($search_context eq 'opac') {
+    if ($search_context->{'interface'} eq 'opac') {
         $hidelostitems = C4::Context->preference('hidelostitems');
         $is_opac       = 1;
     }
 
     #Build branchnames hash
         $hidelostitems = C4::Context->preference('hidelostitems');
         $is_opac       = 1;
     }
 
     #Build branchnames hash
-    #find branchname
-    #get branch information.....
-    my %branches;
-    my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
-    $bsth->execute();
-    while ( my $bdata = $bsth->fetchrow_hashref ) {
-        $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
-    }
+    my %branches = map { $_->branchcode => $_->branchname } Koha::Libraries->search({}, { order_by => 'branchname' });
+
 # FIXME - We build an authorised values hash here, using the default framework
 # though it is possible to have different authvals for different fws.
 
 # FIXME - We build an authorised values hash here, using the default framework
 # though it is possible to have different authvals for different fws.
 
-    my $shelflocations =GetKohaAuthorisedValues('items.location','');
+    my $shelflocations =
+      { map { $_->{authorised_value} => $_->{lib} } Koha::AuthorisedValues->get_descriptions_by_koha_field( { frameworkcode => '', kohafield => 'items.location' } ) };
 
     # get notforloan authorised value list (see $shelflocations  FIXME)
 
     # get notforloan authorised value list (see $shelflocations  FIXME)
-    my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
-
-    #Build itemtype hash
-    #find itemtype & itemtype image
-    my %itemtypes;
-    $bsth =
-      $dbh->prepare(
-        "SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes"
-      );
-    $bsth->execute();
-    while ( my $bdata = $bsth->fetchrow_hashref ) {
-               foreach (qw(description imageurl summary notforloan)) {
-               $itemtypes{ $bdata->{'itemtype'} }->{$_} = $bdata->{$_};
-               }
-    }
+    my $av = Koha::MarcSubfieldStructures->search({ frameworkcode => '', kohafield => 'items.notforloan', authorised_value => [ -and => {'!=' => undef }, {'!=' => ''}] });
+    my $notforloan_authorised_value = $av->count ? $av->next->authorised_value : undef;
+
+    #Get itemtype hash
+    my $itemtypes = Koha::ItemTypes->search_with_localization;
+    my %itemtypes = map { $_->{itemtype} => $_ } @{ $itemtypes->unblessed };
 
     #search item field code
     my ($itemtag, undef) = &GetMarcFromKohaField( "items.itemnumber", "" );
 
     ## find column names of items related to MARC
 
     #search item field code
     my ($itemtag, undef) = &GetMarcFromKohaField( "items.itemnumber", "" );
 
     ## find column names of items related to MARC
-    my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
-    $sth2->execute;
     my %subfieldstosearch;
     my %subfieldstosearch;
-    while ( ( my $column ) = $sth2->fetchrow ) {
+    my @columns = Koha::Database->new()->schema()->resultset('Item')->result_source->columns;
+    for my $column ( @columns ) {
         my ( $tagfield, $tagsubfield ) =
           &GetMarcFromKohaField( "items." . $column, "" );
         if ( defined $tagsubfield ) {
         my ( $tagfield, $tagsubfield ) =
           &GetMarcFromKohaField( "items." . $column, "" );
         if ( defined $tagsubfield ) {
@@ -1888,6 +1909,13 @@ sub searchResults {
     # We get the biblionumber position in MARC
     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
 
     # We get the biblionumber position in MARC
     my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
 
+    # set stuff for XSLT processing here once, not later again for every record we retrieved
+    my $interface = $is_opac ? 'OPAC' : '';
+    my $xslsyspref = $interface . "XSLTResultsDisplay";
+    my $xslfile = C4::Context->preference($xslsyspref);
+    my $lang   = $xslfile ? C4::Languages::getlanguage()  : undef;
+    my $sysxml = $xslfile ? C4::XSLT::get_xslt_sysprefs() : undef;
+
     # loop through all of the records we've retrieved
     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
 
     # loop through all of the records we've retrieved
     for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
 
@@ -1913,14 +1941,16 @@ sub searchResults {
              : $bibliotag < 10
                ? GetFrameworkCode($marcrecord->field($bibliotag)->data)
                : GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
              : $bibliotag < 10
                ? GetFrameworkCode($marcrecord->field($bibliotag)->data)
                : GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
-        my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, $fw );
+
+        SetUTF8Flag($marcrecord);
+        my $oldbiblio = TransformMarcToKoha( $marcrecord, $fw );
         $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
         $oldbiblio->{result_number} = $i + 1;
 
         # add imageurl to itemtype if there is one
         $oldbiblio->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
 
         $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
         $oldbiblio->{result_number} = $i + 1;
 
         # add imageurl to itemtype if there is one
         $oldbiblio->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
 
-        $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->{'authorised_value_images'}  = ($search_context->{'interface'} eq 'opac' && C4::Context->preference('AuthorisedValueImages')) || ($search_context->{'interface'} 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);
                $oldbiblio->{normalized_upc}  = GetNormalizedUPC(       $marcrecord,$marcflavour);
                $oldbiblio->{normalized_ean}  = GetNormalizedEAN(       $marcrecord,$marcflavour);
                $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
@@ -1929,7 +1959,7 @@ sub searchResults {
 
                # edition information, if any
         $oldbiblio->{edition} = $oldbiblio->{editionstatement};
 
                # edition information, if any
         $oldbiblio->{edition} = $oldbiblio->{editionstatement};
-               $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
+        $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{translated_description};
  # Build summary if there is one (the summary is defined in the itemtypes table)
  # FIXME: is this used anywhere, I think it can be commented out? -- JF
         if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
  # Build summary if there is one (the summary is defined in the itemtypes table)
  # FIXME: is this used anywhere, I think it can be commented out? -- JF
         if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
@@ -1963,12 +1993,7 @@ sub searchResults {
                         if($marcrecord->field($1)){
                             my @repl = $marcrecord->field($1)->subfield($2);
                             my $subfieldvalue = $repl[$i];
                         if($marcrecord->field($1)){
                             my @repl = $marcrecord->field($1)->subfield($2);
                             my $subfieldvalue = $repl[$i];
-
-                            if (! utf8::is_utf8($subfieldvalue)) {
-                                utf8::decode($subfieldvalue);
-                            }
-
-                             $newline =~ s/\[$tag\]/$subfieldvalue/g;
+                            $newline =~ s/\[$tag\]/$subfieldvalue/g;
                         }
                     }
                     $newsummary .= "$newline\n";
                         }
                     }
                     $newsummary .= "$newline\n";
@@ -1983,27 +2008,31 @@ sub searchResults {
         # Pull out the items fields
         my @fields = $marcrecord->field($itemtag);
         my $marcflavor = C4::Context->preference("marcflavour");
         # Pull out the items fields
         my @fields = $marcrecord->field($itemtag);
         my $marcflavor = C4::Context->preference("marcflavour");
+
         # adding linked items that belong to host records
         # 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);}
+        if ( C4::Context->preference('EasyAnalyticalRecords') ) {
+            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 ) {
+                    my $hostbiblio = GetMarcBiblio({
+                        biblionumber => $hostbiblionumber,
+                        embed_items  => 1 });
+                    my ($itemfield, undef) = GetMarcFromKohaField( 'items.itemnumber', GetFrameworkCode($hostbiblionumber) );
+                    if( $hostbiblio ) {
+                        my @hostitems = $hostbiblio->field($itemfield);
+                        foreach my $hostitem (@hostitems){
+                            if ($hostitem->subfield("9") eq $linkeditemnumber){
+                                my $linkeditem =$hostitem;
+                                # append linked items if they exist
+                                push @fields, $linkeditem if $linkeditem;
+                            }
                         }
                     }
                 }
                         }
                     }
                 }
@@ -2046,7 +2075,7 @@ sub searchResults {
             foreach my $code ( keys %subfieldstosearch ) {
                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
             }
             foreach my $code ( keys %subfieldstosearch ) {
                 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
             }
-            $item->{description} = $itemtypes{ $item->{itype} }{description};
+            $item->{description} = $itemtypes{ $item->{itype} }{translated_description};
 
                # OPAC hidden items
             if ($is_opac) {
 
                # OPAC hidden items
             if ($is_opac) {
@@ -2056,7 +2085,7 @@ sub searchResults {
                     next;
                 }
                 # hidden based on OpacHiddenItems syspref
                     next;
                 }
                 # hidden based on OpacHiddenItems syspref
-                my @hi = C4::Items::GetHiddenItemnumbers($item);
+                my @hi = C4::Items::GetHiddenItemnumbers({ items=> [ $item ], borcat => $search_context->{category} });
                 if (scalar @hi) {
                     push @hiddenitems, @hi;
                     $hideatopac_count++;
                 if (scalar @hi) {
                     push @hiddenitems, @hi;
                     $hideatopac_count++;
@@ -2078,21 +2107,28 @@ 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
             my $userenv = C4::Context->userenv;
                        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
             my $userenv = C4::Context->userenv;
-            if ( $item->{onloan} && !(C4::Members::GetHideLostItemsPreference($userenv->{'number'}) && $item->{itemlost}) ) {
+            if ( $item->{onloan}
+                && $userenv
+                && $userenv->{number}
+                && !( Koha::Patrons->find($userenv->{number})->category->hidelostitems && $item->{itemlost} ) )
+            {
                 $onloan_count++;
                 $onloan_count++;
-                               my $key = $prefix . $item->{onloan} . $item->{barcode};
-                               $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
-                               $onloan_items->{$key}->{count}++ if $item->{$hbranch};
-                               $onloan_items->{$key}->{branchname} = $item->{branchname};
-                               $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
-                               $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
-                               $onloan_items->{$key}->{description} = $item->{description};
-                               $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
+                my $key = $prefix . $item->{onloan} . $item->{barcode};
+                $onloan_items->{$key}->{due_date} = $item->{onloan};
+                $onloan_items->{$key}->{count}++ if $item->{$hbranch};
+                $onloan_items->{$key}->{branchname}     = $item->{branchname};
+                $onloan_items->{$key}->{location}       = $shelflocations->{ $item->{location} };
+                $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
+                $onloan_items->{$key}->{description}    = $item->{description};
+                $onloan_items->{$key}->{imageurl} =
+                  getitemtypeimagelocation( $search_context->{'interface'}, $itemtypes{ $item->{itype} }->{imageurl} );
+
                 # if something's checked out and lost, mark it as 'long overdue'
                 if ( $item->{itemlost} ) {
                 # if something's checked out and lost, mark it as 'long overdue'
                 if ( $item->{itemlost} ) {
-                    $onloan_items->{$prefix}->{longoverdue}++;
+                    $onloan_items->{$key}->{longoverdue}++;
                     $longoverdue_count++;
                     $longoverdue_count++;
-                } else {       # can place holds as long as item isn't lost
+                }
+                else {    # can place holds as long as item isn't lost
                     $can_place_holds = 1;
                 }
             }
                     $can_place_holds = 1;
                 }
             }
@@ -2100,6 +2136,8 @@ sub searchResults {
          # items not on loan, but still unavailable ( lost, withdrawn, damaged )
             else {
 
          # items not on loan, but still unavailable ( lost, withdrawn, damaged )
             else {
 
+                $item->{notforloan}=1 if !$item->{notforloan}  && $itemtypes{ C4::Context->preference("item-level_itypes")? $item->{itype}: $oldbiblio->{itemtype} }->{notforloan};
+
                 # item is on order
                 if ( $item->{notforloan} < 0 ) {
                     $ordered_count++;
                 # item is on order
                 if ( $item->{notforloan} < 0 ) {
                     $ordered_count++;
@@ -2118,7 +2156,8 @@ sub searchResults {
                         || $item->{itemlost}
                         || $item->{damaged}
                         || $item->{notforloan}
                         || $item->{itemlost}
                         || $item->{damaged}
                         || $item->{notforloan}
-                        || $items_count > 20) {
+                        || ( C4::Context->preference('MaxSearchResultsItemsPerRecordStatusCheck')
+                        && $items_count > C4::Context->preference('MaxSearchResultsItemsPerRecordStatusCheck') ) ) {
 
                     # A couple heuristics to limit how many times
                     # we query the database for item transfer information, sacrificing
 
                     # A couple heuristics to limit how many times
                     # we query the database for item transfer information, sacrificing
@@ -2133,7 +2172,7 @@ sub searchResults {
                     #        should map transit status to record indexed in Zebra.
                     #
                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
                     #        should map transit status to record indexed in Zebra.
                     #
                     ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
-                    $reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber}, $oldbiblio->{biblionumber} );
+                    $reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber} );
                 }
 
                 # item is withdrawn, lost, damaged, not for loan, reserved or in transit
                 }
 
                 # item is withdrawn, lost, damaged, not for loan, reserved or in transit
@@ -2172,21 +2211,21 @@ sub searchResults {
                     $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 and $item->{notforloan};
                     $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 and $item->{notforloan};
-                                       $other_items->{$key}->{count}++ if $item->{$hbranch};
-                                       $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
-                                       $other_items->{$key}->{description} = $item->{description};
-                                       $other_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
+                    $other_items->{$key}->{count}++ if $item->{$hbranch};
+                    $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
+                    $other_items->{$key}->{description} = $item->{description};
+                    $other_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context->{'interface'}, $itemtypes{ $item->{itype} }->{imageurl} );
                 }
                 # item is available
                 else {
                     $can_place_holds = 1;
                     $available_count++;
                 }
                 # item is available
                 else {
                     $can_place_holds = 1;
                     $available_count++;
-                                       $available_items->{$prefix}->{count}++ if $item->{$hbranch};
-                                       foreach (qw(branchname itemcallnumber description)) {
-                       $available_items->{$prefix}->{$_} = $item->{$_};
-                                       }
-                                       $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
-                                       $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
+                    $available_items->{$prefix}->{count}++ if $item->{$hbranch};
+                    foreach (qw(branchname itemcallnumber description)) {
+                        $available_items->{$prefix}->{$_} = $item->{$_};
+                    }
+                    $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
+                    $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( $search_context->{'interface'}, $itemtypes{ $item->{itype} }->{imageurl} );
                 }
             }
         }    # notforloan, item level and biblioitem level
                 }
             }
         }    # notforloan, item level and biblioitem level
@@ -2211,12 +2250,9 @@ sub searchResults {
         }
 
         # XSLT processing of some stuff
         }
 
         # XSLT processing of some stuff
-        SetUTF8Flag($marcrecord);
-        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
+        # we fetched the sysprefs already before the loop through all retrieved record!
+        if (!$scan && $xslfile) {
+            $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display($oldbiblio->{biblionumber}, $marcrecord, $xslsyspref, 1, \@hiddenitems, $sysxml, $xslfile, $lang);
         }
 
         # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
         }
 
         # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
@@ -2226,7 +2262,6 @@ sub searchResults {
             }
         }
         $oldbiblio->{norequests} = 1 unless $can_place_holds;
             }
         }
         $oldbiblio->{norequests} = 1 unless $can_place_holds;
-        $oldbiblio->{itemsplural}          = 1 if $items_count > 1;
         $oldbiblio->{items_count}          = $items_count;
         $oldbiblio->{available_items_loop} = \@available_items_loop;
         $oldbiblio->{onloan_items_loop}    = \@onloan_items_loop;
         $oldbiblio->{items_count}          = $items_count;
         $oldbiblio->{available_items_loop} = \@available_items_loop;
         $oldbiblio->{onloan_items_loop}    = \@onloan_items_loop;
@@ -2279,95 +2314,6 @@ sub searchResults {
     return @newresults;
 }
 
     return @newresults;
 }
 
-=head2 SearchAcquisitions
-    Search for acquisitions
-=cut
-
-sub SearchAcquisitions{
-    my ($datebegin, $dateend, $itemtypes,$criteria, $orderby) = @_;
-
-    my $dbh=C4::Context->dbh;
-    # Variable initialization
-    my $str=qq|
-    SELECT marcxml
-    FROM biblio
-    LEFT JOIN biblioitems ON biblioitems.biblionumber=biblio.biblionumber
-    LEFT JOIN items ON items.biblionumber=biblio.biblionumber
-    WHERE dateaccessioned BETWEEN ? AND ?
-    |;
-
-    my (@params,@loopcriteria);
-
-    push @params, $datebegin->output("iso");
-    push @params, $dateend->output("iso");
-
-    if (scalar(@$itemtypes)>0 and $criteria ne "itemtype" ){
-        if(C4::Context->preference("item-level_itypes")){
-            $str .= "AND items.itype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
-        }else{
-            $str .= "AND biblioitems.itemtype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
-        }
-        push @params, @$itemtypes;
-    }
-
-    if ($criteria =~/itemtype/){
-        if(C4::Context->preference("item-level_itypes")){
-            $str .= "AND items.itype=? ";
-        }else{
-            $str .= "AND biblioitems.itemtype=? ";
-        }
-
-        if(scalar(@$itemtypes) == 0){
-            my $itypes = GetItemTypes();
-            for my $key (keys %$itypes){
-                push @$itemtypes, $key;
-            }
-        }
-
-        @loopcriteria= @$itemtypes;
-    }elsif ($criteria=~/itemcallnumber/){
-        $str .= "AND (items.itemcallnumber LIKE CONCAT(?,'%')
-                 OR items.itemcallnumber is NULL
-                 OR items.itemcallnumber = '')";
-
-        @loopcriteria = ("AA".."ZZ", "") unless (scalar(@loopcriteria)>0);
-    }else {
-        $str .= "AND biblio.title LIKE CONCAT(?,'%') ";
-        @loopcriteria = ("A".."z") unless (scalar(@loopcriteria)>0);
-    }
-
-    if ($orderby =~ /date_desc/){
-        $str.=" ORDER BY dateaccessioned DESC";
-    } else {
-        $str.=" ORDER BY title";
-    }
-
-    my $qdataacquisitions=$dbh->prepare($str);
-
-    my @loopacquisitions;
-    foreach my $value(@loopcriteria){
-        push @params,$value;
-        my %cell;
-        $cell{"title"}=$value;
-        $cell{"titlecode"}=$value;
-
-        eval{$qdataacquisitions->execute(@params);};
-
-        if ($@){ warn "recentacquisitions Error :$@";}
-        else {
-            my @loopdata;
-            while (my $data=$qdataacquisitions->fetchrow_hashref){
-                push @loopdata, {"summary"=>GetBiblioSummary( $data->{'marcxml'} ) };
-            }
-            $cell{"loopdata"}=\@loopdata;
-        }
-        push @loopacquisitions,\%cell if (scalar(@{$cell{loopdata}})>0);
-        pop @params;
-    }
-    $qdataacquisitions->finish;
-    return \@loopacquisitions;
-}
-
 =head2 enabled_staff_search_views
 
 %hash = enabled_staff_search_views()
 =head2 enabled_staff_search_views
 
 %hash = enabled_staff_search_views()
@@ -2401,13 +2347,6 @@ sub enabled_staff_search_views
        );
 }
 
        );
 }
 
-sub PurgeSearchHistory{
-    my ($pSearchhistory)=@_;
-    my $dbh = C4::Context->dbh;
-    my $sth = $dbh->prepare("DELETE FROM search_history WHERE time < DATE_SUB( NOW(), INTERVAL ? DAY )");
-    $sth->execute($pSearchhistory) or die $dbh->errstr;
-}
-
 =head2 z3950_search_args
 
 $arrayref = z3950_search_args($matchpoints)
 =head2 z3950_search_args
 
 $arrayref = z3950_search_args($matchpoints)
@@ -2558,7 +2497,7 @@ sub _ZOOM_event_loop {
 
 =head2 new_record_from_zebra
 
 
 =head2 new_record_from_zebra
 
-Given raw data from a Zebra result set, return a MARC::Record object
+Given raw data from a searchengine result set, return a MARC::Record object
 
 This helper function is needed to take into account all the involved
 system preferences and configuration variables to properly create the
 
 This helper function is needed to take into account all the involved
 system preferences and configuration variables to properly create the
@@ -2567,6 +2506,9 @@ MARC::Record object.
 If we are using GRS-1, then the raw data we get from Zebra should be USMARC
 data. If we are using DOM, then it has to be MARCXML.
 
 If we are using GRS-1, then the raw data we get from Zebra should be USMARC
 data. If we are using DOM, then it has to be MARCXML.
 
+If we are using elasticsearch, it'll already be a MARC::Record and this
+function needs a new name.
+
 =cut
 
 sub new_record_from_zebra {
 =cut
 
 sub new_record_from_zebra {
@@ -2574,8 +2516,12 @@ sub new_record_from_zebra {
     my $server   = shift;
     my $raw_data = shift;
     # Set the default indexing modes
     my $server   = shift;
     my $raw_data = shift;
     # Set the default indexing modes
+    my $search_engine = C4::Context->preference("SearchEngine");
+    if ($search_engine eq 'Elasticsearch') {
+        return ref $raw_data eq 'MARC::Record' ? $raw_data : MARC::Record->new_from_xml( $raw_data, 'UTF-8' );
+    }
     my $index_mode = ( $server eq 'biblioserver' )
     my $index_mode = ( $server eq 'biblioserver' )
-                        ? C4::Context->config('zebra_bib_index_mode') // 'grs1'
+                        ? C4::Context->config('zebra_bib_index_mode') // 'dom'
                         : C4::Context->config('zebra_auth_index_mode') // 'dom';
 
     my $marc_record =  eval {
                         : C4::Context->config('zebra_auth_index_mode') // 'dom';
 
     my $marc_record =  eval {