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

(-)a/Koha/SearchEngine/Elasticsearch/Browse.pm (-1 / +126 lines)
Lines 40-46 Koha::SearchEngine::ElasticSearch::Browse - browse functions for Elasticsearch Link Here
40
40
41
This provides an easy interface to the "browse" functionality. Essentially,
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
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.
43
as "suggestible" in the database when indexing takes place(no action required for autocomplete).
44
44
45
=head1 METHODS
45
=head1 METHODS
46
46
Lines 162-167 sub _build_query { Link Here
162
    return $query;
162
    return $query;
163
}
163
}
164
164
165
=head2 autocomplete_one_idx
166
167
    my $query = $self->autocomplete_one_idx($cgi_q, $prefix, $analyzer, $token_counter);
168
169
Does a prefix search for C<$prefix> (only one prefix), looking for C<$cgi_q> , using analyzer C<$analyzer> ,
170
C<$token_counter> is used for identify which word to use in autocomplete
171
172
=cut
173
174
=head3 Returns
175
176
This returns an arrayref of hashrefs with highlights. Each hashref contains a "text" element that contains the field as returned.
177
178
=cut
179
180
sub autocomplete_one_idx {
181
    my ($self, $cgi_q, $prefix, $analyzer, $token_counter) = @_;
182
    my @source;
183
    my $elasticsearch = $self->get_elasticsearch();
184
    my $query = $self->_build_query_autocomplete($cgi_q, $prefix, $analyzer);
185
    my $res = $elasticsearch->search(
186
        index => $self->index_name,
187
        body => $query
188
    );
189
    $res->{'val'} = $cgi_q;
190
    $res->{'prefix'} = $prefix;
191
    $res->{'token_counter'} = $token_counter;
192
193
  return $res;
194
}
195
196
=head2 autocomplete_idx
197
198
    my $query = $self->autocomplete_idx($cgi_q, $prefix, $analyzer, $token_counter);
199
200
Does a prefix search for C<$prefix> (many prefix), looking for C<$cgi_q>, using analyzer C<$analyzer>,
201
C<$token_counter> is used for identify which word to use in autocomplete
202
203
=cut
204
205
=head3 Returns
206
207
This returns an arrayref for all prefix of hashrefs with highlights. Each hashref contains a "text" element
208
that contains the field as returned.
209
210
=cut
211
212
sub autocomplete_idx {
213
  my ($self, $cgi_q, $prefix, $analyzer, $token_counter) = @_;
214
  my %results;
215
  my $idx = 0;
216
  foreach my $pref ( @$prefix ) {
217
      $results{$idx} = $self->autocomplete_one_idx($cgi_q, $pref, $analyzer, $token_counter);
218
      $idx++;
219
  }
220
  $results{'val'} = $cgi_q;
221
  $results{'prefix'} = join( ',', @$prefix );
222
  $results{'token_counter'} = $token_counter;
223
  return \%results;
224
}
225
226
=head2 _build_query_autocomplete
227
228
    my $query = $self->_build_query_autocomplete($cgi_q, $prefix, $analyzer);
229
230
Arguments:
231
232
=over 4
233
234
=item cgi_q
235
236
GET request
237
238
=item prefix
239
240
Field(s) for autocomplete (title, author, etc...)
241
242
=item analyzer
243
244
Name of analyzer wich we use for autocomplete
245
246
=back
247
248
=cut
249
250
=head3 Returns
251
252
This returns an arrayref for all prefix of hashrefs with highlights. Each hashref contains a "text" element
253
that contains the field as returned.
254
255
=cut
256
257
sub _build_query_autocomplete {
258
    my ($self, $cgi_q, $prefix, $analyzer) = @_;
259
    my (@source);
260
    #prefix + analyzer
261
    my $prefix_analyzer = $prefix . '.' . $analyzer;
262
    # we can change these variables
263
    my ($nb_fragments, $size_fragment, $pre_tags, $post_tags) = (1, 100, ["<strong>"], ["</strong>"]);
264
    push(@source, $prefix);
265
    my $query = {
266
        _source    => \@source,
267
        query => {
268
            match => {
269
                $prefix_analyzer    => {
270
                    query => $cgi_q,
271
                    operator => 'and'
272
                }
273
            }
274
        },
275
        highlight => {
276
            number_of_fragments => $nb_fragments,
277
            fragment_size => $size_fragment,
278
            pre_tags => $pre_tags,
279
            post_tags => $post_tags,
280
            fields => {
281
                $prefix_analyzer => {}
282
            }
283
        }
284
    };
285
    return $query;
286
}
287
165
1;
288
1;
166
289
167
__END__
290
__END__
Lines 172-177 __END__ Link Here
172
295
173
=item Robin Sheat << <robin@catalyst.net.nz> >>
296
=item Robin Sheat << <robin@catalyst.net.nz> >>
174
297
298
=item Ivan Dziuba << <ivan.dziuba@inlibro.com> >>
299
175
=back
300
=back
176
301
177
=cut
302
=cut
(-)a/admin/searchengine/elasticsearch/field_config.yaml (+4 lines)
Lines 50-55 search: Link Here
50
        type: text
50
        type: text
51
        analyzer: analyzer_phrase
51
        analyzer: analyzer_phrase
52
        search_analyzer: analyzer_phrase
52
        search_analyzer: analyzer_phrase
53
      autocomplete:
54
        type: text
55
        analyzer: autocomplete
56
        search_analyzer: standard
53
      raw:
57
      raw:
54
        type: keyword
58
        type: keyword
55
        normalizer: nfkc_cf_normalizer
59
        normalizer: nfkc_cf_normalizer
(-)a/admin/searchengine/elasticsearch/index_config.yaml (+14 lines)
Lines 2-7 Link Here
2
# Index configuration that defines how different analyzers work.
2
# Index configuration that defines how different analyzers work.
3
index:
3
index:
4
  analysis:
4
  analysis:
5
    tokenizer:
6
      autocomplete_tokenizer:
7
        type: edge_ngram
8
        min_gram: 1
9
        max_gram: 16
10
        token_chars:
11
          - letter
12
          - digit
5
    analyzer:
13
    analyzer:
6
      # Phrase analyzer is used for phrases (exact phrase match)
14
      # Phrase analyzer is used for phrases (exact phrase match)
7
      analyzer_phrase:
15
      analyzer_phrase:
Lines 10-15 index: Link Here
10
          - icu_folding
18
          - icu_folding
11
        char_filter:
19
        char_filter:
