# 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 C4::Context;
use C4::Biblio; # GetMarcFromKohaField, GetBiblioData
use C4::Koha; # getFacets
+use Koha::DateUtils;
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::Libraries;
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);
# set the version for version checking
&buildQuery
&GetDistinctValues
&enabled_staff_search_views
- &PurgeSearchHistory
);
# make all your functions, whether exported or not;
* $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.
{
$facet_label_value =
$itemtypes->{$one_facet}
- ->{'description'};
+ ->{translated_description};
}
}
$facets_info->{$link_value}->{'label_value'} =~
/Libraries/
)
- and ( C4::Context->preference('singleBranchMode') )
+ and ( Koha::Libraries->search->count == 1 )
);
}
}
my $rs = shift;
my $facets;
- my $index_mode = C4::Context->config('zebra_auth_index_mode') // 'dom';
+ my $indexing_mode = C4::Context->config('zebra_bib_index_mode') // 'dom';
+ my $use_zebra_facets = C4::Context->config('use_zebra_facets') // 0;
- if ( $index_mode eq 'dom' ) {
+ if ( $indexing_mode eq 'dom' &&
+ $use_zebra_facets ) {
$facets = _get_facets_from_zebra( $rs );
} else {
$facets = _get_facets_from_records( $rs );
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
- 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
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 ) = @_;
'date-entered-on-file',
'Date-of-acquisition',
'Date-of-publication',
+ 'Date-time-last-modified',
'Dewey-classification',
'Dissertation-information',
+ 'diss',
+ 'dtlm',
'EAN',
'extent',
'fic',
'popularity',
'pubdate',
'Publisher',
+ 'Provider',
+ 'pv',
'Record-control-number',
'rcn',
'Record-type',
$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.
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 $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=)/ ) {
if ( @limits ) {
$q .= ' and '.join(' and ', @limits);
}
- return ( undef, $q, $q, "q=ccl=".uri_escape($q), $q, '', '', '', '', 'ccl' );
+ return ( undef, $q, $q, "q=ccl=".uri_escape_utf8($q), $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
# return (
# undef, $query, $simple_query, $query_cgi,
# $query, $limit, $limit_cgi, $limit_desc,
-# $stopwords_removed, 'ccl'
+# 'ccl'
# );
# }
# 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
}
my $index = $indexes[$i];
# Add index-specific attributes
+
+ #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.
+
# Date of Publication
- if ( $index eq 'yr' ) {
- $index .= ",st-numeric";
- $indexes_set++;
- $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
+ if ( $index =~ /yr/ ) {
+ #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
- 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
elsif ( $index eq 'nb' || $index eq 'ns' ) {
(
$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){
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 ( $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";
}
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);
# 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/(?<=(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
return (
undef, $query, $simple_query, $query_cgi,
$query_desc, $limit, $limit_cgi, $limit_desc,
- $stopwords_removed, $query_type
+ $query_type
);
}
#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};
# 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->{$_};
- }
- }
+ #Get itemtype hash
+ my %itemtypes = %{ GetItemTypes() };
#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 ) {
: $bibliotag < 10
? GetFrameworkCode($marcrecord->field($bibliotag)->data)
: GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
+
+ SetUTF8Flag($marcrecord);
my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, $fw );
$oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
$oldbiblio->{result_number} = $i + 1;
# 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} ) {
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";
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) {
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}
+ && !( C4::Members::GetHideLostItemsPreference( $userenv->{'number'} ) && $item->{itemlost} ) )
+ {
$onloan_count++;
- my $key = $prefix . $item->{onloan} . $item->{barcode};
- $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
- $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} = output_pref( { dt => dt_from_string( $item->{onloan} ), dateonly => 1 } );
+ $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} );
+
# 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;
}
}
# 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->{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
# 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
}
# 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);
);
}
-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)
my $raw_data = shift;
# Set the default indexing modes
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 {