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);
184 if (C4::Context->preference('NoZebra')) {
185 my $result = NZorder(NZanalyse($query))->{'biblioserver'}->{'RECORDS'};
190 return (undef,$result);
196 return ( "No query entered", undef ) unless $query;
198 #@servers = (C4::Context->config("biblioserver")) unless @servers;
200 ("biblioserver") unless @servers
201 ; # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
204 for ( my $i = 0 ; $i < @servers ; $i++ ) {
205 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
208 ->search( new ZOOM::Query::CCL2RPN( $query, $zconns[$i] ) );
210 # getting error message if one occured.
212 $zconns[$i]->errmsg() . " ("
213 . $zconns[$i]->errcode() . ") "
214 . $zconns[$i]->addinfo() . " "
215 . $zconns[$i]->diagset();
217 return ( $error, undef ) if $zconns[$i]->errcode();
221 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
222 $ev = $zconns[ $i - 1 ]->last_event();
223 if ( $ev == ZOOM::Event::ZEND ) {
224 $hits = $tmpresults[ $i - 1 ]->size();
227 for ( my $j = 0 ; $j < $hits ; $j++ ) {
228 my $record = $tmpresults[ $i - 1 ]->record($j)->raw();
229 push @results, $record;
233 return ( undef, \@results );
237 # performs the search
240 $koha_query, $federated_query, $sort_by_ref,
241 $servers_ref, $results_per_page, $offset,
242 $expanded_facet, $branches, $query_type,
246 my @servers = @$servers_ref;
247 my @sort_by = @$sort_by_ref;
249 # create the zoom connection and query object
253 my $results_hashref = ();
256 my $facets_counter = ();
257 my $facets_info = ();
258 my $facets = getFacets();
260 #### INITIALIZE SOME VARS USED CREATE THE FACETED RESULTS
261 my @facets_loop; # stores the ref to array of hashes for template
262 for ( my $i = 0 ; $i < @servers ; $i++ ) {
263 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
265 # perform the search, create the results objects
266 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
268 if ( $servers[$i] =~ /biblioserver/ ) {
269 $query_to_use = $koha_query;
272 $query_to_use = $federated_query;
275 # warn "HERE : $query_type => $query_to_use";
276 # check if we've got a query_type defined
280 if ( $query_type =~ /^ccl/ ) {
282 s/\:/\=/g; # change : to = last minute (FIXME)
284 # warn "CCL : $query_to_use";
287 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
290 elsif ( $query_type =~ /^cql/ ) {
292 # warn "CQL : $query_to_use";
295 new ZOOM::Query::CQL( $query_to_use, $zconns[$i] ) );
297 elsif ( $query_type =~ /^pqf/ ) {
299 # warn "PQF : $query_to_use";
302 new ZOOM::Query::PQF( $query_to_use, $zconns[$i] ) );
308 # warn "preparing to scan";
311 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
316 # warn "LAST : $query_to_use";
319 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
325 warn "prob with query toto $query_to_use " . $@;
328 # concatenate the sort_by limits and pass them to the results object
330 foreach my $sort (@sort_by) {
331 $sort_by .= $sort . " "; # used to be $sort,
333 $results[$i]->sort( "yaz", $sort_by ) if $sort_by;
335 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
336 my $ev = $zconns[ $i - 1 ]->last_event();
337 if ( $ev == ZOOM::Event::ZEND ) {
338 my $size = $results[ $i - 1 ]->size();
341 #$results_hash->{'server'} = $servers[$i-1];
342 # loop through the results
343 $results_hash->{'hits'} = $size;
345 if ( $offset + $results_per_page <= $size ) {
346 $times = $offset + $results_per_page;
351 for ( my $j = $offset ; $j < $times ; $j++ )
352 { #(($offset+$count<=$size) ? ($offset+$count):$size) ; $j++){
356 ## This is just an index scan
358 my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
360 # here we create a minimal MARC record and hand it off to the
361 # template just like a normal result ... perhaps not ideal, but
363 my $tmprecord = MARC::Record->new();
364 $tmprecord->encoding('UTF-8');
367 # srote the minimal record in author/title (depending on MARC flavour)
368 if ( C4::Context->preference("marcflavour") eq
371 $tmptitle = MARC::Field->new(
378 $tmptitle = MARC::Field->new(
384 $tmprecord->append_fields($tmptitle);
385 $results_hash->{'RECORDS'}[$j] =
386 $tmprecord->as_usmarc();
389 $record = $results[ $i - 1 ]->record($j)->raw();
391 #warn "RECORD $j:".$record;
392 $results_hash->{'RECORDS'}[$j] =
393 $record; # making a reference to a hash
394 # Fill the facets while we're looping
395 $facet_record = MARC::Record->new_from_usmarc($record);
397 #warn $servers[$i-1].$facet_record->title();
398 for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
399 if ( $facets->[$k] ) {
401 for my $tag ( @{ $facets->[$k]->{'tags'} } ) {
402 push @fields, $facet_record->field($tag);
404 for my $field (@fields) {
405 my @subfields = $field->subfields();
406 for my $subfield (@subfields) {
407 my ( $code, $data ) = @$subfield;
409 $facets->[$k]->{'subfield'} )
411 $facets_counter->{ $facets->[$k]
412 ->{'link_value'} }->{$data}++;
416 $facets_info->{ $facets->[$k]->{'link_value'} }
418 $facets->[$k]->{'label_value'};
419 $facets_info->{ $facets->[$k]->{'link_value'} }
420 ->{'expanded'} = $facets->[$k]->{'expanded'};
425 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
428 #print "connection ", $i-1, ": $size hits";
429 #print $results[$i-1]->record(0)->render() if $size > 0;
432 sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
433 keys %$facets_counter
437 my $number_of_facets;
438 my @this_facets_array;
441 $facets_counter->{$link_value}
442 ->{$b} <=> $facets_counter->{$link_value}->{$a}
443 } keys %{ $facets_counter->{$link_value} }
447 if ( ( $number_of_facets < 6 )
448 || ( $expanded_facet eq $link_value )
449 || ( $facets_info->{$link_value}->{'expanded'} ) )
452 # sanitize the link value ), ( will cause errors with CCL
453 my $facet_link_value = $one_facet;
454 $facet_link_value =~ s/(\(|\))/ /g;
456 # fix the length that will display in the label
457 my $facet_label_value = $one_facet;
458 $facet_label_value = substr( $one_facet, 0, 20 ) . "..."
459 unless length($facet_label_value) <= 20;
461 # well, if it's a branch, label by the name, not the code
462 if ( $link_value =~ /branch/ ) {
464 $branches->{$one_facet}->{'branchname'};
467 # but we're down with the whole label being in the link's title
468 my $facet_title_value = $one_facet;
470 push @this_facets_array,
474 $facets_counter->{$link_value}->{$one_facet},
475 facet_label_value => $facet_label_value,
476 facet_title_value => $facet_title_value,
477 facet_link_value => $facet_link_value,
478 type_link_value => $link_value,
483 unless ( $facets_info->{$link_value}->{'expanded'} ) {
485 if ( ( $number_of_facets > 6 )
486 && ( $expanded_facet ne $link_value ) );
491 type_link_value => $link_value,
492 type_id => $link_value . "_id",
494 $facets_info->{$link_value}->{'label_value'},
495 facets => \@this_facets_array,
496 expandable => $expandable,
497 expand => $link_value,
504 warn Dumper($results_hashref);
505 return ( undef, $results_hashref, \@facets_loop );
508 # build the query itself
510 my ( $query, $operators, $operands, $indexes, $limits, $sort_by ) = @_;
512 my @operators = @$operators if $operators;
513 my @indexes = @$indexes if $indexes;
514 my @operands = @$operands if $operands;
515 my @limits = @$limits if $limits;
516 my @sort_by = @$sort_by if $sort_by;
518 my $human_search_desc; # a human-readable query
519 my $machine_search_desc; #a machine-readable query
520 # FIXME: the locale should be set based on the syspref
521 my $stemmer = Lingua::Stem->new( -locale => 'EN-US' );
523 # FIXME: these should be stored in the db so the librarian can modify the behavior
524 $stemmer->add_exceptions(
532 # STEP I: determine if this is a form-based / simple query or if it's complex (if complex,
533 # we can't handle field weighting, stemming until a formal query parser is written
534 # I'll work on this soon -- JF
535 #if (!$query) { # form-based
536 # check if this is a known query language query, if it is, return immediately:
537 if ( $query =~ /^ccl=/ ) {
538 return ( undef, $', $', $', 'ccl' );
540 if ( $query =~ /^cql=/ ) {
541 return ( undef, $', $', $', 'cql' );
543 if ( $query =~ /^pqf=/ ) {
544 return ( undef, $', $', $', 'pqf' );
546 if ( $query =~ /(\(|\))/ ) { # sorry, too complex
547 return ( undef, $query, $query, $query, 'ccl' );
550 # form-based queries are limited to non-nested a specific depth, so we can easily
551 # modify the incoming query operands and indexes to do stemming and field weighting
552 # Once we do so, we'll end up with a value in $query, just like if we had an
553 # incoming $query from the user
556 ; # clear it out so we can populate properly with field-weighted stemmed query
558 ; # a flag used to keep track if there was a previous query
559 # if there was, we can apply the current operator
560 for ( my $i = 0 ; $i <= @operands ; $i++ ) {
561 my $operand = $operands[$i];
562 my $index = $indexes[$i];
564 my $stemming = C4::Context->parameters("Stemming") || 0;
565 my $weight_fields = C4::Context->parameters("WeightFields") || 0;
567 if ( $operands[$i] ) {
569 # STEMMING FIXME: need to refine the field weighting so stemmed operands don't disrupt the query ranking
571 my @words = split( / /, $operands[$i] );
572 my $stems = $stemmer->stem(@words);
573 foreach my $stem (@$stems) {
574 $stemmed_operand .= "$stem";
575 $stemmed_operand .= "?"
576 unless ( $stem =~ /(and$|or$|not$)/ )
577 || ( length($stem) < 3 );
578 $stemmed_operand .= " ";
580 #warn "STEM: $stemmed_operand";
583 #$operand = $stemmed_operand;
586 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
587 # pretty well but will work much better when we have an actual query parser
589 if ($weight_fields) {
591 " rk=("; # Specifies that we're applying rank
592 # keyword has different weight properties
593 if ( ( $index =~ /kw/ ) || ( !$index ) )
594 { # FIXME: do I need to add right-truncation in the case of stemming?
595 # a simple way to find out if this query uses an index
596 if ( $operand =~ /(\=|\:)/ ) {
597 $weighted_query .= " $operand";
601 " Title-cover,ext,r1=\"$operand\""
602 ; # index label as exact
604 " or ti,ext,r2=$operand"; # index as exact
605 #$weighted_query .= " or ti,phr,r3=$operand"; # index as phrase
606 #$weighted_query .= " or any,ext,r4=$operand"; # index as exact
608 " or kw,wrdl,r5=$operand"; # index as exact
609 $weighted_query .= " or wrd,fuzzy,r9=$operand";
610 $weighted_query .= " or wrd=$stemmed_operand"
614 elsif ( $index =~ /au/ ) {
616 " $index,ext,r1=$operand"; # index label as exact
617 #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
619 " or $index,phr,r3=$operand"; # index as phrase
620 $weighted_query .= " or $index,rt,wrd,r3=$operand";
622 elsif ( $index =~ /ti/ ) {
624 " Title-cover,ext,r1=$operand"; # index label as exact
625 $weighted_query .= " or Title-series,ext,r2=$operand";
627 #$weighted_query .= " or ti,ext,r2=$operand";
628 #$weighted_query .= " or ti,phr,r3=$operand";
629 #$weighted_query .= " or ti,wrd,r3=$operand";
631 " or (title-sort-az=0 or Title-cover,startswithnt,st-word,r3=$operand #)";
633 " or (title-sort-az=0 or Title-cover,phr,r6=$operand)";
635 #$weighted_query .= " or Title-cover,wrd,r5=$operand";
636 #$weighted_query .= " or ti,ext,r6=$operand";
637 #$weighted_query .= " or ti,startswith,phr,r7=$operand";
638 #$weighted_query .= " or ti,phr,r8=$operand";
639 #$weighted_query .= " or ti,wrd,r9=$operand";
641 #$weighted_query .= " or ti,ext,r2=$operand"; # index as exact
642 #$weighted_query .= " or ti,phr,r3=$operand"; # index as phrase
643 #$weighted_query .= " or any,ext,r4=$operand"; # index as exact
644 #$weighted_query .= " or kw,wrd,r5=$operand"; # index as exact
648 " $index,ext,r1=$operand"; # index label as exact
649 #$weighted_query .= " or $index,ext,r2=$operand"; # index as exact
651 " or $index,phr,r3=$operand"; # index as phrase
652 $weighted_query .= " or $index,rt,wrd,r3=$operand";
654 " or $index,wrd,r5=$operand"
655 ; # index as word right-truncated
656 $weighted_query .= " or $index,wrd,fuzzy,r8=$operand";
658 $weighted_query .= ")"; # close rank specification
659 $operand = $weighted_query;
662 # only add an operator if there is a previous operand
663 if ($previous_operand) {
664 if ( $operators[ $i - 1 ] ) {
665 $query .= " $operators[$i-1] $index: $operand";
667 $human_search_desc .=
668 " $operators[$i-1] $operands[$i]";
671 $human_search_desc .=
672 " $operators[$i-1] $index: $operands[$i]";
676 # the default operator is and
678 $query .= " and $index: $operand";
679 $human_search_desc .= " and $index: $operands[$i]";
684 $query .= " $operand";
685 $human_search_desc .= " $operands[$i]";
688 $query .= " $index: $operand";
689 $human_search_desc .= " $index: $operands[$i]";
691 $previous_operand = 1;
699 my $limit_search_desc;
700 foreach my $limit (@limits) {
702 # FIXME: not quite right yet ... will work on this soon -- JF
703 my $type = $1 if $limit =~ m/([^:]+):([^:]*)/;
704 if ( $limit =~ /available/ ) {
706 " (($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))";
708 #$limit_search_desc.=" and available";
710 elsif ( ($limit_query) && ( index( $limit_query, $type, 0 ) > 0 ) ) {
711 if ( $limit_query !~ /\(/ ) {
713 substr( $limit_query, 0, index( $limit_query, $type, 0 ) )
715 . substr( $limit_query, index( $limit_query, $type, 0 ) )
719 substr( $limit_search_desc, 0,
720 index( $limit_search_desc, $type, 0 ) )
722 . substr( $limit_search_desc,
723 index( $limit_search_desc, $type, 0 ) )
729 chop $limit_search_desc;
730 $limit_query .= " or $limit )" if $limit;
731 $limit_search_desc .= " or $limit )" if $limit;
734 elsif ( ($limit_query) && ( $limit =~ /mc/ ) ) {
735 $limit_query .= " or $limit" if $limit;
736 $limit_search_desc .= " or $limit" if $limit;
739 # these are treated as AND
740 elsif ($limit_query) {
741 if ($limit =~ /branch/){
742 $limit_query .= " ) and ( $limit" if $limit;
743 $limit_search_desc .= " ) and ( $limit" if $limit;
745 $limit_query .= " or $limit" if $limit;
746 $limit_search_desc .= " or $limit" if $limit;
750 # otherwise, there is nothing but the limit
752 $limit_query .= "$limit" if $limit;
753 $limit_search_desc .= "$limit" if $limit;
757 # if there's also a query, we need to AND the limits to it
758 if ( ($limit_query) && ($query) ) {
759 $limit_query = " and (" . $limit_query . ")";
760 $limit_search_desc = " and ($limit_search_desc)" if $limit_search_desc;
763 $query .= $limit_query;
764 $human_search_desc .= $limit_search_desc;
766 # now normalize the strings
767 $query =~ s/ / /g; # remove extra spaces
768 $query =~ s/^ //g; # remove any beginning spaces
769 $query =~ s/:/=/g; # causes probs for server
770 $query =~ s/==/=/g; # remove double == from query
772 my $federated_query = $human_search_desc;
773 $federated_query =~ s/ / /g;
774 $federated_query =~ s/^ //g;
775 $federated_query =~ s/:/=/g;
776 my $federated_query_opensearch = $federated_query;
778 # my $federated_query_RPN = new ZOOM::Query::CCL2RPN( $query , C4::Context->ZConn('biblioserver'));
780 $human_search_desc =~ s/ / /g;
781 $human_search_desc =~ s/^ //g;
782 my $koha_query = $query;
784 #warn "QUERY:".$koha_query;
785 #warn "SEARCHDESC:".$human_search_desc;
786 #warn "FEDERATED QUERY:".$federated_query;
787 return ( undef, $human_search_desc, $koha_query, $federated_query );
790 # IMO this subroutine is pretty messy still -- it's responsible for
791 # building the HTML output for the template
793 my ( $searchdesc, $hits, $results_per_page, $offset, @marcresults ) = @_;
795 my $dbh = C4::Context->dbh;
799 my $span_terms_hashref;
800 for my $span_term ( split( / /, $searchdesc ) ) {
801 $span_term =~ s/(.*=|\)|\(|\+|\.)//g;
802 $span_terms_hashref->{$span_term}++;
805 #Build brancnames hash
807 #get branch information.....
810 $dbh->prepare("SELECT branchcode,branchname FROM branches")
811 ; # FIXME : use C4::Koha::GetBranches
813 while ( my $bdata = $bsth->fetchrow_hashref ) {
814 $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
818 #find itemtype & itemtype image
821 $dbh->prepare("SELECT itemtype,description,imageurl,summary FROM itemtypes");
823 while ( my $bdata = $bsth->fetchrow_hashref ) {
824 $itemtypes{ $bdata->{'itemtype'} }->{description} =
825 $bdata->{'description'};
826 $itemtypes{ $bdata->{'itemtype'} }->{imageurl} = $bdata->{'imageurl'};
827 $itemtypes{ $bdata->{'itemtype'} }->{summary} = $bdata->{'summary'};
830 #search item field code
833 "select tagfield from marc_subfield_structure where kohafield like 'items.itemnumber'"
836 my ($itemtag) = $sth->fetchrow;
838 ## find column names of items related to MARC
839 my $sth2 = $dbh->prepare("SHOW COLUMNS from items");
841 my %subfieldstosearch;
842 while ( ( my $column ) = $sth2->fetchrow ) {
843 my ( $tagfield, $tagsubfield ) =
844 &GetMarcFromKohaField( "items." . $column, "" );
845 $subfieldstosearch{$column} = $tagsubfield;
849 if ( $hits && $offset + $results_per_page <= $hits ) {
850 $times = $offset + $results_per_page;
856 for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
858 $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
860 my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, '' );
862 # add image url if there is one
863 if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} =~ /^http:/ ) {
864 $oldbiblio->{imageurl} =
865 $itemtypes{ $oldbiblio->{itemtype} }->{imageurl};
866 $oldbiblio->{description} =
867 $itemtypes{ $oldbiblio->{itemtype} }->{description};
870 $oldbiblio->{imageurl} =
871 getitemtypeimagesrc() . "/"
872 . $itemtypes{ $oldbiblio->{itemtype} }->{imageurl}
873 if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
874 $oldbiblio->{description} =
875 $itemtypes{ $oldbiblio->{itemtype} }->{description};
878 # build summary if there is one (the summary is defined in itemtypes table
880 if ($itemtypes{ $oldbiblio->{itemtype} }->{summary}) {
881 my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
882 my @fields = $marcrecord->fields();
883 foreach my $field (@fields) {
884 my $tag = $field->tag();
885 my $tagvalue = $field->as_string();
886 $summary =~ s/\[(.?.?.?.?)$tag\*(.*?)]/$1$tagvalue$2\[$1$tag$2]/g;
888 my @subf = $field->subfields;
889 for my $i (0..$#subf) {
890 my $subfieldcode = $subf[$i][0];
891 my $subfieldvalue = $subf[$i][1];
892 my $tagsubf = $tag.$subfieldcode;
893 $summary =~ s/\[(.?.?.?.?)$tagsubf(.*?)]/$1$subfieldvalue$2\[$1$tagsubf$2]/g;
897 $summary =~ s/\[(.*?)]//g;
898 $summary =~ s/\n/<br>/g;
899 $oldbiblio->{summary} = $summary;
901 # add spans to search term in results
902 foreach my $term ( keys %$span_terms_hashref ) {
905 my $old_term = $term;
906 if ( length($term) > 3 ) {
907 $term =~ s/(.*=|\)|\(|\+|\.|\?)//g;
909 #FIXME: is there a better way to do this?
910 $oldbiblio->{'title'} =~ s/$term/<span class=term>$&<\/span>/gi;
911 $oldbiblio->{'subtitle'} =~
912 s/$term/<span class=term>$&<\/span>/gi;
914 $oldbiblio->{'author'} =~ s/$term/<span class=term>$&<\/span>/gi;
915 $oldbiblio->{'publishercode'} =~ s/$term/<span class=term>$&<\/span>/gi;
916 $oldbiblio->{'place'} =~ s/$term/<span class=term>$&<\/span>/gi;
917 $oldbiblio->{'pages'} =~ s/$term/<span class=term>$&<\/span>/gi;
918 $oldbiblio->{'notes'} =~ s/$term/<span class=term>$&<\/span>/gi;
919 $oldbiblio->{'size'} =~ s/$term/<span class=term>$&<\/span>/gi;
929 $oldbiblio->{'toggle'} = $toggle;
930 my @fields = $marcrecord->field($itemtag);
933 my $ordered_count = 0;
934 my $onloan_count = 0;
935 my $wthdrawn_count = 0;
936 my $itemlost_count = 0;
937 my $itembinding_count = 0;
940 foreach my $field (@fields) {
942 foreach my $code ( keys %subfieldstosearch ) {
943 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
945 if ( $item->{wthdrawn} ) {
948 elsif ( $item->{notforloan} == -1 ) {
952 elsif ( $item->{itemlost} ) {
955 elsif ( $item->{binding} ) {
956 $itembinding_count++;
958 elsif ( ( $item->{onloan} ) && ( $item->{onloan} != '0000-00-00' ) )
965 if ( $item->{'homebranch'} ) {
966 $items->{ $item->{'homebranch'} }->{count}++;
970 elsif ( $item->{'holdingbranch'} ) {
971 $items->{ $item->{'homebranch'} }->{count}++;
973 $items->{ $item->{homebranch} }->{itemcallnumber} =
974 $item->{itemcallnumber};
975 $items->{ $item->{homebranch} }->{location} =
978 } # notforloan, item level and biblioitem level
979 for my $key ( keys %$items ) {
983 branchname => $branches{$key},
985 count => $items->{$key}->{count},
986 itemcallnumber => $items->{$key}->{itemcallnumber},
987 location => $items->{$key}->{location},
989 push @items_loop, $this_item;
991 $oldbiblio->{norequests} = $norequests;
992 $oldbiblio->{items_loop} = \@items_loop;
993 $oldbiblio->{onloancount} = $onloan_count;
994 $oldbiblio->{wthdrawncount} = $wthdrawn_count;
995 $oldbiblio->{itemlostcount} = $itemlost_count;
996 $oldbiblio->{bindingcount} = $itembinding_count;
997 $oldbiblio->{orderedcount} = $ordered_count;
1000 # Ugh ... this is ugly, I'll re-write it better above then delete it
1001 # my $norequests = 1;
1005 # foreach my $itm (@items) {
1006 # $norequests = 0 unless $itm->{'itemnotforloan'};
1009 # $oldbiblio->{'noitems'} = $noitems;
1010 # $oldbiblio->{'norequests'} = $norequests;
1011 # $oldbiblio->{'even'} = $even = not $even;
1012 # $oldbiblio->{'itemcount'} = $counts{'total'};
1013 # my $totalitemcounts = 0;
1014 # foreach my $key (keys %counts){
1015 # if ($key ne 'total'){
1016 # $totalitemcounts+= $counts{$key};
1017 # $oldbiblio->{'locationhash'}->{$key}=$counts{$key};
1020 # my ($locationtext, $locationtextonly, $notavailabletext) = ('','','');
1021 # foreach (sort keys %{$oldbiblio->{'locationhash'}}) {
1022 # if ($_ eq 'notavailable') {
1023 # $notavailabletext="Not available";
1024 # my $c=$oldbiblio->{'locationhash'}->{$_};
1025 # $oldbiblio->{'not-available-p'}=$c;
1027 # $locationtext.="$_";
1028 # my $c=$oldbiblio->{'locationhash'}->{$_};
1029 # if ($_ eq 'Item Lost') {
1030 # $oldbiblio->{'lost-p'} = $c;
1031 # } elsif ($_ eq 'Withdrawn') {
1032 # $oldbiblio->{'withdrawn-p'} = $c;
1033 # } elsif ($_ eq 'On Loan') {
1034 # $oldbiblio->{'on-loan-p'} = $c;
1036 # $locationtextonly.= $_;
1037 # $locationtextonly.= " ($c)<br/> " if $totalitemcounts > 1;
1039 # if ($totalitemcounts>1) {
1040 # $locationtext.=" ($c)<br/> ";
1044 # if ($notavailabletext) {
1045 # $locationtext.= $notavailabletext;
1047 # $locationtext=~s/, $//;
1049 # $oldbiblio->{'location'} = $locationtext;
1050 # $oldbiblio->{'location-only'} = $locationtextonly;
1051 # $oldbiblio->{'use-location-flags-p'} = 1;
1053 push( @newresults, $oldbiblio );
1061 ($countchanged,$listunchanged) = EditBiblios($listbiblios, $tagsubfield,$initvalue,$targetvalue,$test);
1063 this function changes all the values $initvalue in subfield $tag$subfield in any record in $listbiblios
1064 test parameter if set donot perform change to records in database.
1070 * $listbiblios is an array ref to marcrecords to be changed
1071 * $tagsubfield is the reference of the subfield to change.
1072 * $initvalue is the value to search the record for
1073 * $targetvalue is the value to set the subfield to
1074 * $test is to be set only not to perform changes in database.
1076 =item C<Output arg:>
1077 * $countchanged counts all the changes performed.
1078 * $listunchanged contains the list of all the biblionumbers of records unchanged.
1080 =item C<usage in the script:>
1084 my ($countchanged, $listunchanged) = EditBiblios($results->{RECORD}, $tagsubfield,$initvalue,$targetvalue);;
1085 #If one wants to display unchanged records, you should get biblios foreach @$listunchanged
1086 $template->param(countchanged => $countchanged, loopunchanged=>$listunchanged);
1090 my ($listbiblios,$tagsubfield,$initvalue,$targetvalue,$test)=@_;
1093 my ($tag,$subfield)=($1,$2) if ($tagsubfield=~/^(\d{1,3})(.)$/);
1094 my ($bntag,$bnsubf) = GetMarcFromKohaField('biblio.biblionumber');
1096 foreach my $usmarc (@$listbiblios){
1097 my $record=MARC::Record->new_from_usmarc($usmarc);
1100 $biblionumber = $record->subfield($bntag,$bnsubf);
1102 $biblionumber=$record->field($bntag)->data;
1104 #GetBiblionumber is to be written.
1105 #Could be replaced by TransformMarcToKoha (But Would be longer)
1106 if ($record->field($tag)){
1107 foreach my $field ($record->field($tag)){
1108 if ($field->delete_subfield('code' =>$subfield,'match'=>qr($initvalue))){
1110 $field->update($subfield,$targetvalue) if ($targetvalue);
1113 # warn $record->as_formatted;
1114 ModBiblio($record,$biblionumber,GetFrameworkCode($biblionumber)) unless ($test);
1116 push @unmatched, $biblionumber;
1119 return ($countmatched,\@unmatched);
1122 #----------------------------------------------------------------------
1124 # Non-Zebra GetRecords#
1125 #----------------------------------------------------------------------
1128 NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1133 $koha_query, $federated_query, $sort_by_ref,
1134 $servers_ref, $results_per_page, $offset,
1135 $expanded_facet, $branches, $query_type,
1138 my $result = NZanalyse($koha_query);
1140 # warn "==========".@$sort_by_ref[0];
1141 return (undef,NZorder($result,@$sort_by_ref[0],$results_per_page,$offset),undef);
1146 NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1147 the list is builded from inverted index in nozebra SQL table
1148 note that title is here only for convenience : the sorting will be very fast when requested on title
1149 if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1155 # if we have a ", replace the content to discard temporarily any and/or/not inside
1157 if ($string =~/"/) {
1158 $string =~ s/"(.*?)"/__X__/;
1160 # print "commacontent : $commacontent\n";
1162 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1163 # then, call again NZanalyse with $left and $right
1164 # (recursive until we find a leaf (=> something without and/or/not)
1165 $string =~ /(.*)( and | or | not )(.*)/;
1169 # it's not a leaf, we have a and/or/not
1171 # reintroduce comma content if needed
1172 $right =~ s/__X__/"$commacontent"/ if $commacontent;
1173 $left =~ s/__X__/"$commacontent"/ if $commacontent;
1174 # print "noeud : $left / $operand / $right\n";
1175 my $leftresult = NZanalyse($left);
1176 my $rightresult = NZanalyse($right);
1177 # OK, we have the results for right and left part of the query
1178 # depending of operand, intersect, union or exclude both lists
1179 # to get a result list
1180 if ($operand eq ' and ') {
1181 my @leftresult = split /,/, $leftresult;
1182 # my @rightresult = split /,/,$leftresult;
1184 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1185 # the result is stored twice, to have the same weight for AND than OR.
1186 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1187 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1188 foreach (@leftresult) {
1189 if ($rightresult =~ "$_,") {
1190 $finalresult .= "$_,$_,";
1193 return $finalresult;
1194 } elsif ($operand eq ' or ') {
1195 # just merge the 2 strings
1196 return $leftresult.$rightresult;
1197 } elsif ($operand eq ' not ') {
1198 my @leftresult = split /,/, $leftresult;
1199 # my @rightresult = split /,/,$leftresult;
1201 foreach (@leftresult) {
1202 unless ($rightresult =~ "$_,") {
1203 $finalresult .= "$_,";
1206 return $finalresult;
1208 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1209 die "error : operand unknown : $operand for $string";
1211 # it's a leaf, do the real SQL query and return the result
1213 $string =~ s/__X__/"$commacontent"/ if $commacontent;
1214 $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|<|>|&|\+|\*|\// /g;
1215 # print "feuille : $string\n";
1216 # parse the string in in operator/operand/value again
1217 $string =~ /(.*)(=|>|>=|<|<=)(.*)/;
1222 # automatic replace for short operator
1223 $left='title' if $left eq 'ti';
1224 $left='author' if $left eq 'au';
1226 #do a specific search
1227 my $dbh = C4::Context->dbh;
1228 $operator='LIKE' if $operator eq '=' and $right=~ /%/;
1229 my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE indexname=? AND value $operator ?");
1230 # print "$left / $operator / $right\n";
1231 # split each word, query the DB and build the biblionumbers result
1232 foreach (split / /,$right) {
1234 $sth->execute($left,$_);
1235 while (my $line = $sth->fetchrow) {
1236 $biblionumbers .= $line;
1238 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1240 my @leftresult = split /;/, $biblionumbers;
1242 foreach (@leftresult) {
1243 if ($results =~ "$_;") {
1249 $results = $biblionumbers;
1253 #do a complete search (all indexes)
1254 my $dbh = C4::Context->dbh;
1255 my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE value LIKE ?");
1256 # split each word, query the DB and build the biblionumbers result
1257 foreach (split / /,$string) {
1260 while (my $line = $sth->fetchrow) {
1261 $biblionumbers .= $line;
1263 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1265 my @leftresult = split /,/, $biblionumbers;
1267 foreach (@leftresult) {
1268 if ($results =~ "$_;") {
1274 $results = $biblionumbers;
1283 my ($biblionumbers, $ordering,$results_per_page,$offset) = @_;
1284 # order title asc by default
1285 # $ordering = '1=36 <i' unless $ordering;
1286 $results_per_page=20 unless $results_per_page;
1287 $offset = 0 unless $offset;
1288 my $dbh = C4::Context->dbh;
1290 # order by POPULARITY
1292 if ($ordering =~ /1=9523/) {
1295 # popularity is not in MARC record, it's builded from a specific query
1296 my $sth = $dbh->prepare("select sum(issues) from items where biblionumber=?");
1297 foreach (split /;/,$biblionumbers) {
1298 my ($biblionumber,$title) = split /,/,$_;
1299 $result{$biblionumber}=GetMarcBiblio($biblionumber);
1300 $sth->execute($biblionumber);
1301 my $popularity= $sth->fetchrow ||0;
1302 # hint : the key is popularity.title because we can have
1303 # many results with the same popularity. In this cas, sub-ordering is done by title
1304 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
1305 # (un-frequent, I agree, but we won't forget anything that way ;-)
1306 $popularity{sprintf("%10d",$popularity).$title.$biblionumber} = $biblionumber;
1308 # sort the hash and return the same structure as GetRecords (Zebra querying)
1311 if ($ordering eq '1=9523 >i') { # sort popularity DESC
1312 foreach my $key (sort {$b <=> $a} (keys %popularity)) {
1313 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1315 } else { # sort popularity ASC
1316 foreach my $key (sort (keys %popularity)) {
1317 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1321 $result_hash->{'hits'} = $numbers;
1322 $finalresult->{'biblioserver'} = $result_hash;
1323 return $finalresult;
1327 } elsif ($ordering eq '1=1003 <i'){
1329 foreach (split /;/,$biblionumbers) {
1330 my ($biblionumber,$title) = split /,/,$_;
1331 my $record=GetMarcBiblio($biblionumber);
1333 if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1334 $author=$record->subfield('200','f');
1335 $author=$record->subfield('700','a') unless $author;
1337 $author=$record->subfield('100','a');
1339 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1340 # and we don't want to get only 1 result for each of them !!!
1341 $result{$author.$biblionumber}=$record;
1343 # sort the hash and return the same structure as GetRecords (Zebra querying)
1346 if ($ordering eq '1=1003 <i') { # sort by title desc
1347 foreach my $key (sort (keys %result)) {
1348 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1350 } else { # sort by title ASC
1351 foreach my $key (sort { $a <=> $b } (keys %result)) {
1352 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1356 $result_hash->{'hits'} = $numbers;
1357 $finalresult->{'biblioserver'} = $result_hash;
1358 return $finalresult;
1360 # ORDER BY callnumber
1362 } elsif ($ordering eq '1=20 <i'){
1364 foreach (split /;/,$biblionumbers) {
1365 my ($biblionumber,$title) = split /,/,$_;
1366 my $record=GetMarcBiblio($biblionumber);
1368 my ($callnumber_tag,$callnumber_subfield)=GetMarcFromKohaField($dbh,'items.itemcallnumber');
1369 ($callnumber_tag,$callnumber_subfield)= GetMarcFromKohaField('biblioitems.callnumber') unless $callnumber_tag;
1370 if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1371 $callnumber=$record->subfield('200','f');
1373 $callnumber=$record->subfield('100','a');
1375 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1376 # and we don't want to get only 1 result for each of them !!!
1377 $result{$callnumber.$biblionumber}=$record;
1379 # sort the hash and return the same structure as GetRecords (Zebra querying)
1382 if ($ordering eq '1=1003 <i') { # sort by title desc
1383 foreach my $key (sort (keys %result)) {
1384 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1386 } else { # sort by title ASC
1387 foreach my $key (sort { $a <=> $b } (keys %result)) {
1388 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1392 $result_hash->{'hits'} = $numbers;
1393 $finalresult->{'biblioserver'} = $result_hash;
1394 return $finalresult;
1395 } elsif ($ordering =~ /1=31/){ #pub year
1397 foreach (split /;/,$biblionumbers) {
1398 my ($biblionumber,$title) = split /,/,$_;
1399 my $record=GetMarcBiblio($biblionumber);
1400 my ($publicationyear_tag,$publicationyear_subfield)=GetMarcFromKohaField($dbh,'biblioitems.publicationyear');
1401 my $publicationyear=$record->subfield($publicationyear_tag,$publicationyear_subfield);
1402 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1403 # and we don't want to get only 1 result for each of them !!!
1404 $result{$publicationyear.$biblionumber}=$record;
1406 # sort the hash and return the same structure as GetRecords (Zebra querying)
1409 if ($ordering eq '1=31 <i') { # sort by title desc
1410 foreach my $key (sort (keys %result)) {
1411 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1413 } else { # sort by title ASC
1414 foreach my $key (sort { $a <=> $b } (keys %result)) {
1415 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1419 $result_hash->{'hits'} = $numbers;
1420 $finalresult->{'biblioserver'} = $result_hash;
1421 return $finalresult;
1425 } elsif ($ordering =~ /1=36/) {
1426 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
1428 foreach (split /;/,$biblionumbers) {
1429 my ($biblionumber,$title) = split /,/,$_;
1430 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1431 # and we don't want to get only 1 result for each of them !!!
1432 # hint & speed improvement : we can order without reading the record
1433 # so order, and read records only for the requested page !
1434 $result{$title.$biblionumber}=$biblionumber;
1436 # sort the hash and return the same structure as GetRecords (Zebra querying)
1439 if ($ordering eq '1=36 <i') { # sort by title desc
1440 foreach my $key (sort (keys %result)) {
1441 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1443 } else { # sort by title ASC
1444 foreach my $key (sort { $a <=> $b } (keys %result)) {
1445 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1448 # limit the $results_per_page to result size if it's more
1449 $results_per_page = $numbers-1 if $numbers < $results_per_page;
1450 # for the requested page, replace biblionumber by the complete record
1451 # speed improvement : avoid reading too much things
1452 for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1453 $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1456 $result_hash->{'hits'} = $numbers;
1457 $finalresult->{'biblioserver'} = $result_hash;
1458 return $finalresult;
1463 # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
1466 foreach (split /;/,$biblionumbers) {
1467 my ($biblionumber,$title) = split /,/,$_;
1468 $title =~ /(.*)-(\d)/;
1471 # note that we + the ranking because ranking is calculated on weight of EACH term requested.
1472 # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
1473 # biblio N has ranking = 6
1474 $count_ranking{$biblionumber} =+ $ranking;
1476 # build the result by "inverting" the count_ranking hash
1477 # 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
1479 foreach (keys %count_ranking) {
1480 $result{sprintf("%10d",$count_ranking{$_}).'-'.$_} = $_;
1482 # sort the hash and return the same structure as GetRecords (Zebra querying)
1485 foreach my $key (sort {$b <=> $a} (keys %result)) {
1486 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1488 # limit the $results_per_page to result size if it's more
1489 $results_per_page = $numbers-1 if $numbers < $results_per_page;
1490 # for the requested page, replace biblionumber by the complete record
1491 # speed improvement : avoid reading too much things
1492 for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1493 $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1496 $result_hash->{'hits'} = $numbers;
1497 $finalresult->{'biblioserver'} = $result_hash;
1498 return $finalresult;
1502 END { } # module clean-up code here (global destructor)
1509 Koha Developement team <info@koha.org>