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 47-52 search: Link Here
47
        type: text
47
        type: text
48
        analyzer: analyzer_phrase
48
        analyzer: analyzer_phrase
49
        search_analyzer: analyzer_phrase
49
        search_analyzer: analyzer_phrase
50
      autocomplete:
51
        type: text
52
        analyzer: autocomplete
53
        search_analyzer: standard
50
      raw:
54
      raw:
51
        type: keyword
55
        type: keyword
52
        normalizer: nfkc_cf_normalizer
56
        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: 10
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 (+67 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use strict;
4
use warnings;
5
use CGI qw ( -utf8 );
6
use JSON;
7
use utf8;
8
use Unicode::Normalize;
9
use CGI::Session;
10
use Koha::SearchEngine::Elasticsearch::Browse;
11
12
my $browser = Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } );
13
my $cgi = CGI->new;
14
my $session = CGI::Session->load() or die CGI::Session->errstr();
15
16
$session->param(-name=>'analyzer', -value=>"autocomplete");
17
$session->param(-name=>'prefix', -value=>$cgi->multi_param("prefix"));
18
$session->param(-name=>'q', -value=>$cgi->multi_param("q"));
19
$session->param(-name=>'key', -value=>$cgi->multi_param("key"));
20
$session->param(-name=>'token_counter', -value=>$cgi->multi_param("token_counter"));
21
$session->expire('+1h');
22
23
if ($session->param("key") eq "autocomplete") {
24
  my @prefix = split /,/, $session->param("prefix");
25
  #fields for autocomplete
26
  my $length = scalar @prefix;
27
  my $ses = NFKD( $session->param("q") );
28
  $ses =~ s/\p{NonspacingMark}//g;
29
30
    #search by many prefix fields
31
  if ($length > 1){
32
    my $res = $browser->autocomplete_idx($ses, \@prefix, $session->param("analyzer"), $session->param("token_counter"));
33
    print $cgi->header("application/json");
34
    print to_json($res);
35
  }
36
  #search by one prefix field
37
  elsif ($length == 1) {
38
    my $res = $browser->autocomplete_one_idx($ses, $prefix[0], $session->param("analyzer"), $session->param("token_counter"));
39
    print $cgi->header("application/json");
40
    print to_json($res);
41
  }
42
  #no prefix 404
43
  else {
44
    response404JSON();
45
  }
46
}
47
else {
48
  response404JSON();
49
}
50
51
sub response404JSON {
52
  my $res = CGI->new;
53
  my $json = JSON->new->utf8;
54
  my $header_type = "application/json";
55
  my $header_status = "404";
56
  my $output = $json->encode({
57
    "error" => "No data",
58
    "description" => "Bad request",
59
  });
60
  print $res->header(
61
    -type => $header_type,
62
    -charset => "utf-8",
63
    -status => $header_status
64
  );
65
  print $output;
66
  print "\n";
67
}
(-)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, NULL, 'YesNo')});
5
6
    $dbh->do(q{INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('IntranetAutocompleteElasticSearch', '0', NULL, NULL, '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 (-1 / +3 lines)
Lines 744-748 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
744
('XSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on intranet','Free'),
744
('XSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on intranet','Free'),
745
('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'),
745
('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'),
746
('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'),
746
('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'),
747
('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo')
747
('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo'),
748
('OPACAutocompleteElasticSearch','0',NULL,NULL,'YesNo'),
749
('IntranetAutocompleteElasticSearch','0',NULL,NULL,'YesNo')
748
;
750
;
(-)a/koha-tmpl/intranet-tmpl/js/elasticsearch/autocomplete.js (+283 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
    /*execute a function when someone writes in the text field:*/
68
    var token_counter = 0;
69
    inp.addEventListener("input", function(e) {
70
        var a, val = this.value;
71
        /* var for async compare */
72
        var tmp_input = this.value.replace(/[+']/g, function(matched){
73
            return query_url_decode[matched];
74
        });
75
        token_counter++;
76
        currentFocus = -1;
77
        if (document.getElementsByClassName("autocomplete-items").length !== 0){
78
            a = document.getElementsByClassName("autocomplete-items")[0];
79
        } else {
80
            /*create a DIV element that will contain the items (values):*/
81
            a = document.createElement("DIV");
82
            a.setAttribute("id", this.id + "autocomplete-list");
83
            a.setAttribute("class", "autocomplete-items");
84
            /*append the DIV element as a child of the autocomplete container:*/
85
            this.parentNode.appendChild(a);
86
            /*append position absolute left/right:*/
87
            $(".autocomplete-items").css("left",left);
88
            $(".autocomplete-items").css("right",right);
89
        };
90
        /* get es_prefix key for builder */
91
        var chose_prefix = (select_idx == null || select_idx.length == 0) ? '' : GetValueIdx(select_idx, nb);
92
        chose_prefix = chose_prefix.replace(/([^,])[,-]([^,].*)?$/, '$1');
93
        if (chose_prefix !== null){
94
            var prefix = es_prefix[chose_prefix].toString();
95
            val = val.replace(/[+']/g, function(matched){
96
                return query_url_encode[matched];
97
            });
98
            if (tmp_input == '' || tmp_input == null){
99
                closeAllLists();
100
                token_counter = 0;
101
            } else {
102
                $.ajax({
103
                    type: 'GET',
104
                    url: url_request + val + '&key=' + key + '&prefix=' + prefix + '&token_counter=' + token_counter,
105
                    success: function (data) {
106
                        //console.log(data);
107
                        if (data.length !== 0){
108
                            var myset; //Set for Autocomplete unique
109
                            /* autocomplete for all prefix */
110
                            if (chose_prefix === 'kw' || chose_prefix === ''){
111
                                myset = GetSetAutocompleteAllIdx(data, prefix, key);
112
                            } else { // autocomplete for one prefix
113
                                myset = GetSetAutocompleteOneIdx(data, prefix, key);
114
                            };
115
                            /* append set to autocomplete */
116
                            if ( tmp_input + prefix == data['val'] + data['prefix'] && token_counter === parseInt(data['token_counter'], 10)){
117
                                a.innerHTML = "";
118
                                for (let item of myset){
119
                                    a.appendChild(CreateDivItemAutocomplete(item, val));
120
                                };
121
                            };
122
                        } else {
123
                            closeAllLists(this);
124
                        };
125
                    },
126
                    error: function (data) {
127
                        console.log(data);
128
                    },
129
                });
130
            }
131
132
        };
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 only one prefix title/author/etc... */
146
    function GetSetAutocompleteOneIdx(data, prefix, key){
147
        let myset = new Set();
148
        let tmp_data = data['hits']['hits'];
149
        for (let i = 0; i < tmp_data.length; i++) {
150
            for (let j = 0; j < tmp_data[i]['highlight'][prefix + '.' + key].length; j++){
151
                /* div with data for autocomplete */
152
                let tmp = tmp_data[i]['highlight'][prefix + '.' + key][j];
153
                tmp = tmp.replace(/^\[/g, '');
154
                tmp = tmp.replace(/\]+$/g, '');
155
                myset.add(tmp.replace(/^[ &\/\\#,+)$~%.'":*?>{}!;]+|[ &\/\\#,+($~%.'":*?<{}!;]+$/g, ''));
156
                if (myset.size >= nb_autocomplete) break;
157
            };
158
            if (myset.size >= nb_autocomplete) break;
159
        };
160
        return myset;
161
    };
162
    /* get autocomplete for all prefix */
163
    function GetSetAutocompleteAllIdx(data, prefix, key){
164
        let myset = new Set();
165
        var pref = prefix.split(",");
166
        for (k = 0; k < Object.keys(data).length; k++){ //Object.keys(data).length
167
            if (data[k] != '' && data[k] != null){
168
                let tmp_data = data[k]['hits']['hits'];
169
                for (i = 0; i < tmp_data.length; i++) {
170
                    for (j = 0; j < tmp_data[i]['highlight'][pref[k] + '.' + key].length; j++){
171
                        /* div with data for autocomplete */
172
                        let tmp = tmp_data[i]['highlight'][pref[k] + '.' + key][j]
173
                        myset.add(tmp.replace(/[ &#,+()$~%.'":*?<{}!/;]+$/g, ''));
174
                        if (myset.size >= nb_autocomplete) break;
175
                    };
176
                    if (myset.size >= nb_autocomplete) break;
177
                };
178
                if (myset.size >= nb_autocomplete) break;
179
            }
180
        }
181
        return myset;
182
    };
183
184
    /*execute a function presses a key on the keyboard:*/
185
    inp.addEventListener("keydown", function(e) {
186
        var x = document.getElementById(this.id + "autocomplete-list");
187
        if (x) x = x.getElementsByTagName("div");
188
        if (e.keyCode == 40) { //DOWN
189
            /*If the arrow DOWN key is pressed,
190
            increase the currentFocus variable:*/
191
            currentFocus++;
192
            /*and and make the current item more visible:*/
193
            addActive(x);
194
        } else if (e.keyCode == 38) { //up
195
            /*If the arrow UP key is pressed,
196
            decrease the currentFocus variable:*/
197
            currentFocus--;
198
            /*and and make the current item more visible:*/
199
            addActive(x);
200
            e.preventDefault();
201
        } else if (e.keyCode == 13) {
202
            /*If the ENTER key is pressed, prevent the form from being submitted,*/
203
            //e.preventDefault();
204
            if (currentFocus > -1) {
205
                /*and simulate a click on the "active" item:*/
206
                if (x) x[currentFocus].click();
207
            }
208
        }
209
        /* press Esc clear all autocomplete */
210
        else if (e.keyCode == 27) {
211
            closeAllLists();
212
        }
213
        /* press Esc clear all autocomplete */
214
        else if (e.keyCode == 8) {
215
            closeAllLists();
216
        }
217
        /* press Tab clear all autocomplete */
218
        else if (e.keyCode == 9) {
219
            closeAllLists();
220
        };
221
    });
222
    function addActive(x) {
223
        /*a function to classify an item as "active":*/
224
        if (!x) return false;
225
        /*start by removing the "active" class on all items:*/
226
        removeActive(x);
227
        if (currentFocus >= x.length) currentFocus = 0;
228
        if (currentFocus < 0) currentFocus = (x.length - 1);
229
        /*add class "autocomplete-active":*/
230
        x[currentFocus].classList.add("autocomplete-active");
231
        inp.value = (x[currentFocus].textContent.replace(/<\/?[^>]+(>|$)/g, "")).trim();
232
    };
233
    function removeActive(x) {
234
        /*a function to remove the "active" class from all autocomplete items:*/
235
        for (var i = 0; i < x.length; i++) {
236
            x[i].classList.remove("autocomplete-active");
237
        };
238
    };
239
240
    function closeAllLists(elem) {
241
        /*close all autocomplete lists in the document with class autocomplete-items */
242
        var x = document.getElementsByClassName("autocomplete-items");
243
        for (var i = 0; i < x.length; i++) {
244
            x[i].parentNode.removeChild(x[i])
245
        };
246
    };
247
248
    /* div for one item autocomplete */
249
    function CreateDivItemAutocomplete (elem){
250
        var b = document.createElement("DIV");
251
        // add element ";
252
        b.innerHTML += elem;
253
        /*insert a input field that will hold the current array item's value:*/
254
        b.innerHTML += "<input type='hidden' value=''>";
255
        /*execute a function when someone clicks on the item value (DIV element):*/
256
        b.addEventListener("click", function(e) {
257
            /* insert the value for the autocomplete text field: */
258
            inp.value = this.getElementsByTagName("input")[0].value;
259
            /* normalyzer hightlight without tags */
260
            //inp.value = (inp.value.replace(/<\/?[^>]+(>|$)/g, "")).trim();
261
            inp.value = this.innerText;
262
263
            var autocommit = 1;
264
            const inputs = document.querySelectorAll("#advanced-search input[type='text']");
265
            for (var i = 0; i < inputs.length && autocommit; i++) {
266
                var input = inputs[i];
267
                if (input === inp) {
268
                    autocommit = 0;
269
                }
270
            }
271
            //Submit form click mouse in div if not in advanced search
272
            if (autocommit) this.closest("form").submit();
273
        });
274
        return b;
275
    };
276
277
    /*execute a function when someone clicks in the document:*/
278
    document.addEventListener("click", function (e) {
279
        closeAllLists(e.target);
280
    });
281
};
282
283
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 26-31 Link Here
26
[% Asset.css("css/print.css", { media = "print" }) | $raw %]
26
[% Asset.css("css/print.css", { media = "print" }) | $raw %]
27
[% INCLUDE intranetstylesheet.inc %]
27
[% INCLUDE intranetstylesheet.inc %]
28
[% IF ( bidi ) %][% Asset.css("css/right-to-left.css") | $raw %][% END %]
28
[% IF ( bidi ) %][% Asset.css("css/right-to-left.css") | $raw %][% END %]
29
<!-- Intranet inc CSS IntranetAutocompleteElasticSearch -->
30
[% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %]
31
    [% SET Optylesheet = 'elasticsearch/autocomplete.css' %]
32
    <link rel="stylesheet" type="text/css" href="[% interface | url %]/[% theme | url %]/css/[% Optylesheet | url %]" />
33
[% END %]
29
34
30
<script>
35
<script>
31
var Koha = {};
36
var Koha = {};
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/js_includes.inc (+4 lines)
Lines 171-174 Link Here
171
    });
171
    });
172
    </script>
172
    </script>
173
[% END %]
173
[% END %]
174
<!-- Intranet inc JS IntranetAutocompleteElasticSearch -->
175
[% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %]
176
[% Asset.js("js/elasticsearch/autocomplete.js") | $raw %]
177
[% END %]
174
<!-- / js_includes.inc -->
178
<!-- / js_includes.inc -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref (+17 lines)
Lines 298-300 Searching: Link Here
298
            - LIBRIS base URL
298
            - LIBRIS base URL
299
            - pref: LibrisURL
299
            - pref: LibrisURL
300
            - "Please only change this if you are sure it needs changing."
300
            - "Please only change this if you are sure it needs changing."
301
        -
302
            - pref: OPACAutocompleteElasticSearch
303
              type: boolean
304
              default: 0
305
              choices:
306
                  1: Show
307
                  0: "Don't show"
308
            - looking terms based on a provided text by using an ElasticSearch for OPAC.
309
        -
310
            - pref: IntranetAutocompleteElasticSearch
311
              type: boolean
312
              default: 0
313
              choices:
314
                  1: Show
315
                  0: "Don't show"
316
            - looking terms based on a provided text by using an ElasticSearch for Intranet.
317
        -
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt (+4 lines)
Lines 356-361 Link Here
356
            var dad  = line.parentNode;
356
            var dad  = line.parentNode;
357
            dad.appendChild(line.cloneNode(true));
357
            dad.appendChild(line.cloneNode(true));
358
            line.removeChild(ButtonPlus);
358
            line.removeChild(ButtonPlus);
359
            /* Intranet JS IntranetAutocompleteElasticSearch */
360
            [% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %]
361
                AutocompleteInitIntranet();
362
            [% END %]
359
        }
363
        }
360
364
361
        var Sticky;
365
        var Sticky;
(-)a/koha-tmpl/intranet-tmpl/prog/js/staff-global.js (+7 lines)
Lines 82-87 $(document).ready(function() { Link Here
82
    $(".keep_text").on("click",function(){
82
    $(".keep_text").on("click",function(){
83
        var field_index = $(this).parent().index();
83
        var field_index = $(this).parent().index();
84
        keep_text( field_index );
84
        keep_text( field_index );
85
        /* IntranetAutocompleteElasticSearch Tab */
86
        var tab = this.hash.substr(1, this.hash.length-1);
87
        /*  Koha.Preference('IntranetAutocompleteElasticSearch') == Show */
88
        if (typeof AutocompleteInitIntranet !== "undefined" && tab === 'catalog_search' ){
89
            AutocompleteInitIntranet();
90
        }
91
        $("#search-form").focus();
85
    });
92
    });
86
93
87
    $(".toggle_element").on("click",function(e){
94
    $(".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 23-28 Link Here
23
        [% SET opaclayoutstylesheet = 'opac.css' %]
23
        [% SET opaclayoutstylesheet = 'opac.css' %]
24
    [% END %]
24
    [% END %]
25
[% END %]
25
[% END %]
26
<!-- OPAC inc CSS OPACAutocompleteElasticSearch -->
27
[% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
28
    [% SET Optylesheet = 'opac-elasticsearch/opac-autocomplete.css' %]
29
    <link rel="stylesheet" type="text/css" href="[% interface | url %]/[% theme | url %]/css/[% Optylesheet | url %]" />
30
[% END %]
26
[% IF (opaclayoutstylesheet.match('^https?:|^\/')) %]
31
[% IF (opaclayoutstylesheet.match('^https?:|^\/')) %]
27
    <link rel="stylesheet" type="text/css" href="[% opaclayoutstylesheet | url %]" />
32
    <link rel="stylesheet" type="text/css" href="[% opaclayoutstylesheet | url %]" />
28
[% ELSE %]
33
[% ELSE %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc (+4 lines)
Lines 289-294 $(document).ready(function() { Link Here
289
});
289
});
290
</script>
290
</script>
291
[% PROCESS jsinclude %]
291
[% PROCESS jsinclude %]
292
<!-- OPAC *.inc JS OPACAutocompleteElasticSearch -->
293
[% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
294
    [% Asset.js("js/opac-elasticsearch/opac-autocomplete.js") | $raw %]
295
[% END %]
292
[% IF ( Koha.Preference('OPACUserJS') ) %]
296
[% IF ( Koha.Preference('OPACUserJS') ) %]
293
    <script>
297
    <script>
294
        [% Koha.Preference('OPACUserJS') | $raw %]
298
        [% Koha.Preference('OPACUserJS') | $raw %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-advsearch.tt (+8 lines)
Lines 538-543 $(document).ready(function() { Link Here
538
        $(newLine).find('.search-term-input select[name="op"]').first().prop("disabled",false).show();
538
        $(newLine).find('.search-term-input select[name="op"]').first().prop("disabled",false).show();
539
        newLine.find('input').val('');
539
        newLine.find('input').val('');
540
        thisLine.after(newLine);
540
        thisLine.after(newLine);
541
        /* OPAC JS OPACAutocompleteElasticSearch */
542
        [% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
543
            AutocompleteInitOpac();
544
        [% END %]
541
    });
545
    });
542
546
543
    $(document).on("click", '.ButtonLess', function(e) {
547
    $(document).on("click", '.ButtonLess', function(e) {
Lines 547-552 $(document).ready(function() { Link Here
547
        }
551
        }
548
        $(this).parent().parent().remove();
552
        $(this).parent().parent().remove();
549
        $('.search-term-row .search-term-input select[name="op"]').first().prop("disabled",true).hide();
553
        $('.search-term-row .search-term-input select[name="op"]').first().prop("disabled",true).hide();
554
        /* OPAC JS OPACAutocompleteElasticSearch */
555
        [% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
556
            AutocompleteInitOpac();
557
        [% END %]
550
    });
558
    });
551
559
552
</script>
560
</script>
(-)a/koha-tmpl/opac-tmpl/bootstrap/js/opac-elasticsearch/opac-autocomplete.js (+279 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
    /*execute a function when someone writes in the text field:*/
64
    var token_counter = 0;
65
    inp.addEventListener("input", function(e) {
66
        var a, val = this.value;
67
        /* var for async compare */
68
        var tmp_input = this.value.replace(/[+']/g, function(matched){
69
            return query_url_decode[matched];
70
        });
71
        token_counter++;
72
        currentFocus = -1;
73
        if (document.getElementsByClassName("autocomplete-items").length !== 0){
74
            a = document.getElementsByClassName("autocomplete-items")[0];
75
        } else {
76
            /*create a DIV element that will contain the items (values):*/
77
            a = document.createElement("DIV");
78
            a.setAttribute("id", this.id + "autocomplete-list");
79
            a.setAttribute("class", "autocomplete-items");
80
            /*append the DIV element as a child of the autocomplete container:*/
81
            this.parentNode.appendChild(a);
82
            /*append position absolute left/right:*/
83
            $(".autocomplete-items").css("left",left);
84
            $(".autocomplete-items").css("right",right);
85
        };
86
        /* get es_prefix key for builder */
87
        var chose_prefix = (select_idx == null || select_idx.length == 0) ? '' : GetValueIdx(select_idx, nb);
88
        chose_prefix = chose_prefix.replace(/([^,])[,-]([^,].*)?$/, '$1');
89
        if (chose_prefix !== null){
90
            var prefix = es_prefix[chose_prefix].toString();
91
            val = val.replace(/[+']/g, function(matched){
92
                return query_url_encode[matched];
93
            });
94
            if (tmp_input == '' || tmp_input == null){
95
                closeAllLists();
96
                token_counter = 0;
97
            } else {
98
                $.ajax({
99
                    type: 'GET',
100
                    url: url_request + val + '&key=' + key + '&prefix=' + prefix + '&token_counter=' + token_counter,
101
                    success: function (data) {
102
                        //console.log(data);
103
                        if (data.length !== 0){
104
                            var myset; //Set for Autocomplete unique
105
                            /* autocomplete for all prefix */
106
                            if (chose_prefix === 'kw' || chose_prefix === ''){
107
                                myset = GetSetAutocompleteAllIdx(data, prefix, key);
108
                            } else { // autocomplete for one prefix
109
                                myset = GetSetAutocompleteOneIdx(data, prefix, key);
110
                            };
111
                            /* append set to autocomplete */
112
                            if ( tmp_input + prefix == data['val'] + data['prefix'] && token_counter === parseInt(data['token_counter'], 10)){
113
                                a.innerHTML = "";
114
                                for (let item of myset){
115
                                    a.appendChild(CreateDivItemAutocomplete(item, val));
116
                                };
117
                            };
118
                        } else {
119
                            closeAllLists(this);
120
                        };
121
                    },
122
                    error: function (data) {
123
                        console.log(data);
124
                    },
125
                });
126
            }
127
128
        };
129
    });
130
    /* get value for tag with name idx */
131
    function GetValueIdx(elem, nb){
132
        switch (elem[0].tagName){
133
            case 'INPUT':
134
                return elem[0].value;
135
            case 'SELECT':
136
                return select_idx[nb].options[select_idx[nb].selectedIndex].value;
137
            default:
138
                return null;
139
        };
140
    };
141
    /* get autocomplete for only one prefix title/author/etc... */
142
    function GetSetAutocompleteOneIdx(data, prefix, key){
143
        let myset = new Set();
144
        let tmp_data = data['hits']['hits'];
145
        for (let i = 0; i < tmp_data.length; i++) {
146
            for (let j = 0; j < tmp_data[i]['highlight'][prefix + '.' + key].length; j++){
147
                /* div with data for autocomplete */
148
                let tmp = tmp_data[i]['highlight'][prefix + '.' + key][j];
149
                tmp = tmp.replace(/^\[/g, '');
150
                tmp = tmp.replace(/\]+$/g, '');
151
                myset.add(tmp.replace(/^[ &\/\\#,+)$~%.'":*?>{}!;]+|[ &\/\\#,+($~%.'":*?<{}!;]+$/g, ''));
152
                if (myset.size >= nb_autocomplete) break;
153
            };
154
            if (myset.size >= nb_autocomplete) break;
155
        };
156
        return myset;
157
    };
158
    /* get autocomplete for all prefix */
159
    function GetSetAutocompleteAllIdx(data, prefix, key){
160
        let myset = new Set();
161
        var pref = prefix.split(",");
162
        for (k = 0; k < Object.keys(data).length; k++){ //Object.keys(data).length
163
            if (data[k] != '' && data[k] != null){
164
                let tmp_data = data[k]['hits']['hits'];
165
                for (i = 0; i < tmp_data.length; i++) {
166
                    for (j = 0; j < tmp_data[i]['highlight'][pref[k] + '.' + key].length; j++){
167
                        /* div with data for autocomplete */
168
                        let tmp = tmp_data[i]['highlight'][pref[k] + '.' + key][j]
169
                        myset.add(tmp.replace(/[ &#,+()$~%.'":*?<{}!/;]+$/g, ''));
170
                        if (myset.size >= nb_autocomplete) break;
171
                    };
172
                    if (myset.size >= nb_autocomplete) break;
173
                };
174
                if (myset.size >= nb_autocomplete) break;
175
            }
176
        }
177
        return myset;
178
    };
179
180
    /*execute a function presses a key on the keyboard:*/
181
    inp.addEventListener("keydown", function(e) {
182
        var x = document.getElementById(this.id + "autocomplete-list");
183
        if (x) x = x.getElementsByTagName("div");
184
        if (e.keyCode == 40) { //DOWN
185
            /*If the arrow DOWN key is pressed,
186
            increase the currentFocus variable:*/
187
            currentFocus++;
188
            /*and and make the current item more visible:*/
189
            addActive(x);
190
        } else if (e.keyCode == 38) { //up
191
            /*If the arrow UP key is pressed,
192
            decrease the currentFocus variable:*/
193
            currentFocus--;
194
            /*and and make the current item more visible:*/
195
            addActive(x);
196
            e.preventDefault();
197
        } else if (e.keyCode == 13) {
198
            /*If the ENTER key is pressed, prevent the form from being submitted,*/
199
            //e.preventDefault();
200
            if (currentFocus > -1) {
201
                /*and simulate a click on the "active" item:*/
202
                if (x) x[currentFocus].click();
203
            }
204
        }
205
        /* press Esc clear all autocomplete */
206
        else if (e.keyCode == 27) {
207
            closeAllLists();
208
        }
209
        /* press Esc clear all autocomplete */
210
        else if (e.keyCode == 8) {
211
            closeAllLists();
212
        }
213
        /* press Tab clear all autocomplete */
214
        else if (e.keyCode == 9) {
215
            closeAllLists();
216
        };
217
    });
218
    function addActive(x) {
219
        /*a function to classify an item as "active":*/
220
        if (!x) return false;
221
        /*start by removing the "active" class on all items:*/
222
        removeActive(x);
223
        if (currentFocus >= x.length) currentFocus = 0;
224
        if (currentFocus < 0) currentFocus = (x.length - 1);
225
        /*add class "autocomplete-active":*/
226
        x[currentFocus].classList.add("autocomplete-active");
227
        inp.value = (x[currentFocus].textContent.replace(/<\/?[^>]+(>|$)/g, "")).trim();
228
    };
229
    function removeActive(x) {
230
        /*a function to remove the "active" class from all autocomplete items:*/
231
        for (var i = 0; i < x.length; i++) {
232
            x[i].classList.remove("autocomplete-active");
233
        };
234
    };
235
236
    function closeAllLists(elem) {
237
        /*close all autocomplete lists in the document with class autocomplete-items */
238
        var x = document.getElementsByClassName("autocomplete-items");
239
        for (var i = 0; i < x.length; i++) {
240
            x[i].parentNode.removeChild(x[i])
241
        };
242
    };
243
244
    /* div for one item autocomplete */
245
    function CreateDivItemAutocomplete (elem){
246
        var b = document.createElement("DIV");
247
        // add element ";
248
        b.innerHTML += elem;
249
        /*insert a input field that will hold the current array item's value:*/
250
        b.innerHTML += "<input type='hidden' value=''>";
251
        /*execute a function when someone clicks on the item value (DIV element):*/
252
        b.addEventListener("click", function(e) {
253
            /* insert the value for the autocomplete text field: */
254
            inp.value = this.getElementsByTagName("input")[0].value;
255
            /* normalyzer hightlight without tags */
256
            //inp.value = (inp.value.replace(/<\/?[^>]+(>|$)/g, "")).trim();
257
            inp.value = this.innerText;
258
259
            var autocommit = 1;
260
            const inputs = document.querySelectorAll("#booleansearch input[type='text']");
261
            for (var i = 0; i < inputs.length && autocommit; i++) {
262
                var input = inputs[i];
263
                if (input === inp) {
264
                    autocommit = 0;
265
                }
266
            }
267
            //Submit form click mouse in div if not in advanced search
268
            if (autocommit) this.closest("form").submit();
269
        });
270
        return b;
271
    };
272
273
    /*execute a function when someone clicks in the document:*/
274
    document.addEventListener("click", function (e) {
275
        closeAllLists(e.target);
276
    });
277
};
278
279
AutocompleteInitOpac();
(-)a/opac/svc/elasticsearch/opac-elasticsearch.pl (+162 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use strict;
4
use warnings;
5
use CGI qw ( -utf8 );
6
use JSON;
7
use utf8;
8
use Unicode::Normalize;
9
use CGI::Session;
10
use Koha::SearchEngine::Elasticsearch::Browse;
11
12
use Koha::Items;
13
use C4::Context;
14
use C4::Biblio qw ( GetMarcBiblio );
15
16
my $browser = Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } );
17
my $cgi = CGI->new;
18
my $session = CGI::Session->load() or die CGI::Session->errstr();
19
20
$session->param(-name=>'analyzer', -value=>"autocomplete");
21
$session->param(-name=>'prefix', -value=>$cgi->multi_param("prefix"));
22
$session->param(-name=>'q', -value=>$cgi->multi_param("q"));
23
$session->param(-name=>'key', -value=>$cgi->multi_param("key"));
24
$session->param(-name=>'token_counter', -value=>$cgi->multi_param("token_counter"));
25
$session->expire('+1h');
26
27
my $iskey = 0;
28
29
if ($session->param("key") eq "autocomplete") {
30
  $iskey = 1;
31
  my @prefix = split /,/, $session->param("prefix");
32
  #fields for autocomplete
33
  my $length = scalar @prefix;
34
  my $ses = NFKD( $session->param("q") );
35
  $ses =~ s/\p{NonspacingMark}//g;
36
37
    #search by many prefix fields
38
  if ($length > 1){
39
    my $res = $browser->autocomplete_idx($ses, \@prefix, $session->param("analyzer"), $session->param("token_counter"));
40
41
    if (C4::Context->preference('OpacSuppression') || C4::Context->yaml_preference('OpacHiddenItems')) {
42
      my @prefix = $res->{ "prefix" };
43
      @prefix = split(',', $prefix[0]);
44
45
      for (my $i = 0; $i < scalar @prefix; $i++) {
46
        filterAutocomplete($res->{ $i }->{ 'hits' });
47
      }
48
    }
49
50
    print $cgi->header("application/json");
51
    print to_json($res);
52
  }
53
  #search by one prefix field
54
  elsif ($length == 1) {
55
    my $res = $browser->autocomplete_one_idx($ses, $prefix[0], $session->param("analyzer"), $session->param("token_counter"));
56
57
    if (C4::Context->preference('OpacSuppression') || C4::Context->yaml_preference('OpacHiddenItems')) {
58
      filterAutocomplete($res->{ 'hits' });
59
    }
60
61
    print $cgi->header("application/json");
62
    print to_json($res);
63
  }
64
  #no prefix 404
65
  else {
66
    response404JSON();
67
  }
68
}
69
70
if ($iskey == 0) {
71
  response404JSON();
72
}
73
74
sub filterAutocomplete {
75
  my $hits = $_[0];
76
  my $hitlist = $hits->{ "hits" };
77
  if (@{$hitlist}) {
78
    # Remove item inside hits in elasticsearch response if the item has
79
    # marc field 942$n set to true and OpacSuppression preference on
80
    if (C4::Context->preference('OpacSuppression')) {
81
      for ( my $i = 0; $i < scalar @{$hitlist}; $i++ ) {
82
        my $record = GetMarcBiblio({
83
          biblionumber => $hitlist->[$i]->{ "_id" },
84
          opac         => 1
85
        });
86
        my $opacsuppressionfield = '942';
87
        my $opacsuppressionfieldvalue = $record->field($opacsuppressionfield);
88
        if ( $opacsuppressionfieldvalue &&
89
             $opacsuppressionfieldvalue->subfield("n") &&
90
             $opacsuppressionfieldvalue->subfield("n") == 1) {
91
          # if OPAC suppression by IP address
92
          if (C4::Context->preference('OpacSuppressionByIPRange')) {
93
            my $IPAddress = $ENV{'REMOTE_ADDR'};
94
            my $IPRange = C4::Context->preference('OpacSuppressionByIPRange');
95
            if ($IPAddress !~ /^$IPRange/)  {
96
                splice(@{$hitlist}, $i, 1);
97
                $i--;
98
                $hits->{ "total" }--;
99
            }
100
          } else {
101
            splice(@{$hitlist}, $i, 1);
102
            $i--;
103
            $hits->{ "total" }--;
104
          }
105
        }
106
      }
107
    }
108
    # Remove item inside hits in elasticsearch response if the item is
109
    # declared hidden in OPACHiddenItems preference
110
    if (C4::Context->yaml_preference('OpacHiddenItems')) {
111
      my @biblionumbers;
112
      foreach (@{$hitlist}) {
113
        push(@biblionumbers, $_->{ "_id" });
114
      }
115
      my $autocomplete_items = Koha::Items->search({
116
        biblionumber => { -in => \@biblionumbers }
117
      });
118
      my $filtered_items = $autocomplete_items->filter_by_visible_in_opac({
119
        patron => undef
120
      });
121
      for ( my $i = 0; $i < scalar @{$hitlist}; $i++ ) {
122
        my $item = $filtered_items->find({
123
          biblionumber => $hitlist->[$i]->{ "_id" }
124
        });
125
        if (!$item) {
126
          splice(@{$hitlist}, $i, 1);
127
          $i--;
128
          $hits->{ "total" }--;
129
        }
130
      }
131
    }
132
    # Adjust the max_score inside hits in elasticsearch response
133
    my $maxscore = 0;
134
    foreach ( @{$hitlist} ) {
135
      my $score = $_->{"_score"};
136
      $maxscore = $score if ($maxscore < $score);
137
    }
138
    if ($maxscore == 0) {
139
      $hits->{ "max_score" } = undef;
140
    } else {
141
      $hits->{ "max_score" } = $maxscore;
142
    }
143
  }
144
}
145
146
sub response404JSON {
147
  my $res = CGI->new;
148
  my $json = JSON->new->utf8;
149
  my $header_type = "application/json";
150
  my $header_status = "404";
151
  my $output = $json->encode({
152
    "error" => "No data",
153
    "description" => "Bad request",
154
  });
155
  print $res->header(
156
    -type => $header_type,
157
    -charset => "utf-8",
158
    -status => $header_status
159
  );
160
  print $output;
161
  print "\n";
162
}
(-)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