Bug 8726: ExplodedTerms suggestion plugin (functionality)

When working with hierarchical subject headings, it is sometimes helpful
to do a search for all records with a specific subject, plus
broader/narrower/related subjects. This patch adds a suggestion plugin for
these "exploded" subject searches to Koha. Note that this patch depends on
both bug 8211 AND bug 8209.

To test (NOTE: this test plan covers both 8211 and 8726):
1) Make sure you have a bunch of hierarchical subjects. I created
   geographical subjects for "Arizona," "United States," and "Phoenix,"
   and linked them together using 551s, and made sure I had a half
   dozen records linking to each (but not all to all three).
2) Do a search for su-br:Arizona (or choose "Subject and broader terms"
   on the advanced search screen with "more options" displayed), and
   check that you get the records with the subject "Arizona" and the
   records with the subject "United States"
3) Do a search for su-na:Arizona (or choose "Subject and narrower terms"
   on the advanced search screen with "more options" displayed), and
   check that you get the records with the subject "Arizona" and the
   records with the subject "Phoenix"
4) Do a search for su-rl:Arizona (or choose "Subject and related terms"
   on the advanced search screen with "more options" displayed), and
   check that you get the records with the subject "Arizona," the
   records with the subject "United States," and the records with the
   subject "Phoenix"
5) Ensure that other searches still work (keyword, subject, ccl,
   whatever)
6) Use "Did you mean?" page in admin section to enable ExplodedTerms
   plugin
7) Do a keyword search on the OPAC, confirm that searching for exploded
   terms is suggested.
8) Do a subject search on the OPAC, confirm that searching for exploded
   terms is suggested.
9) Do a non-keyword, non-subject search on the OPAC, confirm that
   searching for exploded terms is NOT suggested.
10) Disable ExplodedTerms plugin and enable AuthorityFile plugin.
11) Do search on OPAC, confirm suggestions are made from authority file.
12) Sign off

Signed-off-by: wajasu <matted-34813@mypacks.net>
Signed-off-by: Jared Camins-Esakov <jcamins@cpbibliography.com>
Split into two patches. This patch includes only the functionality.
This commit is contained in:
Jared Camins-Esakov 2012-09-14 09:08:10 -04:00 committed by Paul Poulain
parent 35b7162adf
commit d2cd2e09aa
15 changed files with 375 additions and 11 deletions

View File

