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.
60 # make all your functions, whether exported or not;
62 =head2 findseealso($dbh,$fields);
64 C<$dbh> is a link to the DB handler.
67 my $dbh =C4::Context->dbh;
69 C<$fields> is a reference to the fields array
71 This function modify the @$fields array and add related fields to search on.
76 my ( $dbh, $fields ) = @_;
77 my $tagslib = GetMarcStructure( $dbh, 1 );
78 for ( my $i = 0 ; $i <= $#{$fields} ; $i++ ) {
79 my ($tag) = substr( @$fields[$i], 1, 3 );
80 my ($subfield) = substr( @$fields[$i], 4, 1 );
81 @$fields[$i] .= ',' . $tagslib->{$tag}->{$subfield}->{seealso}
82 if ( $tagslib->{$tag}->{$subfield}->{seealso} );
88 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
95 my $dbh = C4::Context->dbh;
96 my $result = TransformMarcToKoha( $dbh, $record, '' );
101 my ( $biblionumber, $title );
103 # search duplicate on ISBN, easy and fast..
104 #$search->{'avoidquerylog'}=1;
105 if ( $result->{isbn} ) {
106 $query = "isbn=$result->{isbn}";
109 $result->{title} =~ s /\\//g;
110 $result->{title} =~ s /\"//g;
111 $result->{title} =~ s /\(//g;
112 $result->{title} =~ s /\)//g;
113 $query = "ti,ext=$result->{title}";
115 my ($possible_duplicate_record) =
116 C4::Biblio::getRecord( "biblioserver", $query, "usmarc" ); # FIXME :: hardcoded !
117 if ($possible_duplicate_record) {
119 MARC::Record->new_from_usmarc($possible_duplicate_record);
120 my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
122 # FIXME :: why 2 $biblionumber ?
123 return $result->{'biblionumber'}, $result->{'biblionumber'},
131 ($error,$results) = SimpleSearch($query,@servers);
133 this function performs a simple search on the catalog using zoom.
139 * $query could be a simple keyword or a complete CCL query wich is depending on your ccl file.
140 * @servers is optionnal. default one is read on koha.xml
143 * $error is a string which containt the description error if there is one. Else it's empty.
144 * \@results is an array of marc record.
146 =item C<usage in the script:>
150 my ($error, $marcresults) = SimpleSearch($query);
152 if (defined $error) {
153 $template->param(query_error => $error);
154 warn "error: ".$error;
155 output_html_with_http_headers $input, $cookie, $template->output;
159 my $hits = scalar @$marcresults;
162 for(my $i=0;$i<$hits;$i++) {
164 my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
165 my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
167 #build the hash for the template.
168 $resultsloop{highlight} = ($i % 2)?(1):(0);
169 $resultsloop{title} = $biblio->{'title'};
170 $resultsloop{subtitle} = $biblio->{'subtitle'};
171 $resultsloop{biblionumber} = $biblio->{'biblionumber'};
172 $resultsloop{author} = $biblio->{'author'};
173 $resultsloop{publishercode} = $biblio->{'publishercode'};
174 $resultsloop{publicationyear} = $biblio->{'publicationyear'};
176 push @results, \%resultsloop;
178 $template->param(result=>\@results);
188 return ( "No query entered", undef ) unless $query;
190 #@servers = (C4::Context->config("biblioserver")) unless @servers;
192 ("biblioserver") unless @servers
193 ; # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
196 for ( my $i = 0 ; $i < @servers ; $i++ ) {
197 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
200 ->search( new ZOOM::Query::CCL2RPN( $query, $zconns[$i] ) );
202 # getting error message if one occured.
204 $zconns[$i]->errmsg() . " ("
205 . $zconns[$i]->errcode() . ") "
206 . $zconns[$i]->addinfo() . " "
207 . $zconns[$i]->diagset();
209 return ( $error, undef ) if $zconns[$i]->errcode();
213 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
214 $ev = $zconns[ $i - 1 ]->last_event();
215 if ( $ev == ZOOM::Event::ZEND ) {
216 $hits = $tmpresults[ $i - 1 ]->size();
219 for ( my $j = 0 ; $j < $hits ; $j++ ) {
220 my $record = $tmpresults[ $i - 1 ]->record($j)->raw();
221 push @results, $record;
225 return ( undef, \@results );
228 # performs the search
231 $koha_query, $federated_query, $sort_by_ref,
232 $servers_ref, $results_per_page, $offset,
233 $expanded_facet, $branches, $query_type,
237 my @servers = @$servers_ref;
238 my @sort_by = @$sort_by_ref;
240 # create the zoom connection and query object
244 my $results_hashref = ();
247 my $facets_counter = ();
248 my $facets_info = ();
249 my $facets = getFacets();
251 #### INITIALIZE SOME VARS USED CREATE THE FACETED RESULTS
252 my @facets_loop; # stores the ref to array of hashes for template
253 for ( my $i = 0 ; $i < @servers ; $i++ ) {
254 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
256 # perform the search, create the results objects
257 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
259 if ( $servers[$i] =~ /biblioserver/ ) {
260 $query_to_use = $koha_query;
263 $query_to_use = $federated_query;
266 # warn "HERE : $query_type => $query_to_use";
267 # check if we've got a query_type defined
271 if ( $query_type =~ /^ccl/ ) {
273 s/\:/\=/g; # change : to = last minute (FIXME)
275 # warn "CCL : $query_to_use";
278 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
281 elsif ( $query_type =~ /^cql/ ) {
283 # warn "CQL : $query_to_use";
286 new ZOOM::Query::CQL( $query_to_use, $zconns[$i] ) );
288 elsif ( $query_type =~ /^pqf/ ) {
290 # warn "PQF : $query_to_use";
293 new ZOOM::Query::PQF( $query_to_use, $zconns[$i] ) );
299 # warn "preparing to scan";
302 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
307 # warn "LAST : $query_to_use";
310 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
316 warn "prob with query toto $query_to_use " . $@;
319 # concatenate the sort_by limits and pass them to the results object
321 foreach my $sort (@sort_by) {
322 $sort_by .= $sort . " "; # used to be $sort,
324 $results[$i]->sort( "yaz", $sort_by ) if $sort_by;
326 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
327 my $ev = $zconns[ $i - 1 ]->last_event();
328 if ( $ev == ZOOM::Event::ZEND ) {
329 my $size = $results[ $i - 1 ]->size();
332 #$results_hash->{'server'} = $servers[$i-1];
333 # loop through the results
334 $results_hash->{'hits'} = $size;
336 if ( $offset + $results_per_page <= $size ) {
337 $times = $offset + $results_per_page;
342 for ( my $j = $offset ; $j < $times ; $j++ )
343 { #(($offset+$count<=$size) ? ($offset+$count):$size) ; $j++){
347 ## This is just an index scan
349 my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
351 # here we create a minimal MARC record and hand it off to the
352 # template just like a normal result ... perhaps not ideal, but
354 my $tmprecord = MARC::Record->new();
355 $tmprecord->encoding('UTF-8');
358 # srote the minimal record in author/title (depending on MARC flavour)
359 if ( C4::Context->preference("marcflavour") eq
362 $tmptitle = MARC::Field->new(
369 $tmptitle = MARC::Field->new(
375 $tmprecord->append_fields($tmptitle);
376 $results_hash->{'RECORDS'}[$j] =
377 $tmprecord->as_usmarc();
380 $record = $results[ $i - 1 ]->record($j)->raw();
382 #warn "RECORD $j:".$record;
383 $results_hash->{'RECORDS'}[$j] =
384 $record; # making a reference to a hash
385 # Fill the facets while we're looping
386 $facet_record = MARC::Record->new_from_usmarc($record);
388 #warn $servers[$i-1].$facet_record->title();
389 for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
390 if ( $facets->[$k] ) {
392 for my $tag ( @{ $facets->[$k]->{'tags'} } ) {
393 push @fields, $facet_record->field($tag);
395 for my $field (@fields) {
396 my @subfields = $field->subfields();
397 for my $subfield (@subfields) {
398 my ( $code, $data ) = @$subfield;
400 $facets->[$k]->{'subfield'} )
402 $facets_counter->{ $facets->[$k]
403 ->{'link_value'} }->{$data}++;
407 $facets_info->{ $facets->[$k]->{'link_value'} }
409 $facets->[$k]->{'label_value'};
410 $facets_info->{ $facets->[$k]->{'link_value'} }
411 ->{'expanded'} = $facets->[$k]->{'expanded'};
416 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
419 #print "connection ", $i-1, ": $size hits";
420 #print $results[$i-1]->record(0)->render() if $size > 0;
423 sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
424 keys %$facets_counter
428 my $number_of_facets;
429 my @this_facets_array;
432 $facets_counter->{$link_value}
433 ->{$b} <=> $facets_counter->{$link_value}->{$a}
434 } keys %{ $facets_counter->{$link_value} }
438 if ( ( $number_of_facets < 6 )
439 || ( $expanded_facet eq $link_value )
440 || ( $facets_info->{$link_value}->{'expanded'} ) )
443 # sanitize the link value ), ( will cause errors with CCL
444 my $facet_link_value = $one_facet;
445 $facet_link_value =~ s/(\(|\))/ /g;
447 # fix the length that will display in the label
448 my $facet_label_value = $one_facet;
449 $facet_label_value = substr( $one_facet, 0, 20 ) . "..."
450 unless length($facet_label_value) <= 20;
452 # well, if it's a branch, label by the name, not the code
453 if ( $link_value =~ /branch/ ) {
455 $branches->{$one_facet}->{'branchname'};
458 # but we're down with the whole label being in the link's title
459 my $facet_title_value = $one_facet;
461 push @this_facets_array,
465 $facets_counter->{$link_value}->{$one_facet},
466 facet_label_value => $facet_label_value,
467 facet_title_value => $facet_title_value,
468 facet_link_value => $facet_link_value,
469 type_link_value => $link_value,
474 unless ( $facets_info->{$link_value}->{'expanded'} ) {
476 if ( ( $number_of_facets > 6 )
477 && ( $expanded_facet ne $link_value ) );
482 type_link_value => $link_value,
483 type_id => $link_value . "_id",
485 $facets_info->{$link_value}->{'label_value'},
486 facets => \@this_facets_array,
487 expandable => $expandable,
488 expand => $link_value,
495 warn Dumper($results_hashref);
496 return ( undef, $results_hashref, \@facets_loop );
499 # build the query itself
501 my ( $query, $operators, $operands, $indexes, $limits, $sort_by ) = @_;
503 my @operators = @$operators if $operators;
504 my @indexes = @$indexes if $indexes;
505 my @operands = @$operands if $operands;
506 my @limits = @$limits if $limits;
507 my @sort_by = @$sort_by if $sort_by;
509 my $human_search_desc; # a human-readable query
510 my $machine_search_desc; #a machine-readable query
511 # FIXME: the locale should be set based on the syspref
512 my $stemmer = Lingua::Stem->new( -locale => 'EN-US' );
514 # FIXME: these should be stored in the db so the librarian can modify the behavior
515 $stemmer->add_exceptions(
523 # STEP I: determine if this is a form-based / simple query or if it's complex (if complex,
524 # we can't handle field weighting, stemming until a formal query parser is written
525 # I'll work on this soon -- JF
526 #if (!$query) { # form-based
527 # check if this is a known query language query, if it is, return immediately:
528 if ( $query =~ /^ccl=/ ) {
529 return ( undef, $', $', $', 'ccl' );
531 if ( $query =~ /^cql=/ ) {
532 return ( undef, $', $', $', 'cql' );
534 if ( $query =~ /^pqf=/ ) {
535 return ( undef, $', $', $', 'pqf' );
537 if ( $query =~ /(\(|\))/ ) { # sorry, too complex
538 return ( undef, $query, $query, $query, 'ccl' );
541 # form-based queries are limited to non-nested a specific depth, so we can easily
542 # modify the incoming query operands and indexes to do stemming and field weighting
543 # Once we do so, we'll end up with a value in $query, just like if we had an
544 # incoming $query from the user
547 ; # clear it out so we can populate properly with field-weighted stemmed query
549 ; # a flag used to keep track if there was a previous query
550 # if there was, we can apply the current operator
551 for ( my $i = 0 ; $i <= @operands ; $i++ ) {
552 my $operand = $operands[$i];
553 my $index = $indexes[$i];
555 my $stemming = C4::Context->parameters("Stemming") || 0;
556 my $weight_fields = C4::Context->parameters("WeightFields") || 0;
558 if ( $operands[$i] ) {
560 # STEMMING FIXME: need to refine the field weighting so stemmed operands don't disrupt the query ranking
562 my @words = split( / /, $operands[$i] );
563 my $stems = $stemmer->stem(@words);
564 foreach my $stem (@$stems) {
565 $stemmed_operand .= "$stem";
566 $stemmed_operand .= "?"
567 unless ( $stem =~ /(and$|or$|not$)/ )
568 || ( length($stem) < 3 );
569 $stemmed_operand .= " ";
571 #warn "STEM: $stemmed_operand";
574 #$operand = $stemmed_operand;
577 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
578 # pretty well but will work much better when we have an actual query parser
580 if ($weight_fields) {
582 " rk=("; # Specifies that we're applying rank
583 # keyword has different weight properties
584 if ( ( $index =~ /kw/ ) || ( !$index ) )
585 { # FIXME: do I need to add right-truncation in the case of stemming?
586 # a simple way to find out if this query uses an index
587 if ( $operand =~ /(\=|\:)/ ) {
588 $weighted_query .= " $operand";
592 " Title-cover,ext,r1=\"$operand\""
593 ; # index label as exact
595 " or ti,ext,r2=$operand"; # index as exact
596 #$weighted_query .= " or ti,phr,r3=$operand"; # index as phrase
597 #$weighted_query .= " or any,ext,r4=$operand"; # index as exact
599 " or kw,wrdl,r5=$operand"; # index as exact
600 $weighted_query .= " or wrd,fuzzy,r9=$operand";
601 $weighted_query .= " or wrd=$stemmed_operand"
605 elsif ( $index =~ /au/ ) {
607 " $index,ext,r1=$operand"; # index label as exact
608 #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
610 " or $index,phr,r3=$operand"; # index as phrase
611 $weighted_query .= " or $index,rt,wrd,r3=$operand";
613 elsif ( $index =~ /ti/ ) {
615 " Title-cover,ext,r1=$operand"; # index label as exact
616 $weighted_query .= " or Title-series,ext,r2=$operand";
618 #$weighted_query .= " or ti,ext,r2=$operand";
619 #$weighted_query .= " or ti,phr,r3=$operand";
620 #$weighted_query .= " or ti,wrd,r3=$operand";
622 " or (title-sort-az=0 or Title-cover,startswithnt,st-word,r3=$operand #)";
624 " or (title-sort-az=0 or Title-cover,phr,r6=$operand)";
626 #$weighted_query .= " or Title-cover,wrd,r5=$operand";
627 #$weighted_query .= " or ti,ext,r6=$operand";
628 #$weighted_query .= " or ti,startswith,phr,r7=$operand";
629 #$weighted_query .= " or ti,phr,r8=$operand";
630 #$weighted_query .= " or ti,wrd,r9=$operand";
632 #$weighted_query .= " or ti,ext,r2=$operand"; # index as exact
633 #$weighted_query .= " or ti,phr,r3=$operand"; # index as phrase
634 #$weighted_query .= " or any,ext,r4=$operand"; # index as exact
635 #$weighted_query .= " or kw,wrd,r5=$operand"; # index as exact
639 " $index,ext,r1=$operand"; # index label as exact
640 #$weighted_query .= " or $index,ext,r2=$operand"; # index as exact
642 " or $index,phr,r3=$operand"; # index as phrase
643 $weighted_query .= " or $index,rt,wrd,r3=$operand";
645 " or $index,wrd,r5=$operand"
646 ; # index as word right-truncated
647 $weighted_query .= " or $index,wrd,fuzzy,r8=$operand";
649 $weighted_query .= ")"; # close rank specification
650 $operand = $weighted_query;
653 # only add an operator if there is a previous operand
654 if ($previous_operand) {
655 if ( $operators[ $i - 1 ] ) {
656 $query .= " $operators[$i-1] $index: $operand";
658 $human_search_desc .=
659 " $operators[$i-1] $operands[$i]";
662 $human_search_desc .=
663 " $operators[$i-1] $index: $operands[$i]";
667 # the default operator is and
669 $query .= " and $index: $operand";
670 $human_search_desc .= " and $index: $operands[$i]";
675 $query .= " $operand";
676 $human_search_desc .= " $operands[$i]";
679 $query .= " $index: $operand";
680 $human_search_desc .= " $index: $operands[$i]";
682 $previous_operand = 1;
690 my $limit_search_desc;
691 foreach my $limit (@limits) {
693 # FIXME: not quite right yet ... will work on this soon -- JF
694 my $type = $1 if $limit =~ m/([^:]+):([^:]*)/;
695 if ( $limit =~ /available/ ) {
697 " (($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))";
699 #$limit_search_desc.=" and available";
701 elsif ( ($limit_query) && ( index( $limit_query, $type, 0 ) > 0 ) ) {
702 if ( $limit_query !~ /\(/ ) {
704 substr( $limit_query, 0, index( $limit_query, $type, 0 ) )
706 . substr( $limit_query, index( $limit_query, $type, 0 ) )
710 substr( $limit_search_desc, 0,
711 index( $limit_search_desc, $type, 0 ) )
713 . substr( $limit_search_desc,
714 index( $limit_search_desc, $type, 0 ) )
720 chop $limit_search_desc;
721 $limit_query .= " or $limit )" if $limit;
722 $limit_search_desc .= " or $limit )" if $limit;
725 elsif ( ($limit_query) && ( $limit =~ /mc/ ) ) {
726 $limit_query .= " or $limit" if $limit;
727 $limit_search_desc .= " or $limit" if $limit;
730 # these are treated as AND
731 elsif ($limit_query) {
732 if ($limit =~ /branch/){
733 $limit_query .= " ) and ( $limit" if $limit;
734 $limit_search_desc .= " ) and ( $limit" if $limit;
736 $limit_query .= " or $limit" if $limit;
737 $limit_search_desc .= " or $limit" if $limit;
741 # otherwise, there is nothing but the limit
743 $limit_query .= "$limit" if $limit;
744 $limit_search_desc .= "$limit" if $limit;
748 # if there's also a query, we need to AND the limits to it
749 if ( ($limit_query) && ($query) ) {
750 $limit_query = " and (" . $limit_query . ")";
751 $limit_search_desc = " and ($limit_search_desc)" if $limit_search_desc;
754 $query .= $limit_query;
755 $human_search_desc .= $limit_search_desc;
757 # now normalize the strings
758 $query =~ s/ / /g; # remove extra spaces
759 $query =~ s/^ //g; # remove any beginning spaces
760 $query =~ s/:/=/g; # causes probs for server
761 $query =~ s/==/=/g; # remove double == from query
763 my $federated_query = $human_search_desc;
764 $federated_query =~ s/ / /g;
765 $federated_query =~ s/^ //g;
766 $federated_query =~ s/:/=/g;
767 my $federated_query_opensearch = $federated_query;
769 # my $federated_query_RPN = new ZOOM::Query::CCL2RPN( $query , C4::Context->ZConn('biblioserver'));
771 $human_search_desc =~ s/ / /g;
772 $human_search_desc =~ s/^ //g;
773 my $koha_query = $query;
775 #warn "QUERY:".$koha_query;
776 #warn "SEARCHDESC:".$human_search_desc;
777 #warn "FEDERATED QUERY:".$federated_query;
778 return ( undef, $human_search_desc, $koha_query, $federated_query );
781 # IMO this subroutine is pretty messy still -- it's responsible for
782 # building the HTML output for the template
784 my ( $searchdesc, $hits, $results_per_page, $offset, @marcresults ) = @_;
786 my $dbh = C4::Context->dbh;
790 my $span_terms_hashref;
791 for my $span_term ( split( / /, $searchdesc ) ) {
792 $span_term =~ s/(.*=|\)|\(|\+|\.)//g;
793 $span_terms_hashref->{$span_term}++;
796 #Build brancnames hash
798 #get branch information.....
801 $dbh->prepare("SELECT branchcode,branchname FROM branches")
802 ; # FIXME : use C4::Koha::GetBranches
804 while ( my $bdata = $bsth->fetchrow_hashref ) {
805 $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
809 #find itemtype & itemtype image
812 $dbh->prepare("SELECT itemtype,description,imageurl,summary FROM itemtypes");
814 while ( my $bdata = $bsth->fetchrow_hashref ) {
815 $itemtypes{ $bdata->{'itemtype'} }->{description} =
816 $bdata->{'description'};
817 $itemtypes{ $bdata->{'itemtype'} }->{imageurl} = $bdata->{'imageurl'};
818 $itemtypes{ $bdata->{'itemtype'} }->{summary} = $bdata->{'summary'};
821 #search item field code
824 "select tagfield from marc_subfield_structure where kohafield like 'items.itemnumber'"
827 my ($itemtag) = $sth->fetchrow;
829 ## find column names of items related to MARC
830 my $sth2 = $dbh->prepare("SHOW COLUMNS from items");
832 my %subfieldstosearch;
833 while ( ( my $column ) = $sth2->fetchrow ) {
834 my ( $tagfield, $tagsubfield ) =
835 &GetMarcFromKohaField( "items." . $column, "" );
836 $subfieldstosearch{$column} = $tagsubfield;
840 if ( $hits && $offset + $results_per_page <= $hits ) {
841 $times = $offset + $results_per_page;
847 for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
849 $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
851 my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, '' );
853 # add image url if there is one
854 if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} =~ /^http:/ ) {
855 $oldbiblio->{imageurl} =
856 $itemtypes{ $oldbiblio->{itemtype} }->{imageurl};
857 $oldbiblio->{description} =
858 $itemtypes{ $oldbiblio->{itemtype} }->{description};
861 $oldbiblio->{imageurl} =
862 getitemtypeimagesrc() . "/"
863 . $itemtypes{ $oldbiblio->{itemtype} }->{imageurl}
864 if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
865 $oldbiblio->{description} =
866 $itemtypes{ $oldbiblio->{itemtype} }->{description};
869 # build summary if there is one (the summary is defined in itemtypes table
871 if ($itemtypes{ $oldbiblio->{itemtype} }->{summary}) {
872 my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
873 my @fields = $marcrecord->fields();
874 foreach my $field (@fields) {
875 my $tag = $field->tag();
876 my $tagvalue = $field->as_string();
877 $summary =~ s/\[(.?.?.?.?)$tag\*(.*?)]/$1$tagvalue$2\[$1$tag$2]/g;
879 my @subf = $field->subfields;
880 for my $i (0..$#subf) {
881 my $subfieldcode = $subf[$i][0];
882 my $subfieldvalue = $subf[$i][1];
883 my $tagsubf = $tag.$subfieldcode;
884 $summary =~ s/\[(.?.?.?.?)$tagsubf(.*?)]/$1$subfieldvalue$2\[$1$tagsubf$2]/g;
888 $summary =~ s/\[(.*?)]//g;
889 $summary =~ s/\n/<br>/g;
890 $oldbiblio->{summary} = $summary;
892 # add spans to search term in results
893 foreach my $term ( keys %$span_terms_hashref ) {
896 my $old_term = $term;
897 if ( length($term) > 3 ) {
898 $term =~ s/(.*=|\)|\(|\+|\.|\?)//g;
900 #FIXME: is there a better way to do this?
901 $oldbiblio->{'title'} =~ s/$term/<span class=term>$&<\/span>/gi;
902 $oldbiblio->{'subtitle'} =~
903 s/$term/<span class=term>$&<\/span>/gi;
905 $oldbiblio->{'author'} =~ s/$term/<span class=term>$&<\/span>/gi;
906 $oldbiblio->{'publishercode'} =~ s/$term/<span class=term>$&<\/span>/gi;
907 $oldbiblio->{'place'} =~ s/$term/<span class=term>$&<\/span>/gi;
908 $oldbiblio->{'pages'} =~ s/$term/<span class=term>$&<\/span>/gi;
909 $oldbiblio->{'notes'} =~ s/$term/<span class=term>$&<\/span>/gi;
910 $oldbiblio->{'size'} =~ s/$term/<span class=term>$&<\/span>/gi;
920 $oldbiblio->{'toggle'} = $toggle;
921 my @fields = $marcrecord->field($itemtag);
924 my $ordered_count = 0;
925 my $onloan_count = 0;
926 my $wthdrawn_count = 0;
927 my $itemlost_count = 0;
928 my $itembinding_count = 0;
931 foreach my $field (@fields) {
933 foreach my $code ( keys %subfieldstosearch ) {
934 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
936 if ( $item->{wthdrawn} ) {
939 elsif ( $item->{notforloan} == -1 ) {
943 elsif ( $item->{itemlost} ) {
946 elsif ( $item->{binding} ) {
947 $itembinding_count++;
949 elsif ( ( $item->{onloan} ) && ( $item->{onloan} != '0000-00-00' ) )
956 if ( $item->{'homebranch'} ) {
957 $items->{ $item->{'homebranch'} }->{count}++;
961 elsif ( $item->{'holdingbranch'} ) {
962 $items->{ $item->{'homebranch'} }->{count}++;
964 $items->{ $item->{homebranch} }->{itemcallnumber} =
965 $item->{itemcallnumber};
966 $items->{ $item->{homebranch} }->{location} =
969 } # notforloan, item level and biblioitem level
970 for my $key ( keys %$items ) {
974 branchname => $branches{$key},
976 count => $items->{$key}->{count},
977 itemcallnumber => $items->{$key}->{itemcallnumber},
978 location => $items->{$key}->{location},
980 push @items_loop, $this_item;
982 $oldbiblio->{norequests} = $norequests;
983 $oldbiblio->{items_loop} = \@items_loop;
984 $oldbiblio->{onloancount} = $onloan_count;
985 $oldbiblio->{wthdrawncount} = $wthdrawn_count;
986 $oldbiblio->{itemlostcount} = $itemlost_count;
987 $oldbiblio->{bindingcount} = $itembinding_count;
988 $oldbiblio->{orderedcount} = $ordered_count;
991 # Ugh ... this is ugly, I'll re-write it better above then delete it
992 # my $norequests = 1;
996 # foreach my $itm (@items) {
997 # $norequests = 0 unless $itm->{'itemnotforloan'};
1000 # $oldbiblio->{'noitems'} = $noitems;
1001 # $oldbiblio->{'norequests'} = $norequests;
1002 # $oldbiblio->{'even'} = $even = not $even;
1003 # $oldbiblio->{'itemcount'} = $counts{'total'};
1004 # my $totalitemcounts = 0;
1005 # foreach my $key (keys %counts){
1006 # if ($key ne 'total'){
1007 # $totalitemcounts+= $counts{$key};
1008 # $oldbiblio->{'locationhash'}->{$key}=$counts{$key};
1011 # my ($locationtext, $locationtextonly, $notavailabletext) = ('','','');
1012 # foreach (sort keys %{$oldbiblio->{'locationhash'}}) {
1013 # if ($_ eq 'notavailable') {
1014 # $notavailabletext="Not available";
1015 # my $c=$oldbiblio->{'locationhash'}->{$_};
1016 # $oldbiblio->{'not-available-p'}=$c;
1018 # $locationtext.="$_";
1019 # my $c=$oldbiblio->{'locationhash'}->{$_};
1020 # if ($_ eq 'Item Lost') {
1021 # $oldbiblio->{'lost-p'} = $c;
1022 # } elsif ($_ eq 'Withdrawn') {
1023 # $oldbiblio->{'withdrawn-p'} = $c;
1024 # } elsif ($_ eq 'On Loan') {
1025 # $oldbiblio->{'on-loan-p'} = $c;
1027 # $locationtextonly.= $_;
1028 # $locationtextonly.= " ($c)<br/> " if $totalitemcounts > 1;
1030 # if ($totalitemcounts>1) {
1031 # $locationtext.=" ($c)<br/> ";
1035 # if ($notavailabletext) {
1036 # $locationtext.= $notavailabletext;
1038 # $locationtext=~s/, $//;
1040 # $oldbiblio->{'location'} = $locationtext;
1041 # $oldbiblio->{'location-only'} = $locationtextonly;
1042 # $oldbiblio->{'use-location-flags-p'} = 1;
1044 push( @newresults, $oldbiblio );
1052 ($countchanged,$listunchanged) = EditBiblios($listbiblios, $tagsubfield,$initvalue,$targetvalue,$test);
1054 this function changes all the values $initvalue in subfield $tag$subfield in any record in $listbiblios
1055 test parameter if set donot perform change to records in database.
1061 * $listbiblios is an array ref to marcrecords to be changed
1062 * $tagsubfield is the reference of the subfield to change.
1063 * $initvalue is the value to search the record for
1064 * $targetvalue is the value to set the subfield to
1065 * $test is to be set only not to perform changes in database.
1067 =item C<Output arg:>
1068 * $countchanged counts all the changes performed.
1069 * $listunchanged contains the list of all the biblionumbers of records unchanged.
1071 =item C<usage in the script:>
1075 my ($countchanged, $listunchanged) = EditBiblios($results->{RECORD}, $tagsubfield,$initvalue,$targetvalue);;
1076 #If one wants to display unchanged records, you should get biblios foreach @$listunchanged
1077 $template->param(countchanged => $countchanged, loopunchanged=>$listunchanged);
1081 my ($listbiblios,$tagsubfield,$initvalue,$targetvalue,$test)=@_;
1084 my ($tag,$subfield)=($1,$2) if ($tagsubfield=~/^(\d{1,3})(.)$/);
1085 my ($bntag,$bnsubf) = GetMarcFromKohaField('biblio.biblionumber');
1087 foreach my $usmarc (@$listbiblios){
1088 my $record=MARC::Record->new_from_usmarc($usmarc);
1091 $biblionumber = $record->subfield($bntag,$bnsubf);
1093 $biblionumber=$record->field($bntag)->data;
1095 #GetBiblionumber is to be written.
1096 #Could be replaced by TransformMarcToKoha (But Would be longer)
1097 if ($record->field($tag)){
1098 foreach my $field ($record->field($tag)){
1099 if ($field->delete_subfield('code' =>$subfield,'match'=>qr($initvalue))){
1101 $field->update($subfield,$targetvalue) if ($targetvalue);
1104 # warn $record->as_formatted;
1105 ModBiblio($record,$biblionumber,GetFrameworkCode($biblionumber)) unless ($test);
1107 push @unmatched, $biblionumber;
1110 return ($countmatched,\@unmatched);
1113 #----------------------------------------------------------------------
1115 # Non-Zebra GetRecords#
1116 #----------------------------------------------------------------------
1119 NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1124 $koha_query, $federated_query, $sort_by_ref,
1125 $servers_ref, $results_per_page, $offset,
1126 $expanded_facet, $branches, $query_type,
1129 my $result = NZanalyse($koha_query);
1131 # warn "==========".@$sort_by_ref[0];
1132 return (undef,NZorder($result,@$sort_by_ref[0],$results_per_page,$offset),undef);
1137 NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1138 the list is builded from inverted index in nozebra SQL table
1139 note that title is here only for convenience : the sorting will be very fast when requested on title
1140 if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1146 # if we have a ", replace the content to discard temporarily any and/or/not inside
1148 if ($string =~/"/) {
1149 $string =~ s/"(.*?)"/__X__/;
1151 # print "commacontent : $commacontent\n";
1153 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1154 # then, call again NZanalyse with $left and $right
1155 # (recursive until we find a leaf (=> something without and/or/not)
1156 $string =~ /(.*)( and | or | not )(.*)/;
1160 # it's not a leaf, we have a and/or/not
1162 # reintroduce comma content if needed
1163 $right =~ s/__X__/"$commacontent"/ if $commacontent;
1164 $left =~ s/__X__/"$commacontent"/ if $commacontent;
1165 # print "noeud : $left / $operand / $right\n";
1166 my $leftresult = NZanalyse($left);
1167 my $rightresult = NZanalyse($right);
1168 # OK, we have the results for right and left part of the query
1169 # depending of operand, intersect, union or exclude both lists
1170 # to get a result list
1171 if ($operand eq ' and ') {
1172 my @leftresult = split /,/, $leftresult;
1173 # my @rightresult = split /,/,$leftresult;
1175 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1176 # the result is stored twice, to have the same weight for AND than OR.
1177 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1178 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1179 foreach (@leftresult) {
1180 if ($rightresult =~ "$_,") {
1181 $finalresult .= "$_,$_,";
1184 return $finalresult;
1185 } elsif ($operand eq ' or ') {
1186 # just merge the 2 strings
1187 return $leftresult.$rightresult;
1188 } elsif ($operand eq ' not ') {
1189 my @leftresult = split /,/, $leftresult;
1190 # my @rightresult = split /,/,$leftresult;
1192 foreach (@leftresult) {
1193 unless ($rightresult =~ "$_,") {
1194 $finalresult .= "$_,";
1197 return $finalresult;
1199 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1200 die "error : operand unknown : $operand for $string";
1202 # it's a leaf, do the real SQL query and return the result
1204 $string =~ s/__X__/"$commacontent"/ if $commacontent;
1205 $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|<|>|&|\+|\*|\// /g;
1206 # print "feuille : $string\n";
1207 # parse the string in in operator/operand/value again
1208 $string =~ /(.*)(=|>|>=|<|<=)(.*)/;
1214 #do a specific search
1215 my $dbh = C4::Context->dbh;
1216 $operator='LIKE' if $operator eq '=' and $right=~ /%/;
1217 my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE indexname=? AND value $operator ?");
1218 # print "$left / $operator / $right\n";
1219 # split each word, query the DB and build the biblionumbers result
1220 foreach (split / /,$right) {
1222 $sth->execute($left,$_);
1223 while (my $line = $sth->fetchrow) {
1224 $biblionumbers .= $line;
1226 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1228 my @leftresult = split /;/, $biblionumbers;
1230 foreach (@leftresult) {
1231 if ($results =~ "$_;") {
1237 $results = $biblionumbers;
1241 #do a complete search (all indexes)
1242 my $dbh = C4::Context->dbh;
1243 my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE value LIKE ?");
1244 # split each word, query the DB and build the biblionumbers result
1245 foreach (split / /,$string) {
1248 while (my $line = $sth->fetchrow) {
1249 $biblionumbers .= $line;
1251 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1253 my @leftresult = split /,/, $biblionumbers;
1255 foreach (@leftresult) {
1256 if ($results =~ "$_;") {
1262 $results = $biblionumbers;
1271 my ($biblionumbers, $ordering,$results_per_page,$offset) = @_;
1272 # order title asc by default
1273 # $ordering = '1=36 <i' unless $ordering;
1274 $results_per_page=20 unless $results_per_page;
1275 $offset = 0 unless $offset;
1276 my $dbh = C4::Context->dbh;
1278 # order by POPULARITY
1280 if ($ordering =~ /1=9523/) {
1283 # popularity is not in MARC record, it's builded from a specific query
1284 my $sth = $dbh->prepare("select sum(issues) from items where biblionumber=?");
1285 foreach (split /;/,$biblionumbers) {
1286 my ($biblionumber,$title) = split /,/,$_;
1287 $result{$biblionumber}=GetMarcBiblio($biblionumber);
1288 $sth->execute($biblionumber);
1289 my $popularity= $sth->fetchrow ||0;
1290 # hint : the key is popularity.title because we can have
1291 # many results with the same popularity. In this cas, sub-ordering is done by title
1292 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
1293 # (un-frequent, I agree, but we won't forget anything that way ;-)
1294 $popularity{sprintf("%10d",$popularity).$title.$biblionumber} = $biblionumber;
1296 # sort the hash and return the same structure as GetRecords (Zebra querying)
1299 if ($ordering eq '1=9523 >i') { # sort popularity DESC
1300 foreach my $key (sort {$b <=> $a} (keys %popularity)) {
1301 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1303 } else { # sort popularity ASC
1304 foreach my $key (sort (keys %popularity)) {
1305 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1309 $result_hash->{'hits'} = $numbers;
1310 $finalresult->{'biblioserver'} = $result_hash;
1311 return $finalresult;
1315 } elsif ($ordering eq '1=1003 <i'){
1317 foreach (split /;/,$biblionumbers) {
1318 my ($biblionumber,$title) = split /,/,$_;
1319 my $record=GetMarcBiblio($biblionumber);
1321 if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1322 $author=$record->subfield('200','f');
1323 $author=$record->subfield('700','a') unless $author;
1325 $author=$record->subfield('100','a');
1327 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1328 # and we don't want to get only 1 result for each of them !!!
1329 $result{$author.$biblionumber}=$record;
1331 # sort the hash and return the same structure as GetRecords (Zebra querying)
1334 if ($ordering eq '1=1003 <i') { # sort by title desc
1335 foreach my $key (sort (keys %result)) {
1336 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1338 } else { # sort by title ASC
1339 foreach my $key (sort { $a <=> $b } (keys %result)) {
1340 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1344 $result_hash->{'hits'} = $numbers;
1345 $finalresult->{'biblioserver'} = $result_hash;
1346 return $finalresult;
1348 # ORDER BY callnumber
1350 } elsif ($ordering eq '1=20 <i'){
1352 foreach (split /;/,$biblionumbers) {
1353 my ($biblionumber,$title) = split /,/,$_;
1354 my $record=GetMarcBiblio($biblionumber);
1356 my ($callnumber_tag,$callnumber_subfield)=GetMarcFromKohaField($dbh,'items.itemcallnumber');
1357 ($callnumber_tag,$callnumber_subfield)= GetMarcFromKohaField('biblioitems.callnumber') unless $callnumber_tag;
1358 if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1359 $callnumber=$record->subfield('200','f');
1361 $callnumber=$record->subfield('100','a');
1363 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1364 # and we don't want to get only 1 result for each of them !!!
1365 $result{$callnumber.$biblionumber}=$record;
1367 # sort the hash and return the same structure as GetRecords (Zebra querying)
1370 if ($ordering eq '1=1003 <i') { # sort by title desc
1371 foreach my $key (sort (keys %result)) {
1372 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1374 } else { # sort by title ASC
1375 foreach my $key (sort { $a <=> $b } (keys %result)) {
1376 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1380 $result_hash->{'hits'} = $numbers;
1381 $finalresult->{'biblioserver'} = $result_hash;
1382 return $finalresult;
1383 } elsif ($ordering =~ /1=31/){ #pub year
1385 foreach (split /;/,$biblionumbers) {
1386 my ($biblionumber,$title) = split /,/,$_;
1387 my $record=GetMarcBiblio($biblionumber);
1388 my ($publicationyear_tag,$publicationyear_subfield)=GetMarcFromKohaField($dbh,'biblioitems.publicationyear');
1389 my $publicationyear=$record->subfield($publicationyear_tag,$publicationyear_subfield);
1390 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1391 # and we don't want to get only 1 result for each of them !!!
1392 $result{$publicationyear.$biblionumber}=$record;
1394 # sort the hash and return the same structure as GetRecords (Zebra querying)
1397 if ($ordering eq '1=31 <i') { # sort by title desc
1398 foreach my $key (sort (keys %result)) {
1399 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1401 } else { # sort by title ASC
1402 foreach my $key (sort { $a <=> $b } (keys %result)) {
1403 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1407 $result_hash->{'hits'} = $numbers;
1408 $finalresult->{'biblioserver'} = $result_hash;
1409 return $finalresult;
1413 } elsif ($ordering =~ /1=36/) {
1414 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
1416 foreach (split /;/,$biblionumbers) {
1417 my ($biblionumber,$title) = split /,/,$_;
1418 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1419 # and we don't want to get only 1 result for each of them !!!
1420 # hint & speed improvement : we can order without reading the record
1421 # so order, and read records only for the requested page !
1422 $result{$title.$biblionumber}=$biblionumber;
1424 # sort the hash and return the same structure as GetRecords (Zebra querying)
1427 if ($ordering eq '1=36 <i') { # sort by title desc
1428 foreach my $key (sort (keys %result)) {
1429 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1431 } else { # sort by title ASC
1432 foreach my $key (sort { $a <=> $b } (keys %result)) {
1433 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1436 # for the requested page, replace biblionumber by the complete record
1437 # speed improvement : avoid reading too much things
1438 for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1439 $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1442 $result_hash->{'hits'} = $numbers;
1443 $finalresult->{'biblioserver'} = $result_hash;
1444 return $finalresult;
1449 # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
1452 foreach (split /;/,$biblionumbers) {
1453 my ($biblionumber,$title) = split /,/,$_;
1454 $title =~ /(.*)-(\d)/;
1457 # note that we + the ranking because ranking is calculated on weight of EACH term requested.
1458 # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
1459 # biblio N has ranking = 6
1460 $count_ranking{$biblionumber} =+ $ranking;
1462 # build the result by "inverting" the count_ranking hash
1463 # 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
1465 foreach (keys %count_ranking) {
1466 $result{sprintf("%10d",$count_ranking{$_}).'-'.$_} = $_;
1468 # sort the hash and return the same structure as GetRecords (Zebra querying)
1471 foreach my $key (sort {$b <=> $a} (keys %result)) {
1472 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1474 # for the requested page, replace biblionumber by the complete record
1475 # speed improvement : avoid reading too much things
1476 for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1477 $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1480 $result_hash->{'hits'} = $numbers;
1481 $finalresult->{'biblioserver'} = $result_hash;
1482 return $finalresult;
1486 END { } # module clean-up code here (global destructor)
1493 Koha Developement team <info@koha.org>