From 8471158160b760f614be3b4f3c39297808b117d6 Mon Sep 17 00:00:00 2001 From: Jared Camins-Esakov Date: Thu, 19 Jul 2012 08:02:40 -0400 Subject: [PATCH] Bug 8209: "Did you mean?" from authorities One of the ideas behind authority records is that users who search for one term should have related terms (according to the authority file) suggested to them. At the moment, Koha doesn't do that. Adding an authority searching step to regular searches and displaying any suggestions in a "Did you mean" bar at the top of the results would be very useful. This commit adds a Koha::SuggestionEngine class which is in charge of getting suggestions from individual suggestion engine plugins, which much be in the Koha::SuggestionEngine::Plugin::* namespace, and extend Koha::SuggestionEngine::Base. Suggestions are loaded asynchronously using AJAX, and a link to a page with suggestions is provided for users with Javascript turned off. The AuthorityFile suggestion engine plugin looks up the specified search terms in the authority file and then suggests searches for records using matching authorities. Signed-off-by: Jared Camins-Esakov Rebased 2 August 2012 and incorporated QA feedback Signed-off-by: Katrin Fischer Test plan: - Verified database update added system preference correctly, pref defaults to OFF - Verified search results and detail pages in OPAC and staff still worked the same as before * for no results * with results - Activated system preference and tested various searches * Searches from simple search * Searches from advanced search * Search links in records - Deactivated Javascript - verified fallback works correctly Notes: - Suggested terms can include autorities with no linked records. - When combining more than one search option using advanced search this results in "no suggestions" more often. Feature works best from simple search. Overall great feature making use of authorities in a user friendly way! Signed-off-by: Jared Camins-Esakov Rebased on latest master 2012-09-10 Signed-off-by: wajasu --- C4/AuthoritiesMarc.pm | 3 + Koha/SuggestionEngine.pm | 196 ++++++++++++++++++ Koha/SuggestionEngine/Base.pm | 128 ++++++++++++ Koha/SuggestionEngine/Plugin/AuthorityFile.pm | 87 ++++++++ Koha/SuggestionEngine/Plugin/Null.pm | 60 ++++++ installer/data/mysql/sysprefs.sql | 1 + installer/data/mysql/updatedatabase.pl | 7 + .../modules/admin/preferences/searching.pref | 7 + koha-tmpl/opac-tmpl/prog/en/css/opac.css | 19 ++ .../opac-tmpl/prog/en/modules/opac-results.tt | 20 +- .../prog/en/modules/svc/suggestion.tt | 36 ++++ opac/opac-search.pl | 4 + opac/svc/suggestion | 101 +++++++++ t/SuggestionEngine.t | 46 ++++ t/SuggestionEngine_AuthorityFile.t | 43 ++++ 15 files changed, 748 insertions(+), 10 deletions(-) create mode 100644 Koha/SuggestionEngine.pm create mode 100644 Koha/SuggestionEngine/Base.pm create mode 100644 Koha/SuggestionEngine/Plugin/AuthorityFile.pm create mode 100644 Koha/SuggestionEngine/Plugin/Null.pm create mode 100644 koha-tmpl/opac-tmpl/prog/en/modules/svc/suggestion.tt create mode 100755 opac/svc/suggestion create mode 100755 t/SuggestionEngine.t create mode 100755 t/SuggestionEngine_AuthorityFile.t diff --git a/C4/AuthoritiesMarc.pm b/C4/AuthoritiesMarc.pm index 6593ee2e96..f7bb726681 100644 --- a/C4/AuthoritiesMarc.pm +++ b/C4/AuthoritiesMarc.pm @@ -263,6 +263,9 @@ sub SearchAuthorities { else { $attr .= " \@attr 5=1 \@attr 4=6 " ; ## Word list, right truncated, anywhere + if ($sortby eq 'Relevance') { + $attr .= "\@attr 2=102 "; + } } @$value[$i] =~ s/"/\\"/g; # Escape the double-quotes in the search value $attr =$attr."\"".@$value[$i]."\""; diff --git a/Koha/SuggestionEngine.pm b/Koha/SuggestionEngine.pm new file mode 100644 index 0000000000..0d977c1e22 --- /dev/null +++ b/Koha/SuggestionEngine.pm @@ -0,0 +1,196 @@ +package Koha::SuggestionEngine; + +# Copyright 2012 C & P Bibliography Services +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +=head1 NAME + +Koha::SuggestionEngine - Dispatcher class for suggestion engines + +=head1 SYNOPSIS + + use Koha::SuggestionEngine; + my $suggestor = Koha::SuggestionEngine->new(%params); + $suggestor->get_suggestions($search) + +=head1 DESCRIPTION + +Dispatcher class for retrieving suggestions. SuggestionEngines must +extend Koha::SuggestionEngine::Base, be in the Koha::SuggestionEngine::Plugin +namespace, and provide the following methods: + +B - get suggestions from the plugin for the +specified search. + +These methods may be overriden: + +B - initialize the plugin + +B - destroy the plugin + +These methods should not be overridden unless you are very sure of what +you are doing: + +B - create a new plugin object + +=head1 FUNCTIONS + +=cut + +use strict; +use warnings; +use Module::Load::Conditional qw(can_load); +use Module::Pluggable::Object; + +use base qw(Class::Accessor); + +__PACKAGE__->mk_accessors(qw( schema plugins options record )); + +=head2 new + + my $suggestor = Koha::SuggestionEngine->new(%params); + +Create a new suggestor class. Available parameters are: + +=over 8 + +=item B + +What plugin(s) to use. This must be an arrayref to a list of plugins. Plugins +can be specified either with a complete class path, or, if they are in the +Koha::SuggestionEngine::Plugin namespace, as only the plugin name, and +"Koha::SuggestionEngine::Plugin" will be prepended to it before the plugin +is loaded. + +=back + +=cut + +sub new { + my $class = shift; + my $param = shift; + + my $options = $param->{options} || ''; + my @plugins = (); + + foreach my $plugin ( $param->{plugins} ) { + next unless $plugin; + my $plugin_module = + $plugin =~ m/:/ + ? $plugin + : "Koha::SuggestionEngine::Plugin::${plugin}"; + if ( can_load( modules => { $plugin_module => undef } ) ) { + my $object = $plugin_module->new(); + $plugin_module->initialize($param); + push @plugins, $object; + } + } + + my $self = $class->SUPER::new( + { + plugins => \@plugins, + options => $options + } + ); + bless $self, $class; + return $self; +} + +=head2 get_suggestions + + my $suggestions = $suggester->get_suggestions(\%params) + +Get a list of suggestions based on the search passed in. Available parameters +are: + +=over 8 + +=item B + +Required. The search for which suggestions are desired. + +=item B + +Optional. The number of suggestions to retrieve. Defaults to 10. + +=back + +=cut + +sub get_suggestions { + my $self = shift; + my $param = shift; + + return unless $param->{'search'}; + + my $number = $param->{'count'} || 10; + + my %suggestions; + + foreach my $pluginobj ( @{ $self->plugins } ) { + next unless $pluginobj; + my $pluginres = $pluginobj->get_suggestions($param); + foreach my $suggestion (@$pluginres) { + $suggestions{ $suggestion->{'search'} }->{'relevance'} += + $suggestion->{'relevance'}; + $suggestions{ $suggestion->{'search'} }->{'label'} |= + $suggestion->{'label'}; + } + } + + my @results = (); + for ( + sort { + $suggestions{$b}->{'relevance'} <=> $suggestions{$a}->{'relevance'} + } keys %suggestions + ) + { + last if ( $#results == $number - 1 ); + push @results, + { + 'search' => $_, + relevance => $suggestions{$_}->{'relevance'}, + label => $suggestions{$_}->{'label'} + }; + } + + return \@results; +} + +sub DESTROY { + my $self = shift; + + foreach my $pluginobj ( @{ $self->plugins } ) { + $pluginobj->destroy(); + } +} + +=head2 AvailablePlugins + + my @available_plugins = Koha::SuggestionEngine::AvailablePlugins(); + +Get a list of available plugins. + +=cut + +sub AvailablePlugins { + my $path = 'Koha::SuggestionEngine::Plugin'; + my $finder = Module::Pluggable::Object->new( search_path => $path ); + return $finder->plugins; +} + +1; diff --git a/Koha/SuggestionEngine/Base.pm b/Koha/SuggestionEngine/Base.pm new file mode 100644 index 0000000000..95c66225ba --- /dev/null +++ b/Koha/SuggestionEngine/Base.pm @@ -0,0 +1,128 @@ +package Koha::SuggestionEngine::Base; + +# Copyright 2012 C & P Bibliography Services +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +=head1 NAME + +Koha::SuggestionEngine::Base - Base class for SuggestionEngine plugins + +=head1 SYNOPSIS + + use base qw(Koha::SuggestionEngine::Base); + +=head1 DESCRIPTION + +Base class for suggestion engine plugins. SuggestionEngines must +provide the following methods: + +B - get suggestions for the search described +in $param->{'search'}, and return them in a hashref with the suggestions +as keys and relevance as values. + +The following variables must be defined in each filter: + our $NAME ='Filter'; + our $VERSION = '1.0'; + +These methods may be overriden: + +B - initialize the plugin + +B - destroy the plugin + +These methods should not be overridden unless you are very sure of what +you are doing: + +B - create a new plugin object + +=head1 FUNCTIONS + +=cut + +use strict; +use warnings; + +use base qw(Class::Accessor); + +__PACKAGE__->mk_ro_accessors(qw( name version )); +__PACKAGE__->mk_accessors(qw( params )); +our $NAME = 'Base'; +our $VERSION = '1.0'; + +=head2 new + + my $plugin = Koha::SuggestionEngine::Base->new; + +Create a new filter; + +=cut + +sub new { + my $class = shift; + + my $self = $class->SUPER::new( {} ); #name => $class->NAME, + #version => $class->VERSION }); + + bless $self, $class; + return $self; +} + +=head2 initialize + + $plugin->initalize(%params); + +Initialize a filter using the specified parameters. + +=cut + +sub initialize { + my $self = shift; + my $params = shift; + + #$self->params = $params; + + return $self; +} + +=head2 destroy + + $plugin->destroy(); + +Destroy the filter. + +=cut + +sub destroy { + my $self = shift; + return; +} + +=head2 get_suggestions + + my $suggestions = $plugin->get_suggestions(\%param); + +Return suggestions for the specified search. + +=cut + +sub get_suggestions { + my $self = shift; + my $param = shift; + return; +} + +1; diff --git a/Koha/SuggestionEngine/Plugin/AuthorityFile.pm b/Koha/SuggestionEngine/Plugin/AuthorityFile.pm new file mode 100644 index 0000000000..2636f833c8 --- /dev/null +++ b/Koha/SuggestionEngine/Plugin/AuthorityFile.pm @@ -0,0 +1,87 @@ +package Koha::SuggestionEngine::Plugin::AuthorityFile; + +# Copyright 2012 C & P Bibliography Services +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +=head1 NAME + +Koha::SuggestionEngine::Plugin::AuthorityFile - get suggestions from the authority file + +=head1 SYNOPSIS + + +=head1 DESCRIPTION + +Plugin to get suggestions from Koha's authority file + +=cut + +use strict; +use warnings; +use Carp; + +use base qw(Koha::SuggestionEngine::Base); +our $NAME = 'AuthorityFile'; +our $VERSION = '1.0'; + +=head2 get_suggestions + + my $suggestions = $plugin->get_suggestions(\%param); + +Return suggestions for the specified search by searching for the +search terms in the authority file and returning the results. + +=cut + +sub get_suggestions { + my $self = shift; + my $param = shift; + + my $search = $param->{'search'}; + + # Remove any CCL. This does not handle CQL or PQF, which is unfortunate, + # but what can you do? At some point the search will have to be passed + # not as a string but as some sort of data structure, at which point it + # will be possible to support multiple search syntaxes. + $search =~ s/ccl=//; + $search =~ s/\w*[:=](\w*)/$1/g; + + my @marclist = ['mainentry']; + my @and_or = ['and']; + my @excluding = []; + my @operator = ['any']; + my @value = ["$search"]; + + # FIXME: calling into C4 + require C4::AuthoritiesMarc; + my ( $searchresults, $count ) = C4::AuthoritiesMarc::SearchAuthorities( + \@marclist, \@and_or, \@excluding, \@operator, + @value, 0, $param->{'count'}, '', + 'Relevance', 0 + ); + + my @results; + foreach my $auth (@$searchresults) { + push @results, + { + 'search' => "an=$auth->{'authid'}", + relevance => $count--, + label => $auth->{summary}->{mainentry} + }; + } + return \@results; +} diff --git a/Koha/SuggestionEngine/Plugin/Null.pm b/Koha/SuggestionEngine/Plugin/Null.pm new file mode 100644 index 0000000000..307e035b0e --- /dev/null +++ b/Koha/SuggestionEngine/Plugin/Null.pm @@ -0,0 +1,60 @@ +package Koha::SuggestionEngine::Plugin::Null; + +# Copyright 2012 C & P Bibliography Services +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +=head1 NAME + +Koha::SuggestionEngine::Plugin::Null - an example plugin that does nothing but allow us to run tests + +=head1 SYNOPSIS + + +=head1 DESCRIPTION + +Plugin to allow us to run unit tests and regression tests against the +SuggestionEngine. + +=cut + +use strict; +use warnings; +use Carp; + +use base qw(Koha::SuggestionEngine::Base); +our $NAME = 'Null'; +our $VERSION = '1.0'; + +=head2 get_suggestions + + my $suggestions = $suggestor->get_suggestions( {search => 'books'); + +Return a boring suggestion. + +=cut + +sub get_suggestions { + my $self = shift; + my $param = shift; + + my @result = (); + + push @result, { search => 'book', label => 'Book!', relevance => 1 } + if ( $param->{'search'} eq 'books' ); + + return \@result; +} diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index 3990d978c3..ef6cdb38c0 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -372,3 +372,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES(' INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ('SubfieldsToUseWhenPrefill','','Define a list of subfields to use when prefilling items (separated by space)','','Free'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('AgeRestrictionMarker','','Markers for age restriction indication, e.g. FSK|PEGI|Age|',NULL,'free'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('AgeRestrictionOverride',0,'Allow staff to check out an item with age restriction.',NULL,'YesNo'); +INSERT INTO systempreferences (variable,value,explanation,type) VALUES('DidYouMeanFromAuthorities','0','Suggest searches based on authority file.','YesNo'); diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl index 7decbec08b..a5cc4f27ad 100755 --- a/installer/data/mysql/updatedatabase.pl +++ b/installer/data/mysql/updatedatabase.pl @@ -5722,6 +5722,13 @@ if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { SetVersion($DBversion); } +$DBversion ="3.09.00.039"; +if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { + $dbh->do("INSERT INTO systempreferences (variable,value,explanation,type) VALUES('DidYouMeanFromAuthorities','0','Suggest searches based on authority file.','YesNo');"); + print "Upgrade to $DBversion done (Add system preference DidYouMeanFromAuthorities)\n"; + SetVersion($DBversion); +} + =head1 FUNCTIONS =head2 TableExists($table) diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref index 5176d520b3..ce641720a1 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref @@ -76,6 +76,13 @@ Searching: yes: Using no: "Not using" - 'ICU Zebra indexing. Please note: This setting will not affect Zebra indexing, it should only be used to tell Koha that you have activated ICU indexing if you have actually done so, since there is no way for Koha to figure this out on its own.' + - + - pref: DidYouMeanFromAuthorities + default: 0 + choices: + yes: Suggest + no: "Don't suggest" + - alternate searches based on data in the authority file. Search Form: - - Show tabs in OPAC and staff-side advanced search for limiting searches on the diff --git a/koha-tmpl/opac-tmpl/prog/en/css/opac.css b/koha-tmpl/opac-tmpl/prog/en/css/opac.css index 54bc30fb5a..6a1d5e4390 100644 --- a/koha-tmpl/opac-tmpl/prog/en/css/opac.css +++ b/koha-tmpl/opac-tmpl/prog/en/css/opac.css @@ -2620,6 +2620,25 @@ ul.ui-tabs-nav li { margin-left: 0.5em; } +#didyoumean { + background-color: #EEE; + border: 1px solid #E8E8E8; + margin: 0 0 0.5em; + text-align: left; + padding: 0.5em; + border-radius: 3px 3px 3px 3px; +} + +.suggestionlabel { + font-weight: bold; +} + +.searchsuggestion { + padding: 0.2em 0.5em; + white-space: nowrap; + display: inline-block; +} + /* jQuery UI Datepicker */ .ui-datepicker table {width: 100%; font-size: .9em; border : 0; border-collapse: collapse; margin:0 0 .4em; } .ui-datepicker th { background : transparent none; padding: .7em .3em; text-align: center; font-weight: bold; border: 0; } diff --git a/koha-tmpl/opac-tmpl/prog/en/modules/opac-results.tt b/koha-tmpl/opac-tmpl/prog/en/modules/opac-results.tt index 61913420c2..321d66292f 100644 --- a/koha-tmpl/opac-tmpl/prog/en/modules/opac-results.tt +++ b/koha-tmpl/opac-tmpl/prog/en/modules/opac-results.tt @@ -263,6 +263,13 @@ $(document).ready(function(){ [% IF OpenLibraryCovers %]KOHA.OpenLibrary.GetCoverFromIsbn();[% END %] [% IF OPACLocalCoverImages %]KOHA.LocalCover.GetCoverFromBibnumber(false);[% END %] [% IF ( GoogleJackets ) %]KOHA.Google.GetCoverFromIsbn();[% END %] + + $('#didyoumean').load('/cgi-bin/koha/svc/suggestion?render=stub&q=[% querystring | uri %]', + function() { + $('.searchsuggestion').parent().parent().css({ + 'border-color': '#F4ECBE', + 'background-color': '#FFFBEA'}); + } ); }); //]]> @@ -278,16 +285,9 @@ $(document).ready(function(){
- [% IF ( koha_spsuggest ) %] - Did you mean: - -[% END %] + [% IF ( DidYouMeanFromAuthorities ) %] +
Not what you expected? Check for suggestions
+ [% END %] [% IF ( query_error ) %]
diff --git a/koha-tmpl/opac-tmpl/prog/en/modules/svc/suggestion.tt b/koha-tmpl/opac-tmpl/prog/en/modules/svc/suggestion.tt new file mode 100644 index 0000000000..1bc77ba1e7 --- /dev/null +++ b/koha-tmpl/opac-tmpl/prog/en/modules/svc/suggestion.tt @@ -0,0 +1,36 @@ +[% IF (render=='standalone') %] +[% INCLUDE 'doc-head-open.inc' %]Search suggestions + [% INCLUDE 'doc-head-close.inc' %] + + + [% IF ( OpacNav ) %]
[% ELSE %]
[% END %] +
+ [% INCLUDE 'masthead.inc' %] +
+
+
+
+

Suggestions

+[% END %] +[% IF suggestions && suggestions.size %] +
+ Did you mean: + [% FOREACH suggestion IN suggestions %] + [% suggestion.label %] + [% END %] +
+[% ELSE %] + Sorry, no suggestions. +[% END %] +[% IF (render=='standalone') %] +
+
+
+ [% IF ( OpacNav ) %] +
+ [% INCLUDE 'navigation.inc' %] +
+ [% END %] +
+ [% INCLUDE 'opac-bottom.inc' %] +[% END %] diff --git a/opac/opac-search.pl b/opac/opac-search.pl index 46cf105cce..216de2fa2f 100755 --- a/opac/opac-search.pl +++ b/opac/opac-search.pl @@ -369,6 +369,8 @@ if ($indexes[0] && !$indexes[1]) { my @operands; @operands = split("\0",$params->{'q'}) if $params->{'q'}; +$template->{VARS}->{querystring} = join(' ', @operands); + # if a simple search, display the value in the search box if ($operands[0] && !$operands[1]) { $template->param(ms_value => $operands[0]); @@ -826,5 +828,7 @@ if (C4::Context->preference('GoogleIndicTransliteration')) { $template->param('GoogleIndicTransliteration' => 1); } +$template->{VARS}->{DidYouMeanFromAuthorities} = C4::Context->preference('DidYouMeanFromAuthorities'); + $template->param( borrowernumber => $borrowernumber); output_with_http_headers $cgi, $cookie, $template->output, $content_type; diff --git a/opac/svc/suggestion b/opac/svc/suggestion new file mode 100755 index 0000000000..7234ffdac2 --- /dev/null +++ b/opac/svc/suggestion @@ -0,0 +1,101 @@ +#!/usr/bin/perl + +# Copyright 2012 C & P Bibliography Services +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +=head1 NAME + +opac-suggestion.pl : script to render suggestions for the OPAC + +=head1 SYNOPSIS + +=cut + +=head1 DESCRIPTION + +This script produces suggestions for the OPAC given a search string. + +It takes the following parameters: + +=over 8 + +=item I + +Required. Query string. + +=item I + +If set to 'stub' render a stub HTML page suitable for inclusion into a +div via AJAX. If set to 'standalone', return a full page instead of the stub. +If not set, return JSON. + +=item I + +Number of suggestions to display. Defaults to 4 in stub mode, 20 otherwise. + +=back + +=cut + +use strict; +use warnings; + +use C4::Auth; +use C4::Context; +use C4::Output; +use CGI; +use JSON; +use Koha::SuggestionEngine; + +my $query = new CGI; + +my $dbh = C4::Context->dbh; + +my $search = $query->param('q') || ''; +my $render = $query->param('render') || ''; +my $count = $query->param('count') || ( $render eq 'stub' ? 4 : 20 ); + +# open template +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "svc/suggestion.tt", + query => $query, + type => "opac", + authnotrequired => ( C4::Context->preference("OpacPublic") ? 1 : 0 ), + debug => 1, + } +); + +unless ( C4::Context->preference('DidYouMeanFromAuthorities') ) { + print $query->header; + exit; +} + +my $suggestor = Koha::SuggestionEngine->new( { plugins => ('AuthorityFile') } ); + +my $suggestions = + $suggestor->get_suggestions( { search => $search, count => $count } ); + +if ($render) { + $template->{VARS}->{render} = $render; + $template->{VARS}->{suggestions} = $suggestions if $suggestions; + output_html_with_http_headers $query, $cookie, $template->output; +} +else { + print $query->header; + print to_json($suggestions); +} diff --git a/t/SuggestionEngine.t b/t/SuggestionEngine.t new file mode 100755 index 0000000000..67a4949a40 --- /dev/null +++ b/t/SuggestionEngine.t @@ -0,0 +1,46 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use File::Spec; + +use Test::More; + +BEGIN { + use_ok('Koha::SuggestionEngine'); +} + +my $plugindir = File::Spec->rel2abs('Koha/SuggestionEngine/Plugin'); + +opendir(my $dh, $plugindir); +my @installed_plugins = map { ( /\.pm$/ && -f "$plugindir/$_" && s/\.pm$// ) ? "Koha::SuggestionEngine::Plugin::$_" : () } readdir($dh); +my @available_plugins = Koha::SuggestionEngine::AvailablePlugins(); + +foreach my $plugin (@installed_plugins) { + ok(grep($plugin, @available_plugins), "Found plugin $plugin"); +} + +my $suggestor = Koha::SuggestionEngine->new( { plugins => ( 'ABCD::EFGH::IJKL' ) } ); + +is(ref($suggestor), 'Koha::SuggestionEngine', 'Created suggestion engine with invalid plugin'); +is(scalar @{ $suggestor->get_suggestions({ 'search' => 'books' }) }, 0 , 'Request suggestions with empty suggestor'); + +$suggestor = Koha::SuggestionEngine->new( { plugins => ( 'Null' ) } ); +is(ref($suggestor->plugins->[0]), 'Koha::SuggestionEngine::Plugin::Null', 'Created record suggestor with implicitly scoped Null filter'); + +$suggestor = Koha::SuggestionEngine->new( { plugins => ( 'Koha::SuggestionEngine::Plugin::Null' ) } ); +is(ref($suggestor->plugins->[0]), 'Koha::SuggestionEngine::Plugin::Null', 'Created record suggestor with explicitly scoped Null filter'); + +my $suggestions = $suggestor->get_suggestions({ 'search' => 'books' }); + +is_deeply($suggestions->[0], { 'search' => 'book', label => 'Book!', relevance => 1 }, "Good suggestion"); + +$suggestions = $suggestor->get_suggestions({ 'search' => 'silliness' }); + +eval { + $suggestor = Koha::SuggestionEngine->new( { plugins => ( 'Koha::SuggestionEngine::Plugin::Null' ) } ); + undef $suggestor; +}; +ok(!$@, 'Destroyed suggestor successfully'); + +done_testing(); diff --git a/t/SuggestionEngine_AuthorityFile.t b/t/SuggestionEngine_AuthorityFile.t new file mode 100755 index 0000000000..b9ab35b620 --- /dev/null +++ b/t/SuggestionEngine_AuthorityFile.t @@ -0,0 +1,43 @@ +#!/usr/bin/perl +# +# This Koha test module uses Test::MockModule to get around the need for known +# contents in the authority file by returning a single known authority record +# for every call to SearchAuthorities + +use strict; +use warnings; +use File::Spec; +use MARC::Record; + +use Test::More; +use Test::MockModule; + +BEGIN { + use_ok('Koha::SuggestionEngine'); +} + +my $module = new Test::MockModule('C4::AuthoritiesMarc'); +$module->mock('SearchAuthorities', sub { + return [ { 'authid' => '1234', + 'reported_tag' => undef, + 'even' => 0, + 'summary' => { + 'authorized' => [ 'Cooking' ], + 'otherscript' => [], + 'seefrom' => [ 'Cookery' ], + 'notes' => [ 'Your quintessential poor heading selection' ], + 'seealso' => [] + }, + 'used' => 1, + 'authtype' => 'Topical Term' + } ], 1 +}); + +my $suggestor = Koha::SuggestionEngine->new( { plugins => ( 'AuthorityFile' ) } ); +is(ref($suggestor), 'Koha::SuggestionEngine', 'Created suggestion engine'); + +my $result = $suggestor->get_suggestions({search => 'Cookery'}); + +is_deeply($result, [ { 'search' => 'an=1234', 'relevance' => 1, 'label' => 'Cooking' } ], "Suggested correct alternative to 'Cookery'"); + +done_testing(); -- 2.39.5