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
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
19 # use warnings; # FIXME
22 use C4::Biblio; # GetMarcFromKohaField, GetBiblioData
23 use C4::Koha; # getFacets
25 use C4::Search::PazPar2;
27 use C4::Dates qw(format_date);
35 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
37 # set the version for version checking
40 $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
45 C4::Search - Functions for searching the Koha catalog.
49 See opac/opac-search.pl or catalogue/search.pl for example of usage
53 This module provides searching functions for Koha's bibliographic databases
70 # make all your functions, whether exported or not;
74 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
76 This function attempts to find duplicate records using a hard-coded, fairly simplistic algorithm
82 my $dbh = C4::Context->dbh;
83 my $result = TransformMarcToKoha( $dbh, $record, '' );
88 my ( $biblionumber, $title );
90 # search duplicate on ISBN, easy and fast..
92 if ( $result->{isbn} ) {
93 $result->{isbn} =~ s/\(.*$//;
94 $result->{isbn} =~ s/\s+$//;
95 $query = "isbn=$result->{isbn}";
98 $result->{title} =~ s /\\//g;
99 $result->{title} =~ s /\"//g;
100 $result->{title} =~ s /\(//g;
101 $result->{title} =~ s /\)//g;
103 # FIXME: instead of removing operators, could just do
104 # quotes around the value
105 $result->{title} =~ s/(and|or|not)//g;
106 $query = "ti,ext=$result->{title}";
107 $query .= " and itemtype=$result->{itemtype}"
108 if ( $result->{itemtype} );
109 if ( $result->{author} ) {
110 $result->{author} =~ s /\\//g;
111 $result->{author} =~ s /\"//g;
112 $result->{author} =~ s /\(//g;
113 $result->{author} =~ s /\)//g;
115 # remove valid operators
116 $result->{author} =~ s/(and|or|not)//g;
117 $query .= " and au,ext=$result->{author}";
121 # FIXME: add error handling
122 my ( $error, $searchresults ) = SimpleSearch($query); # FIXME :: hardcoded !
124 foreach my $possible_duplicate_record (@$searchresults) {
126 MARC::Record->new_from_usmarc($possible_duplicate_record);
127 my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
129 # FIXME :: why 2 $biblionumber ?
131 push @results, $result->{'biblionumber'};
132 push @results, $result->{'title'};
140 ( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers] );
142 This function provides a simple search API on the bibliographic catalog
148 * $query can be a simple keyword or a complete CCL query
149 * @servers is optional. Defaults to biblioserver as found in koha-conf.xml
150 * $offset - If present, represents the number of records at the beggining to omit. Defaults to 0
151 * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
156 * $error is a empty unless an error is detected
157 * \@results is an array of records.
158 * $total_hits is the number of hits that would have been returned with no limit
160 =item C<usage in the script:>
164 my ( $error, $marcresults, $total_hits ) = SimpleSearch($query);
166 if (defined $error) {
167 $template->param(query_error => $error);
168 warn "error: ".$error;
169 output_html_with_http_headers $input, $cookie, $template->output;
173 my $hits = scalar @$marcresults;
176 for my $i (0..$hits) {
178 my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
179 my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
181 #build the hash for the template.
182 $resultsloop{title} = $biblio->{'title'};
183 $resultsloop{subtitle} = $biblio->{'subtitle'};
184 $resultsloop{biblionumber} = $biblio->{'biblionumber'};
185 $resultsloop{author} = $biblio->{'author'};
186 $resultsloop{publishercode} = $biblio->{'publishercode'};
187 $resultsloop{publicationyear} = $biblio->{'publicationyear'};
189 push @results, \%resultsloop;
192 $template->param(result=>\@results);
197 my ( $query, $offset, $max_results, $servers ) = @_;
199 if ( C4::Context->preference('NoZebra') ) {
200 my $result = NZorder( NZanalyse($query) )->{'biblioserver'};
203 && $result->{hits} > 0 ? $result->{'RECORDS'} : [] );
204 return ( undef, $search_result, scalar($result->{hits}) );
207 # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
208 my @servers = defined ( $servers ) ? @$servers : ( "biblioserver" );
214 return ( "No query entered", undef, undef ) unless $query;
216 # Initialize & Search Zebra
217 for ( my $i = 0 ; $i < @servers ; $i++ ) {
219 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
220 $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
221 $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
225 $zconns[$i]->errmsg() . " ("
226 . $zconns[$i]->errcode() . ") "
227 . $zconns[$i]->addinfo() . " "
228 . $zconns[$i]->diagset();
230 return ( $error, undef, undef ) if $zconns[$i]->errcode();
234 # caught a ZOOM::Exception
238 . $@->addinfo() . " "
241 return ( $error, undef, undef );
244 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
245 my $event = $zconns[ $i - 1 ]->last_event();
246 if ( $event == ZOOM::Event::ZEND ) {
248 my $first_record = defined( $offset ) ? $offset+1 : 1;
249 my $hits = $tmpresults[ $i - 1 ]->size();
250 $total_hits += $hits;
251 my $last_record = $hits;
252 if ( defined $max_results && $offset + $max_results < $hits ) {
253 $last_record = $offset + $max_results;
256 for my $j ( $first_record..$last_record ) {
257 my $record = $tmpresults[ $i - 1 ]->record( $j-1 )->raw(); # 0 indexed
258 push @results, $record;
263 foreach my $result (@tmpresults) {
266 foreach my $zoom_query (@zoom_queries) {
267 $zoom_query->destroy();
270 return ( undef, \@results, $total_hits );
276 ( undef, $results_hashref, \@facets_loop ) = getRecords (
278 $koha_query, $simple_query, $sort_by_ref, $servers_ref,
279 $results_per_page, $offset, $expanded_facet, $branches,
283 The all singing, all dancing, multi-server, asynchronous, scanning,
284 searching, record nabbing, facet-building
286 See verbse embedded documentation.
292 $koha_query, $simple_query, $sort_by_ref, $servers_ref,
293 $results_per_page, $offset, $expanded_facet, $branches,
297 my @servers = @$servers_ref;
298 my @sort_by = @$sort_by_ref;
300 # Initialize variables for the ZOOM connection and results object
304 my $results_hashref = ();
306 # Initialize variables for the faceted results objects
307 my $facets_counter = ();
308 my $facets_info = ();
309 my $facets = getFacets();
312 ; # stores the ref to array of hashes for template facets loop
314 ### LOOP THROUGH THE SERVERS
315 for ( my $i = 0 ; $i < @servers ; $i++ ) {
316 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
318 # perform the search, create the results objects
319 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
320 my $query_to_use = ($servers[$i] =~ /biblioserver/) ? $koha_query : $simple_query;
322 #$query_to_use = $simple_query if $scan;
323 warn $simple_query if ( $scan and $DEBUG );
325 # Check if we've got a query_type defined, if so, use it
328 if ($query_type =~ /^ccl/) {
329 $query_to_use =~ s/\:/\=/g; # change : to = last minute (FIXME)
330 $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
331 } elsif ($query_type =~ /^cql/) {
332 $results[$i] = $zconns[$i]->search(new ZOOM::Query::CQL($query_to_use, $zconns[$i]));
333 } elsif ($query_type =~ /^pqf/) {
334 $results[$i] = $zconns[$i]->search(new ZOOM::Query::PQF($query_to_use, $zconns[$i]));
336 warn "Unknown query_type '$query_type'. Results undetermined.";
339 $results[$i] = $zconns[$i]->scan( new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
341 $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
345 warn "WARNING: query problem with $query_to_use " . $@;
348 # Concatenate the sort_by limits and pass them to the results object
349 # Note: sort will override rank
351 foreach my $sort (@sort_by) {
352 if ( $sort eq "author_az" ) {
353 $sort_by .= "1=1003 <i ";
355 elsif ( $sort eq "author_za" ) {
356 $sort_by .= "1=1003 >i ";
358 elsif ( $sort eq "popularity_asc" ) {
359 $sort_by .= "1=9003 <i ";
361 elsif ( $sort eq "popularity_dsc" ) {
362 $sort_by .= "1=9003 >i ";
364 elsif ( $sort eq "call_number_asc" ) {
365 $sort_by .= "1=20 <i ";
367 elsif ( $sort eq "call_number_dsc" ) {
368 $sort_by .= "1=20 >i ";
370 elsif ( $sort eq "pubdate_asc" ) {
371 $sort_by .= "1=31 <i ";
373 elsif ( $sort eq "pubdate_dsc" ) {
374 $sort_by .= "1=31 >i ";
376 elsif ( $sort eq "acqdate_asc" ) {
377 $sort_by .= "1=32 <i ";
379 elsif ( $sort eq "acqdate_dsc" ) {
380 $sort_by .= "1=32 >i ";
382 elsif ( $sort eq "title_az" ) {
383 $sort_by .= "1=4 <i ";
385 elsif ( $sort eq "title_za" ) {
386 $sort_by .= "1=4 >i ";
389 warn "Ignoring unrecognized sort '$sort' requested" if $sort_by;
393 if ( $results[$i]->sort( "yaz", $sort_by ) < 0 ) {
394 warn "WARNING sort $sort_by failed";
397 } # finished looping through servers
399 # The big moment: asynchronously retrieve results from all servers
400 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
401 my $ev = $zconns[ $i - 1 ]->last_event();
402 if ( $ev == ZOOM::Event::ZEND ) {
403 next unless $results[ $i - 1 ];
404 my $size = $results[ $i - 1 ]->size();
408 # loop through the results
409 $results_hash->{'hits'} = $size;
411 if ( $offset + $results_per_page <= $size ) {
412 $times = $offset + $results_per_page;
417 for ( my $j = $offset ; $j < $times ; $j++ ) {
422 ## Check if it's an index scan
424 my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
426 # here we create a minimal MARC record and hand it off to the
427 # template just like a normal result ... perhaps not ideal, but
429 my $tmprecord = MARC::Record->new();
430 $tmprecord->encoding('UTF-8');
434 # the minimal record in author/title (depending on MARC flavour)
435 if (C4::Context->preference("marcflavour") eq "UNIMARC") {
436 $tmptitle = MARC::Field->new('200',' ',' ', a => $term, f => $occ);
437 $tmprecord->append_fields($tmptitle);
439 $tmptitle = MARC::Field->new('245',' ',' ', a => $term,);
440 $tmpauthor = MARC::Field->new('100',' ',' ', a => $occ,);
441 $tmprecord->append_fields($tmptitle);
442 $tmprecord->append_fields($tmpauthor);
444 $results_hash->{'RECORDS'}[$j] = $tmprecord->as_usmarc();
449 $record = $results[ $i - 1 ]->record($j)->raw();
450 warn $results[$i-1]->record($j)->render() ;
452 # warn "RECORD $j:".$record;
453 $results_hash->{'RECORDS'}[$j] = $record;
455 # Fill the facets while we're looping, but only for the biblioserver
456 $facet_record = MARC::Record->new_from_usmarc($record)
457 if $servers[ $i - 1 ] =~ /biblioserver/;
459 #warn $servers[$i-1]."\n".$record; #.$facet_record->title();
461 for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
462 ($facets->[$k]) or next;
463 my @fields = map {$facet_record->field($_)} @{$facets->[$k]->{'tags'}} ;
464 for my $field (@fields) {
465 my @subfields = $field->subfields();
466 for my $subfield (@subfields) {
467 my ( $code, $data ) = @$subfield;
468 ($code eq $facets->[$k]->{'subfield'}) or next;
469 $facets_counter->{ $facets->[$k]->{'link_value'} }->{$data}++;
472 $facets_info->{ $facets->[$k]->{'link_value'} }->{'label_value'} =
473 $facets->[$k]->{'label_value'};
474 $facets_info->{ $facets->[$k]->{'link_value'} }->{'expanded'} =
475 $facets->[$k]->{'expanded'};
480 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
483 # warn "connection ", $i-1, ": $size hits";
484 # warn $results[$i-1]->record(0)->render() if $size > 0;
487 if ( $servers[ $i - 1 ] =~ /biblioserver/ ) {
489 sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
490 keys %$facets_counter )
493 my $number_of_facets;
494 my @this_facets_array;
497 $facets_counter->{$link_value}->{$b}
498 <=> $facets_counter->{$link_value}->{$a}
499 } keys %{ $facets_counter->{$link_value} }
503 if ( ( $number_of_facets < 6 )
504 || ( $expanded_facet eq $link_value )
505 || ( $facets_info->{$link_value}->{'expanded'} ) )
508 # Sanitize the link value ), ( will cause errors with CCL,
509 my $facet_link_value = $one_facet;
510 $facet_link_value =~ s/(\(|\))/ /g;
512 # fix the length that will display in the label,
513 my $facet_label_value = $one_facet;
515 substr( $one_facet, 0, 20 ) . "..."
516 unless length($facet_label_value) <= 20;
518 # if it's a branch, label by the name, not the code,
519 if ( $link_value =~ /branch/ ) {
521 $branches->{$one_facet}->{'branchname'};
524 # but we're down with the whole label being in the link's title.
525 push @this_facets_array, {
526 facet_count => $facets_counter->{$link_value}->{$one_facet},
527 facet_label_value => $facet_label_value,
528 facet_title_value => $one_facet,
529 facet_link_value => $facet_link_value,
530 type_link_value => $link_value,
535 # handle expanded option
536 unless ( $facets_info->{$link_value}->{'expanded'} ) {
538 if ( ( $number_of_facets > 6 )
539 && ( $expanded_facet ne $link_value ) );
542 type_link_value => $link_value,
543 type_id => $link_value . "_id",
544 "type_label_" . $facets_info->{$link_value}->{'label_value'} => 1,
545 facets => \@this_facets_array,
546 expandable => $expandable,
547 expand => $link_value,
548 } unless ( ($facets_info->{$link_value}->{'label_value'} =~ /Libraries/) and (C4::Context->preference('singleBranchMode')) );
553 return ( undef, $results_hashref, \@facets_loop );
558 $koha_query, $simple_query, $sort_by_ref, $servers_ref,
559 $results_per_page, $offset, $expanded_facet, $branches,
563 my $paz = C4::Search::PazPar2->new(C4::Context->config('pazpar2url'));
565 $paz->search($simple_query);
569 my $results_hashref = {};
570 my $stats = XMLin($paz->stat);
571 my $results = XMLin($paz->show($offset, $results_per_page, 'work-title:1'), forcearray => 1);
573 # for a grouped search result, the number of hits
574 # is the number of groups returned; 'bib_hits' will have
575 # the total number of bibs.
576 $results_hashref->{'biblioserver'}->{'hits'} = $results->{'merged'}->[0];
577 $results_hashref->{'biblioserver'}->{'bib_hits'} = $stats->{'hits'};
579 HIT: foreach my $hit (@{ $results->{'hit'} }) {
580 my $recid = $hit->{recid}->[0];
582 my $work_title = $hit->{'md-work-title'}->[0];
584 if (exists $hit->{'md-work-author'}) {
585 $work_author = $hit->{'md-work-author'}->[0];
587 my $group_label = (defined $work_author) ? "$work_title / $work_author" : $work_title;
589 my $result_group = {};
590 $result_group->{'group_label'} = $group_label;
591 $result_group->{'group_merge_key'} = $recid;
594 if (exists $hit->{count}) {
595 $count = $hit->{count}->[0];
597 $result_group->{'group_count'} = $count;
599 for (my $i = 0; $i < $count; $i++) {
600 # FIXME -- may need to worry about diacritics here
601 my $rec = $paz->record($recid, $i);
602 push @{ $result_group->{'RECORDS'} }, $rec;
605 push @{ $results_hashref->{'biblioserver'}->{'GROUPS'} }, $result_group;
608 # pass through facets
609 my $termlist_xml = $paz->termlist('author,subject');
610 my $terms = XMLin($termlist_xml, forcearray => 1);
611 my @facets_loop = ();
612 #die Dumper($results);
613 # foreach my $list (sort keys %{ $terms->{'list'} }) {
615 # foreach my $facet (sort @{ $terms->{'list'}->{$list}->{'term'} } ) {
617 # facet_label_value => $facet->{'name'}->[0],
620 # push @facets_loop, ( {
621 # type_label => $list,
622 # facets => \@facets,
626 return ( undef, $results_hashref, \@facets_loop );
630 sub _remove_stopwords {
631 my ( $operand, $index ) = @_;
632 my @stopwords_removed;
634 # phrase and exact-qualified indexes shouldn't have stopwords removed
635 if ( $index !~ m/phr|ext/ ) {
637 # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
638 # we use IsAlpha unicode definition, to deal correctly with diacritics.
639 # otherwise, a French word like "leçon" woudl be split into "le" "çon", "le"
640 # is a stopword, we'd get "çon" and wouldn't find anything...
642 foreach ( keys %{ C4::Context->stopwords } ) {
643 next if ( $_ =~ /(and|or|not)/ ); # don't remove operators
644 $debug && warn "$_ Dump($operand)";
645 if ( my ($matched) = ($operand =~
646 /([^\X\p{isAlnum}]\Q$_\E[^\X\p{isAlnum}]|[^\X\p{isAlnum}]\Q$_\E$|^\Q$_\E[^\X\p{isAlnum}])/gi))
648 $operand =~ s/\Q$matched\E/ /gi;
649 push @stopwords_removed, $_;
653 return ( $operand, \@stopwords_removed );
657 sub _detect_truncation {
658 my ( $operand, $index ) = @_;
659 my ( @nontruncated, @righttruncated, @lefttruncated, @rightlefttruncated,
662 my @wordlist = split( /\s/, $operand );
663 foreach my $word (@wordlist) {
664 if ( $word =~ s/^\*([^\*]+)\*$/$1/ ) {
665 push @rightlefttruncated, $word;
667 elsif ( $word =~ s/^\*([^\*]+)$/$1/ ) {
668 push @lefttruncated, $word;
670 elsif ( $word =~ s/^([^\*]+)\*$/$1/ ) {
671 push @righttruncated, $word;
673 elsif ( index( $word, "*" ) < 0 ) {
674 push @nontruncated, $word;
677 push @regexpr, $word;
681 \@nontruncated, \@righttruncated, \@lefttruncated,
682 \@rightlefttruncated, \@regexpr
687 sub _build_stemmed_operand {
691 # If operand contains a digit, it is almost certainly an identifier, and should
692 # not be stemmed. This is particularly relevant for ISBNs and ISSNs, which
693 # can contain the letter "X" - for example, _build_stemmend_operand would reduce
694 # "014100018X" to "x ", which for a MARC21 database would bring up irrelevant
695 # results (e.g., "23 x 29 cm." from the 300$c). Bug 2098.
696 return $operand if $operand =~ /\d/;
698 # FIXME: the locale should be set based on the user's language and/or search choice
699 my $stemmer = Lingua::Stem->new( -locale => 'EN-US' );
701 # FIXME: these should be stored in the db so the librarian can modify the behavior
702 $stemmer->add_exceptions(
709 my @words = split( / /, $operand );
710 my $stems = $stemmer->stem(@words);
711 for my $stem (@$stems) {
712 $stemmed_operand .= "$stem";
713 $stemmed_operand .= "?"
714 unless ( $stem =~ /(and$|or$|not$)/ ) || ( length($stem) < 3 );
715 $stemmed_operand .= " ";
717 warn "STEMMED OPERAND: $stemmed_operand" if $DEBUG;
718 return $stemmed_operand;
722 sub _build_weighted_query {
724 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
725 # pretty well but could work much better if we had a smarter query parser
726 my ( $operand, $stemmed_operand, $index ) = @_;
727 my $stemming = C4::Context->preference("QueryStemming") || 0;
728 my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
729 my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
731 my $weighted_query .= "(rk=("; # Specifies that we're applying rank
733 # Keyword, or, no index specified
734 if ( ( $index eq 'kw' ) || ( !$index ) ) {
736 "Title-cover,ext,r1=\"$operand\""; # exact title-cover
737 $weighted_query .= " or ti,ext,r2=\"$operand\""; # exact title
738 $weighted_query .= " or ti,phr,r3=\"$operand\""; # phrase title
739 #$weighted_query .= " or any,ext,r4=$operand"; # exact any
740 #$weighted_query .=" or kw,wrdl,r5=\"$operand\""; # word list any
741 $weighted_query .= " or wrdl,fuzzy,r8=\"$operand\""
742 if $fuzzy_enabled; # add fuzzy, word list
743 $weighted_query .= " or wrdl,right-Truncation,r9=\"$stemmed_operand\""
744 if ( $stemming and $stemmed_operand )
745 ; # add stemming, right truncation
746 $weighted_query .= " or wrdl,r9=\"$operand\"";
748 # embedded sorting: 0 a-z; 1 z-a
749 # $weighted_query .= ") or (sort1,aut=1";
752 # Barcode searches should skip this process
753 elsif ( $index eq 'bc' ) {
754 $weighted_query .= "bc=\"$operand\"";
757 # Authority-number searches should skip this process
758 elsif ( $index eq 'an' ) {
759 $weighted_query .= "an=\"$operand\"";
762 # If the index already has more than one qualifier, wrap the operand
763 # in quotes and pass it back (assumption is that the user knows what they
764 # are doing and won't appreciate us mucking up their query
765 elsif ( $index =~ ',' ) {
766 $weighted_query .= " $index=\"$operand\"";
769 #TODO: build better cases based on specific search indexes
771 $weighted_query .= " $index,ext,r1=\"$operand\""; # exact index
772 #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
773 $weighted_query .= " or $index,phr,r3=\"$operand\""; # phrase index
775 " or $index,rt,wrdl,r3=\"$operand\""; # word list index
778 $weighted_query .= "))"; # close rank specification
779 return $weighted_query;
784 Return an array with available indexes.
806 'Author-personal-bibliography',
815 'Chronological-subdivision',
825 'Conference-name-heading',
826 'Conference-name-see',
827 'Conference-name-seealso',
832 'Corporate-name-heading',
833 'Corporate-name-see',
834 'Corporate-name-seealso',
836 'date-entered-on-file',
837 'Date-of-acquisition',
838 'Date-of-publication',
839 'Dewey-classification',
845 'Geographic-subdivision',
848 'Heading-use-main-or-added-entry',
849 'Heading-use-series-added-entry ',
850 'Heading-use-subject-added-entry',
866 'Local-classification',
869 'Match-heading-see-from',
875 'Name-geographic-heading',
876 'Name-geographic-see',
877 'Name-geographic-seealso',
884 'Personal-name-heading',
886 'Personal-name-seealso',
902 'Subject-heading-thesaurus',
903 'Subject-name-personal',
904 'Subject-subdivision',
913 'Term-genre-form-heading',
914 'Term-genre-form-see',
915 'Term-genre-form-seealso',
921 'Title-uniform-heading',
923 'Title-uniform-seealso',
933 'classification-source',
935 'coded-location-qualifier',
946 'Local-classification',
949 'materials-specified',
958 'replacementpricedate',
974 $simple_query, $query_cgi,
976 $limit_cgi, $limit_desc,
977 $stopwords_removed, $query_type ) = getRecords ( $operators, $operands, $indexes, $limits, $sort_by, $scan);
979 Build queries and limits in CCL, CGI, Human,
980 handle truncation, stemming, field weighting, stopwords, fuzziness, etc.
982 See verbose embedded documentation.
988 my ( $operators, $operands, $indexes, $limits, $sort_by, $scan ) = @_;
990 warn "---------\nEnter buildQuery\n---------" if $DEBUG;
993 my @operators = $operators ? @$operators : ();
994 my @indexes = $indexes ? @$indexes : ();
995 my @operands = $operands ? @$operands : ();
996 my @limits = $limits ? @$limits : ();
997 my @sort_by = $sort_by ? @$sort_by : ();
999 my $stemming = C4::Context->preference("QueryStemming") || 0;
1000 my $auto_truncation = C4::Context->preference("QueryAutoTruncate") || 0;
1001 my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
1002 my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
1003 my $remove_stopwords = C4::Context->preference("QueryRemoveStopwords") || 0;
1005 # no stemming/weight/fuzzy in NoZebra
1006 if ( C4::Context->preference("NoZebra") ) {
1010 $auto_truncation = 0;
1013 my $query = $operands[0];
1014 my $simple_query = $operands[0];
1016 # initialize the variables we're passing back
1025 my $stopwords_removed; # flag to determine if stopwords have been removed
1028 my $cclindexes = getIndexes();
1029 if( $query !~ /\s*ccl=/ ){
1030 for my $index (@$cclindexes){
1031 if($query =~ /($index)(,?\w)*[:=]/){
1035 $query = "ccl=$query" if($cclq);
1038 # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
1040 if ( $query =~ /^ccl=/ ) {
1041 return ( undef, $', $', "q=ccl=$'", $', '', '', '', '', 'ccl' );
1043 if ( $query =~ /^cql=/ ) {
1044 return ( undef, $', $', "q=cql=$'", $', '', '', '', '', 'cql' );
1046 if ( $query =~ /^pqf=/ ) {
1047 return ( undef, $', $', "q=pqf=$'", $', '', '', '', '', 'pqf' );
1050 # pass nested queries directly
1051 # FIXME: need better handling of some of these variables in this case
1052 if ( $query =~ /(\(|\))/ ) {
1054 undef, $query, $simple_query, $query_cgi,
1055 $query, $limit, $limit_cgi, $limit_desc,
1056 $stopwords_removed, 'ccl'
1060 # Form-based queries are non-nested and fixed depth, so we can easily modify the incoming
1061 # query operands and indexes and add stemming, truncation, field weighting, etc.
1062 # Once we do so, we'll end up with a value in $query, just like if we had an
1063 # incoming $query from the user
1066 ; # clear it out so we can populate properly with field-weighted, stemmed, etc. query
1067 my $previous_operand
1068 ; # a flag used to keep track if there was a previous query
1069 # if there was, we can apply the current operator
1071 for ( my $i = 0 ; $i <= @operands ; $i++ ) {
1073 # COMBINE OPERANDS, INDEXES AND OPERATORS
1074 if ( $operands[$i] ) {
1076 # A flag to determine whether or not to add the index to the query
1079 # If the user is sophisticated enough to specify an index, turn off field weighting, stemming, and stopword handling
1080 if ( $operands[$i] =~ /(:|=)/ || $scan ) {
1083 $remove_stopwords = 0;
1085 my $operand = $operands[$i];
1086 my $index = $indexes[$i];
1088 # Add index-specific attributes
1089 # Date of Publication
1090 if ( $index eq 'yr' ) {
1091 $index .= ",st-numeric";
1093 $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1096 # Date of Acquisition
1097 elsif ( $index eq 'acqdate' ) {
1098 $index .= ",st-date-normalized";
1100 $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
1102 # ISBN,ISSN,Standard Number, don't need special treatment
1103 elsif ( $index eq 'nb' || $index eq 'ns' ) {
1106 $stemming, $auto_truncation,
1107 $weight_fields, $fuzzy_enabled,
1109 ) = ( 0, 0, 0, 0, 0 );
1117 # Set default structure attribute (word list)
1119 unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl)/ ) {
1120 $struct_attr = ",wrdl";
1123 # Some helpful index variants
1124 my $index_plus = $index . $struct_attr . ":" if $index;
1125 my $index_plus_comma = $index . $struct_attr . "," if $index;
1128 if ($remove_stopwords) {
1129 ( $operand, $stopwords_removed ) =
1130 _remove_stopwords( $operand, $index );
1131 warn "OPERAND w/out STOPWORDS: >$operand<" if $DEBUG;
1132 warn "REMOVED STOPWORDS: @$stopwords_removed"
1133 if ( $stopwords_removed && $DEBUG );
1136 if ($auto_truncation){
1137 unless ( $index =~ /(st-|phr|ext)/ ) {
1138 #FIXME only valid with LTR scripts
1139 $operand=join(" ",map{
1140 (index($_,"*")>0?"$_":"$_*")
1141 }split (/\s+/,$operand));
1142 warn $operand if $DEBUG;
1147 my $truncated_operand;
1148 my( $nontruncated, $righttruncated, $lefttruncated,
1149 $rightlefttruncated, $regexpr
1150 ) = _detect_truncation( $operand, $index );
1152 "TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<"
1157 scalar(@$righttruncated) + scalar(@$lefttruncated) +
1158 scalar(@$rightlefttruncated) > 0 )
1161 # Don't field weight or add the index to the query, we do it here
1163 undef $weight_fields;
1164 my $previous_truncation_operand;
1165 if (scalar @$nontruncated) {
1166 $truncated_operand .= "$index_plus @$nontruncated ";
1167 $previous_truncation_operand = 1;
1169 if (scalar @$righttruncated) {
1170 $truncated_operand .= "and " if $previous_truncation_operand;
1171 $truncated_operand .= $index_plus_comma . "rtrn:@$righttruncated ";
1172 $previous_truncation_operand = 1;
1174 if (scalar @$lefttruncated) {
1175 $truncated_operand .= "and " if $previous_truncation_operand;
1176 $truncated_operand .= $index_plus_comma . "ltrn:@$lefttruncated ";
1177 $previous_truncation_operand = 1;
1179 if (scalar @$rightlefttruncated) {
1180 $truncated_operand .= "and " if $previous_truncation_operand;
1181 $truncated_operand .= $index_plus_comma . "rltrn:@$rightlefttruncated ";
1182 $previous_truncation_operand = 1;
1185 $operand = $truncated_operand if $truncated_operand;
1186 warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
1189 my $stemmed_operand;
1190 $stemmed_operand = _build_stemmed_operand($operand) if $stemming;
1192 warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
1194 # Handle Field Weighting
1195 my $weighted_operand;
1196 if ($weight_fields) {
1197 $weighted_operand = _build_weighted_query( $operand, $stemmed_operand, $index );
1198 $operand = $weighted_operand;
1202 warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
1204 # If there's a previous operand, we need to add an operator
1205 if ($previous_operand) {
1207 # User-specified operator
1208 if ( $operators[ $i - 1 ] ) {
1209 $query .= " $operators[$i-1] ";
1210 $query .= " $index_plus " unless $indexes_set;
1211 $query .= " $operand";
1212 $query_cgi .= "&op=$operators[$i-1]";
1213 $query_cgi .= "&idx=$index" if $index;
1214 $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1216 " $operators[$i-1] $index_plus $operands[$i]";
1219 # Default operator is and
1222 $query .= "$index_plus " unless $indexes_set;
1223 $query .= "$operand";
1224 $query_cgi .= "&op=and&idx=$index" if $index;
1225 $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1226 $query_desc .= " and $index_plus $operands[$i]";
1230 # There isn't a pervious operand, don't need an operator
1233 # Field-weighted queries already have indexes set
1234 $query .= " $index_plus " unless $indexes_set;
1236 $query_desc .= " $index_plus $operands[$i]";
1237 $query_cgi .= "&idx=$index" if $index;
1238 $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1239 $previous_operand = 1;
1244 warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1247 my $group_OR_limits;
1248 my $availability_limit;
1249 foreach my $this_limit (@limits) {
1250 if ( $this_limit =~ /available/ ) {
1252 # 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1254 # all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1255 $availability_limit .=
1256 "( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and (lost,st-numeric=0) )"; #or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
1257 $limit_cgi .= "&limit=available";
1261 # group_OR_limits, prefixed by mc-
1262 # OR every member of the group
1263 elsif ( $this_limit =~ /mc/ ) {
1264 $group_OR_limits .= " or " if $group_OR_limits;
1265 $limit_desc .= " or " if $group_OR_limits;
1266 $group_OR_limits .= "$this_limit";
1267 $limit_cgi .= "&limit=$this_limit";
1268 $limit_desc .= " $this_limit";
1271 # Regular old limits
1273 $limit .= " and " if $limit || $query;
1274 $limit .= "$this_limit";
1275 $limit_cgi .= "&limit=$this_limit";
1276 if ($this_limit =~ /^branch:(.+)/) {
1277 my $branchcode = $1;
1278 my $branchname = GetBranchName($branchcode);
1279 if (defined $branchname) {
1280 $limit_desc .= " branch:$branchname";
1282 $limit_desc .= " $this_limit";
1285 $limit_desc .= " $this_limit";
1289 if ($group_OR_limits) {
1290 $limit .= " and " if ( $query || $limit );
1291 $limit .= "($group_OR_limits)";
1293 if ($availability_limit) {
1294 $limit .= " and " if ( $query || $limit );
1295 $limit .= "($availability_limit)";
1298 # Normalize the query and limit strings
1301 for ( $query, $query_desc, $limit, $limit_desc ) {
1302 s/ / /g; # remove extra spaces
1303 s/^ //g; # remove any beginning spaces
1304 s/ $//g; # remove any ending spaces
1305 s/==/=/g; # remove double == from query
1307 $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1309 for ($query_cgi,$simple_query) {
1312 # append the limit to the query
1313 $query .= " " . $limit;
1317 warn "QUERY:" . $query;
1318 warn "QUERY CGI:" . $query_cgi;
1319 warn "QUERY DESC:" . $query_desc;
1320 warn "LIMIT:" . $limit;
1321 warn "LIMIT CGI:" . $limit_cgi;
1322 warn "LIMIT DESC:" . $limit_desc;
1323 warn "---------\nLeave buildQuery\n---------";
1326 undef, $query, $simple_query, $query_cgi,
1327 $query_desc, $limit, $limit_cgi, $limit_desc,
1328 $stopwords_removed, $query_type
1332 =head2 searchResults
1334 Format results in a form suitable for passing to the template
1338 # IMO this subroutine is pretty messy still -- it's responsible for
1339 # building the HTML output for the template
1341 my ( $searchdesc, $hits, $results_per_page, $offset, $scan, @marcresults ) = @_;
1342 my $dbh = C4::Context->dbh;
1344 #Build branchnames hash
1346 #get branch information.....
1348 my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
1350 while ( my $bdata = $bsth->fetchrow_hashref ) {
1351 $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
1353 # FIXME - We build an authorised values hash here, using the default framework
1354 # though it is possible to have different authvals for different fws.
1356 my $shelflocations =GetKohaAuthorisedValues('items.location','');
1358 # get notforloan authorised value list (see $shelflocations FIXME)
1359 my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
1361 #Build itemtype hash
1362 #find itemtype & itemtype image
1366 "SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes"
1369 while ( my $bdata = $bsth->fetchrow_hashref ) {
1370 foreach (qw(description imageurl summary notforloan)) {
1371 $itemtypes{ $bdata->{'itemtype'} }->{$_} = $bdata->{$_};
1375 #search item field code
1378 "SELECT tagfield FROM marc_subfield_structure WHERE kohafield LIKE 'items.itemnumber'"
1381 my ($itemtag) = $sth->fetchrow;
1383 ## find column names of items related to MARC
1384 my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
1386 my %subfieldstosearch;
1387 while ( ( my $column ) = $sth2->fetchrow ) {
1388 my ( $tagfield, $tagsubfield ) =
1389 &GetMarcFromKohaField( "items." . $column, "" );
1390 $subfieldstosearch{$column} = $tagsubfield;
1393 # handle which records to actually retrieve
1395 if ( $hits && $offset + $results_per_page <= $hits ) {
1396 $times = $offset + $results_per_page;
1399 $times = $hits; # FIXME: if $hits is undefined, why do we want to equal it?
1401 my $marcflavour = C4::Context->preference("marcflavour");
1403 # We get the biblionumber position in MARC
1404 my ($bibliotag,$bibliosubf)=GetMarcFromKohaField('biblio.biblionumber','');
1407 # loop through all of the records we've retrieved
1408 for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1409 my $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
1410 SetUTF8Flag($marcrecord);
1415 $biblionumber = $marcrecord->field($bibliotag) ? $marcrecord->field($bibliotag)->data : undef;
1417 $biblionumber = $marcrecord->subfield($bibliotag,$bibliosubf);
1419 $fw = (defined $biblionumber) ? GetFrameworkCode($biblionumber) : '';
1422 my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, $fw );
1423 $oldbiblio->{subtitle} = GetRecordValue('subtitle', $marcrecord, $fw);
1424 $oldbiblio->{result_number} = $i + 1;
1426 # add imageurl to itemtype if there is one
1427 $oldbiblio->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1429 $oldbiblio->{'authorised_value_images'} = C4::Items::get_authorised_value_images( C4::Biblio::get_biblio_authorised_values( $oldbiblio->{'biblionumber'}, $marcrecord ) );
1430 (my $aisbn) = $oldbiblio->{isbn} =~ /([\d-]*[X]*)/;
1432 $oldbiblio->{amazonisbn} = $aisbn;
1433 $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1434 $oldbiblio->{normalized_upc} = GetNormalizedUPC($marcrecord,$marcflavour);
1435 $oldbiblio->{normalized_ean} = GetNormalizedEAN($marcrecord,$marcflavour);
1436 $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
1437 $oldbiblio->{normalized_isbn} = GetNormalizedISBN(undef,$marcrecord,$marcflavour);
1438 $oldbiblio->{content_identifier_exists} = 1 if ($oldbiblio->{normalized_isbn} or $oldbiblio->{normalized_oclc} or $oldbiblio->{normalized_ean} or $oldbiblio->{normalized_upc});
1439 $oldbiblio->{edition} = $oldbiblio->{editionstatement};
1440 $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1441 # Build summary if there is one (the summary is defined in the itemtypes table)
1442 # FIXME: is this used anywhere, I think it can be commented out? -- JF
1443 if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1444 my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1445 my @fields = $marcrecord->fields();
1448 foreach my $line ( "$summary\n" =~ /(.*)\n/g ){
1450 foreach my $tag ( $line =~ /\[(\d{3}[\w|\d])\]/ ) {
1451 $tag =~ /(.{3})(.)/;
1452 if($marcrecord->field($1)){
1453 my @abc = $marcrecord->field($1)->subfield($2);
1454 $tags->{$tag} = $#abc + 1 ;
1458 # We catch how many times to repeat this line
1460 foreach my $tag (keys(%$tags)){
1461 $max = $tags->{$tag} if($tags->{$tag} > $max);
1464 # we replace, and repeat each line
1465 for (my $i = 0 ; $i < $max ; $i++){
1466 my $newline = $line;
1468 foreach my $tag ( $newline =~ /\[(\d{3}[\w|\d])\]/g ) {
1469 $tag =~ /(.{3})(.)/;
1471 if($marcrecord->field($1)){
1472 my @repl = $marcrecord->field($1)->subfield($2);
1473 my $subfieldvalue = $repl[$i];
1475 if (! utf8::is_utf8($subfieldvalue)) {
1476 utf8::decode($subfieldvalue);
1479 $newline =~ s/\[$tag\]/$subfieldvalue/g;
1482 $newsummary .= "$newline\n";
1486 $newsummary =~ s/\[(.*?)]//g;
1487 $newsummary =~ s/\n/<br\/>/g;
1488 $oldbiblio->{summary} = $newsummary;
1491 # Pull out the items fields
1492 my @fields = $marcrecord->field($itemtag);
1494 # Setting item statuses for display
1495 my @available_items_loop;
1496 my @onloan_items_loop;
1497 my @notforloan_items_loop;
1498 my @other_items_loop;
1500 my $available_items;
1502 my $notforloan_items;
1505 my $ordered_count = 0;
1506 my $available_count = 0;
1507 my $onloan_count = 0;
1508 my $notforloan_count = 0;
1509 my $longoverdue_count = 0;
1510 my $other_count = 0;
1511 my $wthdrawn_count = 0;
1512 my $itemlost_count = 0;
1513 my $itembinding_count = 0;
1514 my $itemdamaged_count = 0;
1515 my $item_in_transit_count = 0;
1516 my $can_place_holds = 0;
1517 my $items_count = scalar(@fields);
1519 ( C4::Context->preference('maxItemsinSearchResults') )
1520 ? C4::Context->preference('maxItemsinSearchResults') - 1
1523 # loop through every item
1524 foreach my $field (@fields) {
1527 # populate the items hash
1528 foreach my $code ( keys %subfieldstosearch ) {
1529 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1531 my $hbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch' : 'holdingbranch';
1532 my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
1533 # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
1534 if ($item->{$hbranch}) {
1535 $item->{'branchname'} = $branches{$item->{$hbranch}};
1537 elsif ($item->{$otherbranch}) { # Last resort
1538 $item->{'branchname'} = $branches{$item->{$otherbranch}};
1541 ($item->{'reserved'}) = C4::Reserves::CheckReserves($item->{itemnumber});
1543 my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
1544 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
1545 if ( $item->{onloan} or $item->{reserved} ) {
1547 my $key = $prefix . $item->{onloan} . $item->{barcode};
1548 $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
1549 $onloan_items->{$key}->{count}++ if $item->{$hbranch};
1550 $onloan_items->{$key}->{branchname} = $item->{branchname};
1551 $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1552 $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
1553 $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1554 $onloan_items->{$key}->{barcode} = $item->{barcode};
1555 $onloan_items->{$key}->{reserved} = $item->{reserved};
1556 # if something's checked out and lost, mark it as 'long overdue'
1557 if ( $item->{itemlost} ) {
1558 $onloan_items->{$prefix}->{longoverdue}++;
1559 $longoverdue_count++;
1560 } else { # can place holds as long as item isn't lost
1561 $can_place_holds = 1;
1565 # items not on loan, but still unavailable ( lost, withdrawn, damaged )
1569 if ( $item->{notforloan} == -1 ) {
1573 # is item in transit?
1574 my $transfertwhen = '';
1575 my ($transfertfrom, $transfertto);
1577 unless ($item->{wthdrawn}
1578 || $item->{itemlost}
1580 || $item->{notforloan}
1581 || $items_count > 20) {
1583 # A couple heuristics to limit how many times
1584 # we query the database for item transfer information, sacrificing
1585 # accuracy in some cases for speed;
1587 # 1. don't query if item has one of the other statuses
1588 # 2. don't check transit status if the bib has
1589 # more than 20 items
1591 # FIXME: to avoid having the query the database like this, and to make
1592 # the in transit status count as unavailable for search limiting,
1593 # should map transit status to record indexed in Zebra.
1595 ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
1598 # item is withdrawn, lost or damaged
1599 if ( $item->{wthdrawn}
1600 || $item->{itemlost}
1602 || $item->{notforloan}
1603 || $item->{reserved}
1604 || ($transfertwhen ne ''))
1606 $wthdrawn_count++ if $item->{wthdrawn};
1607 $itemlost_count++ if $item->{itemlost};
1608 $itemdamaged_count++ if $item->{damaged};
1609 $item_in_transit_count++ if $transfertwhen ne '';
1610 $item->{status} = $item->{wthdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
1612 my $key = $prefix . $item->{status};
1614 foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber)) {
1615 if($item->{notforloan} == 1){
1616 $notforloan_items->{$key}->{$_} = $item->{$_};
1618 $other_items->{$key}->{$_} = $item->{$_};
1621 if($item->{notforloan} == 1){
1622 $notforloan_count++;
1624 $notforloan_items->{$key}->{intransit} = ($transfertwhen ne '') ? 1 : 0;
1625 $notforloan_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
1626 $notforloan_items->{$key}->{count}++ if $item->{$hbranch};
1627 $notforloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1628 $notforloan_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1629 $notforloan_items->{$key}->{barcode} = $item->{barcode};
1633 $other_items->{$key}->{intransit} = ($transfertwhen ne '') ? 1 : 0;
1634 $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
1635 $other_items->{$key}->{count}++ if $item->{$hbranch};
1636 $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1637 $other_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1638 $other_items->{$key}->{barcode} = $item->{barcode};
1644 $can_place_holds = 1;
1646 $available_items->{$prefix}->{count}++ if $item->{$hbranch};
1647 foreach (qw(branchname itemcallnumber barcode)) {
1648 $available_items->{$prefix}->{$_} = $item->{$_};
1650 $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
1651 $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1654 } # notforloan, item level and biblioitem level
1655 my ( $availableitemscount, $onloanitemscount, $notforloanitemscount,$otheritemscount );
1657 ( C4::Context->preference('maxItemsinSearchResults') )
1658 ? C4::Context->preference('maxItemsinSearchResults') - 1
1660 for my $key ( sort keys %$onloan_items ) {
1661 (++$onloanitemscount > $maxitems) and last;
1662 push @onloan_items_loop, $onloan_items->{$key};
1664 for my $key ( sort keys %$other_items ) {
1665 (++$otheritemscount > $maxitems) and last;
1666 push @other_items_loop, $other_items->{$key};
1668 for my $key ( sort keys %$notforloan_items ) {
1669 (++$notforloanitemscount > $maxitems) and last;
1670 push @notforloan_items_loop, $notforloan_items->{$key};
1672 for my $key ( sort keys %$available_items ) {
1673 (++$availableitemscount > $maxitems) and last;
1674 push @available_items_loop, $available_items->{$key}
1677 # XSLT processing of some stuff
1678 if (C4::Context->preference("XSLTResultsDisplay") && !$scan) {
1679 $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display(
1680 $oldbiblio->{biblionumber}, $marcrecord, C4::Context->preference("XSLTResultsDisplay") );
1683 # last check for norequest : if itemtype is notforloan, it can't be reserved either, whatever the items
1684 $can_place_holds = 0 if $itemtypes{ $oldbiblio->{itemtype} }->{notforloan};
1685 $oldbiblio->{norequests} = 1 unless $can_place_holds;
1686 $oldbiblio->{itemsplural} = 1 if $items_count > 1;
1687 $oldbiblio->{items_count} = $items_count;
1688 $oldbiblio->{available_items_loop} = \@available_items_loop;
1689 $oldbiblio->{notforloan_items_loop}= \@notforloan_items_loop;
1690 $oldbiblio->{onloan_items_loop} = \@onloan_items_loop;
1691 $oldbiblio->{other_items_loop} = \@other_items_loop;
1692 $oldbiblio->{availablecount} = $available_count;
1693 $oldbiblio->{availableplural} = 1 if $available_count > 1;
1694 $oldbiblio->{onloancount} = $onloan_count;
1695 $oldbiblio->{onloanplural} = 1 if $onloan_count > 1;
1696 $oldbiblio->{notforloancount} = $notforloan_count;
1697 $oldbiblio->{othercount} = $other_count;
1698 $oldbiblio->{otherplural} = 1 if $other_count > 1;
1699 $oldbiblio->{wthdrawncount} = $wthdrawn_count;
1700 $oldbiblio->{itemlostcount} = $itemlost_count;
1701 $oldbiblio->{damagedcount} = $itemdamaged_count;
1702 $oldbiblio->{intransitcount} = $item_in_transit_count;
1703 $oldbiblio->{orderedcount} = $ordered_count;
1704 $oldbiblio->{isbn} =~
1705 s/-//g; # deleting - in isbn to enable amazon content
1706 push( @newresults, $oldbiblio );
1711 =head2 SearchAcquisitions
1712 Search for acquisitions
1715 sub SearchAcquisitions{
1716 my ($datebegin, $dateend, $itemtypes,$criteria, $orderby) = @_;
1718 my $dbh=C4::Context->dbh;
1719 # Variable initialization
1723 LEFT JOIN biblioitems ON biblioitems.biblionumber=biblio.biblionumber
1724 LEFT JOIN items ON items.biblionumber=biblio.biblionumber
1725 WHERE dateaccessioned BETWEEN ? AND ?
1728 my (@params,@loopcriteria);
1730 push @params, $datebegin->output("iso");
1731 push @params, $dateend->output("iso");
1733 if (scalar(@$itemtypes)>0 and $criteria ne "itemtype" ){
1734 if(C4::Context->preference("item-level_itypes")){
1735 $str .= "AND items.itype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1737 $str .= "AND biblioitems.itemtype IN (?".( ',?' x scalar @$itemtypes - 1 ).") ";
1739 push @params, @$itemtypes;
1742 if ($criteria =~/itemtype/){
1743 if(C4::Context->preference("item-level_itypes")){
1744 $str .= "AND items.itype=? ";
1746 $str .= "AND biblioitems.itemtype=? ";
1749 if(scalar(@$itemtypes) == 0){
1750 my $itypes = GetItemTypes();
1751 for my $key (keys %$itypes){
1752 push @$itemtypes, $key;
1756 @loopcriteria= @$itemtypes;
1757 }elsif ($criteria=~/itemcallnumber/){
1758 $str .= "AND (items.itemcallnumber LIKE CONCAT(?,'%')
1759 OR items.itemcallnumber is NULL
1760 OR items.itemcallnumber = '')";
1762 @loopcriteria = ("AA".."ZZ", "") unless (scalar(@loopcriteria)>0);
1764 $str .= "AND biblio.title LIKE CONCAT(?,'%') ";
1765 @loopcriteria = ("A".."z") unless (scalar(@loopcriteria)>0);
1768 if ($orderby =~ /date_desc/){
1769 $str.=" ORDER BY dateaccessioned DESC";
1771 $str.=" ORDER BY title";
1774 my $qdataacquisitions=$dbh->prepare($str);
1776 my @loopacquisitions;
1777 foreach my $value(@loopcriteria){
1778 push @params,$value;
1780 $cell{"title"}=$value;
1781 $cell{"titlecode"}=$value;
1783 eval{$qdataacquisitions->execute(@params);};
1785 if ($@){ warn "recentacquisitions Error :$@";}
1788 while (my $data=$qdataacquisitions->fetchrow_hashref){
1789 push @loopdata, {"summary"=>GetBiblioSummary( $data->{'marcxml'} ) };
1791 $cell{"loopdata"}=\@loopdata;
1793 push @loopacquisitions,\%cell if (scalar(@{$cell{loopdata}})>0);
1796 $qdataacquisitions->finish;
1797 return \@loopacquisitions;
1800 #----------------------------------------------------------------------
1802 # Non-Zebra GetRecords#
1803 #----------------------------------------------------------------------
1807 NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1813 $query, $simple_query, $sort_by_ref, $servers_ref,
1814 $results_per_page, $offset, $expanded_facet, $branches,
1817 warn "query =$query" if $DEBUG;
1818 my $result = NZanalyse($query);
1819 warn "results =$result" if $DEBUG;
1821 NZorder( $result, @$sort_by_ref[0], $results_per_page, $offset ),
1827 NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1828 the list is built from an inverted index in the nozebra SQL table
1829 note that title is here only for convenience : the sorting will be very fast when requested on title
1830 if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1835 my ( $string, $server ) = @_;
1836 # warn "---------" if $DEBUG;
1837 warn " NZanalyse" if $DEBUG;
1838 # warn "---------" if $DEBUG;
1840 # $server contains biblioserver or authorities, depending on what we search on.
1841 #warn "querying : $string on $server";
1842 $server = 'biblioserver' unless $server;
1844 # if we have a ", replace the content to discard temporarily any and/or/not inside
1846 if ( $string =~ /"/ ) {
1847 $string =~ s/"(.*?)"/__X__/;
1849 warn "commacontent : $commacontent" if $DEBUG;
1852 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1853 # then, call again NZanalyse with $left and $right
1854 # (recursive until we find a leaf (=> something without and/or/not)
1855 # delete repeated operator... Would then go in infinite loop
1856 while ( $string =~ s/( and| or| not| AND| OR| NOT)\1/$1/g ) {
1859 #process parenthesis before.
1860 if ( $string =~ /^\s*\((.*)\)(( and | or | not | AND | OR | NOT )(.*))?/ ) {
1863 my $operator = lc($3); # FIXME: and/or/not are operators, not operands
1865 "dealing w/parenthesis before recursive sub call. left :$left operator:$operator right:$right"
1867 my $leftresult = NZanalyse( $left, $server );
1869 my $rightresult = NZanalyse( $right, $server );
1871 # OK, we have the results for right and left part of the query
1872 # depending of operand, intersect, union or exclude both lists
1873 # to get a result list
1874 if ( $operator eq ' and ' ) {
1875 return NZoperatorAND($leftresult,$rightresult);
1877 elsif ( $operator eq ' or ' ) {
1879 # just merge the 2 strings
1880 return $leftresult . $rightresult;
1882 elsif ( $operator eq ' not ' ) {
1883 return NZoperatorNOT($leftresult,$rightresult);
1887 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1891 warn "string :" . $string if $DEBUG;
1895 if ($string =~ /(.*?)( and | or | not | AND | OR | NOT )(.*)/) {
1898 $operator = lc($2); # FIXME: and/or/not are operators, not operands
1900 warn "no parenthesis. left : $left operator: $operator right: $right"
1903 # it's not a leaf, we have a and/or/not
1906 # reintroduce comma content if needed
1907 $right =~ s/__X__/"$commacontent"/ if $commacontent;
1908 $left =~ s/__X__/"$commacontent"/ if $commacontent;
1909 warn "node : $left / $operator / $right\n" if $DEBUG;
1910 my $leftresult = NZanalyse( $left, $server );
1911 my $rightresult = NZanalyse( $right, $server );
1912 warn " leftresult : $leftresult" if $DEBUG;
1913 warn " rightresult : $rightresult" if $DEBUG;
1914 # OK, we have the results for right and left part of the query
1915 # depending of operand, intersect, union or exclude both lists
1916 # to get a result list
1917 if ( $operator eq ' and ' ) {
1919 return NZoperatorAND($leftresult,$rightresult);
1921 elsif ( $operator eq ' or ' ) {
1923 # just merge the 2 strings
1924 return $leftresult . $rightresult;
1926 elsif ( $operator eq ' not ' ) {
1927 return NZoperatorNOT($leftresult,$rightresult);
1931 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1932 die "error : operand unknown : $operator for $string";
1935 # it's a leaf, do the real SQL query and return the result
1938 $string =~ s/__X__/"$commacontent"/ if $commacontent;
1939 $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|&|\+|\*|\// /g;
1940 #remove trailing blank at the beginning
1942 warn "leaf:$string" if $DEBUG;
1944 # parse the string in in operator/operand/value again
1948 if ($string =~ /(.*)(>=|<=)(.*)/) {
1955 # warn "handling leaf... left:$left operator:$operator right:$right"
1957 unless ($operator) {
1958 if ($string =~ /(.*)(>|<|=)(.*)/) {
1963 "handling unless (operator)... left:$left operator:$operator right:$right"
1971 # strip adv, zebra keywords, currently not handled in nozebra: wrdl, ext, phr...
1974 # automatic replace for short operators
1975 $left = 'title' if $left =~ '^ti$';
1976 $left = 'author' if $left =~ '^au$';
1977 $left = 'publisher' if $left =~ '^pb$';
1978 $left = 'subject' if $left =~ '^su$';
1979 $left = 'koha-Auth-Number' if $left =~ '^an$';
1980 $left = 'keyword' if $left =~ '^kw$';
1981 $left = 'itemtype' if $left =~ '^mc$'; # Fix for Bug 2599 - Search limits not working for NoZebra
1982 warn "handling leaf... left:$left operator:$operator right:$right" if $DEBUG;
1983 my $dbh = C4::Context->dbh;
1984 if ( $operator && $left ne 'keyword' ) {
1985 #do a specific search
1986 $operator = 'LIKE' if $operator eq '=' and $right =~ /%/;
1987 my $sth = $dbh->prepare(
1988 "SELECT biblionumbers,value FROM nozebra WHERE server=? AND indexname=? AND value $operator ?"
1990 warn "$left / $operator / $right\n" if $DEBUG;
1992 # split each word, query the DB and build the biblionumbers result
1993 #sanitizing leftpart
1994 $left =~ s/^\s+|\s+$//;
1995 foreach ( split / /, $right ) {
1997 $_ =~ s/^\s+|\s+$//;
1999 warn "EXECUTE : $server, $left, $_" if $DEBUG;
2000 $sth->execute( $server, $left, $_ )
2001 or warn "execute failed: $!";
2002 while ( my ( $line, $value ) = $sth->fetchrow ) {
2004 # if we are dealing with a numeric value, use only numeric results (in case of >=, <=, > or <)
2005 # otherwise, fill the result
2006 $biblionumbers .= $line
2007 unless ( $right =~ /^\d+$/ && $value =~ /\D/ );
2008 warn "result : $value "
2009 . ( $right =~ /\d/ ) . "=="
2010 . ( $value =~ /\D/?$line:"" ) if $DEBUG; #= $line";
2013 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2015 warn "NZAND" if $DEBUG;
2016 $results = NZoperatorAND($biblionumbers,$results);
2018 $results = $biblionumbers;
2023 #do a complete search (all indexes), if index='kw' do complete search too.
2024 my $sth = $dbh->prepare(
2025 "SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?"
2028 # split each word, query the DB and build the biblionumbers result
2029 foreach ( split (/ /, $string )) {
2030 next if C4::Context->stopwords->{ uc($_) }; # skip if stopword
2031 warn "search on all indexes on $_" if $DEBUG;
2034 $sth->execute( $server, $_ );
2035 while ( my $line = $sth->fetchrow ) {
2036 $biblionumbers .= $line;
2039 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
2041 $results = NZoperatorAND($biblionumbers,$results);
2044 warn "NEW RES for $_ = $biblionumbers" if $DEBUG;
2045 $results = $biblionumbers;
2049 warn "return : $results for LEAF : $string" if $DEBUG;
2052 warn "---------\nLeave NZanalyse\n---------" if $DEBUG;
2056 my ($rightresult, $leftresult)=@_;
2058 my @leftresult = split (/;/, $leftresult);
2059 warn " @leftresult / $rightresult \n" if $DEBUG;
2061 # my @rightresult = split /;/,$leftresult;
2064 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
2065 # the result is stored twice, to have the same weight for AND than OR.
2066 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
2067 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
2068 foreach (@leftresult) {
2071 ( $value, $countvalue ) = ( $1, $2 ) if ($value=~/(.*)-(\d+)$/);
2072 if ( $rightresult =~ /\Q$value\E-(\d+);/ ) {
2073 $countvalue = ( $1 > $countvalue ? $countvalue : $1 );
2075 "$value-$countvalue;$value-$countvalue;";
2078 warn "NZAND DONE : $finalresult \n" if $DEBUG;
2079 return $finalresult;
2083 my ($rightresult, $leftresult)=@_;
2084 return $rightresult.$leftresult;
2088 my ($leftresult, $rightresult)=@_;
2090 my @leftresult = split /;/, $leftresult;
2092 # my @rightresult = split /;/,$leftresult;
2094 foreach (@leftresult) {
2096 $value=$1 if $value=~m/(.*)-\d+$/;
2097 unless ($rightresult =~ "$value-") {
2098 $finalresult .= "$_;";
2101 return $finalresult;
2106 $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
2113 my ( $biblionumbers, $ordering, $results_per_page, $offset ) = @_;
2114 warn "biblionumbers = $biblionumbers and ordering = $ordering\n" if $DEBUG;
2116 # order title asc by default
2117 # $ordering = '1=36 <i' unless $ordering;
2118 $results_per_page = 20 unless $results_per_page;
2119 $offset = 0 unless $offset;
2120 my $dbh = C4::Context->dbh;
2123 # order by POPULARITY
2125 if ( $ordering =~ /popularity/ ) {
2129 # popularity is not in MARC record, it's builded from a specific query
2131 $dbh->prepare("select sum(issues) from items where biblionumber=?");
2132 foreach ( split (/;/, $biblionumbers )) {
2133 my ( $biblionumber, $title ) = split (/,/, $_);
2134 $result{$biblionumber} = GetMarcBiblio($biblionumber);
2135 $sth->execute($biblionumber);
2136 my $popularity = $sth->fetchrow || 0;
2138 # hint : the key is popularity.title because we can have
2139 # many results with the same popularity. In this case, sub-ordering is done by title
2140 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
2141 # (un-frequent, I agree, but we won't forget anything that way ;-)
2142 $popularity{ sprintf( "%10d", $popularity ) . $title
2143 . $biblionumber } = $biblionumber;
2146 # sort the hash and return the same structure as GetRecords (Zebra querying)
2149 if ( $ordering eq 'popularity_dsc' ) { # sort popularity DESC
2150 foreach my $key ( sort { $b cmp $a } ( keys %popularity ) ) {
2151 $result_hash->{'RECORDS'}[ $numbers++ ] =
2152 $result{ $popularity{$key} }->as_usmarc();
2155 else { # sort popularity ASC
2156 foreach my $key ( sort ( keys %popularity ) ) {
2157 $result_hash->{'RECORDS'}[ $numbers++ ] =
2158 $result{ $popularity{$key} }->as_usmarc();
2161 my $finalresult = ();
2162 $result_hash->{'hits'} = $numbers;
2163 $finalresult->{'biblioserver'} = $result_hash;
2164 return $finalresult;
2170 elsif ( $ordering =~ /author/ ) {
2172 foreach ( split (/;/, $biblionumbers )) {
2173 my ( $biblionumber, $title ) = split (/,/, $_);
2174 my $record = GetMarcBiblio($biblionumber);
2176 if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2177 $author = $record->subfield( '200', 'f' );
2178 $author = $record->subfield( '700', 'a' ) unless $author;
2181 $author = $record->subfield( '100', 'a' );
2184 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2185 # and we don't want to get only 1 result for each of them !!!
2186 $result{ $author . $biblionumber } = $record;
2189 # sort the hash and return the same structure as GetRecords (Zebra querying)
2192 if ( $ordering eq 'author_za' ) { # sort by author desc
2193 foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2194 $result_hash->{'RECORDS'}[ $numbers++ ] =
2195 $result{$key}->as_usmarc();
2198 else { # sort by author ASC
2199 foreach my $key ( sort ( keys %result ) ) {
2200 $result_hash->{'RECORDS'}[ $numbers++ ] =
2201 $result{$key}->as_usmarc();
2204 my $finalresult = ();
2205 $result_hash->{'hits'} = $numbers;
2206 $finalresult->{'biblioserver'} = $result_hash;
2207 return $finalresult;
2210 # ORDER BY callnumber
2213 elsif ( $ordering =~ /callnumber/ ) {
2215 foreach ( split (/;/, $biblionumbers )) {
2216 my ( $biblionumber, $title ) = split (/,/, $_);
2217 my $record = GetMarcBiblio($biblionumber);
2219 my ( $callnumber_tag, $callnumber_subfield ) =
2220 GetMarcFromKohaField( 'items.itemcallnumber','' );
2221 ( $callnumber_tag, $callnumber_subfield ) =
2222 GetMarcFromKohaField('biblioitems.callnumber','')
2223 unless $callnumber_tag;
2224 if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
2225 $callnumber = $record->subfield( '200', 'f' );
2228 $callnumber = $record->subfield( '100', 'a' );
2231 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2232 # and we don't want to get only 1 result for each of them !!!
2233 $result{ $callnumber . $biblionumber } = $record;
2236 # sort the hash and return the same structure as GetRecords (Zebra querying)
2239 if ( $ordering eq 'call_number_dsc' ) { # sort by title desc
2240 foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2241 $result_hash->{'RECORDS'}[ $numbers++ ] =
2242 $result{$key}->as_usmarc();
2245 else { # sort by title ASC
2246 foreach my $key ( sort { $a cmp $b } ( keys %result ) ) {
2247 $result_hash->{'RECORDS'}[ $numbers++ ] =
2248 $result{$key}->as_usmarc();
2251 my $finalresult = ();
2252 $result_hash->{'hits'} = $numbers;
2253 $finalresult->{'biblioserver'} = $result_hash;
2254 return $finalresult;
2256 elsif ( $ordering =~ /pubdate/ ) { #pub year
2258 foreach ( split (/;/, $biblionumbers )) {
2259 my ( $biblionumber, $title ) = split (/,/, $_);
2260 my $record = GetMarcBiblio($biblionumber);
2261 my ( $publicationyear_tag, $publicationyear_subfield ) =
2262 GetMarcFromKohaField( 'biblioitems.publicationyear', '' );
2263 my $publicationyear =
2264 $record->subfield( $publicationyear_tag,
2265 $publicationyear_subfield );
2267 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2268 # and we don't want to get only 1 result for each of them !!!
2269 $result{ $publicationyear . $biblionumber } = $record;
2272 # sort the hash and return the same structure as GetRecords (Zebra querying)
2275 if ( $ordering eq 'pubdate_dsc' ) { # sort by pubyear desc
2276 foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2277 $result_hash->{'RECORDS'}[ $numbers++ ] =
2278 $result{$key}->as_usmarc();
2281 else { # sort by pub year ASC
2282 foreach my $key ( sort ( keys %result ) ) {
2283 $result_hash->{'RECORDS'}[ $numbers++ ] =
2284 $result{$key}->as_usmarc();
2287 my $finalresult = ();
2288 $result_hash->{'hits'} = $numbers;
2289 $finalresult->{'biblioserver'} = $result_hash;
2290 return $finalresult;
2296 elsif ( $ordering =~ /title/ ) {
2298 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
2300 foreach ( split (/;/, $biblionumbers )) {
2301 my ( $biblionumber, $title ) = split (/,/, $_);
2303 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
2304 # and we don't want to get only 1 result for each of them !!!
2305 # hint & speed improvement : we can order without reading the record
2306 # so order, and read records only for the requested page !
2307 $result{ $title . $biblionumber } = $biblionumber;
2310 # sort the hash and return the same structure as GetRecords (Zebra querying)
2313 if ( $ordering eq 'title_az' ) { # sort by title desc
2314 foreach my $key ( sort ( keys %result ) ) {
2315 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2318 else { # sort by title ASC
2319 foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2320 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2324 # limit the $results_per_page to result size if it's more
2325 $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2327 # for the requested page, replace biblionumber by the complete record
2328 # speed improvement : avoid reading too much things
2330 my $counter = $offset ;
2331 $counter <= $offset + $results_per_page ;
2335 $result_hash->{'RECORDS'}[$counter] =
2336 GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc;
2338 my $finalresult = ();
2339 $result_hash->{'hits'} = $numbers;
2340 $finalresult->{'biblioserver'} = $result_hash;
2341 return $finalresult;
2348 # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
2351 foreach ( split /;/, $biblionumbers ) {
2352 my ( $biblionumber, $title ) = split /,/, $_;
2353 $title =~ /(.*)-(\d)/;
2358 # note that we + the ranking because ranking is calculated on weight of EACH term requested.
2359 # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
2360 # biblio N has ranking = 6
2361 $count_ranking{$biblionumber} += $ranking;
2364 # build the result by "inverting" the count_ranking hash
2365 # hing : as usual, we don't order by ranking only, to avoid having only 1 result for each rank. We build an hash on concat(ranking,biblionumber) instead
2367 foreach ( keys %count_ranking ) {
2368 $result{ sprintf( "%10d", $count_ranking{$_} ) . '-' . $_ } = $_;
2371 # sort the hash and return the same structure as GetRecords (Zebra querying)
2374 foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
2375 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
2378 # limit the $results_per_page to result size if it's more
2379 $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2381 # for the requested page, replace biblionumber by the complete record
2382 # speed improvement : avoid reading too much things
2384 my $counter = $offset ;
2385 $counter <= $offset + $results_per_page ;
2389 $result_hash->{'RECORDS'}[$counter] =
2390 GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc
2391 if $result_hash->{'RECORDS'}[$counter];
2393 my $finalresult = ();
2394 $result_hash->{'hits'} = $numbers;
2395 $finalresult->{'biblioserver'} = $result_hash;
2396 return $finalresult;
2400 =head2 enabled_staff_search_views
2402 %hash = enabled_staff_search_views()
2404 This function returns a hash that contains three flags obtained from the system
2405 preferences, used to determine whether a particular staff search results view
2410 =item C<Output arg:>
2412 * $hash{can_view_MARC} is true only if the MARC view is enabled
2413 * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2414 * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2416 =item C<usage in the script:>
2420 $template->param ( C4::Search::enabled_staff_search_views );
2424 sub enabled_staff_search_views
2427 can_view_MARC => C4::Context->preference('viewMARC'), # 1 if the staff search allows the MARC view
2428 can_view_ISBD => C4::Context->preference('viewISBD'), # 1 if the staff search allows the ISBD view
2429 can_view_labeledMARC => C4::Context->preference('viewLabeledMARC'), # 1 if the staff search allows the Labeled MARC view
2433 =head2 enabled_opac_search_views
2435 %hash = enabled_opac_search_views()
2437 This function returns a hash that contains two flags obtained from the system
2438 preferences, used to determine whether a particular opac search results view
2443 =item C<Output arg:>
2445 * $hash{can_view_MARC} is true only if the MARC view is enabled
2446 * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2448 =item C<usage in the script:>
2452 $template->param ( C4::Search::enabled_opac_search_views );
2456 sub enabled_opac_search_views
2459 can_opac_view_MARC => C4::Context->preference('OPACviewMARC'), # 1 if the opac search allows the MARC view
2460 can_opac_view_ISBD => C4::Context->preference('OPACviewISBD'), # 1 if the opac search allows the ISBD view
2465 =head2 z3950_search_args
2467 $arrayref = z3950_search_args($matchpoints)
2469 This function returns an array reference that contains the search parameters to be
2470 passed to the Z39.50 search script (z3950_search.pl). The array elements
2471 are hash refs whose keys are name, value and encvalue, and whose values are the
2472 name of a search parameter, the value of that search parameter and the URL encoded
2473 value of that parameter.
2475 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2477 The search parameter values are obtained from the bibliographic record whose
2478 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2480 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2481 a general purpose search argument. In this case, the returned array contains only
2482 entry: the key is 'title' and the value and encvalue are derived from $matchpoints.
2484 If a search parameter value is undefined or empty, it is not included in the returned
2487 The returned array reference may be passed directly to the template parameters.
2491 =item C<Output arg:>
2493 * $array containing hash refs as described above
2495 =item C<usage in the script:>
2499 $data = Biblio::GetBiblioData($bibno);
2500 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2504 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2508 sub z3950_search_args {
2510 $bibrec = { title => $bibrec } if !ref $bibrec;
2512 for my $field (qw/ lccn isbn issn title author dewey subject /)
2514 my $encvalue = URI::Escape::uri_escape_utf8($bibrec->{$field});
2515 push @$array, { name=>$field, value=>$bibrec->{$field}, encvalue=>$encvalue } if defined $bibrec->{$field};
2521 END { } # module clean-up code here (global destructor)
2528 Koha Developement team <info@koha.org>