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

(-)a/Koha/Schema/Result/SearchField.pm (+6 lines)
Lines 119-122 __PACKAGE__->has_many( Link Here
119
119
120
__PACKAGE__->many_to_many("search_marc_maps", "search_marc_to_fields", "search_marc_map");
120
__PACKAGE__->many_to_many("search_marc_maps", "search_marc_to_fields", "search_marc_map");
121
121
122
123
# Created by DBIx::Class::Schema::Loader v0.07042 @ 2015-07-24 15:59:15
124
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:pd5chYwH+KRhI2O8/e7bSg
125
126
127
# You can replace this text with custom code or comments, and it will be preserved on regeneration
122
1;
128
1;
(-)a/Koha/Schema/Result/SearchMarcMap.pm (+6 lines)
Lines 127-130 __PACKAGE__->has_many( Link Here
127
127
128
__PACKAGE__->many_to_many("search_fields", "search_marc_to_fields", "search_field");
128
__PACKAGE__->many_to_many("search_fields", "search_marc_to_fields", "search_field");
129
129
130
131
# Created by DBIx::Class::Schema::Loader v0.07042 @ 2015-07-24 15:59:15
132
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:/6VCm/kMfCjtmD4Kx3HGMA
133
134
135
# You can replace this text with custom code or comments, and it will be preserved on regeneration
130
1;
136
1;
(-)a/Koha/SearchEngine/Elasticsearch/Browse.pm (+186 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
use Carp;
55
use Data::Dumper;
56
57
Koha::SearchEngine::Elasticsearch::Browse->mk_accessors(qw( store ));
58
59
=head2 browse
60
61
    my $results = $browser->browse($prefix, $field, \%options);
62
63
Does a prefix search for C<$prefix>, looking in C<$field>. Options are:
64
65
=over 4
66
67
=item count
68
69
The number of results to return. For Koha browse purposes, this should
70
probably be fairly high. Defaults to 500.
71
72
=item fuzziness
73
74
How much allowing for typos and misspellings is done. If 0, then it must match
75
exactly. If unspecified, it defaults to '1', which is probably the most useful.
76
Otherwise, it is a number specifying the Levenshtein edit distance relative to
77
the string length, according to the following lengths:
78
79
=over 4
80
81
=item 0..2
82
83
must match exactly
84
85
=item 3..5
86
87
C<fuzziness> edits allowed
88
89
=item >5
90
91
C<fuzziness>+1 edits allowed
92
93
=back
94
95
In all cases the maximum number of edits allowed is two (an elasticsearch
96
restriction.)
97
98
=back
99
100
=head3 Returns
101
102
This returns an arrayref of hashrefs. Each hashref contains a "text" element
103
that contains the field as returned. There may be other fields in that
104
hashref too, but they're less likely to be important.
105
106
The array will be ordered as returned from Elasticsearch, which seems to be
107
in order of some form of relevance.
108
109
=cut
110
111
sub browse {
112
    my ($self, $prefix, $field, $options) = @_;
113
114
    my $params = $self->get_elasticsearch_params();
115
    $self->store(
116
        Catmandu::Store::ElasticSearch->new(
117
            %$params,
118
        )
119
    ) unless $self->store;
120
121
    my $query = $self->_build_query($prefix, $field, $options);
122
    my $results = $self->store->bag->search(%$query);
123
    return $results->{suggest}{suggestions}[0]{options};
124
}
125
126
=head2 _build_query
127
128
    my $query = $self->_build_query($prefix, $field, $options);
129
130
Arguments are the same as for L<browse>. This will return a query structure
131
for elasticsearch to use.
132
133
=cut
134
135
sub _build_query {
136
    my ( $self, $prefix, $field, $options ) = @_;
137
138
    $options = {} unless $options;
139
    my $f = $options->{fuzziness} // 1;
140
    my $l = length($prefix);
141
    my $fuzzie;
142
    if ( $l <= 2 ) {
143
        $fuzzie = 0;
144
    }
145
    elsif ( $l <= 5 ) {
146
        $fuzzie = $f;
147
    }
148
    else {
149
        $fuzzie = $f + 1;
150
    }
151
    $fuzzie = 2 if $fuzzie > 2;
152
153
    my $size = $options->{count} // 500;
154
    my $query = {
155
        # this is an annoying thing, if we set size to 0 it gets rewritten
156
        # to 10. There's a bug somewhere in one of the libraries.
157
        size    => 1,
158
        suggest => {
159
            suggestions => {
160
                text       => $prefix,
161
                completion => {
162
                    field => $field . '__suggestion',
163
                    size  => $size,
164
                    fuzzy => {
165
                        fuzziness => $fuzzie,
166
                    }
167
                }
168
            }
169
        }
170
    };
171
    return $query;
172
}
173
174
1;
175
176
__END__
177
178
=head1 AUTHOR
179
180
=over 4
181
182
=item Robin Sheat C<< <robin@catalyst.net.nz> >>
183
184
=back
185
186
=cut
(-)a/Koha/SearchEngine/Elasticsearch/QueryBuilder.pm (-1 lines)
Lines 47-53 use Modern::Perl; Link Here
47
use URI::Escape;
47
use URI::Escape;
48
48
49
use C4::Context;
49
use C4::Context;
50
use Data::Dumper;    # TODO remove
51
50
52
=head2 build_query
51
=head2 build_query
53
52
(-)a/Koha/SearchEngine/Elasticsearch/Search.pm (-5 / +8 lines)
Lines 78-94 Returns Link Here
78
=cut
78
=cut
79
79
80
sub search {
80
sub search {
81
    my ($self, $query, $page, $count, %options) = @_;
81
    my ( $self, $query, $page, $count, %options ) = @_;
82
82
83
    my $params = $self->get_elasticsearch_params();
83
    my $params = $self->get_elasticsearch_params();
84
    my %paging;
84
    my %paging;
85
85
    # 20 is the default number of results per page
86
    # 20 is the default number of results per page
86
    $paging{limit} = $count || 20;
87
    $paging{limit} = $count || 20;
88
87
    # ES/Catmandu doesn't want pages, it wants a record to start from.
89
    # ES/Catmandu doesn't want pages, it wants a record to start from.
88
    if (exists $options{offset}) {
90
    if ( exists $options{offset} ) {
89
        $paging{start} = $options{offset};
91
        $paging{start} = $options{offset};
90
    } else {
92
    }
91
        $page = (!defined($page) || ($page <= 0)) ? 0 : $page - 1;
93
    else {
94
        $page = ( !defined($page) || ( $page <= 0 ) ) ? 0 : $page - 1;
92
        $paging{start} = $page * $paging{limit};
95
        $paging{start} = $page * $paging{limit};
93
    }
96
    }
94
    $self->store(
97
    $self->store(
Lines 165-171 sub search_compat { Link Here
165
    # consumers of this expect a name-spaced result, we provide the default
168
    # consumers of this expect a name-spaced result, we provide the default
166
    # configuration.
169
    # configuration.
167
    my %result;
170
    my %result;
168
    $result{biblioserver}{hits} = $results->total;
171
    $result{biblioserver}{hits}    = $results->total;
169
    $result{biblioserver}{RECORDS} = \@records;
172
    $result{biblioserver}{RECORDS} = \@records;
170
    return (undef, \%result, $self->_convert_facets($results->{aggregations}, $expanded_facet));
173
    return (undef, \%result, $self->_convert_facets($results->{aggregations}, $expanded_facet));
171
}
174
}
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-browse.tt (+89 lines)
Line 0 Link Here
1
[% USE Koha %]
2
[% INCLUDE 'doc-head-open.inc' %]
3
<title>[% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog &rsaquo; Browse our catalog</title>
4
[% INCLUDE 'doc-head-close.inc' %]
5
[% BLOCK cssinclude %][% END %]
6
[% INCLUDE 'bodytag.inc' bodyid='opac-browser' %]
7
[% INCLUDE 'masthead.inc' %]
8
9
10
<div class="main">
11
    <ul class="breadcrumb">
12
        <li><a href="/cgi-bin/koha/opac-main.pl">Home</a>
13
        <span class="divider">&rsaquo;</span></li>
14
15
        <li><a href="#">Browse search</a></li>
16
    </ul>
17
18
    <div class="container-fluid">
19
        <div class="row-fluid">
20
        [% IF ( OpacNav || OpacNavBottom ) %]
21
22
            <div class="span2">
23
                <div id="navigation">
24
                [% INCLUDE 'navigation.inc' %]
25
                </div>
26
            </div>
27
        [% END %]
28
29
        [% IF ( OpacNav ) %]
30
31
            <div class="span10">
32
            [% ELSE %]
33
34
            <div class="span12">
35
            [% END %]
36
37
                <div id="browse-search">
38
                    <h1>Browse search</h1>
39
40
                    <form>
41
                        <label for="browse-searchterm">Search for:</label>
42
                        <input type="search" id="browse-searchterm" name="searchterm" value="">
43
                        <label for="browse-searchfield" class="hide-text">Search type:</label>
44
                        <select id="browse-searchfield" name="searchfield">
45
                            <option value="author">Author</option>
46
                            <option value="subject">Subject</option>
47
                            <option value="title">Title</option>
48
                        </select>
49
50
                        <div id="browse-searchfuzziness">
51
                            <label for="exact" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="exact" value="0">Exact</label>
52
                            <label for="fuzzy" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="fuzzy" value="1" checked="checked"> Fuzzy</label>
53
                            <label for="reallyfuzzy" class="radio inline"><input type="radio" name="browse-searchfuzziness" id="reallyfuzzy" value="2"> Really Fuzzy</label>
54
                        </div>
55
                        <button class="btn btn-success" type="submit" accesskey="s">Search</button>
56
                    </form>
57
58
                    <p id="browse-suggestionserror" class="error hidden">
59
                    An error occurred, please try again.</p>
60
61
                    <div id="browse-resultswrapper" class="hidden">
62
                        <ul id="browse-searchresults" class="span3" start="-1" aria-live="polite">
63
                            <li class="loading hidden"><img src="[% interface %]/[% theme %]/images/loading.gif" alt=""> Loading</li>
64
65
                            <li class="no-results hidden">Sorry, there are no results, try a different search term.</li>
66
                        </ul>
67
68
                        <h3 id="browse-selection" class="span9"></h3>
69
70
                        <div id="browse-selectionsearch" class="span9 hidden">
71
                            <div class="loading hidden">
72
                                <img src="[% interface %]/[% theme %]/images/loading.gif" alt=""> Loading
73
                            </div>
74
75
                            <div class="no-results hidden">No results</div>
76
77
                            <ol aria-live="polite"></ol>
78
                        </div>
79
                    </div><!-- / .results-wrapper -->
80
                </div><!-- / .userbrowser -->
81
            </div><!-- / .span10 -->
82
        </div><!-- / .row-fluid -->
83
    </div><!-- / .container-fluid -->
84
</div><!-- / .main -->
85
[% INCLUDE 'opac-bottom.inc' %]
86
[% BLOCK jsinclude %]
87
<script type="text/javascript" src="[% interface %]/[% theme %]/js/browse.js">
88
</script>
89
[% 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/koha-tmpl/opac-tmpl/bootstrap/less/opac.less (+101 lines)
Lines 312-317 td { Link Here
312
    }
312
    }
313
}
313
}
314
314
315
/*opac browse search*/
316
#browse-search {
317
318
    form {
319
        label {
320
            display: inline-block;
321
            margin-right:5px;
322
        }
323
324
        [type=submit] {
325
            margin-top: 10px;
326
        }
327
    }
328
329
    #browse-resultswrapper {
330
       margin-top: 4em;
331
332
        @media (min-width: 768px) and (max-width: 984px) {
333
            margin-top: 2em;
334
        }
335
336
        @media (max-width: 767px) {
337
            margin-top: 1em;
338
        }
339
    }
340
    #browse-searchresults, #browse-selectionsearch {
341
        border: 1px solid #E3E3E3;
342
        .border-radius-all(4px);
343
        padding: 0;
344
        overflow-y: auto;
345
        max-height: 31em;
346
        margin-bottom: 2em;
347
    }
348
    #browse-searchresults {
349
        max-height: 31em;
350
        list-style: none;
351
        padding: 10px;
352
353
        a {
354
            display: block;
355
            margin-bottom: 5px;
356
357
            &.selected {
358
                background-color:#EEE;
359
            }
360
        }
361
362
        li:last-child a {
363
            margin-bottom: 0;
364
        }
365
366
        @media (max-width: 767px) {
367
            max-height: 13em;
368
        }
369
    }
370
    #browse-selection {
371
        margin-top: -40px;
372
        padding-top: 0;
373
374
        @media (max-width: 767px) {
375
            margin-top: 0;
376
        }
377
    }
378
    #browse-selectionsearch ol {
379
        list-style: none;
380
        margin: 0;
381
382
        li {
383
            padding: 1em;
384
385
            &:nth-child(odd) {
386
                background-color: #F4F4F4;
387
            }
388
        }
389
    }
