Browse Source

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>
20.05.x
Robin Sheat 9 years ago
committed by Martin Renvoize
parent
commit
0e7f7ab051
Signed by: martin.renvoize GPG Key ID: 422B469130441A0F
  1. 183
      Koha/SearchEngine/Elasticsearch/Browse.pm
  2. 1
      admin/searchengine/elasticsearch/field_config.yaml
  3. 10
      installer/data/mysql/atomicupdate/bug_14567_add_es_catalog_browse_syspref.perl
  4. 1
      installer/data/mysql/sysprefs.sql
  5. 7
      koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref
  6. 101
      koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss
  7. 1
      koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc
  8. 94
      koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt
  9. 172
      koha-tmpl/opac-tmpl/bootstrap/js/browse.js
  10. 112
      opac/opac-browse.pl
  11. 68
      t/Koha_SearchEngine_Elasticsearch_Browse.t

183
Koha/SearchEngine/Elasticsearch/Browse.pm

@ -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

1
admin/searchengine/elasticsearch/field_config.yaml

@ -59,6 +59,7 @@ suggestible:
default: default:
type: completion type: completion
analyzer: simple analyzer: simple
max_input_length: 100
search_analyzer: simple search_analyzer: simple
# Sort # Sort
sort: sort:

10
installer/data/mysql/atomicupdate/bug_14567_add_es_catalog_browse_syspref.perl

@ -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";
}

1
installer/data/mysql/sysprefs.sql

@ -365,6 +365,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
('opacbookbag','1','','If ON, enables display of Cart feature','YesNo'), ('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'), ('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'), ('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'), ('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'), ('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'), ('OpacCoce','0', NULL, 'If on, enables cover retrieval from the configured Coce server in the OPAC', 'YesNo'),

7
koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref

@ -558,6 +558,13 @@ OPAC:
type: textarea type: textarea
syntax: text/html syntax: text/html
class: code class: code
-
- pref: OpacBrowseSearch
default: 0
choices:
yes: Enable
no: Disable
- "(Elasticsearch only) Enable the interface allowing to browse all holdings."
OpenURL: OpenURL:
- -
- 'Complete URL of OpenURL resolver (starting with <code>http://</code> or <code>https://</code>):' - 'Complete URL of OpenURL resolver (starting with <code>http://</code> or <code>https://</code>):'

101
koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss

@ -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"; @import "responsive";

1
koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc

@ -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( '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( '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( '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( 'suggestion' ) == 1 %]
[% IF Koha.Preference( 'AnonSuggestions' ) == 1 %] [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
<li><a href="/cgi-bin/koha/opac-suggestions.pl">Purchase suggestions</a></li> <li><a href="/cgi-bin/koha/opac-suggestions.pl">Purchase suggestions</a></li>

94
koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt

@ -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 &rsaquo; 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">&rsaquo;</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 %]

172
koha-tmpl/opac-tmpl/bootstrap/js/browse.js

@ -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');
}
});
});
});

112
opac/opac-browse.pl

@ -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;
}

68
t/Koha_SearchEngine_Elasticsearch_Browse.t

@ -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…
Cancel
Save