Bug 14567: Add a browse interface to the OPAC
This is an interface for quick and efficient browsing through records. It presents a page at /cgi-bin/koha/opac-browse.pl that allows you to enter the prefix of an author, title, or subject and it'll give you a list of the options that match that. You can then scroll through these and select the one you're after. Selecting it provides a list of records that match that particular search. To Test: 1 - Apply patches 2 - Update database (updatedatabase on kohadevbox) 3 - Compile the CSS https://wiki.koha-community.org/wiki/Working_with_SCSS_in_the_OPAC_and_staff_client yarn build --view=opac on kohadevbox 4 - Enable the new syspref OpacBrowseSearch 5 - Have ES running and some records in it SearchEngine syspref set to Elasticsearch 6 - Browse to opac home, click 'Browse search' link for your site) 7 - Test searching for author, title, and subject 8 - Verify that results are returned in expected order 9 - Experiment with fuzziness https://www.elastic.co/guide/en/elasticsearch/reference/5.6/common-options.html#fuzziness Options are: exact (0 edits), fuzzy (1 edit), very fuzzy (2 edits) 10 - Click any result and verify specific titles are correct 11 - Click through title to record and verify it is the correct record 12 - Test that disabling pref removes the link on the opac home Signed-off-by: David Nind <david@davidnind.com> Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de> Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
This commit is contained in:
parent
24154f2ec9
commit
0e7f7ab051
|
|
@ -0,0 +1,183 @@
|
|||
package Koha::SearchEngine::Elasticsearch::Browse;
|
||||
|
||||
# Copyright 2015 Catalyst IT
|
||||
#
|
||||
# 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 3 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::SearchEngine::ElasticSearch::Browse - browse functions for Elasticsearch
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
my $browser =
|
||||
Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } );
|
||||
my $results = $browser->browse(
|
||||
'prefi', 'title',
|
||||
{
|
||||
results => '500',
|
||||
fuzziness => 2,
|
||||
}
|
||||
);
|
||||
foreach my $r (@$results) {
|
||||
push @hits, $r->{text};
|
||||
}
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This provides an easy interface to the "browse" functionality. Essentially,
|
||||
it does a fast prefix search on defined fields. The fields have to be marked
|
||||
as "suggestible" in the database when indexing takes place.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=cut
|
||||
|
||||
use base qw(Koha::SearchEngine::Elasticsearch);
|
||||
use Modern::Perl;
|
||||
|
||||
use Catmandu::Store::ElasticSearch;
|
||||
|
||||
Koha::SearchEngine::Elasticsearch::Browse->mk_accessors(qw( store ));
|
||||
|
||||
=head2 browse
|
||||
|
||||
my $results = $browser->browse($prefix, $field, \%options);
|
||||
|
||||
Does a prefix search for C<$prefix>, looking in C<$field>. Options are:
|
||||
|
||||
=over 4
|
||||
|
||||
=item count
|
||||
|
||||
The number of results to return. For Koha browse purposes, this should
|
||||
probably be fairly high. Defaults to 500.
|
||||
|
||||
=item fuzziness
|
||||
|
||||
How much allowing for typos and misspellings is done. If 0, then it must match
|
||||
exactly. If unspecified, it defaults to '1', which is probably the most useful.
|
||||
Otherwise, it is a number specifying the Levenshtein edit distance relative to
|
||||
the string length, according to the following lengths:
|
||||
|
||||
=over 4
|
||||
|
||||
=item 0..2
|
||||
|
||||
must match exactly
|
||||
|
||||
=item 3..5
|
||||
|
||||
C<fuzziness> edits allowed
|
||||
|
||||
=item >5
|
||||
|
||||
C<fuzziness>+1 edits allowed
|
||||
|
||||
=back
|
||||
|
||||
In all cases the maximum number of edits allowed is two (an elasticsearch
|
||||
restriction.)
|
||||
|
||||
=back
|
||||
|
||||
=head3 Returns
|
||||
|
||||
This returns an arrayref of hashrefs. Each hashref contains a "text" element
|
||||
that contains the field as returned. There may be other fields in that
|
||||
hashref too, but they're less likely to be important.
|
||||
|
||||
The array will be ordered as returned from Elasticsearch, which seems to be
|
||||
in order of some form of relevance.
|
||||
|
||||
=cut
|
||||
|
||||
sub browse {
|
||||
my ($self, $prefix, $field, $options) = @_;
|
||||
|
||||
my $params = $self->get_elasticsearch_params();
|
||||
$self->store(
|
||||
Catmandu::Store::ElasticSearch->new(
|
||||
%$params,
|
||||
)
|
||||
) unless $self->store;
|
||||
|
||||
my $query = $self->_build_query($prefix, $field, $options);
|
||||
my $results = $self->store->bag->search(%$query);
|
||||
return $results->{suggest}{suggestions}[0]{options};
|
||||
}
|
||||
|
||||
=head2 _build_query
|
||||
|
||||
my $query = $self->_build_query($prefix, $field, $options);
|
||||
|
||||
Arguments are the same as for L<browse>. This will return a query structure
|
||||
for elasticsearch to use.
|
||||
|
||||
=cut
|
||||
|
||||
sub _build_query {
|
||||
my ( $self, $prefix, $field, $options ) = @_;
|
||||
|
||||
$options = {} unless $options;
|
||||
my $f = $options->{fuzziness} // 1;
|
||||
my $l = length($prefix);
|
||||
my $fuzzie;
|
||||
if ( $l <= 2 ) {
|
||||
$fuzzie = 0;
|
||||
}
|
||||
elsif ( $l <= 5 ) {
|
||||
$fuzzie = $f;
|
||||
}
|
||||
else {
|
||||
$fuzzie = $f + 1;
|
||||
}
|
||||
$fuzzie = 2 if $fuzzie > 2;
|
||||
|
||||
my $size = $options->{count} // 500;
|
||||
my $query = {
|
||||
# this is an annoying thing, if we set size to 0 it gets rewritten
|
||||
# to 10. There's a bug somewhere in one of the libraries.
|
||||
size => 1,
|
||||
suggest => {
|
||||
suggestions => {
|
||||
text => $prefix,
|
||||
completion => {
|
||||
field => $field . '__suggestion',
|
||||
size => $size,
|
||||
fuzzy => {
|
||||
fuzziness => $fuzzie,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return $query;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
=over 4
|
||||
|
||||
=item Robin Sheat << <robin@catalyst.net.nz> >>
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
|
@ -59,6 +59,7 @@ suggestible:
|
|||
default:
|
||||
type: completion
|
||||
analyzer: simple
|
||||
max_input_length: 100
|
||||
search_analyzer: simple
|
||||
# Sort
|
||||
sort:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
$DBversion = 'XXX';
|
||||
if( CheckVersion( $DBversion ) ) {
|
||||
$dbh->do(q{
|
||||
INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type)
|
||||
VALUES
|
||||
('OpacBrowseSearch', '0',NULL, "Elasticsearch only: add a page allowing users to 'browse' all items in the collection",'YesNo')
|
||||
});
|
||||
SetVersion( $DBversion );
|
||||
print "Upgrade to $DBversion done (Bug 14567: Add OpacBrowseSearch syspref)\n";
|
||||
}
|
||||
|
|
@ -365,6 +365,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
|
|||
('opacbookbag','1','','If ON, enables display of Cart feature','YesNo'),
|
||||
('OpacBrowser','0',NULL,'If ON, enables subject authorities browser on OPAC (needs to set misc/cronjob/sbuild_browser_and_cloud.pl)','YesNo'),
|
||||
('OpacBrowseResults','1',NULL,'Disable/enable browsing and paging search results from the OPAC detail page.','YesNo'),
|
||||
('OpacBrowseSearch', '0',NULL, "Elasticsearch only: add a page allowing users to 'browse' all items in the collection",'YesNo'),
|
||||
('OpacCloud','0',NULL,'If ON, enables subject cloud on OPAC','YesNo'),
|
||||
('OpacAdditionalStylesheet','','','Define an auxiliary stylesheet for OPAC use, to override specified settings from the primary opac.css stylesheet. Enter the filename (if the file is in the server\'s css directory) or a complete URL beginning with http (if the file lives on a remote server).','free'),
|
||||
('OpacCoce','0', NULL, 'If on, enables cover retrieval from the configured Coce server in the OPAC', 'YesNo'),
|
||||
|
|
|
|||
|
|
@ -558,6 +558,13 @@ OPAC:
|
|||
type: textarea
|
||||
syntax: text/html
|
||||
class: code
|
||||
-
|
||||
- pref: OpacBrowseSearch
|
||||
default: 0
|
||||
choices:
|
||||
yes: Enable
|
||||
no: Disable
|
||||
- "(Elasticsearch only) Enable the interface allowing to browse all holdings."
|
||||
OpenURL:
|
||||
-
|
||||
- 'Complete URL of OpenURL resolver (starting with <code>http://</code> or <code>https://</code>):'
|
||||
|
|
|
|||
|
|
@ -3101,4 +3101,105 @@ $star-selected: #EDB867;
|
|||
}
|
||||
}
|
||||
|
||||
/*opac browse search*/
|
||||
#browse-search {
|
||||
|
||||
form {
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right:5px;
|
||||
}
|
||||
|
||||
[type=submit] {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#browse-resultswrapper {
|
||||
margin-top: 4em;
|
||||
|
||||
@media (min-width: 768px) and (max-width: 984px) {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
#browse-searchresults, #browse-selectionsearch {
|
||||
border: 1px solid #E3E3E3;
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
max-height: 31em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
#browse-searchresults {
|
||||
max-height: 31em;
|
||||
list-style: none;
|
||||
padding: 10px;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&.selected {
|
||||
background-color:#EEE;
|
||||
}
|
||||
}
|
||||
|
||||
li:last-child a {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
max-height: 13em;
|
||||
}
|
||||
}
|
||||
#browse-selection {
|
||||
margin-top: -40px;
|
||||
padding-top: 0;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
#browse-selectionsearch ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
padding: 1em;
|
||||
|
||||
&:nth-child(odd) {
|
||||
background-color: #F4F4F4;
|
||||
}
|
||||
}
|
||||
}
|
||||
#browse-selectionsearch p.subjects {
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#browse-selectionsearch h4 {
|
||||
margin: 0;
|
||||
}
|
||||
.error, .no-results {
|
||||
background-color: #EEE;
|
||||
border: 1px solid #E8E8E8;
|
||||
text-align: left;
|
||||
padding: 0.5em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
margin:0.5em 0;
|
||||
position: relative;
|
||||
left: -5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
/*end browse search*/
|
||||
|
||||
@import "responsive";
|
||||
|
|
|
|||
|
|
@ -304,6 +304,7 @@
|
|||
[% IF Koha.Preference( 'TagsEnabled' ) == 1 %]<li><a href="/cgi-bin/koha/opac-tags.pl">Tag cloud</a></li>[% END %]
|
||||
[% IF Koha.Preference( 'OpacCloud' ) == 1 %]<li><a href="/cgi-bin/koha/opac-tags_subject.pl">Subject cloud</a></li>[% END %]
|
||||
[% IF Koha.Preference( 'OpacTopissue' ) == 1 %]<li><a href="/cgi-bin/koha/opac-topissues.pl">Most popular</a></li>[% END %]
|
||||
[% IF Koha.Preference('SearchEngine') == 'Elasticsearch' && Koha.Preference( 'OpacBrowseSearch' ) == 1 %]<li><a href="/cgi-bin/koha/opac-browse.pl">Browse search</a></li>[% END %]
|
||||
[% IF Koha.Preference( 'suggestion' ) == 1 %]
|
||||
[% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
|
||||
<li><a href="/cgi-bin/koha/opac-suggestions.pl">Purchase suggestions</a></li>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
[% USE Koha %]
|
||||
[% USE Asset %]
|
||||
[% USE raw %]
|
||||
[% INCLUDE 'doc-head-open.inc' %]
|
||||
<title>[% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog › Browse our catalog</title>
|
||||
[% INCLUDE 'doc-head-close.inc' %]
|
||||
[% BLOCK cssinclude %][% END %]
|
||||
[% INCLUDE 'bodytag.inc' bodyid='opac-browser' %]
|
||||
[% INCLUDE 'masthead.inc' %]
|
||||
|
||||
<div class="main">
|
||||
<ul class="breadcrumb">
|
||||
<li><a href="/cgi-bin/koha/opac-main.pl">Home</a>
|
||||
<span class="divider">›</span></li>
|
||||
|
||||
<li><a href="#">Browse search</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
[% IF Koha.Preference('SearchEngine') == 'Elasticsearch' && Koha.Preference('OpacBrowseSearch') %]
|
||||
[% IF ( OpacNav || OpacNavBottom ) %]
|
||||
|
||||
<div class="span2">
|
||||
<div id="navigation">
|
||||
[% INCLUDE 'navigation.inc' %]
|
||||
</div>
|
||||
</div>
|
||||
[% END %]
|
||||
|
||||
[% IF ( OpacNav ) %]
|
||||
|
||||
<div class="span10">
|
||||
[% ELSE %]
|
||||
|
||||
<div class="span12">
|
||||
[% END %]
|
||||
|
||||
<div id="browse-search">
|
||||
<h1>Browse search</h1>
|
||||
|
||||
<form>
|
||||
<label for="browse-searchterm">Search for:</label>
|
||||
<input type="search" id="browse-searchterm" name="searchterm" value="">
|
||||
<label for="browse-searchfield" class="hide-text">Search type:</label>
|
||||
<select id="browse-searchfield" name="searchfield">
|
||||
<option value="author">Author</option>
|
||||
<option value="subject">Subject</option>
|
||||
<option value="title">Title</option>
|
||||
</select>
|
||||
|
||||
<div id="browse-searchfuzziness">
|
||||
<label for="exact" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="exact" value="0">Exact</label>
|
||||
<label for="fuzzy" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="fuzzy" value="1" checked="checked"> Fuzzy</label>
|
||||
<label for="reallyfuzzy" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="reallyfuzzy" value="2"> Really fuzzy</label>
|
||||
</div>
|
||||
<button class="btn btn-success" type="submit" accesskey="s">Search</button>
|
||||
</form>
|
||||
|
||||
<p id="browse-suggestionserror" class="error hidden">
|
||||
An error occurred, please try again.</p>
|
||||
|
||||
<div id="browse-resultswrapper" class="hidden">
|
||||
<ul id="browse-searchresults" class="span3" start="-1" aria-live="polite">
|
||||
<li class="loading hidden"><img src="[% interface | html %]/[% theme |html %]/images/loading.gif" alt=""> Loading</li>
|
||||
|
||||
<li class="no-results hidden">Sorry, there are no results, try a different search term.</li>
|
||||
</ul>
|
||||
|
||||
<h3 id="browse-selection" class="span9"></h3>
|
||||
|
||||
<div id="browse-selectionsearch" class="span9 hidden">
|
||||
<div class="loading hidden">
|
||||
<img src="[% interface | html %]/[% theme | html %]/images/loading.gif" alt=""> Loading
|
||||
</div>
|
||||
|
||||
<div class="no-results hidden">No results</div>
|
||||
|
||||
<ol aria-live="polite"></ol>
|
||||
</div>
|
||||
</div><!-- / .results-wrapper -->
|
||||
</div><!-- / .userbrowser -->
|
||||
</div><!-- / .span10 -->
|
||||
[% ELSE %]
|
||||
<h3>This feature is not enabled</h3>
|
||||
[% END %]
|
||||
</div><!-- / .row-fluid -->
|
||||
</div><!-- / .container-fluid -->
|
||||
</div><!-- / .main -->
|
||||
[% INCLUDE 'opac-bottom.inc' %]
|
||||
[% BLOCK jsinclude %]
|
||||
[% Asset.js("/js/browse.js") | $raw %]
|
||||
</script>
|
||||
[% END %]
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
jQuery.fn.overflowScrollReset = function() {
|
||||
$(this).scrollTop($(this).scrollTop() - $(this).offset().top);
|
||||
return this;
|
||||
};
|
||||
|
||||
$(document).ready(function(){
|
||||
var xhrGetSuggestions, xhrGetResults;
|
||||
|
||||
$('#browse-search form').submit(function(event) {
|
||||
// if there is an in progress request, abort it so we
|
||||
// don't end up with a race condition
|
||||
if(xhrGetSuggestions && xhrGetSuggestions.readyState != 4){
|
||||
xhrGetSuggestions.abort();
|
||||
}
|
||||
|
||||
var userInput = $('#browse-searchterm').val().trim();
|
||||
var userField = $('#browse-searchfield').val();
|
||||
var userFuzziness = $('input[name=browse-searchfuzziness]:checked', '#browse-searchfuzziness').val();
|
||||
var leftPaneResults = $('#browse-searchresults li').not('.loading, .no-results');
|
||||
var rightPaneResults = $('#browse-selectionsearch ol li');
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if(!userInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove any error states and show the results area (except right pane)
|
||||
$('#browse-suggestionserror').addClass('hidden');
|
||||
$('#browse-searchresults .no-results').addClass('hidden');
|
||||
$('#browse-resultswrapper').removeClass('hidden');
|
||||
$('#browse-selection').addClass('hidden').text("");
|
||||
$('#browse-selectionsearch').addClass('hidden');
|
||||
|
||||
// clear any results from left and right panes
|
||||
leftPaneResults.remove();
|
||||
rightPaneResults.remove();
|
||||
|
||||
// show the spinner in the left pane
|
||||
$('#browse-searchresults .loading').removeClass('hidden');
|
||||
|
||||
xhrGetSuggestions = $.get(window.location.pathname, {api: "GetSuggestions", field: userField, prefix: userInput, fuzziness: userFuzziness})
|
||||
.always(function() {
|
||||
// hide spinner
|
||||
$('#browse-searchresults .loading').addClass('hidden');
|
||||
})
|
||||
.done(function(data) {
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
if (data.length === 0) {
|
||||
$('#browse-searchresults .no-results').removeClass('hidden');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// scroll to top of container again
|
||||
$("#browse-searchresults").overflowScrollReset();
|
||||
|
||||
// store the type of search that was performed as an attrib
|
||||
$('#browse-searchresults').data('field', userField);
|
||||
|
||||
$.each(data, function(index, object) {
|
||||
// use a document fragment so we don't need to nest the elems
|
||||
// or append during each iteration (which would be slow)
|
||||
var elem = document.createElement("li");
|
||||
var link = document.createElement("a");
|
||||
link.textContent = object.text;
|
||||
link.setAttribute("href", "#");
|
||||
elem.appendChild(link);
|
||||
fragment.appendChild(elem);
|
||||
});
|
||||
|
||||
$('#browse-searchresults').append(fragment.cloneNode(true));
|
||||
})
|
||||
.fail(function(jqXHR) {
|
||||
//if 500 or 404 (abort is okay though)
|
||||
if (jqXHR.statusText !== "abort") {
|
||||
$('#browse-resultswrapper').addClass('hidden');
|
||||
$('#browse-suggestionserror').removeClass('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#browse-searchresults').on("click", 'a', function(event) {
|
||||
// if there is an in progress request, abort it so we
|
||||
// don't end up with a race condition
|
||||
if(xhrGetResults && xhrGetResults.readyState != 4){
|
||||
xhrGetResults.abort();
|
||||
}
|
||||
|
||||
var term = $(this).text();
|
||||
var field = $('#browse-searchresults').data('field');
|
||||
var rightPaneResults = $('#browse-selectionsearch ol li');
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// clear any current selected classes and add a new one
|
||||
$(this).parent().siblings().children().removeClass('selected');
|
||||
$(this).addClass('selected');
|
||||
|
||||
// copy in the clicked text
|
||||
$('#browse-selection').removeClass('hidden').text(term);
|
||||
|
||||
// show the right hand pane if it is not shown already
|
||||
$('#browse-selectionsearch').removeClass('hidden');
|
||||
|
||||
// hide the no results element
|
||||
$('#browse-selectionsearch .no-results').addClass('hidden');
|
||||
|
||||
// clear results
|
||||
rightPaneResults.remove();
|
||||
|
||||
// turn the spinner on
|
||||
$('#browse-selectionsearch .loading').removeClass('hidden');
|
||||
|
||||
// do the query for the term
|
||||
xhrGetResults = $.get(window.location.pathname, {api: "GetResults", field: field, term: term})
|
||||
.always(function() {
|
||||
// hide spinner
|
||||
$('#browse-selectionsearch .loading').addClass('hidden');
|
||||
})
|
||||
.done(function(data) {
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
if (data.length === 0) {
|
||||
$('#browse-selectionsearch .no-results').removeClass('hidden');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// scroll to top of container again
|
||||
$("#browse-selectionsearch").overflowScrollReset();
|
||||
|
||||
$.each(data, function(index, object) {
|
||||
// use a document fragment so we don't need to nest the elems
|
||||
// or append during each iteration (which would be slow)
|
||||
var elem = document.createElement("li");
|
||||
var title = document.createElement("h4");
|
||||
var link = document.createElement("a");
|
||||
var author = document.createElement("p");
|
||||
var destination = window.location.pathname;
|
||||
|
||||
destination = destination.replace("browse", "detail");
|
||||
destination = destination + "?biblionumber=" + object.id;
|
||||
|
||||
author.className = "author";
|
||||
|
||||
link.setAttribute("href", destination);
|
||||
link.setAttribute("target", "_blank");
|
||||
link.textContent = object.title;
|
||||
title.appendChild(link);
|
||||
|
||||
author.textContent = object.author;
|
||||
|
||||
elem.appendChild(title);
|
||||
elem.appendChild(author);
|
||||
fragment.appendChild(elem);
|
||||
});
|
||||
|
||||
$('#browse-selectionsearch ol').append(fragment.cloneNode(true));
|
||||
})
|
||||
.fail(function(jqXHR) {
|
||||
//if 500 or 404 (abort is okay though)
|
||||
if (jqXHR.statusText !== "abort") {
|
||||
$('#browse-resultswrapper').addClass('hidden');
|
||||
$('#browse-suggestionserror').removeClass('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
# This is a CGI script that handles the browse feature.
|
||||
|
||||
# Copyright 2015 Catalyst IT
|
||||
#
|
||||
# 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 3 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.
|
||||
|
||||
use Modern::Perl;
|
||||
use CGI qw ( -utf8 );
|
||||
|
||||
use C4::Auth;
|
||||
use C4::Context;
|
||||
use C4::Output;
|
||||
|
||||
use Koha::SearchEngine::Elasticsearch;
|
||||
use Koha::SearchEngine::Elasticsearch::Browse;
|
||||
use Koha::SearchEngine::Elasticsearch::QueryBuilder;
|
||||
use Koha::SearchEngine::Elasticsearch::Search;
|
||||
|
||||
use JSON;
|
||||
use Unicode::Collate;
|
||||
|
||||
my $query = new CGI;
|
||||
binmode STDOUT, ':encoding(UTF-8)';
|
||||
|
||||
# If calling via JS, 'api' is used to route to correct step in process
|
||||
my $api = $query->param('api');
|
||||
|
||||
if ( !$api ) {
|
||||
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
|
||||
{
|
||||
template_name => "opac-browse.tt",
|
||||
query => $query,
|
||||
type => "opac",
|
||||
authnotrequired => ( C4::Context->preference("OpacPublic") ? 1 : 0 ),
|
||||
}
|
||||
);
|
||||
$template->param();
|
||||
output_html_with_http_headers $query, $cookie, $template->output;
|
||||
|
||||
|
||||
}
|
||||
elsif ( $api eq 'GetSuggestions' ) {
|
||||
my $fuzzie = $query->param('fuzziness');
|
||||
my $prefix = $query->param('prefix');
|
||||
my $field = $query->param('field');
|
||||
|
||||
# Under a persistent environment, we should probably not reinit this every time.
|
||||
my $browser = Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } );
|
||||
my $res = $browser->browse( $prefix, $field, { fuzziness => $fuzzie } );
|
||||
|
||||
my %seen;
|
||||
my @sorted =
|
||||
grep { !$seen{$_->{text}}++ }
|
||||
sort { lc($a->{text}) cmp lc($b->{text}) } @$res;
|
||||
print CGI::header(
|
||||
-type => 'application/json',
|
||||
-charset => 'utf-8'
|
||||
);
|
||||
print to_json( \@sorted );
|
||||
}
|
||||
elsif ( $api eq 'GetResults' ) {
|
||||
my $term = $query->param('term');
|
||||
my $field = $query->param('field');
|
||||
|
||||
my $builder = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'biblios' } );
|
||||
my $searcher = Koha::SearchEngine::Elasticsearch::Search->new(
|
||||
{ index => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX } );
|
||||
|
||||
my $query = { query => { term => { $field.".raw" => $term } } } ;
|
||||
my $results = $searcher->search( $query, undef, 500 );
|
||||
my @output = _filter_for_output( $results->{hits}->{hits} );
|
||||
print CGI::header(
|
||||
-type => 'application/json',
|
||||
-charset => 'utf-8'
|
||||
);
|
||||
print to_json( \@output );
|
||||
}
|
||||
|
||||
# This should probably be done with some templatey gizmo
|
||||
# in the future.
|
||||
sub _filter_for_output {
|
||||
my ($records) = @_;
|
||||
my @output;
|
||||
foreach my $rec (@$records) {
|
||||
my $biblionumber = $rec->{_id};
|
||||
my $biblio = Koha::Biblios->find( $biblionumber );
|
||||
next unless $biblio;
|
||||
push @output,
|
||||
{
|
||||
id => $biblionumber,
|
||||
title => $biblio->title,
|
||||
author => $biblio->author,
|
||||
};
|
||||
};
|
||||
my @sorted = sort { lc($a->{title}) cmp lc($b->{title}) } @output;
|
||||
return @sorted;
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/perl
|
||||
|
||||
# Copyright 2015 Catalyst IT
|
||||
#
|
||||
# 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 3 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.
|
||||
|
||||
use Modern::Perl;
|
||||
|
||||
use Test::More;
|
||||
|
||||
use_ok('Koha::SearchEngine::Elasticsearch::Browse');
|
||||
|
||||
# testing browse itself not implemented as it'll require a running ES
|
||||
can_ok('Koha::SearchEngine::Elasticsearch::Browse',
|
||||
qw/ _build_query browse /);
|
||||
|
||||
subtest "_build_query tests" => sub {
|
||||
plan tests => 2;
|
||||
|
||||
my $browse = Koha::SearchEngine::Elasticsearch::Browse->new({index=>'dummy'});
|
||||
my $q = $browse->_build_query('foo', 'title');
|
||||
is_deeply($q, { size => 1,
|
||||
suggest => {
|
||||
suggestions => {
|
||||
text => 'foo',
|
||||
completion => {
|
||||
field => 'title__suggestion',
|
||||
size => 500,
|
||||
fuzzy => {
|
||||
fuzziness => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 'No fuzziness or size specified');
|
||||
|
||||
# Note that a fuzziness of 4 will get reduced to 2.
|
||||
$q = $browse->_build_query('foo', 'title', { fuzziness => 4, count => 400 });
|
||||
is_deeply($q, { size => 1,
|
||||
suggest => {
|
||||
suggestions => {
|
||||
text => 'foo',
|
||||
completion => {
|
||||
field => 'title__suggestion',
|
||||
size => 400,
|
||||
fuzzy => {
|
||||
fuzziness => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 'Fuzziness and size specified');
|
||||
};
|
||||
|
||||
done_testing();
|
||||
Loading…
Reference in New Issue