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 161-166 sub _build_query { Link Here
161
    return $query;
161
    return $query;
162
}
162
}
163
163
164
=head2 autocomplete_one_idx
165
166
    my $query = $self->autocomplete_one_idx($cgi_q, $prefix, $analyzer, $token_counter);
167
168
Does a prefix search for C<$prefix> (only one prefix), looking for C<$cgi_q> , using analyzer C<$analyzer> ,
169
C<$token_counter> is used for identify which word to use in autocomplete
170
171
=cut
172
173
=head3 Returns
174
175
This returns an arrayref of hashrefs with highlights. Each hashref contains a "text" element that contains the field as returned.
176
177
=cut
178
179
sub autocomplete_one_idx {
180
    my ($self, $cgi_q, $prefix, $analyzer, $token_counter) = @_;
181
    my @source;
182
    my $elasticsearch = $self->get_elasticsearch();
183
    my $query = $self->_build_query_autocomplete($cgi_q, $prefix, $analyzer);
184
    my $res = $elasticsearch->search(
185
        index => $self->index_name,
186
        body => $query
187
    );
188
    $res->{'val'} = $cgi_q;
189
    $res->{'prefix'} = $prefix;
190
    $res->{'token_counter'} = $token_counter;
191
192
  return $res;
193
}
194
195
=head2 autocomplete_idx
196
197
    my $query = $self->autocomplete_idx($cgi_q, $prefix, $analyzer, $token_counter);
198
199
Does a prefix search for C<$prefix> (many prefix), looking for C<$cgi_q>, using analyzer C<$analyzer>,
200
C<$token_counter> is used for identify which word to use in autocomplete
201
202
=cut
203
204
=head3 Returns
205
206
This returns an arrayref for all prefix of hashrefs with highlights. Each hashref contains a "text" element
207
that contains the field as returned.
208
209
=cut
210
211
sub autocomplete_idx {
212
  my ($self, $cgi_q, $prefix, $analyzer, $token_counter) = @_;
213
  my %results;
214
  my $idx = 0;
215
  foreach my $pref ( @$prefix ) {
216
      $results{$idx} = $self->autocomplete_one_idx($cgi_q, $pref, $analyzer, $token_counter);
217
      $idx++;
218
  }
219
  $results{'val'} = $cgi_q;
220
  $results{'prefix'} = join( ',', @$prefix );
221
  $results{'token_counter'} = $token_counter;
222
  return \%results;
223
}
224
225
=head2 _build_query_autocomplete
226
227
    my $query = $self->_build_query_autocomplete($cgi_q, $prefix, $analyzer);
228
229
Arguments:
230
231
=over 4
232
233
=item cgi_q
234
235
GET request
236
237
=item prefix
238
239
Field(s) for autocomplete (title, author, etc...)
240
241
=item analyzer
242
243
Name of analyzer wich we use for autocomplete
244
245
=back
246
247
=cut
248
249
=head3 Returns
250
251
This returns an arrayref for all prefix of hashrefs with highlights. Each hashref contains a "text" element
252
that contains the field as returned.
253
254
=cut
255
256
sub _build_query_autocomplete {
257
    my ($self, $cgi_q, $prefix, $analyzer) = @_;
258
    my (@source);
259
    #prefix + analyzer
260
    my $prefix_analyzer = $prefix . '.' . $analyzer;
261
    # we can change these variables
262
    my ($nb_fragments, $size_fragment, $pre_tags, $post_tags) = (1, 100, ["<strong>"], ["</strong>"]);
263
    push(@source, $prefix);
264
    my $query = {
265
        _source    => \@source,
266
        query => {
267
            match => {
268
                $prefix_analyzer    => {
269
                    query => $cgi_q,
270
                    operator => 'and'
271
                }
272
            }
273
        },
274
        highlight => {
275
            number_of_fragments => $nb_fragments,
276
            fragment_size => $size_fragment,
277
            pre_tags => $pre_tags,
278
            post_tags => $post_tags,
279
            fields => {
280
                $prefix_analyzer => {}
281
            }
282
        }
283
    };
284
    return $query;
285
}
286
164
1;
287
1;
165
288
166
__END__
289
__END__
Lines 171-176 __END__ Link Here
171
294
172
=item Robin Sheat << <robin@catalyst.net.nz> >>
295
=item Robin Sheat << <robin@catalyst.net.nz> >>
173
296
297
=item Ivan Dziuba << <ivan.dziuba@inlibro.com> >>
298
174
=back
299
=back
175
300
176
=cut
301
=cut
(-)a/admin/searchengine/elasticsearch/field_config.yaml (+4 lines)
Lines 52-57 search: Link Here
52
        type: text
