fixing itemtype image, sample data, improving
[koha.git] / C4 / Search.pm
blob09b324d28f4977e418d0868a37cf9a295a56b095
1 package C4::Search;
3 # This file is part of Koha.
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 2 of the License, or (at your option) any later
8 # version.
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along with
15 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
16 # Suite 330, Boston, MA 02111-1307 USA
18 use strict;
19 require Exporter;
20 use C4::Context;
21 use C4::Biblio; # GetMarcFromKohaField
22 use C4::Koha; # getFacets
23 use Lingua::Stem;
24 use C4::Date;
26 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
28 # set the version for version checking
29 $VERSION = 3.00;
31 =head1 NAME
33 C4::Search - Functions for searching the Koha catalog.
35 =head1 SYNOPSIS
37 see opac/opac-search.pl or catalogue/search.pl for example of usage
39 =head1 DESCRIPTION
41 This module provides the searching facilities for the Koha into a zebra catalog.
43 =head1 FUNCTIONS
45 =cut
47 @ISA = qw(Exporter);
48 @EXPORT = qw(
49 &SimpleSearch
50 &findseealso
51 &FindDuplicate
52 &searchResults
53 &getRecords
54 &buildQuery
55 &NZgetRecords
56 &ModBiblios
59 # make all your functions, whether exported or not;
61 =head2 findseealso($dbh,$fields);
63 C<$dbh> is a link to the DB handler.
65 use C4::Context;
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.
72 =cut
74 sub findseealso {
75 my ( $dbh, $fields ) = @_;
76 my $tagslib = GetMarcStructure( 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} );
85 =head2 FindDuplicate
87 ($biblionumber,$biblionumber,$title) = FindDuplicate($record);
89 =cut
91 sub FindDuplicate {
92 my ($record) = @_;
93 my $dbh = C4::Context->dbh;
94 my $result = TransformMarcToKoha( $dbh, $record, '' );
95 my $sth;
96 my $query;
97 my $search;
98 my $type;
99 my ( $biblionumber, $title );
101 # search duplicate on ISBN, easy and fast..
102 # ... normalize first
103 if ( $result->{isbn} ) {
104 $result->{isbn} =~ s/\(.*$//;
105 $result->{isbn} =~ s/\s+$//;
107 #$search->{'avoidquerylog'}=1;
108 if ( $result->{isbn} ) {
109 $query = "isbn=$result->{isbn}";
111 else {
112 $result->{title} =~ s /\\//g;
113 $result->{title} =~ s /\"//g;
114 $result->{title} =~ s /\(//g;
115 $result->{title} =~ s /\)//g;
116 $query = "ti,ext=$result->{title}";
117 $query .= " and mt=$result->{itemtype}" if ($result->{itemtype});
118 if ($result->{author}){
119 $result->{author} =~ s /\\//g;
120 $result->{author} =~ s /\"//g;
121 $result->{author} =~ s /\(//g;
122 $result->{author} =~ s /\)//g;
123 $query .= " and au,ext=$result->{author}";
126 my ($error,$searchresults) =
127 SimpleSearch($query); # FIXME :: hardcoded !
128 my @results;
129 foreach my $possible_duplicate_record (@$searchresults) {
130 my $marcrecord =
131 MARC::Record->new_from_usmarc($possible_duplicate_record);
132 my $result = TransformMarcToKoha( $dbh, $marcrecord, '' );
134 # FIXME :: why 2 $biblionumber ?
135 if ($result){
136 push @results, $result->{'biblionumber'};
137 push @results, $result->{'title'};
140 return @results;
143 =head2 SimpleSearch
145 ($error,$results) = SimpleSearch($query,@servers);
147 this function performs a simple search on the catalog using zoom.
149 =over 2
151 =item C<input arg:>
153 * $query could be a simple keyword or a complete CCL query wich is depending on your ccl file.
154 * @servers is optionnal. default one is read on koha.xml
156 =item C<Output arg:>
157 * $error is a string which containt the description error if there is one. Else it's empty.
158 * \@results is an array of marc record.
160 =item C<usage in the script:>
162 =back
164 my ($error, $marcresults) = SimpleSearch($query);
166 if (defined $error) {
167 $template->param(query_error => $error);
168 warn "error: ".$error;
169 output_html_with_http_headers $input, $cookie, $template->output;
170 exit;
173 my $hits = scalar @$marcresults;
174 my @results;
176 for(my $i=0;$i<$hits;$i++) {
177 my %resultsloop;
178 my $marcrecord = MARC::File::USMARC::decode($marcresults->[$i]);
179 my $biblio = TransformMarcToKoha(C4::Context->dbh,$marcrecord,'');
181 #build the hash for the template.
182 $resultsloop{highlight} = ($i % 2)?(1):(0);
183 $resultsloop{title} = $biblio->{'title'};
184 $resultsloop{subtitle} = $biblio->{'subtitle'};
185 $resultsloop{biblionumber} = $biblio->{'biblionumber'};
186 $resultsloop{author} = $biblio->{'author'};
187 $resultsloop{publishercode} = $biblio->{'publishercode'};
188 $resultsloop{publicationyear} = $biblio->{'publicationyear'};
190 push @results, \%resultsloop;
192 $template->param(result=>\@results);
194 =cut
196 sub SimpleSearch {
197 my $query = shift;
198 if (C4::Context->preference('NoZebra')) {
199 my $result = NZorder(NZanalyse($query))->{'biblioserver'}->{'RECORDS'};
200 return (undef,$result);
201 } else {
202 my @servers = @_;
203 my @results;
204 my @tmpresults;
205 my @zconns;
206 return ( "No query entered", undef ) unless $query;
208 #@servers = (C4::Context->config("biblioserver")) unless @servers;
209 @servers =
210 ("biblioserver") unless @servers
211 ; # FIXME hardcoded value. See catalog/search.pl & opac-search.pl too.
213 # Connect & Search
214 for ( my $i = 0 ; $i < @servers ; $i++ ) {
215 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
216 $tmpresults[$i] =
217 $zconns[$i]
218 ->search( new ZOOM::Query::CCL2RPN( $query, $zconns[$i] ) );
220 # getting error message if one occured.
221 my $error =
222 $zconns[$i]->errmsg() . " ("
223 . $zconns[$i]->errcode() . ") "
224 . $zconns[$i]->addinfo() . " "
225 . $zconns[$i]->diagset();
227 return ( $error, undef ) if $zconns[$i]->errcode();
229 my $hits;
230 my $ev;
231 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
232 $ev = $zconns[ $i - 1 ]->last_event();
233 if ( $ev == ZOOM::Event::ZEND ) {
234 $hits = $tmpresults[ $i - 1 ]->size();
236 if ( $hits > 0 ) {
237 for ( my $j = 0 ; $j < $hits ; $j++ ) {
238 my $record = $tmpresults[ $i - 1 ]->record($j)->raw();
239 push @results, $record;
243 return ( undef, \@results );
247 # performs the search
248 sub getRecords {
249 my (
250 $koha_query, $federated_query, $sort_by_ref,
251 $servers_ref, $results_per_page, $offset,
252 $expanded_facet, $branches, $query_type,
253 $scan
254 ) = @_;
255 # warn "Query : $koha_query";
256 my @servers = @$servers_ref;
257 my @sort_by = @$sort_by_ref;
259 # create the zoom connection and query object
260 my $zconn;
261 my @zconns;
262 my @results;
263 my $results_hashref = ();
265 ### FACETED RESULTS
266 my $facets_counter = ();
267 my $facets_info = ();
268 my $facets = getFacets();
270 #### INITIALIZE SOME VARS USED CREATE THE FACETED RESULTS
271 my @facets_loop; # stores the ref to array of hashes for template
272 for ( my $i = 0 ; $i < @servers ; $i++ ) {
273 $zconns[$i] = C4::Context->Zconn( $servers[$i], 1 );
275 # perform the search, create the results objects
276 # if this is a local search, use the $koha-query, if it's a federated one, use the federated-query
277 my $query_to_use;
278 if ( $servers[$i] =~ /biblioserver/ ) {
279 $query_to_use = $koha_query;
281 else {
282 $query_to_use = $federated_query;
285 # check if we've got a query_type defined
286 eval {
287 if ($query_type)
289 if ( $query_type =~ /^ccl/ ) {
290 $query_to_use =~
291 s/\:/\=/g; # change : to = last minute (FIXME)
293 # warn "CCL : $query_to_use";
294 $results[$i] =
295 $zconns[$i]->search(
296 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
299 elsif ( $query_type =~ /^cql/ ) {
301 # warn "CQL : $query_to_use";
302 $results[$i] =
303 $zconns[$i]->search(
304 new ZOOM::Query::CQL( $query_to_use, $zconns[$i] ) );
306 elsif ( $query_type =~ /^pqf/ ) {
308 # warn "PQF : $query_to_use";
309 $results[$i] =
310 $zconns[$i]->search(
311 new ZOOM::Query::PQF( $query_to_use, $zconns[$i] ) );
314 else {
315 if ($scan) {
317 # warn "preparing to scan";
318 $results[$i] =
319 $zconns[$i]->scan(
320 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
323 else {
325 # warn "LAST : $query_to_use";
326 $results[$i] =
327 $zconns[$i]->search(
328 new ZOOM::Query::CCL2RPN( $query_to_use, $zconns[$i] )
333 if ($@) {
334 warn "prob with query toto $query_to_use " . $@;
337 # concatenate the sort_by limits and pass them to the results object
338 my $sort_by;
339 foreach my $sort (@sort_by) {
340 $sort_by .= $sort . " "; # used to be $sort,
342 $results[$i]->sort( "yaz", $sort_by ) if $sort_by;
344 while ( ( my $i = ZOOM::event( \@zconns ) ) != 0 ) {
345 my $ev = $zconns[ $i - 1 ]->last_event();
346 if ( $ev == ZOOM::Event::ZEND ) {
347 my $size = $results[ $i - 1 ]->size();
348 if ( $size > 0 ) {
349 my $results_hash;
350 #$results_hash->{'server'} = $servers[$i-1];
351 # loop through the results
352 $results_hash->{'hits'} = $size;
353 my $times;
354 if ( $offset + $results_per_page <= $size ) {
355 $times = $offset + $results_per_page;
357 else {
358 $times = $size;
360 for ( my $j = $offset ; $j < $times ; $j++ )
361 { #(($offset+$count<=$size) ? ($offset+$count):$size) ; $j++){
362 my $records_hash;
363 my $record;
364 my $facet_record;
365 ## This is just an index scan
366 if ($scan) {
367 my ( $term, $occ ) = $results[ $i - 1 ]->term($j);
369 # here we create a minimal MARC record and hand it off to the
370 # template just like a normal result ... perhaps not ideal, but
371 # it works for now
372 my $tmprecord = MARC::Record->new();
373 $tmprecord->encoding('UTF-8');
374 my $tmptitle;
376 # srote the minimal record in author/title (depending on MARC flavour)
377 if ( C4::Context->preference("marcflavour") eq
378 "UNIMARC" )
380 $tmptitle = MARC::Field->new(
381 '200', ' ', ' ',
382 a => $term,
383 f => $occ
386 else {
387 $tmptitle = MARC::Field->new(
388 '245', ' ', ' ',
389 a => $term,
390 b => $occ
393 $tmprecord->append_fields($tmptitle);
394 $results_hash->{'RECORDS'}[$j] =
395 $tmprecord->as_usmarc();
397 else {
398 $record = $results[ $i - 1 ]->record($j)->raw();
400 #warn "RECORD $j:".$record;
401 $results_hash->{'RECORDS'}[$j] =
402 $record; # making a reference to a hash
403 # Fill the facets while we're looping
404 $facet_record = MARC::Record->new_from_usmarc($record);
406 #warn $servers[$i-1].$facet_record->title();
407 for ( my $k = 0 ; $k <= @$facets ; $k++ ) {
408 if ( $facets->[$k] ) {
409 my @fields;
410 for my $tag ( @{ $facets->[$k]->{'tags'} } ) {
411 push @fields, $facet_record->field($tag);
413 for my $field (@fields) {
414 my @subfields = $field->subfields();
415 for my $subfield (@subfields) {
416 my ( $code, $data ) = @$subfield;
417 if ( $code eq
418 $facets->[$k]->{'subfield'} )
420 $facets_counter->{ $facets->[$k]
421 ->{'link_value'} }->{$data}++;
425 $facets_info->{ $facets->[$k]->{'link_value'} }
426 ->{'label_value'} =
427 $facets->[$k]->{'label_value'};
428 $facets_info->{ $facets->[$k]->{'link_value'} }
429 ->{'expanded'} = $facets->[$k]->{'expanded'};
434 $results_hashref->{ $servers[ $i - 1 ] } = $results_hash;
437 #print "connection ", $i-1, ": $size hits";
438 #print $results[$i-1]->record(0)->render() if $size > 0;
439 # BUILD FACETS
440 for my $link_value (
441 sort { $facets_counter->{$b} <=> $facets_counter->{$a} }
442 keys %$facets_counter
445 my $expandable;
446 my $number_of_facets;
447 my @this_facets_array;
448 for my $one_facet (
449 sort {
450 $facets_counter->{$link_value}
451 ->{$b} <=> $facets_counter->{$link_value}->{$a}
452 } keys %{ $facets_counter->{$link_value} }
455 $number_of_facets++;
456 if ( ( $number_of_facets < 6 )
457 || ( $expanded_facet eq $link_value )
458 || ( $facets_info->{$link_value}->{'expanded'} ) )
461 # sanitize the link value ), ( will cause errors with CCL
462 my $facet_link_value = $one_facet;
463 $facet_link_value =~ s/(\(|\))/ /g;
465 # fix the length that will display in the label
466 my $facet_label_value = $one_facet;
467 $facet_label_value = substr( $one_facet, 0, 20 ) . "..."
468 unless length($facet_label_value) <= 20;
470 # well, if it's a branch, label by the name, not the code
471 if ( $link_value =~ /branch/ ) {
472 $facet_label_value =
473 $branches->{$one_facet}->{'branchname'};
476 # but we're down with the whole label being in the link's title
477 my $facet_title_value = $one_facet;
479 push @this_facets_array,
482 facet_count =>
483 $facets_counter->{$link_value}->{$one_facet},
484 facet_label_value => $facet_label_value,
485 facet_title_value => $facet_title_value,
486 facet_link_value => $facet_link_value,
487 type_link_value => $link_value,
492 unless ( $facets_info->{$link_value}->{'expanded'} ) {
493 $expandable = 1
494 if ( ( $number_of_facets > 6 )
495 && ( $expanded_facet ne $link_value ) );
497 push @facets_loop,
500 type_link_value => $link_value,
501 type_id => $link_value . "_id",
502 type_label =>
503 $facets_info->{$link_value}->{'label_value'},
504 facets => \@this_facets_array,
505 expandable => $expandable,
506 expand => $link_value,
512 return ( undef, $results_hashref, \@facets_loop );
515 # build the query itself
516 sub buildQuery {
517 my ( $query, $operators, $operands, $indexes, $limits, $sort_by ) = @_;
519 my @operators = @$operators if $operators;
520 my @indexes = @$indexes if $indexes;
521 my @operands = @$operands if $operands;
522 my @limits = @$limits if $limits;
523 my @sort_by = @$sort_by if $sort_by;
525 my $human_search_desc; # a human-readable query
526 my $machine_search_desc; #a machine-readable query
527 # FIXME: the locale should be set based on the syspref
528 my $stemmer = Lingua::Stem->new( -locale => 'EN-US' );
530 # FIXME: these should be stored in the db so the librarian can modify the behavior
531 $stemmer->add_exceptions(
533 'and' => 'and',
534 'or' => 'or',
535 'not' => 'not',
540 # STEP I: determine if this is a form-based / simple query or if it's complex (if complex,
541 # we can't handle field weighting, stemming until a formal query parser is written
542 # I'll work on this soon -- JF
543 #if (!$query) { # form-based
544 # check if this is a known query language query, if it is, return immediately:
545 if ( $query =~ /^ccl=/ ) {
546 return ( undef, $', $', $', 'ccl' );
548 if ( $query =~ /^cql=/ ) {
549 return ( undef, $', $', $', 'cql' );
551 if ( $query =~ /^pqf=/ ) {
552 return ( undef, $', $', $', 'pqf' );
554 if ( $query =~ /(\(|\))/ ) { # sorry, too complex
555 return ( undef, $query, $query, $query, 'ccl' );
558 # form-based queries are limited to non-nested a specific depth, so we can easily
559 # modify the incoming query operands and indexes to do stemming and field weighting
560 # Once we do so, we'll end up with a value in $query, just like if we had an
561 # incoming $query from the user
562 else {
563 $query = ""
564 ; # clear it out so we can populate properly with field-weighted stemmed query
565 my $previous_operand
566 ; # a flag used to keep track if there was a previous query
567 # if there was, we can apply the current operator
568 for ( my $i = 0 ; $i <= @operands ; $i++ ) {
569 my $operand = $operands[$i];
570 # remove stopwords from operand : parse all stopwords & remove them (case insensitive)
571 # we use IsAlpha unicode definition, to deal correctly with diacritics.
572 # otherwise, a french word like "leçon" is splitted in "le" "çon", le is an empty word, we get "çon"
573 # and don't find anything...
574 my $stemmed_operand;
575 my $stemming = C4::Context->parameters("Stemming") || 0;
576 my $weight_fields = C4::Context->parameters("WeightFields") || 0;
578 # We Have to do this more carefully.
579 #Since Phrase Search Is Phrase search.
580 #phrase "Physics In Collision" will not be found if we do it like that.
581 my $index = $indexes[$i];
582 my (@nontruncated,@righttruncated,@lefttruncated,@rightlefttruncated,@regexpr);
583 if (index($index,"phr")<0 && index($index,",")>0){
584 #operand may be a wordlist deleting stopwords
585 foreach (keys %{C4::Context->stopwords}) {
586 $operand=~ s/\P{IsAlpha}$_\P{IsAlpha}/ /i;
587 $operand=~ s/^$_\P{IsAlpha}/ /i;
588 $operand=~ s/\P{IsAlpha}$_$/ /i;
590 #now coping with words
591 my @wordlist= split (/\s/,$operand);
592 foreach my $word (@wordlist){
593 if (index($word,"*")==0 && index($word,"*",1)==length($word)-2){
594 $word=~s/\*//;
595 push @rightlefttruncated,$word;
596 } elsif(index($word,"*")==0 && index($word,"*",1)<0){
597 $word=~s/\*//;
598 push @lefttruncated,$word;
599 } elsif (index($word,"*")==length($word)-1){
600 $word=~s/\*//;
601 push @righttruncated,$word;
602 } elsif (index($word,"*")<0){
603 push @nontruncated,$word;
604 } else {
605 push @regexpr,$word;
610 if ( $operands[$i] ) {
611 $operand =~ s/^(and |or |not )//i;
613 # STEMMING FIXME: need to refine the field weighting so stemmed operands don't disrupt the query ranking
614 if ($stemming) {
615 my @words = split( / /, $operands[$i] );
616 my $stems = $stemmer->stem(@words);
617 foreach my $stem (@$stems) {
618 $stemmed_operand .= "$stem";
619 $stemmed_operand .= "?"
620 unless ( $stem =~ /(and$|or$|not$)/ )
621 || ( length($stem) < 3 );
622 $stemmed_operand .= " ";
624 #warn "STEM: $stemmed_operand";
627 #$operand = $stemmed_operand;
630 # FIELD WEIGHTING - This is largely experimental stuff. What I'm committing works
631 # pretty well but will work much better when we have an actual query parser
632 my $weighted_query;
633 if ($weight_fields) {
634 $weighted_query .=
635 " rk=("; # Specifies that we're applying rank
636 # keyword has different weight properties
637 if ( ( $index =~ /kw/ ) || ( !$index ) )
638 { # FIXME: do I need to add right-truncation in the case of stemming?
639 # a simple way to find out if this query uses an index
640 if ( $operand =~ /(\=|\:)/ ) {
641 $weighted_query .= " $operand";
643 else {
644 $weighted_query .=
645 " Title-cover,ext,r1=\"$operand\""
646 ; # index label as exact
647 $weighted_query .=
648 " or ti,ext,r2=$operand"; # index as exact
649 #$weighted_query .= " or ti,phr,r3=$operand"; # index as phrase
650 #$weighted_query .= " or any,ext,r4=$operand"; # index as exact
651 $weighted_query .=
652 " or kw,wrdl,r5=$operand"; # index as exact
653 $weighted_query .= " or wrd,fuzzy,r9=$operand";
654 $weighted_query .= " or wrd=$stemmed_operand"
655 if $stemming;
658 elsif ( $index =~ /au/ ) {
659 $weighted_query .=
660 " $index,ext,r1=$operand"; # index label as exact
661 #$weighted_query .= " or (title-sort-az=0 or $index,startswithnt,st-word,r3=$operand #)";
662 $weighted_query .=
663 " or $index,phr,r3=$operand"; # index as phrase
664 $weighted_query .= " or $index,rt,wrd,r3=$operand";
666 elsif ( $index =~ /ti/ ) {
667 $weighted_query .=
668 " Title-cover,ext,r1=$operand"; # index label as exact
669 $weighted_query .= " or Title-series,ext,r2=$operand";
671 #$weighted_query .= " or ti,ext,r2=$operand";
672 #$weighted_query .= " or ti,phr,r3=$operand";
673 #$weighted_query .= " or ti,wrd,r3=$operand";
674 $weighted_query .=
675 " or (title-sort-az=0 or Title-cover,startswithnt,st-word,r3=$operand #)";
676 $weighted_query .=
677 " or (title-sort-az=0 or Title-cover,phr,r6=$operand)";
679 #$weighted_query .= " or Title-cover,wrd,r5=$operand";
680 #$weighted_query .= " or ti,ext,r6=$operand";
681 #$weighted_query .= " or ti,startswith,phr,r7=$operand";
682 #$weighted_query .= " or ti,phr,r8=$operand";
683 #$weighted_query .= " or ti,wrd,r9=$operand";
685 #$weighted_query .= " or ti,ext,r2=$operand"; # index as exact
686 #$weighted_query .= " or ti,phr,r3=$operand"; # index as phrase
687 #$weighted_query .= " or any,ext,r4=$operand"; # index as exact
688 #$weighted_query .= " or kw,wrd,r5=$operand"; # index as exact
690 else {
691 $weighted_query .=
692 " $index,ext,r1=$operand"; # index label as exact
693 #$weighted_query .= " or $index,ext,r2=$operand"; # index as exact
694 $weighted_query .=
695 " or $index,phr,r3=$operand"; # index as phrase
696 $weighted_query .= " or $index,rt,wrd,r3=$operand";
697 $weighted_query .=
698 " or $index,wrd,r5=$operand"
699 ; # index as word right-truncated
700 $weighted_query .= " or $index,wrd,fuzzy,r8=$operand";
702 $weighted_query .= ")"; # close rank specification
703 $operand = $weighted_query;
706 # only add an operator if there is a previous operand
707 if ($previous_operand) {
708 if ( $operators[ $i - 1 ] ) {
709 $query .= " $operators[$i-1] $index: $operand";
710 if ( !$index ) {
711 $human_search_desc .=
712 " $operators[$i-1] $operands[$i]";
714 else {
715 $human_search_desc .=
716 " $operators[$i-1] $index: $operands[$i]";
720 # the default operator is and
721 else {
722 $query .= " and $index: $operand";
723 $human_search_desc .= " and $index: $operands[$i]";
726 else {
727 if ( !$index ) {
728 $query .= " $operand";
729 $human_search_desc .= " $operands[$i]";
731 else {
732 if (scalar(@righttruncated)+scalar(@lefttruncated)+scalar(@rightlefttruncated)>0){
733 $query.= "$index: @nontruncated " if (scalar(@nontruncated)>0);
734 if (scalar(@righttruncated)>0){
735 $query .= "and $index,rtrn:@righttruncated ";
737 if (scalar(@lefttruncated)>0){
738 $query .= "and $index,ltrn:@lefttruncated ";
740 if (scalar(@rightlefttruncated)>0){
741 $query .= "and $index,rltrn:@rightlefttruncated ";
743 $query=~s/^and//;
744 $human_search_desc .= $query;
745 } else {
746 $query .= " $index: $operand";
747 $human_search_desc .= " $index: $operands[$i]";
750 $previous_operand = 1;
752 } #/if $operands
753 } # /for
756 # add limits
757 my $limit_query;
758 my $limit_search_desc;
759 foreach my $limit (@limits) {
761 # FIXME: not quite right yet ... will work on this soon -- JF
762 my $type = $1 if $limit =~ m/([^:]+):([^:]*)/;
763 if ( $limit =~ /available/ ) {
764 $limit_query .=
765 " (($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))";
767 #$limit_search_desc.=" and available";
769 elsif ( ($limit_query) && ( index( $limit_query, $type, 0 ) > 0 ) ) {
770 if ( $limit_query !~ /\(/ ) {
771 $limit_query =
772 substr( $limit_query, 0, index( $limit_query, $type, 0 ) )
773 . "("
774 . substr( $limit_query, index( $limit_query, $type, 0 ) )
775 . " or $limit )"
776 if $limit;
777 $limit_search_desc =
778 substr( $limit_search_desc, 0,
779 index( $limit_search_desc, $type, 0 ) )
780 . "("
781 . substr( $limit_search_desc,
782 index( $limit_search_desc, $type, 0 ) )
783 . " or $limit )"
784 if $limit;
786 else {
787 chop $limit_query;
788 chop $limit_search_desc;
789 $limit_query .= " or $limit )" if $limit;
790 $limit_search_desc .= " or $limit )" if $limit;
793 elsif ( ($limit_query) && ( $limit =~ /mc/ ) ) {
794 $limit_query .= " or $limit" if $limit;
795 $limit_search_desc .= " or $limit" if $limit;
798 # these are treated as AND
799 elsif ($limit_query) {
800 if ($limit =~ /branch/){
801 $limit_query .= " ) and ( $limit" if $limit;
802 $limit_search_desc .= " ) and ( $limit" if $limit;
803 }else{
804 $limit_query .= " or $limit" if $limit;
805 $limit_search_desc .= " or $limit" if $limit;
809 # otherwise, there is nothing but the limit
810 else {
811 $limit_query .= "$limit" if $limit;
812 $limit_search_desc .= "$limit" if $limit;
816 # if there's also a query, we need to AND the limits to it
817 if ( ($limit_query) && ($query) ) {
818 $limit_query = " and (" . $limit_query . ")";
819 $limit_search_desc = " and ($limit_search_desc)" if $limit_search_desc;
822 $query .= $limit_query;
823 $human_search_desc .= $limit_search_desc;
825 # now normalize the strings
826 $query =~ s/ / /g; # remove extra spaces
827 $query =~ s/^ //g; # remove any beginning spaces
828 $query =~ s/:/=/g; # causes probs for server
829 $query =~ s/==/=/g; # remove double == from query
831 my $federated_query = $human_search_desc;
832 $federated_query =~ s/ / /g;
833 $federated_query =~ s/^ //g;
834 $federated_query =~ s/:/=/g;
835 my $federated_query_opensearch = $federated_query;
837 # my $federated_query_RPN = new ZOOM::Query::CCL2RPN( $query , C4::Context->ZConn('biblioserver'));
839 $human_search_desc =~ s/ / /g;
840 $human_search_desc =~ s/^ //g;
841 my $koha_query = $query;
843 # warn "QUERY:".$koha_query;
844 # warn "SEARCHDESC:".$human_search_desc;
845 # warn "FEDERATED QUERY:".$federated_query;
846 return ( undef, $human_search_desc, $koha_query, $federated_query );
849 # IMO this subroutine is pretty messy still -- it's responsible for
850 # building the HTML output for the template
851 sub searchResults {
852 my ( $searchdesc, $hits, $results_per_page, $offset, @marcresults ) = @_;
854 my $dbh = C4::Context->dbh;
855 my $toggle;
856 my $even = 1;
857 my @newresults;
858 my $span_terms_hashref;
859 for my $span_term ( split( / /, $searchdesc ) ) {
860 $span_term =~ s/(.*=|\)|\(|\+|\.)//g;
861 $span_terms_hashref->{$span_term}++;
864 #Build brancnames hash
865 #find branchname
866 #get branch information.....
867 my %branches;
868 my $bsth =
869 $dbh->prepare("SELECT branchcode,branchname FROM branches")
870 ; # FIXME : use C4::Koha::GetBranches
871 $bsth->execute();
872 while ( my $bdata = $bsth->fetchrow_hashref ) {
873 $branches{ $bdata->{'branchcode'} } = $bdata->{'branchname'};
876 #Build itemtype hash
877 #find itemtype & itemtype image
878 my %itemtypes;
879 $bsth =
880 $dbh->prepare("SELECT itemtype,description,imageurl,summary,notforloan FROM itemtypes");
881 $bsth->execute();
882 while ( my $bdata = $bsth->fetchrow_hashref ) {
883 $itemtypes{ $bdata->{'itemtype'} }->{description} =
884 $bdata->{'description'};
885 $itemtypes{ $bdata->{'itemtype'} }->{imageurl} = $bdata->{'imageurl'};
886 $itemtypes{ $bdata->{'itemtype'} }->{summary} = $bdata->{'summary'};
887 $itemtypes{ $bdata->{'itemtype'} }->{notforloan} = $bdata->{'notforloan'};
890 #search item field code
891 my $sth =
892 $dbh->prepare(
893 "select tagfield from marc_subfield_structure where kohafield like 'items.itemnumber'"
895 $sth->execute;
896 my ($itemtag) = $sth->fetchrow;
898 ## find column names of items related to MARC
899 my $sth2 = $dbh->prepare("SHOW COLUMNS from items");
900 $sth2->execute;
901 my %subfieldstosearch;
902 while ( ( my $column ) = $sth2->fetchrow ) {
903 my ( $tagfield, $tagsubfield ) =
904 &GetMarcFromKohaField( "items." . $column, "" );
905 $subfieldstosearch{$column} = $tagsubfield;
907 my $times;
909 if ( $hits && $offset + $results_per_page <= $hits ) {
910 $times = $offset + $results_per_page;
912 else {
913 $times = $hits;
916 for ( my $i = $offset ; $i <= $times - 1 ; $i++ ) {
917 my $marcrecord;
918 $marcrecord = MARC::File::USMARC::decode( $marcresults[$i] );
919 my $oldbiblio = TransformMarcToKoha( $dbh, $marcrecord, '' );
920 # add image url if there is one
921 if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} =~ /^http:/ ) {
922 $oldbiblio->{imageurl} =
923 $itemtypes{ $oldbiblio->{itemtype} }->{imageurl};
924 $oldbiblio->{description} =
925 $itemtypes{ $oldbiblio->{itemtype} }->{description};
927 else {
928 $oldbiblio->{imageurl} =
929 getitemtypeimagesrc() . "/"
930 . $itemtypes{ $oldbiblio->{itemtype} }->{imageurl}
931 if ( $itemtypes{ $oldbiblio->{itemtype} }->{imageurl} );
932 $oldbiblio->{description} =
933 $itemtypes{ $oldbiblio->{itemtype} }->{description};
936 # build summary if there is one (the summary is defined in itemtypes table
938 if ($itemtypes{ $oldbiblio->{itemtype} }->{summary}) {
939 my $summary = $itemtypes{ $oldbiblio->{itemtype} }->{summary};
940 my @fields = $marcrecord->fields();
941 foreach my $field (@fields) {
942 my $tag = $field->tag();
943 my $tagvalue = $field->as_string();
944 $summary =~ s/\[(.?.?.?.?)$tag\*(.*?)]/$1$tagvalue$2\[$1$tag$2]/g;
945 unless ($tag<10) {
946 my @subf = $field->subfields;
947 for my $i (0..$#subf) {
948 my $subfieldcode = $subf[$i][0];
949 my $subfieldvalue = $subf[$i][1];
950 my $tagsubf = $tag.$subfieldcode;
951 $summary =~ s/\[(.?.?.?.?)$tagsubf(.*?)]/$1$subfieldvalue$2\[$1$tagsubf$2]/g;
955 $summary =~ s/\[(.*?)]//g;
956 $summary =~ s/\n/<br>/g;
957 $oldbiblio->{summary} = $summary;
959 # add spans to search term in results
960 foreach my $term ( keys %$span_terms_hashref ) {
962 #warn "term: $term";
963 my $old_term = $term;
964 if ( length($term) > 3 ) {
965 $term =~ s/(.*=|\)|\(|\+|\.|\?|\[|\])//g;
966 $term =~ s/\\//g;
968 #FIXME: is there a better way to do this?
969 $oldbiblio->{'title'} =~ s/$term/<span class=term>$&<\/span>/gi;
970 $oldbiblio->{'subtitle'} =~
971 s/$term/<span class=term>$&<\/span>/gi;
973 $oldbiblio->{'author'} =~ s/$term/<span class=term>$&<\/span>/gi;
974 $oldbiblio->{'publishercode'} =~ s/$term/<span class=term>$&<\/span>/gi;
975 $oldbiblio->{'place'} =~ s/$term/<span class=term>$&<\/span>/gi;
976 $oldbiblio->{'pages'} =~ s/$term/<span class=term>$&<\/span>/gi;
977 $oldbiblio->{'notes'} =~ s/$term/<span class=term>$&<\/span>/gi;
978 $oldbiblio->{'size'} =~ s/$term/<span class=term>$&<\/span>/gi;
982 if ( $i % 2 ) {
983 $toggle = "#ffffcc";
985 else {
986 $toggle = "white";
988 $oldbiblio->{'toggle'} = $toggle;
989 my @fields = $marcrecord->field($itemtag);
990 my @items_loop;
991 my $items;
992 my $ordered_count = 0;
993 my $onloan_count = 0;
994 my $wthdrawn_count = 0;
995 my $itemlost_count = 0;
996 my $norequests = 1;
999 # check the loan status of the item :
1000 # it is not stored in the MARC record, for pref (zebra reindexing)
1001 # reason. Thus, we have to get the status from a specific SQL query
1003 my $sth_issue = $dbh->prepare("
1004 SELECT date_due,returndate
1005 FROM issues
1006 WHERE itemnumber=? AND returndate IS NULL");
1007 my $items_count=scalar(@fields);
1008 foreach my $field (@fields) {
1009 my $item;
1010 foreach my $code ( keys %subfieldstosearch ) {
1011 $item->{$code} = $field->subfield( $subfieldstosearch{$code} );
1013 $sth_issue->execute($item->{itemnumber});
1014 $item->{due_date} = format_date($sth_issue->fetchrow);
1015 $item->{onloan} = 1 if $item->{due_date};
1016 # at least one item can be reserved : suppose no
1017 $norequests = 1;
1018 if ( $item->{wthdrawn} ) {
1019 $wthdrawn_count++;
1020 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1021 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{wthdrawn}=1;
1023 elsif ( $item->{itemlost} ) {
1024 $itemlost_count++;
1025 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1026 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{itemlost}=1;
1028 unless ( $item->{notforloan}) {
1029 # OK, this one can be issued, so at least one can be reserved
1030 $norequests = 0;
1032 if ( ( $item->{onloan} ) && ( $item->{onloan} != '0000-00-00' ) )
1034 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{unavailable}=1;
1035 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{onloancount} = 1;
1036 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{due_date} = $item->{due_date};
1037 $onloan_count++;
1039 if ( $item->{'homebranch'} ) {
1040 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{count}++;
1043 # Last resort
1044 elsif ( $item->{'holdingbranch'} ) {
1045 $items->{ $item->{'holdingbranch'} }->{count}++;
1047 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{itemcallnumber} = $item->{itemcallnumber};
1048 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{location} = $item->{location};
1049 $items->{ $item->{'homebranch'}.'--'.$item->{'itemcallnumber'} }->{branchcode} = $item->{homebranch};
1050 } # notforloan, item level and biblioitem level
1052 # last check for norequest : if itemtype is notforloan, it can't be reserved either, whatever the items
1053 $norequests = 1 if $itemtypes{$oldbiblio->{itemtype}}->{notforloan};
1055 for my $key ( sort keys %$items ) {
1056 my $this_item = {
1057 branchname => $branches{$items->{$key}->{branchcode}},
1058 branchcode => $items->{$key}->{branchcode},
1059 count => $items->{$key}->{count}==1 ?"":$items->{$key}->{count},
1060 itemcallnumber => $items->{$key}->{itemcallnumber},
1061 location => $items->{$key}->{location},
1062 onloancount => $items->{$key}->{onloancount},
1063 due_date => $items->{$key}->{due_date},
1064 wthdrawn => $items->{$key}->{wthdrawn},
1065 lost => $items->{$key}->{itemlost},
1067 push @items_loop, $this_item;
1069 $oldbiblio->{norequests} = $norequests;
1070 $oldbiblio->{items_count} = $items_count;
1071 $oldbiblio->{items_loop} = \@items_loop;
1072 $oldbiblio->{onloancount} = $onloan_count;
1073 $oldbiblio->{wthdrawncount} = $wthdrawn_count;
1074 $oldbiblio->{itemlostcount} = $itemlost_count;
1075 $oldbiblio->{orderedcount} = $ordered_count;
1076 $oldbiblio->{isbn} =~ s/-//g; # deleting - in isbn to enable amazon content
1077 push( @newresults, $oldbiblio );
1079 return @newresults;
1084 #----------------------------------------------------------------------
1086 # Non-Zebra GetRecords#
1087 #----------------------------------------------------------------------
1089 =head2 NZgetRecords
1091 NZgetRecords has the same API as zera getRecords, even if some parameters are not managed
1093 =cut
1095 sub NZgetRecords {
1096 my (
1097 $koha_query, $federated_query, $sort_by_ref,
1098 $servers_ref, $results_per_page, $offset,
1099 $expanded_facet, $branches, $query_type,
1100 $scan
1101 ) = @_;
1102 my $result = NZanalyse($koha_query);
1103 return (undef,NZorder($result,@$sort_by_ref[0],$results_per_page,$offset),undef);
1106 =head2 NZanalyse
1108 NZanalyse : get a CQL string as parameter, and returns a list of biblionumber;title,biblionumber;title,...
1109 the list is builded from inverted index in nozebra SQL table
1110 note that title is here only for convenience : the sorting will be very fast when requested on title
1111 if the sorting is requested on something else, we will have to reread all results, and that may be longer.
1113 =cut
1115 sub NZanalyse {
1116 my ($string,$server) = @_;
1117 # $server contains biblioserver or authorities, depending on what we search on.
1118 #warn "querying : $string on $server";
1119 $server='biblioserver' unless $server;
1120 # if we have a ", replace the content to discard temporarily any and/or/not inside
1121 my $commacontent;
1122 if ($string =~/"/) {
1123 $string =~ s/"(.*?)"/__X__/;
1124 $commacontent = $1;
1125 # print "commacontent : $commacontent\n";
1127 # split the query string in 3 parts : X AND Y means : $left="X", $operand="AND" and $right="Y"
1128 # then, call again NZanalyse with $left and $right
1129 # (recursive until we find a leaf (=> something without and/or/not)
1130 $string =~ /(.*)( and | or | not | AND | OR | NOT )(.*)/;
1131 my $left = $1;
1132 my $right = $3;
1133 my $operand = lc($2);
1134 # it's not a leaf, we have a and/or/not
1135 if ($operand) {
1136 # reintroduce comma content if needed
1137 $right =~ s/__X__/"$commacontent"/ if $commacontent;
1138 $left =~ s/__X__/"$commacontent"/ if $commacontent;
1139 # warn "node : $left / $operand / $right\n";
1140 my $leftresult = NZanalyse($left,$server);
1141 my $rightresult = NZanalyse($right,$server);
1142 # OK, we have the results for right and left part of the query
1143 # depending of operand, intersect, union or exclude both lists
1144 # to get a result list
1145 if ($operand eq ' and ') {
1146 my @leftresult = split /;/, $leftresult;
1147 # my @rightresult = split /;/,$leftresult;
1148 my $finalresult;
1149 # parse the left results, and if the biblionumber exist in the right result, save it in finalresult
1150 # the result is stored twice, to have the same weight for AND than OR.
1151 # example : TWO : 61,61,64,121 (two is twice in the biblio #61) / TOWER : 61,64,130
1152 # result : 61,61,61,61,64,64 for two AND tower : 61 has more weight than 64
1153 foreach (@leftresult) {
1154 if ($rightresult =~ "$_;") {
1155 $finalresult .= "$_;$_;";
1158 return $finalresult;
1159 } elsif ($operand eq ' or ') {
1160 # just merge the 2 strings
1161 return $leftresult.$rightresult;
1162 } elsif ($operand eq ' not ') {
1163 my @leftresult = split /;/, $leftresult;
1164 # my @rightresult = split /;/,$leftresult;
1165 my $finalresult;
1166 foreach (@leftresult) {
1167 unless ($rightresult =~ "$_;") {
1168 $finalresult .= "$_;";
1171 return $finalresult;
1172 } else {
1173 # this error is impossible, because of the regexp that isolate the operand, but just in case...
1174 die "error : operand unknown : $operand for $string";
1176 # it's a leaf, do the real SQL query and return the result
1177 } else {
1178 $string =~ s/__X__/"$commacontent"/ if $commacontent;
1179 $string =~ s/-|\.|\?|,|;|!|'|\(|\)|\[|\]|{|}|"|<|>|&|\+|\*|\// /g;
1180 # warn "leaf : $string\n";
1181 # parse the string in in operator/operand/value again
1182 $string =~ /(.*)(=|>|>=|<|<=)(.*)/;
1183 my $left = $1;
1184 my $operator = $2;
1185 my $right = $3;
1186 my $results;
1187 # automatic replace for short operators
1188 $left='title' if $left eq 'ti';
1189 $left='author' if $left eq 'au';
1190 $left='publisher' if $left eq 'pb';
1191 $left='subject' if $left eq 'su';
1192 $left='koha-Auth-Number' if $left eq 'an';
1193 $left='keyword' if $left eq 'kw';
1194 if ($operator) {
1195 #do a specific search
1196 my $dbh = C4::Context->dbh;
1197 $operator='LIKE' if $operator eq '=' and $right=~ /%/;
1198 my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE server=? AND indexname=? AND value $operator ?");
1199 # warn "$left / $operator / $right\n";
1200 # split each word, query the DB and build the biblionumbers result
1201 foreach (split / /,$right) {
1202 my $biblionumbers;
1203 next unless $_;
1204 # warn "EXECUTE : $server, $left, $_";
1205 $sth->execute($server, $left, $_);
1206 while (my $line = $sth->fetchrow) {
1207 $biblionumbers .= $line;
1208 # warn "result : $line";
1210 # do a AND with existing list if there is one, otherwise, use the biblionumbers list as 1st result list
1211 if ($results) {
1212 my @leftresult = split /;/, $biblionumbers;
1213 my $temp;
1214 foreach (@leftresult) {
1215 if ($results =~ "$_;") {
1216 $temp .= "$_;$_;";
1219 $results = $temp;
1220 } else {
1221 $results = $biblionumbers;
1224 } else {
1225 #do a complete search (all indexes)
1226 my $dbh = C4::Context->dbh;
1227 my $sth = $dbh->prepare("SELECT biblionumbers FROM nozebra WHERE server=? AND value LIKE ?");
1228 # split each word, query the DB and build the biblionumbers result
1229 foreach (split / /,$string) {
1230 next if C4::Context->stopwords->{uc($_)}; # skip if stopword
1231 #warn "search on all indexes on $_";
1232 my $biblionumbers;
1233 next unless $_;
1234 $sth->execute($server, $_);
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
1239 if ($results) {
1240 my @leftresult = split /;/, $biblionumbers;
1241 my $temp;
1242 foreach (@leftresult) {
1243 if ($results =~ "$_;") {
1244 $temp .= "$_;$_;";
1247 $results = $temp;
1248 } else {
1249 $results = $biblionumbers;
1253 # warn "return : $results for LEAF : $string";
1254 return $results;
1258 =head2 NZorder
1260 $finalresult = NZorder($biblionumbers, $ordering,$results_per_page,$offset);
1262 TODO :: Description
1264 =cut
1267 sub NZorder {
1268 my ($biblionumbers, $ordering,$results_per_page,$offset) = @_;
1269 # order title asc by default
1270 # $ordering = '1=36 <i' unless $ordering;
1271 $results_per_page=20 unless $results_per_page;
1272 $offset = 0 unless $offset;
1273 my $dbh = C4::Context->dbh;
1275 # order by POPULARITY
1277 if ($ordering =~ /1=9523/) {
1278 my %result;
1279 my %popularity;
1280 # popularity is not in MARC record, it's builded from a specific query
1281 my $sth = $dbh->prepare("select sum(issues) from items where biblionumber=?");
1282 foreach (split /;/,$biblionumbers) {
1283 my ($biblionumber,$title) = split /,/,$_;
1284 $result{$biblionumber}=GetMarcBiblio($biblionumber);
1285 $sth->execute($biblionumber);
1286 my $popularity= $sth->fetchrow ||0;
1287 # hint : the key is popularity.title because we can have
1288 # many results with the same popularity. In this cas, sub-ordering is done by title
1289 # we also have biblionumber to avoid bug for 2 biblios with the same title & popularity
1290 # (un-frequent, I agree, but we won't forget anything that way ;-)
1291 $popularity{sprintf("%10d",$popularity).$title.$biblionumber} = $biblionumber;
1293 # sort the hash and return the same structure as GetRecords (Zebra querying)
1294 my $result_hash;
1295 my $numbers=0;
1296 if ($ordering eq '1=9523 >i') { # sort popularity DESC
1297 foreach my $key (sort {$b cmp $a} (keys %popularity)) {
1298 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1300 } else { # sort popularity ASC
1301 foreach my $key (sort (keys %popularity)) {
1302 $result_hash->{'RECORDS'}[$numbers++] = $result{$popularity{$key}}->as_usmarc();
1305 my $finalresult=();
1306 $result_hash->{'hits'} = $numbers;
1307 $finalresult->{'biblioserver'} = $result_hash;
1308 return $finalresult;
1310 # ORDER BY author
1312 } elsif ($ordering eq '1=1003 <i'){
1313 my %result;
1314 foreach (split /;/,$biblionumbers) {
1315 my ($biblionumber,$title) = split /,/,$_;
1316 my $record=GetMarcBiblio($biblionumber);
1317 my $author;
1318 if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1319 $author=$record->subfield('200','f');
1320 $author=$record->subfield('700','a') unless $author;
1321 } else {
1322 $author=$record->subfield('100','a');
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{$author.$biblionumber}=$record;
1328 # sort the hash and return the same structure as GetRecords (Zebra querying)
1329 my $result_hash;
1330 my $numbers=0;
1331 if ($ordering eq '1=1003 <i') { # sort by author desc
1332 foreach my $key (sort (keys %result)) {
1333 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1335 } else { # sort by author ASC
1336 foreach my $key (sort { $a cmp $b } (keys %result)) {
1337 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1340 my $finalresult=();
1341 $result_hash->{'hits'} = $numbers;
1342 $finalresult->{'biblioserver'} = $result_hash;
1343 return $finalresult;
1345 # ORDER BY callnumber
1347 } elsif ($ordering eq '1=20 <i'){
1348 my %result;
1349 foreach (split /;/,$biblionumbers) {
1350 my ($biblionumber,$title) = split /,/,$_;
1351 my $record=GetMarcBiblio($biblionumber);
1352 my $callnumber;
1353 my ($callnumber_tag,$callnumber_subfield)=GetMarcFromKohaField($dbh,'items.itemcallnumber');
1354 ($callnumber_tag,$callnumber_subfield)= GetMarcFromKohaField('biblioitems.callnumber') unless $callnumber_tag;
1355 if (C4::Context->preference('marcflavour') eq 'UNIMARC') {
1356 $callnumber=$record->subfield('200','f');
1357 } else {
1358 $callnumber=$record->subfield('100','a');
1360 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1361 # and we don't want to get only 1 result for each of them !!!
1362 $result{$callnumber.$biblionumber}=$record;
1364 # sort the hash and return the same structure as GetRecords (Zebra querying)
1365 my $result_hash;
1366 my $numbers=0;
1367 if ($ordering eq '1=1003 <i') { # sort by title desc
1368 foreach my $key (sort (keys %result)) {
1369 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1371 } else { # sort by title ASC
1372 foreach my $key (sort { $a cmp $b } (keys %result)) {
1373 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1376 my $finalresult=();
1377 $result_hash->{'hits'} = $numbers;
1378 $finalresult->{'biblioserver'} = $result_hash;
1379 return $finalresult;
1380 } elsif ($ordering =~ /1=31/){ #pub year
1381 my %result;
1382 foreach (split /;/,$biblionumbers) {
1383 my ($biblionumber,$title) = split /,/,$_;
1384 my $record=GetMarcBiblio($biblionumber);
1385 my ($publicationyear_tag,$publicationyear_subfield)=GetMarcFromKohaField($dbh,'biblioitems.publicationyear');
1386 my $publicationyear=$record->subfield($publicationyear_tag,$publicationyear_subfield);
1387 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1388 # and we don't want to get only 1 result for each of them !!!
1389 $result{$publicationyear.$biblionumber}=$record;
1391 # sort the hash and return the same structure as GetRecords (Zebra querying)
1392 my $result_hash;
1393 my $numbers=0;
1394 if ($ordering eq '1=31 <i') { # sort by pubyear desc
1395 foreach my $key (sort (keys %result)) {
1396 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1398 } else { # sort by pub year ASC
1399 foreach my $key (sort { $b cmp $a } (keys %result)) {
1400 $result_hash->{'RECORDS'}[$numbers++] = $result{$key}->as_usmarc();
1403 my $finalresult=();
1404 $result_hash->{'hits'} = $numbers;
1405 $finalresult->{'biblioserver'} = $result_hash;
1406 return $finalresult;
1408 # ORDER BY title
1410 } elsif ($ordering =~ /1=4/) {
1411 # the title is in the biblionumbers string, so we just need to build a hash, sort it and return
1412 my %result;
1413 foreach (split /;/,$biblionumbers) {
1414 my ($biblionumber,$title) = split /,/,$_;
1415 # hint : the result is sorted by title.biblionumber because we can have X biblios with the same title
1416 # and we don't want to get only 1 result for each of them !!!
1417 # hint & speed improvement : we can order without reading the record
1418 # so order, and read records only for the requested page !
1419 $result{$title.$biblionumber}=$biblionumber;
1421 # sort the hash and return the same structure as GetRecords (Zebra querying)
1422 my $result_hash;
1423 my $numbers=0;
1424 if ($ordering eq '1=4 <i') { # sort by title desc
1425 foreach my $key (sort (keys %result)) {
1426 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1428 } else { # sort by title ASC
1429 foreach my $key (sort { $b cmp $a } (keys %result)) {
1430 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1433 # limit the $results_per_page to result size if it's more
1434 $results_per_page = $numbers-1 if $numbers < $results_per_page;
1435 # for the requested page, replace biblionumber by the complete record
1436 # speed improvement : avoid reading too much things
1437 for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1438 $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1440 my $finalresult=();
1441 $result_hash->{'hits'} = $numbers;
1442 $finalresult->{'biblioserver'} = $result_hash;
1443 return $finalresult;
1444 } else {
1446 # order by ranking
1448 # we need 2 hashes to order by ranking : the 1st one to count the ranking, the 2nd to order by ranking
1449 my %result;
1450 my %count_ranking;
1451 foreach (split /;/,$biblionumbers) {
1452 my ($biblionumber,$title) = split /,/,$_;
1453 $title =~ /(.*)-(\d)/;
1454 # get weight
1455 my $ranking =$2;
1456 # note that we + the ranking because ranking is calculated on weight of EACH term requested.
1457 # if we ask for "two towers", and "two" has weight 2 in biblio N, and "towers" has weight 4 in biblio N
1458 # biblio N has ranking = 6
1459 $count_ranking{$biblionumber} += $ranking;
1461 # build the result by "inverting" the count_ranking hash
1462 # 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
1463 # warn "counting";
1464 foreach (keys %count_ranking) {
1465 $result{sprintf("%10d",$count_ranking{$_}).'-'.$_} = $_;
1467 # sort the hash and return the same structure as GetRecords (Zebra querying)
1468 my $result_hash;
1469 my $numbers=0;
1470 foreach my $key (sort {$b cmp $a} (keys %result)) {
1471 $result_hash->{'RECORDS'}[$numbers++] = $result{$key};
1473 # limit the $results_per_page to result size if it's more
1474 $results_per_page = $numbers-1 if $numbers < $results_per_page;
1475 # for the requested page, replace biblionumber by the complete record
1476 # speed improvement : avoid reading too much things
1477 for (my $counter=$offset;$counter<=$offset+$results_per_page;$counter++) {
1478 $result_hash->{'RECORDS'}[$counter] = GetMarcBiblio($result_hash->{'RECORDS'}[$counter])->as_usmarc;
1480 my $finalresult=();
1481 $result_hash->{'hits'} = $numbers;
1482 $finalresult->{'biblioserver'} = $result_hash;
1483 return $finalresult;
1486 =head2 ModBiblios
1488 ($countchanged,$listunchanged) = ModBiblios($listbiblios, $tagsubfield,$initvalue,$targetvalue,$test);
1490 this function changes all the values $initvalue in subfield $tag$subfield in any record in $listbiblios
1491 test parameter if set donot perform change to records in database.
1493 =over 2
1495 =item C<input arg:>
1497 * $listbiblios is an array ref to marcrecords to be changed
1498 * $tagsubfield is the reference of the subfield to change.
1499 * $initvalue is the value to search the record for
1500 * $targetvalue is the value to set the subfield to
1501 * $test is to be set only not to perform changes in database.
1503 =item C<Output arg:>
1504 * $countchanged counts all the changes performed.
1505 * $listunchanged contains the list of all the biblionumbers of records unchanged.
1507 =item C<usage in the script:>
1509 =back
1511 my ($countchanged, $listunchanged) = EditBiblios($results->{RECORD}, $tagsubfield,$initvalue,$targetvalue);;
1512 #If one wants to display unchanged records, you should get biblios foreach @$listunchanged
1513 $template->param(countchanged => $countchanged, loopunchanged=>$listunchanged);
1515 =cut
1517 sub ModBiblios{
1518 my ($listbiblios,$tagsubfield,$initvalue,$targetvalue,$test)=@_;
1519 my $countmatched;
1520 my @unmatched;
1521 my ($tag,$subfield)=($1,$2) if ($tagsubfield=~/^(\d{1,3})([a-z0-9A-Z@])?$/);
1522 if ((length($tag)<3)&& $subfield=~/0-9/){
1523 $tag=$tag.$subfield;
1524 undef $subfield;
1526 my ($bntag,$bnsubf) = GetMarcFromKohaField('biblio.biblionumber');
1527 my ($itemtag,$itemsubf) = GetMarcFromKohaField('items.itemnumber');
1528 foreach my $usmarc (@$listbiblios){
1529 my $record;
1530 $record=eval{MARC::Record->new_from_usmarc($usmarc)};
1531 my $biblionumber;
1532 if ($@){
1533 # usmarc is not a valid usmarc May be a biblionumber
1534 if ($tag eq $itemtag){
1535 my $bib=GetBiblioFromItemNumber($usmarc);
1536 $record=GetMarcItem($bib->{'biblionumber'},$usmarc) ;
1537 $biblionumber=$bib->{'biblionumber'};
1538 } else {
1539 $record=GetMarcBiblio($usmarc);
1540 $biblionumber=$usmarc;
1542 } else {
1543 if ($bntag >= 010){
1544 $biblionumber = $record->subfield($bntag,$bnsubf);
1545 }else {
1546 $biblionumber=$record->field($bntag)->data;
1549 #GetBiblionumber is to be written.
1550 #Could be replaced by TransformMarcToKoha (But Would be longer)
1551 if ($record->field($tag)){
1552 my $modify=0;
1553 foreach my $field ($record->field($tag)){
1554 if ($subfield){
1555 if ($field->delete_subfield('code' =>$subfield,'match'=>qr($initvalue))){
1556 $countmatched++;
1557 $modify=1;
1558 $field->update($subfield,$targetvalue) if ($targetvalue);
1560 } else {
1561 if ($tag >= 010){
1562 if ($field->delete_field($field)){
1563 $countmatched++;
1564 $modify=1;
1566 } else {
1567 $field->data=$targetvalue if ($field->data=~qr($initvalue));
1571 # warn $record->as_formatted;
1572 if ($modify){
1573 ModBiblio($record,$biblionumber,GetFrameworkCode($biblionumber)) unless ($test);
1574 } else {
1575 push @unmatched, $biblionumber;
1577 } else {
1578 push @unmatched, $biblionumber;
1581 return ($countmatched,\@unmatched);
1584 END { } # module clean-up code here (global destructor)
1587 __END__
1589 =head1 AUTHOR
1591 Koha Developement team <info@koha.org>
1593 =cut