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
21 use C4::Biblio; # GetMarcFromKohaField
22 use C4::Koha; # getFacets
25 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
27 # set the version for version checking
28 $VERSION = do { my @v = '$Revision$' =~ /\d+/g;
29 shift(@v) . "." . join( "_", map { sprintf "%03d", $_ } @v );
34 C4::Search - Functions for searching the Koha catalog.
38 see opac/opac-search.pl or catalogue/search.pl for example of usage
42 This module provides the searching facilities for the Koha into a zebra catalog.
59 # make all your functions, whether exported or not;
61 =head2 findseealso($dbh,$fields);
63 C<$dbh> is a link to the DB handler.
66 my $dbh =C4::Context->dbh;
68 C<$fields> is a reference to the fields array
70 This function modify the @$fields array and add related fields to search on.
75 my ( $dbh, $fields ) = @_;
76 my $tagslib = GetMarcStructure( $dbh, 1 );
77 for ( my $i = 0 ; $i <= $#{$fields} ; $i++ ) {
78 my ($tag) = substr( @$fields[$i], 1, 3 );
79 my ($subfield) = substr( @$fields[$i], 4, 1 );
80 @$fields[$i] .= ',' . $tagslib->{$tag}->{$subfield}->{seealso}
81 if ( $tagslib->{$tag}->{$subfield}->{seealso} );
87 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
94 my $dbh = C4::Context->dbh;
95 my $result = TransformMarcToKoha( $dbh, $record, '' );
100 my ( $biblionumber, $title );
102 # search duplicate on ISBN, easy and fast..
103 #$search->{'avoidquerylog'}=1;
104 if ( $result->{isbn} ) {
105 $query = "isbn=$result->{isbn}";
108 $result->{title} =~ s /\\//g;
109 $result->{title} =~ s /\"//g;
110 $result->{title} =~ s /\(//g;
111 $result->{title} =~ s /\)//g;
112 $query = "ti,ext=$result->{title}";
114 my ($possible_duplicate_record) =
115 C4::Biblio::getRecord( "biblioserver", $query, "usmarc" ); # FIXME :: hardcoded !
116 if ($possible_duplicate_record) {
118 MARC::Record->new_from_usmarc($possible_duplicate_record);
119 my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
121 # FIXME :: why 2 $biblionumber ?
122 return $result->{'biblionumber'}, $result->{'biblionumber'},
130 ($error,$results) = SimpleSearch($query,@servers);
132 this function performs a simple search on the catalog using zoom.
138 * $query could be a simple keyword or a complete CCL query wich is depending on your ccl file.
139 * @servers is optionnal. default one is read on koha.xml
142 * $error is a string which containt the description error if there is one. Else it's empty.
143 * \@results is an array of marc record.
145 =item C<usage in the script:>
149 my ($error, $marcresults) = SimpleSearch($query);
151 if (defined $error) {
152 $template->param(query_error => $error);
153 warn "error: ".$error;
154 output_html_with_http_headers $input, $cookie, $template->output;
158 my $hits = scalar @$marcresults;
161 for(my $i=0;$i<$hits;$i++) {
163 my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
164 my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
166 #build the hash for the template.
167 $resultsloop{highlight} = ($i % 2)?(1):(0);
168 $resultsloop{title} = $biblio->{'title'};
169 $resultsloop{subtitle} = $biblio->{'subtitle'};
170 $resultsloop{biblionumber} = $biblio->{'biblionumber'};
171 $resultsloop{author} = $biblio->{'author'};
172 $resultsloop{publishercode} = $biblio->{'publishercode'};
173 $resultsloop{publicationyear} = $biblio->{'publicationyear'};
175 push @results, \%resultsloop;
177 $template->param(result=>\@results);
187 return ( "No query entered", undef ) unless $query;
189 #@servers = (C4::Context->config("biblioserver")) unless @servers;
191 ("biblioserver") unless @servers
192 ; # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
195 for ( my $i = 0 ; $i < @servers ; $i++ ) {
196 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
199 ->search( new ZOOM::Query::CCL2RPN( $query, $zconns[$i] ) );
201 # getting error message if one occured.
203 $zconns[$i]->errmsg() . " ("
204 . $zconns[$i]->errcode() . ") "
205 . $zconns[$i]->addinfo() . " "
206 . $zconns[$i]->diagset();
208 return ( $error, undef ) if $zconns[$i]->errcode();
212 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
213 $ev = $zconns[ $i - 1 ]->last_event();
214 if ( $ev == ZOOM::Event::ZEND ) {
215 $hits = $tmpresults[ $i - 1 ]->size();
218 for ( my $j = 0 ; $j < $hits ; $j++ ) {
219 my $record = $tmpresults[ $i - 1 ]->record($j)->raw();
220 push @results, $record;
224 return ( undef, \@results );
227 # performs the search
230 $koha_query, $federated_query, $sort_by_ref,
231 $servers_ref, $results_per_page, $offset,
232 $expanded_facet, $branches, $query_type,
236 my @servers = @$servers_ref;
237 my @sort_by = @$sort_by_ref;
239 # create the zoom connection and query object
243 my $results_hashref = ();
246 my $facets_counter = ();
247 my $facets_info = ();
248 my $facets = getFacets();
250 #### INITIALIZE SOME VARS USED CREATE THE FACETED RESULTS
251 my @facets_loop; # stores the ref to array of hashes for template
252 for ( my $i = 0 ; $i < @servers ; $i++ ) {
253 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
255 # perform the search, create the results objects
256 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
258 if ( $servers[$i] =~ /biblioserver/ ) {
259 $query_to_use = $koha_query;
262 $query_to_use = $federated_query;
265 # warn "HERE : $query_type => $query_to_use";
266 # check if we've got a query_type defined
270 if ( $query_type =~ /^ccl/ ) {
272 s/\:/\=/g; # change : to = last minute (FIXME)
274 # warn "CCL : $query_to_use";
277 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
280 elsif ( $query_type =~ /^cql/ ) {
282 # warn "CQL : $query_to_use";
285 new ZOOM::Query::CQL( $query_to_use, $zconns[$i] ) );
287 elsif ( $query_type =~ /^pqf/ ) {
289 # warn "PQF : $query_to_use";
292 new ZOOM::Query::PQF( $query_to_use, $zconns[$i] ) );
298 # warn "preparing to scan";
301 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
306 # warn "LAST : $query_to_use";
309 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
315 warn "prob with query toto $query_to_use " . $@;
318 # concatenate the sort_by limits and pass them to the results object
320 foreach my $sort (@sort_by) {
321 $sort_by .= $sort . " "; # used to be $sort,
323 $results[$i]->sort( "yaz", $sort_by ) if $sort_by;
325 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
326 my $ev = $zconns[ $i - 1 ]->last_event();
327 if ( $ev == ZOOM::Event::ZEND ) {
328 my $size = $results[ $i - 1 ]->size();
331 #$results_hash->{'server'} = $servers[$i-1];
332 # loop through the results
333 $results_hash->{'hits'} = $size;
335 if ( $offset + $results_per_page <= $size ) {
336 $times = $offset + $results_per_page;
341 for ( my $j = $offset ; $j < $times ; $j++ )
342 { #(($offset+$count<=$size) ? ($offset+$count):$size) ; $j++){
346 ## This is just an index scan
348 my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
350 # here we create a minimal MARC record and hand it off to the
351 # template just like a normal result ... perhaps not ideal, but
353 my $tmprecord = MARC::Record->new();
354 $tmprecord->encoding('UTF-8');
357 # srote the minimal record in author/title (depending on MARC flavour)
358 if ( C4::Context->preference("marcflavour") eq
361 $tmptitle = MARC::Field->new(
368 $tmptitle = MARC::Field->new(
374 $tmprecord->append_fields($tmptitle);
375 $results_hash->{'RECORDS'}[$j] =
376 $tmprecord->as_usmarc();
379 $record = $results[ $i - 1 ]->record($j)->raw();
381 #warn "RECORD $j:".$record;
382 $results_hash->{'RECORDS'}[$j] =
383 $record; # making a reference to a hash
384 # Fill the facets while we're looping
385 $facet_record = MARC::Record->new_from_usmarc($record);
387 #warn $servers[$i-1].$facet_record->title();
388 for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
389 if ( $facets->[$k] ) {
391 for my $tag ( @{ $facets->[$k]->{'tags'} } ) {
392 push @fields, $facet_record->field($tag);
394 for my $field (@fields) {
395 my @subfields = $field->subfields();
396 for my $subfield (@subfields) {
397 my ( $code, $data ) = @$subfield;
399 $facets->[$k]->{'subfield'} )
401 $facets_counter->{ $facets->[$k]
402 ->{'link_value'} }->{$data}++;
406 $facets_info->{ $facets->[$k]->{'link_value'} }
408 $facets->[$k]->{'label_value'};
409 $facets_info->{ $facets->[$k]->{'link_value'} }
410 ->{'expanded'} = $facets->[$k]->{'expanded'};
415 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
418 #print "connection ", $i-1, ": $size hits";
419 #print $results[$i-1]->record(0)->render() if $size > 0;
422 sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
423 keys %$facets_counter
427 my $number_of_facets;
428 my @this_facets_array;
431 $facets_counter->{$link_value}
432 ->{$b} <=> $facets_counter->{$link_value}->{$a}
433 } keys %{ $facets_counter->{$link_value} }
437 if ( ( $number_of_facets < 6 )
438 || ( $expanded_facet eq $link_value )
439 || ( $facets_info->{$link_value}->{'expanded'} ) )
442 # sanitize the link value ), ( will cause errors with CCL
443 my $facet_link_value = $one_facet;
444 $facet_link_value =~ s/(\(|\))/ /g;
446 # fix the length that will display in the label
447 my $facet_label_value = $one_facet;
448 $facet_label_value = substr( $one_facet, 0, 20 ) . "..."
449 unless length($facet_label_value) <= 20;
451 # well, if it's a branch, label by the name, not the code
452 if ( $link_value =~ /branch/ ) {
454 $branches->{$one_facet}->{'branchname'};
457 # but we're down with the whole label being in the link's title
458 my $facet_title_value = $one_facet;
460 push @this_facets_array,
464 $facets_counter->{$link_value}->{$one_facet},
465 facet_label_value => $facet_label_value,
466 facet_title_value => $facet_title_value,
467 facet_link_value => $facet_link_value,
468 type_link_value => $link_value,
473 unless ( $facets_info->{$link_value}->{'expanded'} ) {
475 if ( ( $number_of_facets > 6 )
476 && ( $expanded_facet ne $link_value ) );
481 type_link_value => $link_value,
482 type_id => $link_value . "_id",
484 $facets_info->{$link_value}->{'label_value'},
485 facets => \@this_facets_array,
486 expandable => $expandable,
487 expand => $link_value,
494 warn Dumper($results_hashref);
495 return ( undef, $results_hashref, \@facets_loop );
498 # build the query itself
500 my ( $query, $operators, $operands, $indexes, $limits, $sort_by ) = @_;
502 my @operators = @$operators if $operators;
503 my @indexes = @$indexes if $indexes;
504 my @operands = @$operands if $operands;
505 my @limits = @$limits if $limits;
506 my @sort_by = @$sort_by if $sort_by;
508 my $human_search_desc; # a human-readable query
509 my $machine_search_desc; #a machine-readable query
510 # FIXME: the locale should be set based on the syspref
511 my $stemmer = Lingua::Stem->new( -locale => 'EN-US' );
513 # FIXME: these should be stored in the db so the librarian can modify the behavior
514 $stemmer->add_exceptions(
522 # STEP I: determine if this is a form-based / simple query or if it's complex (if complex,
523 # we can't handle field weighting, stemming until a formal query parser is written
524 # I'll work on this soon -- JF
525 #if (!$query) { # form-based
526 # check if this is a known query language query, if it is, return immediately:
527 if ( $query =~ /^ccl=/ ) {
528 return ( undef, $', $', $', 'ccl' );
530 if ( $query =~ /^cql=/ ) {
531 return ( undef, $', $', $', 'cql' );
533 if ( $query =~ /^pqf=/ ) {
534 return ( undef, $', $', $', 'pqf' );
536 if ( $query =~ /(\(|\))/ ) { # sorry, too complex
537 return ( undef, $query, $query, $query, 'ccl' );
540 # form-based queries are limited to non-nested a specific depth, so we can easily
541 # modify the incoming query operands and indexes to do stemming and field weighting
542 # Once we do so, we'll end up with a value in $query, just like if we had an
543 # incoming $query from the user
546 ; # clear it out so we can populate properly with field-weighted stemmed query
548 ; # a flag used to keep track if there was a previous query
549 # if there was, we can apply the current operator
550 for ( my $i = 0 ; $i <= @operands ; $i++ ) {
551 my $operand = $operands[$i];
552 my $index = $indexes[$i];
554 my $stemming = C4::Context->parameters("Stemming") || 0;
555 my $weight_fields = C4::Context->parameters("WeightFields") || 0;
557 if ( $operands[$i] ) {
559 # STEMMING FIXME: need to refine the field weighting so stemmed operands don't disrupt the query ranking
561 my @words = split( / /, $operands[$i] );
562 my $stems = $stemmer->stem(@words);
563 foreach my $stem (@$stems) {
564 $stemmed_operand .= "$stem";
565 $stemmed_operand .= "?"
566 unless ( $stem =~ /(and$|or$|not$)/ )
567 || ( length($stem) < 3 );
568 $stemmed_operand .= " ";
570 #warn "STEM: $stemmed_operand";
573 #$operand = $stemmed_operand;
576 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
577 # pretty well but will work much better when we have an actual query parser
579 if ($weight_fields) {
581 " rk=("; # Specifies that we're applying rank
582 # keyword has different weight properties
583 if ( ( $index =~ /kw/ ) || ( !$index ) )
584 { # FIXME: do I need to add right-truncation in the case of stemming?
585 # a simple way to find out if this query uses an index
586 if ( $operand =~ /(\=|\:)/ ) {
587 $weighted_query .= " $operand";
591 " Title-cover,ext,r1=\"$operand\""
592 ; # index label as exact
594 " or ti,ext,r2=$operand"; # index as exact
595 #$weighted_query .= " or ti,phr,r3=$operand"; # index as phrase
596 #$weighted_query .= " or any,ext,r4=$operand"; # index as exact
598 " or kw,wrdl,r5=$operand"; # index as exact
599 $weighted_query .= " or wrd,fuzzy,r9=$operand";
600 $weighted_query .= " or wrd=$stemmed_operand"
604 elsif ( $index =~ /au/ ) {
606 " $index,ext,r1=$operand"; # index label as exact
607 #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
609 " or $index,phr,r3=$operand"; # index as phrase
610 $weighted_query .= " or $index,rt,wrd,r3=$operand";
612 elsif ( $index =~ /ti/ ) {
614 " Title-cover,ext,r1=$operand"; # index label as exact
615 $weighted_query .= " or Title-series,ext,r2=$operand";
617 #$weighted_query .= " or ti,ext,r2=$operand";
618 #$weighted_query .= " or ti,phr,r3=$operand";
619 #$weighted_query .= " or ti,wrd,r3=$operand";
621 " or (title-sort-az=0 or Title-cover,startswithnt,st-word,r3=$operand #)";
623 " or (title-sort-az=0 or Title-cover,phr,r6=$operand)";
625 #$weighted_query .= " or Title-cover,wrd,r5=$operand";
626 #$weighted_query .= " or ti,ext,r6=$operand";
627 #$weighted_query .= " or ti,startswith,phr,r7=$operand";
628 #$weighted_query .= " or ti,phr,r8=$operand";
629 #$weighted_query .= " or ti,wrd,r9=$operand";
631 #$weighted_query .= " or ti,ext,r2=$operand"; # index as exact
632 #$weighted_query .= " or ti,phr,r3=$operand"; # index as phrase
633 #$weighted_query .= " or any,ext,r4=$operand"; # index as exact
634 #$weighted_query .= " or kw,wrd,r5=$operand"; # index as exact
638 " $index,ext,r1=$operand"; # index label as exact
639 #$weighted_query .= " or $index,ext,r2=$operand"; # index as exact
641 " or $index,phr,r3=$operand"; # index as phrase
642 $weighted_query .= " or $index,rt,wrd,r3=$operand";
644 " or $index,wrd,r5=$operand"
645 ; # index as word right-truncated
646 $weighted_query .= " or $index,wrd,fuzzy,r8=$operand";
648 $weighted_query .= ")"; # close rank specification
649 $operand = $weighted_query;
652 # only add an operator if there is a previous operand
653 if ($previous_operand) {
654 if ( $operators[ $i - 1 ] ) {
655 $query .= " $operators[$i-1] $index: $operand";
657 $human_search_desc .=
658 " $operators[$i-1] $operands[$i]";
661 $human_search_desc .=
662 " $operators[$i-1] $index: $operands[$i]";
666 # the default operator is and
668 $query .= " and $index: $operand";
669 $human_search_desc .= " and $index: $operands[$i]";
674 $query .= " $operand";
675 $human_search_desc .= " $operands[$i]";
678 $query .= " $index: $operand";
679 $human_search_desc .= " $index: $operands[$i]";
681 $previous_operand = 1;
689 my $limit_search_desc;
690 foreach my $limit (@limits) {
692 # FIXME: not quite right yet ... will work on this soon -- JF
693 my $type = $1 if $limit =~ m/([^:]+):([^:]*)/;
694 if ( $limit =~ /available/ ) {
696 " (($query and datedue=0000-00-00) or ($query and datedue=0000-00-00 not lost=1) or ($query and datedue=0000-00-00 not lost=2))";
698 #$limit_search_desc.=" and available";
700 elsif ( ($limit_query) && ( index( $limit_query, $type, 0 ) > 0 ) ) {
701 if ( $limit_query !~ /\(/ ) {
703 substr( $limit_query, 0, index( $limit_query, $type, 0 ) )
705 . substr( $limit_query, index( $limit_query, $type, 0 ) )
709 substr( $limit_search_desc, 0,
710 index( $limit_search_desc, $type, 0 ) )
712 . substr( $limit_search_desc,
713 index( $limit_search_desc, $type, 0 ) )
719 chop $limit_search_desc;
720 $limit_query .= " or $limit )" if $limit;
721 $limit_search_desc .= " or $limit )" if $limit;
724 elsif ( ($limit_query) && ( $limit =~ /mc/ ) ) {
725 $limit_query .= " or $limit" if $limit;
726 $limit_search_desc .= " or $limit" if $limit;
729 # these are treated as AND
730 elsif ($limit_query) {
731 if ($limit =~ /branch/){
732 $limit_query .= " ) and ( $limit" if $limit;
733 $limit_search_desc .= " ) and ( $limit" if $limit;
735 $limit_query .= " or $limit" if $limit;
736 $limit_search_desc .= " or $limit" if $limit;
740 # otherwise, there is nothing but the limit
742 $limit_query .= "$limit" if $limit;
743 $limit_search_desc .= "$limit" if $limit;
747 # if there's also a query, we need to AND the limits to it
748 if ( ($limit_query) && ($query) ) {
749 $limit_query = " and (" . $limit_query . ")";
750 $limit_search_desc = " and ($limit_search_desc)" if $limit_search_desc;
753 $query .= $limit_query;
754 $human_search_desc .= $limit_search_desc;
756 # now normalize the strings
757 $query =~ s/ / /g; # remove extra spaces
758 $query =~ s/^ //g; # remove any beginning spaces
759 $query =~ s/:/=/g; # causes probs for server
760 $query =~ s/==/=/g; # remove double == from query
762 my $federated_query = $human_search_desc;
763 $federated_query =~ s/ / /g;
764 $federated_query =~ s/^ //g;
765 $federated_query =~ s/:/=/g;
766 my $federated_query_opensearch = $federated_query;
768 # my $federated_query_RPN = new ZOOM::Query::CCL2RPN( $query , C4::Context->ZConn('biblioserver'));
770 $human_search_desc =~ s/ / /g;
771 $human_search_desc =~ s/^ //g;
772 my $koha_query = $query;
774 #warn "QUERY:".$koha_query;
775 #warn "SEARCHDESC:".$human_search_desc;
776 #warn "FEDERATED QUERY:".$federated_query;
777 return ( undef, $human_search_desc, $koha_query, $federated_query );
780 # IMO this subroutine is pretty messy still -- it's responsible for
781 # building the HTML output for the template
783 my ( $searchdesc, $hits, $results_per_page, $offset, @marcresults ) = @_;
785 my $dbh = C4::Context->dbh;
789 my $span_terms_hashref;
790 for my $span_term ( split( / /, $searchdesc ) ) {
791 $span_term =~ s/(.*=|\)|\(|\+|\.)//g;
792 $span_terms_hashref->{$span_term}++;
795 #Build brancnames hash
797 #get branch information.....
800 $dbh->prepare("SELECT branchcode,branchname FROM branches")
801 ; # FIXME : use C4::Koha::GetBranches
803 while ( my $bdata = $bsth->fetchrow_hashref ) {
804 $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
808 #find itemtype & itemtype image
811 $dbh->prepare("SELECT itemtype,description,imageurl,summary FROM itemtypes");
813 while ( my $bdata = $bsth->fetchrow_hashref ) {
814 $itemtypes{ $bdata->{'itemtype'} }->{description} =
815 $bdata->{'description'};
816 $itemtypes{ $bdata->{'itemtype'} }->{imageurl} = $bdata->{'imageurl'};
817 $itemtypes{ $bdata->{'itemtype'} }->{summary} = $bdata->{'summary'};
820 #search item field code
823 "select tagfield from marc_subfield_structure where kohafield like 'items.itemnumber'"
826 my ($itemtag) = $sth->fetchrow;
828 ## find column names of items related to MARC
829 my $sth2 = $dbh->prepare("SHOW COLUMNS from items");
831 my %subfieldstosearch;
832 while ( ( my $column ) = $sth2->fetchrow ) {
833 my ( $tagfield, $tagsubfield ) =
834 &GetMarcFromKohaField( $dbh, "items." . $column, "" );
835 $subfieldstosearch{$column} = $tagsubfield;
839 if ( $hits && $offset + $results_per_page <= $hits ) {
840 $times = $offset + $results_per_page;
846 for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
848 $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
850 my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, '' );
852 # add image url if there is one
853 if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} =~ /^http:/ ) {
854 $oldbiblio->{imageurl} =
855 $itemtypes{ $oldbiblio->{itemtype} }->{imageurl};
856 $oldbiblio->{description} =
857 $itemtypes{ $oldbiblio->{itemtype} }->{description};
860 $oldbiblio->{imageurl} =
861 getitemtypeimagesrc() . "/"
862 . $itemtypes{ $oldbiblio->{itemtype} }->{imageurl}
863 if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
864 $oldbiblio->{description} =
865 $itemtypes{ $oldbiblio->{itemtype} }->{description};
868 # build summary if there is one (the summary is defined in itemtypes table
870 if ($itemtypes{ $oldbiblio->{itemtype} }->{summary}) {
871 my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
872 my @fields = $marcrecord->fields();
873 foreach my $field (@fields) {
874 my $tag = $field->tag();
875 my $tagvalue = $field->as_string();
876 $summary =~ s/\[(.?.?.?.?)$tag\*(.*?)]/$1$tagvalue$2\[$1$tag$2]/g;
878 my @subf = $field->subfields;
879 for my $i (0..$#subf) {
880 my $subfieldcode = $subf[$i][0];
881 my $subfieldvalue = $subf[$i][1];
882 my $tagsubf = $tag.$subfieldcode;
883 $summary =~ s/\[(.?.?.?.?)$tagsubf(.*?)]/$1$subfieldvalue$2\[$1$tagsubf$2]/g;
887 $summary =~ s/\[(.*?)]//g;
888 $summary =~ s/\n/<br>/g;
889 $oldbiblio->{summary} = $summary;
891 # add spans to search term in results
892 foreach my $term ( keys %$span_terms_hashref ) {
895 my $old_term = $term;
896 if ( length($term) > 3 ) {
897 $term =~ s/(.*=|\)|\(|\+|\.|\?)//g;
899 #FIXME: is there a better way to do this?
900 $oldbiblio->{'title'} =~ s/$term/<span class=term>$&<\/span>/gi;
901 $oldbiblio->{'subtitle'} =~
902 s/$term/<span class=term>$&<\/span>/gi;
904 $oldbiblio->{'author'} =~ s/$term/<span class=term>$&<\/span>/gi;
905 $oldbiblio->{'publishercode'} =~ s/$term/<span class=term>$&<\/span>/gi;
906 $oldbiblio->{'place'} =~ s/$term/<span class=term>$&<\/span>/gi;
907 $oldbiblio->{'pages'} =~ s/$term/<span class=term>$&<\/span>/gi;
908 $oldbiblio->{'notes'} =~ s/$term/<span class=term>$&<\/span>/gi;
909 $oldbiblio->{'size'} =~ s/$term/<span class=term>$&<\/span>/gi;
919 $oldbiblio->{'toggle'} = $toggle;
920 my @fields = $marcrecord->field($itemtag);
923 my $ordered_count = 0;
924 my $onloan_count = 0;
925 my $wthdrawn_count = 0;
926 my $itemlost_count = 0;
927 my $itembinding_count = 0;
930 foreach my $field (@fields) {
932 foreach my $code ( keys %subfieldstosearch ) {
933 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
935 if ( $item->{wthdrawn} ) {
938 elsif ( $item->{notforloan} == -1 ) {
942 elsif ( $item->{itemlost} ) {
945 elsif ( $item->{binding} ) {
946 $itembinding_count++;
948 elsif ( ( $item->{onloan} ) && ( $item->{onloan} != '0000-00-00' ) )
955 if ( $item->{'homebranch'} ) {
956 $items->{ $item->{'homebranch'} }->{count}++;
960 elsif ( $item->{'holdingbranch'} ) {
961 $items->{ $item->{'homebranch'} }->{count}++;
963 $items->{ $item->{homebranch} }->{itemcallnumber} =
964 $item->{itemcallnumber};
965 $items->{ $item->{homebranch} }->{location} =
968 } # notforloan, item level and biblioitem level
969 for my $key ( keys %$items ) {
973 branchname => $branches{$key},
975 count => $items->{$key}->{count},
976 itemcallnumber => $items->{$key}->{itemcallnumber},
977 location => $items->{$key}->{location},
979 push @items_loop, $this_item;
981 $oldbiblio->{norequests} = $norequests;
982 $oldbiblio->{items_loop} = \@items_loop;
983 $oldbiblio->{onloancount} = $onloan_count;
984 $oldbiblio->{wthdrawncount} = $wthdrawn_count;
985 $oldbiblio->{itemlostcount} = $itemlost_count;
986 $oldbiblio->{bindingcount} = $itembinding_count;
987 $oldbiblio->{orderedcount} = $ordered_count;
990 # Ugh ... this is ugly, I'll re-write it better above then delete it
991 # my $norequests = 1;
995 # foreach my $itm (@items) {
996 # $norequests = 0 unless $itm->{'itemnotforloan'};
999 # $oldbiblio->{'noitems'} = $noitems;
1000 # $oldbiblio->{'norequests'} = $norequests;
1001 # $oldbiblio->{'even'} = $even = not $even;
1002 # $oldbiblio->{'itemcount'} = $counts{'total'};
1003 # my $totalitemcounts = 0;
1004 # foreach my $key (keys %counts){
1005 # if ($key ne 'total'){
1006 # $totalitemcounts+= $counts{$key};
1007 # $oldbiblio->{'locationhash'}->{$key}=$counts{$key};
1010 # my ($locationtext, $locationtextonly, $notavailabletext) = ('','','');
1011 # foreach (sort keys %{$oldbiblio->{'locationhash'}}) {
1012 # if ($_ eq 'notavailable') {
1013 # $notavailabletext="Not available";
1014 # my $c=$oldbiblio->{'locationhash'}->{$_};
1015 # $oldbiblio->{'not-available-p'}=$c;
1017 # $locationtext.="$_";
1018 # my $c=$oldbiblio->{'locationhash'}->{$_};
1019 # if ($_ eq 'Item Lost') {
1020 # $oldbiblio->{'lost-p'} = $c;
1021 # } elsif ($_ eq 'Withdrawn') {
1022 # $oldbiblio->{'withdrawn-p'} = $c;
1023 # } elsif ($_ eq 'On Loan') {
1024 # $oldbiblio->{'on-loan-p'} = $c;
1026 # $locationtextonly.= $_;
1027 # $locationtextonly.= " ($c)<br/> " if $totalitemcounts > 1;
1029 # if ($totalitemcounts>1) {
1030 # $locationtext.=" ($c)<br/> ";
1034 # if ($notavailabletext) {
1035 # $locationtext.= $notavailabletext;
1037 # $locationtext=~s/, $//;
1039 # $oldbiblio->{'location'} = $locationtext;
1040 # $oldbiblio->{'location-only'} = $locationtextonly;
1041 # $oldbiblio->{'use-location-flags-p'} = 1;
1043 push( @newresults, $oldbiblio );
1049 #----------------------------------------------------------------------
1051 # Non-Zebra GetRecords#
1052 #----------------------------------------------------------------------
1055 NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1060 $koha_query, $federated_query, $sort_by_ref,
1061 $servers_ref, $results_per_page, $offset,
1062 $expanded_facet, $branches, $query_type,
1065 my $result = NZanalyse($koha_query);
1067 # warn "==========".@$sort_by_ref[0];
1068 return (undef,NZorder($result,@$sort_by_ref[0]),undef);
1073 NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1074 the list is builded from inverted index in nozebra SQL table
1075 note that title is here only for convenience : the sorting will be very fast when requested on title
1076 if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1082 # if we have a ", replace the content to discard temporarily any and/or/not inside
1084 if ($string =~/"/) {
1085 $string =~ s/"(.*?)"/__X__/;
1087 # print "commacontent : $commacontent\n";
1089 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1090 # then, call again NZanalyse with $left and $right
1091 # (recursive until we find a leaf (=> something without and/or/not)
1092 $string =~ /(.*)( and | or | not )(.*)/;
1096 # it's not a leaf, we have a and/or/not
1098 # reintroduce comma content if needed
1099 $right =~ s/__X__/"$commacontent"/ if $commacontent;
1100 $left =~ s/__X__/"$commacontent"/ if $commacontent;
1101 # print "noeud : $left / $operand / $right\n";
1102 my $leftresult = NZanalyse($left);
1103 my $rightresult = NZanalyse($right);
1104 # OK, we have the results for right and left part of the query
1105 # depending of operand, intersect, union or exclude both lists
1106 # to get a result list
1107 if ($operand eq ' and ') {
1108 my @leftresult = split /,/, $leftresult;
1109 # my @rightresult = split /,/,$leftresult;
1111 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1112 # the result is stored twice, to have the same weight for AND than OR.
1113 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1114 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1115 foreach (@leftresult) {
1116 if ($rightresult =~ "$_,") {
1117 $finalresult .= "$_,$_,";
1120 return $finalresult;
1121 } elsif ($operand eq ' or ') {
1122 # just merge the 2 strings
1123 return $leftresult.$rightresult;
1124 } elsif ($operand eq ' not ') {
1125 my @leftresult = split /,/, $leftresult;
1126 # my @rightresult = split /,/,$leftresult;
1128 foreach (@leftresult) {
1129 unless ($rightresult =~ "$_,") {
1130 $finalresult .= "$_,";
1133 return $finalresult;
1135 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1136 die "error : operand unknown : $operand for $string";
1138 # it's a leaf, do the real SQL query and return the result
1140 $string =~ s/__X__/"$commacontent"/ if $commacontent;
1141 $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|<|>|&|\+|\*|\// /g;
1142 # print "feuille : $string\n";
1143 # parse the string in in operator/operand/value again
1144 $string =~ /(.*)(=|>|>=|<|<=)(.*)/;
1150 #do a specific search
1151 my $dbh = C4::Context->dbh;
1152 $operator='LIKE' if $operator eq '=' and $right=~ /%/;
1153 my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE indexname=? AND value $operator ?");
1154 # print "$left / $operator / $right\n";
1155 # split each word, query the DB and build the biblionumbers result
1156 foreach (split / /,$right) {
1158 $sth->execute($left,$_);
1159 while (my $line = $sth->fetchrow) {
1160 $biblionumbers .= $line;
1162 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1164 my @leftresult = split /,/, $biblionumbers;
1166 foreach (@leftresult) {
1167 if ($results =~ "$_,") {
1173 $results = $biblionumbers;
1177 #do a complete search (all indexes)
1178 my $dbh = C4::Context->dbh;
1179 my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE value LIKE ?");
1180 # split each word, query the DB and build the biblionumbers result
1181 foreach (split / /,$string) {
1184 while (my $line = $sth->fetchrow) {
1185 $biblionumbers .= $line;
1187 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1189 my @leftresult = split /,/, $biblionumbers;
1191 foreach (@leftresult) {
1192 if ($results =~ "$_,") {
1198 $results = $biblionumbers;
1207 my ($biblionumbers, $ordering) = @_;
1208 # order title asc by default
1209 $ordering = '1=36 <i' unless $ordering;
1210 my $dbh = C4::Context->dbh;
1212 # order by POPULARITY
1214 if ($ordering =~ /1=9523/) {
1217 # popularity is not in MARC record, it's builded from a specific query
1218 my $sth = $dbh->prepare("select sum(issues) from items where biblionumber=?");
1219 foreach (split /,/,$biblionumbers) {
1220 my ($biblionumber,$title) = split /;/,$_;
1221 $result{$biblionumber}=GetMarcBiblio($biblionumber);
1222 $sth->execute($biblionumber);
1223 my $popularity= $sth->fetchrow ||0;
1224 # hint : the key is popularity.title because we can have
1225 # many results with the same popularity. In this cas, sub-ordering is done by title
1226 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
1227 # (un-frequent, I agree, but we won't forget anything that way ;-)
1228 $popularity{sprintf("%10d",$popularity).$title.$biblionumber} = $biblionumber;
1230 # sort the hash and return the same structure as GetRecords (Zebra querying)
1233 if ($ordering eq '1=9523 >i') { # sort popularity DESC
1234 foreach my $key (sort {$b <=> $a} (keys %popularity)) {
1235 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1237 } else { # sort popularity ASC
1238 foreach my $key (sort (keys %popularity)) {
1239 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1243 $result_hash->{'hits'} = $numbers;
1244 $finalresult->{'biblioserver'} = $result_hash;
1245 return $finalresult;
1249 } elsif ($ordering eq '1=1003 <i'){
1251 foreach (split /,/,$biblionumbers) {
1252 my ($biblionumber,$title) = split /;/,$_;
1253 my $record=GetMarcBiblio($biblionumber);
1255 if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1256 $author=$record->subfield('200','f');
1257 $author=$record->subfield('700','a') unless $author;
1259 $author=$record->subfield('100','a');
1261 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1262 # and we don't want to get only 1 result for each of them !!!
1263 $result{$author.$biblionumber}=$record;
1265 # sort the hash and return the same structure as GetRecords (Zebra querying)
1268 if ($ordering eq '1=1003 <i') { # sort by title desc
1269 foreach my $key (sort (keys %result)) {
1270 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1272 } else { # sort by title ASC
1273 foreach my $key (sort { $a <=> $b } (keys %result)) {
1274 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1278 $result_hash->{'hits'} = $numbers;
1279 $finalresult->{'biblioserver'} = $result_hash;
1280 return $finalresult;
1282 # ORDER BY callnumber
1284 } elsif ($ordering eq '1=20 <i'){
1286 foreach (split /,/,$biblionumbers) {
1287 my ($biblionumber,$title) = split /;/,$_;
1288 my $record=GetMarcBiblio($biblionumber);
1290 my ($callnumber_tag,$callnumber_subfield)=GetMarcFromKohaField($dbh,'items.itemcallnumber');
1291 ($callnumber_tag,$callnumber_subfield)= GetMarcFromKohaField('biblioitems.callnumber') unless $callnumber_tag;
1292 if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1293 $callnumber=$record->subfield('200','f');
1295 $callnumber=$record->subfield('100','a');
1297 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1298 # and we don't want to get only 1 result for each of them !!!
1299 $result{$callnumber.$biblionumber}=$record;
1301 # sort the hash and return the same structure as GetRecords (Zebra querying)
1304 if ($ordering eq '1=1003 <i') { # sort by title desc
1305 foreach my $key (sort (keys %result)) {
1306 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1308 } else { # sort by title ASC
1309 foreach my $key (sort { $a <=> $b } (keys %result)) {
1310 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1314 $result_hash->{'hits'} = $numbers;
1315 $finalresult->{'biblioserver'} = $result_hash;
1316 return $finalresult;
1317 } elsif ($ordering =~ /1=31/){ #pub year
1319 foreach (split /,/,$biblionumbers) {
1320 my ($biblionumber,$title) = split /;/,$_;
1321 my $record=GetMarcBiblio($biblionumber);
1322 my ($publicationyear_tag,$publicationyear_subfield)=GetMarcFromKohaField($dbh,'biblioitems.publicationyear');
1323 my $publicationyear=$record->subfield($publicationyear_tag,$publicationyear_subfield);
1324 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1325 # and we don't want to get only 1 result for each of them !!!
1326 $result{$publicationyear.$biblionumber}=$record;
1328 # sort the hash and return the same structure as GetRecords (Zebra querying)
1331 if ($ordering eq '1=31 <i') { # sort by title desc
1332 foreach my $key (sort (keys %result)) {
1333 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1335 } else { # sort by title ASC
1336 foreach my $key (sort { $a <=> $b } (keys %result)) {
1337 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1341 $result_hash->{'hits'} = $numbers;
1342 $finalresult->{'biblioserver'} = $result_hash;
1343 return $finalresult;
1348 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
1350 foreach (split /,/,$biblionumbers) {
1351 my ($biblionumber,$title) = split /;/,$_;
1352 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1353 # and we don't want to get only 1 result for each of them !!!
1354 $result{$title.$biblionumber}=GetMarcBiblio($biblionumber);
1356 # sort the hash and return the same structure as GetRecords (Zebra querying)
1359 if ($ordering eq '1=36 <i') { # sort by title desc
1360 foreach my $key (sort (keys %result)) {
1361 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1363 } else { # sort by title ASC
1364 foreach my $key (sort { $a <=> $b } (keys %result)) {
1365 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1369 $result_hash->{'hits'} = $numbers;
1370 $finalresult->{'biblioserver'} = $result_hash;
1371 return $finalresult;
1374 END { } # module clean-up code here (global destructor)
1381 Koha Developement team <info@koha.org>