52
        type: text
53
        analyzer: analyzer_phrase
53
        analyzer: analyzer_phrase
54
        search_analyzer: analyzer_phrase
54
        search_analyzer: analyzer_phrase
55
      autocomplete:
56
        type: text
57
        analyzer: autocomplete
58
        search_analyzer: standard
55
      raw:
59
      raw:
56
        type: keyword
60
        type: keyword
57
        normalizer: nfkc_cf_normalizer
61
        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 337-342 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
337
('IndependentBranchesPatronModifications','0', NULL, 'Show only modification request for the logged in branch','YesNo'),
337
('IndependentBranchesPatronModifications','0', NULL, 'Show only modification request for the logged in branch','YesNo'),
338
('IndependentBranchesTransfers','0', NULL, 'Allow non-superlibrarians to transfer items between libraries','YesNo'),
338
('IndependentBranchesTransfers','0', NULL, 'Allow non-superlibrarians to transfer items between libraries','YesNo'),
339
('IntranetAddMastheadLibraryPulldown','0', NULL, 'Add a library select pulldown menu on the staff header search','YesNo'),
339
('IntranetAddMastheadLibraryPulldown','0', NULL, 'Add a library select pulldown menu on the staff header search','YesNo'),
340
('IntranetAutocompleteElasticSearch','0',NULL,'If ON, show search suggestions in the staff interface when using Elasticsearch.','YesNo'),
340
('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'),
341
('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'),
341
('intranetbookbag','1','','If ON, enables display of Cart feature in the intranet','YesNo'),
342
('intranetbookbag','1','','If ON, enables display of Cart feature in the intranet','YesNo'),
342
('IntranetCatalogSearchPulldown','0', NULL, 'Show a search field pulldown for \"Search the catalog\" boxes','YesNo'),
343
('IntranetCatalogSearchPulldown','0', NULL, 'Show a search field pulldown for \"Search the catalog\" boxes','YesNo'),
Lines 472-477 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
472
('OPACAmazonCoverImages','0','','Display cover images on OPAC from Amazon Web Services','YesNo'),
473
('OPACAmazonCoverImages','0','','Display cover images on OPAC from Amazon Web Services','YesNo'),
473
('OPACAuthorIdentifiersAndInformation', '', '', 'Display author information on the OPAC detail page','multiple_sortable'),
474
('OPACAuthorIdentifiersAndInformation', '', '', 'Display author information on the OPAC detail page','multiple_sortable'),
474
('OpacAuthorities','1',NULL,'If ON, enables the search authorities link on OPAC','YesNo'),
475
('OpacAuthorities','1',NULL,'If ON, enables the search authorities link on OPAC','YesNo'),
476
('OPACAutocompleteElasticSearch','0',NULL,'If ON, show search suggestions in the OPAC when using Elasticsearch.','YesNo'),
475
('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'),
477
('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'),
476
('opacbookbag','1','','If ON, enables display of Cart feature','YesNo'),
478
('opacbookbag','1','','If ON, enables display of Cart feature','YesNo'),
477
('OpacBrowser','0',NULL,'If ON, enables subject authorities browser on OPAC (needs to set misc/cronjob/sbuild_browser_and_cloud.pl)','YesNo'),
479
('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 31-36 Link Here
31
[% Asset.css("css/print.css", { media = "print" }) | $raw %]
31
[% Asset.css("css/print.css", { media = "print" }) | $raw %]
32
[% INCLUDE intranetstylesheet.inc %]
32
[% INCLUDE intranetstylesheet.inc %]
33
[% IF ( bidi ) %][% Asset.css("css/right-to-left.css") | $raw %][% END %]
33
[% IF ( bidi ) %][% Asset.css("css/right-to-left.css") | $raw %][% END %]
34
<!-- Intranet inc CSS IntranetAutocompleteElasticSearch -->
35
[% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %]
36
    [% SET Optylesheet = 'elasticsearch/autocomplete.css' %]
37
    <link rel="stylesheet" type="text/css" href="[% interface | url %]/[% theme | url %]/css/[% Optylesheet | url %]" />
38
[% END %]
34
<script type="module">
39
<script type="module">
35
    import { APIClient } from "/intranet-tmpl/prog/js/fetch/api-client.js";
40
    import { APIClient } from "/intranet-tmpl/prog/js/fetch/api-client.js";
36
    window.APIClient = APIClient;
41
    window.APIClient = APIClient;
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/js_includes.inc (+4 lines)
Lines 94-97 Link Here
94
[% IF Koha.Preference( 'CookieConsent' ) %]
94
[% IF Koha.Preference( 'CookieConsent' ) %]
95
    [% Asset.js("js/cookieconsent.js") | $raw %]
95
    [% Asset.js("js/cookieconsent.js") | $raw %]
96
[% END %]
96
[% END %]
97
<!-- Intranet inc JS IntranetAutocompleteElasticSearch -->
98
[% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %]
99
[% Asset.js("js/elasticsearch/autocomplete.js") | $raw %]
100
[% END %]
97
<!-- / js_includes.inc -->
101
<!-- / js_includes.inc -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref (+17 lines)
Lines 383-385 Searching: Link Here
383
            - pref: LibrisURL
383
            - pref: LibrisURL
384
              class: url
384
              class: url
385
            - "Please only change this if you are sure it needs changing."
385
            - "Please only change this if you are sure it needs changing."
386
        -
387
            - pref: OPACAutocompleteElasticSearch
388
              type: boolean
389
              default: 0
390
              choices:
391
                  1: Show
392
                  0: "Don't show"
393
            - looking terms based on a provided text by using an ElasticSearch for OPAC.
394
        -
395
            - pref: IntranetAutocompleteElasticSearch
396
              type: boolean
397
              default: 0
398
              choices:
399
                  1: Show
400
                  0: "Don't show"
401
            - looking terms based on a provided text by using an ElasticSearch for Intranet.
402
        -
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt (+4 lines)
Lines 393-398 Link Here
393
            var dad  = line.parentNode;
393
            var dad  = line.parentNode;
394
            dad.appendChild(line.cloneNode(true));
394
            dad.appendChild(line.cloneNode(true));
395
            line.removeChild(ButtonPlus);
395
            line.removeChild(ButtonPlus);
396
            /* Intranet JS IntranetAutocompleteElasticSearch */
397
            [% IF ( Koha.Preference('IntranetAutocompleteElasticSearch') ) %]
398
                AutocompleteInitIntranet();
399
            [% END %]
396
        }
