3 # Copyright 2006 Liblime
5 # This file is part of Koha.
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
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.
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
20 # load our Koha modules
22 use C4::Interface::CGI::Output;
27 use POSIX qw(ceil floor);
35 my $op = $query->param('op'); # show the search form or execute the search
38 my $expanded_facet = $query->param('expand');
40 ### Gather up all our search queries
42 my $cql_query = $query->param('cql_query');
45 my @previous_ccl_queries; # array of hashes
46 my @previous_ccl_queries_array = $query->param('previous_ccl_queries');
48 my @ccl_query = $query->param('ccl_query');
50 foreach my $ccl (@ccl_query) {
51 $ccl_query.="$ccl " if $ccl;
53 push @previous_ccl_queries_array, $ccl_query;
54 # put the queries in a form the template can use
55 my $previous_ccl_queries_hash;
56 foreach my $ccl (@previous_ccl_queries_array) {
61 push @previous_ccl_queries, %row;
65 my $pqf_query = $query->param('pqf_query');
68 my ($template,$borrowernumber,$cookie);
69 my @forminputs; # this is for the links to navigate among the results when they are more than the maximum number of results per page
71 my $search_type = $query->param('search_type');
73 my $dbh = C4::Context->dbh;
74 ## Get Itemtypes (Collection Codes)
75 my $itemtypequery="Select itemtype,description from itemtypes order by description";
76 my $sth=$dbh->prepare($itemtypequery);
80 while (my ($value,$lib) = $sth->fetchrow_array) {
85 push @itemtypeloop, \%row;
94 my $branches = getallbranches();
96 foreach my $thisbranch (keys %$branches) {
97 my $selected = 1 if (C4::Context->userenv && ($thisbranch eq C4::Context->userenv->{branch}));
100 selected => $selected,
101 branchname => $branches->{$thisbranch}->{'branchname'},
103 push @branchloop, \%row;
106 ## Check if we're searching
107 if ($op eq 'get_results') { # Yea, we're searching, load the results template
108 ($template, $borrowernumber, $cookie)
109 = get_template_and_user({template_name => "opac-results.tmpl",
112 authnotrequired => 1,});
114 my $number_of_results = $query->param('results_per_page');
115 $number_of_results = 20 unless ($number_of_results); #this could be a parameter with 20 50 or 100 results per page
116 my $startfrom = $query->param('startfrom');
117 ($startfrom) || ($startfrom=0);
119 ## OK, WE'RE SEARCHING
120 # STEP 1. We're a CGI script,so first thing to do is get the
121 # query into PQF format so we can use the Koha API properly
122 my ($error,$pqf_sort_by, $pqf_prox_ops, $pqf_bool_ops, $nice_query);
123 ($error,$pqf_sort_by, $pqf_prox_ops, $pqf_bool_ops, $pqf_query, $nice_query)= cgi2pqf($query);
124 my $then_sort_by = $query->param('then_sort_by');
125 # implement a query history
127 # lets store the query details in an array for later
128 push @forminputs, { field => "cql_query" , value => $cql_query} ;
129 push @forminputs, { field => "ccl_query" , value => $ccl_query} ;
130 push @forminputs, { field => 'pqf_sort_by', value => $pqf_sort_by} ;
131 push @forminputs, { field => 'pqf_prox_ops', value => $pqf_prox_ops};
132 push @forminputs, { field => 'pqf_bool_ops' , value => $pqf_bool_ops};
133 push @forminputs, { field => 'pqf_query' , value => $pqf_query };
134 $searchdesc=$cql_query.$ccl_query.$nice_query; # FIXME: this should be a more use-friendly string
136 # STEP 2. OK, now we have PQF, so we can pass off the query to
138 my ($count,@results,$facets);
140 # queries are handled differently, so alert our API and pass in the variables
141 if ($ccl_query) { # CCL
142 if ($query->param('scan')) {
143 ($error,$count, $facets,@results) = searchZOOM('scan','ccl',$ccl_query,$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
144 $template->param(scan => 1);
146 ($error,$count,$facets,@results) = searchZOOM('search','ccl',$ccl_query,$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
148 } elsif ($query->param('cql_query')) { # CQL
149 if ($query->param('scan')) {
150 ($error,$count,$facets,@results) = searchZOOM('scan','cql',$cql_query,$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
151 $template->param(scan => 1);
153 ($error,$count,$facets,@results) = searchZOOM('search','cql',$cql_query,$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
155 } else { # we're in PQF territory now
156 if ($query->param('scan')) {
157 $template->param(scan => 1);
158 ($error,$count,$facets,@results) = searchZOOM('scan','pqf',"$pqf_sort_by $pqf_prox_ops $pqf_bool_ops $pqf_query",$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
160 ($error,$count,$facets,@results) = searchZOOM('search','pqf',"$pqf_sort_by $pqf_prox_ops $pqf_bool_ops $pqf_query",$number_of_results,$startfrom,$then_sort_by,$expanded_facet);
163 @newresults=searchResults( $searchdesc,$number_of_results,$count,@results) ;
165 # How many did we get back?
166 my $num = scalar(@newresults);
168 # sorting out which results to display.
169 # the result number to start to show
170 $template->param(starting => $startfrom+1);
171 $template->param(ending => $startfrom+$number_of_results);
172 # the result number to end to show
173 ($startfrom+$num<=$count) ? ($template->param(endat => $startfrom+$num)) : ($template->param(endat => $count));
174 # the total results found
175 $template->param(total => $count);
176 $template->param(FORMINPUTS => \@forminputs);
177 #$template->param(pqf_query => $pqf_query);
178 $template->param(ccl_query => $ccl_query);
179 $template->param(searchdesc => $searchdesc );
180 $template->param(results_per_page => $number_of_results );
181 $template->param(SEARCH_RESULTS => \@newresults);
182 $template->param(PREVIOUS_CCL_QUERIES => \@previous_ccl_queries);
183 $template->param(facets_loop => $facets);
185 #this is to show the page numbers to navigate among the results, whether it has to show the number highlighted or not
189 if (defined($query->param('pg'))) {
190 $pg = $query->param('pg');
194 $start = ($pg - 1) * $number_of_results;
195 my $pages = ceil($count / $number_of_results);
196 my $total_pages = ceil($count / $number_of_results);
201 number => "<<",
209 startfrom => ($url-1)*$number_of_results,
212 my $current_ten = $pg / 10;
213 if ($current_ten == 0) {
214 $current_ten = 0.1; # In case it's in ten = 0
216 my $from = $current_ten * 10; # Calculate the initial page
217 my $end_ten = $from + 9;
219 if ($pages > $end_ten) {
224 for (my $i =$from; $i <= $to ; $i++) {
226 if ($count > $number_of_results) {
230 startfrom => ($i-1)*$number_of_results ,
237 startfrom => ($i-1)*$number_of_results ,
246 startfrom => ($url-1)*$number_of_results,
250 number => ">>",
252 startfrom => ($total_pages-1)*$number_of_results,
257 pqf_sort_by => $pqf_sort_by,
258 pqf_query => "$pqf_prox_ops $pqf_bool_ops $pqf_query",
259 numbers => $numbers);
262 $template->param('Disable_Dictionary'=>C4::Context->preference("Disable_Dictionary")) if (C4::Context->preference("Disable_Dictionary"));
263 my $scan_use = $query->param('use1');
265 #classlist => $classlist,
266 suggestion => C4::Context->preference("suggestion"),
267 virtualshelves => C4::Context->preference("virtualshelves"),
268 LibraryName => C4::Context->preference("LibraryName"),
269 OpacNav => C4::Context->preference("OpacNav"),
270 opaccredits => C4::Context->preference("opaccredits"),
271 AmazonContent => C4::Context->preference("AmazonContent"),
272 opacsmallimage => C4::Context->preference("opacsmallimage"),
273 opaclayoutstylesheet => C4::Context->preference("opaclayoutstylesheet"),
274 opaccolorstylesheet => C4::Context->preference("opaccolorstylesheet"),
275 scan_use => $scan_use,
276 search_error => $error,
279 ## Now let's find out if we have any supplemental data to show the user
280 # and in the meantime, save the current query for statistical purposes, etc.
281 my $koha_spsuggest; # a flag to tell if we've got suggestions coming from Koha
282 my @koha_spsuggest; # place we store the suggestions to be returned to the template as LOOP
283 my $phrases = $searchdesc;
286 if ( C4::Context->preference("kohaspsuggest") ) {
288 my $koha_spsuggest_dbh;
290 $koha_spsuggest_dbh=DBI->connect("DBI:mysql:suggest:66.213.78.76","auth","Free2cirC");
292 if ($@) { warn "can't connect to spsuggest db";
295 my $koha_spsuggest_insert = "INSERT INTO phrase_log(phr_phrase,phr_resultcount,phr_ip) VALUES(?,?,?)";
296 my $koha_spsuggest_query = "SELECT display FROM distincts WHERE strcmp(soundex(suggestion), soundex(?)) = 0 order by soundex(suggestion) limit 0,5";
297 my $koha_spsuggest_sth = $koha_spsuggest_dbh->prepare($koha_spsuggest_query);
298 $koha_spsuggest_sth->execute($phrases);
299 while (my $spsuggestion = $koha_spsuggest_sth->fetchrow_array) {
300 $spsuggestion =~ s/(:|\/)//g;
302 $line{spsuggestion} = $spsuggestion;
303 push @koha_spsuggest,\%line;
307 # Now save the current query
308 $koha_spsuggest_sth=$koha_spsuggest_dbh->prepare($koha_spsuggest_insert);
309 #$koha_spsuggest_sth->execute($phrases,$count,$ipaddress);
310 $koha_spsuggest_sth->finish;
312 $template->param( koha_spsuggest => $koha_spsuggest ) unless $num;
313 $template->param( SPELL_SUGGEST => \@koha_spsuggest,
314 branchloop=>\@branchloop,
315 itemtypeloop=>\@itemtypeloop,
320 warn "Kohaspsuggest failure:".$@;
324 ## Spellchecking using Google API
325 ## Did you mean? Suggestions using spsuggest table
329 ## OK, we're not searching, load the search template
332 ($template, $borrowernumber, $cookie)
333 = get_template_and_user({template_name => "opac-zoomsearch.tmpl",
336 authnotrequired => 1,
339 # set the default tab, etc.
340 my $search_type = $query->param('query_form');
341 if ((!$search_type) || ($search_type eq 'advanced')) {
342 $template->param(advanced_search => 1);
343 } elsif ($search_type eq 'format') {
344 $template->param(format_search => 1);
345 } elsif ($search_type eq 'power') {
346 $template->param(power_search => 1);
347 } elsif ($search_type eq 'cql') {
348 $template->param(power_search => 1);
349 } elsif ($search_type eq 'pqf') {
350 $template->param(power_search => 1);
351 } elsif ($search_type eq 'proximity') {
352 $template->param(proximity_search => 1);
356 branchloop=>\@branchloop,
357 itemtypeloop=>\@itemtypeloop,
361 output_html_with_http_headers $query, $cookie, $template->output;
365 # build a valid PQF query from a CGI form
368 my $nice_query; # just a string storing a nicely formatted query
369 my @default_attributes = ('sort_by');
370 # attributes specific to the advanced search - a search_point is actually a combination of
371 # several bib1 attributes
372 my @advanced_attributes = ('search_point','op','query');
373 # attributes specific to the power search
374 my @power_attributes = ( 'use','relation','structure','truncation','completeness','op','query');
375 # attributes specific to the proximity search
376 my @proximity_attributes = ('prox_op','prox_exclusion','prox_distance','prox_ordered','prox_relation','prox_which-code','prox_unit-code','query');
378 my @specific_attributes; # these will be looped as many times as needed
380 my $query_form = $query->param('query_form');
382 # bunch of places to store the various queries we're working with
383 my $cql_query = $query->param('cql_query');
385 my $pqf_query = $query->param('pqf_query');
388 my @counting_pqf_query_array;
390 my $pqf_prox_ops = $query->param('pqf_prox_ops');
391 my @pqf_prox_ops_array;
393 my $pqf_bool_ops = $query->param('pqf_bool_ops');
394 my @pqf_bool_ops_array;
396 my $pqf_sort_by = $query->param('pqf_sort_by');
401 if (($query_form) eq 'advanced') {
402 @specific_attributes = @advanced_attributes;
404 } elsif (($query_form) eq 'power') {
405 @specific_attributes = @power_attributes;
407 } elsif (($query_form) eq 'proximity') {
408 @specific_attributes = @proximity_attributes;
412 # load the default attributes, set once per query
413 foreach my $def_attr (@default_attributes) {
414 $pqf_sort_by .= " ".$query->param($def_attr);
416 # these are attributes specific to this query_form, set many times per query
417 # First, process the 'operators' and put them in an array
418 # proximity and boolean
419 foreach my $spec_attr (@specific_attributes) {
420 for (my $i=1;$i<15;$i++) {
421 if ($query->param("query$i")) { # make sure this set should be used
422 if ($spec_attr =~ /^op/) { # process the operators separately
423 push @pqf_bool_ops_array, $query->param("$spec_attr$i");
424 $nice_query .=" ".$query->param("$spec_attr$i")." ".$query->param("query$i");
425 $nice_query =~ s/\@and/AND/g;
426 $nice_query =~ s/\@or/OR/g;
427 } elsif ($spec_attr =~ /^prox/) { # process the proximity operators separately
428 if ($query->param("$spec_attr$i")) {
429 #warn "PQF:".$query->param("$spec_attr$i");
430 push @pqf_prox_ops_array,$query->param("$spec_attr$i");
432 if (($spec_attr =~ /^prox_exclusion/) || ($spec_attr =~ /^prox_ordered/)) { # this is an exception, sloppy way to handle it
434 push @pqf_prox_ops_array,0;
442 # by now, we have two operator arrays: @pqf_bool_ops_array (boolean) and @pqf_prox_ops_array (proximity)
444 # Next, we process the attributes (operands)
445 for (my $i=1;$i<15;$i++) {
446 foreach my $spec_attr (@specific_attributes) {
447 if ($query->param("query$i")) {
448 if ($spec_attr =~ /^query/) {
449 push @counting_pqf_query_array,$query->param("$spec_attr$i") if $query->param("$spec_attr$i");
450 push @pqf_query_array,$query->param("$spec_attr$i") if $query->param("$spec_attr$i")
451 } elsif ($spec_attr =~ /^op/) { # don't process the operators again
452 } elsif ($spec_attr =~ /^prox/) {
454 push @pqf_query_array,$query->param("$spec_attr$i") if $query->param("$spec_attr$i");
460 # we have to make sure that there # of operators == # of operands-1
461 # because second, third, etc queries, come in with an operator attached
462 # ...but if there is no previous queries, it should be dropped ...
463 # that's what we're doing here
464 my $count_pqf_query = @counting_pqf_query_array;
465 my $count_pqf_bool_ops = @pqf_bool_ops_array;
467 if ($count_pqf_bool_ops == $count_pqf_query-1) {
468 for (my $i=$count_pqf_query;$i>=0;$i--) {
469 $pqf_bool_ops.=" ".$pqf_bool_ops_array[$i];
472 for (my $i=$count_pqf_query;$i>=1;$i--) {
473 $pqf_bool_ops.=" ".$pqf_bool_ops_array[$i];
476 foreach my $que(@pqf_query_array) {
478 $pqf_query .=" ".$que;
480 $que =~ s/(\"|\'|\.)//g;
481 $pqf_query .=" \"".$que."\"";
484 foreach my $prox(@pqf_prox_ops_array) {
485 $pqf_prox_ops.=" ".$prox;
488 # finally, nice_query needs to be populated if it hasn't been
489 $nice_query = $pqf_query unless $nice_query;
490 # and cleaned up FIXME: bad bad ...
491 $nice_query =~ s/\@attr//g;
492 $nice_query =~ s/\d+=\d+//g;
493 # OK, done with that, now lets have a look
494 #warn "Boolean Operators: ".$pqf_bool_ops if $pqf_bool_ops;
495 #warn "Proximigy Operators: ".$pqf_prox_ops if $pqf_prox_ops;
496 #warn "Sort by: ".$pqf_sort_by;
497 #warn "PQF:".$pqf_query;
498 #warn "Full PQF: $pqf_sort_by $pqf_prox_ops $pqf_bool_ops $pqf_query";
499 #warn "NICE: $nice_query";
500 return ('',$pqf_sort_by, $pqf_prox_ops, $pqf_bool_ops, $pqf_query, $nice_query);