View | Details | Raw Unified | Return to bug 14567
Collapse All | Expand All

(-)a/Koha/SearchEngine/Elasticsearch/Browse.pm (+183 lines)
Line 0 Link Here
1
package Koha::SearchEngine::Elasticsearch::Browse;
2
3
# Copyright 2015 Catalyst IT
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
=head1 NAME
21
22
Koha::SearchEngine::ElasticSearch::Browse - browse functions for Elasticsearch
23
24
=head1 SYNOPSIS
25
26
    my $browser =
27
      Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } );
28
    my $results = $browser->browse(
29
        'prefi', 'title',
30
        {
31
            results   => '500',
32
            fuzziness => 2,
33
        }
34
    );
35
    foreach my $r (@$results) {
36
        push @hits, $r->{text};
37
    }
38
39
=head1 DESCRIPTION
40
41
This provides an easy interface to the "browse" functionality. Essentially,
42
it does a fast prefix search on defined fields. The fields have to be marked
43
as "suggestible" in the database when indexing takes place.
44
45
=head1 METHODS
46
47
=cut
48
49
use base qw(Koha::SearchEngine::Elasticsearch);
50
use Modern::Perl;
51
52
use Catmandu::Store::ElasticSearch;
53
54
Koha::SearchEngine::Elasticsearch::Browse->mk_accessors(qw( store ));
55
56
=head2 browse
57
58
    my $results = $browser->browse($prefix, $field, \%options);
59
60
Does a prefix search for C<$prefix>, looking in C<$field>. Options are:
61
62
=over 4
63
64
=item count
65
66
The number of results to return. For Koha browse purposes, this should
67
probably be fairly high. Defaults to 500.
68
69
=item fuzziness
70
71
How much allowing for typos and misspellings is done. If 0, then it must match
72
exactly. If unspecified, it defaults to '1', which is probably the most useful.
73
Otherwise, it is a number specifying the Levenshtein edit distance relative to
74
the string length, according to the following lengths:
75
76
=over 4
77
78
=item 0..2
79
80
must match exactly
81
82
=item 3..5
83
84
C<fuzziness> edits allowed
85
86
=item >5
87
88
C<fuzziness>+1 edits allowed
89
90
=back
91
92
In all cases the maximum number of edits allowed is two (an elasticsearch
93
restriction.)
94
95
=back
96
97
=head3 Returns
98
99
This returns an arrayref of hashrefs. Each hashref contains a "text" element
100
that contains the field as returned. There may be other fields in that
101
hashref too, but they're less likely to be important.
102
103
The array will be ordered as returned from Elasticsearch, which seems to be
104
in order of some form of relevance.
105
106
=cut
107
108
sub browse {
109
    my ($self, $prefix, $field, $options) = @_;
110
111
    my $params = $self->get_elasticsearch_params();
112
    $self->store(
113
        Catmandu::Store::ElasticSearch->new(
114
            %$params,
115
        )
116
    ) unless $self->store;
117
118
    my $query = $self->_build_query($prefix, $field, $options);
119
    my $results = $self->store->bag->search(%$query);
120
    return $results->{suggest}{suggestions}[0]{options};
121
}
122
123
=head2 _build_query
124
125
    my $query = $self->_build_query($prefix, $field, $options);
126
127
Arguments are the same as for L<browse>. This will return a query structure
128
for elasticsearch to use.
129
130
=cut
131
132
sub _build_query {
133
    my ( $self, $prefix, $field, $options ) = @_;
134
135
    $options = {} unless $options;
136
    my $f = $options->{fuzziness} // 1;
137
    my $l = length($prefix);
138
    my $fuzzie;
139
    if ( $l <= 2 ) {
140
        $fuzzie = 0;
141
    }
142
    elsif ( $l <= 5 ) {
143
        $fuzzie = $f;
144
    }
145
    else {
146
        $fuzzie = $f + 1;
147
    }
148
    $fuzzie = 2 if $fuzzie > 2;
149
150
    my $size = $options->{count} // 500;
151
    my $query = {
152
        # this is an annoying thing, if we set size to 0 it gets rewritten
153
        # to 10. There's a bug somewhere in one of the libraries.
154
        size    => 1,
155
        suggest => {
156
            suggestions => {
157
                text       => $prefix,
158
                completion => {
159
                    field => $field . '__suggestion',
160
                    size  => $size,
161
                    fuzzy => {
162
                        fuzziness => $fuzzie,
163
                    }
164
                }
165
            }
166
        }
167
    };
168
    return $query;
169
}
170
171
1;
172
173
__END__
174
175
=head1 AUTHOR
176
177
=over 4
178
179
=item Robin Sheat << <robin@catalyst.net.nz> >>
180
181
=back
182
183
=cut
(-)a/admin/searchengine/elasticsearch/field_config.yaml (+1 lines)
Lines 59-64 suggestible: Link Here
59
  default:
59
  default:
60
    type: completion
60
    type: completion
61
    analyzer: simple
61
    analyzer: simple
62
    max_input_length: 100
62
    search_analyzer: simple
63
    search_analyzer: simple
63
# Sort
64
# Sort
64
sort:
65
sort:
(-)a/installer/data/mysql/atomicupdate/bug_14567_add_es_catalog_browse_syspref.perl (+10 lines)
Line 0 Link Here
1
$DBversion = 'XXX';
2
if( CheckVersion( $DBversion ) ) {
3
    $dbh->do(q{
4
        INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type)
5
        VALUES
6
        ('OpacBrowseSearch', '0',NULL, "Elasticsearch only: add a page allowing users to 'browse' all items in the collection",'YesNo')
7
    });
8
    SetVersion( $DBversion );
9
    print "Upgrade to $DBversion done (Bug 14567: Add OpacBrowseSearch syspref)\n";
10
}
(-)a/installer/data/mysql/sysprefs.sql (+1 lines)
Lines 364-369 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
364
('opacbookbag','1','','If ON, enables display of Cart feature','YesNo'),
364
('opacbookbag','1','','If ON, enables display of Cart feature','YesNo'),
365
('OpacBrowser','0',NULL,'If ON, enables subject authorities browser on OPAC (needs to set misc/cronjob/sbuild_browser_and_cloud.pl)','YesNo'),
365
('OpacBrowser','0',NULL,'If ON, enables subject authorities browser on OPAC (needs to set misc/cronjob/sbuild_browser_and_cloud.pl)','YesNo'),
366
('OpacBrowseResults','1',NULL,'Disable/enable browsing and paging search results from the OPAC detail page.','YesNo'),
366
('OpacBrowseResults','1',NULL,'Disable/enable browsing and paging search results from the OPAC detail page.','YesNo'),
367
('OpacBrowseSearch', '0',NULL, "Elasticsearch only: add a page allowing users to 'browse' all items in the collection",'YesNo'),
367
('OpacCloud','0',NULL,'If ON, enables subject cloud on OPAC','YesNo'),
368
('OpacCloud','0',NULL,'If ON, enables subject cloud on OPAC','YesNo'),
368
('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'),
369
('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'),
369
('OpacCoce','0', NULL, 'If on, enables cover retrieval from the configured Coce server in the OPAC', 'YesNo'),
370
('OpacCoce','0', NULL, 'If on, enables cover retrieval from the configured Coce server in the OPAC', 'YesNo'),
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref (+7 lines)
Lines 560-565 OPAC: Link Here
560
              class: code
560
              class: code
561
    OpenURL:
561
    OpenURL:
562
        -
562
        -
563
            - pref: OpacBrowseSearch
564
              default: 0
565
              choices:
566
                  yes: Enable
567
                  no: Disable
568
            - "(Elasticsearch only) Enable the interface allowing to browse all holdings."
569
        -
563
            - 'Complete URL of OpenURL resolver (starting with <code>http://</code> or <code>https://</code>):'
570
            - 'Complete URL of OpenURL resolver (starting with <code>http://</code> or <code>https://</code>):'
564
            - pref: OpenURLResolverURL
571
            - pref: OpenURLResolverURL
565
              class: url
572
              class: url
