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 365-370 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
365
('opacbookbag','1','','If ON, enables display of Cart feature','YesNo'),
365
('opacbookbag','1','','If ON, enables display of Cart feature','YesNo'),
366
('OpacBrowser','0',NULL,'If ON, enables subject authorities browser on OPAC (needs to set misc/cronjob/sbuild_browser_and_cloud.pl)','YesNo'),
366
('OpacBrowser','0',NULL,'If ON, enables subject authorities browser on OPAC (needs to set misc/cronjob/sbuild_browser_and_cloud.pl)','YesNo'),
367
('OpacBrowseResults','1',NULL,'Disable/enable browsing and paging search results from the OPAC detail page.','YesNo'),
367
('OpacBrowseResults','1',NULL,'Disable/enable browsing and paging search results from the OPAC detail page.','YesNo'),
368
('OpacBrowseSearch', '0',NULL, "Elasticsearch only: add a page allowing users to 'browse' all items in the collection",'YesNo'),
368
('OpacCloud','0',NULL,'If ON, enables subject cloud on OPAC','YesNo'),
369
('OpacCloud','0',NULL,'If ON, enables subject cloud on OPAC','YesNo'),
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'),
370
('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'),
370
('OpacCoce','0', NULL, 'If on, enables cover retrieval from the configured Coce server in the OPAC', 'YesNo'),
371
('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 558-563 OPAC: Link Here
558
              type: textarea
558
              type: textarea
559
              syntax: text/html
559
              syntax: text/html
560
              class: code
560
              class: code
561
        -
562
            - pref: OpacBrowseSearch
563
              default: 0
564
              choices:
565
                  yes: Enable
566
                  no: Disable
567
            - "(Elasticsearch only) Enable the interface allowing to browse all holdings."
561
    OpenURL:
568
    OpenURL:
562
        -
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>):'
(-)a/koha-tmpl/opac-tmpl/bootstrap/css/src/opac.scss (+101 lines)
Lines 3101-3104 $star-selected: #EDB867; Link Here
3101
    }
3101
    }
3102
}
3102
}
3103
3103
3104
/*opac browse search*/
3105
#browse-search {
3106
3107
    form {
3108
        label {
3109
            display: inline-block;
3110
            margin-right:5px;
3111
        }
3112
3113
        [type=submit] {
3114
            margin-top: 10px;
3115
        }
3116
    }
3117
3118
    #browse-resultswrapper {
3119
       margin-top: 4em;
3120
3121
        @media (min-width: 768px) and (max-width: 984px) {
3122
            margin-top: 2em;
3123
        }
3124
3125
        @media (max-width: 767px) {
3126
            margin-top: 1em;
3127
        }
3128
    }
3129
    #browse-searchresults, #browse-selectionsearch {
3130
        border: 1px solid #E3E3E3;
3131
        border-radius: 4px;
3132
        padding: 0;
3133
        overflow-y: auto;
3134
        max-height: 31em;
3135
        margin-bottom: 2em;
3136
    }
3137
    #browse-searchresults {
3138
        max-height: 31em;
3139
        list-style: none;
3140
        padding: 10px;
3141
3142
        a {
3143
            display: block;
3144
            margin-bottom: 5px;
3145
3146
            &.selected {
3147
                background-color:#EEE;
3148
            }
3149
        }
3150
3151
        li:last-child a {
3152
            margin-bottom: 0;
3153
        }
3154
3155
        @media (max-width: 767px) {
3156
            max-height: 13em;
3157
        }
3158
    }
3159
    #browse-selection {
3160
        margin-top: -40px;
3161
        padding-top: 0;
3162
3163
        @media (max-width: 767px) {
3164
            margin-top: 0;
3165
        }
3166
    }
3167
    #browse-selectionsearch ol {
3168
        list-style: none;
3169
        margin: 0;
3170
3171
        li {
3172
            padding: 1em;
3173
3174
            &:nth-child(odd) {
3175
                background-color: #F4F4F4;
3176
            }
3177
        }
3178
    }
3179
   #browse-selectionsearch p.subjects {
3180
        font-size: 0.9em;
3181
        margin-bottom: 0;
3182
    }
3183
    #browse-selectionsearch h4 {
3184
        margin: 0;
3185
    }
3186
    .error, .no-results {
3187
        background-color: #EEE;
3188
        border: 1px solid #E8E8E8;
3189
        text-align: left;
3190
        padding: 0.5em;
3191
        border-radius: 3px;
3192
    }
3193
    .loading {
3194
        text-align: center;
3195
3196
        img {
3197
            margin:0.5em 0;
3198
            position: relative;
3199
            left: -5px;
3200
        }
3201
    }
3202
}
3203
/*end browse search*/
3204
3104
@import "responsive";
3205
@import "responsive";
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/masthead.inc (+1 lines)
Lines 304-309 Link Here
304
                                    [% IF Koha.Preference( 'TagsEnabled' ) == 1 %]<li><a href="/cgi-bin/koha/opac-tags.pl">Tag cloud</a></li>[% END %]
304
                                    [% IF Koha.Preference( 'TagsEnabled' ) == 1 %]<li><a href="/cgi-bin/koha/opac-tags.pl">Tag cloud</a></li>[% END %]
305
                                    [% IF Koha.Preference( 'OpacCloud' ) == 1 %]<li><a href="/cgi-bin/koha/opac-tags_subject.pl">Subject cloud</a></li>[% END %]
305
                                    [% IF Koha.Preference( 'OpacCloud' ) == 1 %]<li><a href="/cgi-bin/koha/opac-tags_subject.pl">Subject cloud</a></li>[% END %]
306
                                    [% IF Koha.Preference( 'OpacTopissue' ) == 1 %]<li><a href="/cgi-bin/koha/opac-topissues.pl">Most popular</a></li>[% END %]
306
                                    [% IF Koha.Preference( 'OpacTopissue' ) == 1 %]<li><a href="/cgi-bin/koha/opac-topissues.pl">Most popular</a></li>[% END %]
307
                                    [% IF Koha.Preference('SearchEngine') == 'Elasticsearch' && Koha.Preference( 'OpacBrowseSearch' ) == 1 %]<li><a href="/cgi-bin/koha/opac-browse.pl">Browse search</a></li>[% END %]
307
                                    [% IF Koha.Preference( 'suggestion' ) == 1 %]
308
                                    [% IF Koha.Preference( 'suggestion' ) == 1 %]
308
                                        [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
309
                                        [% IF Koha.Preference( 'AnonSuggestions' ) == 1 %]
309
                                            <li><a href="/cgi-bin/koha/opac-suggestions.pl">Purchase suggestions</a></li>
310
                                            <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