12
          - punctuation
20
          - punctuation
21
      autocomplete:
22
        type: custom
23
        filter:
24
          - icu_folding
25
          - lowercase
26
        tokenizer: autocomplete_tokenizer
13
      analyzer_standard:
27
      analyzer_standard:
14
        tokenizer: icu_tokenizer
28
        tokenizer: icu_tokenizer
15
        filter:
29
        filter:
(-)a/api/elasticsearch/elasticsearch.pl (+58 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
use CGI qw ( -utf8 );
5
use JSON;
6
use utf8;
7
use Unicode::Normalize;
8
use CGI::Session;
9
use Koha::SearchEngine::Elasticsearch::Browse;
10
11
my $browser = Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } );
12
my $cgi = CGI->new;
13
my $session = CGI::Session->load() or die CGI::Session->errstr();
14
15
$session->param(-name=>'analyzer', -value=>"autocomplete");
16
$session->param(-name=>'prefix', -value=>$cgi->multi_param("prefix"));
17
$session->param(-name=>'q', -value=>$cgi->multi_param("q"));
18
$session->param(-name=>'key', -value=>$cgi->multi_param("key"));
19
$session->param(-name=>'token_counter', -value=>$cgi->multi_param("token_counter"));
20
$session->expire('+1h');
21
22
if ($session->param("key") eq "autocomplete") {
23
  my @prefix = split /,/, $session->param("prefix");
24
  #fields for autocomplete
25
  my $length = scalar @prefix;
26
  my $ses = NFKD( $session->param("q") );
27
  $ses =~ s/\p{NonspacingMark}//g;
28
29
  if ($length >= 1) {
30
    my $res = $browser->autocomplete_idx($ses, \@prefix, $session->param("analyzer"), $session->param("token_counter"));
31
    print $cgi->header("application/json;charset=UTF-8");
32
    print to_json($res, {utf8 => 1});
33
  }
34
  #no prefix 404
35
  else {
36
    response404JSON();
37
  }
38
} else {
39
  response404JSON();
40
}
41
42
sub response404JSON {
43
  my $res = CGI->new;
44
  my $json = JSON->new->utf8;
45
  my $header_type = "application/json;charset=UTF-8";
46
  my $header_status = "404";
47
  my $output = $json->encode({
48
    "error" => "No data",
49
    "description" => "Bad request",
50
  });
51
  print $res->header(
52
    -type => $header_type,
53
    -charset => "utf-8",
54
    -status => $header_status
55
  );
56
  print $output;
57
  print "\n";
58
}
(-)a/installer/data/mysql/atomicupdate/bug_27113-elasticsearch_autocomplete_input_search.perl (+10 lines)
Line 0 Link Here
1
$DBversion = 'XXX'; # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
    # you can use $dbh here like:
4
    $dbh->do(q{INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('OPACAutocompleteElasticSearch', '0', NULL, 'If ON, show search suggestions in the OPAC when using Elasticsearch.', 'YesNo')});
5
6
    $dbh->do(q{INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('IntranetAutocompleteElasticSearch', '0', NULL, 'If ON, show search suggestions in the staff interface when using Elasticsearch.', 'YesNo')});
7
8
    # Always end with this (adjust the bug info)
9
    NewVersion( $DBversion, 27113, "Autocomplete with Elasticsearch");