(-)a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss (+101 lines)
Lines 3093-3096 $star-selected: #EDB867; Link Here
3093
    }
3093
    }
3094
}
3094
}
3095
3095
3096
/*opac browse search*/
3097
#browse-search {
3098
3099
    form {
3100
        label {
3101
            display: inline-block;
3102
            margin-right:5px;
3103
        }
3104
3105
        [type=submit] {
3106
            margin-top: 10px;
3107
        }
3108
    }
3109
3110
    #browse-resultswrapper {
3111
       margin-top: 4em;
3112
3113
        @media (min-width: 768px) and (max-width: 984px) {
3114
            margin-top: 2em;
3115
        }
3116
3117
        @media (max-width: 767px) {
3118
            margin-top: 1em;
3119
        }
3120
    }
3121
    #browse-searchresults, #browse-selectionsearch {
3122
        border: 1px solid #E3E3E3;
3123
        border-radius: 4px;
3124
        padding: 0;
3125
        overflow-y: auto;
3126
        max-height: 31em;
3127
        margin-bottom: 2em;
3128
    }
3129
    #browse-searchresults {
3130
        max-height: 31em;
3131
        list-style: none;
3132
        padding: 10px;
3133
3134
        a {
3135
            display: block;
3136
            margin-bottom: 5px;
3137
3138
            &.selected {
3139
                background-color:#EEE;
3140
            }
3141
        }
3142
3143
        li:last-child a {
3144
            margin-bottom: 0;
3145
        }
3146
3147
        @media (max-width: 767px) {
3148
            max-height: 13em;
3149
        }
3150
    }
3151
    #browse-selection {
3152
        margin-top: -40px;
3153
        padding-top: 0;
3154
3155
        @media (max-width: 767px) {
3156
            margin-top: 0;
3157
        }
3158
    }
3159
    #browse-selectionsearch ol {
3160
        list-style: none;
3161
        margin: 0;
3162
3163
        li {
3164
            padding: 1em;
3165
3166
            &:nth-child(odd) {
3167
                background-color: #F4F4F4;
3168
            }
3169
        }
3170
    }
3171
   #browse-selectionsearch p.subjects {
3172
        font-size: 0.9em;
3173
        margin-bottom: 0;
3174
    }
3175
    #browse-selectionsearch h4 {
3176
        margin: 0;
3177
    }
3178
    .error, .no-results {
3179
        background-color: #EEE;
3180
        border: 1px solid #E8E8E8;
3181
        text-align: left;
3182
        padding: 0.5em;
3183
        border-radius: 3px;
3184
    }
3185
    .loading {
3186
        text-align: center;
3187
3188
        img {
3189
            margin:0.5em 0;
3190
            position: relative;
3191
            left: -5px;
3192
        }
3193
    }
3194
}
3195
/*end browse search*/
3196
3096
@import "responsive";
3197
@import "responsive";
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc (+1 lines)
Lines 309-314 Link Here
309
                                    [% IF Koha.Preference( 'TagsEnabled' ) == 1 %]<li><a href="/cgi-bin/koha/opac-tags.pl">Tag cloud</a></li>[% END %]
309
                                    [% IF Koha.Preference( 'TagsEnabled' ) == 1 %]<li><a href="/cgi-bin/koha/opac-tags.pl">Tag cloud</a></li>[% END %]
310
                                    [% IF Koha.Preference( 'OpacCloud' ) == 1 %]<li><a href="/cgi-bin/koha/opac-tags_subject.pl">Subject cloud</a></li>[% END %]
310
                                    [% IF Koha.Preference( 'OpacCloud' ) == 1 %]<li><a href="/cgi-bin/koha/opac-tags_subject.pl">Subject cloud</a></li>[% END %]
311
                                    [% IF Koha.Preference( 'OpacTopissue' ) == 1 %]<li><a href="/cgi-bin/koha/opac-topissues.pl">Most popular</a></li>[% END %]
311
                                    [% IF Koha.Preference( 'OpacTopissue' ) == 1 %]<li><a href="/cgi-bin/koha/opac-topissues.pl">Most popular</a></li>[% END %]
