Bug 9032: add ability to accept list share invitations and remove shares
[koha.git] / C4 / Search.pm
blob2c63f9c7ec15524c8b826acece42e836d088e4a9
1 package C4::Search;
3 # This file is part of Koha.
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along with
15 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
16 # Suite 330, Boston, MA 02111-1307 USA
18 use strict;
19 #use warnings; FIXME - Bug 2505
20 require Exporter;
21 use C4::Context;
22 use C4::Biblio; # GetMarcFromKohaField, GetBiblioData
23 use C4::Koha; # getFacets
24 use Lingua::Stem;
25 use C4::Search::PazPar2;
26 use XML::Simple;
27 use C4::Dates qw(format_date);
28 use C4::Members qw(GetHideLostItemsPreference);
29 use C4::XSLT;
30 use C4::Branch;
31 use C4::Reserves; # GetReserveStatus
32 use C4::Debug;
33 use C4::Charset;
34 use YAML;
35 use URI::Escape;
36 use Business::ISBN;
37 use MARC::Record;
38 use MARC::Field;
39 use utf8;
40 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
42 # set the version for version checking
43 BEGIN {
44 $VERSION = 3.07.00.049;
45 $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
48 =head1 NAME
50 C4::Search - Functions for searching the Koha catalog.
52 =head1 SYNOPSIS
54 See opac/opac-search.pl or catalogue/search.pl for example of usage
56 =head1 DESCRIPTION
58 This module provides searching functions for Koha's bibliographic databases
60 =head1 FUNCTIONS
62 =cut
64 @ISA = qw(Exporter);
65 @EXPORT = qw(
66 &FindDuplicate
67 &SimpleSearch
68 &searchResults
69 &getRecords
70 &buildQuery
71 &AddSearchHistory
72 &GetDistinctValues
73 &enabled_staff_search_views
74 &PurgeSearchHistory
77 # make all your functions, whether exported or not;
79 =head2 FindDuplicate
81 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
83 This function attempts to find duplicate records using a hard-coded, fairly simplistic algorithm
85 =cut
87 sub FindDuplicate {
88 my ($record) = @_;
89 my $dbh = C4::Context->dbh;
90 my $result = TransformMarcToKoha( $dbh, $record, '' );
91 my $sth;
92 my $query;
93 my $search;
94 my $type;
95 my ( $biblionumber, $title );
97 # search duplicate on ISBN, easy and fast..
98 # ... normalize first
99 if ( $result->{isbn} ) {
100 $result->{isbn} =~ s/\(.*$//;
101 $result->{isbn} =~ s/\s+$//;
102 $query = "isbn:$result->{isbn}";
104 else {
105 my $QParser;
106 $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
107 my $titleindex;
108 my $authorindex;
109 my $op;
111 if ($QParser) {
112 $titleindex = 'title|exact';
113 $authorindex = 'author|exact';
114 $op = '&&';
115 $QParser->custom_data->{'QueryAutoTruncate'} = C4::Context->preference('QueryAutoTruncate');
116 } else {
117 $titleindex = 'ti,ext';
118 $authorindex = 'au,ext';
119 $op = 'and';
122 $result->{title} =~ s /\\//g;
123 $result->{title} =~ s /\"//g;
124 $result->{title} =~ s /\(//g;
125 $result->{title} =~ s /\)//g;
127 # FIXME: instead of removing operators, could just do
128 # quotes around the value
129 $result->{title} =~ s/(and|or|not)//g;
130 $query = "$titleindex:\"$result->{title}\"";
131 if ( $result->{author} ) {
132 $result->{author} =~ s /\\//g;
133 $result->{author} =~ s /\"//g;
134 $result->{author} =~ s /\(//g;
135 $result->{author} =~ s /\)//g;
137 # remove valid operators
138 $result->{author} =~ s/(and|or|not)//g;
139 $query .= " $op $authorindex:\"$result->{author}\"";
143 my ( $error, $searchresults, undef ) = SimpleSearch($query); # FIXME :: hardcoded !
144 my @results;
145 if (!defined $error) {
146 foreach my $possible_duplicate_record (@{$searchresults}) {
147 my $marcrecord = new_record_from_zebra(
148 'biblioserver',
149 $possible_duplicate_record
152 my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
154 # FIXME :: why 2 $biblionumber ?
155 if ($result) {
156 push @results, $result->{'biblionumber'};
157 push @results, $result->{'title'};
161 return @results;
164 =head2 SimpleSearch
166 ( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers] );
168 This function provides a simple search API on the bibliographic catalog
170 =over 2
172 =item C<input arg:>
174 * $query can be a simple keyword or a complete CCL query
175 * @servers is optional. Defaults to biblioserver as found in koha-conf.xml
176 * $offset - If present, represents the number of records at the beggining to omit. Defaults to 0
177 * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
180 =item C<Return:>
182 Returns an array consisting of three elements
183 * $error is undefined unless an error is detected
184 * $results is a reference to an array of records.
185 * $total_hits is the number of hits that would have been returned with no limit
187 If an error is returned the two other return elements are undefined. If error itself is undefined
188 the other two elements are always defined
190 =item C<usage in the script:>
192 =back
194 my ( $error, $marcresults, $total_hits ) = SimpleSearch($query);
196 if (defined $error) {
197 $template->param(query_error => $error);
198 warn "error: ".$error;
199 output_html_with_http_headers $input, $cookie, $template->output;
200 exit;
203 my $hits = @{$marcresults};
204 my @results;
206 for my $r ( @{$marcresults} ) {
207 my $marcrecord = MARC::File::USMARC::decode($r);
208 my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,q{});
210 #build the iarray of hashs for the template.
211 push @results, {
212 title => $biblio->{'title'},
213 subtitle => $biblio->{'subtitle'},
214 biblionumber => $biblio->{'biblionumber'},
215 author => $biblio->{'author'},
216 publishercode => $biblio->{'publishercode'},
217 publicationyear => $biblio->{'publicationyear'},
222 $template->param(result=>\@results);
224 =cut
226 sub SimpleSearch {
227 my ( $query, $offset, $max_results, $servers ) = @_;
229 return ( 'No query entered', undef, undef ) unless $query;
230 # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
231 my @servers = defined ( $servers ) ? @$servers : ( 'biblioserver' );
232 my @zoom_queries;
233 my @tmpresults;
234 my @zconns;
235 my $results = [];
236 my $total_hits = 0;
238 my $QParser;
239 $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser') && ! ($query =~ m/\w,\w|\w=\w/));
240 if ($QParser) {
241 $QParser->custom_data->{'QueryAutoTruncate'} = C4::Context->preference('QueryAutoTruncate');
244 # Initialize & Search Zebra
245 for ( my $i = 0 ; $i < @servers ; $i++ ) {
246 eval {
247 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
248 if ($QParser) {
249 $query =~ s/=/:/g;
250 $QParser->parse( $query );
251 $query = $QParser->target_syntax($servers[$i]);
252 $zoom_queries[$i] = new ZOOM::Query::PQF( $query, $zconns[$i]);
253 } else {
254 $query =~ s/:/=/g;
255 $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
257 $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
259 # error handling
260 my $error =
261 $zconns[$i]->errmsg() . " ("
262 . $zconns[$i]->errcode() . ") "
263 . $zconns[$i]->addinfo() . " "
264 . $zconns[$i]->diagset();
266 return ( $error, undef, undef ) if $zconns[$i]->errcode();
268 if ($@) {
270 # caught a ZOOM::Exception
271 my $error =
272 $@->message() . " ("
273 . $@->code() . ") "
274 . $@->addinfo() . " "
275 . $@->diagset();
276 warn $error." for query: $query";
277 return ( $error, undef, undef );
281 _ZOOM_event_loop(
282 \@zconns,
283 \@tmpresults,
284 sub {
285 my ($i, $size) = @_;
286 my $first_record = defined($offset) ? $offset + 1 : 1;
287 my $hits = $tmpresults[ $i - 1 ]->size();
288 $total_hits += $hits;
289 my $last_record = $hits;
290 if ( defined $max_results && $offset + $max_results < $hits ) {
291 $last_record = $offset + $max_results;
294 for my $j ( $first_record .. $last_record ) {
295 my $record = eval {
296 $tmpresults[ $i - 1 ]->record( $j - 1 )->raw()
297 ; # 0 indexed
299 push @{$results}, $record if defined $record;
304 foreach my $zoom_query (@zoom_queries) {
305 $zoom_query->destroy();
308 return ( undef, $results, $total_hits );
311 =head2 getRecords
313 ( undef, $results_hashref, \@facets_loop ) = getRecords (
315 $koha_query, $simple_query, $sort_by_ref, $servers_ref,
316 $results_per_page, $offset, $expanded_facet, $branches,$itemtypes,
317 $query_type, $scan
320 The all singing, all dancing, multi-server, asynchronous, scanning,
321 searching, record nabbing, facet-building
323 See verbse embedded documentation.
325 =cut
327 sub getRecords {
328 my (
329 $koha_query, $simple_query, $sort_by_ref, $servers_ref,
330 $results_per_page, $offset, $expanded_facet, $branches,
331 $itemtypes, $query_type, $scan, $opac
332 ) = @_;
334 my @servers = @$servers_ref;
335 my @sort_by = @$sort_by_ref;
337 # Initialize variables for the ZOOM connection and results object
338 my $zconn;
339 my @zconns;
340 my @results;
341 my $results_hashref = ();
343 # Initialize variables for the faceted results objects
344 my $facets_counter = ();
345 my $facets_info = ();
346 my $facets = getFacets();
347 my $facets_maxrecs = C4::Context->preference('maxRecordsForFacets')||20;
349 my @facets_loop; # stores the ref to array of hashes for template facets loop
351 ### LOOP THROUGH THE SERVERS
352 for ( my $i = 0 ; $i < @servers ; $i++ ) {
353 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
355 # perform the search, create the results objects
356 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
357 my $query_to_use = ($servers[$i] =~ /biblioserver/) ? $koha_query : $simple_query;
359 #$query_to_use = $simple_query if $scan;
360 warn $simple_query if ( $scan and $DEBUG );
362 # Check if we've got a query_type defined, if so, use it
363 eval {
364 if ($query_type) {
365 if ($query_type =~ /^ccl/) {
366 $query_to_use =~ s/\:/\=/g; # change : to = last minute (FIXME)
367 $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
368 } elsif ($query_type =~ /^cql/) {
369 $results[$i] = $zconns[$i]->search(new ZOOM::Query::CQL($query_to_use, $zconns[$i]));
370 } elsif ($query_type =~ /^pqf/) {
371 $results[$i] = $zconns[$i]->search(new ZOOM::Query::PQF($query_to_use, $zconns[$i]));
372 } else {
373 warn "Unknown query_type '$query_type'. Results undetermined.";
375 } elsif ($scan) {
376 $results[$i] = $zconns[$i]->scan( new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
377 } else {
378 $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
381 if ($@) {
382 warn "WARNING: query problem with $query_to_use " . $@;
385 # Concatenate the sort_by limits and pass them to the results object
386 # Note: sort will override rank
387 my $sort_by;
388 foreach my $sort (@sort_by) {
389 if ( $sort eq "author_az" || $sort eq "author_asc" ) {
390 $sort_by .= "1=1003 <i ";
392 elsif ( $sort eq "author_za" || $sort eq "author_dsc" ) {
393 $sort_by .= "1=1003 >i ";
395 elsif ( $sort eq "popularity_asc" ) {
396 $sort_by .= "1=9003 <i ";
398 elsif ( $sort eq "popularity_dsc" ) {
399 $sort_by .= "1=9003 >i ";
401 elsif ( $sort eq "call_number_asc" ) {
402 $sort_by .= "1=8007 <i ";
404 elsif ( $sort eq "call_number_dsc" ) {
405 $sort_by .= "1=8007 >i ";
407 elsif ( $sort eq "pubdate_asc" ) {
408 $sort_by .= "1=31 <i ";
410 elsif ( $sort eq "pubdate_dsc" ) {
411 $sort_by .= "1=31 >i ";
413 elsif ( $sort eq "acqdate_asc" ) {
414 $sort_by .= "1=32 <i ";
416 elsif ( $sort eq "acqdate_dsc" ) {
417 $sort_by .= "1=32 >i ";
419 elsif ( $sort eq "title_az" || $sort eq "title_asc" ) {
420 $sort_by .= "1=4 <i ";
422 elsif ( $sort eq "title_za" || $sort eq "title_dsc" ) {
423 $sort_by .= "1=4 >i ";
425 else {
426 warn "Ignoring unrecognized sort '$sort' requested" if $sort_by;
429 if ( $sort_by && !$scan && $results[$i] ) {
430 if ( $results[$i]->sort( "yaz", $sort_by ) < 0 ) {
431 warn "WARNING sort $sort_by failed";
434 } # finished looping through servers
436 # The big moment: asynchronously retrieve results from all servers
437 _ZOOM_event_loop(
438 \@zconns,
439 \@results,
440 sub {
441 my ( $i, $size ) = @_;
442 my $results_hash;
444 # loop through the results
445 $results_hash->{'hits'} = $size;
446 my $times;
447 if ( $offset + $results_per_page <= $size ) {
448 $times = $offset + $results_per_page;
450 else {
451 $times = $size;
454 for ( my $j = $offset ; $j < $times ; $j++ ) {
455 my $records_hash;
456 my $record;
458 ## Check if it's an index scan
459 if ($scan) {
460 my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
462 # here we create a minimal MARC record and hand it off to the
463 # template just like a normal result ... perhaps not ideal, but
464 # it works for now
465 my $tmprecord = MARC::Record->new();
466 $tmprecord->encoding('UTF-8');
467 my $tmptitle;
468 my $tmpauthor;
470 # the minimal record in author/title (depending on MARC flavour)
471 if ( C4::Context->preference("marcflavour") eq
472 "UNIMARC" )
474 $tmptitle = MARC::Field->new(
475 '200', ' ', ' ',
476 a => $term,
477 f => $occ
479 $tmprecord->append_fields($tmptitle);
481 else {
482 $tmptitle =
483 MARC::Field->new( '245', ' ', ' ', a => $term, );
484 $tmpauthor =
485 MARC::Field->new( '100', ' ', ' ', a => $occ, );
486 $tmprecord->append_fields($tmptitle);
487 $tmprecord->append_fields($tmpauthor);
489 $results_hash->{'RECORDS'}[$j] =
490 $tmprecord->as_usmarc();
493 # not an index scan
494 else {
495 $record = $results[ $i - 1 ]->record($j)->raw();
496 # warn "RECORD $j:".$record;
497 $results_hash->{'RECORDS'}[$j] = $record;
501 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
503 # Fill the facets while we're looping, but only for the biblioserver and not for a scan
504 if ( !$scan && $servers[ $i - 1 ] =~ /biblioserver/ ) {
506 my $jmax =
507 $size > $facets_maxrecs ? $facets_maxrecs : $size;
508 for my $facet (@$facets) {
509 for ( my $j = 0 ; $j < $jmax ; $j++ ) {
511 my $marc_record = new_record_from_zebra (
512 'biblioserver',
513 $results[ $i - 1 ]->record($j)->raw()
516 if ( ! defined $marc_record ) {
517 warn "ERROR DECODING RECORD - $@: " .
518 $results[ $i - 1 ]->record($j)->raw();
519 next;
522 my @used_datas = ();
524 foreach my $tag ( @{ $facet->{tags} } ) {
526 # avoid first line
527 my $tag_num = substr( $tag, 0, 3 );
528 my $subfield_letters = substr( $tag, 3 );
529 # Removed when as_string fixed
530 my @subfields = $subfield_letters =~ /./sg;
532 my @fields = $marc_record->field($tag_num);
533 foreach my $field (@fields) {
534 my $data = $field->as_string( $subfield_letters, $facet->{sep} );
536 unless ( $data ~~ @used_datas ) {
537 push @used_datas, $data;
538 $facets_counter->{ $facet->{idx} }->{$data}++;
540 } # fields
541 } # field codes
542 } # records
543 $facets_info->{ $facet->{idx} }->{label_value} =
544 $facet->{label};
545 $facets_info->{ $facet->{idx} }->{expanded} =
546 $facet->{expanded};
547 } # facets
550 # warn "connection ", $i-1, ": $size hits";
551 # warn $results[$i-1]->record(0)->render() if $size > 0;
553 # BUILD FACETS
554 if ( $servers[ $i - 1 ] =~ /biblioserver/ ) {
555 for my $link_value (
556 sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
557 keys %$facets_counter
560 my $expandable;
561 my $number_of_facets;
562 my @this_facets_array;
563 for my $one_facet (
564 sort {
565 $facets_counter->{$link_value}
566 ->{$b} <=> $facets_counter->{$link_value}
567 ->{$a}
568 } keys %{ $facets_counter->{$link_value} }
571 $number_of_facets++;
572 if ( ( $number_of_facets <= 5 )
573 || ( $expanded_facet eq $link_value )
574 || ( $facets_info->{$link_value}->{'expanded'} )
578 # Sanitize the link value : parenthesis, question and exclamation mark will cause errors with CCL
579 my $facet_link_value = $one_facet;
580 $facet_link_value =~ s/[()!?¡¿؟]/ /g;
582 # fix the length that will display in the label,
583 my $facet_label_value = $one_facet;
584 my $facet_max_length = C4::Context->preference(
585 'FacetLabelTruncationLength')
586 || 20;
587 $facet_label_value =
588 substr( $one_facet, 0, $facet_max_length )
589 . "..."
590 if length($facet_label_value) >
591 $facet_max_length;
593 # if it's a branch, label by the name, not the code,
594 if ( $link_value =~ /branch/ ) {
595 if ( defined $branches
596 && ref($branches) eq "HASH"
597 && defined $branches->{$one_facet}
598 && ref( $branches->{$one_facet} ) eq
599 "HASH" )
601 $facet_label_value =
602 $branches->{$one_facet}
603 ->{'branchname'};
605 else {
606 $facet_label_value = "*";
610 # if it's a itemtype, label by the name, not the code,
611 if ( $link_value =~ /itype/ ) {
612 if ( defined $itemtypes
613 && ref($itemtypes) eq "HASH"
614 && defined $itemtypes->{$one_facet}
615 && ref( $itemtypes->{$one_facet} ) eq
616 "HASH" )
618 $facet_label_value =
619 $itemtypes->{$one_facet}
620 ->{'description'};
624 # also, if it's a location code, use the name instead of the code
625 if ( $link_value =~ /location/ ) {
626 $facet_label_value =
627 GetKohaAuthorisedValueLib( 'LOC',
628 $one_facet, $opac );
631 # but we're down with the whole label being in the link's title.
632 push @this_facets_array,
634 facet_count =>
635 $facets_counter->{$link_value}
636 ->{$one_facet},
637 facet_label_value => $facet_label_value,
638 facet_title_value => $one_facet,
639 facet_link_value => $facet_link_value,
640 type_link_value => $link_value,
642 if ($facet_label_value);
646 # handle expanded option
647 unless ( $facets_info->{$link_value}->{'expanded'} ) {
648 $expandable = 1
649 if ( ( $number_of_facets > 5 )
650 && ( $expanded_facet ne $link_value ) );
652 push @facets_loop,
654 type_link_value => $link_value,
655 type_id => $link_value . "_id",
656 "type_label_"
657 . $facets_info->{$link_value}->{'label_value'} =>
659 facets => \@this_facets_array,
660 expandable => $expandable,
661 expand => $link_value,
663 unless (
665 $facets_info->{$link_value}->{'label_value'} =~
666 /Libraries/
668 and ( C4::Context->preference('singleBranchMode') )
674 return ( undef, $results_hashref, \@facets_loop );
677 sub pazGetRecords {
678 my (
679 $koha_query, $simple_query, $sort_by_ref, $servers_ref,
680 $results_per_page, $offset, $expanded_facet, $branches,
681 $query_type, $scan
682 ) = @_;
684 my $paz = C4::Search::PazPar2->new(C4::Context->config('pazpar2url'));
685 $paz->init();
686 $paz->search($simple_query);
687 sleep 1; # FIXME: WHY?
689 # do results
690 my $results_hashref = {};
691 my $stats = XMLin($paz->stat);
692 my $results = XMLin($paz->show($offset, $results_per_page, 'work-title:1'), forcearray => 1);
694 # for a grouped search result, the number of hits
695 # is the number of groups returned; 'bib_hits' will have
696 # the total number of bibs.
697 $results_hashref->{'biblioserver'}->{'hits'} = $results->{'merged'}->[0];
698 $results_hashref->{'biblioserver'}->{'bib_hits'} = $stats->{'hits'};
700 HIT: foreach my $hit (@{ $results->{'hit'} }) {
701 my $recid = $hit->{recid}->[0];
703 my $work_title = $hit->{'md-work-title'}->[0];
704 my $work_author;
705 if (exists $hit->{'md-work-author'}) {
706 $work_author = $hit->{'md-work-author'}->[0];
708 my $group_label = (defined $work_author) ? "$work_title / $work_author" : $work_title;
710 my $result_group = {};
711 $result_group->{'group_label'} = $group_label;
712 $result_group->{'group_merge_key'} = $recid;
714 my $count = 1;
715 if (exists $hit->{count}) {
716 $count = $hit->{count}->[0];
718 $result_group->{'group_count'} = $count;
720 for (my $i = 0; $i < $count; $i++) {
721 # FIXME -- may need to worry about diacritics here
722 my $rec = $paz->record($recid, $i);
723 push @{ $result_group->{'RECORDS'} }, $rec;
726 push @{ $results_hashref->{'biblioserver'}->{'GROUPS'} }, $result_group;
729 # pass through facets
730 my $termlist_xml = $paz->termlist('author,subject');
731 my $terms = XMLin($termlist_xml, forcearray => 1);
732 my @facets_loop = ();
733 #die Dumper($results);
734 # foreach my $list (sort keys %{ $terms->{'list'} }) {
735 # my @facets = ();
736 # foreach my $facet (sort @{ $terms->{'list'}->{$list}->{'term'} } ) {
737 # push @facets, {
738 # facet_label_value => $facet->{'name'}->[0],
739 # };
741 # push @facets_loop, ( {
742 # type_label => $list,
743 # facets => \@facets,
744 # } );
747 return ( undef, $results_hashref, \@facets_loop );
750 # STOPWORDS
751 sub _remove_stopwords {
752 my ( $operand, $index ) = @_;
753 my @stopwords_removed;
755 # phrase and exact-qualified indexes shouldn't have stopwords removed
756 if ( $index !~ m/,(phr|ext)/ ) {
758 # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
759 # we use IsAlpha unicode definition, to deal correctly with diacritics.
760 # otherwise, a French word like "leçon" woudl be split into "le" "çon", "le"
761 # is a stopword, we'd get "çon" and wouldn't find anything...
763 foreach ( keys %{ C4::Context->stopwords } ) {
764 next if ( $_ =~ /(and|or|not)/ ); # don't remove operators
765 if ( my ($matched) = ($operand =~
766 /([^\X\p{isAlnum}]\Q$_\E[^\X\p{isAlnum}]|[^\X\p{isAlnum}]\Q$_\E$|^\Q$_\E[^\X\p{isAlnum}])/gi))
768 $operand =~ s/\Q$matched\E/ /gi;
769 push @stopwords_removed, $_;
773 return ( $operand, \@stopwords_removed );
776 # TRUNCATION
777 sub _detect_truncation {
778 my ( $operand, $index ) = @_;
779 my ( @nontruncated, @righttruncated, @lefttruncated, @rightlefttruncated,
780 @regexpr );
781 $operand =~ s/^ //g;
782 my @wordlist = split( /\s/, $operand );
783 foreach my $word (@wordlist) {
784 if ( $word =~ s/^\*([^\*]+)\*$/$1/ ) {
785 push @rightlefttruncated, $word;
787 elsif ( $word =~ s/^\*([^\*]+)$/$1/ ) {
788 push @lefttruncated, $word;
790 elsif ( $word =~ s/^([^\*]+)\*$/$1/ ) {
791 push @righttruncated, $word;
793 elsif ( index( $word, "*" ) < 0 ) {
794 push @nontruncated, $word;
796 else {
797 push @regexpr, $word;
800 return (
801 \@nontruncated, \@righttruncated, \@lefttruncated,
802 \@rightlefttruncated, \@regexpr
806 # STEMMING
807 sub _build_stemmed_operand {
808 my ($operand,$lang) = @_;
809 require Lingua::Stem::Snowball ;
810 my $stemmed_operand=q{};
812 # If operand contains a digit, it is almost certainly an identifier, and should
813 # not be stemmed. This is particularly relevant for ISBNs and ISSNs, which
814 # can contain the letter "X" - for example, _build_stemmend_operand would reduce
815 # "014100018X" to "x ", which for a MARC21 database would bring up irrelevant
816 # results (e.g., "23 x 29 cm." from the 300$c). Bug 2098.
817 return $operand if $operand =~ /\d/;
819 # FIXME: the locale should be set based on the user's language and/or search choice
820 #warn "$lang";
821 # Make sure we only use the first two letters from the language code
822 $lang = lc(substr($lang, 0, 2));
823 # The language codes for the two variants of Norwegian will now be "nb" and "nn",
824 # none of which Lingua::Stem::Snowball can use, so we need to "translate" them
825 if ($lang eq 'nb' || $lang eq 'nn') {
826 $lang = 'no';
828 my $stemmer = Lingua::Stem::Snowball->new( lang => $lang,
829 encoding => "UTF-8" );
831 my @words = split( / /, $operand );
832 my @stems = $stemmer->stem(\@words);
833 for my $stem (@stems) {
834 $stemmed_operand .= "$stem";
835 $stemmed_operand .= "?"
836 unless ( $stem =~ /(and$|or$|not$)/ ) || ( length($stem) < 3 );
837 $stemmed_operand .= " ";
839 warn "STEMMED OPERAND: $stemmed_operand" if $DEBUG;
840 return $stemmed_operand;
843 # FIELD WEIGHTING
844 sub _build_weighted_query {
846 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
847 # pretty well but could work much better if we had a smarter query parser
848 my ( $operand, $stemmed_operand, $index ) = @_;
849 my $stemming = C4::Context->preference("QueryStemming") || 0;
850 my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
851 my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
852 $operand =~ s/"/ /g; # Bug 7518: searches with quotation marks don't work
854 my $weighted_query .= "(rk=("; # Specifies that we're applying rank
856 # Keyword, or, no index specified
857 if ( ( $index eq 'kw' ) || ( !$index ) ) {
858 $weighted_query .=
859 "Title-cover,ext,r1=\"$operand\""; # exact title-cover
860 $weighted_query .= " or ti,ext,r2=\"$operand\""; # exact title
861 $weighted_query .= " or Title-cover,phr,r3=\"$operand\""; # phrase title
862 $weighted_query .= " or ti,wrdl,r4=\"$operand\""; # words in title
863 #$weighted_query .= " or any,ext,r4=$operand"; # exact any
864 #$weighted_query .=" or kw,wrdl,r5=\"$operand\""; # word list any
865 $weighted_query .= " or wrdl,fuzzy,r8=\"$operand\""
866 if $fuzzy_enabled; # add fuzzy, word list
867 $weighted_query .= " or wrdl,right-Truncation,r9=\"$stemmed_operand\""
868 if ( $stemming and $stemmed_operand )
869 ; # add stemming, right truncation
870 $weighted_query .= " or wrdl,r9=\"$operand\"";
872 # embedded sorting: 0 a-z; 1 z-a
873 # $weighted_query .= ") or (sort1,aut=1";
876 # Barcode searches should skip this process
877 elsif ( $index eq 'bc' ) {
878 $weighted_query .= "bc=\"$operand\"";
881 # Authority-number searches should skip this process
882 elsif ( $index eq 'an' ) {
883 $weighted_query .= "an=\"$operand\"";
886 # If the index already has more than one qualifier, wrap the operand
887 # in quotes and pass it back (assumption is that the user knows what they
888 # are doing and won't appreciate us mucking up their query
889 elsif ( $index =~ ',' ) {
890 $weighted_query .= " $index=\"$operand\"";
893 #TODO: build better cases based on specific search indexes
894 else {
895 $weighted_query .= " $index,ext,r1=\"$operand\""; # exact index
896 #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
897 $weighted_query .= " or $index,phr,r3=\"$operand\""; # phrase index
898 $weighted_query .= " or $index,wrdl,r6=\"$operand\""; # word list index
899 $weighted_query .= " or $index,wrdl,fuzzy,r8=\"$operand\""
900 if $fuzzy_enabled; # add fuzzy, word list
901 $weighted_query .= " or $index,wrdl,rt,r9=\"$stemmed_operand\""
902 if ( $stemming and $stemmed_operand ); # add stemming, right truncation
905 $weighted_query .= "))"; # close rank specification
906 return $weighted_query;
909 =head2 getIndexes
911 Return an array with available indexes.
913 =cut
915 sub getIndexes{
916 my @indexes = (
917 # biblio indexes
918 'ab',
919 'Abstract',
920 'acqdate',
921 'allrecords',
922 'an',
923 'Any',
924 'at',
925 'au',
926 'aub',
927 'aud',
928 'audience',
929 'auo',
930 'aut',
931 'Author',
932 'Author-in-order ',
933 'Author-personal-bibliography',
934 'Authority-Number',
935 'authtype',
936 'bc',
937 'Bib-level',
938 'biblionumber',
939 'bio',
940 'biography',
941 'callnum',
942 'cfn',
943 'Chronological-subdivision',
944 'cn-bib-source',
945 'cn-bib-sort',
946 'cn-class',
947 'cn-item',
948 'cn-prefix',
949 'cn-suffix',
950 'cpn',
951 'Code-institution',
952 'Conference-name',
953 'Conference-name-heading',
954 'Conference-name-see',
955 'Conference-name-seealso',
956 'Content-type',
957 'Control-number',
958 'copydate',
959 'Corporate-name',
960 'Corporate-name-heading',
961 'Corporate-name-see',
962 'Corporate-name-seealso',
963 'Country-publication',
964 'ctype',
965 'curriculum',
966 'date-entered-on-file',
967 'Date-of-acquisition',
968 'Date-of-publication',
969 'Dewey-classification',
970 'Dissertation-information',
971 'EAN',
972 'extent',
973 'fic',
974 'fiction',
975 'Form-subdivision',
976 'format',
977 'Geographic-subdivision',
978 'he',
979 'Heading',
980 'Heading-use-main-or-added-entry',
981 'Heading-use-series-added-entry ',
982 'Heading-use-subject-added-entry',
983 'Host-item',
984 'id-other',
985 'Illustration-code',
986 'Index-term-genre',
987 'Index-term-uncontrolled',
988 'ISBN',
989 'isbn',
990 'ISSN',
991 'issn',
992 'itemtype',
993 'kw',
994 'Koha-Auth-Number',
995 'l-format',
996 'language',
997 'language-original',
998 'lc-card',
999 'LC-card-number',
1000 'lcn',
1001 'lex',
1002 'llength',
1003 'ln',
1004 'ln-audio',
1005 'ln-subtitle',
1006 'Local-classification',
1007 'Local-number',
1008 'Match-heading',
1009 'Match-heading-see-from',
1010 'Material-type',
1011 'mc-itemtype',
1012 'mc-rtype',
1013 'mus',
1014 'name',
1015 'Music-number',
1016 'Name-geographic',
1017 'Name-geographic-heading',
1018 'Name-geographic-see',
1019 'Name-geographic-seealso',
1020 'nb',
1021 'Note',
1022 'notes',
1023 'ns',
1024 'nt',
1025 'pb',
1026 'Personal-name',
1027 'Personal-name-heading',
1028 'Personal-name-see',
1029 'Personal-name-seealso',
1030 'pl',
1031 'Place-publication',
1032 'pn',
1033 'popularity',
1034 'pubdate',
1035 'Publisher',
1036 'Record-control-number',
1037 'rcn',
1038 'Record-type',
1039 'rtype',
1040 'se',
1041 'See',
1042 'See-also',
1043 'sn',
1044 'Stock-number',
1045 'su',
1046 'Subject',
1047 'Subject-heading-thesaurus',
1048 'Subject-name-personal',
1049 'Subject-subdivision',
1050 'Summary',
1051 'Suppress',
1052 'su-geo',
1053 'su-na',
1054 'su-to',
1055 'su-ut',
1056 'ut',
1057 'Term-genre-form',
1058 'Term-genre-form-heading',
1059 'Term-genre-form-see',
1060 'Term-genre-form-seealso',
1061 'ti',
1062 'Title',
1063 'Title-cover',
1064 'Title-series',
1065 'Title-uniform',
1066 'Title-uniform-heading',
1067 'Title-uniform-see',
1068 'Title-uniform-seealso',
1069 'totalissues',
1070 'yr',
1072 # items indexes
1073 'acqsource',
1074 'barcode',
1075 'bc',
1076 'branch',
1077 'ccode',
1078 'classification-source',
1079 'cn-sort',
1080 'coded-location-qualifier',
1081 'copynumber',
1082 'damaged',
1083 'datelastborrowed',
1084 'datelastseen',
1085 'holdingbranch',
1086 'homebranch',
1087 'issues',
1088 'item',
1089 'itemnumber',
1090 'itype',
1091 'Local-classification',
1092 'location',
1093 'lost',
1094 'materials-specified',
1095 'mc-ccode',
1096 'mc-itype',
1097 'mc-loc',
1098 'notforloan',
1099 'Number-local-acquisition',
1100 'onloan',
1101 'price',
1102 'renewals',
1103 'replacementprice',
1104 'replacementpricedate',
1105 'reserves',
1106 'restricted',
1107 'stack',
1108 'stocknumber',
1109 'inv',
1110 'uri',
1111 'withdrawn',
1113 # subject related
1116 return \@indexes;
1119 =head2 _handle_exploding_index
1121 my $query = _handle_exploding_index($index, $term)
1123 Callback routine to generate the search for "exploding" indexes (i.e.
1124 those indexes which are turned into multiple or-connected searches based
1125 on authority data).
1127 =cut
1129 sub _handle_exploding_index {
1130 my ($QParser, $filter, $params, $negate, $server) = @_;
1131 my $index = $filter;
1132 my $term = join(' ', @$params);
1134 return unless ($index =~ m/(su-br|su-na|su-rl)/ && $term);
1136 my $marcflavour = C4::Context->preference('marcflavour');
1138 my $codesubfield = $marcflavour eq 'UNIMARC' ? '5' : 'w';
1139 my $wantedcodes = '';
1140 my @subqueries = ( "\@attr 1=Subject \@attr 4=1 \"$term\"");
1141 my ($error, $results, $total_hits) = SimpleSearch( "he:$term", undef, undef, [ "authorityserver" ] );
1142 foreach my $auth (@$results) {
1143 my $record = MARC::Record->new_from_usmarc($auth);
1144 my @references = $record->field('5..');
1145 if (@references) {
1146 if ($index eq 'su-br') {
1147 $wantedcodes = 'g';
1148 } elsif ($index eq 'su-na') {
1149 $wantedcodes = 'h';
1150 } elsif ($index eq 'su-rl') {
1151 $wantedcodes = '';
1153 foreach my $reference (@references) {
1154 my $codes = $reference->subfield($codesubfield);
1155 push @subqueries, '@attr 1=Subject @attr 4=1 "' . $reference->as_string('abcdefghijlmnopqrstuvxyz') . '"' if (($codes && $codes eq $wantedcodes) || !$wantedcodes);
1159 my $query = ' @or ' x (scalar(@subqueries) - 1) . join(' ', @subqueries);
1160 return $query;
1163 =head2 parseQuery
1165 ( $operators, $operands, $indexes, $limits,
1166 $sort_by, $scan, $lang ) =
1167 buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
1169 Shim function to ease the transition from buildQuery to a new QueryParser.
1170 This function is called at the beginning of buildQuery, and modifies
1171 buildQuery's input. If it can handle the input, it returns a query that
1172 buildQuery will not try to parse.
1173 =cut
1175 sub parseQuery {
1176 my ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang) = @_;
1178 my @operators = $operators ? @$operators : ();
1179 my @indexes = $indexes ? @$indexes : ();
1180 my @operands = $operands ? @$operands : ();
1181 my @limits = $limits ? @$limits : ();
1182 my @sort_by = $sort_by ? @$sort_by : ();
1184 my $query = $operands[0];
1185 my $index;
1186 my $term;
1187 my $query_desc;
1189 my $QParser;
1190 $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser') || $query =~ s/^qp=//);
1191 undef $QParser if ($query =~ m/^(ccl=|pqf=|cql=)/ || grep (/\w,\w|\w=\w/, @operands, @indexes) );
1192 undef $QParser if (scalar @limits > 0);
1194 if ($QParser)
1196 $QParser->custom_data->{'QueryAutoTruncate'} = C4::Context->preference('QueryAutoTruncate');
1197 $query = '';
1198 for ( my $ii = 0 ; $ii <= @operands ; $ii++ ) {
1199 next unless $operands[$ii];
1200 $query .= $operators[ $ii - 1 ] eq 'or' ? ' || ' : ' && '
1201 if ($query);
1202 if ( $operands[$ii] =~ /^[^"]\W*[-|_\w]*:\w.*[^"]$/ ) {
1203 $query .= $operands[$ii];
1205 elsif ( $indexes[$ii] =~ m/su-/ ) {
1206 $query .= $indexes[$ii] . '(' . $operands[$ii] . ')';
1208 else {
1209 $query .=
1210 ( $indexes[$ii] ? "$indexes[$ii]:" : '' ) . $operands[$ii];
1213 foreach my $limit (@limits) {
1215 if ( scalar(@sort_by) > 0 ) {
1216 my $modifier_re =
1217 '#(' . join( '|', @{ $QParser->modifiers } ) . ')';
1218 $query =~ s/$modifier_re//g;
1219 foreach my $modifier (@sort_by) {
1220 $query .= " #$modifier";
1224 $query_desc = $query;
1225 $query_desc =~ s/\s+/ /g;
1226 if ( C4::Context->preference("QueryWeightFields") ) {
1228 $QParser->add_bib1_filter_map( 'su-br' => 'biblioserver' =>
1229 { 'target_syntax_callback' => \&_handle_exploding_index } );
1230 $QParser->add_bib1_filter_map( 'su-na' => 'biblioserver' =>
1231 { 'target_syntax_callback' => \&_handle_exploding_index } );
1232 $QParser->add_bib1_filter_map( 'su-rl' => 'biblioserver' =>
1233 { 'target_syntax_callback' => \&_handle_exploding_index } );
1234 $QParser->parse($query);
1235 $operands[0] = "pqf=" . $QParser->target_syntax('biblioserver');
1237 else {
1238 require Koha::QueryParser::Driver::PQF;
1239 my $modifier_re = '#(' . join( '|', @{Koha::QueryParser::Driver::PQF->modifiers}) . ')';
1240 s/$modifier_re//g for @operands;
1243 return ( $operators, \@operands, $indexes, $limits, $sort_by, $scan, $lang, $query_desc);
1246 =head2 buildQuery
1248 ( $error, $query,
1249 $simple_query, $query_cgi,
1250 $query_desc, $limit,
1251 $limit_cgi, $limit_desc,
1252 $stopwords_removed, $query_type ) = buildQuery ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
1254 Build queries and limits in CCL, CGI, Human,
1255 handle truncation, stemming, field weighting, stopwords, fuzziness, etc.
1257 See verbose embedded documentation.
1260 =cut
1262 sub buildQuery {
1263 my ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang) = @_;
1265 warn "---------\nEnter buildQuery\n---------" if $DEBUG;
1267 my $query_desc;
1268 ( $operators, $operands, $indexes, $limits, $sort_by, $scan, $lang, $query_desc) = parseQuery($operators, $operands, $indexes, $limits, $sort_by, $scan, $lang);
1270 # dereference
1271 my @operators = $operators ? @$operators : ();
1272 my @indexes = $indexes ? @$indexes : ();
1273 my @operands = $operands ? @$operands : ();
1274 my @limits = $limits ? @$limits : ();
1275 my @sort_by = $sort_by ? @$sort_by : ();
1277 my $stemming = C4::Context->preference("QueryStemming") || 0;
1278 my $auto_truncation = C4::Context->preference("QueryAutoTruncate") || 0;
1279 my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
1280 my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
1281 my $remove_stopwords = C4::Context->preference("QueryRemoveStopwords") || 0;
1283 my $query = $operands[0];
1284 my $simple_query = $operands[0];
1286 # initialize the variables we're passing back
1287 my $query_cgi;
1288 my $query_type;
1290 my $limit;
1291 my $limit_cgi;
1292 my $limit_desc;
1294 my $stopwords_removed; # flag to determine if stopwords have been removed
1296 my $cclq = 0;
1297 my $cclindexes = getIndexes();
1298 if ( $query !~ /\s*(ccl=|pqf=|cql=)/ ) {
1299 while ( !$cclq && $query =~ /(?:^|\W)([\w-]+)(,[\w-]+)*[:=]/g ) {
1300 my $dx = lc($1);
1301 $cclq = grep { lc($_) eq $dx } @$cclindexes;
1303 $query = "ccl=$query" if $cclq;
1306 # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
1307 # DIAGNOSTIC ONLY!!
1308 if ( $query =~ /^ccl=/ ) {
1309 my $q=$';
1310 # This is needed otherwise ccl= and &limit won't work together, and
1311 # this happens when selecting a subject on the opac-detail page
1312 @limits = grep {!/^$/} @limits;
1313 if ( @limits ) {
1314 $q .= ' and '.join(' and ', @limits);
1316 return ( undef, $q, $q, "q=ccl=".uri_escape($q), $q, '', '', '', '', 'ccl' );
1318 if ( $query =~ /^cql=/ ) {
1319 return ( undef, $', $', "q=cql=".uri_escape($'), $', '', '', '', '', 'cql' );
1321 if ( $query =~ /^pqf=/ ) {
1322 if ($query_desc) {
1323 $query_cgi = "q=".uri_escape($query_desc);
1324 } else {
1325 $query_desc = $';
1326 $query_cgi = "q=pqf=".uri_escape($');
1328 return ( undef, $', $', $query_cgi, $query_desc, '', '', '', '', 'pqf' );
1331 # pass nested queries directly
1332 # FIXME: need better handling of some of these variables in this case
1333 # Nested queries aren't handled well and this implementation is flawed and causes users to be
1334 # unable to search for anything containing () commenting out, will be rewritten for 3.4.0
1335 # if ( $query =~ /(\(|\))/ ) {
1336 # return (
1337 # undef, $query, $simple_query, $query_cgi,
1338 # $query, $limit, $limit_cgi, $limit_desc,
1339 # $stopwords_removed, 'ccl'
1340 # );
1343 # Form-based queries are non-nested and fixed depth, so we can easily modify the incoming
1344 # query operands and indexes and add stemming, truncation, field weighting, etc.
1345 # Once we do so, we'll end up with a value in $query, just like if we had an
1346 # incoming $query from the user
1347 else {
1348 $query = ""
1349 ; # clear it out so we can populate properly with field-weighted, stemmed, etc. query
1350 my $previous_operand
1351 ; # a flag used to keep track if there was a previous query
1352 # if there was, we can apply the current operator
1353 # for every operand
1354 for ( my $i = 0 ; $i <= @operands ; $i++ ) {
1356 # COMBINE OPERANDS, INDEXES AND OPERATORS
1357 if ( $operands[$i] ) {
1358 $operands[$i]=~s/^\s+//;
1360 # A flag to determine whether or not to add the index to the query
1361 my $indexes_set;
1363 # If the user is sophisticated enough to specify an index, turn off field weighting, stemming, and stopword handling
1364 if ( $operands[$i] =~ /\w(:|=)/ || $scan ) {
1365 $weight_fields = 0;
1366 $stemming = 0;
1367 $remove_stopwords = 0;
1368 } else {
1369 $operands[$i] =~ s/\?/{?}/g; # need to escape question marks
1371 my $operand = $operands[$i];
1372 my $index = $indexes[$i];
1374 # Add index-specific attributes
1375 # Date of Publication
1376 if ( $index eq 'yr' ) {
1377 $index .= ",st-numeric";
1378 $indexes_set++;
1379 $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1382 # Date of Acquisition
1383 elsif ( $index eq 'acqdate' ) {
1384 $index .= ",st-date-normalized";
1385 $indexes_set++;
1386 $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1388 # ISBN,ISSN,Standard Number, don't need special treatment
1389 elsif ( $index eq 'nb' || $index eq 'ns' ) {
1391 $stemming, $auto_truncation,
1392 $weight_fields, $fuzzy_enabled,
1393 $remove_stopwords
1394 ) = ( 0, 0, 0, 0, 0 );
1398 if(not $index){
1399 $index = 'kw';
1402 # Set default structure attribute (word list)
1403 my $struct_attr = q{};
1404 unless ( $indexes_set || !$index || $index =~ /,(st-|phr|ext|wrdl)/ || $index =~ /^(nb|ns)$/ ) {
1405 $struct_attr = ",wrdl";
1408 # Some helpful index variants
1409 my $index_plus = $index . $struct_attr . ':';
1410 my $index_plus_comma = $index . $struct_attr . ',';
1412 # Remove Stopwords
1413 if ($remove_stopwords) {
1414 ( $operand, $stopwords_removed ) =
1415 _remove_stopwords( $operand, $index );
1416 warn "OPERAND w/out STOPWORDS: >$operand<" if $DEBUG;
1417 warn "REMOVED STOPWORDS: @$stopwords_removed"
1418 if ( $stopwords_removed && $DEBUG );
1421 if ($auto_truncation){
1422 unless ( $index =~ /,(st-|phr|ext)/ ) {
1423 #FIXME only valid with LTR scripts
1424 $operand=join(" ",map{
1425 (index($_,"*")>0?"$_":"$_*")
1426 }split (/\s+/,$operand));
1427 warn $operand if $DEBUG;
1431 # Detect Truncation
1432 my $truncated_operand;
1433 my( $nontruncated, $righttruncated, $lefttruncated,
1434 $rightlefttruncated, $regexpr
1435 ) = _detect_truncation( $operand, $index );
1436 warn
1437 "TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<"
1438 if $DEBUG;
1440 # Apply Truncation
1441 if (
1442 scalar(@$righttruncated) + scalar(@$lefttruncated) +
1443 scalar(@$rightlefttruncated) > 0 )
1446 # Don't field weight or add the index to the query, we do it here
1447 $indexes_set = 1;
1448 undef $weight_fields;
1449 my $previous_truncation_operand;
1450 if (scalar @$nontruncated) {
1451 $truncated_operand .= "$index_plus @$nontruncated ";
1452 $previous_truncation_operand = 1;
1454 if (scalar @$righttruncated) {
1455 $truncated_operand .= "and " if $previous_truncation_operand;
1456 $truncated_operand .= $index_plus_comma . "rtrn:@$righttruncated ";
1457 $previous_truncation_operand = 1;
1459 if (scalar @$lefttruncated) {
1460 $truncated_operand .= "and " if $previous_truncation_operand;
1461 $truncated_operand .= $index_plus_comma . "ltrn:@$lefttruncated ";
1462 $previous_truncation_operand = 1;
1464 if (scalar @$rightlefttruncated) {
1465 $truncated_operand .= "and " if $previous_truncation_operand;
1466 $truncated_operand .= $index_plus_comma . "rltrn:@$rightlefttruncated ";
1467 $previous_truncation_operand = 1;
1470 $operand = $truncated_operand if $truncated_operand;
1471 warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
1473 # Handle Stemming
1474 my $stemmed_operand;
1475 $stemmed_operand = _build_stemmed_operand($operand, $lang)
1476 if $stemming;
1478 warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
1480 # Handle Field Weighting
1481 my $weighted_operand;
1482 if ($weight_fields) {
1483 $weighted_operand = _build_weighted_query( $operand, $stemmed_operand, $index );
1484 $operand = $weighted_operand;
1485 $indexes_set = 1;
1488 warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
1490 # If there's a previous operand, we need to add an operator
1491 if ($previous_operand) {
1493 # User-specified operator
1494 if ( $operators[ $i - 1 ] ) {
1495 $query .= " $operators[$i-1] ";
1496 $query .= " $index_plus " unless $indexes_set;
1497 $query .= " $operand";
1498 $query_cgi .= "&op=".uri_escape($operators[$i-1]);
1499 $query_cgi .= "&idx=".uri_escape($index) if $index;
1500 $query_cgi .= "&q=".uri_escape($operands[$i]) if $operands[$i];
1501 $query_desc .=
1502 " $operators[$i-1] $index_plus $operands[$i]";
1505 # Default operator is and
1506 else {
1507 $query .= " and ";
1508 $query .= "$index_plus " unless $indexes_set;
1509 $query .= "$operand";
1510 $query_cgi .= "&op=and&idx=".uri_escape($index) if $index;
1511 $query_cgi .= "&q=".uri_escape($operands[$i]) if $operands[$i];
1512 $query_desc .= " and $index_plus $operands[$i]";
1516 # There isn't a pervious operand, don't need an operator
1517 else {
1519 # Field-weighted queries already have indexes set
1520 $query .= " $index_plus " unless $indexes_set;
1521 $query .= $operand;
1522 $query_desc .= " $index_plus $operands[$i]";
1523 $query_cgi .= "&idx=".uri_escape($index) if $index;
1524 $query_cgi .= "&q=".uri_escape($operands[$i]) if $operands[$i];
1525 $previous_operand = 1;
1527 } #/if $operands
1528 } # /for
1530 warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1532 # add limits
1533 my %group_OR_limits;
1534 my $availability_limit;
1535 foreach my $this_limit (@limits) {
1536 next unless $this_limit;
1537 if ( $this_limit =~ /available/ ) {
1539 ## 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1540 ## In English:
1541 ## all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1542 $availability_limit .=
1543 "( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and (lost,st-numeric=0) )"; #or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
1544 $limit_cgi .= "&limit=available";
1545 $limit_desc .= "";
1548 # group_OR_limits, prefixed by mc-
1549 # OR every member of the group
1550 elsif ( $this_limit =~ /mc/ ) {
1551 my ($k,$v) = split(/:/, $this_limit,2);
1552 if ( $k !~ /mc-i(tem)?type/ ) {
1553 # in case the mc-ccode value has complicating chars like ()'s inside it we wrap in quotes
1554 $this_limit =~ tr/"//d;
1555 $this_limit = $k.":\"".$v."\"";
1558 $group_OR_limits{$k} .= " or " if $group_OR_limits{$k};
1559 $limit_desc .= " or " if $group_OR_limits{$k};
1560 $group_OR_limits{$k} .= "$this_limit";
1561 $limit_cgi .= "&limit=$this_limit";
1562 $limit_desc .= " $this_limit";
1565 # Regular old limits
1566 else {
1567 $limit .= " and " if $limit || $query;
1568 $limit .= "$this_limit";
1569 $limit_cgi .= "&limit=$this_limit";
1570 if ($this_limit =~ /^branch:(.+)/) {
1571 my $branchcode = $1;
1572 my $branchname = GetBranchName($branchcode);
1573 if (defined $branchname) {
1574 $limit_desc .= " branch:$branchname";
1575 } else {
1576 $limit_desc .= " $this_limit";
1578 } else {
1579 $limit_desc .= " $this_limit";
1583 foreach my $k (keys (%group_OR_limits)) {
1584 $limit .= " and " if ( $query || $limit );
1585 $limit .= "($group_OR_limits{$k})";
1587 if ($availability_limit) {
1588 $limit .= " and " if ( $query || $limit );
1589 $limit .= "($availability_limit)";
1592 # Normalize the query and limit strings
1593 # This is flawed , means we can't search anything with : in it
1594 # if user wants to do ccl or cql, start the query with that
1595 # $query =~ s/:/=/g;
1596 $query =~ s/(?<=(ti|au|pb|su|an|kw|mc|nb|ns)):/=/g;
1597 $query =~ s/(?<=(wrdl)):/=/g;
1598 $query =~ s/(?<=(trn|phr)):/=/g;
1599 $limit =~ s/:/=/g;
1600 for ( $query, $query_desc, $limit, $limit_desc ) {
1601 s/ +/ /g; # remove extra spaces
1602 s/^ //g; # remove any beginning spaces
1603 s/ $//g; # remove any ending spaces
1604 s/==/=/g; # remove double == from query
1606 $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1608 for ($query_cgi,$simple_query) {
1609 s/"//g;
1611 # append the limit to the query
1612 $query .= " " . $limit;
1614 # Warnings if DEBUG
1615 if ($DEBUG) {
1616 warn "QUERY:" . $query;
1617 warn "QUERY CGI:" . $query_cgi;
1618 warn "QUERY DESC:" . $query_desc;
1619 warn "LIMIT:" . $limit;
1620 warn "LIMIT CGI:" . $limit_cgi;
1621 warn "LIMIT DESC:" . $limit_desc;
1622 warn "---------\nLeave buildQuery\n---------";
1624 return (
1625 undef, $query, $simple_query, $query_cgi,
1626 $query_desc, $limit, $limit_cgi, $limit_desc,
1627 $stopwords_removed, $query_type
1631 =head2 searchResults
1633 my @search_results = searchResults($search_context, $searchdesc, $hits,
1634 $results_per_page, $offset, $scan,
1635 @marcresults);
1637 Format results in a form suitable for passing to the template
1639 =cut
1641 # IMO this subroutine is pretty messy still -- it's responsible for
1642 # building the HTML output for the template
1643 sub searchResults {
1644 my ( $search_context, $searchdesc, $hits, $results_per_page, $offset, $scan, $marcresults ) = @_;
1645 my $dbh = C4::Context->dbh;
1646 my @newresults;
1648 require C4::Items;
1650 $search_context = 'opac' if !$search_context || $search_context ne 'intranet';
1651 my ($is_opac, $hidelostitems);
1652 if ($search_context eq 'opac') {
1653 $hidelostitems = C4::Context->preference('hidelostitems');
1654 $is_opac = 1;
1657 #Build branchnames hash
1658 #find branchname
1659 #get branch information.....
1660 my %branches;
1661 my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
1662 $bsth->execute();
1663 while ( my $bdata = $bsth->fetchrow_hashref ) {
1664 $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
1666 # FIXME - We build an authorised values hash here, using the default framework
1667 # though it is possible to have different authvals for different fws.
1669 my $shelflocations =GetKohaAuthorisedValues('items.location','');
1671 # get notforloan authorised value list (see $shelflocations FIXME)
1672 my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
1674 #Build itemtype hash
1675 #find itemtype & itemtype image
1676 my %itemtypes;
1677 $bsth =
1678 $dbh->prepare(
1679 "SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes"
1681 $bsth->execute();
1682 while ( my $bdata = $bsth->fetchrow_hashref ) {
1683 foreach (qw(description imageurl summary notforloan)) {
1684 $itemtypes{ $bdata->{'itemtype'} }->{$_} = $bdata->{$_};
1688 #search item field code
1689 my ($itemtag, undef) = &GetMarcFromKohaField( "items.itemnumber", "" );
1691 ## find column names of items related to MARC
1692 my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
1693 $sth2->execute;
1694 my %subfieldstosearch;
1695 while ( ( my $column ) = $sth2->fetchrow ) {
1696 my ( $tagfield, $tagsubfield ) =
1697 &GetMarcFromKohaField( "items." . $column, "" );
1698 if ( defined $tagsubfield ) {
1699 $subfieldstosearch{$column} = $tagsubfield;
1703 # handle which records to actually retrieve
1704 my $times;
1705 if ( $hits && $offset + $results_per_page <= $hits ) {
1706 $times = $offset + $results_per_page;
1708 else {
1709 $times = $hits; # FIXME: if $hits is undefined, why do we want to equal it?
1712 my $marcflavour = C4::Context->preference("marcflavour");
1713 # We get the biblionumber position in MARC
1714 my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
1716 # loop through all of the records we've retrieved
1717 for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1719 my $marcrecord;
1720 if ($scan) {
1721 # For Scan searches we built USMARC data
1722 $marcrecord = MARC::Record->new_from_usmarc( $marcresults->[$i]);
1723 } else {
1724 # Normal search, render from Zebra's output
1725 $marcrecord = new_record_from_zebra(
1726 'biblioserver',
1727 $marcresults->[$i]
1730 if ( ! defined $marcrecord ) {
1731 warn "ERROR DECODING RECORD - $@: " . $marcresults->[$i];
1732 next;
1736 my $fw = $scan
1737 ? undef
1738 : $bibliotag < 10
1739 ? GetFrameworkCode($marcrecord->field($bibliotag)->data)
1740 : GetFrameworkCode($marcrecord->subfield($bibliotag,$bibliosubf));
1741 my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, $fw );
1742 $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
1743 $oldbiblio->{result_number} = $i + 1;
1745 # add imageurl to itemtype if there is one
1746 $oldbiblio->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1748 $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 ) ) : [];
1749 $oldbiblio->{normalized_upc} = GetNormalizedUPC( $marcrecord,$marcflavour);
1750 $oldbiblio->{normalized_ean} = GetNormalizedEAN( $marcrecord,$marcflavour);
1751 $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
1752 $oldbiblio->{normalized_isbn} = GetNormalizedISBN(undef,$marcrecord,$marcflavour);
1753 $oldbiblio->{content_identifier_exists} = 1 if ($oldbiblio->{normalized_isbn} or $oldbiblio->{normalized_oclc} or $oldbiblio->{normalized_ean} or $oldbiblio->{normalized_upc});
1755 # edition information, if any
1756 $oldbiblio->{edition} = $oldbiblio->{editionstatement};
1757 $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1758 # Build summary if there is one (the summary is defined in the itemtypes table)
1759 # FIXME: is this used anywhere, I think it can be commented out? -- JF
1760 if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1761 my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1762 my @fields = $marcrecord->fields();
1764 my $newsummary;
1765 foreach my $line ( "$summary\n" =~ /(.*)\n/g ){
1766 my $tags = {};
1767 foreach my $tag ( $line =~ /\[(\d{3}[\w|\d])\]/ ) {
1768 $tag =~ /(.{3})(.)/;
1769 if($marcrecord->field($1)){
1770 my @abc = $marcrecord->field($1)->subfield($2);
1771 $tags->{$tag} = $#abc + 1 ;
1775 # We catch how many times to repeat this line
1776 my $max = 0;
1777 foreach my $tag (keys(%$tags)){
1778 $max = $tags->{$tag} if($tags->{$tag} > $max);
1781 # we replace, and repeat each line
1782 for (my $i = 0 ; $i < $max ; $i++){
1783 my $newline = $line;
1785 foreach my $tag ( $newline =~ /\[(\d{3}[\w|\d])\]/g ) {
1786 $tag =~ /(.{3})(.)/;
1788 if($marcrecord->field($1)){
1789 my @repl = $marcrecord->field($1)->subfield($2);
1790 my $subfieldvalue = $repl[$i];
1792 if (! utf8::is_utf8($subfieldvalue)) {
1793 utf8::decode($subfieldvalue);
1796 $newline =~ s/\[$tag\]/$subfieldvalue/g;
1799 $newsummary .= "$newline\n";
1803 $newsummary =~ s/\[(.*?)]//g;
1804 $newsummary =~ s/\n/<br\/>/g;
1805 $oldbiblio->{summary} = $newsummary;
1808 # Pull out the items fields
1809 my @fields = $marcrecord->field($itemtag);
1810 my $marcflavor = C4::Context->preference("marcflavour");
1811 # adding linked items that belong to host records
1812 my $analyticsfield = '773';
1813 if ($marcflavor eq 'MARC21' || $marcflavor eq 'NORMARC') {
1814 $analyticsfield = '773';
1815 } elsif ($marcflavor eq 'UNIMARC') {
1816 $analyticsfield = '461';
1818 foreach my $hostfield ( $marcrecord->field($analyticsfield)) {
1819 my $hostbiblionumber = $hostfield->subfield("0");
1820 my $linkeditemnumber = $hostfield->subfield("9");
1821 if(!$hostbiblionumber eq undef){
1822 my $hostbiblio = GetMarcBiblio($hostbiblionumber, 1);
1823 my ($itemfield, undef) = GetMarcFromKohaField( 'items.itemnumber', GetFrameworkCode($hostbiblionumber) );
1824 if(!$hostbiblio eq undef){
1825 my @hostitems = $hostbiblio->field($itemfield);
1826 foreach my $hostitem (@hostitems){
1827 if ($hostitem->subfield("9") eq $linkeditemnumber){
1828 my $linkeditem =$hostitem;
1829 # append linked items if they exist
1830 if (!$linkeditem eq undef){
1831 push (@fields, $linkeditem);}
1838 # Setting item statuses for display
1839 my @available_items_loop;
1840 my @onloan_items_loop;
1841 my @other_items_loop;
1843 my $available_items;
1844 my $onloan_items;
1845 my $other_items;
1847 my $ordered_count = 0;
1848 my $available_count = 0;
1849 my $onloan_count = 0;
1850 my $longoverdue_count = 0;
1851 my $other_count = 0;
1852 my $withdrawn_count = 0;
1853 my $itemlost_count = 0;
1854 my $hideatopac_count = 0;
1855 my $itembinding_count = 0;
1856 my $itemdamaged_count = 0;
1857 my $item_in_transit_count = 0;
1858 my $can_place_holds = 0;
1859 my $item_onhold_count = 0;
1860 my $items_count = scalar(@fields);
1861 my $maxitems_pref = C4::Context->preference('maxItemsinSearchResults');
1862 my $maxitems = $maxitems_pref ? $maxitems_pref - 1 : 1;
1863 my @hiddenitems; # hidden itemnumbers based on OpacHiddenItems syspref
1865 # loop through every item
1866 foreach my $field (@fields) {
1867 my $item;
1869 # populate the items hash
1870 foreach my $code ( keys %subfieldstosearch ) {
1871 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1873 $item->{description} = $itemtypes{ $item->{itype} }{description};
1875 # OPAC hidden items
1876 if ($is_opac) {
1877 # hidden because lost
1878 if ($hidelostitems && $item->{itemlost}) {
1879 $hideatopac_count++;
1880 next;
1882 # hidden based on OpacHiddenItems syspref
1883 my @hi = C4::Items::GetHiddenItemnumbers($item);
1884 if (scalar @hi) {
1885 push @hiddenitems, @hi;
1886 $hideatopac_count++;
1887 next;
1891 my $hbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch' : 'holdingbranch';
1892 my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
1894 # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
1895 if ($item->{$hbranch}) {
1896 $item->{'branchname'} = $branches{$item->{$hbranch}};
1898 elsif ($item->{$otherbranch}) { # Last resort
1899 $item->{'branchname'} = $branches{$item->{$otherbranch}};
1902 my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
1903 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
1904 my $userenv = C4::Context->userenv;
1905 if ( $item->{onloan} && !(C4::Members::GetHideLostItemsPreference($userenv->{'number'}) && $item->{itemlost}) ) {
1906 $onloan_count++;
1907 my $key = $prefix . $item->{onloan} . $item->{barcode};
1908 $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
1909 $onloan_items->{$key}->{count}++ if $item->{$hbranch};
1910 $onloan_items->{$key}->{branchname} = $item->{branchname};
1911 $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1912 $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
1913 $onloan_items->{$key}->{description} = $item->{description};
1914 $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
1915 # if something's checked out and lost, mark it as 'long overdue'
1916 if ( $item->{itemlost} ) {
1917 $onloan_items->{$prefix}->{longoverdue}++;
1918 $longoverdue_count++;
1919 } else { # can place holds as long as item isn't lost
1920 $can_place_holds = 1;
1924 # items not on loan, but still unavailable ( lost, withdrawn, damaged )
1925 else {
1927 # item is on order
1928 if ( $item->{notforloan} < 0 ) {
1929 $ordered_count++;
1932 # is item in transit?
1933 my $transfertwhen = '';
1934 my ($transfertfrom, $transfertto);
1936 # is item on the reserve shelf?
1937 my $reservestatus = '';
1939 unless ($item->{withdrawn}
1940 || $item->{itemlost}
1941 || $item->{damaged}
1942 || $item->{notforloan}
1943 || $items_count > 20) {
1945 # A couple heuristics to limit how many times
1946 # we query the database for item transfer information, sacrificing
1947 # accuracy in some cases for speed;
1949 # 1. don't query if item has one of the other statuses
1950 # 2. don't check transit status if the bib has
1951 # more than 20 items
1953 # FIXME: to avoid having the query the database like this, and to make
1954 # the in transit status count as unavailable for search limiting,
1955 # should map transit status to record indexed in Zebra.
1957 ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
1958 $reservestatus = C4::Reserves::GetReserveStatus( $item->{itemnumber}, $oldbiblio->{biblionumber} );
1961 # item is withdrawn, lost, damaged, not for loan, reserved or in transit
1962 if ( $item->{withdrawn}
1963 || $item->{itemlost}
1964 || $item->{damaged}
1965 || $item->{notforloan}
1966 || $reservestatus eq 'Waiting'
1967 || ($transfertwhen ne ''))
1969 $withdrawn_count++ if $item->{withdrawn};
1970 $itemlost_count++ if $item->{itemlost};
1971 $itemdamaged_count++ if $item->{damaged};
1972 $item_in_transit_count++ if $transfertwhen ne '';
1973 $item_onhold_count++ if $reservestatus eq 'Waiting';
1974 $item->{status} = $item->{withdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
1976 # can place a hold on a item if
1977 # not lost nor withdrawn
1978 # not damaged unless AllowHoldsOnDamagedItems is true
1979 # item is either for loan or on order (notforloan < 0)
1980 $can_place_holds = 1
1981 if (
1982 !$item->{itemlost}
1983 && !$item->{withdrawn}
1984 && ( !$item->{damaged} || C4::Context->preference('AllowHoldsOnDamagedItems') )
1985 && ( !$item->{notforloan} || $item->{notforloan} < 0 )
1988 $other_count++;
1990 my $key = $prefix . $item->{status};
1991 foreach (qw(withdrawn itemlost damaged branchname itemcallnumber)) {
1992 $other_items->{$key}->{$_} = $item->{$_};
1994 $other_items->{$key}->{intransit} = ( $transfertwhen ne '' ) ? 1 : 0;
1995 $other_items->{$key}->{onhold} = ($reservestatus) ? 1 : 0;
1996 $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value and $item->{notforloan};
1997 $other_items->{$key}->{count}++ if $item->{$hbranch};
1998 $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1999 $other_items->{$key}->{description} = $item->{description};
2000 $other_items->{$key}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
2002 # item is available
2003 else {
2004 $can_place_holds = 1;
2005 $available_count++;
2006 $available_items->{$prefix}->{count}++ if $item->{$hbranch};
2007 foreach (qw(branchname itemcallnumber description)) {
2008 $available_items->{$prefix}->{$_} = $item->{$_};
2010 $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
2011 $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( $search_context, $itemtypes{ $item->{itype} }->{imageurl} );
2014 } # notforloan, item level and biblioitem level
2016 # if all items are hidden, do not show the record
2017 if ($items_count > 0 && $hideatopac_count == $items_count) {
2018 next;
2021 my ( $availableitemscount, $onloanitemscount, $otheritemscount );
2022 for my $key ( sort keys %$onloan_items ) {
2023 (++$onloanitemscount > $maxitems) and last;
2024 push @onloan_items_loop, $onloan_items->{$key};
2026 for my $key ( sort keys %$other_items ) {
2027 (++$otheritemscount > $maxitems) and last;
2028 push @other_items_loop, $other_items->{$key};
2030 for my $key ( sort keys %$available_items ) {
2031 (++$availableitemscount > $maxitems) and last;
2032 push @available_items_loop, $available_items->{$key}
2035 # XSLT processing of some stuff
2036 use C4::Charset;
2037 SetUTF8Flag($marcrecord);
2038 warn $marcrecord->as_formatted if $DEBUG;
2039 my $interface = $search_context eq 'opac' ? 'OPAC' : '';
2040 if (!$scan && C4::Context->preference($interface . "XSLTResultsDisplay")) {
2041 $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display($oldbiblio->{biblionumber}, $marcrecord, $interface."XSLTResultsDisplay", 1, \@hiddenitems);
2042 # the last parameter tells Koha to clean up the problematic ampersand entities that Zebra outputs
2045 # if biblio level itypes are used and itemtype is notforloan, it can't be reserved either
2046 if (!C4::Context->preference("item-level_itypes")) {
2047 if ($itemtypes{ $oldbiblio->{itemtype} }->{notforloan}) {
2048 $can_place_holds = 0;
2051 $oldbiblio->{norequests} = 1 unless $can_place_holds;
2052 $oldbiblio->{itemsplural} = 1 if $items_count > 1;
2053 $oldbiblio->{items_count} = $items_count;
2054 $oldbiblio->{available_items_loop} = \@available_items_loop;
2055 $oldbiblio->{onloan_items_loop} = \@onloan_items_loop;
2056 $oldbiblio->{other_items_loop} = \@other_items_loop;
2057 $oldbiblio->{availablecount} = $available_count;
2058 $oldbiblio->{availableplural} = 1 if $available_count > 1;
2059 $oldbiblio->{onloancount} = $onloan_count;
2060 $oldbiblio->{onloanplural} = 1 if $onloan_count > 1;
2061 $oldbiblio->{othercount} = $other_count;
2062 $oldbiblio->{otherplural} = 1 if $other_count > 1;
2063 $oldbiblio->{withdrawncount} = $withdrawn_count;
2064 $oldbiblio->{itemlostcount} = $itemlost_count;
2065 $oldbiblio->{damagedcount} = $itemdamaged_count;
2066 $oldbiblio->{intransitcount} = $item_in_transit_count;
2067 $oldbiblio->{onholdcount} = $item_onhold_count;
2068 $oldbiblio->{orderedcount} = $ordered_count;
2070 if (C4::Context->preference("AlternateHoldingsField") && $items_count == 0) {
2071 my $fieldspec = C4::Context->preference("AlternateHoldingsField");
2072 my $subfields = substr $fieldspec, 3;
2073 my $holdingsep = C4::Context->preference("AlternateHoldingsSeparator") || ' ';
2074 my @alternateholdingsinfo = ();
2075 my @holdingsfields = $marcrecord->field(substr $fieldspec, 0, 3);
2076 my $alternateholdingscount = 0;
2078 for my $field (@holdingsfields) {
2079 my %holding = ( holding => '' );
2080 my $havesubfield = 0;
2081 for my $subfield ($field->subfields()) {
2082 if ((index $subfields, $$subfield[0]) >= 0) {
2083 $holding{'holding'} .= $holdingsep if (length $holding{'holding'} > 0);
2084 $holding{'holding'} .= $$subfield[1];
2085 $havesubfield++;
2088 if ($havesubfield) {
2089 push(@alternateholdingsinfo, \%holding);
2090 $alternateholdingscount++;
2094 $oldbiblio->{'ALTERNATEHOLDINGS'} = \@alternateholdingsinfo;
2095 $oldbiblio->{'alternateholdings_count'} = $alternateholdingscount;
2098 push( @newresults, $oldbiblio );
2101 return @newresults;
2104 =head2 SearchAcquisitions
2105 Search for acquisitions
2106 =cut
2108 sub SearchAcquisitions{
2109 my ($datebegin, $dateend, $itemtypes,$criteria, $orderby) = @_;
2111 my $dbh=C4::Context->dbh;
2112 # Variable initialization
2113 my $str=qq|
2114 SELECT marcxml
2115 FROM biblio
2116 LEFT JOIN biblioitems ON biblioitems.biblionumber=biblio.biblionumber
2117 LEFT JOIN items ON items.biblionumber=biblio.biblionumber
2118 WHERE dateaccessioned BETWEEN ? AND ?
2121 my (@params,@loopcriteria);
2123 push @params, $datebegin->output("iso");
2124 push @params, $dateend->output("iso");
2126 if (scalar(@$itemtypes)>0 and $criteria ne "itemtype" ){
2127 if(C4::Context->preference("item-level_itypes")){
2128 $str .= "AND items.itype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
2129 }else{
2130 $str .= "AND biblioitems.itemtype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
2132 push @params, @$itemtypes;
2135 if ($criteria =~/itemtype/){
2136 if(C4::Context->preference("item-level_itypes")){
2137 $str .= "AND items.itype=? ";
2138 }else{
2139 $str .= "AND biblioitems.itemtype=? ";
2142 if(scalar(@$itemtypes) == 0){
2143 my $itypes = GetItemTypes();
2144 for my $key (keys %$itypes){
2145 push @$itemtypes, $key;
2149 @loopcriteria= @$itemtypes;
2150 }elsif ($criteria=~/itemcallnumber/){
2151 $str .= "AND (items.itemcallnumber LIKE CONCAT(?,'%')
2152 OR items.itemcallnumber is NULL
2153 OR items.itemcallnumber = '')";
2155 @loopcriteria = ("AA".."ZZ", "") unless (scalar(@loopcriteria)>0);
2156 }else {
2157 $str .= "AND biblio.title LIKE CONCAT(?,'%') ";
2158 @loopcriteria = ("A".."z") unless (scalar(@loopcriteria)>0);
2161 if ($orderby =~ /date_desc/){
2162 $str.=" ORDER BY dateaccessioned DESC";
2163 } else {
2164 $str.=" ORDER BY title";
2167 my $qdataacquisitions=$dbh->prepare($str);
2169 my @loopacquisitions;
2170 foreach my $value(@loopcriteria){
2171 push @params,$value;
2172 my %cell;
2173 $cell{"title"}=$value;
2174 $cell{"titlecode"}=$value;
2176 eval{$qdataacquisitions->execute(@params);};
2178 if ($@){ warn "recentacquisitions Error :$@";}
2179 else {
2180 my @loopdata;
2181 while (my $data=$qdataacquisitions->fetchrow_hashref){
2182 push @loopdata, {"summary"=>GetBiblioSummary( $data->{'marcxml'} ) };
2184 $cell{"loopdata"}=\@loopdata;
2186 push @loopacquisitions,\%cell if (scalar(@{$cell{loopdata}})>0);
2187 pop @params;
2189 $qdataacquisitions->finish;
2190 return \@loopacquisitions;
2193 =head2 enabled_staff_search_views
2195 %hash = enabled_staff_search_views()
2197 This function returns a hash that contains three flags obtained from the system
2198 preferences, used to determine whether a particular staff search results view
2199 is enabled.
2201 =over 2
2203 =item C<Output arg:>
2205 * $hash{can_view_MARC} is true only if the MARC view is enabled
2206 * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2207 * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2209 =item C<usage in the script:>
2211 =back
2213 $template->param ( C4::Search::enabled_staff_search_views );
2215 =cut
2217 sub enabled_staff_search_views
2219 return (
2220 can_view_MARC => C4::Context->preference('viewMARC'), # 1 if the staff search allows the MARC view
2221 can_view_ISBD => C4::Context->preference('viewISBD'), # 1 if the staff search allows the ISBD view
2222 can_view_labeledMARC => C4::Context->preference('viewLabeledMARC'), # 1 if the staff search allows the Labeled MARC view
2226 sub AddSearchHistory{
2227 my ($borrowernumber,$session,$query_desc,$query_cgi, $total)=@_;
2228 my $dbh = C4::Context->dbh;
2230 # Add the request the user just made
2231 my $sql = "INSERT INTO search_history(userid, sessionid, query_desc, query_cgi, total, time) VALUES(?, ?, ?, ?, ?, NOW())";
2232 my $sth = $dbh->prepare($sql);
2233 $sth->execute($borrowernumber, $session, $query_desc, $query_cgi, $total);
2234 return $dbh->last_insert_id(undef, 'search_history', undef,undef,undef);
2237 sub GetSearchHistory{
2238 my ($borrowernumber,$session)=@_;
2239 my $dbh = C4::Context->dbh;
2241 # Add the request the user just made
2242 my $query = "SELECT FROM search_history WHERE (userid=? OR sessionid=?)";
2243 my $sth = $dbh->prepare($query);
2244 $sth->execute($borrowernumber, $session);
2245 return $sth->fetchall_hashref({});
2248 sub PurgeSearchHistory{
2249 my ($pSearchhistory)=@_;
2250 my $dbh = C4::Context->dbh;
2251 my $sth = $dbh->prepare("DELETE FROM search_history WHERE time < DATE_SUB( NOW(), INTERVAL ? DAY )");
2252 $sth->execute($pSearchhistory) or die $dbh->errstr;
2255 =head2 z3950_search_args
2257 $arrayref = z3950_search_args($matchpoints)
2259 This function returns an array reference that contains the search parameters to be
2260 passed to the Z39.50 search script (z3950_search.pl). The array elements
2261 are hash refs whose keys are name and value, and whose values are the
2262 name of a search parameter, the value of that search parameter and the URL encoded
2263 value of that parameter.
2265 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2267 The search parameter values are obtained from the bibliographic record whose
2268 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2270 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2271 a general purpose search argument. In this case, the returned array contains only
2272 entry: the key is 'title' and the value is derived from $matchpoints.
2274 If a search parameter value is undefined or empty, it is not included in the returned
2275 array.
2277 The returned array reference may be passed directly to the template parameters.
2279 =over 2
2281 =item C<Output arg:>
2283 * $array containing hash refs as described above
2285 =item C<usage in the script:>
2287 =back
2289 $data = Biblio::GetBiblioData($bibno);
2290 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2292 *OR*
2294 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2296 =cut
2298 sub z3950_search_args {
2299 my $bibrec = shift;
2301 my $isbn_string = ref( $bibrec ) ? $bibrec->{title} : $bibrec;
2302 my $isbn = Business::ISBN->new( $isbn_string );
2304 if (defined $isbn && $isbn->is_valid)
2306 if ( ref($bibrec) ) {
2307 $bibrec->{isbn} = $isbn_string;
2308 $bibrec->{title} = undef;
2309 } else {
2310 $bibrec = { isbn => $isbn_string };
2313 else {
2314 $bibrec = { title => $bibrec } if !ref $bibrec;
2316 my $array = [];
2317 for my $field (qw/ lccn isbn issn title author dewey subject /)
2319 push @$array, { name => $field, value => $bibrec->{$field} }
2320 if defined $bibrec->{$field};
2322 return $array;
2325 =head2 GetDistinctValues($field);
2327 C<$field> is a reference to the fields array
2329 =cut
2331 sub GetDistinctValues {
2332 my ($fieldname,$string)=@_;
2333 # returns a reference to a hash of references to branches...
2334 if ($fieldname=~/\./){
2335 my ($table,$column)=split /\./, $fieldname;
2336 my $dbh = C4::Context->dbh;
2337 warn "select DISTINCT($column) as value, count(*) as cnt from $table group by lib order by $column " if $DEBUG;
2338 my $sth = $dbh->prepare("select DISTINCT($column) as value, count(*) as cnt from $table ".($string?" where $column like \"$string%\"":"")."group by value order by $column ");
2339 $sth->execute;
2340 my $elements=$sth->fetchall_arrayref({});
2341 return $elements;
2343 else {
2344 $string||= qq("");
2345 my @servers=qw<biblioserver authorityserver>;
2346 my (@zconns,@results);
2347 for ( my $i = 0 ; $i < @servers ; $i++ ) {
2348 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
2349 $results[$i] =
2350 $zconns[$i]->scan(
2351 ZOOM::Query::CCL2RPN->new( qq"$fieldname $string", $zconns[$i])
2354 # The big moment: asynchronously retrieve results from all servers
2355 my @elements;
2356 _ZOOM_event_loop(
2357 \@zconns,
2358 \@results,
2359 sub {
2360 my ( $i, $size ) = @_;
2361 for ( my $j = 0 ; $j < $size ; $j++ ) {
2362 my %hashscan;
2363 @hashscan{qw(value cnt)} =
2364 $results[ $i - 1 ]->display_term($j);
2365 push @elements, \%hashscan;
2369 return \@elements;
2373 =head2 _ZOOM_event_loop
2375 _ZOOM_event_loop(\@zconns, \@results, sub {
2376 my ( $i, $size ) = @_;
2377 ....
2378 } );
2380 Processes a ZOOM event loop and passes control to a closure for
2381 processing the results, and destroying the resultsets.
2383 =cut
2385 sub _ZOOM_event_loop {
2386 my ($zconns, $results, $callback) = @_;
2387 while ( ( my $i = ZOOM::event( $zconns ) ) != 0 ) {
2388 my $ev = $zconns->[ $i - 1 ]->last_event();
2389 if ( $ev == ZOOM::Event::ZEND ) {
2390 next unless $results->[ $i - 1 ];
2391 my $size = $results->[ $i - 1 ]->size();
2392 if ( $size > 0 ) {
2393 $callback->($i, $size);
2398 foreach my $result (@$results) {
2399 $result->destroy();
2403 =head2 new_record_from_zebra
2405 Given raw data from a Zebra result set, return a MARC::Record object
2407 This helper function is needed to take into account all the involved
2408 system preferences and configuration variables to properly create the
2409 MARC::Record object.
2411 If we are using GRS-1, then the raw data we get from Zebra should be USMARC
2412 data. If we are using DOM, then it has to be MARCXML.
2414 =cut
2416 sub new_record_from_zebra {
2418 my $server = shift;
2419 my $raw_data = shift;
2420 # Set the default indexing modes
2421 my $index_mode = ( $server eq 'biblioserver' )
2422 ? C4::Context->config('zebra_bib_index_mode') // 'grs1'
2423 : C4::Context->config('zebra_auth_index_mode') // 'dom';
2425 my $marc_record = eval {
2426 if ( $index_mode eq 'dom' ) {
2427 MARC::Record->new_from_xml( $raw_data, 'UTF-8' );
2428 } else {
2429 MARC::Record->new_from_usmarc( $raw_data );
2433 if ($@) {
2434 return;
2435 } else {
2436 return $marc_record;
2441 END { } # module clean-up code here (global destructor)
2444 __END__
2446 =head1 AUTHOR
2448 Koha Development Team <http://koha-community.org/>
2450 =cut