Testing sorting result sets
[koha.git] / C4 / SearchMarc.pm
1 package C4::SearchMarc;
2
3 # Copyright 2000-2002 Katipo Communications
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
11 #
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along with
17 # Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
18 # Suite 330, Boston, MA  02111-1307 USA
19
20 use strict;
21 require Exporter;
22 use DBI;
23 use C4::Context;
24 use C4::Biblio;
25 use C4::Date;
26 use Date::Manip;
27 use ZOOM;
28
29 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
30
31 # set the version for version checking
32 $VERSION = 0.02;
33
34 =head1 NAME
35
36 C4::Search - Functions for searching the Koha MARC catalog
37
38 =head1 FUNCTIONS
39
40 This module provides the searching facilities for the Koha MARC catalog
41
42 =cut
43
44 @ISA = qw(Exporter);
45 @EXPORT = qw(&catalogsearch &findseealso &findsuggestion &getMARCnotes &getMARCsubjects);
46
47 =head1 findsuggestion($dbh,$values);
48
49 =head2 $dbh is a link to the DB handler.
50
51 use C4::Context;
52 my $dbh =C4::Context->dbh;
53
54 =head2 $values is a word
55
56 Searches words with the same soundex, ordered by frequency of use.
57 Useful to suggest other searches to the users.
58
59 =cut
60
61 sub findsuggestion {
62         my ($dbh,$values) = @_;
63         my $sth = $dbh->prepare("SELECT count( * ) AS total, word FROM marc_word WHERE sndx_word = soundex( ? ) AND word <> ? GROUP BY word ORDER BY total DESC");
64         my @results;
65         for(my $i = 0 ; $i <= $#{$values} ; $i++) {
66                 if (length(@$values[$i]) >=5) {
67                         $sth->execute(@$values[$i],@$values[$i]);
68                         my $resfound = 1;
69                         my @resline;
70                         while ((my ($count,$word) = $sth->fetchrow) and $resfound <=10) {
71                                 push @results, "@$values[$i]|$word|$count";
72 #                               $results{@$values[$i]} = \@resline;
73                                 $resfound++;
74                         }
75                 }
76         }
77         return \@results;
78 }
79
80 =head1 findseealso($dbh,$fields);
81
82 =head2 $dbh is a link to the DB handler.
83
84 use C4::Context;
85 my $dbh =C4::Context->dbh;
86
87 =head2 $fields is a reference to the fields array
88
89 This function modify the @$fields array and add related fields to search on.
90
91 =cut
92
93 sub findseealso {
94         my ($dbh, $fields) = @_;
95         my $tagslib = MARCgettagslib ($dbh,1);
96         for (my $i=0;$i<=$#{$fields};$i++) {
97                 next unless @$fields[$i];
98                 my ($tag) =substr(@$fields[$i],1,3);
99                 my ($subfield) =substr(@$fields[$i],4,1);
100                 @$fields[$i].=','.$tagslib->{$tag}->{$subfield}->{seealso} if ($tagslib->{$tag}->{$subfield}->{seealso});
101         }
102 }
103
104 =head1  my ($count, @results) = catalogsearch($dbh, $tags, $and_or, $excluding, $operator, $value, $offset,$length,$orderby,$sqlstring);
105
106 =head2 $dbh is a link to the DB handler.
107
108 use C4::Context;
109 my $dbh =C4::Context->dbh;
110
111 $tags,$and_or, $excluding, $operator, $value are references to array
112
113 =head2 $tags
114
115 contains the list of tags+subfields (for example : $@tags[0] = '200a')
116 A field can be a list of fields : '200f','700a','700b','701a','701b'
117
118 Example
119
120 =head2 $and_or
121
122 contains  a list of strings containing and or or. The 1st value is useless.
123
124 =head2 $excluding
125
126 contains 0 or 1. If 1, then the request is negated.
127
128 =head2 $operator
129
130 contains contains,=,start,>,>=,<,<= the = and start work on the complete subfield. The contains operator works on every word in the subfield.
131
132 examples :
133 contains home, search home anywhere.
134 = home, search a string being home.
135
136 =head2 $value
137
138 contains the value to search
139 If it contains a * or a %, then the search is partial.
140
141 =head2 $offset and $length
142
143 returns $length results, beginning at $offset
144
145 =head2 $orderby
146
147 define the field used to order the request. Any field in the biblio/biblioitem tables can be used. DESC is possible too
148
149 (for example title, title DESC,...)
150
151 =head2 $sqlstring
152
153 optional argument containing an sql string to be used in the 'where' statement. see usage in opac-search.pl.
154
155 =head2 $extratables
156
157 optional argument containing extra tables to search. Used in conjunction with $sqlstring. See usage in opac-search.pl.
158 String... so ',items,issues,reserves' allows the items, issues and reserves tables to be used.in a where.
159
160 =head2 RETURNS
161
162 returns an array containing hashes. The hash contains all biblio & biblioitems fields and a reference to an item hash. The "item hash contains one line for each callnumber & the number of items related to the callnumber.
163
164 =cut
165 =head2 my $marcurlsarray = &getMARCurls($dbh,$bibid,$marcflavour);
166
167 Returns a reference to an array containing all the URLS stored in the MARC database for the given bibid.
168 $marcflavour ("MARC21" or "UNIMARC") isn't used in this version because both flavours of MARC use the same subfield for URLS (but eventually when we get the lables working we'll need to change this.
169
170 =cut
171 sub catalogsearch {
172         my ($dbh, $tags, $and_or, $excluding, $operator, $value, $offset,$length,$orderby,$desc_or_asc,$sqlstring, $extratables) = @_;
173
174 # the item.notforloan contains an integer. Every value <>0 means "book unavailable for loan".
175 # but each library can have it's own table of meaning for each value. Get them
176 # 1st search if there is a list of authorised values connected to items.notforloan
177         my $sth = $dbh->prepare('select authorised_value from marc_subfield_structure where kohafield="items.notforloan"');
178         $sth->execute;
179         my %notforloanstatus;
180         my ($authorised_valuecode) = $sth->fetchrow;
181         if ($authorised_valuecode) {
182                 $sth = $dbh->prepare("select authorised_value,lib from authorised_values where category=?");
183                 $sth->execute($authorised_valuecode);
184                 while (my ($authorised_value,$lib) = $sth->fetchrow) {
185                         $notforloanstatus{$authorised_value} = $lib?$lib:$authorised_value;
186                 }
187         }
188         my $subtitle; # Added by JF for Subtitles
189
190         # prepare the query to find item status
191         my $sth_itemCN;
192         if (C4::Context->preference('hidelostitem')) {
193                 $sth_itemCN = $dbh->prepare("select items.* from items where biblionumber=? and (itemlost = 0 or itemlost is NULL)");
194         } else {
195                 $sth_itemCN = $dbh->prepare("select items.* from items where biblionumber=?");
196         }
197         # prepare the query to find date_due where applicable
198         my $sth_issue = $dbh->prepare("select date_due,returndate from issues where itemnumber=?");
199         my $sth_itemtype = $dbh->prepare("select itemtypes.description,itemtypes.notforloan,itemtypes.imageurl from itemtypes where itemtype=?");
200         
201         # prepare the query to find subtitles
202         my $sth_subtitle = $dbh->prepare("SELECT subtitle FROM bibliosubtitle WHERE biblionumber=?"); # Added BY JF for Subtitles
203
204         #
205         # now, do stupid things, that have to be modified for 3.0 :
206         # retrieve the 1st MARC tag.
207         # find the matching non-MARC field
208         # find bib1 attribute. This way, we will be MARC-independant (as title is in 200$a in UNIMARC and 245ùa in MARC21, we use "title" !)
209         # the best method to do this would probably to add a "bib1 attribute" column to marc_subfield_structure
210         # (or a CQL attribute name if we don't want to build bib1 requests)
211         # for instance, we manage only author / title / isbn. Any other field is considered as a keyword/anywhere search
212         #
213         my $tagslib = MARCgettagslib($dbh,$1,'');
214         my $query='';
215         for(my $i = 0 ; $i <= $#{$value} ; $i++){
216                 # 1st split on , then remove ' in the 1st, the find koha field
217                 my @x = split /,/, @$tags[$i];
218                 $x[0] =~ s/'//g if $x[0];
219                 $x[0] =~ /(...)(.)/ if $x[0];
220                 my ($tag,$subfield) = ($1,$2);
221                 if (@$value[$i]) { # if there is something to search, build the request
222                         # if $query already contains something, add @and
223                         $query .= " and " if ($query);
224                         my $field = $tagslib->{$tag}->{$subfield}->{kohafield};
225                         if ($field eq 'biblio.author') {
226                                 $query .= "Author all \"".@$value[$i]."\"";
227                         } elsif ($field eq 'biblio.title') {
228                                 $query .= "Title all \"".@$value[$i]."\"";
229                         } elsif ($field eq 'biblioitems.isbn') {
230                                 $query .= "Isbn= ".@$value[$i];
231                         } elsif ($field eq 'bibliosubject.subject'){
232                             $query.="Subject all \"@$value[$i]\"";
233                         } elsif ($field eq 'biblioitems.itemtype'){
234                             $query="Itemtype=@$value[$i]";
235                         } elsif ($field eq 'items.homebranch'){
236                             $query="Branch=@$value[$i]";
237                         } elsif ($field eq 'items.barcode'){
238                             $query="Barcode=@$value[$i]";
239                         } else {
240                                 warn $field;
241                                 my @spacedout=split(/ /,@$value[$i]);
242                                 my $text = join(" and ",@spacedout);
243                                 $query .= "$text";
244                         }
245                 }
246 #               warn "$i : ".@$tags[$i]. "=> $tag / $subfield = ".$tagslib->{$tag}->{$subfield}->{kohafield};
247         }
248 #        $query.= " ordered title";
249         warn "QUERY : $query";
250         my $Zconn = C4::Context->Zconn or die "unable to set Zconn";
251         my $q = new ZOOM::Query::CQL2RPN( $query, $Zconn);
252         my $rs = $Zconn->search($q);
253         my $numresults=$rs->size();
254         if ($numresults eq 0) {
255                 warn "no records found\n";
256         } else {
257                 warn "$numresults records found, retrieving them (max 80)\n";
258         }
259         # sort result set 
260         # in theory this should sort by title
261         if ($rs->sort("yaz", "1=4 <i") < 0) {
262                  warn "sort failed";
263         }
264         else {
265             warn "sorted";
266             }
267         my $result='';
268         my $scantimerstart=time();
269         my @finalresult = ();
270         my @CNresults=();
271         my $totalitems=0;
272         $offset=1 unless $offset;
273         # calculate max offset
274         my $maxrecordnum = $offset+$length<$numresults?$offset+$length:($numresults);
275         for (my $i=$offset-1; $i <= $maxrecordnum-1; $i++) {
276                 # get the MARC record (in XML)...
277                 # warn "REC $i = ".$rs->record($i)->raw();
278 # FIXME : it's a silly way to do things : XML => MARC::Record => hash. We had better developping a XML=> hash (in biblio.pm)
279                 my $record = MARC::Record->new_from_xml($rs->record($i)->raw(), 'UTF-8');
280                 # transform it into a meaningul hash
281                 my $line = MARCmarc2koha($dbh,$record);
282                 my $biblionumber=$line->{biblionumber};
283         # Return subtitles first ADDED BY JF
284 #                 $sth_subtitle->execute($biblionumber);
285 #                 my $subtitle_here.= $sth_subtitle->fetchrow." ";
286 #                 chop $subtitle_here;
287 #                 $subtitle = $subtitle_here;
288 #               warn "Here's the Biblionumber ".$biblionumber;
289 #                warn "and here's the subtitle: ".$subtitle_here;
290
291         # /ADDED BY JF
292                 # search itemtype information
293                 $sth_itemtype->execute($line->{itemtype});
294                 my ($itemtype_description,$itemtype_notforloan,$itemtype_imageurl) = $sth_itemtype->fetchrow;
295                 $line->{description} = $itemtype_description;
296                 $line->{imageurl} = $itemtype_imageurl;
297                 $line->{notforloan} = $itemtype_notforloan;
298                 $sth_itemCN->execute($biblionumber);
299                 my @CNresults = ();
300                 my $notforloan=1; # to see if there is at least 1 item that can be issued
301                 while (my $item = $sth_itemCN->fetchrow_hashref) {
302                         # parse the result, putting holdingbranch & itemcallnumber in separate array
303                         # then all other fields in the main array
304                         
305                         # search if item is on loan
306                         my $date_due;
307                         $sth_issue->execute($item->{itemnumber});
308                         while (my $loan = $sth_issue->fetchrow_hashref) {
309                                 if ($loan->{date_due} and !$loan->{returndate}) {
310                                         $date_due = $loan->{date_due};
311                                 }
312                         }
313                         # store this item
314                         my %lineCN;
315                         $lineCN{holdingbranch} = $item->{holdingbranch};
316                         $lineCN{itemcallnumber} = $item->{itemcallnumber};
317                         $lineCN{location} = $item->{location};
318                         $lineCN{date_due} = format_date($date_due);
319                         $lineCN{notforloan} = $notforloanstatus{$line->{notforloan}} if ($line->{notforloan}); # setting not forloan if itemtype is not for loan
320                         $lineCN{notforloan} = $notforloanstatus{$item->{notforloan}} if ($item->{notforloan}); # setting not forloan it this item is not for loan
321                         $notforloan=0 unless ($item->{notforloan} or $item->{wthdrawn} or $item->{itemlost});
322                         push @CNresults,\%lineCN;
323                         $totalitems++;
324                 }
325                 # save the biblio in the final array, with item and item issue status
326                 my %newline;
327                 %newline = %$line;
328                 $newline{totitem} = $totalitems;
329                 # if $totalitems == 0, check if it's being ordered.
330                 if ($totalitems == 0) {
331                         my $sth = $dbh->prepare("select count(*) from aqorders where biblionumber=? and datecancellationprinted is NULL");
332                         $sth->execute($biblionumber);
333                         my ($ordered) = $sth->fetchrow;
334                         $newline{onorder} = 1 if $ordered;
335                 }
336                 $newline{biblionumber} = $biblionumber;
337                 $newline{norequests} = 0;
338                 $newline{norequests} = 1 if ($line->{notforloan}); # itemtype not issuable
339                 $newline{norequests} = 1 if (!$line->{notforloan} && $notforloan); # itemtype issuable but all items not issuable for instance
340                 $newline{subtitle} = $subtitle;  # put the subtitle in ADDED BY JF
341
342                 my @CNresults2= @CNresults;
343                 $newline{CN} = \@CNresults2;
344                 $newline{'even'} = 1 if $#finalresult % 2 == 0;
345                 $newline{'odd'} = 1 if $#finalresult % 2 == 1;
346                 $newline{'timestamp'} = format_date($newline{timestamp});
347                 @CNresults = ();
348                 push @finalresult, \%newline;
349                 $totalitems=0;
350         }
351         my $nbresults = $#finalresult+1;
352         return (\@finalresult, $nbresults);
353 }
354
355 =head2 my $marcnotesarray = &getMARCnotes($dbh,$bibid,$marcflavour);
356
357 Returns a reference to an array containing all the notes stored in the MARC database for the given bibid.
358 $marcflavour ("MARC21" or "UNIMARC") determines which tags are used for retrieving subjects.
359
360 =cut
361
362 sub getMARCnotes {
363         my ($dbh, $bibid, $marcflavour) = @_;
364         my ($mintag, $maxtag);
365         if ($marcflavour eq "MARC21") {
366                 $mintag = "500";
367                 $maxtag = "599";
368         } else {           # assume unimarc if not marc21
369                 $mintag = "300";
370                 $maxtag = "399";
371         }
372
373         my $sth=$dbh->prepare("SELECT subfieldvalue,tag FROM marc_subfield_table WHERE bibid=? AND tag BETWEEN ? AND ? ORDER BY tagorder");
374
375         $sth->execute($bibid,$mintag,$maxtag);
376
377         my @marcnotes;
378         my $note = "";
379         my $tag = "";
380         my $marcnote;
381
382         while (my $data=$sth->fetchrow_arrayref) {
383                 my $value=$data->[0];
384                 my $thistag=$data->[1];
385                 if ($value=~/\.$/) {
386                         $value=$value . "  ";
387                 }
388                 if ($thistag ne $tag && $note ne "") {
389                         $marcnote = {marcnote => $note,};
390                         push @marcnotes, $marcnote;
391                         $note=$value;
392                         $tag=$thistag;
393                 }
394                 if ($note ne $value) {
395                         $note = $note." ".$value;
396                 }
397         }
398
399         if ($note) {
400                 $marcnote = {marcnote => $note};
401                 push @marcnotes, $marcnote;   #load last tag into array
402         }
403
404         $sth->finish;
405
406         my $marcnotesarray=\@marcnotes;
407         return $marcnotesarray;
408 }  # end getMARCnotes
409
410
411 =head2 my $marcsubjctsarray = &getMARCsubjects($dbh,$bibid,$marcflavour);
412
413 Returns a reference to an array containing all the subjects stored in the MARC database for the given bibid.
414 $marcflavour ("MARC21" or "UNIMARC") determines which tags are used for retrieving subjects.
415
416 =cut
417
418 sub getMARCsubjects {
419     my ($dbh, $bibid, $marcflavour) = @_;
420         my ($mintag, $maxtag);
421         if ($marcflavour eq "MARC21") {
422                 $mintag = "600";
423                 $maxtag = "699";
424         } else {           # assume unimarc if not marc21
425                 $mintag = "600";
426                 $maxtag = "699";
427         }
428         my $sth=$dbh->prepare("SELECT `subfieldvalue`,`subfieldcode`,`tagorder`,`tag` FROM `marc_subfield_table` WHERE `bibid`= ? AND `subfieldcode` NOT IN ('2','4','6','8') AND `tag` BETWEEN ? AND ? ORDER BY `tagorder`,`subfieldorder`");
429         # Subfield exclusion for $2, $4, $6, $8 protects against searching for
430         # variant data in otherwise invariant authorised subject headings when all
431         # returned subfields are used to form a query for matching subjects.  One
432         # example is the use of $2 in MARC 21 where the value of $2 changes for
433         # different editions of the thesaurus used, even where the subject heading
434         # is otherwise the same.  There is certainly a better fix for many cases
435         # where the value of the subfield may be parsed for the invariant data.  
436         # More complete display values may also be separated from query values
437         # containing only the actual invariant authorised subject headings.  More
438         # coding is required for careful value parsing, or display and query
439         # separation; instead of blanket subfield exclusion.
440         # 
441         # As implemented, $3 is passed and might still pose a problem.  Passing $3
442         # could have benefits for some proper use of $3 for UNIMARC, however, might
443         # restrict query usage to a given material type.  -- thd
444
445         $sth->execute($bibid,$mintag,$maxtag);
446
447         my @marcsubjcts;
448         my $subject = "";
449         my $marcsubjct;
450         my $field9;
451         my $activetagorder=0;
452         my $lasttag;
453         my ($subfieldvalue,$subfieldcode,$tagorder,$tag);
454         while (($subfieldvalue,$subfieldcode,$tagorder,$tag)=$sth->fetchrow) {
455                 $lasttag=$tag if $tag;
456                 if ($activetagorder && $tagorder != $activetagorder) {
457                         $subject=~ s/ -- $//;
458                         $marcsubjct = {MARCSUBJCT => $subject,
459                                                         link => $tag."9",
460                                                         linkvalue => $field9,
461                                                         };
462                         push @marcsubjcts, $marcsubjct;
463                         $subject='';
464                         $tag='';
465                         $field9='';
466                 }
467                 if ($subfieldcode eq 9) {
468                         $field9=$subfieldvalue;
469                 } elsif ($subfieldcode eq (3 || 5)) {
470                         $subject .= $subfieldvalue . " ";
471                 } else {
472                         $subject .= $subfieldvalue . " -- ";
473                 }
474                 $activetagorder=$tagorder;
475         }
476         $subject=~ s/ -- $//;
477         $marcsubjct = {MARCSUBJCT => $subject,
478                                         link => $lasttag."9",
479                                         linkvalue => $field9,
480                                         };
481         push @marcsubjcts, $marcsubjct;
482
483         $sth->finish;
484
485         my $marcsubjctsarray=\@marcsubjcts;
486         return $marcsubjctsarray;
487 }  #end getMARCsubjects
488
489 END { }       # module clean-up code here (global destructor)
490
491 1;
492 __END__
493
494 =back
495
496 =head1 AUTHOR
497
498 Koha Developement team <info@koha.org>
499
500 =cut