@ -87,7 +87,7 @@ sub new {
my $options = $param->{options} || '';
my @plugins = ();
foreach my $plugin ( $param->{plugins} ) {
foreach my $plugin ( @{$param->{plugins}} ) {
next unless $plugin;
my $plugin_module =
$plugin =~ m/:/
@ -141,15 +141,18 @@ sub get_suggestions {
my %suggestions;
my $index = scalar @{ $self->plugins };
foreach my $pluginobj ( @{ $self->plugins } ) {
next unless $pluginobj;
my $pluginres = $pluginobj->get_suggestions($param);
foreach my $suggestion (@$pluginres) {
$suggestions{ $suggestion->{'search'} }->{'relevance'} +=
$suggestion->{'relevance'};
$suggestion->{'relevance'} * $index;
$suggestions{ $suggestion->{'search'} }->{'label'} |=
$suggestion->{'label'};
}
$index--;
}
my @results = ();

View File

@ -60,8 +60,6 @@ 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
@ -125,4 +123,46 @@ sub get_suggestions {
return;
}
=head2 NAME
my $name = $plugin->NAME;
Getter function for plugin names.
=cut
sub NAME {
my $self = shift;
my $package = ref $self || $self;
return eval '$' . $package . '::NAME';
}
=head2 VERSION
my $version = $plugin->VERSION;
Getter function for plugin versions.
=cut
sub VERSION {
my $self = shift;
my $package = ref $self || $self;
return eval '$' . $package . '::VERSION';
}
=head2 DESCRIPTION
my $description = $plugin->DESCRIPTION;
Getter function for plugin descriptions.
=cut
sub DESCRIPTION {
my $self = shift;
my $package = ref $self || $self;
return eval '$' . $package . '::DESCRIPTION';
}
1;

View File

@ -0,0 +1,87 @@
package Koha::SuggestionEngine::Plugin::ExplodedTerms;
# 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::ExplodedTerms - suggest searches for broader/narrower/related subjects
=head1 SYNOPSIS
=head1 DESCRIPTION
Plugin to suggest expanding the search by adding broader/narrower/related
subjects to subject searches.
=cut
use strict;
use warnings;
use Carp;
use C4::Templates qw(gettemplate); # This is necessary for translatability
use base qw(Koha::SuggestionEngine::Base);
our $NAME = 'ExplodedTerms';
our $VERSION = '1.0';
=head2 get_suggestions
my $suggestions = $plugin->get_suggestions(\%param);
Return suggestions for the specified search that add broader/narrower/related
terms to the search.
=cut
sub get_suggestions {
my $self = shift;
my $param = shift;
my $search = $param->{'search'};
return if ( $search =~ m/^(ccl=|cql=|pqf=)/ );
$search =~ s/(su|su-br|su-na|su-rl)[:=](\w*)/OP!$2/g;
return if ( $search =~ m/\w+[:=]\w+/ );
my @indexes = (
'su-na',
'su-br',
'su-rl'
);
my $cgi = new CGI;
my $template = C4::Templates::gettemplate('text/explodedterms.tt', 'opac', $cgi);
my @results;
foreach my $index (@indexes) {
my $thissearch = $search;
$thissearch = "$index=$thissearch"
unless ( $thissearch =~ s/OP!/$index=/g );
$template->{VARS}->{index} = $index;
my $label = pack("U0a*", $template->output); #FIXME: C4::Templates is
# returning incorrectly-marked UTF-8. This fixes the problem, but is
# an annoying workaround.
push @results,
{
'search' => $thissearch,
relevance => 100,
# FIXME: it'd be nice to have some empirical measure of
# "relevance" in this case, but we don't.
label => $label
};
} return \@results;
}

37
admin/didyoumean.pl Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/perl
use strict;
use warnings;
use CGI;
use C4::Context;
use C4::Auth;
use C4::Output;
use Koha::SuggestionEngine;
use Module::Load::Conditional qw(can_load);
use JSON;
my $input = new CGI;
my ($template, $loggedinuser, $cookie)
= get_template_and_user({template_name => "admin/didyoumean.tt",
query => $input,
type => "intranet",
authnotrequired => 0,
flagsrequired => {parameters => 'parameters_remaining_permissions'},
debug => 1,
});
my $opacplugins = from_json(C4::Context->preference('OPACdidyoumean') || '[]');
my $intraplugins = from_json(C4::Context->preference('INTRAdidyoumean') || '[]');
my @pluginlist = Koha::SuggestionEngine::AvailablePlugins();
foreach my $plugin (@pluginlist) {
next if $plugin eq 'Koha::SuggestionEngine::Plugin::Null';
next unless (can_load( modules => { "$plugin" => undef } ));
push @$opacplugins, { name => $plugin->NAME } unless grep { $_->{name} eq $plugin->NAME } @$opacplugins;
push @$intraplugins, { name => $plugin->NAME } unless grep { $_->{name} eq $plugin->NAME } @$intraplugins;
}
$template->{VARS}->{OPACpluginlist} = $opacplugins;
$template->{VARS}->{INTRApluginlist} = $intraplugins;
output_html_with_http_headers $input, $cookie, $template->output;

View File

@ -376,10 +376,11 @@ 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');
INSERT INTO systempreferences (variable,value,options,explanation,type) VALUES ('IncludeSeeFromInSearches','0','','Include see-from references in searches.','YesNo');
INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OPACMobileUserCSS','','Include the following CSS for the mobile view on all pages in the OPAC:',NULL,'free');
INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpacMainUserBlockMobile','','Show the following HTML in its own column on the main page of the OPAC (mobile version):',NULL,'free');
INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpacShowLibrariesPulldownMobile','1','Show the libraries pulldown on the mobile version of the OPAC.',NULL,'YesNo');
INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpacShowFiltersPulldownMobile','1','Show the search filters pulldown on the mobile version of the OPAC.',NULL,'YesNo');
INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES('AuthDisplayHierarchy','0','Display authority hierarchies','','YesNo');
INSERT INTO systempreferences (variable,value,explanation,type) VALUES('OPACdidyoumean',NULL,'Did you mean? configuration for the OPAC. Do not change, as this is controlled by /cgi-bin/koha/admin/didyoumean.pl.','Free');
INSERT INTO systempreferences (variable,value,explanation,type) VALUES('INTRAdidyoumean',NULL,'Did you mean? configuration for the Intranet. Do not change, as this is controlled by /cgi-bin/koha/admin/didyoumean.pl.','Free');

View File

@ -5768,7 +5768,6 @@ if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) {
SetVersion($DBversion);
}
$DBversion = '3.09.00.044';
if (C4::Context->preference("Version") < TransformToNum($DBversion)) {
$dbh->do("ALTER TABLE statistics ADD COLUMN ccode VARCHAR ( 10 ) NULL AFTER associatedborrower");
@ -5951,7 +5950,6 @@ if (C4::Context->preference("Version") < TransformToNum($DBversion)) {
SetVersion ($DBversion);
}
$DBversion = "3.09.00.054";
if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) {
$dbh->do("ALTER TABLE aqorders CHANGE COLUMN gst gstrate DECIMAL(6,4) DEFAULT NULL");
@ -5982,6 +5980,14 @@ if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) {
SetVersion($DBversion);
}
$DBversion ="3.09.00.058";
if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) {
$dbh->do("INSERT INTO systempreferences (variable,value,explanation,type) VALUES('OPACdidyoumean',NULL,'Did you mean? configuration for the OPAC. Do not change, as this is controlled by /cgi-bin/koha/admin/didyoumean.pl.','Free');");
$dbh->do("INSERT INTO systempreferences (variable,value,explanation,type) VALUES('INTRAdidyoumean',NULL,'Did you mean? configuration for the Intranet. Do not change, as this is controlled by /cgi-bin/koha/admin/didyoumean.pl.','Free');");
print "Upgrade to $DBversion done (Add Did You Mean? configuration)\n";
SetVersion($DBversion);
}
=head1 FUNCTIONS
=head2 TableExists($table)

View File

@ -2434,6 +2434,42 @@ ul.ui-tabs-nav li {
color: #990033;
}
#didyoumeanopac, #didyoumeanintranet {
float: left;
width: 260px;
}
#didyoumeanlegend {
float: right;
}
.pluginlist {
padding-bottom: 10px;
}
.plugin {
margin: 0 1em 1em 0;
}
.pluginname {
margin: 0.3em;
padding-bottom: 4px;
padding-left: 0.2em;
background-color: #E6F0F2;
}
.pluginname .ui-icon {
float: right;
}
.plugindesc {
padding: 0.4em;
}
.ui-sortable-placeholder {
border: 1px dotted black;
visibility: visible !important;
height: 80px !important;
}
.ui-sortable-placeholder * {
visibility: hidden;
}
/* jQuery UI Datepicker */
.ui-datepicker table {
width: 100%;

View File

@ -62,6 +62,7 @@
[% IF ( NoZebra ) %]<li><a href="/cgi-bin/koha/admin/stopwords.pl">Stop words</a></li>[% END %]
<!-- <li><a href="/cgi-bin/koha/admin/printers.pl">Network Printers</a></li> -->
<li><a href="/cgi-bin/koha/admin/z3950servers.pl">Z39.50 client targets</a></li>
<li><a href="/cgi-bin/koha/admin/didyoumean.pl">Did you mean?</a></li>
</ul>
</div>
</div>

View File

@ -107,6 +107,8 @@
<dd>Printers (UNIX paths).</dd> -->
<dt><a href="/cgi-bin/koha/admin/z3950servers.pl">Z39.50 client targets</a></dt>
<dd>Define which servers to query for MARC data in the integrated Z39.50 client.</dd>
<dt><a href="/cgi-bin/koha/admin/didyoumean.pl">Did you mean?</a></dt>
<dd>Choose which plugins to use to suggest searches to patrons and staff.</dd>
</dl>
</div>

View File

@ -0,0 +1,104 @@
[% BLOCK pluginlist %]
<div class="pluginlist">
[% FOREACH plugin IN plugins %]
<div class="plugin">
<div class="pluginname">
[% IF plugin.enabled %]<input type="checkbox" checked="checked" id="checkbox_[% plugin.name %]">[% ELSE %]<input type="checkbox" id="checkbox_[% plugin.name %]">[% END %]
<label class='pluginlabel' for="checkbox_[% plugin.name %]">[% plugin.name %]</label></div>
<div class="plugindesc">
[% SWITCH plugin.name %]
[% CASE 'AuthorityFile' %]
Suggest authorities which are relevant to the term the user searched for.
[% CASE 'ExplodedTerms' %]
Suggest that patrons expand their searches to include
broader/narrower/related terms.
[% END %]
</div>
</div>
[% END %]
</div>
[% END %]
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Administration &rsaquo; Did you mean?</title>
[% INCLUDE 'doc-head-close.inc' %]
<script>
$(document).ready(function() {
$( ".pluginlist" ).sortable();
$( ".plugin" ).addClass( "ui-widget ui-widget-content ui-helper-clearfix ui-corner-all" )
.find( ".pluginname" )
.addClass( "ui-widget-header ui-corner-all" )
.end()
.find( ".plugindesc" );
});
function yesimeant() {
var OPACdidyoumean = serialize_plugins('opac');
var INTRAdidyoumean = serialize_plugins('intranet');
var data = "pref_OPACdidyoumean=" + encodeURIComponent(OPACdidyoumean) + "&pref_INTRAdidyoumean=" + encodeURIComponent(INTRAdidyoumean);
$.ajax({
data: data,
type: 'POST',
url: '/cgi-bin/koha/svc/config/systempreferences/',
success: function () { alert("Successfully saved configuration"); },
});
return false;
}
function serialize_plugins(interface) {
var serializedconfig = '[';
$('#didyoumean' + interface + ' .pluginlist .plugin').each(function(index) {
var name = $(this).find('.pluginlabel').text();
var enabled = $(this).find('#checkbox_' + name).attr('checked') == 'checked' ?
', "enabled": 1' : '';
serializedconfig += '{ "name": "' + name + '"' + enabled + '}, ';
});
serializedconfig = serializedconfig.substring(0, serializedconfig.length - 2);
serializedconfig += ']';
return serializedconfig;
}
</script>
</head>
<body id="admin_didyoumean" class="admin">
[% INCLUDE 'header.inc' %]
[% INCLUDE 'cat-search.inc' %]
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/admin/admin-home.pl">Administration</a> &rsaquo; Did you mean?</div>
<div id="doc3" class="yui-t2">
<div id="bd">
<div id="yui-main">
<div class="yui-b">
<h3>Did you mean?</h3>
<noscript><div class="dialog alert"><strong>Please enable Javascript:</strong>
Configuring <em>Did you mean?</em> plugins requires Javascript. If
you are unable to use Javascript, you may be able to enter the
configuration (which is stored in JSON in the OPACdidyoumean and
INTRAdidyoumean system preferences) in the Local Preferences tab in
the system preference editor, but this is unsupported, not
recommended, and likely will not work.</div></noscript>
<div id="didyoumeanlegend">
Please put the <em>Did you mean?</em> plugins in order by significance, from
most significant to least significant, and check the box to enable those
plugins that you want to use.
</div>
<form action="/cgi-bin/koha/admin/didyoumean.pl" method="post">
<fieldset id="didyoumeanopac">
<legend>OPAC</legend>
[% PROCESS pluginlist plugins=OPACpluginlist %]
</fieldset>
<fieldset id="didyoumeanintranet">
<legend>Intranet</legend>
[% PROCESS pluginlist plugins=INTRApluginlist %]
</fieldset>
<fieldset class="action"><button class="save-all submit" onclick="yesimeant();return false;" type="submit">Save configuration</button> <a href="#" onclick="window.location.reload(true);" class="cancel">Cancel</a></fieldset>
</form>
</div>
</div>
<div class="yui-b">
[% INCLUDE 'admin-menu.inc' %]
</div>
</div>
[% INCLUDE 'intranet-bottom.inc' %]

View File

@ -285,7 +285,7 @@ $(document).ready(function(){
<div id="yui-main">
<div class="yui-b">
<div id="userresults" class="container">
[% IF ( DidYouMeanFromAuthorities ) %]
[% IF ( DidYouMean ) %]
<div id='didyoumean'>Not what you expected? Check for <a href='/cgi-bin/koha/svc/suggestion?render=standalone&q=[% querystring | uri %]'>suggestions</a></div>
[% END %]
[% INCLUDE 'page-numbers.inc' %]<br />

View File

@ -0,0 +1,8 @@
[%- SWITCH index -%]
[%- CASE 'su-na' -%]
Search also for narrower subjects
[%- CASE 'su-br' -%]
Search also for broader subjects
[%- CASE 'su-rl' -%]
Search also for related subjects
[%- END -%]

View File

@ -823,7 +823,7 @@ if (C4::Context->preference('GoogleIndicTransliteration')) {
$template->param('GoogleIndicTransliteration' => 1);
}
$template->{VARS}->{DidYouMeanFromAuthorities} = C4::Context->preference('DidYouMeanFromAuthorities');
$template->{VARS}->{DidYouMean} = C4::Context->preference('OPACdidyoumean') =~ m/enable/;
$template->param( borrowernumber => $borrowernumber);
output_with_http_headers $cgi, $cookie, $template->output, $content_type;

View File

@ -80,12 +80,20 @@ my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
}
);
unless ( C4::Context->preference('DidYouMeanFromAuthorities') ) {
my @plugins = ();
my $pluginsconfig = from_json(C4::Context->preference('OPACdidyoumean') || '[]');
foreach my $plugin (@$pluginsconfig) {
push @plugins, $plugin->{name} if ($plugin->{enabled});
}
unless ( @plugins ) {
print $query->header;
exit;
}
my $suggestor = Koha::SuggestionEngine->new( { plugins => ('AuthorityFile') } );
my $suggestor = Koha::SuggestionEngine->new( { plugins => \@plugins } );
my $suggestions =
$suggestor->get_suggestions( { search => $search, count => $count } );

View File

@ -0,0 +1,31 @@
#!/usr/bin/perl
use strict;
use warnings;
use Test::More;
BEGIN {
use_ok('Koha::SuggestionEngine');
}
my $suggestor = Koha::SuggestionEngine->new( { plugins => ( 'ExplodedTerms' ) } );
is(ref($suggestor), 'Koha::SuggestionEngine', 'Created suggestion engine');
my $result = $suggestor->get_suggestions({search => 'Cookery'});
ok((grep { $_->{'search'} eq 'su-na=Cookery' } @$result) && (grep { $_->{'search'} eq 'su-br=Cookery' } @$result) && (grep { $_->{'search'} eq 'su-rl=Cookery' } @$result), "Suggested correct alternatives for keyword search 'Cookery'");
$result = $suggestor->get_suggestions({search => 'su:Cookery'});
ok((grep { $_->{'search'} eq 'su-na=Cookery' } @$result) && (grep { $_->{'search'} eq 'su-br=Cookery' } @$result) && (grep { $_->{'search'} eq 'su-rl=Cookery' } @$result), "Suggested correct alternatives for subject search 'Cookery'");
$result = $suggestor->get_suggestions({search => 'nt:Cookery'});
is(scalar @$result, 0, "No suggestions for fielded search");
$result = $suggestor->get_suggestions({search => 'ccl=su:Cookery'});
is(scalar @$result, 0, "No suggestions for CCL search");
done_testing();