400
        }
397
401
398
        $(document).ready(function() {
402
        $(document).ready(function() {
(-)a/koha-tmpl/intranet-tmpl/prog/js/staff-global.js (+7 lines)
Lines 199-204 $(document).ready(function () { Link Here
199
    $("#header_search .nav-tabs a").on("click", function () {
199
    $("#header_search .nav-tabs a").on("click", function () {
200
        var field_index = $(this).parent().index();
200
        var field_index = $(this).parent().index();
201
        keep_text(field_index);
201
        keep_text(field_index);
202
        /* IntranetAutocompleteElasticSearch Tab */
203
        var tab = this.hash.substr(1, this.hash.length-1);
204
        /*  Koha.Preference('IntranetAutocompleteElasticSearch') == Show */
205
        if (typeof AutocompleteInitIntranet !== "undefined" && tab === 'catalog_search' ){
206
            AutocompleteInitIntranet();
207
        }
208
        $("#search-form").focus();
202
    });
209
    });
203
210
204
    $(".toggle_element").on("click", function (e) {
211
    $(".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 25-30 Link Here
25
        [% SET opaclayoutstylesheet = 'opac.css' %]
25
        [% SET opaclayoutstylesheet = 'opac.css' %]
26
    [% END %]
26
    [% END %]
27
[% END %]
27
[% END %]
28
<!-- OPAC inc CSS OPACAutocompleteElasticSearch -->
29
[% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
30
    [% SET Optylesheet = 'opac-elasticsearch/opac-autocomplete.css' %]
31
    <link rel="stylesheet" type="text/css" href="[% interface | url %]/[% theme | url %]/css/[% Optylesheet | url %]" />
32
[% END %]
28
[% IF (opaclayoutstylesheet.match('^https?:|^\/')) %]
33
[% IF (opaclayoutstylesheet.match('^https?:|^\/')) %]
29
    <link rel="stylesheet" type="text/css" href="[% opaclayoutstylesheet | url %]" />
34
    <link rel="stylesheet" type="text/css" href="[% opaclayoutstylesheet | url %]" />
30
[% ELSE %]
35
[% ELSE %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-bottom.inc (+4 lines)
Lines 194-199 Link Here
194
[% INCLUDE 'js-date-format.inc' %]
194
[% INCLUDE 'js-date-format.inc' %]
195
[% INCLUDE 'js-biblio-format.inc' %]
195
[% INCLUDE 'js-biblio-format.inc' %]
196
[% PROCESS jsinclude %]
196
[% PROCESS jsinclude %]
197
<!-- OPAC *.inc JS OPACAutocompleteElasticSearch -->
198
[% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
199
    [% Asset.js("js/opac-elasticsearch/opac-autocomplete.js") | $raw %]
200
[% END %]
197
[% IF ( Koha.Preference('OPACUserJS') ) %]
201
[% IF ( Koha.Preference('OPACUserJS') ) %]
198
    <script>
202
    <script>
199
        [% Koha.Preference('OPACUserJS') | $raw %]
203
        [% Koha.Preference('OPACUserJS') | $raw %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-advsearch.tt (+8 lines)
Lines 612-617 Link Here
612
                $(newLine).find('.search-term-input select[name="op"]').first().prop("disabled",false).show();
612
                $(newLine).find('.search-term-input select[name="op"]').first().prop("disabled",false).show();
613
                newLine.find('input').val('');
613
                newLine.find('input').val('');
614
                thisLine.after(newLine);
614
                thisLine.after(newLine);
615
                /* OPAC JS OPACAutocompleteElasticSearch */
616
                [% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
617
                    AutocompleteInitOpac();
618
                [% END %]
615
            });
619
            });
616
620
617
            $(document).on("click", '.ButtonLess', function(e) {
621
            $(document).on("click", '.ButtonLess', function(e) {
Lines 621-626 Link Here
621
                }
625
                }
622
                $(this).parent().parent().remove();
626
                $(this).parent().parent().remove();
623
                $('.search-term-row .search-term-input select[name="op"]').first().prop("disabled",true).hide();
627
                $('.search-term-row .search-term-input select[name="op"]').first().prop("disabled",true).hide();
628
                /* OPAC JS OPACAutocompleteElasticSearch */
629
                [% IF ( Koha.Preference('OPACAutocompleteElasticSearch') ) %]
630
                    AutocompleteInitOpac();
631
                [% END %]
624
            });
632
            });
625
    </script>
633
    </script>
626
[% END %]
634
[% 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 77-80 subtest "_build_query tests" => sub { Link Here
77
    );
77
    );
78
};
78
};
79
79
80
subtest "_build_query_autocomplete tests" => sub {
81
    plan tests => 1;
82
83
    my $browse = Koha::SearchEngine::Elasticsearch::Browse->new({index=>'dummy'});
84
85
    my $q = $browse->_build_query_autocomplete('a', 'title', 'autocomplete');
86
87
    is_deeply($q, {
88
        _source    => ["title"],
89
        query => {
90
            match => {
91
                "title.autocomplete"  => {
92
                    query => 'a',
93
                    operator => 'and'
94
                }
95
            }
96
        },
97
        highlight => {
98
            number_of_fragments => 1,
99
            fragment_size => 100,
100
            pre_tags => ["<strong>"],
101
            post_tags => ["</strong>"],
102
            fields => {
103
                "title.autocomplete" => {}
104
            }
105
        }
106
    }, 'Autocomplete for title is specified');
107
108
};
109
80
done_testing();
110
done_testing();
81
- 

Return to bug 27113