312
                                    [% IF Koha.Preference('SearchEngine') == 'Elasticsearch' && Koha.Preference( 'OpacBrowseSearch' ) == 1 %]<li><a href="/cgi-bin/koha/opac-browse.pl">Browse search</a></li>[% END %]
312
                                    [% IF Koha.Preference( 'suggestion' ) == 1 %]
313
                                    [% IF Koha.Preference( 'suggestion' ) == 1 %]
313
                                        [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
314
                                        [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
314
                                            <li><a href="/cgi-bin/koha/opac-suggestions.pl">Purchase suggestions</a></li>
315
                                            <li><a href="/cgi-bin/koha/opac-suggestions.pl">Purchase suggestions</a></li>
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt (+94 lines)
Line 0 Link Here
1
[% USE Koha %]
2
[% USE Asset %]
3
[% USE raw %]
4
[% INCLUDE 'doc-head-open.inc' %]
5
<title>[% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog &rsaquo; Browse our catalog</title>
6
[% INCLUDE 'doc-head-close.inc' %]
7
[% BLOCK cssinclude %][% END %]
8
[% INCLUDE 'bodytag.inc' bodyid='opac-browser' %]
9
[% INCLUDE 'masthead.inc' %]
10
11
<div class="main">
12
    <ul class="breadcrumb">
13
        <li><a href="/cgi-bin/koha/opac-main.pl">Home</a>
14
        <span class="divider">&rsaquo;</span></li>
15
16
        <li><a href="#">Browse search</a></li>
17
    </ul>
18
19
    <div class="container-fluid">
20
        <div class="row-fluid">
21
    [% IF Koha.Preference('SearchEngine') == 'Elasticsearch' && Koha.Preference('OpacBrowseSearch') %]
22
        [% IF ( OpacNav || OpacNavBottom ) %]
23
24
            <div class="span2">
25
                <div id="navigation">
26
                [% INCLUDE 'navigation.inc' %]
27
                </div>
28
            </div>
29
        [% END %]
30
31
        [% IF ( OpacNav ) %]
32
33
            <div class="span10">
34
            [% ELSE %]
35
36
            <div class="span12">
37
            [% END %]
38
39
                <div id="browse-search">
40
                    <h1>Browse search</h1>
41
42
                    <form>
43
                        <label for="browse-searchterm">Search for:</label>
44
                        <input type="search" id="browse-searchterm" name="searchterm" value="">
45
                        <label for="browse-searchfield" class="hide-text">Search type:</label>
46
                        <select id="browse-searchfield" name="searchfield">
47
                            <option value="author">Author</option>
48
                            <option value="subject">Subject</option>
49
                            <option value="title">Title</option>
50
                        </select>
51
52
                        <div id="browse-searchfuzziness">
53
                            <label for="exact" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="exact" value="0">Exact</label>
54
                            <label for="fuzzy" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="fuzzy" value="1" checked="checked"> Fuzzy</label>
55
                            <label for="reallyfuzzy" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="reallyfuzzy" value="2"> Really Fuzzy</label>
56
                        </div>
57
                        <button class="btn btn-success" type="submit" accesskey="s">Search</button>
58
                    </form>
59
60
                    <p id="browse-suggestionserror" class="error hidden">
61
                    An error occurred, please try again.</p>
62
63
                    <div id="browse-resultswrapper" class="hidden">
64
                        <ul id="browse-searchresults" class="span3" start="-1" aria-live="polite">
65
                            <li class="loading hidden"><img src="[% interface | html %]/[% theme |html %]/images/loading.gif" alt=""> Loading</li>
66
67
                            <li class="no-results hidden">Sorry, there are no results, try a different search term.</li>
68
                        </ul>
69
70
                        <h3 id="browse-selection" class="span9"></h3>
71
72
                        <div id="browse-selectionsearch" class="span9 hidden">
73
                            <div class="loading hidden">
74
                                <img src="[% interface | html %]/[% theme | html %]/images/loading.gif" alt=""> Loading
75
                            </div>
76
77
                            <div class="no-results hidden">No results</div>
78
79
                            <ol aria-live="polite"></ol>
80
                        </div>
81
                    </div><!-- / .results-wrapper -->
82
                </div><!-- / .userbrowser -->
83
            </div><!-- / .span10 -->
84
    [% ELSE %]
85
        <h3>This feature is not enabled</h3>
86
    [% END %]
87
        </div><!-- / .row-fluid -->
88
    </div><!-- / .container-fluid -->
89
</div><!-- / .main -->
90
[% INCLUDE 'opac-bottom.inc' %]
91
[% BLOCK jsinclude %]
92
[% Asset.js("/js/browse.js") | $raw %]
93
</script>
94
[% END %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/js/browse.js (+172 lines)
Line 0 Link Here
1
jQuery.fn.overflowScrollReset = function() {
2
    $(this).scrollTop($(this).scrollTop() - $(this).offset().top);
3
    return this;
4
};
5
6
$(document).ready(function(){
7
    var xhrGetSuggestions, xhrGetResults;
8
9
    $('#browse-search form').submit(function(event) {
10
        // if there is an in progress request, abort it so we
11
        // don't end up with  a race condition
12
        if(xhrGetSuggestions && xhrGetSuggestions.readyState != 4){
13
            xhrGetSuggestions.abort();
14
        }
15
16
        var userInput = $('#browse-searchterm').val().trim();
17
        var userField = $('#browse-searchfield').val();
18
        var userFuzziness = $('input[name=browse-searchfuzziness]:checked', '#browse-searchfuzziness').val();
19
        var leftPaneResults = $('#browse-searchresults li').not('.loading, .no-results');
20
        var rightPaneResults = $('#browse-selectionsearch ol li');
21
22
        event.preventDefault();
23
24
        if(!userInput) {
25
            return;
26
        }
27
28
        // remove any error states and show the results area (except right pane)
29
        $('#browse-suggestionserror').addClass('hidden');
30
        $('#browse-searchresults .no-results').addClass('hidden');
31
        $('#browse-resultswrapper').removeClass('hidden');
32
        $('#browse-selection').addClass('hidden').text("");
33
        $('#browse-selectionsearch').addClass('hidden');
34
35
        // clear any results from left and right panes
36
        leftPaneResults.remove();
37
        rightPaneResults.remove();
38
39
        // show the spinner in the left pane
40
        $('#browse-searchresults .loading').removeClass('hidden');
41
42
        xhrGetSuggestions = $.get(window.location.pathname, {api: "GetSuggestions", field: userField, prefix: userInput, fuzziness: userFuzziness})
43
            .always(function() {
44
                // hide spinner
45
                $('#browse-searchresults .loading').addClass('hidden');
46
            })
47
            .done(function(data) {
48
                var fragment = document.createDocumentFragment();
49
50
                if (data.length === 0) {
51
                    $('#browse-searchresults .no-results').removeClass('hidden');
52
53
                    return;
54
                }
55
56
                // scroll to top of container again
57
                $("#browse-searchresults").overflowScrollReset();
58
59
                // store the type of search that was performed as an attrib
60
                $('#browse-searchresults').data('field', userField);
61
62
                $.each(data, function(index, object) {
63
                    // use a document fragment so we don't need to nest the elems
64
                    // or append during each iteration (which would be slow)
65
                    var elem = document.createElement("li");
66
                    var link = document.createElement("a");
67
                    link.textContent = object.text;
68
                    link.setAttribute("href", "#");
69
                    elem.appendChild(link);
70
                    fragment.appendChild(elem);
71
                });
72
73
                $('#browse-searchresults').append(fragment.cloneNode(true));
74
            })
75
            .fail(function(jqXHR) {
76
                //if 500 or 404 (abort is okay though)
77
                if (jqXHR.statusText !== "abort") {
78
                    $('#browse-resultswrapper').addClass('hidden');
79
                    $('#browse-suggestionserror').removeClass('hidden');
80
                }
81
            });
82
    });
83
84
    $('#browse-searchresults').on("click", 'a', function(event) {
85
        // if there is an in progress request, abort it so we
86
        // don't end up with  a race condition
87
        if(xhrGetResults && xhrGetResults.readyState != 4){
88
            xhrGetResults.abort();
89
        }
90
91
        var term = $(this).text();
92
        var field = $('#browse-searchresults').data('field');
93
        var rightPaneResults = $('#browse-selectionsearch ol li');
94
95
        event.preventDefault();
96
97
        // clear any current selected classes and add a new one
98
        $(this).parent().siblings().children().removeClass('selected');
99
        $(this).addClass('selected');
100
101
        // copy in the clicked text
102
        $('#browse-selection').removeClass('hidden').text(term);
103
104
        // show the right hand pane if it is not shown already
105
        $('#browse-selectionsearch').removeClass('hidden');
106
107
        // hide the no results element
108
        $('#browse-selectionsearch .no-results').addClass('hidden');
109
110
        // clear results
111
        rightPaneResults.remove();
112
113
        // turn the spinner on
114
        $('#browse-selectionsearch .loading').removeClass('hidden');
115
116
        // do the query for the term
117
        xhrGetResults = $.get(window.location.pathname, {api: "GetResults", field: field, term: term})
118
            .always(function() {
119
                // hide spinner
120
                $('#browse-selectionsearch .loading').addClass('hidden');
121
            })
122
            .done(function(data) {
123
                var fragment = document.createDocumentFragment();
124
125
                if (data.length === 0) {
126
                    $('#browse-selectionsearch .no-results').removeClass('hidden');
127
128
                    return;
129
                }
130
131
                // scroll to top of container again
132
                $("#browse-selectionsearch").overflowScrollReset();
133
134
                $.each(data, function(index, object) {
135
                    // use a document fragment so we don't need to nest the elems
136
                    // or append during each iteration (which would be slow)
137
                    var elem = document.createElement("li");
138
                    var title = document.createElement("h4");
139
                    var link = document.createElement("a");
140
                    var author = document.createElement("p");
141
                    var destination = window.location.pathname;
142
143
                    destination = destination.replace("browse", "detail");
144
                    destination = destination + "?biblionumber=" + object.id;
145
146
                    author.className = "author";
147
148
                    link.setAttribute("href", destination);
149
                    link.setAttribute("target", "_blank");
150
                    link.textContent = object.title;
151
                    title.appendChild(link);
152
153
                    author.textContent = object.author;
154
155
                    elem.appendChild(title);
156
                    elem.appendChild(author);
157
                    fragment.appendChild(elem);
158
                });
159
160
                $('#browse-selectionsearch ol').append(fragment.cloneNode(true));
161
            })
162
            .fail(function(jqXHR) {
163
                //if 500 or 404 (abort is okay though)
164
                if (jqXHR.statusText !== "abort") {
165
                    $('#browse-resultswrapper').addClass('hidden');
166
                    $('#browse-suggestionserror').removeClass('hidden');
167
                }
168
            });
169
170
    });
171
172
});
(-)a/opac/opac-browse.pl (+112 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# This is a CGI script that handles the browse feature.
4
5
# Copyright 2015 Catalyst IT
6
#
7
# This file is part of Koha.
8
#
9
# Koha is free software; you can redistribute it and/or modify it under the
10
# terms of the GNU General Public License as published by the Free Software
11
# Foundation; either version 3 of the License, or (at your option) any later
12
# version.
13
#
14
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
15
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License along
19
# with Koha; if not, write to the Free Software Foundation, Inc.,
20
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22
use Modern::Perl;
23
use CGI qw ( -utf8 );
24
25
use C4::Auth;
26
use C4::Context;
27
use C4::Output;
28
29
use Koha::SearchEngine::Elasticsearch;
30
use Koha::SearchEngine::Elasticsearch::Browse;
31
use Koha::SearchEngine::Elasticsearch::QueryBuilder;
32
use Koha::SearchEngine::Elasticsearch::Search;
33
34
use JSON;
35
use Unicode::Collate;
36
37
my $query = new CGI;
38
binmode STDOUT, ':encoding(UTF-8)';
39
40
# If calling via JS, 'api' is used to route to correct step in process
41
my $api = $query->param('api');
42
43
if ( !$api ) {
44
    my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
45
        {
46
            template_name   => "opac-browse.tt",
47
            query           => $query,
48
            type            => "opac",
49
            authnotrequired => ( C4::Context->preference("OpacPublic") ? 1 : 0 ),
50
        }
51
    );
52
   $template->param();
53
    output_html_with_http_headers $query, $cookie, $template->output;
54
55
56
}
57
elsif ( $api eq 'GetSuggestions' ) {
58
    my $fuzzie = $query->param('fuzziness');
59
    my $prefix = $query->param('prefix');
60
    my $field  = $query->param('field');
61
62
# Under a persistent environment, we should probably not reinit this every time.
63
    my $browser = Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } );
64
    my $res = $browser->browse( $prefix, $field, { fuzziness => $fuzzie } );
65
66
    my %seen;
67
    my @sorted =
68
        grep { !$seen{$_->{text}}++ }
69
        sort { lc($a->{text}) cmp lc($b->{text}) } @$res;
70
    print CGI::header(
71
        -type    => 'application/json',
72
        -charset => 'utf-8'
73
    );
74
    print to_json( \@sorted );
75
}
76
elsif ( $api eq 'GetResults' ) {
77
    my $term  = $query->param('term');
78
    my $field = $query->param('field');
79
80
    my $builder  = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'biblios' } );
81
    my $searcher = Koha::SearchEngine::Elasticsearch::Search->new(
82
        { index => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX } );
83
84
    my $query = { query => { term => { $field.".raw" => $term } } } ;
85
    my $results = $searcher->search( $query, undef, 500 );
86
    my @output = _filter_for_output( $results->{hits}->{hits} );
87
    print CGI::header(
88
        -type    => 'application/json',
89
        -charset => 'utf-8'
90
    );
91
    print to_json( \@output );
92
}
93
94
# This should probably be done with some templatey gizmo
95
# in the future.
96
sub _filter_for_output {
97
    my ($records) = @_;
98
    my @output;
99
    foreach my $rec (@$records) {
100
        my $biblionumber = $rec->{_id};
101
        my $biblio = Koha::Biblios->find( $biblionumber );
102
        next unless $biblio;
103
        push @output,
104
          {
105
            id => $biblionumber,
106
            title    => $biblio->title,
107
            author  => $biblio->author,
108
          };
109
    };
110
    my @sorted = sort { lc($a->{title}) cmp lc($b->{title}) } @output;
111
    return @sorted;
112
}
(-)a/t/Koha_SearchEngine_Elasticsearch_Browse.t (-1 / +68 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright 2015 Catalyst IT
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
22
use Test::More;
23
24
use_ok('Koha::SearchEngine::Elasticsearch::Browse');
25
26
# testing browse itself not implemented as it'll require a running ES
27
can_ok('Koha::SearchEngine::Elasticsearch::Browse',
28
    qw/ _build_query browse /);
29
30
subtest "_build_query tests" => sub {
31
    plan tests => 2;
32
33
    my $browse = Koha::SearchEngine::Elasticsearch::Browse->new({index=>'dummy'});
34
    my $q = $browse->_build_query('foo', 'title');
35
    is_deeply($q, { size => 1,
36
        suggest => {
37
            suggestions => {
38
                text       => 'foo',
39
                completion => {
40
                    field => 'title__suggestion',
41
                    size  => 500,
42
                    fuzzy => {
43
                        fuzziness => 1,
44
                    }
45
                }
46
            }
47
        }
48
    }, 'No fuzziness or size specified');
49
50
    # Note that a fuzziness of 4 will get reduced to 2.
51
    $q = $browse->_build_query('foo', 'title', { fuzziness => 4, count => 400 });
52
    is_deeply($q, { size => 1,
53
        suggest => {
54
            suggestions => {
55
                text       => 'foo',
56
                completion => {
57
                    field => 'title__suggestion',
58
                    size  => 400,
59
                    fuzzy => {
60
                        fuzziness => 2,
61
                    }
62
                }
63
            }
64
        }
65
    }, 'Fuzziness and size specified');
66
};
67
68
done_testing();

Return to bug 14567