Corrections to ensure message list appears in the proper box (Bug 3668).
[koha.git] / C4 / Search.pm
blobf72bb115714b79549911994ace3afede52b7b71c
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
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::XSLT;
29 use C4::Branch;
30 use URI::Escape;
32 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $DEBUG);
34 # set the version for version checking
35 BEGIN {
36 $VERSION = 3.01;
37 $DEBUG = ($ENV{DEBUG}) ? 1 : 0;
40 =head1 NAME
42 C4::Search - Functions for searching the Koha catalog.
44 =head1 SYNOPSIS
46 See opac/opac-search.pl or catalogue/search.pl for example of usage
48 =head1 DESCRIPTION
50 This module provides searching functions for Koha's bibliographic databases
52 =head1 FUNCTIONS
54 =cut
56 @ISA = qw(Exporter);
57 @EXPORT = qw(
58 &FindDuplicate
59 &SimpleSearch
60 &searchResults
61 &getRecords
62 &buildQuery
63 &NZgetRecords
66 # make all your functions, whether exported or not;
68 =head2 FindDuplicate
70 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
72 This function attempts to find duplicate records using a hard-coded, fairly simplistic algorithm
74 =cut
76 sub FindDuplicate {
77 my ($record) = @_;
78 my $dbh = C4::Context->dbh;
79 my $result = TransformMarcToKoha( $dbh, $record, '' );
80 my $sth;
81 my $query;
82 my $search;
83 my $type;
84 my ( $biblionumber, $title );
86 # search duplicate on ISBN, easy and fast..
87 # ... normalize first
88 if ( $result->{isbn} ) {
89 $result->{isbn} =~ s/\(.*$//;
90 $result->{isbn} =~ s/\s+$//;
91 $query = "isbn=$result->{isbn}";
93 else {
94 $result->{title} =~ s /\\//g;
95 $result->{title} =~ s /\"//g;
96 $result->{title} =~ s /\(//g;
97 $result->{title} =~ s /\)//g;
99 # FIXME: instead of removing operators, could just do
100 # quotes around the value
101 $result->{title} =~ s/(and|or|not)//g;
102 $query = "ti,ext=$result->{title}";
103 $query .= " and itemtype=$result->{itemtype}"
104 if ( $result->{itemtype} );
105 if ( $result->{author} ) {
106 $result->{author} =~ s /\\//g;
107 $result->{author} =~ s /\"//g;
108 $result->{author} =~ s /\(//g;
109 $result->{author} =~ s /\)//g;
111 # remove valid operators
112 $result->{author} =~ s/(and|or|not)//g;
113 $query .= " and au,ext=$result->{author}";
117 # FIXME: add error handling
118 my ( $error, $searchresults ) = SimpleSearch($query); # FIXME :: hardcoded !
119 my @results;
120 foreach my $possible_duplicate_record (@$searchresults) {
121 my $marcrecord =
122 MARC::Record->new_from_usmarc($possible_duplicate_record);
123 my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
125 # FIXME :: why 2 $biblionumber ?
126 if ($result) {
127 push @results, $result->{'biblionumber'};
128 push @results, $result->{'title'};
131 return @results;
134 =head2 SimpleSearch
136 ( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers] );
138 This function provides a simple search API on the bibliographic catalog
140 =over 2
142 =item C<input arg:>
144 * $query can be a simple keyword or a complete CCL query
145 * @servers is optional. Defaults to biblioserver as found in koha-conf.xml
146 * $offset - If present, represents the number of records at the beggining to omit. Defaults to 0
147 * $max_results - if present, determines the maximum number of records to fetch. undef is All. defaults to undef.
150 =item C<Output:>
152 * $error is a empty unless an error is detected
153 * \@results is an array of records.
154 * $total_hits is the number of hits that would have been returned with no limit
156 =item C<usage in the script:>
158 =back
160 my ( $error, $marcresults, $total_hits ) = SimpleSearch($query);
162 if (defined $error) {
163 $template->param(query_error => $error);
164 warn "error: ".$error;
165 output_html_with_http_headers $input, $cookie, $template->output;
166 exit;
169 my $hits = scalar @$marcresults;
170 my @results;
172 for my $i (0..$hits) {
173 my %resultsloop;
174 my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
175 my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
177 #build the hash for the template.
178 $resultsloop{title} = $biblio->{'title'};
179 $resultsloop{subtitle} = $biblio->{'subtitle'};
180 $resultsloop{biblionumber} = $biblio->{'biblionumber'};
181 $resultsloop{author} = $biblio->{'author'};
182 $resultsloop{publishercode} = $biblio->{'publishercode'};
183 $resultsloop{publicationyear} = $biblio->{'publicationyear'};
185 push @results, \%resultsloop;
188 $template->param(result=>\@results);
190 =cut
192 sub SimpleSearch {
193 my ( $query, $offset, $max_results, $servers ) = @_;
195 if ( C4::Context->preference('NoZebra') ) {
196 my $result = NZorder( NZanalyse($query) )->{'biblioserver'};
197 my $search_result =
198 ( $result->{hits}
199 && $result->{hits} > 0 ? $result->{'RECORDS'} : [] );
200 return ( undef, $search_result, scalar($result->{hits}) );
202 else {
203 # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
204 my @servers = defined ( $servers ) ? @$servers : ( "biblioserver" );
205 my @results;
206 my @zoom_queries;
207 my @tmpresults;
208 my @zconns;
209 my $total_hits;
210 return ( "No query entered", undef, undef ) unless $query;
212 # Initialize & Search Zebra
213 for ( my $i = 0 ; $i < @servers ; $i++ ) {
214 eval {
215 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
216 $zoom_queries[$i] = new ZOOM::Query::CCL2RPN( $query, $zconns[$i]);
217 $tmpresults[$i] = $zconns[$i]->search( $zoom_queries[$i] );
219 # error handling
220 my $error =
221 $zconns[$i]->errmsg() . " ("
222 . $zconns[$i]->errcode() . ") "
223 . $zconns[$i]->addinfo() . " "
224 . $zconns[$i]->diagset();
226 return ( $error, undef, undef ) if $zconns[$i]->errcode();
228 if ($@) {
230 # caught a ZOOM::Exception
231 my $error =
232 $@->message() . " ("
233 . $@->code() . ") "
234 . $@->addinfo() . " "
235 . $@->diagset();
236 warn $error;
237 return ( $error, undef, undef );
240 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
241 my $event = $zconns[ $i - 1 ]->last_event();
242 if ( $event == ZOOM::Event::ZEND ) {
244 my $first_record = defined( $offset ) ? $offset+1 : 1;
245 my $hits = $tmpresults[ $i - 1 ]->size();
246 $total_hits += $hits;
247 my $last_record = $hits;
248 if ( defined $max_results && $offset + $max_results < $hits ) {
249 $last_record = $offset + $max_results;
252 for my $j ( $first_record..$last_record ) {
253 my $record = $tmpresults[ $i - 1 ]->record( $j-1 )->raw(); # 0 indexed
254 push @results, $record;
259 foreach my $result (@tmpresults) {
260 $result->destroy();
262 foreach my $zoom_query (@zoom_queries) {
263 $zoom_query->destroy();
266 return ( undef, \@results, $total_hits );
270 =head2 getRecords
272 ( undef, $results_hashref, \@facets_loop ) = getRecords (
274 $koha_query, $simple_query, $sort_by_ref, $servers_ref,
275 $results_per_page, $offset, $expanded_facet, $branches,
276 $query_type, $scan
279 The all singing, all dancing, multi-server, asynchronous, scanning,
280 searching, record nabbing, facet-building
282 See verbse embedded documentation.
284 =cut
286 sub getRecords {
287 my (
288 $koha_query, $simple_query, $sort_by_ref, $servers_ref,
289 $results_per_page, $offset, $expanded_facet, $branches,
290 $query_type, $scan
291 ) = @_;
293 my @servers = @$servers_ref;
294 my @sort_by = @$sort_by_ref;
296 # Initialize variables for the ZOOM connection and results object
297 my $zconn;
298 my @zconns;
299 my @results;
300 my $results_hashref = ();
302 # Initialize variables for the faceted results objects
303 my $facets_counter = ();
304 my $facets_info = ();
305 my $facets = getFacets();
307 my @facets_loop; # stores the ref to array of hashes for template facets loop
309 ### LOOP THROUGH THE SERVERS
310 for ( my $i = 0 ; $i < @servers ; $i++ ) {
311 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
313 # perform the search, create the results objects
314 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
315 my $query_to_use = ($servers[$i] =~ /biblioserver/) ? $koha_query : $simple_query;
317 #$query_to_use = $simple_query if $scan;
318 warn $simple_query if ( $scan and $DEBUG );
320 # Check if we've got a query_type defined, if so, use it
321 eval {
322 if ($query_type) {
323 if ($query_type =~ /^ccl/) {
324 $query_to_use =~ s/\:/\=/g; # change : to = last minute (FIXME)
325 $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
326 } elsif ($query_type =~ /^cql/) {
327 $results[$i] = $zconns[$i]->search(new ZOOM::Query::CQL($query_to_use, $zconns[$i]));
328 } elsif ($query_type =~ /^pqf/) {
329 $results[$i] = $zconns[$i]->search(new ZOOM::Query::PQF($query_to_use, $zconns[$i]));
330 } else {
331 warn "Unknown query_type '$query_type'. Results undetermined.";
333 } elsif ($scan) {
334 $results[$i] = $zconns[$i]->scan( new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
335 } else {
336 $results[$i] = $zconns[$i]->search(new ZOOM::Query::CCL2RPN($query_to_use, $zconns[$i]));
339 if ($@) {
340 warn "WARNING: query problem with $query_to_use " . $@;
343 # Concatenate the sort_by limits and pass them to the results object
344 # Note: sort will override rank
345 my $sort_by;
346 foreach my $sort (@sort_by) {
347 if ( $sort eq "author_az" ) {
348 $sort_by .= "1=1003 <i ";
350 elsif ( $sort eq "author_za" ) {
351 $sort_by .= "1=1003 >i ";
353 elsif ( $sort eq "popularity_asc" ) {
354 $sort_by .= "1=9003 <i ";
356 elsif ( $sort eq "popularity_dsc" ) {
357 $sort_by .= "1=9003 >i ";
359 elsif ( $sort eq "call_number_asc" ) {
360 $sort_by .= "1=20 <i ";
362 elsif ( $sort eq "call_number_dsc" ) {
363 $sort_by .= "1=20 >i ";
365 elsif ( $sort eq "pubdate_asc" ) {
366 $sort_by .= "1=31 <i ";
368 elsif ( $sort eq "pubdate_dsc" ) {
369 $sort_by .= "1=31 >i ";
371 elsif ( $sort eq "acqdate_asc" ) {
372 $sort_by .= "1=32 <i ";
374 elsif ( $sort eq "acqdate_dsc" ) {
375 $sort_by .= "1=32 >i ";
377 elsif ( $sort eq "title_az" ) {
378 $sort_by .= "1=4 <i ";
380 elsif ( $sort eq "title_za" ) {
381 $sort_by .= "1=4 >i ";
383 else {
384 warn "Ignoring unrecognized sort '$sort' requested" if $sort_by;
387 if ($sort_by) {
388 if ( $results[$i]->sort( "yaz", $sort_by ) < 0 ) {
389 warn "WARNING sort $sort_by failed";
392 } # finished looping through servers
394 # The big moment: asynchronously retrieve results from all servers
395 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
396 my $ev = $zconns[ $i - 1 ]->last_event();
397 if ( $ev == ZOOM::Event::ZEND ) {
398 next unless $results[ $i - 1 ];
399 my $size = $results[ $i - 1 ]->size();
400 if ( $size > 0 ) {
401 my $results_hash;
403 # loop through the results
404 $results_hash->{'hits'} = $size;
405 my $times;
406 if ( $offset + $results_per_page <= $size ) {
407 $times = $offset + $results_per_page;
409 else {
410 $times = $size;
412 for ( my $j = $offset ; $j < $times ; $j++ ) {
413 my $records_hash;
414 my $record;
415 my $facet_record;
417 ## Check if it's an index scan
418 if ($scan) {
419 my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
421 # here we create a minimal MARC record and hand it off to the
422 # template just like a normal result ... perhaps not ideal, but
423 # it works for now
424 my $tmprecord = MARC::Record->new();
425 $tmprecord->encoding('UTF-8');
426 my $tmptitle;
427 my $tmpauthor;
429 # the minimal record in author/title (depending on MARC flavour)
430 if (C4::Context->preference("marcflavour") eq "UNIMARC") {
431 $tmptitle = MARC::Field->new('200',' ',' ', a => $term, f => $occ);
432 $tmprecord->append_fields($tmptitle);
433 } else {
434 $tmptitle = MARC::Field->new('245',' ',' ', a => $term,);
435 $tmpauthor = MARC::Field->new('100',' ',' ', a => $occ,);
436 $tmprecord->append_fields($tmptitle);
437 $tmprecord->append_fields($tmpauthor);
439 $results_hash->{'RECORDS'}[$j] = $tmprecord->as_usmarc();
442 # not an index scan
443 else {
444 $record = $results[ $i - 1 ]->record($j)->raw();
446 # warn "RECORD $j:".$record;
447 $results_hash->{'RECORDS'}[$j] = $record;
449 # Fill the facets while we're looping, but only for the biblioserver
450 $facet_record = MARC::Record->new_from_usmarc($record)
451 if $servers[ $i - 1 ] =~ /biblioserver/;
453 #warn $servers[$i-1]."\n".$record; #.$facet_record->title();
454 if ($facet_record) {
455 for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
456 ($facets->[$k]) or next;
457 my @fields = map {$facet_record->field($_)} @{$facets->[$k]->{'tags'}} ;
458 for my $field (@fields) {
459 my @subfields = $field->subfields();
460 for my $subfield (@subfields) {
461 my ( $code, $data ) = @$subfield;
462 ($code eq $facets->[$k]->{'subfield'}) or next;
463 $facets_counter->{ $facets->[$k]->{'link_value'} }->{$data}++;
466 $facets_info->{ $facets->[$k]->{'link_value'} }->{'label_value'} =
467 $facets->[$k]->{'label_value'};
468 $facets_info->{ $facets->[$k]->{'link_value'} }->{'expanded'} =
469 $facets->[$k]->{'expanded'};
474 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
477 # warn "connection ", $i-1, ": $size hits";
478 # warn $results[$i-1]->record(0)->render() if $size > 0;
480 # BUILD FACETS
481 if ( $servers[ $i - 1 ] =~ /biblioserver/ ) {
482 for my $link_value (
483 sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
484 keys %$facets_counter )
486 my $expandable;
487 my $number_of_facets;
488 my @this_facets_array;
489 for my $one_facet (
490 sort {
491 $facets_counter->{$link_value}->{$b}
492 <=> $facets_counter->{$link_value}->{$a}
493 } keys %{ $facets_counter->{$link_value} }
496 $number_of_facets++;
497 if ( ( $number_of_facets < 6 )
498 || ( $expanded_facet eq $link_value )
499 || ( $facets_info->{$link_value}->{'expanded'} ) )
502 # Sanitize the link value ), ( will cause errors with CCL,
503 my $facet_link_value = $one_facet;
504 $facet_link_value =~ s/(\(|\))/ /g;
506 # fix the length that will display in the label,
507 my $facet_label_value = $one_facet;
508 $facet_label_value =
509 substr( $one_facet, 0, 20 ) . "..."
510 unless length($facet_label_value) <= 20;
512 # if it's a branch, label by the name, not the code,
513 if ( $link_value =~ /branch/ ) {
514 $facet_label_value =
515 $branches->{$one_facet}->{'branchname'};
518 # but we're down with the whole label being in the link's title.
519 push @this_facets_array, {
520 facet_count => $facets_counter->{$link_value}->{$one_facet},
521 facet_label_value => $facet_label_value,
522 facet_title_value => $one_facet,
523 facet_link_value => $facet_link_value,
524 type_link_value => $link_value,
529 # handle expanded option
530 unless ( $facets_info->{$link_value}->{'expanded'} ) {
531 $expandable = 1
532 if ( ( $number_of_facets > 6 )
533 && ( $expanded_facet ne $link_value ) );
535 push @facets_loop, {
536 type_link_value => $link_value,
537 type_id => $link_value . "_id",
538 "type_label_" . $facets_info->{$link_value}->{'label_value'} => 1,
539 facets => \@this_facets_array,
540 expandable => $expandable,
541 expand => $link_value,
542 } unless ( ($facets_info->{$link_value}->{'label_value'} =~ /Libraries/) and (C4::Context->preference('singleBranchMode')) );
547 return ( undef, $results_hashref, \@facets_loop );
550 sub pazGetRecords {
551 my (
552 $koha_query, $simple_query, $sort_by_ref, $servers_ref,
553 $results_per_page, $offset, $expanded_facet, $branches,
554 $query_type, $scan
555 ) = @_;
557 my $paz = C4::Search::PazPar2->new(C4::Context->config('pazpar2url'));
558 $paz->init();
559 $paz->search($simple_query);
560 sleep 1; # FIXME: WHY?
562 # do results
563 my $results_hashref = {};
564 my $stats = XMLin($paz->stat);
565 my $results = XMLin($paz->show($offset, $results_per_page, 'work-title:1'), forcearray => 1);
567 # for a grouped search result, the number of hits
568 # is the number of groups returned; 'bib_hits' will have
569 # the total number of bibs.
570 $results_hashref->{'biblioserver'}->{'hits'} = $results->{'merged'}->[0];
571 $results_hashref->{'biblioserver'}->{'bib_hits'} = $stats->{'hits'};
573 HIT: foreach my $hit (@{ $results->{'hit'} }) {
574 my $recid = $hit->{recid}->[0];
576 my $work_title = $hit->{'md-work-title'}->[0];
577 my $work_author;
578 if (exists $hit->{'md-work-author'}) {
579 $work_author = $hit->{'md-work-author'}->[0];
581 my $group_label = (defined $work_author) ? "$work_title / $work_author" : $work_title;
583 my $result_group = {};
584 $result_group->{'group_label'} = $group_label;
585 $result_group->{'group_merge_key'} = $recid;
587 my $count = 1;
588 if (exists $hit->{count}) {
589 $count = $hit->{count}->[0];
591 $result_group->{'group_count'} = $count;
593 for (my $i = 0; $i < $count; $i++) {
594 # FIXME -- may need to worry about diacritics here
595 my $rec = $paz->record($recid, $i);
596 push @{ $result_group->{'RECORDS'} }, $rec;
599 push @{ $results_hashref->{'biblioserver'}->{'GROUPS'} }, $result_group;
602 # pass through facets
603 my $termlist_xml = $paz->termlist('author,subject');
604 my $terms = XMLin($termlist_xml, forcearray => 1);
605 my @facets_loop = ();
606 #die Dumper($results);
607 # foreach my $list (sort keys %{ $terms->{'list'} }) {
608 # my @facets = ();
609 # foreach my $facet (sort @{ $terms->{'list'}->{$list}->{'term'} } ) {
610 # push @facets, {
611 # facet_label_value => $facet->{'name'}->[0],
612 # };
614 # push @facets_loop, ( {
615 # type_label => $list,
616 # facets => \@facets,
617 # } );
620 return ( undef, $results_hashref, \@facets_loop );
623 # STOPWORDS
624 sub _remove_stopwords {
625 my ( $operand, $index ) = @_;
626 my @stopwords_removed;
628 # phrase and exact-qualified indexes shouldn't have stopwords removed
629 if ( $index !~ m/phr|ext/ ) {
631 # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
632 # we use IsAlpha unicode definition, to deal correctly with diacritics.
633 # otherwise, a French word like "leçon" woudl be split into "le" "çon", "le"
634 # is a stopword, we'd get "çon" and wouldn't find anything...
635 foreach ( keys %{ C4::Context->stopwords } ) {
636 next if ( $_ =~ /(and|or|not)/ ); # don't remove operators
637 if ( my ($matched) = ($operand =~
638 /(\P{IsAlnum}\Q$_\E\P{IsAlnum}|^\Q$_\E\P{IsAlnum}|\P{IsAlnum}\Q$_\E$|^\Q$_\E$)/gi) )
640 $operand =~ s/\Q$matched\E/ /gi;
641 push @stopwords_removed, $_;
645 return ( $operand, \@stopwords_removed );
648 # TRUNCATION
649 sub _detect_truncation {
650 my ( $operand, $index ) = @_;
651 my ( @nontruncated, @righttruncated, @lefttruncated, @rightlefttruncated,
652 @regexpr );
653 $operand =~ s/^ //g;
654 my @wordlist = split( /\s/, $operand );
655 foreach my $word (@wordlist) {
656 if ( $word =~ s/^\*([^\*]+)\*$/$1/ ) {
657 push @rightlefttruncated, $word;
659 elsif ( $word =~ s/^\*([^\*]+)$/$1/ ) {
660 push @lefttruncated, $word;
662 elsif ( $word =~ s/^([^\*]+)\*$/$1/ ) {
663 push @righttruncated, $word;
665 elsif ( index( $word, "*" ) < 0 ) {
666 push @nontruncated, $word;
668 else {
669 push @regexpr, $word;
672 return (
673 \@nontruncated, \@righttruncated, \@lefttruncated,
674 \@rightlefttruncated, \@regexpr
678 # STEMMING
679 sub _build_stemmed_operand {
680 my ($operand) = @_;
681 my $stemmed_operand;
683 # If operand contains a digit, it is almost certainly an identifier, and should
684 # not be stemmed. This is particularly relevant for ISBNs and ISSNs, which
685 # can contain the letter "X" - for example, _build_stemmend_operand would reduce
686 # "014100018X" to "x ", which for a MARC21 database would bring up irrelevant
687 # results (e.g., "23 x 29 cm." from the 300$c). Bug 2098.
688 return $operand if $operand =~ /\d/;
690 # FIXME: the locale should be set based on the user's language and/or search choice
691 my $stemmer = Lingua::Stem->new( -locale => 'EN-US' );
693 # FIXME: these should be stored in the db so the librarian can modify the behavior
694 $stemmer->add_exceptions(
696 'and' => 'and',
697 'or' => 'or',
698 'not' => 'not',
701 my @words = split( / /, $operand );
702 my $stems = $stemmer->stem(@words);
703 for my $stem (@$stems) {
704 $stemmed_operand .= "$stem";
705 $stemmed_operand .= "?"
706 unless ( $stem =~ /(and$|or$|not$)/ ) || ( length($stem) < 3 );
707 $stemmed_operand .= " ";
709 warn "STEMMED OPERAND: $stemmed_operand" if $DEBUG;
710 return $stemmed_operand;
713 # FIELD WEIGHTING
714 sub _build_weighted_query {
716 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
717 # pretty well but could work much better if we had a smarter query parser
718 my ( $operand, $stemmed_operand, $index ) = @_;
719 my $stemming = C4::Context->preference("QueryStemming") || 0;
720 my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
721 my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
723 my $weighted_query .= "(rk=("; # Specifies that we're applying rank
725 # Keyword, or, no index specified
726 if ( ( $index eq 'kw' ) || ( !$index ) ) {
727 $weighted_query .=
728 "Title-cover,ext,r1=\"$operand\""; # exact title-cover
729 $weighted_query .= " or ti,ext,r2=\"$operand\""; # exact title
730 $weighted_query .= " or ti,phr,r3=\"$operand\""; # phrase title
731 #$weighted_query .= " or any,ext,r4=$operand"; # exact any
732 #$weighted_query .=" or kw,wrdl,r5=\"$operand\""; # word list any
733 $weighted_query .= " or wrdl,fuzzy,r8=\"$operand\""
734 if $fuzzy_enabled; # add fuzzy, word list
735 $weighted_query .= " or wrdl,right-Truncation,r9=\"$stemmed_operand\""
736 if ( $stemming and $stemmed_operand )
737 ; # add stemming, right truncation
738 $weighted_query .= " or wrdl,r9=\"$operand\"";
740 # embedded sorting: 0 a-z; 1 z-a
741 # $weighted_query .= ") or (sort1,aut=1";
744 # Barcode searches should skip this process
745 elsif ( $index eq 'bc' ) {
746 $weighted_query .= "bc=\"$operand\"";
749 # Authority-number searches should skip this process
750 elsif ( $index eq 'an' ) {
751 $weighted_query .= "an=\"$operand\"";
754 # If the index already has more than one qualifier, wrap the operand
755 # in quotes and pass it back (assumption is that the user knows what they
756 # are doing and won't appreciate us mucking up their query
757 elsif ( $index =~ ',' ) {
758 $weighted_query .= " $index=\"$operand\"";
761 #TODO: build better cases based on specific search indexes
762 else {
763 $weighted_query .= " $index,ext,r1=\"$operand\""; # exact index
764 #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
765 $weighted_query .= " or $index,phr,r3=\"$operand\""; # phrase index
766 $weighted_query .=
767 " or $index,rt,wrdl,r3=\"$operand\""; # word list index
770 $weighted_query .= "))"; # close rank specification
771 return $weighted_query;
774 =head2 buildQuery
776 ( $error, $query,
777 $simple_query, $query_cgi,
778 $query_desc, $limit,
779 $limit_cgi, $limit_desc,
780 $stopwords_removed, $query_type ) = getRecords ( $operators, $operands, $indexes, $limits, $sort_by, $scan);
782 Build queries and limits in CCL, CGI, Human,
783 handle truncation, stemming, field weighting, stopwords, fuzziness, etc.
785 See verbose embedded documentation.
788 =cut
790 sub buildQuery {
791 my ( $operators, $operands, $indexes, $limits, $sort_by, $scan ) = @_;
793 warn "---------\nEnter buildQuery\n---------" if $DEBUG;
795 # dereference
796 my @operators = $operators ? @$operators : ();
797 my @indexes = $indexes ? @$indexes : ();
798 my @operands = $operands ? @$operands : ();
799 my @limits = $limits ? @$limits : ();
800 my @sort_by = $sort_by ? @$sort_by : ();
802 my $stemming = C4::Context->preference("QueryStemming") || 0;
803 my $auto_truncation = C4::Context->preference("QueryAutoTruncate") || 0;
804 my $weight_fields = C4::Context->preference("QueryWeightFields") || 0;
805 my $fuzzy_enabled = C4::Context->preference("QueryFuzzy") || 0;
806 my $remove_stopwords = C4::Context->preference("QueryRemoveStopwords") || 0;
808 # no stemming/weight/fuzzy in NoZebra
809 if ( C4::Context->preference("NoZebra") ) {
810 $stemming = 0;
811 $weight_fields = 0;
812 $fuzzy_enabled = 0;
815 my $query = $operands[0];
816 my $simple_query = $operands[0];
818 # initialize the variables we're passing back
819 my $query_cgi;
820 my $query_desc;
821 my $query_type;
823 my $limit;
824 my $limit_cgi;
825 my $limit_desc;
827 my $stopwords_removed; # flag to determine if stopwords have been removed
829 # for handling ccl, cql, pqf queries in diagnostic mode, skip the rest of the steps
830 # DIAGNOSTIC ONLY!!
831 if ( $query =~ /^ccl=/ ) {
832 return ( undef, $', $', "q=ccl=$'", $', '', '', '', '', 'ccl' );
834 if ( $query =~ /^cql=/ ) {
835 return ( undef, $', $', "q=cql=$'", $', '', '', '', '', 'cql' );
837 if ( $query =~ /^pqf=/ ) {
838 return ( undef, $', $', "q=pqf=$'", $', '', '', '', '', 'pqf' );
841 # pass nested queries directly
842 # FIXME: need better handling of some of these variables in this case
843 if ( $query =~ /(\(|\))/ ) {
844 return (
845 undef, $query, $simple_query, $query_cgi,
846 $query, $limit, $limit_cgi, $limit_desc,
847 $stopwords_removed, 'ccl'
851 # Form-based queries are non-nested and fixed depth, so we can easily modify the incoming
852 # query operands and indexes and add stemming, truncation, field weighting, etc.
853 # Once we do so, we'll end up with a value in $query, just like if we had an
854 # incoming $query from the user
855 else {
856 $query = ""
857 ; # clear it out so we can populate properly with field-weighted, stemmed, etc. query
858 my $previous_operand
859 ; # a flag used to keep track if there was a previous query
860 # if there was, we can apply the current operator
861 # for every operand
862 for ( my $i = 0 ; $i <= @operands ; $i++ ) {
864 # COMBINE OPERANDS, INDEXES AND OPERATORS
865 if ( $operands[$i] ) {
867 # A flag to determine whether or not to add the index to the query
868 my $indexes_set;
870 # If the user is sophisticated enough to specify an index, turn off field weighting, stemming, and stopword handling
871 if ( $operands[$i] =~ /(:|=)/ || $scan ) {
872 $weight_fields = 0;
873 $stemming = 0;
874 $remove_stopwords = 0;
876 my $operand = $operands[$i];
877 my $index = $indexes[$i];
879 # Add index-specific attributes
880 # Date of Publication
881 if ( $index eq 'yr' ) {
882 $index .= ",st-numeric";
883 $indexes_set++;
884 $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
887 # Date of Acquisition
888 elsif ( $index eq 'acqdate' ) {
889 $index .= ",st-date-normalized";
890 $indexes_set++;
891 $stemming = $auto_truncation = $weight_fields = $fuzzy_enabled = $remove_stopwords = 0;
893 # ISBN,ISSN,Standard Number, don't need special treatment
894 elsif ( $index eq 'nb' || $index eq 'ns' ) {
895 $indexes_set++;
897 $stemming, $auto_truncation,
898 $weight_fields, $fuzzy_enabled,
899 $remove_stopwords
900 ) = ( 0, 0, 0, 0, 0 );
903 # Set default structure attribute (word list)
904 my $struct_attr;
905 unless ( $indexes_set || !$index || $index =~ /(st-|phr|ext|wrdl)/ ) {
906 $struct_attr = ",wrdl";
909 # Some helpful index variants
910 my $index_plus = $index . $struct_attr . ":" if $index;
911 my $index_plus_comma = $index . $struct_attr . "," if $index;
912 if ($auto_truncation){
913 # FIXME Auto Truncation is only valid for LTR languages
914 # use C4::Output;
915 # use C4::Languages qw(regex_lang_subtags get_bidi);
916 # $lang = $query->cookie('KohaOpacLanguage') if (defined $query && $query->cookie('KohaOpacLanguage'));
917 # my $current_lang = regex_lang_subtags($lang);
918 # my $bidi;
919 # $bidi = get_bidi($current_lang->{script}) if $current_lang->{script};
920 $index_plus_comma .= "rtrn:";
923 # Remove Stopwords
924 if ($remove_stopwords) {
925 ( $operand, $stopwords_removed ) =
926 _remove_stopwords( $operand, $index );
927 warn "OPERAND w/out STOPWORDS: >$operand<" if $DEBUG;
928 warn "REMOVED STOPWORDS: @$stopwords_removed"
929 if ( $stopwords_removed && $DEBUG );
932 # Detect Truncation
933 my $truncated_operand;
934 my( $nontruncated, $righttruncated, $lefttruncated,
935 $rightlefttruncated, $regexpr
936 ) = _detect_truncation( $operand, $index );
937 warn
938 "TRUNCATION: NON:>@$nontruncated< RIGHT:>@$righttruncated< LEFT:>@$lefttruncated< RIGHTLEFT:>@$rightlefttruncated< REGEX:>@$regexpr<"
939 if $DEBUG;
941 # Apply Truncation
942 if (
943 scalar(@$righttruncated) + scalar(@$lefttruncated) +
944 scalar(@$rightlefttruncated) > 0 )
947 # Don't field weight or add the index to the query, we do it here
948 $indexes_set = 1;
949 undef $weight_fields;
950 my $previous_truncation_operand;
951 if (scalar @$nontruncated) {
952 $truncated_operand .= "$index_plus @$nontruncated ";
953 $previous_truncation_operand = 1;
955 if (scalar @$righttruncated) {
956 $truncated_operand .= "and " if $previous_truncation_operand;
957 $truncated_operand .= $index_plus_comma . "rtrn:@$righttruncated ";
958 $previous_truncation_operand = 1;
960 if (scalar @$lefttruncated) {
961 $truncated_operand .= "and " if $previous_truncation_operand;
962 $truncated_operand .= $index_plus_comma . "ltrn:@$lefttruncated ";
963 $previous_truncation_operand = 1;
965 if (scalar @$rightlefttruncated) {
966 $truncated_operand .= "and " if $previous_truncation_operand;
967 $truncated_operand .= $index_plus_comma . "rltrn:@$rightlefttruncated ";
968 $previous_truncation_operand = 1;
971 $operand = $truncated_operand if $truncated_operand;
972 warn "TRUNCATED OPERAND: >$truncated_operand<" if $DEBUG;
974 # Handle Stemming
975 my $stemmed_operand;
976 $stemmed_operand = _build_stemmed_operand($operand) if $stemming;
978 warn "STEMMED OPERAND: >$stemmed_operand<" if $DEBUG;
980 # Handle Field Weighting
981 my $weighted_operand;
982 if ($weight_fields) {
983 $weighted_operand = _build_weighted_query( $operand, $stemmed_operand, $index );
984 $operand = $weighted_operand;
985 $indexes_set = 1;
988 warn "FIELD WEIGHTED OPERAND: >$weighted_operand<" if $DEBUG;
990 # If there's a previous operand, we need to add an operator
991 if ($previous_operand) {
993 # User-specified operator
994 if ( $operators[ $i - 1 ] ) {
995 $query .= " $operators[$i-1] ";
996 $query .= " $index_plus " unless $indexes_set;
997 $query .= " $operand";
998 $query_cgi .= "&op=$operators[$i-1]";
999 $query_cgi .= "&idx=$index" if $index;
1000 $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1001 $query_desc .=
1002 " $operators[$i-1] $index_plus $operands[$i]";
1005 # Default operator is and
1006 else {
1007 $query .= " and ";
1008 $query .= "$index_plus " unless $indexes_set;
1009 $query .= "$operand";
1010 $query_cgi .= "&op=and&idx=$index" if $index;
1011 $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1012 $query_desc .= " and $index_plus $operands[$i]";
1016 # There isn't a pervious operand, don't need an operator
1017 else {
1019 # Field-weighted queries already have indexes set
1020 $query .= " $index_plus " unless $indexes_set;
1021 $query .= $operand;
1022 $query_desc .= " $index_plus $operands[$i]";
1023 $query_cgi .= "&idx=$index" if $index;
1024 $query_cgi .= "&q=$operands[$i]" if $operands[$i];
1025 $previous_operand = 1;
1027 } #/if $operands
1028 } # /for
1030 warn "QUERY BEFORE LIMITS: >$query<" if $DEBUG;
1032 # add limits
1033 my $group_OR_limits;
1034 my $availability_limit;
1035 foreach my $this_limit (@limits) {
1036 if ( $this_limit =~ /available/ ) {
1038 # 'available' is defined as (items.onloan is NULL) and (items.itemlost = 0)
1039 # In English:
1040 # all records not indexed in the onloan register (zebra) and all records with a value of lost equal to 0
1041 $availability_limit .=
1042 "( ( allrecords,AlwaysMatches='' not onloan,AlwaysMatches='') and (lost,st-numeric=0) )"; #or ( allrecords,AlwaysMatches='' not lost,AlwaysMatches='')) )";
1043 $limit_cgi .= "&limit=available";
1044 $limit_desc .= "";
1047 # group_OR_limits, prefixed by mc-
1048 # OR every member of the group
1049 elsif ( $this_limit =~ /mc/ ) {
1050 $group_OR_limits .= " or " if $group_OR_limits;
1051 $limit_desc .= " or " if $group_OR_limits;
1052 $group_OR_limits .= "$this_limit";
1053 $limit_cgi .= "&limit=$this_limit";
1054 $limit_desc .= " $this_limit";
1057 # Regular old limits
1058 else {
1059 $limit .= " and " if $limit || $query;
1060 $limit .= "$this_limit";
1061 $limit_cgi .= "&limit=$this_limit";
1062 if ($this_limit =~ /^branch:(.+)/) {
1063 my $branchcode = $1;
1064 my $branchname = GetBranchName($branchcode);
1065 if (defined $branchname) {
1066 $limit_desc .= " branch:$branchname";
1067 } else {
1068 $limit_desc .= " $this_limit";
1070 } else {
1071 $limit_desc .= " $this_limit";
1075 if ($group_OR_limits) {
1076 $limit .= " and " if ( $query || $limit );
1077 $limit .= "($group_OR_limits)";
1079 if ($availability_limit) {
1080 $limit .= " and " if ( $query || $limit );
1081 $limit .= "($availability_limit)";
1084 # Normalize the query and limit strings
1085 $query =~ s/:/=/g;
1086 $limit =~ s/:/=/g;
1087 for ( $query, $query_desc, $limit, $limit_desc ) {
1088 s/ / /g; # remove extra spaces
1089 s/^ //g; # remove any beginning spaces
1090 s/ $//g; # remove any ending spaces
1091 s/==/=/g; # remove double == from query
1093 $query_cgi =~ s/^&//; # remove unnecessary & from beginning of the query cgi
1095 for ($query_cgi,$simple_query) {
1096 s/"//g;
1098 # append the limit to the query
1099 $query .= " " . $limit;
1101 # Warnings if DEBUG
1102 if ($DEBUG) {
1103 warn "QUERY:" . $query;
1104 warn "QUERY CGI:" . $query_cgi;
1105 warn "QUERY DESC:" . $query_desc;
1106 warn "LIMIT:" . $limit;
1107 warn "LIMIT CGI:" . $limit_cgi;
1108 warn "LIMIT DESC:" . $limit_desc;
1109 warn "---------\nLeave buildQuery\n---------";
1111 return (
1112 undef, $query, $simple_query, $query_cgi,
1113 $query_desc, $limit, $limit_cgi, $limit_desc,
1114 $stopwords_removed, $query_type
1118 =head2 searchResults
1120 Format results in a form suitable for passing to the template
1122 =cut
1124 # IMO this subroutine is pretty messy still -- it's responsible for
1125 # building the HTML output for the template
1126 sub searchResults {
1127 my ( $searchdesc, $hits, $results_per_page, $offset, $scan, @marcresults ) = @_;
1128 my $dbh = C4::Context->dbh;
1129 my @newresults;
1131 #Build branchnames hash
1132 #find branchname
1133 #get branch information.....
1134 my %branches;
1135 my $bsth =$dbh->prepare("SELECT branchcode,branchname FROM branches"); # FIXME : use C4::Branch::GetBranches
1136 $bsth->execute();
1137 while ( my $bdata = $bsth->fetchrow_hashref ) {
1138 $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
1140 # FIXME - We build an authorised values hash here, using the default framework
1141 # though it is possible to have different authvals for different fws.
1143 my $shelflocations =GetKohaAuthorisedValues('items.location','');
1145 # get notforloan authorised value list (see $shelflocations FIXME)
1146 my $notforloan_authorised_value = GetAuthValCode('items.notforloan','');
1148 #Build itemtype hash
1149 #find itemtype & itemtype image
1150 my %itemtypes;
1151 $bsth =
1152 $dbh->prepare(
1153 "SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes"
1155 $bsth->execute();
1156 while ( my $bdata = $bsth->fetchrow_hashref ) {
1157 foreach (qw(description imageurl summary notforloan)) {
1158 $itemtypes{ $bdata->{'itemtype'} }->{$_} = $bdata->{$_};
1162 #search item field code
1163 my $sth =
1164 $dbh->prepare(
1165 "SELECT tagfield FROM marc_subfield_structure WHERE kohafield LIKE 'items.itemnumber'"
1167 $sth->execute;
1168 my ($itemtag) = $sth->fetchrow;
1170 ## find column names of items related to MARC
1171 my $sth2 = $dbh->prepare("SHOW COLUMNS FROM items");
1172 $sth2->execute;
1173 my %subfieldstosearch;
1174 while ( ( my $column ) = $sth2->fetchrow ) {
1175 my ( $tagfield, $tagsubfield ) =
1176 &GetMarcFromKohaField( "items." . $column, "" );
1177 $subfieldstosearch{$column} = $tagsubfield;
1180 # handle which records to actually retrieve
1181 my $times;
1182 if ( $hits && $offset + $results_per_page <= $hits ) {
1183 $times = $offset + $results_per_page;
1185 else {
1186 $times = $hits; # FIXME: if $hits is undefined, why do we want to equal it?
1189 my $marcflavour = C4::Context->preference("marcflavour");
1190 # loop through all of the records we've retrieved
1191 for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
1192 my $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
1193 my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, '' );
1194 $oldbiblio->{subtitle} = C4::Biblio::get_koha_field_from_marc('bibliosubtitle', 'subtitle', $marcrecord, '');
1195 $oldbiblio->{result_number} = $i + 1;
1197 # add imageurl to itemtype if there is one
1198 $oldbiblio->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
1200 $oldbiblio->{'authorised_value_images'} = C4::Items::get_authorised_value_images( C4::Biblio::get_biblio_authorised_values( $oldbiblio->{'biblionumber'}, $marcrecord ) );
1201 $oldbiblio->{normalized_upc} = GetNormalizedUPC( $marcrecord,$marcflavour);
1202 $oldbiblio->{normalized_ean} = GetNormalizedEAN( $marcrecord,$marcflavour);
1203 $oldbiblio->{normalized_oclc} = GetNormalizedOCLCNumber($marcrecord,$marcflavour);
1204 $oldbiblio->{normalized_isbn} = GetNormalizedISBN(undef,$marcrecord,$marcflavour);
1205 $oldbiblio->{content_identifier_exists} = 1 if ($oldbiblio->{normalized_isbn} or $oldbiblio->{normalized_oclc} or $oldbiblio->{normalized_ean} or $oldbiblio->{normalized_upc});
1207 # edition information, if any
1208 $oldbiblio->{edition} = $oldbiblio->{editionstatement};
1209 $oldbiblio->{description} = $itemtypes{ $oldbiblio->{itemtype} }->{description};
1210 # Build summary if there is one (the summary is defined in the itemtypes table)
1211 # FIXME: is this used anywhere, I think it can be commented out? -- JF
1212 if ( $itemtypes{ $oldbiblio->{itemtype} }->{summary} ) {
1213 my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
1214 my @fields = $marcrecord->fields();
1215 foreach my $field (@fields) {
1216 my $tag = $field->tag();
1217 my $tagvalue = $field->as_string();
1218 $summary =~
1219 s/\[(.?.?.?.?)$tag\*(.*?)]/$1$tagvalue$2\[$1$tag$2]/g;
1220 unless ( $tag < 10 ) {
1221 my @subf = $field->subfields;
1222 for my $i ( 0 .. $#subf ) {
1223 my $subfieldcode = $subf[$i][0];
1224 my $subfieldvalue = $subf[$i][1];
1225 my $tagsubf = $tag . $subfieldcode;
1226 $summary =~
1227 s/\[(.?.?.?.?)$tagsubf(.*?)]/$1$subfieldvalue$2\[$1$tagsubf$2]/g;
1231 # FIXME: yuk
1232 $summary =~ s/\[(.*?)]//g;
1233 $summary =~ s/\n/<br\/>/g;
1234 $oldbiblio->{summary} = $summary;
1237 # Pull out the items fields
1238 my @fields = $marcrecord->field($itemtag);
1240 # Setting item statuses for display
1241 my @available_items_loop;
1242 my @onloan_items_loop;
1243 my @other_items_loop;
1245 my $available_items;
1246 my $onloan_items;
1247 my $other_items;
1249 my $ordered_count = 0;
1250 my $available_count = 0;
1251 my $onloan_count = 0;
1252 my $longoverdue_count = 0;
1253 my $other_count = 0;
1254 my $wthdrawn_count = 0;
1255 my $itemlost_count = 0;
1256 my $itembinding_count = 0;
1257 my $itemdamaged_count = 0;
1258 my $item_in_transit_count = 0;
1259 my $can_place_holds = 0;
1260 my $items_count = scalar(@fields);
1261 my $maxitems =
1262 ( C4::Context->preference('maxItemsinSearchResults') )
1263 ? C4::Context->preference('maxItemsinSearchResults') - 1
1264 : 1;
1266 # loop through every item
1267 foreach my $field (@fields) {
1268 my $item;
1270 # populate the items hash
1271 foreach my $code ( keys %subfieldstosearch ) {
1272 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1274 my $hbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'homebranch' : 'holdingbranch';
1275 my $otherbranch = C4::Context->preference('HomeOrHoldingBranch') eq 'homebranch' ? 'holdingbranch' : 'homebranch';
1276 # set item's branch name, use HomeOrHoldingBranch syspref first, fall back to the other one
1277 if ($item->{$hbranch}) {
1278 $item->{'branchname'} = $branches{$item->{$hbranch}};
1280 elsif ($item->{$otherbranch}) { # Last resort
1281 $item->{'branchname'} = $branches{$item->{$otherbranch}};
1284 my $prefix = $item->{$hbranch} . '--' . $item->{location} . $item->{itype} . $item->{itemcallnumber};
1285 # For each grouping of items (onloan, available, unavailable), we build a key to store relevant info about that item
1286 if ( $item->{onloan} ) {
1287 $onloan_count++;
1288 my $key = $prefix . $item->{onloan} . $item->{barcode};
1289 $onloan_items->{$key}->{due_date} = format_date($item->{onloan});
1290 $onloan_items->{$key}->{count}++ if $item->{$hbranch};
1291 $onloan_items->{$key}->{branchname} = $item->{branchname};
1292 $onloan_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1293 $onloan_items->{$key}->{itemcallnumber} = $item->{itemcallnumber};
1294 $onloan_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1295 # if something's checked out and lost, mark it as 'long overdue'
1296 if ( $item->{itemlost} ) {
1297 $onloan_items->{$prefix}->{longoverdue}++;
1298 $longoverdue_count++;
1299 } else { # can place holds as long as item isn't lost
1300 $can_place_holds = 1;
1304 # items not on loan, but still unavailable ( lost, withdrawn, damaged )
1305 else {
1307 # item is on order
1308 if ( $item->{notforloan} == -1 ) {
1309 $ordered_count++;
1312 # is item in transit?
1313 my $transfertwhen = '';
1314 my ($transfertfrom, $transfertto);
1316 unless ($item->{wthdrawn}
1317 || $item->{itemlost}
1318 || $item->{damaged}
1319 || $item->{notforloan}
1320 || $items_count > 20) {
1322 # A couple heuristics to limit how many times
1323 # we query the database for item transfer information, sacrificing
1324 # accuracy in some cases for speed;
1326 # 1. don't query if item has one of the other statuses
1327 # 2. don't check transit status if the bib has
1328 # more than 20 items
1330 # FIXME: to avoid having the query the database like this, and to make
1331 # the in transit status count as unavailable for search limiting,
1332 # should map transit status to record indexed in Zebra.
1334 ($transfertwhen, $transfertfrom, $transfertto) = C4::Circulation::GetTransfers($item->{itemnumber});
1337 # item is withdrawn, lost or damaged
1338 if ( $item->{wthdrawn}
1339 || $item->{itemlost}
1340 || $item->{damaged}
1341 || $item->{notforloan}
1342 || ($transfertwhen ne ''))
1344 $wthdrawn_count++ if $item->{wthdrawn};
1345 $itemlost_count++ if $item->{itemlost};
1346 $itemdamaged_count++ if $item->{damaged};
1347 $item_in_transit_count++ if $transfertwhen ne '';
1348 $item->{status} = $item->{wthdrawn} . "-" . $item->{itemlost} . "-" . $item->{damaged} . "-" . $item->{notforloan};
1349 $other_count++;
1351 my $key = $prefix . $item->{status};
1352 foreach (qw(wthdrawn itemlost damaged branchname itemcallnumber)) {
1353 $other_items->{$key}->{$_} = $item->{$_};
1355 $other_items->{$key}->{intransit} = ($transfertwhen ne '') ? 1 : 0;
1356 $other_items->{$key}->{notforloan} = GetAuthorisedValueDesc('','',$item->{notforloan},'','',$notforloan_authorised_value) if $notforloan_authorised_value;
1357 $other_items->{$key}->{count}++ if $item->{$hbranch};
1358 $other_items->{$key}->{location} = $shelflocations->{ $item->{location} };
1359 $other_items->{$key}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1361 # item is available
1362 else {
1363 $can_place_holds = 1;
1364 $available_count++;
1365 $available_items->{$prefix}->{count}++ if $item->{$hbranch};
1366 foreach (qw(branchname itemcallnumber)) {
1367 $available_items->{$prefix}->{$_} = $item->{$_};
1369 $available_items->{$prefix}->{location} = $shelflocations->{ $item->{location} };
1370 $available_items->{$prefix}->{imageurl} = getitemtypeimagelocation( 'opac', $itemtypes{ $item->{itype} }->{imageurl} );
1373 } # notforloan, item level and biblioitem level
1374 my ( $availableitemscount, $onloanitemscount, $otheritemscount );
1375 $maxitems =
1376 ( C4::Context->preference('maxItemsinSearchResults') )
1377 ? C4::Context->preference('maxItemsinSearchResults') - 1
1378 : 1;
1379 for my $key ( sort keys %$onloan_items ) {
1380 (++$onloanitemscount > $maxitems) and last;
1381 push @onloan_items_loop, $onloan_items->{$key};
1383 for my $key ( sort keys %$other_items ) {
1384 (++$otheritemscount > $maxitems) and last;
1385 push @other_items_loop, $other_items->{$key};
1387 for my $key ( sort keys %$available_items ) {
1388 (++$availableitemscount > $maxitems) and last;
1389 push @available_items_loop, $available_items->{$key}
1392 # XSLT processing of some stuff
1393 if (C4::Context->preference("XSLTResultsDisplay") && !$scan) {
1394 $oldbiblio->{XSLTResultsRecord} = XSLTParse4Display(
1395 $oldbiblio->{biblionumber}, $marcrecord, 'Results' );
1398 # last check for norequest : if itemtype is notforloan, it can't be reserved either, whatever the items
1399 $can_place_holds = 0
1400 if $itemtypes{ $oldbiblio->{itemtype} }->{notforloan};
1401 $oldbiblio->{norequests} = 1 unless $can_place_holds;
1402 $oldbiblio->{itemsplural} = 1 if $items_count > 1;
1403 $oldbiblio->{items_count} = $items_count;
1404 $oldbiblio->{available_items_loop} = \@available_items_loop;
1405 $oldbiblio->{onloan_items_loop} = \@onloan_items_loop;
1406 $oldbiblio->{other_items_loop} = \@other_items_loop;
1407 $oldbiblio->{availablecount} = $available_count;
1408 $oldbiblio->{availableplural} = 1 if $available_count > 1;
1409 $oldbiblio->{onloancount} = $onloan_count;
1410 $oldbiblio->{onloanplural} = 1 if $onloan_count > 1;
1411 $oldbiblio->{othercount} = $other_count;
1412 $oldbiblio->{otherplural} = 1 if $other_count > 1;
1413 $oldbiblio->{wthdrawncount} = $wthdrawn_count;
1414 $oldbiblio->{itemlostcount} = $itemlost_count;
1415 $oldbiblio->{damagedcount} = $itemdamaged_count;
1416 $oldbiblio->{intransitcount} = $item_in_transit_count;
1417 $oldbiblio->{orderedcount} = $ordered_count;
1418 push( @newresults, $oldbiblio );
1420 return @newresults;
1423 #----------------------------------------------------------------------
1425 # Non-Zebra GetRecords#
1426 #----------------------------------------------------------------------
1428 =head2 NZgetRecords
1430 NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1432 =cut
1434 sub NZgetRecords {
1435 my (
1436 $query, $simple_query, $sort_by_ref, $servers_ref,
1437 $results_per_page, $offset, $expanded_facet, $branches,
1438 $query_type, $scan
1439 ) = @_;
1440 warn "query =$query" if $DEBUG;
1441 my $result = NZanalyse($query);
1442 warn "results =$result" if $DEBUG;
1443 return ( undef,
1444 NZorder( $result, @$sort_by_ref[0], $results_per_page, $offset ),
1445 undef );
1448 =head2 NZanalyse
1450 NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1451 the list is built from an inverted index in the nozebra SQL table
1452 note that title is here only for convenience : the sorting will be very fast when requested on title
1453 if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1455 =cut
1457 sub NZanalyse {
1458 my ( $string, $server ) = @_;
1459 # warn "---------" if $DEBUG;
1460 warn " NZanalyse" if $DEBUG;
1461 # warn "---------" if $DEBUG;
1463 # $server contains biblioserver or authorities, depending on what we search on.
1464 #warn "querying : $string on $server";
1465 $server = 'biblioserver' unless $server;
1467 # if we have a ", replace the content to discard temporarily any and/or/not inside
1468 my $commacontent;
1469 if ( $string =~ /"/ ) {
1470 $string =~ s/"(.*?)"/__X__/;
1471 $commacontent = $1;
1472 warn "commacontent : $commacontent" if $DEBUG;
1475 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1476 # then, call again NZanalyse with $left and $right
1477 # (recursive until we find a leaf (=> something without and/or/not)
1478 # delete repeated operator... Would then go in infinite loop
1479 while ( $string =~ s/( and| or| not| AND| OR| NOT)\1/$1/g ) {
1482 #process parenthesis before.
1483 if ( $string =~ /^\s*\((.*)\)(( and | or | not | AND | OR | NOT )(.*))?/ ) {
1484 my $left = $1;
1485 my $right = $4;
1486 my $operator = lc($3); # FIXME: and/or/not are operators, not operands
1487 warn
1488 "dealing w/parenthesis before recursive sub call. left :$left operator:$operator right:$right"
1489 if $DEBUG;
1490 my $leftresult = NZanalyse( $left, $server );
1491 if ($operator) {
1492 my $rightresult = NZanalyse( $right, $server );
1494 # OK, we have the results for right and left part of the query
1495 # depending of operand, intersect, union or exclude both lists
1496 # to get a result list
1497 if ( $operator eq ' and ' ) {
1498 return NZoperatorAND($leftresult,$rightresult);
1500 elsif ( $operator eq ' or ' ) {
1502 # just merge the 2 strings
1503 return $leftresult . $rightresult;
1505 elsif ( $operator eq ' not ' ) {
1506 return NZoperatorNOT($leftresult,$rightresult);
1509 else {
1510 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1511 return $leftresult;
1514 warn "string :" . $string if $DEBUG;
1515 my $left = "";
1516 my $right = "";
1517 my $operator = "";
1518 if ($string =~ /(.*?)( and | or | not | AND | OR | NOT )(.*)/) {
1519 $left = $1;
1520 $right = $3;
1521 $operator = lc($2); # FIXME: and/or/not are operators, not operands
1523 warn "no parenthesis. left : $left operator: $operator right: $right"
1524 if $DEBUG;
1526 # it's not a leaf, we have a and/or/not
1527 if ($operator) {
1529 # reintroduce comma content if needed
1530 $right =~ s/__X__/"$commacontent"/ if $commacontent;
1531 $left =~ s/__X__/"$commacontent"/ if $commacontent;
1532 warn "node : $left / $operator / $right\n" if $DEBUG;
1533 my $leftresult = NZanalyse( $left, $server );
1534 my $rightresult = NZanalyse( $right, $server );
1535 warn " leftresult : $leftresult" if $DEBUG;
1536 warn " rightresult : $rightresult" if $DEBUG;
1537 # OK, we have the results for right and left part of the query
1538 # depending of operand, intersect, union or exclude both lists
1539 # to get a result list
1540 if ( $operator eq ' and ' ) {
1541 warn "NZAND";
1542 return NZoperatorAND($leftresult,$rightresult);
1544 elsif ( $operator eq ' or ' ) {
1546 # just merge the 2 strings
1547 return $leftresult . $rightresult;
1549 elsif ( $operator eq ' not ' ) {
1550 return NZoperatorNOT($leftresult,$rightresult);
1552 else {
1554 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1555 die "error : operand unknown : $operator for $string";
1558 # it's a leaf, do the real SQL query and return the result
1560 else {
1561 $string =~ s/__X__/"$commacontent"/ if $commacontent;
1562 $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|&|\+|\*|\// /g;
1563 #remove trailing blank at the beginning
1564 $string =~ s/^ //g;
1565 warn "leaf:$string" if $DEBUG;
1567 # parse the string in in operator/operand/value again
1568 my $left = "";
1569 my $operator = "";
1570 my $right = "";
1571 if ($string =~ /(.*)(>=|<=)(.*)/) {
1572 $left = $1;
1573 $operator = $2;
1574 $right = $3;
1575 } else {
1576 $left = $string;
1578 # warn "handling leaf... left:$left operator:$operator right:$right"
1579 # if $DEBUG;
1580 unless ($operator) {
1581 if ($string =~ /(.*)(>|<|=)(.*)/) {
1582 $left = $1;
1583 $operator = $2;
1584 $right = $3;
1585 warn
1586 "handling unless (operator)... left:$left operator:$operator right:$right"
1587 if $DEBUG;
1588 } else {
1589 $left = $string;
1592 my $results;
1594 # strip adv, zebra keywords, currently not handled in nozebra: wrdl, ext, phr...
1595 $left =~ s/ .*$//;
1597 # automatic replace for short operators
1598 $left = 'title' if $left =~ '^ti$';
1599 $left = 'author' if $left =~ '^au$';
1600 $left = 'publisher' if $left =~ '^pb$';
1601 $left = 'subject' if $left =~ '^su$';
1602 $left = 'koha-Auth-Number' if $left =~ '^an$';
1603 $left = 'keyword' if $left =~ '^kw$';
1604 $left = 'itemtype' if $left =~ '^mc$'; # Fix for Bug 2599 - Search limits not working for NoZebra
1605 warn "handling leaf... left:$left operator:$operator right:$right" if $DEBUG;
1606 my $dbh = C4::Context->dbh;
1607 if ( $operator && $left ne 'keyword' ) {
1608 #do a specific search
1609 $operator = 'LIKE' if $operator eq '=' and $right =~ /%/;
1610 my $sth = $dbh->prepare(
1611 "SELECT biblionumbers,value FROM nozebra WHERE server=? AND indexname=? AND value $operator ?"
1613 warn "$left / $operator / $right\n" if $DEBUG;
1615 # split each word, query the DB and build the biblionumbers result
1616 #sanitizing leftpart
1617 $left =~ s/^\s+|\s+$//;
1618 foreach ( split / /, $right ) {
1619 my $biblionumbers;
1620 $_ =~ s/^\s+|\s+$//;
1621 next unless $_;
1622 warn "EXECUTE : $server, $left, $_" if $DEBUG;
1623 $sth->execute( $server, $left, $_ )
1624 or warn "execute failed: $!";
1625 while ( my ( $line, $value ) = $sth->fetchrow ) {
1627 # if we are dealing with a numeric value, use only numeric results (in case of >=, <=, > or <)
1628 # otherwise, fill the result
1629 $biblionumbers .= $line
1630 unless ( $right =~ /^\d+$/ && $value =~ /\D/ );
1631 warn "result : $value "
1632 . ( $right =~ /\d/ ) . "=="
1633 . ( $value =~ /\D/?$line:"" ) if $DEBUG; #= $line";
1636 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1637 if ($results) {
1638 warn "NZAND" if $DEBUG;
1639 $results = NZoperatorAND($biblionumbers,$results);
1640 } else {
1641 $results = $biblionumbers;
1645 else {
1646 #do a complete search (all indexes), if index='kw' do complete search too.
1647 my $sth = $dbh->prepare(
1648 "SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?"
1651 # split each word, query the DB and build the biblionumbers result
1652 foreach ( split / /, $string ) {
1653 next if C4::Context->stopwords->{ uc($_) }; # skip if stopword
1654 warn "search on all indexes on $_" if $DEBUG;
1655 my $biblionumbers;
1656 next unless $_;
1657 $sth->execute( $server, $_ );
1658 while ( my $line = $sth->fetchrow ) {
1659 $biblionumbers .= $line;
1662 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1663 if ($results) {
1664 $results = NZoperatorAND($biblionumbers,$results);
1666 else {
1667 warn "NEW RES for $_ = $biblionumbers" if $DEBUG;
1668 $results = $biblionumbers;
1672 warn "return : $results for LEAF : $string" if $DEBUG;
1673 return $results;
1675 warn "---------\nLeave NZanalyse\n---------" if $DEBUG;
1678 sub NZoperatorAND{
1679 my ($rightresult, $leftresult)=@_;
1681 my @leftresult = split /;/, $leftresult;
1682 warn " @leftresult / $rightresult \n" if $DEBUG;
1684 # my @rightresult = split /;/,$leftresult;
1685 my $finalresult;
1687 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1688 # the result is stored twice, to have the same weight for AND than OR.
1689 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1690 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1691 foreach (@leftresult) {
1692 my $value = $_;
1693 my $countvalue;
1694 ( $value, $countvalue ) = ( $1, $2 ) if ($value=~/(.*)-(\d+)$/);
1695 if ( $rightresult =~ /\Q$value\E-(\d+);/ ) {
1696 $countvalue = ( $1 > $countvalue ? $countvalue : $1 );
1697 $finalresult .=
1698 "$value-$countvalue;$value-$countvalue;";
1701 warn "NZAND DONE : $finalresult \n" if $DEBUG;
1702 return $finalresult;
1705 sub NZoperatorOR{
1706 my ($rightresult, $leftresult)=@_;
1707 return $rightresult.$leftresult;
1710 sub NZoperatorNOT{
1711 my ($leftresult, $rightresult)=@_;
1713 my @leftresult = split /;/, $leftresult;
1715 # my @rightresult = split /;/,$leftresult;
1716 my $finalresult;
1717 foreach (@leftresult) {
1718 my $value=$_;
1719 $value=$1 if $value=~m/(.*)-\d+$/;
1720 unless ($rightresult =~ "$value-") {
1721 $finalresult .= "$_;";
1724 return $finalresult;
1727 =head2 NZorder
1729 $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
1731 TODO :: Description
1733 =cut
1735 sub NZorder {
1736 my ( $biblionumbers, $ordering, $results_per_page, $offset ) = @_;
1737 warn "biblionumbers = $biblionumbers and ordering = $ordering\n" if $DEBUG;
1739 # order title asc by default
1740 # $ordering = '1=36 <i' unless $ordering;
1741 $results_per_page = 20 unless $results_per_page;
1742 $offset = 0 unless $offset;
1743 my $dbh = C4::Context->dbh;
1746 # order by POPULARITY
1748 if ( $ordering =~ /popularity/ ) {
1749 my %result;
1750 my %popularity;
1752 # popularity is not in MARC record, it's builded from a specific query
1753 my $sth =
1754 $dbh->prepare("select sum(issues) from items where biblionumber=?");
1755 foreach ( split /;/, $biblionumbers ) {
1756 my ( $biblionumber, $title ) = split /,/, $_;
1757 $result{$biblionumber} = GetMarcBiblio($biblionumber);
1758 $sth->execute($biblionumber);
1759 my $popularity = $sth->fetchrow || 0;
1761 # hint : the key is popularity.title because we can have
1762 # many results with the same popularity. In this case, sub-ordering is done by title
1763 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
1764 # (un-frequent, I agree, but we won't forget anything that way ;-)
1765 $popularity{ sprintf( "%10d", $popularity ) . $title
1766 . $biblionumber } = $biblionumber;
1769 # sort the hash and return the same structure as GetRecords (Zebra querying)
1770 my $result_hash;
1771 my $numbers = 0;
1772 if ( $ordering eq 'popularity_dsc' ) { # sort popularity DESC
1773 foreach my $key ( sort { $b cmp $a } ( keys %popularity ) ) {
1774 $result_hash->{'RECORDS'}[ $numbers++ ] =
1775 $result{ $popularity{$key} }->as_usmarc();
1778 else { # sort popularity ASC
1779 foreach my $key ( sort ( keys %popularity ) ) {
1780 $result_hash->{'RECORDS'}[ $numbers++ ] =
1781 $result{ $popularity{$key} }->as_usmarc();
1784 my $finalresult = ();
1785 $result_hash->{'hits'} = $numbers;
1786 $finalresult->{'biblioserver'} = $result_hash;
1787 return $finalresult;
1790 # ORDER BY author
1793 elsif ( $ordering =~ /author/ ) {
1794 my %result;
1795 foreach ( split /;/, $biblionumbers ) {
1796 my ( $biblionumber, $title ) = split /,/, $_;
1797 my $record = GetMarcBiblio($biblionumber);
1798 my $author;
1799 if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
1800 $author = $record->subfield( '200', 'f' );
1801 $author = $record->subfield( '700', 'a' ) unless $author;
1803 else {
1804 $author = $record->subfield( '100', 'a' );
1807 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1808 # and we don't want to get only 1 result for each of them !!!
1809 $result{ $author . $biblionumber } = $record;
1812 # sort the hash and return the same structure as GetRecords (Zebra querying)
1813 my $result_hash;
1814 my $numbers = 0;
1815 if ( $ordering eq 'author_za' ) { # sort by author desc
1816 foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
1817 $result_hash->{'RECORDS'}[ $numbers++ ] =
1818 $result{$key}->as_usmarc();
1821 else { # sort by author ASC
1822 foreach my $key ( sort ( keys %result ) ) {
1823 $result_hash->{'RECORDS'}[ $numbers++ ] =
1824 $result{$key}->as_usmarc();
1827 my $finalresult = ();
1828 $result_hash->{'hits'} = $numbers;
1829 $finalresult->{'biblioserver'} = $result_hash;
1830 return $finalresult;
1833 # ORDER BY callnumber
1836 elsif ( $ordering =~ /callnumber/ ) {
1837 my %result;
1838 foreach ( split /;/, $biblionumbers ) {
1839 my ( $biblionumber, $title ) = split /,/, $_;
1840 my $record = GetMarcBiblio($biblionumber);
1841 my $callnumber;
1842 my $frameworkcode = GetFrameworkCode($biblionumber);
1843 my ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField( 'items.itemcallnumber', $frameworkcode);
1844 ( $callnumber_tag, $callnumber_subfield ) = GetMarcFromKohaField('biblioitems.callnumber', $frameworkcode)
1845 unless $callnumber_tag;
1846 if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
1847 $callnumber = $record->subfield( '200', 'f' );
1848 } else {
1849 $callnumber = $record->subfield( '100', 'a' );
1852 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1853 # and we don't want to get only 1 result for each of them !!!
1854 $result{ $callnumber . $biblionumber } = $record;
1857 # sort the hash and return the same structure as GetRecords (Zebra querying)
1858 my $result_hash;
1859 my $numbers = 0;
1860 if ( $ordering eq 'call_number_dsc' ) { # sort by title desc
1861 foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
1862 $result_hash->{'RECORDS'}[ $numbers++ ] =
1863 $result{$key}->as_usmarc();
1866 else { # sort by title ASC
1867 foreach my $key ( sort { $a cmp $b } ( keys %result ) ) {
1868 $result_hash->{'RECORDS'}[ $numbers++ ] =
1869 $result{$key}->as_usmarc();
1872 my $finalresult = ();
1873 $result_hash->{'hits'} = $numbers;
1874 $finalresult->{'biblioserver'} = $result_hash;
1875 return $finalresult;
1877 elsif ( $ordering =~ /pubdate/ ) { #pub year
1878 my %result;
1879 foreach ( split /;/, $biblionumbers ) {
1880 my ( $biblionumber, $title ) = split /,/, $_;
1881 my $record = GetMarcBiblio($biblionumber);
1882 my ( $publicationyear_tag, $publicationyear_subfield ) =
1883 GetMarcFromKohaField( 'biblioitems.publicationyear', '' );
1884 my $publicationyear =
1885 $record->subfield( $publicationyear_tag,
1886 $publicationyear_subfield );
1888 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1889 # and we don't want to get only 1 result for each of them !!!
1890 $result{ $publicationyear . $biblionumber } = $record;
1893 # sort the hash and return the same structure as GetRecords (Zebra querying)
1894 my $result_hash;
1895 my $numbers = 0;
1896 if ( $ordering eq 'pubdate_dsc' ) { # sort by pubyear desc
1897 foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
1898 $result_hash->{'RECORDS'}[ $numbers++ ] =
1899 $result{$key}->as_usmarc();
1902 else { # sort by pub year ASC
1903 foreach my $key ( sort ( keys %result ) ) {
1904 $result_hash->{'RECORDS'}[ $numbers++ ] =
1905 $result{$key}->as_usmarc();
1908 my $finalresult = ();
1909 $result_hash->{'hits'} = $numbers;
1910 $finalresult->{'biblioserver'} = $result_hash;
1911 return $finalresult;
1914 # ORDER BY title
1917 elsif ( $ordering =~ /title/ ) {
1919 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
1920 my %result;
1921 foreach ( split /;/, $biblionumbers ) {
1922 my ( $biblionumber, $title ) = split /,/, $_;
1924 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1925 # and we don't want to get only 1 result for each of them !!!
1926 # hint & speed improvement : we can order without reading the record
1927 # so order, and read records only for the requested page !
1928 $result{ $title . $biblionumber } = $biblionumber;
1931 # sort the hash and return the same structure as GetRecords (Zebra querying)
1932 my $result_hash;
1933 my $numbers = 0;
1934 if ( $ordering eq 'title_az' ) { # sort by title desc
1935 foreach my $key ( sort ( keys %result ) ) {
1936 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
1939 else { # sort by title ASC
1940 foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
1941 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
1945 # limit the $results_per_page to result size if it's more
1946 $results_per_page = $numbers - 1 if $numbers < $results_per_page;
1948 # for the requested page, replace biblionumber by the complete record
1949 # speed improvement : avoid reading too much things
1950 for (
1951 my $counter = $offset ;
1952 $counter <= $offset + $results_per_page ;
1953 $counter++
1956 $result_hash->{'RECORDS'}[$counter] =
1957 GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc;
1959 my $finalresult = ();
1960 $result_hash->{'hits'} = $numbers;
1961 $finalresult->{'biblioserver'} = $result_hash;
1962 return $finalresult;
1964 else {
1967 # order by ranking
1969 # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
1970 my %result;
1971 my %count_ranking;
1972 foreach ( split /;/, $biblionumbers ) {
1973 my ( $biblionumber, $title ) = split /,/, $_;
1974 $title =~ /(.*)-(\d)/;
1976 # get weight
1977 my $ranking = $2;
1979 # note that we + the ranking because ranking is calculated on weight of EACH term requested.
1980 # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
1981 # biblio N has ranking = 6
1982 $count_ranking{$biblionumber} += $ranking;
1985 # build the result by "inverting" the count_ranking hash
1986 # 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
1987 # warn "counting";
1988 foreach ( keys %count_ranking ) {
1989 $result{ sprintf( "%10d", $count_ranking{$_} ) . '-' . $_ } = $_;
1992 # sort the hash and return the same structure as GetRecords (Zebra querying)
1993 my $result_hash;
1994 my $numbers = 0;
1995 foreach my $key ( sort { $b cmp $a } ( keys %result ) ) {
1996 $result_hash->{'RECORDS'}[ $numbers++ ] = $result{$key};
1999 # limit the $results_per_page to result size if it's more
2000 $results_per_page = $numbers - 1 if $numbers < $results_per_page;
2002 # for the requested page, replace biblionumber by the complete record
2003 # speed improvement : avoid reading too much things
2004 for (
2005 my $counter = $offset ;
2006 $counter <= $offset + $results_per_page ;
2007 $counter++
2010 $result_hash->{'RECORDS'}[$counter] =
2011 GetMarcBiblio( $result_hash->{'RECORDS'}[$counter] )->as_usmarc
2012 if $result_hash->{'RECORDS'}[$counter];
2014 my $finalresult = ();
2015 $result_hash->{'hits'} = $numbers;
2016 $finalresult->{'biblioserver'} = $result_hash;
2017 return $finalresult;
2021 =head2 enabled_staff_search_views
2023 %hash = enabled_staff_search_views()
2025 This function returns a hash that contains three flags obtained from the system
2026 preferences, used to determine whether a particular staff search results view
2027 is enabled.
2029 =over 2
2031 =item C<Output arg:>
2033 * $hash{can_view_MARC} is true only if the MARC view is enabled
2034 * $hash{can_view_ISBD} is true only if the ISBD view is enabled
2035 * $hash{can_view_labeledMARC} is true only if the Labeled MARC view is enabled
2037 =item C<usage in the script:>
2039 =back
2041 $template->param ( C4::Search::enabled_staff_search_views );
2043 =cut
2045 sub enabled_staff_search_views
2047 return (
2048 can_view_MARC => C4::Context->preference('viewMARC'), # 1 if the staff search allows the MARC view
2049 can_view_ISBD => C4::Context->preference('viewISBD'), # 1 if the staff search allows the ISBD view
2050 can_view_labeledMARC => C4::Context->preference('viewLabeledMARC'), # 1 if the staff search allows the Labeled MARC view
2055 =head2 z3950_search_args
2057 $arrayref = z3950_search_args($matchpoints)
2059 This function returns an array reference that contains the search parameters to be
2060 passed to the Z39.50 search script (z3950_search.pl). The array elements
2061 are hash refs whose keys are name, value and encvalue, and whose values are the
2062 name of a search parameter, the value of that search parameter and the URL encoded
2063 value of that parameter.
2065 The search parameter names are lccn, isbn, issn, title, author, dewey and subject.
2067 The search parameter values are obtained from the bibliographic record whose
2068 data is in a hash reference in $matchpoints, as returned by Biblio::GetBiblioData().
2070 If $matchpoints is a scalar, it is assumed to be an unnamed query descriptor, e.g.
2071 a general purpose search argument. In this case, the returned array contains only
2072 entry: the key is 'title' and the value and encvalue are derived from $matchpoints.
2074 If a search parameter value is undefined or empty, it is not included in the returned
2075 array.
2077 The returned array reference may be passed directly to the template parameters.
2079 =over 2
2081 =item C<Output arg:>
2083 * $array containing hash refs as described above
2085 =item C<usage in the script:>
2087 =back
2089 $data = Biblio::GetBiblioData($bibno);
2090 $template->param ( MYLOOP => C4::Search::z3950_search_args($data) )
2092 *OR*
2094 $template->param ( MYLOOP => C4::Search::z3950_search_args($searchscalar) )
2096 =cut
2098 sub z3950_search_args {
2099 my $bibrec = shift;
2100 $bibrec = { title => $bibrec } if !ref $bibrec;
2101 my $array = [];
2102 for my $field (qw/ lccn isbn issn title author dewey subject /)
2104 my $encvalue = URI::Escape::uri_escape_utf8($bibrec->{$field});
2105 push @$array, { name=>$field, value=>$bibrec->{$field}, encvalue=>$encvalue } if defined $bibrec->{$field};
2107 return $array;
2111 END { } # module clean-up code here (global destructor)
2114 __END__
2116 =head1 AUTHOR
2118 Koha Developement team <info@koha.org>
2120 =cut