390
   #browse-selectionsearch p.subjects {
391
        font-size: 0.9em;
392
        margin-bottom: 0;
393
    }
394
    #browse-selectionsearch h4 {
395
        margin: 0;
396
    }
397
    .error, .no-results {
398
        background-color: #EEE;
399
        border: 1px solid #E8E8E8;
400
        text-align: left;
401
        padding: 0.5em;
402
        .border-radius-all(3px);
403
    }
404
    .loading {
405
        text-align: center;
406
407
        img {
408
            margin:0.5em 0;
409
            position: relative;
410
            left: -5px;
411
        }
412
    }
413
}
414
/*end browse search*/
415
315
/* Override Bootstrap alert */
416
/* Override Bootstrap alert */
316
.alert {
417
.alert {
317
    background: #fffbe5; /* Old browsers */
418
    background: #fffbe5; /* Old browsers */
(-)a/opac/opac-browse.pl (+114 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;
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
# This is the temporary entrance point to the API. Once bug #13799 is settled,
41
# it should be ported to using that.
42
my $api = $query->param('api');
43
44
if ( !$api ) {
45
    my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
46
        {
47
            template_name   => "opac-browse.tt",
48
            query           => $query,
49
            type            => "opac",
50
            authnotrequired => ( C4::Context->preference("OpacPublic") ? 1 : 0 ),
51
        }
52
    );
53
   $template->param();
54
    output_html_with_http_headers $query, $cookie, $template->output;
55
56
57
}
58
elsif ( $api eq 'GetSuggestions' ) {
59
    my $fuzzie = $query->param('fuzziness');
60
    my $prefix = $query->param('prefix');
61
    my $field  = $query->param('field');
62
63
# Under a persistent environment, we should probably not reinit this every time.
64
    my $browser = Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } );
65
    my $res = $browser->browse( $prefix, $field, { fuzziness => $fuzzie } );
66
67
    my %seen;
68
    my @sorted =
69
        grep { !$seen{$_->{text}}++ }
70
        sort { lc($a->{text}) cmp lc($b->{text}) } @$res;
71
    print header(
72
        -type    => 'application/json',
73
        -charset => 'utf-8'
74
    );
75
    print to_json( \@sorted );
76
}
77
elsif ( $api eq 'GetResults' ) {
78
    my $term  = $query->param('term');
79
    my $field = $query->param('field');
80
81
    my $builder  = Koha::SearchEngine::Elasticsearch::QueryBuilder->new( { index => 'biblios' } );
82
    my $searcher = Koha::SearchEngine::Elasticsearch::Search->new(
83
        { index => $Koha::SearchEngine::Elasticsearch::BIBLIOS_INDEX } );
84
85
    my $query = { query => { term => { $field.".raw" => $term } } } ;
86
    my $results = $searcher->search( $query, undef, 500 );
87
    my @output = _filter_for_output( $results->{hits} );
88
    print header(
89
        -type    => 'application/json',
90
        -charset => 'utf-8'
91
    );
92
    print to_json( \@output );
93
}
94
95
# This is a temporary, MARC21-only thing that will grab titles, and authors
96
# This should probably be done with some templatey gizmo
97
# in the future.
98
sub _filter_for_output {
99
    my ($records) = @_;
100
    my @output;
101
    foreach my $rec (@$records) {
102
        my $biblionumber = $rec->{es_id};
103
        my $biblio = Koha::Biblios->find({ biblionumber=>$biblionumber });
104
warn $biblio->title;
105
        push @output,
106
          {
107
            id => $biblionumber,
108
            title    => $biblio->title,
109
            author  => $biblio->author,
110
          };
111
    };
112
    my @sorted = sort { lc($a->{title}) cmp lc($b->{title}) } @output;
113
    return @sorted;
114
}
(-)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