From 0e7f7ab051c8a23705ea802005f15877ca40d16f Mon Sep 17 00:00:00 2001 From: Robin Sheat Date: Tue, 21 Jul 2015 17:14:09 +1200 Subject: [PATCH] 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 Signed-off-by: Katrin Fischer Signed-off-by: Martin Renvoize --- Koha/SearchEngine/Elasticsearch/Browse.pm | 183 ++++++++++++++++++ .../elasticsearch/field_config.yaml | 1 + ...g_14567_add_es_catalog_browse_syspref.perl | 10 + installer/data/mysql/sysprefs.sql | 1 + .../en/modules/admin/preferences/opac.pref | 7 + .../opac-tmpl/bootstrap/css/src/opac.scss | 101 ++++++++++ .../bootstrap/en/includes/masthead.inc | 1 + .../bootstrap/en/modules/opac-browse.tt | 94 +++++++++ koha-tmpl/opac-tmpl/bootstrap/js/browse.js | 172 ++++++++++++++++ opac/opac-browse.pl | 112 +++++++++++ t/Koha_SearchEngine_Elasticsearch_Browse.t | 68 +++++++ 11 files changed, 750 insertions(+) create mode 100644 Koha/SearchEngine/Elasticsearch/Browse.pm create mode 100644 installer/data/mysql/atomicupdate/bug_14567_add_es_catalog_browse_syspref.perl create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt create mode 100644 koha-tmpl/opac-tmpl/bootstrap/js/browse.js create mode 100755 opac/opac-browse.pl create mode 100755 t/Koha_SearchEngine_Elasticsearch_Browse.t diff --git a/Koha/SearchEngine/Elasticsearch/Browse.pm b/Koha/SearchEngine/Elasticsearch/Browse.pm new file mode 100644 index 0000000000..2207fb7f99 --- /dev/null +++ b/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 edits allowed + +=item >5 + +C+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. 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 << >> + +=back + +=cut diff --git a/admin/searchengine/elasticsearch/field_config.yaml b/admin/searchengine/elasticsearch/field_config.yaml index e48cde7f57..8723a0713c 100644 --- a/admin/searchengine/elasticsearch/field_config.yaml +++ b/admin/searchengine/elasticsearch/field_config.yaml @@ -59,6 +59,7 @@ suggestible: default: type: completion analyzer: simple + max_input_length: 100 search_analyzer: simple # Sort sort: diff --git a/installer/data/mysql/atomicupdate/bug_14567_add_es_catalog_browse_syspref.perl b/installer/data/mysql/atomicupdate/bug_14567_add_es_catalog_browse_syspref.perl new file mode 100644 index 0000000000..1bb48f3a04 --- /dev/null +++ b/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"; +} diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index 57ab2480a1..944b36db08 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/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'), ('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'), diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref index fd12d8487e..7c77af5b30 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref @@ -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 http:// or https://):' diff --git a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss b/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss index 7d31d80cfb..525e39e3d2 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss +++ b/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"; diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc index a54b7ccc42..82a1ae6440 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc @@ -304,6 +304,7 @@ [% IF Koha.Preference( 'TagsEnabled' ) == 1 %]
  • Tag cloud
  • [% END %] [% IF Koha.Preference( 'OpacCloud' ) == 1 %]
  • Subject cloud
  • [% END %] [% IF Koha.Preference( 'OpacTopissue' ) == 1 %]
  • Most popular
  • [% END %] + [% IF Koha.Preference('SearchEngine') == 'Elasticsearch' && Koha.Preference( 'OpacBrowseSearch' ) == 1 %]
  • Browse search
  • [% END %] [% IF Koha.Preference( 'suggestion' ) == 1 %] [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
  • Purchase suggestions
  • diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt new file mode 100644 index 0000000000..7336ae869c --- /dev/null +++ b/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' %] +[% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog › Browse our catalog +[% INCLUDE 'doc-head-close.inc' %] +[% BLOCK cssinclude %][% END %] +[% INCLUDE 'bodytag.inc' bodyid='opac-browser' %] +[% INCLUDE 'masthead.inc' %] + +
    + + +
    +
    + [% IF Koha.Preference('SearchEngine') == 'Elasticsearch' && Koha.Preference('OpacBrowseSearch') %] + [% IF ( OpacNav || OpacNavBottom ) %] + +
    + +
    + [% END %] + + [% IF ( OpacNav ) %] + +
    + [% ELSE %] + +
    + [% END %] + + +
    + [% ELSE %] +

    This feature is not enabled

    + [% END %] +
    +
    +
    +[% INCLUDE 'opac-bottom.inc' %] +[% BLOCK jsinclude %] +[% Asset.js("/js/browse.js") | $raw %] + +[% END %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/js/browse.js b/koha-tmpl/opac-tmpl/bootstrap/js/browse.js new file mode 100644 index 0000000000..26272b17a2 --- /dev/null +++ b/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'); + } + }); + + }); + +}); diff --git a/opac/opac-browse.pl b/opac/opac-browse.pl new file mode 100755 index 0000000000..68a746bc1f --- /dev/null +++ b/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; +} diff --git a/t/Koha_SearchEngine_Elasticsearch_Browse.t b/t/Koha_SearchEngine_Elasticsearch_Browse.t new file mode 100755 index 0000000000..d7927349ea --- /dev/null +++ b/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(); -- 2.39.5