From 9254b1ee419e77289d789d9f710d910a7d480257 Mon Sep 17 00:00:00 2001 From: Ivan Dziuba Date: Thu, 3 Dec 2020 16:24:48 -0500 Subject: [PATCH] Bug 27113: ElasticSearch: Autocomplete in input search Bug 27113: ElasticSearch: Autocomplete in input search. Fix bugs. Bug 27113: ElasticSearch: Autocomplete in input search. Use Unicode::Normalize instead Text::Unaccent. --- Koha/SearchEngine/Elasticsearch/Browse.pm | 64 +++++ .../elasticsearch/field_config.yaml | 4 + .../elasticsearch/index_config.yaml | 14 + api/elasticsearch/autocomplete.pl | 69 +++++ ...asticsearch_autocomplete_input_search.perl | 8 + installer/data/mysql/mandatory/sysprefs.sql | 3 +- .../js/elasticsearch/autocomplete.js | 270 ++++++++++++++++++ .../prog/css/elasticsearch/autocomplete.css | 28 ++ .../prog/en/includes/doc-head-close.inc | 5 + .../prog/en/includes/js_includes.inc | 4 + .../modules/admin/preferences/searching.pref | 9 + .../prog/en/modules/catalogue/advsearch.tt | 10 +- .../intranet-tmpl/prog/js/staff-global.js | 6 + .../opac-elasticsearch/opac-autocomplete.css | 29 ++ .../bootstrap/en/includes/doc-head-close.inc | 5 + .../bootstrap/en/includes/opac-bottom.inc | 4 + .../bootstrap/en/modules/opac-advsearch.tt | 8 + .../opac-elasticsearch/opac-autocomplete.js | 265 +++++++++++++++++ opac/svc/elasticsearch/opac-autocomplete.pl | 71 +++++ 19 files changed, 870 insertions(+), 6 deletions(-) create mode 100755 api/elasticsearch/autocomplete.pl create mode 100644 installer/data/mysql/atomicupdate/bug_27113-elasticsearch_autocomplete_input_search.perl create mode 100644 koha-tmpl/intranet-tmpl/js/elasticsearch/autocomplete.js create mode 100644 koha-tmpl/intranet-tmpl/prog/css/elasticsearch/autocomplete.css create mode 100644 koha-tmpl/opac-tmpl/bootstrap/css/opac-elasticsearch/opac-autocomplete.css create mode 100644 koha-tmpl/opac-tmpl/bootstrap/js/opac-elasticsearch/opac-autocomplete.js create mode 100755 opac/svc/elasticsearch/opac-autocomplete.pl diff --git a/Koha/SearchEngine/Elasticsearch/Browse.pm b/Koha/SearchEngine/Elasticsearch/Browse.pm index 85070730a7..57c0a74023 100644 --- a/Koha/SearchEngine/Elasticsearch/Browse.pm +++ b/Koha/SearchEngine/Elasticsearch/Browse.pm @@ -162,6 +162,70 @@ sub _build_query { return $query; } +sub autocomplete_one_idx { + my ($self, $cgi_q, $prefix, $analyzer, $token_counter) = @_; + my @source; + my $elasticsearch = $self->get_elasticsearch(); + # we can change these variables + my ($nb_fragments, $size_fragment, $pre_tags, $post_tags) = (1, 100, [""], [""]); + + my $query = $self->_build_query_autocomplete($cgi_q, $prefix, $analyzer); + my $res = $elasticsearch->search( + index => $self->index_name, + body => $query + ); + $res->{'val'} = $cgi_q; + $res->{'prefix'} = $prefix; + $res->{'token_counter'} = $token_counter; + + return $res; +} + +sub autocomplete_idx { + my ($self, $cgi_q, $prefix, $analyzer, $token_counter) = @_; + my %results; + my $idx = 0; + foreach my $pref ( @$prefix ) { + $results{$idx} = $self->autocomplete_one_idx($cgi_q, $pref, $analyzer, $token_counter); + $idx++; + } + $results{'val'} = $cgi_q; + $results{'prefix'} = join( ',', @$prefix ); + $results{'token_counter'} = $token_counter; + return \%results; +} + +sub _build_query_autocomplete { + my ($self, $cgi_q, $prefix, $analyzer) = @_; + my (@source); + #prefix + analyzer + my $prefix_analyzer = $prefix . '.' . $analyzer; + # we can change these variables + my ($nb_fragments, $size_fragment, $pre_tags, $post_tags) = (1, 100, [""], [""]); + push(@source, $prefix); + my $query = { + _source => \@source, + query => { + match => { + $prefix_analyzer => { + query => $cgi_q, + operator => 'and' + } + } + }, + highlight => { + number_of_fragments => $nb_fragments, + fragment_size => $size_fragment, + pre_tags => $pre_tags, + post_tags => $post_tags, + fields => { + $prefix_analyzer => {} + } + } + }; + return $query; +} + 1; __END__ diff --git a/admin/searchengine/elasticsearch/field_config.yaml b/admin/searchengine/elasticsearch/field_config.yaml index d78bdf06dd..cae35c9585 100644 --- a/admin/searchengine/elasticsearch/field_config.yaml +++ b/admin/searchengine/elasticsearch/field_config.yaml @@ -47,6 +47,10 @@ search: type: text analyzer: analyzer_phrase search_analyzer: analyzer_phrase + autocomplete: + type: text + analyzer: autocomplete + search_analyzer: standard raw: type: keyword normalizer: nfkc_cf_normalizer diff --git a/admin/searchengine/elasticsearch/index_config.yaml b/admin/searchengine/elasticsearch/index_config.yaml index b8b2ac6071..ab1e5a631b 100644 --- a/admin/searchengine/elasticsearch/index_config.yaml +++ b/admin/searchengine/elasticsearch/index_config.yaml @@ -2,6 +2,14 @@ # Index configuration that defines how different analyzers work. index: analysis: + tokenizer: + autocomplete_tokenizer: + type: edge_ngram + min_gram: 1 + max_gram: 10 + token_chars: + - letter + - digit analyzer: # Phrase analyzer is used for phrases (exact phrase match) analyzer_phrase: @@ -10,6 +18,12 @@ index: - icu_folding char_filter: - punctuation + autocomplete: + type: custom + filter: + - icu_folding + - lowercase + tokenizer: autocomplete_tokenizer analyzer_standard: tokenizer: icu_tokenizer filter: diff --git a/api/elasticsearch/autocomplete.pl b/api/elasticsearch/autocomplete.pl new file mode 100755 index 0000000000..babcbb7335 --- /dev/null +++ b/api/elasticsearch/autocomplete.pl @@ -0,0 +1,69 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use CGI qw ( -utf8 ); +use v5.10; +use JSON; +use Switch; +use utf8; +use Text::Unaccent; +use CGI::Session; +use Koha::SearchEngine::Elasticsearch::Browse; + +my $browser = Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } ); +my $cgi = CGI->new; +my $session = CGI::Session->new(); + +$session->param(-name=>'analyzer', -value=>"autocomplete"); +$session->param(-name=>'prefix', -value=>$cgi->param("prefix")); +$session->param(-name=>'q', -value=>$cgi->param("q")); +$session->param(-name=>'key', -value=>$cgi->param("key")); +$session->param(-name=>'token_counter', -value=>$cgi->param("token_counter")); +$session->expire('+1h'); + +#Chose GET in key parametre +given ($session->param("key")) { + when ("autocomplete") { + my @prefix = split /,/, $session->param("prefix"); + #fields for autocomplete + my $length = scalar @prefix; + #search by many prefix fields + if ($length > 1){ + my $res = $browser->autocomplete_idx(unac_string("UTF-8", $session->param("q")), \@prefix, $session->param("analyzer"), $session->param("token_counter")); + print $cgi->header("application/json"); + print to_json($res); + } + #search by one prefix field + elsif ($length == 1) { + my $res = $browser->autocomplete_one_idx(unac_string("UTF-8", $session->param("q")), $prefix[0], $session->param("analyzer"), $session->param("token_counter")); + print $cgi->header("application/json"); + print to_json($res); + } + #no prefix 404 + else { + response404JSON(); + } + } + #no key 404 + default { + response404JSON(); + } +} + +sub response404JSON { + my $json = JSON->new->utf8; + my $header_type = "application/json"; + my $header_status = "404"; + my $output = $json->encode({ + "error" => "No data", + "description" => "Bad request", + }); + print $cgi->header( + -type => $header_type, + -charset => "utf-8", + -status => $header_status + ); + print $output; + print "\n"; +} diff --git a/installer/data/mysql/atomicupdate/bug_27113-elasticsearch_autocomplete_input_search.perl b/installer/data/mysql/atomicupdate/bug_27113-elasticsearch_autocomplete_input_search.perl new file mode 100644 index 0000000000..cdfc4bf876 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_27113-elasticsearch_autocomplete_input_search.perl @@ -0,0 +1,8 @@ +$DBversion = 'XXX'; # will be replaced by the RM +if( CheckVersion( $DBversion ) ) { + # you can use $dbh here like: + $dbh->do(q{INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('AutocompleteElasticSearch', '0', NULL, NULL, 'YesNo')}); + + # Always end with this (adjust the bug info) + NewVersion( $DBversion, 27113, "Autocomplete with elasticsearch"); +} diff --git a/installer/data/mysql/mandatory/sysprefs.sql b/installer/data/mysql/mandatory/sysprefs.sql index ef7d4561fb..f9a20ee3c0 100644 --- a/installer/data/mysql/mandatory/sysprefs.sql +++ b/installer/data/mysql/mandatory/sysprefs.sql @@ -733,5 +733,6 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('XSLTListsDisplay','default','','Enable XSLT stylesheet control over lists pages display on intranet','Free'), ('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'), ('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'), -('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo') +('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo'), +('AutocompleteElasticSearch','0',NULL,NULL,'YesNo') ; diff --git a/koha-tmpl/intranet-tmpl/js/elasticsearch/autocomplete.js b/koha-tmpl/intranet-tmpl/js/elasticsearch/autocomplete.js new file mode 100644 index 0000000000..2c021a53e3 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/js/elasticsearch/autocomplete.js @@ -0,0 +1,270 @@ +/* OPAC JS file AutocompleteElasticSearch */ +/* prefix for search ES */ +var es_prefix = { + 'au': 'author', + 'pb': 'publisher', + 'se': 'title-series', + 'su': 'subject', + 'ti': 'title-cover', + /* for all */ + '': ['title-cover', 'author', 'subject', 'title-series', 'publisher'], + 'kw': ['title-cover', 'author', 'subject', 'title-series', 'publisher'] +}; + +/* stop class for elements name=["q"] */ +var stop_class_input = ["form-field-value"]; + +var url_request = '/cgi-bin/koha/api/elasticsearch/autocomplete.pl?q='; +/* query for elasticsearch encode*/ +var query_url_encode = { + '\'':'%5C%27', /* \\' for decode */ + '+': '' +}; +/* query for elasticsearch decode*/ +var query_url_decode = { + '\'':"\\'", + '+': '' +}; +/* count of lines for autocomplete */ +var nb_autocomplete = 10; +/* key API */ +var key = 'autocomplete'; + +function AutocompleteInitIntranet(){ + /* vars for class position absolute autocomplete */ + var left = "0px"; + var right = "0px"; + var top = ""; + /* get all input name q for search */ + var input_q = document.getElementsByName("q"); + for (var nb = 0; nb < input_q.length; nb++){ + /* addEventListener for every 'input' */ + if (!stop_class_input.includes(input_q[nb].className)){ + autocomplete(input_q[nb], nb, left, right, top); + } + }; +}; + +function autocomplete(inp, nb, left, right) { + var select_idx = document.getElementsByName("idx"); + /* autocomplete off for input */ + inp.setAttribute("autocomplete", "off"); + /* get parent of input */ + var parent_inp = $(inp).parent(); + /* get element after input */ + var next_elem_inp = inp.nextElementSibling; + /* create new div with position relative for class .autocomplete with absolute */ + var div_relative = document.createElement('div'); + $(div_relative).addClass( "autocomplete" ); + div_relative.append(inp); + /* input doesn't have an elem after, add it to parent */ + if (next_elem_inp === null){ + parent_inp.append( div_relative ); + } else { // input has an elem after, add elem after it + next_elem_inp.before(div_relative); + }; + var currentFocus; + /*execute a function when someone writes in the text field:*/ + var token_counter = 0; + inp.addEventListener("input", function(e) { + var a, val = this.value; + /* var for async compare */ + var tmp_input = this.value.replace(/[+']/g, function(matched){ + return query_url_decode[matched]; + }); + token_counter++; + currentFocus = -1; + if (document.getElementsByClassName("autocomplete-items").length !== 0){ + a = document.getElementsByClassName("autocomplete-items")[0]; + } else { + /*create a DIV element that will contain the items (values):*/ + a = document.createElement("DIV"); + a.setAttribute("id", this.id + "autocomplete-list"); + a.setAttribute("class", "autocomplete-items"); + /*append the DIV element as a child of the autocomplete container:*/ + this.parentNode.appendChild(a); + /*append position absolute left/right:*/ + $(".autocomplete-items").css("left",left); + $(".autocomplete-items").css("right",right); + }; + /* get es_prefix key for builder */ + var chose_prefix = (select_idx == null || select_idx.length == 0) ? '' : GetValueIdx(select_idx, nb); + chose_prefix = chose_prefix.replace(/([^,])[,-]([^,].*)?$/, '$1'); + if (chose_prefix !== null){ + var prefix = es_prefix[chose_prefix].toString(); + val = val.replace(/[+']/g, function(matched){ + return query_url_encode[matched]; + }); + if (tmp_input == '' || tmp_input == null){ + closeAllLists(); + token_counter = 0; + } else { + $.ajax({ + type: 'GET', + url: url_request + val + '&key=' + key + '&prefix=' + prefix + '&token_counter=' + token_counter, + success: function (data) { + //console.log(data); + if (data.length !== 0){ + var myset; //Set for Autocomplete unique + /* autocomplete for all prefix */ + if (chose_prefix === 'kw' || chose_prefix === ''){ + myset = GetSetAutocompleteAllIdx(data, prefix, key); + } else { // autocomplete for one prefix + myset = GetSetAutocompleteOneIdx(data, prefix, key); + }; + /* append set to autocomplete */ + if ( tmp_input + prefix == data['val'] + data['prefix'] && token_counter === parseInt(data['token_counter'], 10)){ + a.innerHTML = ""; + for (let item of myset){ + a.appendChild(CreateDivItemAutocomplete(item, val)); + }; + }; + } else { + closeAllLists(this); + }; + }, + error: function (data) { + console.log(data); + }, + }); + } + + }; + }); + /* get value for tag with name idx */ + function GetValueIdx(elem, nb){ + switch (elem[0].tagName){ + case 'INPUT': + return elem[0].value; + case 'SELECT': + return select_idx[nb].options[select_idx[nb].selectedIndex].value; + default: + return null; + }; + }; + /* get autocomplete for only one prefix title/author/etc... */ + function GetSetAutocompleteOneIdx(data, prefix, key){ + let myset = new Set(); + let tmp_data = data['hits']['hits']; + for (let i = 0; i < tmp_data.length; i++) { + for (let j = 0; j < tmp_data[i]['highlight'][prefix + '.' + key].length; j++){ + /* div with data for autocomplete */ + let tmp = tmp_data[i]['highlight'][prefix + '.' + key][j]; + tmp = tmp.replace(/^\[/g, ''); + tmp = tmp.replace(/\]+$/g, ''); + myset.add(tmp.replace(/^[ &\/\\#,+)$~%.'":*?>{}!;]+|[ &\/\\#,+($~%.'":*?<{}!;]+$/g, '')); + if (myset.size >= nb_autocomplete) break; + }; + if (myset.size >= nb_autocomplete) break; + }; + return myset; + }; + /* get autocomplete for all prefix */ + function GetSetAutocompleteAllIdx(data, prefix, key){ + let myset = new Set(); + var pref = prefix.split(","); + for (k = 0; k < Object.keys(data).length; k++){ //Object.keys(data).length + if (data[k] != '' && data[k] != null){ + let tmp_data = data[k]['hits']['hits']; + for (i = 0; i < tmp_data.length; i++) { + for (j = 0; j < tmp_data[i]['highlight'][pref[k] + '.' + key].length; j++){ + /* div with data for autocomplete */ + let tmp = tmp_data[i]['highlight'][pref[k] + '.' + key][j] + myset.add(tmp.replace(/[ &#,+()$~%.'":*?<{}!/;]+$/g, '')); + if (myset.size >= nb_autocomplete) break; + }; + if (myset.size >= nb_autocomplete) break; + }; + if (myset.size >= nb_autocomplete) break; + } + } + return myset; + }; + + /*execute a function presses a key on the keyboard:*/ + inp.addEventListener("keydown", function(e) { + var x = document.getElementById(this.id + "autocomplete-list"); + if (x) x = x.getElementsByTagName("div"); + if (e.keyCode == 40) { //DOWN + /*If the arrow DOWN key is pressed, + increase the currentFocus variable:*/ + currentFocus++; + /*and and make the current item more visible:*/ + addActive(x); + } else if (e.keyCode == 38) { //up + /*If the arrow UP key is pressed, + decrease the currentFocus variable:*/ + currentFocus--; + /*and and make the current item more visible:*/ + addActive(x); + e.preventDefault(); + } else if (e.keyCode == 13) { + /*If the ENTER key is pressed, prevent the form from being submitted,*/ + //e.preventDefault(); + if (currentFocus > -1) { + /*and simulate a click on the "active" item:*/ + if (x) x[currentFocus].click(); + } + } + /* press Esc clear all autocomplete */ + else if (e.keyCode == 27) { + closeAllLists(); + } + /* press Esc clear all autocomplete */ + else if (e.keyCode == 8) { + closeAllLists(); + }; + }); + function addActive(x) { + /*a function to classify an item as "active":*/ + if (!x) return false; + /*start by removing the "active" class on all items:*/ + removeActive(x); + if (currentFocus >= x.length) currentFocus = 0; + if (currentFocus < 0) currentFocus = (x.length - 1); + /*add class "autocomplete-active":*/ + x[currentFocus].classList.add("autocomplete-active"); + inp.value = (x[currentFocus].textContent.replace(/<\/?[^>]+(>|$)/g, "")).trim(); + }; + function removeActive(x) { + /*a function to remove the "active" class from all autocomplete items:*/ + for (var i = 0; i < x.length; i++) { + x[i].classList.remove("autocomplete-active"); + }; + }; + + function closeAllLists(elem) { + /*close all autocomplete lists in the document with class autocomplete-items */ + var x = document.getElementsByClassName("autocomplete-items"); + for (var i = 0; i < x.length; i++) { + x[i].parentNode.removeChild(x[i]) + }; + }; + + /* div for one item autocomplete */ + function CreateDivItemAutocomplete (elem){ + var b = document.createElement("DIV"); + // add element "; + b.innerHTML += elem; + /*insert a input field that will hold the current array item's value:*/ + b.innerHTML += ""; + /*execute a function when someone clicks on the item value (DIV element):*/ + b.addEventListener("click", function(e) { + /* insert the value for the autocomplete text field: */ + inp.value = this.getElementsByTagName("input")[0].value; + /* normalyzer hightlight without tags */ + //inp.value = (inp.value.replace(/<\/?[^>]+(>|$)/g, "")).trim(); + inp.value = this.innerText; + /* Submit form click mouse in div */ + this.closest("form").submit(); + }); + return b; + }; + + /*execute a function when someone clicks in the document:*/ + document.addEventListener("click", function (e) { + closeAllLists(e.target); + }); +}; + +AutocompleteInitIntranet(); diff --git a/koha-tmpl/intranet-tmpl/prog/css/elasticsearch/autocomplete.css b/koha-tmpl/intranet-tmpl/prog/css/elasticsearch/autocomplete.css new file mode 100644 index 0000000000..40dfefb0d6 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/css/elasticsearch/autocomplete.css @@ -0,0 +1,28 @@ +.autocomplete { + /*the container must be positioned relative:*/ + position: relative; + display: inline-block; +} +.autocomplete-items { + position: absolute; + border: 1px solid #d4d4d4; + border-bottom: none; + border-top: none; + z-index: 99; + /*position the autocomplete items to be the same width as the container:*/ + } + .autocomplete-items div { + padding: 10px; + cursor: pointer; + background-color: #fff; + border-bottom: 1px solid #d4d4d4; + } + .autocomplete-items div:hover { + /*when hovering an item:*/ + background-color: #e9e9e9; + } + .autocomplete-active { + /*when navigating through the items using the arrow keys:*/ + background-color: #cedfb1 !important; + color: #ffffff; + } \ No newline at end of file diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc index c2833e3009..6999bc90ab 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc @@ -21,6 +21,11 @@ [% Asset.css("css/print.css", { media = "print" }) | $raw %] [% INCLUDE intranetstylesheet.inc %] [% IF ( bidi ) %][% Asset.css("css/right-to-left.css") | $raw %][% END %] + +[% IF ( Koha.Preference('AutocompleteElasticSearch') ) %] + [% SET Optylesheet = 'elasticsearch/autocomplete.css' %] + +[% END %] [% END %] + +[% IF ( Koha.Preference('AutocompleteElasticSearch') ) %] + [% Asset.js("js/elasticsearch/autocomplete.js") %] +[% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref index 04a77f8085..df01fc283d 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/searching.pref @@ -276,3 +276,12 @@ Searching: - LIBRIS base URL - pref: LibrisURL - "Please only change this if you are sure it needs changing." + - + - pref: AutocompleteElasticSearch + type: boolean + default: no + choices: + yes: Show + no: "Don't show" + - looking terms based on a provided text by using an ElasticSearch. + - diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt index 6660d2ca8f..8bf716c467 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/advsearch.tt @@ -106,11 +106,7 @@
Search for [% FOREACH search_box IN search_boxes_loop %] - [% IF ( search_boxes_label ) %] -
- [% ELSE %] -
- [% END %] + [% IF ( search_boxes_label ) %]
[% ELSE %]
[% END %] [% IF ( expanded_options ) %] [% IF ( search_box.boolean ) %] "; + /*execute a function when someone clicks on the item value (DIV element):*/ + b.addEventListener("click", function(e) { + /* insert the value for the autocomplete text field: */ + inp.value = this.getElementsByTagName("input")[0].value; + /* normalyzer hightlight without tags */ + //inp.value = (inp.value.replace(/<\/?[^>]+(>|$)/g, "")).trim(); + inp.value = this.innerText; + /* Submit form click mouse in div */ + this.closest("form").submit(); + }); + return b; + }; + + /*execute a function when someone clicks in the document:*/ + document.addEventListener("click", function (e) { + closeAllLists(e.target); + }); +}; + +AutocompleteInitOpac(); diff --git a/opac/svc/elasticsearch/opac-autocomplete.pl b/opac/svc/elasticsearch/opac-autocomplete.pl new file mode 100755 index 0000000000..a3c86e0189 --- /dev/null +++ b/opac/svc/elasticsearch/opac-autocomplete.pl @@ -0,0 +1,71 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use CGI qw ( -utf8 ); +use v5.10; +use JSON; +use utf8; +use Unicode::Normalize; +use CGI::Session; +use Koha::SearchEngine::Elasticsearch::Browse; + +my $browser = Koha::SearchEngine::Elasticsearch::Browse->new( { index => 'biblios' } ); +my $cgi = CGI->new; +my $session = CGI::Session->new(); + +$session->param(-name=>'analyzer', -value=>"autocomplete"); +$session->param(-name=>'prefix', -value=>$cgi->param("prefix")); +$session->param(-name=>'q', -value=>$cgi->param("q")); +$session->param(-name=>'key', -value=>$cgi->param("key")); +$session->param(-name=>'token_counter', -value=>$cgi->param("token_counter")); +$session->expire('+1h'); + +#Chose GET in key parametre +given ($session->param("key")) { + when ("autocomplete") { + my @prefix = split /,/, $session->param("prefix"); + #fields for autocomplete + my $length = scalar @prefix; + my $ses = NFKD( $session->param("q") ); + $ses =~ s/\p{NonspacingMark}//g; + + #search by many prefix fields + if ($length > 1){ + my $res = $browser->autocomplete_idx($ses, \@prefix, $session->param("analyzer"), $session->param("token_counter")); + print $cgi->header("application/json"); + print to_json($res); + } + #search by one prefix field + elsif ($length == 1) { + my $res = $browser->autocomplete_one_idx($ses, $prefix[0], $session->param("analyzer"), $session->param("token_counter")); + print $cgi->header("application/json"); + print to_json($res); + } + #no prefix 404 + else { + response404JSON(); + } + } + #no key 404 + default { + response404JSON(); + } +} + +sub response404JSON { + my $json = JSON->new->utf8; + my $header_type = "application/json"; + my $header_status = "404"; + my $output = $json->encode({ + "error" => "No data", + "description" => "Bad request", + }); + print $cgi->header( + -type => $header_type, + -charset => "utf-8", + -status => $header_status + ); + print $output; + print "\n"; +} -- 2.17.1