10
}
(-)a/installer/data/mysql/mandatory/sysprefs.sql (+2 lines)
Lines 322-327 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
322
('OnSiteCheckouts','0','','Enable/Disable the on-site checkouts feature','YesNo'),
322
('OnSiteCheckouts','0','','Enable/Disable the on-site checkouts feature','YesNo'),
323
('OnSiteCheckoutsForce','0','','Enable/Disable the on-site for all cases (Even if a user is debarred, etc.)','YesNo'),
323
('OnSiteCheckoutsForce','0','','Enable/Disable the on-site for all cases (Even if a user is debarred, etc.)','YesNo'),
324
('OnSiteCheckoutAutoCheck','0','','Enable/Do not enable onsite checkout by default if last checkout was an onsite checkout','YesNo'),
324
('OnSiteCheckoutAutoCheck','0','','Enable/Do not enable onsite checkout by default if last checkout was an onsite checkout','YesNo'),
325
('IntranetAutocompleteElasticSearch','0',NULL,'If ON, show search suggestions in the staff interface when using Elasticsearch.','YesNo'),
325
('IntranetBiblioDefaultView','normal','normal|marc|isbd|labeled_marc','Choose the default detail view in the staff interface; choose between normal, labeled_marc, marc or isbd','Choice'),
326
('IntranetBiblioDefaultView','normal','normal|marc|isbd|labeled_marc','Choose the default detail view in the staff interface; choose between normal, labeled_marc, marc or isbd','Choice'),
326
('intranetbookbag','1','','If ON, enables display of Cart feature in the intranet','YesNo'),
327
('intranetbookbag','1','','If ON, enables display of Cart feature in the intranet','YesNo'),
327
('IntranetCirculationHomeHTML', '', NULL, 'Show the following HTML in a div on the bottom of the reports home page', 'Free'),
328
('IntranetCirculationHomeHTML', '', NULL, 'Show the following HTML in a div on the bottom of the reports home page', 'Free'),
Lines 449-454 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
449
('OPACAmazonCoverImages','0','','Display cover images on OPAC from Amazon Web Services','YesNo'),
450
('OPACAmazonCoverImages','0','','Display cover images on OPAC from Amazon Web Services','YesNo'),
450
('OPACAuthorIdentifiers','0','','Display author identifiers on the OPAC detail page','YesNo'),
451
('OPACAuthorIdentifiers','0','','Display author identifiers on the OPAC detail page','YesNo'),
451
('OpacAuthorities','1',NULL,'If ON, enables the search authorities link on OPAC','YesNo'),
452
('OpacAuthorities','1',NULL,'If ON, enables the search authorities link on OPAC','YesNo'),
453
('OPACAutocompleteElasticSearch','0',NULL,'If ON, show search suggestions in the OPAC when using Elasticsearch.','YesNo'),
452
('OPACBaseURL','',NULL,'Specify the Base URL of the OPAC, e.g., http://opac.mylibrary.com, including the protocol (http:// or https://). Otherwise, the http:// will be added automatically by Koha upon saving.','Free'),
454
('OPACBaseURL','',NULL,'Specify the Base URL of the OPAC, e.g., http://opac.mylibrary.com, including the protocol (http:// or https://). Otherwise, the http:// will be added automatically by Koha upon saving.','Free'),
453
('opacbookbag','1','','If ON, enables display of Cart feature','YesNo'),
455
('opacbookbag','1','','If ON, enables display of Cart feature','YesNo'),
454
('OpacBrowser','0',NULL,'If ON, enables subject authorities browser on OPAC (needs to set misc/cronjob/sbuild_browser_and_cloud.pl)','YesNo'),
456
('OpacBrowser','0',NULL,'If ON, enables subject authorities browser on OPAC (needs to set misc/cronjob/sbuild_browser_and_cloud.pl)','YesNo'),
(-)a/koha-tmpl/intranet-tmpl/js/elasticsearch/autocomplete.js (+270 lines)
Line 0 Link Here
1
/* OPAC JS file OPACAutocompleteElasticSearch */
2
/* prefix for search ES */
3
var es_prefix = {
4
    'au': 'author',
5
    'pb': 'publisher',
6
    'se': 'title-series',
7
    'su': 'subject',
8
    'ti': 'title-cover',
9
    /* for all */
10
    '': ['title-cover', 'author', 'subject', 'title-series', 'publisher'],
11
    'kw': ['title-cover', 'author', 'subject', 'title-series', 'publisher']
12
};
13
14
/* stop class for elements name=["q"] */
15
var stop_class_input = ["form-field-value"];
16
17
var url_request = '/cgi-bin/koha/api/elasticsearch/elasticsearch.pl?q=';
18
/* query for elasticsearch encode*/
19
var query_url_encode = {
20
    '\'':'%5C%27', /* \\' for decode */
21
    '+': ''
22
};
23
/* query for elasticsearch decode*/
24
var query_url_decode = {
25
    '\'':"\\'",
26
    '+': ''
27
};
28
/* count of lines for autocomplete */
29
var nb_autocomplete = 10;
30
/* key API */
31
var key = 'autocomplete';
32
33
function AutocompleteInitIntranet(){
34
    /* vars for class position absolute autocomplete */
35
    var left = "0px";
36
    var right = "0px";
37
    var top = "";
38
    /* get all input name q for search */
39
    var input_q = document.getElementsByName("q");
40
    for (var nb = 0; nb < input_q.length; nb++){
41
        /* addEventListener for every 'input' */
42
        if (!stop_class_input.includes(input_q[nb].className)){
43
            autocomplete(input_q[nb], nb, left, right, top);
44
        }
45
    };
46
};
47
48
function autocomplete(inp, nb, left, right) {
49
    var select_idx = document.getElementsByName("idx");
50
    /* autocomplete off for input */
51
    inp.setAttribute("autocomplete", "off");
52
    /* get parent of input */
53
    var parent_inp = $(inp).parent();
54
    /* get element after input */
55
    var next_elem_inp = inp.nextElementSibling;
56
    /* create new div with position relative for class .autocomplete with absolute */
57
    var div_relative = document.createElement('div');
58
    $(div_relative).addClass( "autocomplete" );
59
    div_relative.append(inp);
60
    /* input doesn't have an elem after, add it to parent */
61
    if (next_elem_inp === null){
62
        parent_inp.append( div_relative );
63
    }  else {  // input has an elem after, add elem after it
64
        next_elem_inp.before(div_relative);
65
    };
66
    var currentFocus;
67
68
    var timer;
69
    var doneTimerInterval = 500;
70
71
    /*execute a function when someone writes in the text field:*/
72
    var token_counter = 0;
73
    inp.addEventListener("input", function(e) {
74
        clearTimeout(timer);
75
        timer = setTimeout( function() {
76
            var a, val = inp.value;
77
            /* var for async compare */
78
            var tmp_input = inp.value.replace(/[+']/g, function(matched){
79
                return query_url_decode[matched];
80
            });
81
            token_counter++;
82
            currentFocus = -1;
83
            if (document.getElementsByClassName("autocomplete-items").length !== 0){
84
                a = document.getElementsByClassName("autocomplete-items")[0];
85
            } else {
86
                /*create a DIV element that will contain the items (values):*/
87
                a = document.createElement("DIV");
88
                a.setAttribute("id", inp.id + "autocomplete-list");
89
                a.setAttribute("class", "autocomplete-items");
90
                /*append the DIV element as a child of the autocomplete container:*/
91
                inp.parentNode.appendChild(a);
92
                /*append position absolute left/right:*/
93
                $(".autocomplete-items").css("left",left);
94
                $(".autocomplete-items").css("right",right);
95
            };
96
            /* get es_prefix key for builder */
97
            var chose_prefix = (select_idx == null || select_idx.length == 0) ? '' : GetValueIdx(select_idx, nb);
98
            chose_prefix = chose_prefix.replace(/([^,])[,-]([^,].*)?$/, '$1');
99
            if (chose_prefix !== null){
100
                var prefix = es_prefix[chose_prefix].toString();
101
                val = val.replace(/[+']/g, function(matched){
102
                    return query_url_encode[matched];
103
                });
104
                if (tmp_input == '' || tmp_input == null){
105
                    closeAllLists();
106
                    token_counter = 0;
107
                } else {
108
                    $.ajax({
109
                        type: 'GET',
110
                        url: url_request + val + '&key=' + key + '&prefix=' + prefix + '&token_counter=' + token_counter,
111
                        contentType: "application/json;charset=UTF-8",
112
                        success: function (data) {
113
                            //console.log(data);
114
                            if (data.length !== 0){
115
                                var myset; //Set for Autocomplete unique
116
117
                                myset = GetSetAutocomplete(data, prefix, key);
118
                                /* append set to autocomplete */
119
                                if ( tmp_input + prefix == data['val'] + data['prefix'] && token_counter === parseInt(data['token_counter'], 10)){
120
                                    a.innerHTML = "";
121
                                    for (let item of myset){
122
                                        a.appendChild(CreateDivItemAutocomplete(item, val));
123
                                    };
124
                                };
125
                            } else {
126
                                closeAllLists(inp);
127
                            };
128
                        },
129
                        error: function (data) {
130
                            console.log(data);
131
                        },
132
                    });
133
                }
134
135
            };
136
        }, doneTimerInterval);
137
    });
138
    /* get value for tag with name idx */
139
    function GetValueIdx(elem, nb){
140
        switch (elem[0].tagName){
141
            case 'INPUT':
142
                return elem[0].value;
143
            case 'SELECT':
144
                return select_idx[nb].options[select_idx[nb].selectedIndex].value;
145
            default:
146
                return null;
147
        };
148
    };
149
    /* get autocomplete for all prefix */
150
    function GetSetAutocomplete(data, prefix, key){
151
        let myset = new Set();
152
        var pref = prefix.split(",");
153
        for (k = 0; k < Object.keys(data).length; k++){ //Object.keys(data).length
154
            if (data[k] != '' && data[k] != null){
155
                let tmp_data = data[k]['hits']['hits'];
156
                for (i = 0; i < tmp_data.length; i++) {
157
                    for (j = 0; j < tmp_data[i]['highlight'][pref[k] + '.' + key].length; j++){
158
                        /* div with data for autocomplete */
159
                        let tmp = tmp_data[i]['highlight'][pref[k] + '.' + key][j]
160
                        myset.add(tmp.replace(/[ &#,+()$~%.'":*?<{}!/;]+$/g, ''));
161
                        if (myset.size >= nb_autocomplete) break;
162
                    };
163
                    if (myset.size >= nb_autocomplete) break;
164
                };
165
                if (myset.size >= nb_autocomplete) break;
166
            }
167
        }
168
        return myset;
169
    };
170
171
    /*execute a function presses a key on the keyboard:*/
172
    inp.addEventListener("keydown", function(e) {
173
        var x = document.getElementById(this.id + "autocomplete-list");
174
        if (x) x = x.getElementsByTagName("div");
175
        if (e.keyCode == 40) { //DOWN
176
            /*If the arrow DOWN key is pressed,
177
            increase the currentFocus variable:*/
178
            currentFocus++;
179
            /*and and make the current item more visible:*/
180
            addActive(x);
181
        } else if (e.keyCode == 38) { //up
182
            /*If the arrow UP key is pressed,
183
            decrease the currentFocus variable:*/
184
            currentFocus--;
185
            /*and and make the current item more visible:*/
186
            addActive(x);
187
            e.preventDefault();
188
        } else if (e.keyCode == 13) {
189
            /*If the ENTER key is pressed, prevent the form from being submitted,*/
190
            //e.preventDefault();
191
            if (currentFocus > -1) {
192
                /*and simulate a click on the "active" item:*/
193
                if (x) x[currentFocus].click();
194
            }
195
        }
196
        /* press Esc clear all autocomplete */
197
        else if (e.keyCode == 27) {
198
            closeAllLists();
199
        }
200
        /* press Esc clear all autocomplete */
201
        else if (e.keyCode == 8) {
202
            closeAllLists();
203
        }
204
        /* press Tab clear all autocomplete */
205
        else if (e.keyCode == 9) {
206
            closeAllLists();
207
        };
208
    });
209
    function addActive(x) {
210
        /*a function to classify an item as "active":*/
211
        if (!x) return false;
212
        /*start by removing the "active" class on all items:*/
213
        removeActive(x);
214
        if (currentFocus >= x.length) currentFocus = 0;
215
        if (currentFocus < 0) currentFocus = (x.length - 1);
216
        /*add class "autocomplete-active":*/
217
        x[currentFocus].classList.add("autocomplete-active");
218
        inp.value = (x[currentFocus].textContent.replace(/<\/?[^>]+(>|$)/g, "")).trim();
219
    };
220
    function removeActive(x) {
221
        /*a function to remove the "active" class from all autocomplete items:*/
222
        for (var i = 0; i < x.length; i++) {
223
            x[i].classList.remove("autocomplete-active");
224
        };
225
    };
226
227
    function closeAllLists(elem) {
228
        /*close all autocomplete lists in the document with class autocomplete-items */
229
        var x = document.getElementsByClassName("autocomplete-items");
230
        for (var i = 0; i < x.length; i++) {
231
            x[i].parentNode.removeChild(x[i])
232
        };
233
    };
234
235
    /* div for one item autocomplete */
236
    function CreateDivItemAutocomplete (elem){
237
        var b = document.createElement("DIV");
238
        // add element ";
239
        b.innerHTML += elem;
240
        /*insert a input field that will hold the current array item's value:*/
241
        b.innerHTML += "<input type='hidden' value=''>";
242
        /*execute a function when someone clicks on the item value (DIV element):*/
243
        b.addEventListener("click", function(e) {
244
            /* insert the value for the autocomplete text field: */
245
            inp.value = this.getElementsByTagName("input")[0].value;
246
            /* normalyzer hightlight without tags */
247
            //inp.value = (inp.value.replace(/<\/?[^>]+(>|$)/g, "")).trim();
248
            inp.value = this.innerText;
249
250
            var autocommit = 1;
251
            const inputs = document.querySelectorAll("#advanced-search input[type='text']");
252
            for (var i = 0; i < inputs.length && autocommit; i++) {
253
                var input = inputs[i];
254
                if (input === inp) {
255
                    autocommit = 0;
256
                }
257
            }
258
            //Submit form click mouse in div if not in advanced search
259
            if (autocommit) this.closest("form").submit();
260
        });
261
        return b;
262
    };
263
264
    /*execute a function when someone clicks in the document:*/
265
    document.addEventListener("click", function (e) {
266
        closeAllLists(e.target);
267
    });
268
};
269
270
AutocompleteInitIntranet();
(-)a/koha-tmpl/intranet-tmpl/prog/css/elasticsearch/autocomplete.css (+28 lines)
Line 0 Link Here
1
.autocomplete {
2
  /*the container must be positioned relative:*/
3
  position: relative;
4
  display: inline-block;
5
}
6
.autocomplete-items {
7
    position: absolute;
8
    border: 1px solid #d4d4d4;
9
    border-bottom: none;
10
    border-top: none;
11
    z-index: 99;
12
    /*position the autocomplete items to be the same width as the container:*/
13
  }
14
  .autocomplete-items div {
15
    padding: 10px;
16
    cursor: pointer;
17
    background-color: #fff;
18
    border-bottom: 1px solid #d4d4d4;
19
  }
20
  .autocomplete-items div:hover {
21
    /*when hovering an item:*/
22
    background-color: #e9e9e9;
23
  }
24
  .autocomplete-active {
25
    /*when navigating through the items using the arrow keys:*/
26
    background-color: #cedfb1 !important;
27
    color: #ffffff;
28
  }
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc (+5 lines)
Lines 28-33 Link Here
28
[% Asset.css("css/print.css", { media = "print" }) | $raw %]
28
[% Asset.css("css/print.css", { media = "print" }) | $raw %]
29
[% INCLUDE intranetstylesheet.inc %]
29
[% INCLUDE intranetstylesheet.inc %]
30
[% IF ( bidi ) %][% Asset.css("css/right-to-left.css") | $raw %][% END %]
30
[% IF ( bidi ) %][% Asset.css("css/right-to-left.css") | $raw %][% END %]
31
<!-- Intranet inc CSS IntranetAutocompleteElasticSearch -->
32
[% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %]
33
    [% SET Optylesheet = 'elasticsearch/autocomplete.css' %]
34
    <link rel="stylesheet" type="text/css" href="[% interface | url %]/[% theme | url %]/css/[% Optylesheet | url %]" />
35
[% END %]
31
36
32
<script>
37
<script>
33
var Koha = {};
38
var Koha = {};
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/js_includes.inc (+4 lines)
Lines 91-94 Link Here
91
    });
91
    });
