Bug 21206: (QA follow-up) Rebase problem and leftover mocked GetItem
[koha.git] / C4 / Search.pm
index 1326f69..ef81e19 100644 (file)
@@ -2,18 +2,18 @@ package C4::Search;
 
 # 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
@@ -21,27 +21,27 @@ require Exporter;
 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 C4::Dates qw(format_date);
-use C4::Members qw(GetHideLostItemsPreference);
 use C4::XSLT;
-use C4::Branch;
 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 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 {
-    $VERSION = 3.07.00.049;
     $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
-  &PurgeSearchHistory
 );
 
 # 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;
-    my $result = TransformMarcToKoha( $dbh, $record, '' );
+    my $result = TransformMarcToKoha( $record, '' );
     my $sth;
     my $query;
     my $search;
@@ -148,7 +147,7 @@ sub FindDuplicate {
                 $possible_duplicate_record
             );
 
-            my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
+            my $result = TransformMarcToKoha( $marcrecord, '' );
 
             # FIXME :: why 2 $biblionumber ?
             if ($result) {
@@ -162,7 +161,7 @@ sub FindDuplicate {
 
 =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
 
@@ -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
-    * $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.
+    * %options is optional. (e.g. "skip_normalize" allows you to skip changing : to = )
 
 
 =item C<Return:>
@@ -204,7 +204,7 @@ my @results;
 
 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, {
@@ -223,7 +223,7 @@ $template->param(result=>\@results);
 =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.
@@ -245,12 +245,12 @@ sub SimpleSearch {
         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 {
-                $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] );
@@ -332,6 +332,7 @@ sub getRecords {
 
     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;
@@ -339,6 +340,9 @@ sub getRecords {
     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    = {};
@@ -572,15 +576,22 @@ sub getRecords {
                                     {
                                         $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/ ) {
-                                    $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.
@@ -620,12 +631,21 @@ sub getRecords {
                                 $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 );
 }
 
@@ -634,11 +654,9 @@ sub GetFacets {
     my $rs = shift;
     my $facets;
 
-    my $indexing_mode    = C4::Context->config('zebra_bib_index_mode') // 'dom';
     my $use_zebra_facets = C4::Context->config('use_zebra_facets') // 0;
 
-    if ( $indexing_mode eq 'dom' &&
-         $use_zebra_facets ) {
+    if ( $use_zebra_facets ) {
         $facets = _get_facets_from_zebra( $rs );
     } else {
         $facets = _get_facets_from_records( $rs );
@@ -846,6 +864,7 @@ sub pazGetRecords {
         $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();
@@ -913,32 +932,6 @@ sub pazGetRecords {
     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 ) = @_;
@@ -975,6 +968,9 @@ sub _build_stemmed_operand {
     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
@@ -1049,6 +1045,11 @@ sub _build_weighted_query {
         $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
@@ -1088,6 +1089,8 @@ sub getIndexes{
                     'an',
                     'Any',
                     'at',
+                    'arl',
+                    'arp',
                     'au',
                     'aub',
                     'aud',
@@ -1132,8 +1135,11 @@ sub getIndexes{
                     'date-entered-on-file',
                     'Date-of-acquisition',
                     'Date-of-publication',
+                    'Date-time-last-modified',
                     'Dewey-classification',
                     'Dissertation-information',
+                    'diss',
+                    'dtlm',
                     'EAN',
                     'extent',
                     'fic',
@@ -1148,9 +1154,13 @@ sub getIndexes{
                     'Heading-use-subject-added-entry',
                     'Host-item',
                     'id-other',
+                    'ident',
+                    'Identifier-standard',
                     'Illustration-code',
                     'Index-term-genre',
                     'Index-term-uncontrolled',
+                    'Interest-age-level',
+                    'Interest-grade-level',
                     'ISBN',
                     'isbn',
                     'ISSN',
@@ -1165,6 +1175,7 @@ sub getIndexes{
                     'LC-card-number',
                     'lcn',
                     'lex',
+                    'lexile-number',
                     'llength',
                     'ln',
                     'ln-audio',
@@ -1188,6 +1199,7 @@ sub getIndexes{
                     'notes',
                     'ns',
                     'nt',
+                    'Other-control-number',
                     'pb',
                     'Personal-name',
                     'Personal-name-heading',
@@ -1199,6 +1211,9 @@ sub getIndexes{
                     'popularity',
                     'pubdate',
                     'Publisher',
+                    'Provider',
+                    'pv',
+                    'Reading-grade-level',
                     'Record-control-number',
                     'rcn',
                     'Record-type',
@@ -1330,12 +1345,13 @@ sub _handle_exploding_index {
 
     ( $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.
+
 =cut
 
 sub parseQuery {
@@ -1415,10 +1431,10 @@ sub parseQuery {
 $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,
-handle truncation, stemming, field weighting, stopwords, fuzziness, etc.
+handle truncation, stemming, field weighting, fuzziness, etc.
 
 See verbose embedded documentation.
 
@@ -1444,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 $remove_stopwords = C4::Context->preference("QueryRemoveStopwords") || 0;
 
     my $query        = $operands[0];
     my $simple_query = $operands[0];
@@ -1457,8 +1472,6 @@ sub buildQuery {
     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=)/ ) {
@@ -1476,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;
+        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 ) {
-            $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=/ ) {
-        return ( undef, $', $', "q=cql=".uri_escape($'), $', '', '', '', '', 'cql' );
+        return ( undef, $', $', "q=cql=".uri_escape_utf8($'), $', '', '', '', 'cql' );
     }
     if ( $query =~ /^pqf=/ ) {
         if ($query_desc) {
-            $query_cgi = "q=".uri_escape($query_desc);
+            $query_cgi = "q=".uri_escape_utf8($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
@@ -1502,7 +1524,7 @@ sub buildQuery {
 #        return (
 #            undef,              $query, $simple_query, $query_cgi,
 #            $query,             $limit, $limit_cgi,    $limit_desc,
-#            $stopwords_removed, 'ccl'
+#            'ccl'
 #        );
 #    }
 
@@ -1520,17 +1542,16 @@ sub buildQuery {
         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;
 
-# 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;
-                    $remove_stopwords = 0;
                 } else {
                     $operands[$i] =~ s/\?/{?}/g; # need to escape question marks
                 }
@@ -1544,12 +1565,12 @@ sub buildQuery {
                 #which is processed higher up in this sub. Other than that, year searches are typically
                 #handled as limits which are not processed her either.
 
-                # Date of Publication
-                if ( $index =~ /yr/ ) {
+                # 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 = $remove_stopwords = 0;
+                    $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = 0;
                 }
 
                 # Date of Acquisition
@@ -1559,17 +1580,22 @@ sub buildQuery {
                       #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
-                    #remove_stopwords doesn't function anymore so is irrelevant
-                                       $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
+                    $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = 0;
                 }
                 # 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,
-                        $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){
@@ -1586,15 +1612,6 @@ sub buildQuery {
                 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
@@ -1670,7 +1687,7 @@ sub buildQuery {
                     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,
@@ -1705,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;
-                $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";
-            $limit_cgi       .= "&limit=" . uri_escape($this_limit);
+            $limit_cgi       .= "&limit=" . uri_escape_utf8($this_limit);
             $limit_desc      .= " $this_limit";
         }
 
@@ -1719,12 +1736,12 @@ sub buildQuery {
         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;
-                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";
                 }
@@ -1778,10 +1795,11 @@ sub buildQuery {
         warn "LIMIT DESC:" . $limit_desc;
         warn "---------\nLeave buildQuery\n---------";
     }
+
     return (
         undef,              $query, $simple_query, $query_cgi,
         $query_desc,        $limit, $limit_cgi,    $limit_desc,
-        $stopwords_removed, $query_type
+        $query_type
     );
 }
 
@@ -1809,9 +1827,9 @@ sub _build_initial_query {
     #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};
@@ -1840,52 +1858,37 @@ sub searchResults {
 
     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);
-    if ($search_context eq 'opac') {
+    if ($search_context->{'interface'} eq 'opac') {
         $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.
 
-    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)
-    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
-    my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
-    $sth2->execute;
     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 ) {
@@ -1906,6 +1909,13 @@ sub searchResults {
     # 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++ ) {
 
@@ -1931,14 +1941,16 @@ sub searchResults {
              : $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->{'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);
@@ -1947,7 +1959,7 @@ sub searchResults {
 
                # 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} ) {
@@ -1981,12 +1993,7 @@ sub searchResults {
                         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";
@@ -2001,27 +2008,31 @@ 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);}
+        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;
+                            }
                         }
                     }
                 }
@@ -2064,7 +2075,7 @@ sub searchResults {
             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) {
@@ -2074,7 +2085,7 @@ sub searchResults {
                     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++;
@@ -2096,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;
-            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++;
-                               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} ) {
-                    $onloan_items->{$prefix}->{longoverdue}++;
+                    $onloan_items->{$key}->{longoverdue}++;
                     $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;
                 }
             }
@@ -2118,6 +2136,8 @@ sub searchResults {
          # 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++;
@@ -2136,7 +2156,8 @@ sub searchResults {
                         || $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
@@ -2151,7 +2172,7 @@ sub searchResults {
                     #        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
@@ -2190,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}->{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++;
-                                       $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
@@ -2229,12 +2250,9 @@ sub searchResults {
         }
 
         # 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
@@ -2244,7 +2262,6 @@ sub searchResults {
             }
         }
         $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;
@@ -2297,95 +2314,6 @@ sub searchResults {
     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()
@@ -2419,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)
@@ -2576,7 +2497,7 @@ sub _ZOOM_event_loop {
 
 =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
@@ -2585,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 elasticsearch, it'll already be a MARC::Record and this
+function needs a new name.
+
 =cut
 
 sub new_record_from_zebra {
@@ -2592,6 +2516,10 @@ sub new_record_from_zebra {
     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' )
                         ? C4::Context->config('zebra_bib_index_mode') // 'dom'
                         : C4::Context->config('zebra_auth_index_mode') // 'dom';