92
    </script>
92
    </script>
93
[% END %]
93
[% END %]
94
<!-- Intranet inc JS IntranetAutocompleteElasticSearch -->
95
[% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %]
96
[% Asset.js("js/elasticsearch/autocomplete.js") | $raw %]
97
[% END %]
94
<!-- / js_includes.inc -->
98
<!-- / js_includes.inc -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref (+17 lines)
Lines 344-346 Searching: Link Here
344
            - pref: LibrisURL
344
            - pref: LibrisURL
345
              class: url
345
              class: url
346
            - "Please only change this if you are sure it needs changing."
346
            - "Please only change this if you are sure it needs changing."
347
        -
348
            - pref: OPACAutocompleteElasticSearch
349
              type: boolean
350
              default: 0
351
              choices:
352
                  1: Show
353
                  0: "Don't show"
354
            - looking terms based on a provided text by using an ElasticSearch for OPAC.
355
        -
356
            - pref: IntranetAutocompleteElasticSearch
357
              type: boolean
358
              default: 0
359
              choices:
360
                  1: Show
361
                  0: "Don't show"
362
            - looking terms based on a provided text by using an ElasticSearch for Intranet.
363
        -
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt (+4 lines)
Lines 420-425 Link Here
420
            var dad  = line.parentNode;
420
            var dad  = line.parentNode;
421
            dad.appendChild(line.cloneNode(true));
421
            dad.appendChild(line.cloneNode(true));
422
            line.removeChild(ButtonPlus);
422
            line.removeChild(ButtonPlus);
423
            /* Intranet JS IntranetAutocompleteElasticSearch */
424
            [% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %]
425
                AutocompleteInitIntranet();
426
            [% END %]
423
        }
427
        }
424
428
425
        var Sticky;
429
        var Sticky;
(-)a/koha-tmpl/intranet-tmpl/prog/js/staff-global.js (+7 lines)
Lines 134-139 $(document).ready(function() { Link Here
134
    $(".keep_text").on("click",function(){
134
    $(".keep_text").on("click",function(){
135
        var field_index = $(this).parent().index();
135
        var field_index = $(this).parent().index();
136
        keep_text( field_index );
136
        keep_text( field_index );
137
        /* IntranetAutocompleteElasticSearch Tab */
138
        var tab = this.hash.substr(1, this.hash.length-1);
139
        /*  Koha.Preference('IntranetAutocompleteElasticSearch') == Show */
140
        if (typeof AutocompleteInitIntranet !== "undefined" && tab === 'catalog_search' ){
141
            AutocompleteInitIntranet();
142
        }
143
        $("#search-form").focus();
137
    });
144
    });
138
145
139
    $(".toggle_element").on("click",function(e){
146
    $(".toggle_element").on("click",function(e){
(-)a/koha-tmpl/opac-tmpl/bootstrap/css/opac-elasticsearch/opac-autocomplete.css (+29 lines)
Line 0 Link Here
1
/* CSS file OPACAutocompleteElasticSearch */
2
.autocomplete {
3
  /*the container must be positioned relative:*/
4
  position: relative;
5
  display: inline-block;
6
  width: 100%;
7
}
8
.autocomplete-items {
9
    position: absolute;
10
    border: 1px solid #d4d4d4;
11
    border-bottom: none;
12
    border-top: none;
13
    z-index: 99;
14
  }
15
  .autocomplete-items div {
16
    padding: 10px;
17
    cursor: pointer;
18
    background-color: #fff;
19
    border-bottom: 1px solid #d4d4d4;
20
  }
21
  .autocomplete-items div:hover {
22
    /*when hovering an item:*/
23
    background-color: #e9e9e9;
24
  }
25
  .autocomplete-active {
26
    /*when navigating through the items using the arrow keys:*/
27
    background-color: #cedfb1 !important;
28
    color: #ffffff;
29
  }
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/doc-head-close.inc (+5 lines)
Lines 21-26 Link Here
21
        [% SET opaclayoutstylesheet = 'opac.css' %]
21
        [% SET opaclayoutstylesheet = 'opac.css' %]
22
    [% END %]
22
    [% END %]
23
[% END %]
23
[% END %]
24
<!-- OPAC inc CSS OPACAutocompleteElasticSearch -->
25
[% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
26
    [% SET Optylesheet = 'opac-elasticsearch/opac-autocomplete.css' %]
27
    <link rel="stylesheet" type="text/css" href="[% interface | url %]/[% theme | url %]/css/[% Optylesheet | url %]" />
28
[% END %]
24
[% IF (opaclayoutstylesheet.match('^https?:|^\/')) %]
29
[% IF (opaclayoutstylesheet.match('^https?:|^\/')) %]
25
    <link rel="stylesheet" type="text/css" href="[% opaclayoutstylesheet | url %]" />
30
    <link rel="stylesheet" type="text/css" href="[% opaclayoutstylesheet | url %]" />
26
[% ELSE %]
31
[% ELSE %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc (+4 lines)
Lines 235-240 $(document).ready(function() { Link Here
235
[% INCLUDE 'js-date-format.inc' %]
235
[% INCLUDE 'js-date-format.inc' %]
236
[% INCLUDE 'js-biblio-format.inc' %]
236
[% INCLUDE 'js-biblio-format.inc' %]
237
[% PROCESS jsinclude %]
237
[% PROCESS jsinclude %]
238
<!-- OPAC *.inc JS OPACAutocompleteElasticSearch -->
239
[% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
240
    [% Asset.js("js/opac-elasticsearch/opac-autocomplete.js") | $raw %]
241
[% END %]
238
[% IF ( Koha.Preference('OPACUserJS') ) %]
242
[% IF ( Koha.Preference('OPACUserJS') ) %]
239
    <script>
243
    <script>
240
        [% Koha.Preference('OPACUserJS') | $raw %]
244
        [% Koha.Preference('OPACUserJS') | $raw %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-advsearch.tt (+8 lines)
Lines 545-550 $(document).ready(function() { Link Here
545
        $(newLine).find('.search-term-input select[name="op"]').first().prop("disabled",false).show();
545
        $(newLine).find('.search-term-input select[name="op"]').first().prop("disabled",false).show();
546
        newLine.find('input').val('');
546
        newLine.find('input').val('');
547
        thisLine.after(newLine);
547
        thisLine.after(newLine);
548
        /* OPAC JS OPACAutocompleteElasticSearch */
549
        [% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
550
            AutocompleteInitOpac();
551
        [% END %]
548
    });
552
    });
549
553
550
    $(document).on("click", '.ButtonLess', function(e) {
554
    $(document).on("click", '.ButtonLess', function(e) {
Lines 554-559 $(document).ready(function() { Link Here
554
        }
558
        }
555
        $(this).parent().parent().remove();
559
        $(this).parent().parent().remove();
556
        $('.search-term-row .search-term-input select[name="op"]').first().prop("disabled",true).hide();
560
        $('.search-term-row .search-term-input select[name="op"]').first().prop("disabled",true).hide();
561
        /* OPAC JS OPACAutocompleteElasticSearch */
562
        [% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
563
            AutocompleteInitOpac();
564
        [% END %]
557
    });
565
    });
558
</script>
566
</script>
559
[% END %]
567
[% END %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/js/opac-elasticsearch/opac-autocomplete.js (+266 lines)
Line 0 Link Here
1
/* OPAC JS file OPACAutocompleteElasticSearch */
2
/* prefix for search ES */
3
var es_prefix = {
4
    'au': 'author',
5
    'pb': 'publisher',
6
    'se': 'title-series',
7
    'su': 'subject',
8
    'ti': 'title-cover',
9
    /* for all */
10
    '': ['title-cover', 'author', 'subject', 'title-series', 'publisher'],
11
    'kw': ['title-cover', 'author', 'subject', 'title-series', 'publisher']
12
};
13
14
var url_request = '/cgi-bin/koha/svc/elasticsearch/opac-elasticsearch.pl?q=';
15
/* query for elasticsearch encode*/
16
var query_url_encode = {
17
    '\'':'%5C%27', /* \\' for decode */
18
    '+': ''
19
};
20
/* query for elasticsearch decode*/
21
var query_url_decode = {
22
    '\'':"\\'",
23
    '+': ''
24
};
25
/* count of lines for autocomplete */
26
var nb_autocomplete = 10;
27
/* key API */
28
var key = 'autocomplete';
29
30
function AutocompleteInitOpac(){
31
    /* vars for class position absolute autocomplete */
32
    var left = "0px";
33
    var right = "0px";
34
    var top = "";
35
    /* get all input name q for search */
36
    var input_q = document.getElementsByName("q");
37
    for (var nb = 0; nb < input_q.length; nb++){
38
        /* addEventListener for every 'input' */
39
        autocomplete(input_q[nb], nb, left, right, top);
40
    };
41
};
42
43
function autocomplete(inp, nb, left, right) {
44
    var select_idx = document.getElementsByName("idx");
45
    /* autocomplete off for input */
46
    inp.setAttribute("autocomplete", "off");
47
    /* get parent of input */
48
    var parent_inp = $(inp).parent();
49
    /* get element after input */
50
    var next_elem_inp = inp.nextElementSibling;
51
    /* create new div with position relative for class .autocomplete with absolute */
52
    var div_relative = document.createElement('div');
53
    $(div_relative).addClass( "autocomplete" );
54
    $(div_relative).css("display", "inline-block");
55
    div_relative.append(inp);
56
    /* input doesn't have an elem after, add it to parent */
57
    if (next_elem_inp === null){
58
        parent_inp.append( div_relative );
59
    }  else {  // input has an elem after, add elem after it
60
        next_elem_inp.before(div_relative);
61
    };
62
    var currentFocus;
63
64
    var timer;
65
    var doneTimerInterval = 500;
66
67
    /*execute a function when someone writes in the text field:*/
68
    var token_counter = 0;
69
    inp.addEventListener("input", function(e) {
70
        clearTimeout(timer);
71
        timer = setTimeout( function() {
72
            var a, val = inp.value;
73
            /* var for async compare */
74
            var tmp_input = inp.value.replace(/[+']/g, function(matched){
75
                return query_url_decode[matched];
76
            });
77
            token_counter++;
78
            currentFocus = -1;
79
            if (document.getElementsByClassName("autocomplete-items").length !== 0){
80
                a = document.getElementsByClassName("autocomplete-items")[0];
81
            } else {
82
                /*create a DIV element that will contain the items (values):*/
83
                a = document.createElement("DIV");
84
                a.setAttribute("id", inp.id + "autocomplete-list");
85
                a.setAttribute("class", "autocomplete-items");
86
                /*append the DIV element as a child of the autocomplete container:*/
87
                inp.parentNode.appendChild(a);
88
                /*append position absolute left/right:*/
89
                $(".autocomplete-items").css("left",left);
90
                $(".autocomplete-items").css("right",right);
91
            };
92
            /* get es_prefix key for builder */
93
            var chose_prefix = (select_idx == null || select_idx.length == 0) ? '' : GetValueIdx(select_idx, nb);
94
            chose_prefix = chose_prefix.replace(/([^,])[,-]([^,].*)?$/, '$1');
95
            if (chose_prefix !== null){
96
                var prefix = es_prefix[chose_prefix].toString();
97
                val = val.replace(/[+']/g, function(matched){
98
                    return query_url_encode[matched];
99
                });
100
                if (tmp_input == '' || tmp_input == null){
101
                    closeAllLists();
102
                    token_counter = 0;
103
                } else {
104
                    $.ajax({
105
                        type: 'GET',
106
                        url: url_request + val + '&key=' + key + '&prefix=' + prefix + '&token_counter=' + token_counter,
107
                        contentType: "application/json;charset=UTF-8",
108
                        success: function (data) {
109
                            //console.log(data);
110
                            if (data.length !== 0){
111
                                var myset; //Set for Autocomplete unique
112
113
                                myset = GetSetAutocomplete(data, prefix, key);
114
                                /* append set to autocomplete */
115
                                if ( tmp_input + prefix == data['val'] + data['prefix'] && token_counter === parseInt(data['token_counter'], 10)){
116
                                    a.innerHTML = "";
117
                                    for (let item of myset){
118
                                        a.appendChild(CreateDivItemAutocomplete(item, val));
119
                                    };
120
                                };
121
                            } else {
122
                                closeAllLists(inp);
123
                            };
124
                        },
125
                        error: function (data) {
126
                            console.log(data);
127
                        },
128
                    });
129
                }
130
131
            };
132
        }, doneTimerInterval);
133
    });
134
    /* get value for tag with name idx */
135
    function GetValueIdx(elem, nb){
136
        switch (elem[0].tagName){
137
            case 'INPUT':
138
                return elem[0].value;
139
            case 'SELECT':
140
                return select_idx[nb].options[select_idx[nb].selectedIndex].value;
141
            default:
142
                return null;
143
        };
144
    };
145
    /* get autocomplete for all prefix */
146
    function GetSetAutocomplete(data, prefix, key){
147
        let myset = new Set();
148
        var pref = prefix.split(",");
149
        for (k = 0; k < Object.keys(data).length; k++){ //Object.keys(data).length
150
            if (data[k] != '' && data[k] != null){
151
                let tmp_data = data[k]['hits']['hits'];
152
                for (i = 0; i < tmp_data.length; i++) {
153
                    for (j = 0; j < tmp_data[i]['highlight'][pref[k] + '.' + key].length; j++){
154
                        /* div with data for autocomplete */
155
                        let tmp = tmp_data[i]['highlight'][pref[k] + '.' + key][j]
156
                        myset.add(tmp.replace(/[ &#,+()$~%.'":*?<{}!/;]+$/g, ''));
157
                        if (myset.size >= nb_autocomplete) break;
158
                    };
159
                    if (myset.size >= nb_autocomplete) break;
160
                };
161
                if (myset.size >= nb_autocomplete) break;
162
            }
163
        }
164
        return myset;
165
    };
166
167
    /*execute a function presses a key on the keyboard:*/
168
    inp.addEventListener("keydown", function(e) {
169
        var x = document.getElementById(this.id + "autocomplete-list");
170
        if (x) x = x.getElementsByTagName("div");
171
        if (e.keyCode == 40) { //DOWN
172
            /*If the arrow DOWN key is pressed,
173
            increase the currentFocus variable:*/
174
            currentFocus++;
175
            /*and and make the current item more visible:*/
176
            addActive(x);
177
        } else if (e.keyCode == 38) { //up
178
            /*If the arrow UP key is pressed,
179
            decrease the currentFocus variable:*/
180
            currentFocus--;
181
            /*and and make the current item more visible:*/
182
            addActive(x);
183
            e.preventDefault();
184
        } else if (e.keyCode == 13) {
185
            /*If the ENTER key is pressed, prevent the form from being submitted,*/
186
            //e.preventDefault();
187
            if (currentFocus > -1) {
188
                /*and simulate a click on the "active" item:*/
189
                if (x) x[currentFocus].click();
190
            }
191
        }
192
        /* press Esc clear all autocomplete */
193
        else if (e.keyCode == 27) {
194
            closeAllLists();
195
        }
196
        /* press Esc clear all autocomplete */
197
        else if (e.keyCode == 8) {
198
            closeAllLists();
199
        }
200
        /* press Tab clear all autocomplete */
201
        else if (e.keyCode == 9) {
202
            closeAllLists();
203
        };
204
    });
205
    function addActive(x) {
206
        /*a function to classify an item as "active":*/
207
        if (!x) return false;
208
        /*start by removing the "active" class on all items:*/
209
        removeActive(x);
210
        if (currentFocus >= x.length) currentFocus = 0;
211
        if (currentFocus < 0) currentFocus = (x.length - 1);
212
        /*add class "autocomplete-active":*/
213
        x[currentFocus].classList.add("autocomplete-active");
214
        inp.value = (x[currentFocus].textContent.replace(/<\/?[^>]+(>|$)/g, "")).trim();
215
    };
216
    function removeActive(x) {
217
        /*a function to remove the "active" class from all autocomplete items:*/
218
        for (var i = 0; i < x.length; i++) {
219
            x[i].classList.remove("autocomplete-active");
220
        };
221
    };
222
223
    function closeAllLists(elem) {
224
        /*close all autocomplete lists in the document with class autocomplete-items */
225
        var x = document.getElementsByClassName("autocomplete-items");
226
        for (var i = 0; i < x.length; i++) {
227
            x[i].parentNode.removeChild(x[i])
228
        };
229
    };
230
231
    /* div for one item autocomplete */
232
    function CreateDivItemAutocomplete (elem){
233
        var b = document.createElement("DIV");
234
        // add element ";
235
        b.innerHTML += elem;
236
        /*insert a input field that will hold the current array item's value:*/
237
        b.innerHTML += "<input type='hidden' value=''>";
238
        /*execute a function when someone clicks on the item value (DIV element):*/
239
        b.addEventListener("click", function(e) {
240
            /* insert the value for the autocomplete text field: */
241
            inp.value = this.getElementsByTagName("input")[0].value;
242
            /* normalyzer hightlight without tags */
243
            //inp.value = (inp.value.replace(/<\/?[^>]+(>|$)/g, "")).trim();
244
            inp.value = this.innerText;
245
246
            var autocommit = 1;
247
            const inputs = document.querySelectorAll("#booleansearch input[type='text']");
248
            for (var i = 0; i < inputs.length && autocommit; i++) {
249
                var input = inputs[i];
250
                if (input === inp) {
251
                    autocommit = 0;
252
                }
253
            }
254
            //Submit form click mouse in div if not in advanced search
255
            if (autocommit) this.closest("form").submit();
256
        });
257
        return b;
258
    };
259
260
    /*execute a function when someone clicks in the document:*/
261
    document.addEventListener("click", function (e) {
262
        closeAllLists(e.target);
263
    });
264
};
265
266
AutocompleteInitOpac();
(-)a/opac/svc/elasticsearch/opac-elasticsearch.pl (+145 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
use CGI qw ( -utf8 );
5
use JSON;
6
use utf8;
7
use Unicode::Normalize;
8
use CGI::Session;
9
use Koha::SearchEngine::Elasticsearch::Browse;
10
11
use Koha::Items;
12
use C4::Context;
13
use C4::Biblio qw ( GetMarcBiblio );
14
15
my $browser = Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } );
16
my $cgi = CGI->new;
17
my $session = CGI::Session->load() or die CGI::Session->errstr();
18
19
$session->param(-name=>'analyzer', -value=>"autocomplete");
20
$session->param(-name=>'prefix', -value=>$cgi->multi_param("prefix"));
21
$session->param(-name=>'q', -value=>$cgi->multi_param("q"));
22
$session->param(-name=>'key', -value=>$cgi->multi_param("key"));
23
$session->param(-name=>'token_counter', -value=>$cgi->multi_param("token_counter"));
24
$session->expire('+1h');
25
26
if ($session->param("key") eq "autocomplete") {
27
  my @prefix = split /,/, $session->param("prefix");
28
  #fields for autocomplete
29
  my $length = scalar @prefix;
30
  my $ses = NFKD( $session->param("q") );
31
  $ses =~ s/\p{NonspacingMark}//g;
32
33
  if ($length >= 1) {
34
    my $res = $browser->autocomplete_idx($ses, \@prefix, $session->param("analyzer"), $session->param("token_counter"));
35
36
    filterAutocomplete($res);
37
38
    print $cgi->header("application/json;charset=UTF-8");
39
    print to_json($res, {utf8 => 1});
40
  }
41
  #no prefix 404
42
  else {
43
    response404JSON();
44
  }
45
} else {
46
  response404JSON();
47
}
48
49
sub filterAutocomplete {
50
  if (C4::Context->preference('OpacSuppression') || C4::Context->yaml_preference('OpacHiddenItems')) {
51
    my $res = shift;
52
    my @prefix = $res->{ "prefix" };
53
    @prefix = split(',', $prefix[0]);
54
55
    for (my $i = 0; $i < scalar @prefix; $i++) {
56
      my $hits = $res->{ $i }->{ 'hits' };
57
      my $hitlist = $hits->{ "hits" };
58
      if (@{$hitlist}) {
59
        # Remove item inside hits in elasticsearch response if the item has
60
        # marc field 942$n set to true and OpacSuppression preference on
61
        if (C4::Context->preference('OpacSuppression')) {
62
          for ( my $i = 0; $i < scalar @{$hitlist}; $i++ ) {
63
            my $record = GetMarcBiblio({
64
              biblionumber => $hitlist->[$i]->{ "_id" },
65
              opac         => 1
66
            });
67
            my $opacsuppressionfield = '942';
68
            my $opacsuppressionfieldvalue = $record->field($opacsuppressionfield);
69
            if ( $opacsuppressionfieldvalue &&
70
                $opacsuppressionfieldvalue->subfield("n") &&
71
                $opacsuppressionfieldvalue->subfield("n") == 1) {
72
              # if OPAC suppression by IP address
73
              if (C4::Context->preference('OpacSuppressionByIPRange')) {
74
                my $IPAddress = $ENV{'REMOTE_ADDR'};
75
                my $IPRange = C4::Context->preference('OpacSuppressionByIPRange');
76
                if ($IPAddress !~ /^$IPRange/)  {
77
                    splice(@{$hitlist}, $i, 1);
78
                    $i--;
79
                    $hits->{ "total" }--;
80
                }
81
              } else {
82
                splice(@{$hitlist}, $i, 1);
83
                $i--;
84
                $hits->{ "total" }--;
85
              }
86
            }
87
          }
88
        }
89
        # Remove item inside hits in elasticsearch response if the item is
90
        # declared hidden in OPACHiddenItems preference
91
        if (C4::Context->yaml_preference('OpacHiddenItems')) {
92
          my @biblionumbers;
93
          foreach (@{$hitlist}) {
94
            push(@biblionumbers, $_->{ "_id" });
95
          }
96
          my $autocomplete_items = Koha::Items->search({
97
            biblionumber => { -in => \@biblionumbers }
98
          });
99
          my $filtered_items = $autocomplete_items->filter_by_visible_in_opac({
100
            patron => undef
101
          });
102
          for ( my $i = 0; $i < scalar @{$hitlist}; $i++ ) {
103
            my $item = $filtered_items->find({
104
              biblionumber => $hitlist->[$i]->{ "_id" }
105
            });
106
            if (!$item) {
107
              splice(@{$hitlist}, $i, 1);
108
              $i--;
109
              $hits->{ "total" }--;
110
            }
111
          }
112
        }
113
        # Adjust the max_score inside hits in elasticsearch response
114
        my $maxscore = 0;
115
        foreach ( @{$hitlist} ) {
116
          my $score = $_->{"_score"};
117
          $maxscore = $score if ($maxscore < $score);
118
        }
119
        if ($maxscore == 0) {
120
          $hits->{ "max_score" } = undef;
121
        } else {
122
          $hits->{ "max_score" } = $maxscore;
123
        }
124
      }
125
    }
126
  }
127
}
128
129
sub response404JSON {
130
  my $res = CGI->new;
131
  my $json = JSON->new->utf8;
132
  my $header_type = "application/json;charset=UTF-8";
133
  my $header_status = "404";
134
  my $output = $json->encode({
135
    "error" => "No data",
136
    "description" => "Bad request",
137
  });
138
  print $res->header(
139
    -type => $header_type,
140
    -charset => "utf-8",
141
    -status => $header_status
142
  );
143
  print $output;
144
  print "\n";
145
}
(-)a/t/Koha_SearchEngine_Elasticsearch_Browse.t (-1 / +30 lines)
Lines 65-68 subtest "_build_query tests" => sub { Link Here
65
    }, 'Fuzziness and size specified');
65
    }, 'Fuzziness and size specified');
66
};
66
};
67
67
68
subtest "_build_query_autocomplete tests" => sub {
69
    plan tests => 1;
70
71
    my $browse = Koha::SearchEngine::Elasticsearch::Browse->new({index=>'dummy'});
72
73
    my $q = $browse->_build_query_autocomplete('a', 'title', 'autocomplete');
74
75
    is_deeply($q, {
76
        _source    => ["title"],
77
        query => {
78
            match => {
79
                "title.autocomplete"  => {
80
                    query => 'a',
81
                    operator => 'and'
82
                }
83
            }
84
        },
85
        highlight => {
86
            number_of_fragments => 1,
87
            fragment_size => 100,
88
            pre_tags => ["<strong>"],
89
            post_tags => ["</strong>"],
90
            fields => {
91
                "title.autocomplete" => {}
92
            }
93
        }
94
    }, 'Autocomplete for title is specified');
95
96
};
97
68
done_testing();
98
done_testing();
69
- 

Return to bug 27113