diff options
| author | Geo Halkiadakis <gchalkiadakis@sklavenitis.co.gr> | 2023-03-17 13:30:46 +0200 |
|---|---|---|
| committer | Geo Halkiadakis <gchalkiadakis@sklavenitis.co.gr> | 2023-03-17 13:30:46 +0200 |
| commit | c50d4c645cd3c04204106c4f9f026e5910afa3d5 (patch) | |
| tree | 6cc6bdb9cb159a96b8335419691a08a941ea0165 /html | |
| parent | 504732c3d35d003fd5067240b98bc35f03c8cad9 (diff) | |
| download | linkeysearch-c50d4c645cd3c04204106c4f9f026e5910afa3d5.tar.gz linkeysearch-c50d4c645cd3c04204106c4f9f026e5910afa3d5.tar.bz2 linkeysearch-c50d4c645cd3c04204106c4f9f026e5910afa3d5.zip | |
Code tree reorganized; older implemenatations act as a start point
Diffstat (limited to 'html')
| -rw-r--r-- | html/inter.html | 61 | ||||
| -rw-r--r-- | html/map.html | 126 | ||||
| -rw-r--r-- | html/search-v2.html | 336 | ||||
| -rw-r--r-- | html/search-v3.html | 422 | ||||
| -rw-r--r-- | html/search.html | 403 | ||||
| -rw-r--r-- | html/search2.html | 642 |
6 files changed, 1123 insertions, 867 deletions
diff --git a/html/inter.html b/html/inter.html new file mode 100644 index 0000000..fed1bd4 --- /dev/null +++ b/html/inter.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + </head> + <body> + <div id="result"></div> + </body> + <script> + + +function intersect(arr1, arr2) { + var result = arr1.filter(function(n) { return arr2.indexOf(n) !== -1; }); + return result; +} + +var a = [] +var b = [] +var k = [] +var l = [] +var m = [] + +var minitems = 75 +var maxitems = 150; +var maxnumber = 500; + +/* --- +var a = [2, 4, 6, 8, 10, 12, 14]; +var b = [3, 6, 9, 12, 15]; +console.log( intersect(a,b) ); --- */ + +start = new Date(); + +for(j=0 ; j < 10000; j++) { + a = []; + b = []; + for (i=0 ; i< (Math.floor(Math.random() * (maxitems-minitems)) + minitems) ; i++ ) { + x = Math.floor(Math.random() * maxnumber+1); + if (!a.includes(x)) + a.push(x); + } + + for (i=0 ; i< (Math.floor(Math.random() * (maxitems-minitems)) + minitems) ; i++ ) { + x = Math.floor(Math.random() * maxnumber+1); + if (!b.includes(x)) + b.push(x); + } + // m = intersect(a, b); + m = a.filter(value => b.includes(value)); +} +console.log(m); + +end = new Date(); + +console.log(start, end, end-start); +document.getElementById("result").innerHTML = (end-start) +"ms"; + + + + </script> +</html>
\ No newline at end of file diff --git a/html/map.html b/html/map.html new file mode 100644 index 0000000..6be59fe --- /dev/null +++ b/html/map.html @@ -0,0 +1,126 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + </head> + <body> + <div id="result"></div> + </body> + + + <script> +// arrays for kb-format (utf/EL-Gr to ascii translation) +var ORiGiNal = 'ςερτυθιοπασδφγηξκλζχψωβνμΕΡΤΥΘΙΟΠΑΣΔΦΓΗΞΚΛΖΧΨΩΒΝΜάέήίόύώϊϋΆΈΉΊΌΎΏΪΫQWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789-'.split(''); +var kbKeyZed = 'sertyuiopasdfghjklzxcvbnmertyuiopasdfghjklzxcvbnmaehioyviyaehioyviyqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm0123456789-'.split(''); +const map = new Map(); +for (i = 0 ; i < ORiGiNal.length; i++) map.set(ORiGiNal[i], kbKeyZed[i]); + +function kb_trans(str) { + str = str.replace('\'',''); + var out = '' + for (var i=0 ; i< str.length; i++) { out += map.get(str[i]); }; + // for (ch of str) { out += map.get(ch); }; + return out; +} + +/* --- +function kb_trans(s) { + var charArr = s.split('') + var i + var out = '' + charArr.forEach( el => { + i = 0 + exist = -1 + ORiGiNal.forEach( ori => { + if (ori == el) { + exist = i; + } + i++; + }) + out += (exist == -1) ? el : kbKeyZed[exist]; + }); + out = out.replace('\'',''); + out = out.replace('-',''); + return out; +} +--- */ + +list = [ "καλαμπόκι-διαβητικών", "cocacola-zero", "γιουβαρλάκια", "WELCOME", "σφενδόνα", "σκλαβενίτης", 'bonora', 'kris-κρις-παπαδοπούλου', "τηλεφώνημα", "χωρίς-αλάτι" ]; +/* --- +start = new Date(); +for (i=0 ; i<1000000 ; i++) { + tmp = kb_trans(list[i%10]); +} +end = new Date(); +console.log(start, end, end-start); +document.getElementById("result").innerHTML = (end-start) +"ms"; +--- */ + + +start = new Date(); +for (i=0; i< 100000000 ; i++ ) { a = 0; } +t0 = new Date(); +for (i=0; i< 100000000 ; i += 1 ) { a = 0; } +end = new Date(); +console.log('++ :', t0-start); +console.log('+= :', end-t0); + + +someObj = [ + { w: 'alfa', l: [ 1, 2, 3, 4, 5, 6, 7 ] }, + { w: 'beta', l: [ 5, 4, 3, 2, 1] }, + { w: 'pi', l: [ 3, 1, 4, 1, 5, 1, 9 ] } +] + +function get_me(a, b) { + var _track = [] + someObj.forEach( it => { + _track.push(it.w); + + if (it.w == a) { + + it.l.forEach( i => { + _track.push(i.toString()); + if (i == b) { + _track.push('done!'); + return true; + } + + + }); + return true; + } + + }); + return _track; +} + + +function find_me(a, b) { + var _track = [] + someObj.find( it => { + _track.push(it.w); + + if (it.w == a) { + + it.l.forEach( i => { + _track.push(i.toString()); + if (i == b) { + _track.push('done!'); + return true; + } + + + }); + return true; + } + + }); + return _track; +} +get_me('beta',3); + + + </script> + +</html>
\ No newline at end of file diff --git a/html/search-v2.html b/html/search-v2.html deleted file mode 100644 index 100565b..0000000 --- a/html/search-v2.html +++ /dev/null @@ -1,336 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="utf-8"> - - <style> -body { font-family: 'Cantarell', Helvetica, Arial, sans-serif; } - -.twitter-typeahead { width: 87% ;} -.typeahead, .tt-query, .tt-hint { - width: 100%; height: 30px; - padding: 8px 12px; outline: none; - font-size: 20px; line-height: 30px; - border: 2px solid #ccc; border-radius: 8px; - -webkit-border-radius: 8px; - -moz-border-radius: 8px; -} -.tt-menu { - width: 100%; margin: 12px 0; padding: 8px 0; - background-color: #fff; - border: 1px solid #ccc; border-radius: 8px; - -webkit-border-radius: 8px; - -moz-border-radius: 8px; - -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); - -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); - box-shadow: 0 5px 10px rgba(0,0,0,.2); -} -.tt-suggestion { padding: 3px 20px; line-height: 24px; font-size: 18px; } -.tt-suggestion span { padding-left: 16px; font-size: 14px; color: #777; float: right; } -.tt-cursor { background: #ddd; } -.tt-highlight { font-weight: normal; color: #777; } - -#selections { width: 87%; padding-top: 40px; } -#selections div { padding: 4px 40px; line-height: 24px; font-size: 18px; color: #666; } -#selections div span { padding-left: 16px; font-size: 14px; color: #999; float: right; } - </style> - - <!-- js labraries --> - <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.2.1/bloodhound.min.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.2.1/typeahead.jquery.min.js"></script> - </head> - <body> - - <div id="the-basics"> - <input class="typeahead" id="tagsInput" type="text" placeholder="try me!"> - </div> - - <div id="selections"> - </div> - - </body> - <script> - -// arrays for kb-format (utf/EL-Gr to ascii translation) -var ORiGiN = 'ςερτυθιοπασδφγηξκλζχψωβνμΕΡΤΥΘΙΟΠΑΣΔΦΓΗΞΚΛΖΧΨΩΒΝΜάέήίόύώϊϋΆΈΉΊΌΎΏΪΫQWERTYUIOPASDFGHJKLZXCVBNM'.split(''); -var kbKeyZ = 'sertyuiopasdfghjklzxcvbnmertyuiopasdfghjklzxcvbnmaehioyviyaehioyviyqwertyuiopasdfghjklzxcvbnm'.split(''); - -// convert string to kb-format -function kb_trans(s) { - var charArr = s.split('') - var i - var out = '' - charArr.forEach( el => { - i = 0 - exist = -1 - ORiGiN.forEach( ori => { - if (ori == el) { - exist = i; - } - i++; - }) - out += (exist == -1) ? el : kbKeyZ[exist]; - }); - return out; -} - -// public data objects -var products; -var everyProduct; - -function loadData() { - const xhttp = new XMLHttpRequest(); - xhttp.onload = function() { - products = JSON.parse(this.responseText); - } - xhttp.open("GET", "results/keywords-v3.json"); - xhttp.send(); -} -loadData(); - -function loadData2() { - const xhttp = new XMLHttpRequest(); - xhttp.onload = function() { - everyProduct = JSON.parse(this.responseText); - } - xhttp.open("GET", "results/products.json"); - xhttp.send(); -} -loadData2(); - -// bigram fuzzy match -// --- credit: https://dirask.com/posts/JavaScript-check-words-similarity-fuzzy-compare-with-bigrams-paola1 -const createBigram = word => { - const input = word.toLowerCase(); - const vector = []; - for (let i = 0; i < input.length; ++i) { - vector.push(input.slice(i, i + 2)); - } - return vector; -}; -const checkSimilarity = (a, b) => { - if (a.length > 0 && b.length > 0) { - const aBigram = createBigram(a); - const bBigram = createBigram(b); - let hits = 0; - for (let x = 0; x < aBigram.length; ++x) { - for (let y = 0; y < bBigram.length; ++y) { - if (aBigram[x] === bBigram[y]) { - hits += 1; - } - } - } - if (hits > 0) { - const union = aBigram.length + bBigram.length; - return (2.0 * hits) / union; - } - } - return 0; -}; -var bi_1st = .6; // bigram minimum match score for 1st word -var bi_2nd = .8; // bigram minimum match score for 2nd word - -// on document ready code ///////////////////////////////////////////////////// -$(document).ready(function() { - - // suggestions engine //////////////////////////////////////////////////// - // --- - function suggestions_engine(qOrig) { - var results = []; // suggestions to respond - var proList = []; // list of products (for all suggestions) - var commonL = []; // list of common products (for multiple suggestions) - var possibleNext = []; // list of possible next suggestions - - var root, last; - - // clean q(uery) string from symbols and multiple spaces - var q = qOrig.replace('+',' ').replace('.',' ') - .replace(' ',' ') - .replace(' ',' '); - - var qAr = q.split(' '); // split to words - - if (qAr.length == 1) { // suggest 1st word //////////////////////// - var kbq = kb_trans(q) - // regex match all possible suggestions; (in kb-format) - substrRegex = new RegExp( kbq, 'i'); // match q anywhere - products.forEach( it => { - if ( (substrRegex.test(it.kb)) - || (checkSimilarity(it.kb, kbq) > bi_1st) ) { - results.push(it); - } - }); - } - - if (qAr.length == 2) { // suggest 2nd word /////////////////////////// - root = qAr[0].trim(); - kbroot = kb_trans(root); - - substrRegex = new RegExp( kb_trans(qAr[1]), 'i'); - - products.forEach( it => { // loop through suggestions - if (it.kb == kbroot ) { // match 1st suggestion - it.c.forEach ( wo => { // regex match linked words - if ( (substrRegex.test(wo.kb)) - || (checkSimilarity(wo.kb, kbroot) > bi_2nd) ) { - results.push({ - w: root +' '+ wo.w, - f: 100 - }); - proList = proList.concat(wo.p) - } - }); - } - }); - } - - if (qAr.length > 2) { - root = qAr.shift(); // get out the first item of qAr - last = qAr.pop(); // get out the lase item of qAr - // now qAr includes only the items after root and before last; - // so qAr includes all already selected suggestions (but root) - - var kbqAr = []; // array of selected suggestions in kb-format - qAr.forEach( w => { kbqAr.push(kb_trans(w)); }) - - // kb-translate the root/last keys - kbroot = kb_trans(root); - kblast = kb_trans(last); - - substrRegex = new RegExp( kblast, 'i'); // construct regex for mathing - - products.forEach( it => { - if (it.kb == kbroot ) { // find root - - // calculate list of common items/products (commonL) - // for selected suggestions - // --- - is1stOcc = true; // 1st occurance flag - it.c.forEach ( swo => { - if (kbqAr.includes( swo.kb )) { - // swo is one of the already selected suggestions - // so... - // update the commonL(ist) - if (is1stOcc) { - commonL = swo.p; - is1stOcc = false; - } - else { - // list ot common products - // = intersection of (so-far) commonL and swo.p - commonL = commonL.filter(value => swo.p.includes(value)); - } - } - else { // if swo is not already selected - // then it is a possible next suggestion - possibleNext.push(swo); - } - }); - // console.log('commonL:', commonL) - - possibleNext.forEach( poss => { // for tthe possible next suggestions - // if word matches regex - // and list of word's products has commons with commonL - // then it is a valid next suggestion - if (substrRegex.test(poss.kb)) { - // check intersection of commonL and suggestion's product-lists - tempL = commonL.filter(value => poss.p.includes(value)); - if (tempL.length) { - results.push({ - w: root +' '+ qAr.join(' ') +' '+ poss.w, - f: 100 - }); - // update proList too - proList = proList.concat(tempL) - } - } - }); - } - - }); - } - - - if ((qAr.length != 1) && (proList.length < 13)) { - // get unique product ids - let unique = proList.filter((item, i, ar) => ar.indexOf(item) === i); - // credit: https://stackoverflow.com/questions/11246758/ - - results = []; - unique.forEach( pr => { - everyProduct.forEach( pi => { - if (pi.id == pr) - results.push(pi); - }) - }); - } - return results; - } - - var isuggest = function(qOrig, list) { - var results = suggestions_engine(qOrig); - if (results.length == 0) { - var qAr = qOrig.trim().split(' '); - qAr.pop(); // remove last word - results = suggestions_engine(qAr.join(' ')); - } - list(results); - } - - // setup suggestions search/input control - // --- - const $tagsInput = $('#tagsInput') - $tagsInput.typeahead( - { - hint: true, - highlight: true, - minLength: 1 - }, - { - limit: 12, - name: 'products', - displayKey: 'w', - source: isuggest, - templates: { - suggestion: function(data) { - // console.log(data.w); - if (data.id) - return '<div>'+ data.w + '<span>' + data.id + '</span></div>'; - return '<div>'+ data.w +'</div>'; - } - } - } - ) - .bind("typeahead:selected", function(obj, datum, name) { - console.log(datum); - if (datum.hasOwnProperty('id')) { - // final product selected; do whatever ... - // ex. add to selection list - $('#selections').append('<div>'+ datum.w + '<span>' + datum.id + '</span></div>'); - - // then reset search control - $('.typeahead').typeahead('val','').trigger('blur') - .trigger("query"); - setTimeout(() => { $('.typeahead').focus(); }, 100); - } - else { - $('.typeahead').typeahead('val','').trigger('blur'); - $('.typeahead').typeahead('val', datum.w +' ') - .trigger("query"); - // give some time to the engine to calculate results - // then fire focus again... - setTimeout(() => { $('.typeahead').focus(); }, 100); - } - }) - .bind("typeahead:cursorchange", function(obj, data) { - // console.log(obj, data); - // var dt = new Date(); - // console.log('triggered cursorchange /'+dt); - }); - - - -}); - </script> -</html> diff --git a/html/search-v3.html b/html/search-v3.html deleted file mode 100644 index 35e29ca..0000000 --- a/html/search-v3.html +++ /dev/null @@ -1,422 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="utf-8"> - - <style> -body { font-family: 'Cantarell', Helvetica, Arial, sans-serif; margin: 2em; } - -.twitter-typeahead { width: 87%; } -.typeahead, .tt-query, .tt-hint { - width: 100%; height: 30px; - padding: 8px 12px; outline: none; - font-size: 20px; line-height: 30px; - border: 2px solid #ccc; border-radius: 8px; - -webkit-border-radius: 8px; - -moz-border-radius: 8px; -} -.tt-menu { - width: 100%; margin: 12px 0; padding: 8px 0; - background-color: #fff; - border: 1px solid #ccc; border-radius: 8px; - -webkit-border-radius: 8px; - -moz-border-radius: 8px; - -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); - -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); - box-shadow: 0 5px 10px rgba(0,0,0,.2); -} -.tt-suggestion { padding: 3px 20px; line-height: 24px; font-size: 18px; } -.tt-suggestion:hover { cursor: pointer; } -.tt-suggestion span { padding-left: 16px; font-size: 14px; color: #777; float: right; } -.tt-cursor { background: #ddd; } -.tt-highlight { font-weight: normal; color: #777; } -.tt-hint { color: #9598; } - -#selections { width: 87%; padding-top: 40px; } -#selections div { padding: 4px 40px; line-height: 24px; font-size: 18px; color: #666; } -#selections div span { padding-left: 16px; font-size: 14px; color: #999; float: right; } -.-info- { font-size: 12px !important; color: #959 !important; line-height: 14px !important; font-family: 'JetBrains Mono NL', Consolas, Monaco, monospace, fixed !important; } -.-info- b { font-weight: 900;} - </style> - - <!-- js labraries --> - <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.2.1/bloodhound.min.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.2.1/typeahead.jquery.min.js"></script> - </head> - <body> - - <div id="the-basics"> - <input class="typeahead" id="tagsInput" type="text" placeholder="try me!"> - </div> - - <div id="selections"> - </div> - - </body> - <script> - - -// PUBLIC VARIABLES //////////////////////////////////////////////////////////// - -var kwlinks; // keyword links (word-connections) -var products; // all products - -var trackSearch = []; // searching analytics - -// setup options -var sgLimit = 12; // limit suggestions -var bi_1st = .65; // bigram minimum match score for 1st word -var bi_2nd = .85; // bigram minimum match score for 2nd word - - -// SUPLAMENARY FUNCTIONS /////////////////////////////////////////////////////// - -// arrays for kb-format (utf/EL-Gr to ascii translation) -var ORiGiN = 'ςερτυθιοπασδφγηξκλζχψωβνμΕΡΤΥΘΙΟΠΑΣΔΦΓΗΞΚΛΖΧΨΩΒΝΜάέήίόύώϊϋΆΈΉΊΌΎΏΪΫQWERTYUIOPASDFGHJKLZXCVBNM'.split(''); -var kbKeyZ = 'sertyuiopasdfghjklzxcvbnmertyuiopasdfghjklzxcvbnmaehioyviyaehioyviyqwertyuiopasdfghjklzxcvbnm'.split(''); - -// convert string to kb-format -// --- -function kb_trans(s) { - var charArr = s.split('') - var i - var out = '' - charArr.forEach( el => { - i = 0 - exist = -1 - ORiGiN.forEach( ori => { - if (ori == el) { - exist = i; - } - i++; - }) - out += (exist == -1) ? el : kbKeyZ[exist]; - }); - return out; -} - - -function loadData() { - const xhttp = new XMLHttpRequest(); - xhttp.onload = function() { - kwlinks = JSON.parse(this.responseText); - } - xhttp.open("GET", "results/keywords-v3.json"); - xhttp.send(); -} -loadData(); - -function loadData2() { - const xhttp = new XMLHttpRequest(); - xhttp.onload = function() { - products = JSON.parse(this.responseText); - } - xhttp.open("GET", "results/products.json"); - xhttp.send(); -} -loadData2(); - -// bigram fuzzy match -// --- credit: https://dirask.com/posts/JavaScript-check-words-similarity-fuzzy-compare-with-bigrams-paola1 -const createBigram = word => { - const input = word.toLowerCase(); - const vector = []; - for (let i = 0; i < input.length; ++i) { - vector.push(input.slice(i, i + 2)); - } - return vector; -}; -const checkSimilarity = (a, b) => { - if (a.length > 0 && b.length > 0) { - const aBigram = createBigram(a); - const bBigram = createBigram(b); - let hits = 0; - for (let x = 0; x < aBigram.length; ++x) { - for (let y = 0; y < bBigram.length; ++y) { - if (aBigram[x] === bBigram[y]) { - hits += 1; - } - } - } - if (hits > 0) { - const union = aBigram.length + bBigram.length; - return (2.0 * hits) / union; - } - } - return 0; -}; - - - -function echo_tracking() { - var actions = []; - var c; - var chs = 0; // number of characters pressed; - var uis = 0; // number of UI actions used (arrows, enters etc.) - var countingStarted = false; // flag - trackSearch.forEach(e => { - switch(e.v) { - // use of ui actions - case 'ArrowDown' : c = '↓'; uis++; break; - case 'ArrowUp' : c = '↑'; uis++; break; - case 'Enter' : c = '↲ '; uis++; break; - case 'ArrowLeft' : c = '←'; uis++; break; - case 'ArrowRight': c = '→'; uis++; break; - case ' ' : c = '· '; uis++; break; - // ignored keys - case 'Alt' : c = 'Alt'; break; - case 'Control' : c = 'Ctrl'; break; - case 'Escape' : c = 'Esc'; break; - case 'Shift' : c = 'Shft'; break; - case 'Home' : c = 'Home'; break; - case 'End' : c = 'End'; break; - // backspace (user's typing errors) - case 'Backspace' : c = 'BkSp'; break; - case 'Delete' : c = 'Del'; break; - // actual typed characters - default: - if (e.v.length == 1) { - c = '<b><u>'+ e.v +'</u></b>'; - chs++; - } - else { // some non important key; no counter increased - c = e.v; // just record the key - } - } - actions.push(c); - }); - - return actions.join(',') +' (<u>'+ chs +' chs</u>, '+ uis +' uis)'; -} - - -// on document ready code /////////////////////////////////////////////////////// -$(document).ready(function() { - - // suggestions engine ////////////////////////////////////////////////////// - // --- - function suggestions_engine(qOrig) { - var results = []; // suggestions to respond - var proList = []; // list of products (for all suggestions) - var commonL = []; // list of common products (for multiple suggestions) - var possibleNext = []; // list of possible next suggestions - - var root, last; - - // clean q(uery) string from symbols and multiple spaces - var q = qOrig.replace('+',' ').replace('.',' ') - .replace(' ',' ') - .replace(' ',' '); - - var qAr = q.split(' '); // split to words - - if (qAr.length == 1) { // suggest 1st word //////////////////////// - var kbq = kb_trans(q) - // regex match all possible suggestions; (in kb-format) - substrRegex = new RegExp( kbq, 'i'); // match q anywhere - kwlinks.forEach( it => { - if ( (substrRegex.test(it.kb)) - || (checkSimilarity(it.kb, kbq) > bi_1st) ) { - results.push(it); - } - }); - } - - if (qAr.length == 2) { // suggest 2nd word /////////////////////////// - root = qAr[0].trim(); - kbroot = kb_trans(root); - - substrRegex = new RegExp( kb_trans(qAr[1]), 'i'); - - kwlinks.forEach( it => { // loop through suggestions - if (it.kb == kbroot ) { // match 1st suggestion - it.c.forEach ( wo => { // regex match linked words - if ( (substrRegex.test(wo.kb)) - || (checkSimilarity(wo.kb, kbroot) > bi_2nd) ) { - results.push({ - w: root +' '+ wo.w, - f: 100 - }); - proList = proList.concat(wo.p) - } - }); - } - }); - } - - if (qAr.length > 2) { - root = qAr.shift(); // get out the first item of qAr - last = qAr.pop(); // get out the lase item of qAr - // now qAr includes only the items after root and before last; - // so qAr includes all already selected suggestions (but root) - - var kbqAr = []; // array of selected suggestions in kb-format - qAr.forEach( w => { kbqAr.push(kb_trans(w)); }) - - // kb-translate the root/last keys - kbroot = kb_trans(root); - kblast = kb_trans(last); - - substrRegex = new RegExp( kblast, 'i'); // construct regex for mathing - - kwlinks.forEach( it => { - if (it.kb == kbroot ) { // find root - - // calculate list of common items/products (commonL) - // for selected suggestions - // --- - is1stOcc = true; // 1st occurance flag - it.c.forEach ( swo => { - if (kbqAr.includes( swo.kb )) { - // swo is one of the already selected suggestions - // so... - // update the commonL(ist) - if (is1stOcc) { - commonL = swo.p; - is1stOcc = false; - } - else { - // list ot common products - // = intersection of (so-far) commonL and swo.p - commonL = commonL.filter(value => swo.p.includes(value)); - } - } - else { // if swo is not already selected - // then it is a possible next suggestion - possibleNext.push(swo); - } - }); - // console.log('commonL:', commonL) - - possibleNext.forEach( poss => { // for tthe possible next suggestions - // if word matches regex - // and list of word's products has commons with commonL - // then it is a valid next suggestion - if (substrRegex.test(poss.kb)) { - // check intersection of commonL and suggestion's product-lists - tempL = commonL.filter(value => poss.p.includes(value)); - if (tempL.length) { - results.push({ - w: root +' '+ qAr.join(' ') +' '+ poss.w, - f: 100 - }); - // update proList too - proList = proList.concat(tempL) - } - } - }); - } - - }); - } - - - if ( (qAr.length != 1) && (proList.length < (sgLimit +1)) ) { - // get unique product ids - let unique = proList.filter((item, i, ar) => ar.indexOf(item) === i); - // credit: https://stackoverflow.com/questions/11246758/ - - results = []; - unique.forEach( pr => { - products.forEach( pi => { - if (pi.id == pr) - results.push(pi); - }) - }); - } - return results; - } - - // request suggestions procedure - // args... - // qOrig: original query string - // list: artay structure to host results - // --- - var isuggest = function(qOrig, list) { - var results = suggestions_engine(qOrig); - - // if no results... - // request again after removing last (key)word - if (results.length == 0) { - var qAr = qOrig.trim().split(' '); - qAr.pop(); // remove last word - results = suggestions_engine(qAr.join(' ')); - } - - list(results); - } - - // setup suggestions search/input control - // --- - const $tagsInput = $('#tagsInput') - $tagsInput.typeahead( - { - hint: true, - highlight: true, - minLength: 1 - }, - { - limit: sgLimit, - name: 'kwlinks', - displayKey: 'w', - source: isuggest, - templates: { - suggestion: function(data) { - // console.log(data.w); - if (data.id) - return '<div>'+ data.w + '<span>' + data.id + '</span></div>'; - return '<div>'+ data.w +'<span>+</span></div>'; - } - } - } - ) - .bind("typeahead:selected", function(obj, datum, name) { - // console.log(datum); - if (datum.hasOwnProperty('id')) { - // final product selected; do whatever ... - // ex. add to selection list - $('#selections').append('<div>'+ datum.w + '<span>' + datum.id + '</span></div>'); - - // then reset search control - $('.typeahead').typeahead('val','').trigger('blur') - .trigger("query"); - setTimeout(() => { $('.typeahead').focus(); }, 100); - - // finaly save tracking info; - // $('#selections').append('<div class="-info-">'+ JSON.stringify(trackSearch) +'</div>'); - $('#selections').append('<div class="-info-">'+ echo_tracking() +'</div>'); - trackSearch.length = 0; // ... and reset info to be ready for nextsearch - } - else { - $('.typeahead').typeahead('val','').trigger('blur'); - $('.typeahead').typeahead('val', datum.w +' ') - .trigger("query"); - // give some time to the engine to calculate results - // then fire focus again... - setTimeout(() => { $('.typeahead').focus(); }, 100); - } - trackSearch.push({ - e: 'key', - v: 'Enter', - i: $('#tagsInput').val() - }); - }) - .bind("typeahead:cursorchange", function(obj, data) { - // console.log(obj, data); - // var dt = new Date(); - // console.log('triggered cursorchange /'+dt); - }); - - // TRACK user search attempt /////////////////////////////////////////////// - $('.typeahead').on('keyup', function(e) { - trackSearch.push({ - e: 'key', - v: e.key, - i: $('#tagsInput').val() - }); - }); - -}); - </script> -</html> diff --git a/html/search.html b/html/search.html index 2a12941..5c5e64b 100644 --- a/html/search.html +++ b/html/search.html @@ -4,9 +4,9 @@ <meta charset="utf-8"> <style> -body { font-family: 'Cantarell', Helvetica, Arial, sans-serif; } +body { font-family: 'Cantarell', Helvetica, Arial, sans-serif; margin: 2em; } -.twitter-typeahead { width: 87% ;} +.twitter-typeahead { width: 87%; } .typeahead, .tt-query, .tt-hint { width: 100%; height: 30px; padding: 8px 12px; outline: none; @@ -26,9 +26,20 @@ body { font-family: 'Cantarell', Helvetica, Arial, sans-serif; } box-shadow: 0 5px 10px rgba(0,0,0,.2); } .tt-suggestion { padding: 3px 20px; line-height: 24px; font-size: 18px; } +.tt-suggestion:hover { cursor: pointer; } .tt-suggestion span { padding-left: 16px; font-size: 14px; color: #777; float: right; } .tt-cursor { background: #ddd; } .tt-highlight { font-weight: normal; color: #777; } +.tt-hint { color: #9598; } + +#selections { width: 87%; padding-top: 40px; } +#selections div { padding: 4px 40px; line-height: 24px; font-size: 18px; color: #666; } +#selections div span { padding-left: 16px; font-size: 14px; color: #999; float: right; } +.-info- { font-size: 12px !important; color: #959 !important; + font-family: 'JetBrains Mono NL', Consolas, Monaco, monospace, fixed !important; + line-height: 14px !important; border-bottom: 1px solid #ddda; +} +.-info- b { font-weight: 900; } </style> <!-- js labraries --> @@ -42,55 +53,73 @@ body { font-family: 'Cantarell', Helvetica, Arial, sans-serif; } <input class="typeahead" id="tagsInput" type="text" placeholder="try me!"> </div> + <div id="selections"> + </div> + </body> <script> -// arrays for kb-format (utf/EL-Gr to ascii translation) -var ORiGiN = 'ςερτυθιοπασδφγηξκλζχψωβνμΕΡΤΥΘΙΟΠΑΣΔΦΓΗΞΚΛΖΧΨΩΒΝΜάέήίόύώϊϋΆΈΉΊΌΎΏΪΫQWERTYUIOPASDFGHJKLZXCVBNM'.split(''); -var kbKeyZ = 'sertyuiopasdfghjklzxcvbnmertyuiopasdfghjklzxcvbnmaehioyviyaehioyviyqwertyuiopasdfghjklzxcvbnm'.split(''); -// convert string to kb-format -function kb_trans(s) { - var charArr = s.split('') - var i +// PUBLIC VARIABLES //////////////////////////////////////////////////////////// + +var _kwlinks; // keyword links (word-connections; imported via ajax-get) +var _products; // all products (imported via ajax-get) + +var trackSearch = []; // searching analytics + +// setup options +var maxResults = 24; // limit suggestions +var blendProds = 4; // minimum final-produncts to blend with next-word suggestions +var allowFuzzy = false; // enable|disable fuzzy search +var bi1stScore = .65; // bigram minimum match score for 1st word +var bi2ndScore = .85; // bigram minimum match score for 2nd word + + +var ignoredKeys_kb = []; // keywords to ignore (in kb-format) +['μας με σε για του της των από στο στον &'].split(' ').forEach(w => { ignoredKbs.push(kb_trans(w)); }); + + +// SUPLAMENARY FUNCTIONS /////////////////////////////////////////////////////// + +// arrays for kb-format (utf/EL-Gr to ascii translation) +var ORiGiNal = 'ςερτυθιοπασδφγηξκλζχψωβνμΕΡΤΥΘΙΟΠΑΣΔΦΓΗΞΚΛΖΧΨΩΒΝΜάέήίόύώϊϋΆΈΉΊΌΎΏΪΫQWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789-'.split(''); +var kbKeyZed = 'sertyuiopasdfghjklzxcvbnmertyuiopasdfghjklzxcvbnmaehioyviyaehioyviyqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm0123456789-'.split(''); +const map = new Map(); +for (var i=0; i<ORiGiNal.length; i++) map.set(ORiGiNal[i], kbKeyZed[i]); +// function kb_trans +// translates string to keyboard-latin keys; +// [map]'s implementation is 40x faster than [for]'s +function kb_trans(str) { + str = str.replace('\'',''); var out = '' - charArr.forEach( el => { - i = 0 - exist = -1 - ORiGiN.forEach( ori => { - if (ori == el) { - exist = i; - } - i++; - }) - out += (exist == -1) ? el : kbKeyZ[exist]; - }); + for (var i=0 ; i< str.length; i++) out += map.get(str[i]); return out; } -// public data objects -var products; -var everyProduct; - -function loadData() { - const xhttp = new XMLHttpRequest(); - xhttp.onload = function() { - products = JSON.parse(this.responseText); - } - xhttp.open("GET", "results/keywords-v3.json"); - xhttp.send(); -} -loadData(); - -function loadData2() { - const xhttp = new XMLHttpRequest(); - xhttp.onload = function() { - everyProduct = JSON.parse(this.responseText); - } - xhttp.open("GET", "results/products.json"); - xhttp.send(); + + +// pure JS ajax GET request; return data as JSON +// no fancy things like UTF8; if needed use base64 +// --- +function ajax_get(url, callback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + + try { + var data = JSON.parse(xmlhttp.responseText); + } catch(err) { + console.log(err.message + " in " + xmlhttp.responseText); + return; + } + callback(data); + } + }; + + xmlhttp.open("GET", url, true); + xmlhttp.send(); } -loadData2(); + // bigram fuzzy match // --- credit: https://dirask.com/posts/JavaScript-check-words-similarity-fuzzy-compare-with-bigrams-paola1 @@ -98,11 +127,13 @@ const createBigram = word => { const input = word.toLowerCase(); const vector = []; for (let i = 0; i < input.length; ++i) { - vector.push(input.slice(i, i + 2)); + vector.push(input.slice(i, i + 2)); } return vector; }; const checkSimilarity = (a, b) => { + if (!allowFuzzy) return false; + if (a.length > 0 && b.length > 0) { const aBigram = createBigram(a); const bBigram = createBigram(b); @@ -112,27 +143,110 @@ const checkSimilarity = (a, b) => { if (aBigram[x] === bBigram[y]) { hits += 1; } - } + } } if (hits > 0) { - const union = aBigram.length + bBigram.length; - return (2.0 * hits) / union; + const union = aBigram.length + bBigram.length; + return (2.0 * hits) / union; } } return 0; }; -var biMMS = .6; // bigram minimum match score -// on document ready code ///////////////////////////////////////////////////// + +// compact print of search-tracking +// --- +function echo_tracking() { + var actions = []; + var c; + var chs = 0; // number of characters pressed; + var uis = 0; // number of UI actions used (arrows, enters etc.) + var countingStarted = false; // flag + trackSearch.forEach(e => { + switch(e.v) { + // use of ui actions + case 'ArrowDown' : c = '↓'; uis++; break; + case 'ArrowUp' : c = '↑'; uis++; break; + case 'Enter' : c = '↲ '; uis++; break; + case 'ArrowLeft' : c = '←'; uis++; break; + case 'ArrowRight': c = '→'; uis++; break; + case ' ' : c = '· '; uis++; break; + // ignored keys + case 'Alt' : c = 'Alt'; break; + case 'Control' : c = 'Ctrl'; break; + case 'Escape' : c = 'Esc'; break; + case 'Shift' : c = 'Shft'; break; + case 'Home' : c = 'Home'; break; + case 'End' : c = 'End'; break; + // backspace (user's typing errors) + case 'Backspace' : c = 'BkSp'; break; + case 'Delete' : c = 'Del'; break; + // actual typed characters + default: + if (e.v.length == 1) { + c = '<b><u>'+ e.v +'</u></b>'; + chs++; + } + else { // some non important key; no counter increased + c = e.v; // just record the key + } + } + actions.push(c); + }); + + return actions.join(',') +' (<u>'+ chs +' chs</u>, '+ uis +' uis)'; +} + + +// workline object to track status of async svents +// --- +var workline = { + + trackerJL : 0, + set jsonLoaded(x) { + this.trackerJL = x; + + // fire event on certain values + if (x == 2) { + console.log('All streams loaded'); + + // code to execute + // ... + + } + }, + get jsonLoaded() { return this.trackerJL; } + +}; + + + +// LOAD DATA /////////////////////////////////////////////////////////////////// + +ajax_get('results/keywords-v3.json', function(data) { + _kwlinks = data; + workline.jsonLoaded++; console.log('keywords loaded'); +}); + +ajax_get('results/products.json', function(data) { + _products = data; + workline.jsonLoaded++; console.log('products loaded') +}); + + + + + +// on document ready code /////////////////////////////////////////////////////// $(document).ready(function() { - // suggestions engine //////////////////////////////////////////////////// + // suggestions engine ////////////////////////////////////////////////////// // --- - var isuggest = function(qOrig, list) { - var results = []; // suggestions to respond - var proList = []; // list of products (for all suggestions) - var commonL = []; // list of common products (for multiple suggestions) - var possibleNext = []; // list of possible next suggestions + function suggestions_engine(qOrig) { + var results = []; // suggestions to respond + var proList = []; // list of products (for all suggestions) + var commonL = []; // list of common products (for multiple suggestions) + var possibleNext = []; // list of possible next suggestions var root, last; @@ -140,34 +254,43 @@ $(document).ready(function() { var q = qOrig.replace('+',' ').replace('.',' ') .replace(' ',' ') .replace(' ',' '); - // split to words - var qAr = q.split(' '); - // console.log(qAr); + + var qAr = q.split(' '); // split to words if (qAr.length == 1) { // suggest 1st word //////////////////////// - var kbq = kb_trans(q) - // regex match all possible suggestions; (in kb-format) - substrRegex = new RegExp( kbq, 'i'); // match q anywhere - products.forEach( it => { - if ( (substrRegex.test(it.kb)) - || (checkSimilarity(it.kb, kbq) > biMMS) ) { - // console.log(it.kb, kbq, checkSimilarity(it.kb, kbq)); + var kbq = kb_trans(q) + + // ### (-) regex match all possible suggestions; (in kb-format) + // ### (-) substrRegex = new RegExp( kbq, 'i'); // match q anywhere + _kwlinks.forEach( it => { + // ### (-) if ( (substrRegex.test(it.kb)) + if ( (it.kb.indexOf(kbq) !== -1) + || (checkSimilarity(it.kb, kbq) > bi1stScore) ) { + // a match found results.push(it); + // also get possible products + // (but only if products are less max-results) + if (proList.length < maxResults + 2) { + it.c.forEach( wo => { proList = proList.concat(wo.p); }); + // keep unique products in the list + proList = proList.filter((item, i, ar) => ar.indexOf(item) === i); + } } }); } - if (qAr.length == 2) { // suggest 2nd word /////////////////////////// + if (qAr.length == 2) { // suggest 2nd word /////////////////////////// root = qAr[0].trim(); kbroot = kb_trans(root); - substrRegex = new RegExp( kb_trans(qAr[1]), 'i'); + // ### (-) substrRegex = new RegExp( kb_trans(qAr[1]), 'i'); - products.forEach( it => { // loop through suggestions - if (it.kb == kbroot ) { // match 1st suggestion - it.c.forEach ( wo => { // regex match linked words - if ( (substrRegex.test(wo.kb)) - || (checkSimilarity(wo.kb, kbroot) > biMMS) ) { + _kwlinks.forEach( it => { // loop through suggestions + if (it.kb == kbroot ) { // match 1st suggestion + it.c.forEach ( wo => { // regex match linked words + // ### (-) if ( (substrRegex.test(wo.kb)) + if ( (wo.kb.indexOf(qAr[1]) !== -1) + || (checkSimilarity(wo.kb, kbroot) > bi2ndScore) ) { results.push({ w: root +' '+ wo.w, f: 100 @@ -180,21 +303,22 @@ $(document).ready(function() { } if (qAr.length > 2) { - root = qAr.shift(); // get out the first item of qAr + root = qAr.shift(); // get out the first item of tempQAr last = qAr.pop(); // get out the lase item of qAr // now qAr includes only the items after root and before last; // so qAr includes all already selected suggestions (but root) + // prepare/cache kb-formats for any key we may need + // --- var kbqAr = []; // array of selected suggestions in kb-format qAr.forEach( w => { kbqAr.push(kb_trans(w)); }) - - // kb-translate the root/last keys + // kb-translate the root/last keys too kbroot = kb_trans(root); kblast = kb_trans(last); - substrRegex = new RegExp( kblast, 'i'); // construct regex for mathing + // ### (-) substrRegex = new RegExp( kblast, 'i'); // construct regex for mathing - products.forEach( it => { + _kwlinks.forEach( it => { if (it.kb == kbroot ) { // find root // calculate list of common items/products (commonL) @@ -202,18 +326,19 @@ $(document).ready(function() { // --- is1stOcc = true; // 1st occurance flag it.c.forEach ( swo => { - if (kbqAr.includes( swo.kb )) { + if (kbqAr.indexOf( swo.kb ) !== -1) { // swo is one of the already selected suggestions // so... // update the commonL(ist) if (is1stOcc) { - commonL = swo.p; + commonL = swo.p; // init list (on 1st occurance) is1stOcc = false; } else { - // list ot common products + // caclulate list of common products // = intersection of (so-far) commonL and swo.p - commonL = commonL.filter(value => swo.p.includes(value)); + // commonL = commonL.filter(value => swo.p.includes(value)); + commonL = commonL.filter(function(n) { return swo.p.indexOf(n) !== -1; }); } } else { // if swo is not already selected @@ -221,13 +346,12 @@ $(document).ready(function() { possibleNext.push(swo); } }); - // console.log('commonL:', commonL) possibleNext.forEach( poss => { // for tthe possible next suggestions // if word matches regex // and list of word's products has commons with commonL // then it is a valid next suggestion - if (substrRegex.test(poss.kb)) { + if (poss.kb.indexOf(kblast) !== -1 ) { // check intersection of commonL and suggestion's product-lists tempL = commonL.filter(value => poss.p.includes(value)); if (tempL.length) { @@ -245,55 +369,97 @@ $(document).ready(function() { }); } + // calculate unique products + let unique = proList.filter((item, i, ar) => ar.indexOf(item) === i); + // credit: https://stackoverflow.com/questions/11246758/ - if ((qAr.length != 1) && (proList.length < 13)) { - // get unique product ids - let unique = proList.filter((item, i, ar) => ar.indexOf(item) === i); - // credit: https://stackoverflow.com/questions/11246758/ - + // console.log(q, qAr, qAr.length, proList.length); + + if (unique.length < (maxResults +1)) { results = []; unique.forEach( pr => { - everyProduct.forEach( pi => { + _products.forEach( pi => { if (pi.id == pr) results.push(pi); }) }); } + return results; + } + + // request suggestions procedure + // args... + // qOrig: original query string + // list: artay structure to host results + // --- + var isuggest = function(qOrig, list) { + var results = suggestions_engine(qOrig); + + // if no results... + // request again after removing last (key)word + if (results.length == 0) { + var qAr = qOrig.trim().split(' '); + qAr.pop(); // remove last word + results = suggestions_engine(qAr.join(' ')); + } + list(results); } - // setup suggestions search/input control - // --- + // setup suggestions search/input control + // --- const $tagsInput = $('#tagsInput') $tagsInput.typeahead( { hint: true, highlight: true, - minLength: 0 + minLength: 1 }, { - limit: 12, - name: 'products', + limit: maxResults, + name: 'kwlinks', displayKey: 'w', source: isuggest, - templates: { - suggestion: function(data) { - console.log(data.w); - if (data.id) - return '<div>'+ data.w + '<span>' + data.id + '</span></div>'; - return '<div>'+ data.w +'</div>'; - } - } + templates: { + suggestion: function(data) { + // console.log(data.w); + if (data.id) + return '<div>'+ data.w + '<span>' + data.id + '</span></div>'; + return '<div>'+ data.w +'<span>+</span></div>'; + } + } } ) .bind("typeahead:selected", function(obj, datum, name) { - $('.typeahead').typeahead('val','').trigger('blur'); - $('.typeahead').typeahead('val', datum.w +' ') + // console.log(datum); + if (datum.hasOwnProperty('id')) { + // final product selected; do whatever ... + // ex. add to selection list + $('#selections').append('<div>'+ datum.w + '<span>' + datum.id + '</span></div>'); + + // then reset search control + $('.typeahead').typeahead('val','').trigger('blur') .trigger("query"); - // give some time to the engine to calculate results - // then fire focus again... - setTimeout(() => { $('.typeahead').focus(); }, 100); + setTimeout(() => { $('.typeahead').focus(); }, 100); + + // finaly save tracking info; + $('#selections').append('<div class="-info-">'+ echo_tracking() +'</div>'); + trackSearch.length = 0; // ... and reset info to be ready for nextsearch + } + else { + $('.typeahead').typeahead('val','').trigger('blur'); + $('.typeahead').typeahead('val', datum.w +' ') + .trigger("query"); + // give some time to the engine to calculate results + // then fire focus again... + setTimeout(() => { $('.typeahead').focus(); }, 100); + } + trackSearch.push({ + e: 'key', + v: 'Enter', + i: $('#tagsInput').val() + }); }) .bind("typeahead:cursorchange", function(obj, data) { // console.log(obj, data); @@ -301,8 +467,27 @@ $(document).ready(function() { // console.log('triggered cursorchange /'+dt); }); + // TRACK user search attempt /////////////////////////////////////////////// + $('.typeahead').on('keyup', function(e) { + trackSearch.push({ + e: 'key', + v: e.key, + i: $('#tagsInput').val() + }); + }); + +}); + + + + +// also check (for suearching benchmarks) +// --- +// https://www.measurethat.net/Benchmarks/Show/13675/0/regextest-vs-stringincludes-vs-stringmatch +// https://stackoverflow.com/questions/5296268/fastest-way-to-check-a-string-contain-another-substring-in-javascript +// https://stackoverflow.com/questions/40387106/string-startwith-vs-regex +// https://www.measurethat.net/Benchmarks/Show/4797/1/js-regex-vs-startswith-vs-indexof -}); - </script> -</html> + </script> +</html>
\ No newline at end of file diff --git a/html/search2.html b/html/search2.html new file mode 100644 index 0000000..bb88958 --- /dev/null +++ b/html/search2.html @@ -0,0 +1,642 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + + <style> +body { font-family: 'Cantarell', Helvetica, Arial, sans-serif; margin: 2em; } + +.twitter-typeahead { width: 87%; } +.typeahead, .tt-query, .tt-hint { + width: 100%; height: 30px; + padding: 8px 12px; outline: none; + font-size: 20px; line-height: 30px; + border: 2px solid #ccc; border-radius: 8px; + -webkit-border-radius: 8px; + -moz-border-radius: 8px; +} +.tt-menu { + width: 100%; margin: 12px 0; padding: 8px 0; + background-color: #fff; + border: 1px solid #ccc; border-radius: 8px; + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); + -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); + box-shadow: 0 5px 10px rgba(0,0,0,.2); +} +.tt-suggestion { padding: 3px 20px; line-height: 24px; font-size: 18px; } +.tt-suggestion:hover { cursor: pointer; } +.tt-suggestion span { padding-left: 16px; font-size: 14px; color: #777; float: right; } +.tt-cursor { background: #ddd; } +.tt-highlight { font-weight: normal; color: #777; } +.tt-hint { color: #9598; } + +#selections { width: 87%; padding-top: 40px; } +#selections div { padding: 4px 40px; line-height: 24px; font-size: 18px; color: #666; } +#selections div span { padding-left: 16px; font-size: 14px; color: #999; float: right; } +.-info- { font-size: 12px !important; color: #959 !important; + font-family: 'JetBrains Mono NL', Consolas, Monaco, monospace, fixed !important; + line-height: 14px !important; border-bottom: 1px solid #ddda; +} +.-info- b { font-weight: 900; } + </style> + + <!-- js labraries --> + <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.2.1/bloodhound.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/corejs-typeahead/1.2.1/typeahead.jquery.min.js"></script> + </head> + <body> + + <div id="the-basics"> + <input class="typeahead" id="tagsInput" type="text" placeholder="try me!"> + </div> + + <div id="selections"> + </div> + + </body> + <script> + + +// PUBLIC VARIABLES //////////////////////////////////////////////////////////// + +var _kwlinks; // keyword links (word-connections; imported via ajax-get) +var _products; // all products (imported via ajax-get) + +var trackSearch = []; // searching analytics + +// setup options +var _maxResults = 24; // limit suggestions +var _blendProds = 4; // minimum final-produncts to blend with next-word suggestions +var _timeout_ms = 100; // time (in ms) for the search engine to find matches (before rendering) +var _allowFuzzy = true; // enable|disable fuzzy search +var _fuzzyLimit = .5; // minimum bigram score for being considered a match +var _Ngram_base = 2 // number of N in Ngram spliting algorithm + + + +// SUPPLEMENTARY FUNCTIONS /////////////////////////////////////////////////////// + +// arrays for kb-format (utf/EL-Gr to ascii translation) +var ORiGiNal = 'ςερτυθιοπασδφγηξκλζχψωβνμΕΡΤΥΘΙΟΠΑΣΔΦΓΗΞΚΛΖΧΨΩΒΝΜάέήίόύώϊΐϋΆΈΉΊΌΎΏΪΫQWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789- '.split(''); +var kbKeyZed = 'sertyuiopasdfghjklzxcvbnmertyuiopasdfghjklzxcvbnmaehioyviiyaehioyviyqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm0123456789- '.split(''); +const map = new Map(); +for (var i=0; i<ORiGiNal.length; i++) map.set(ORiGiNal[i], kbKeyZed[i]); +// function kb_trans +// translates string to keyboard-latin keys; +// [map]'s implementation is 40x faster than [for]'s +function kb_trans(str) { + str = str.replace('\'',''); + var out = '' + for (var i=0 ; i< str.length; i++) out += map.get(str[i]); + return out; +} + +var ignoredKeys_kb = []; // keywords to ignore (in kb-format) +'μας με σε για του της των από στο στον &'.split(' ').forEach(w => { ignoredKeys_kb.push(kb_trans(w)); }); + + + +// pure JS ajax GET request; return data as JSON +// no fancy things like UTF8; if needed use base64 +// --- +function ajax_get(url, callback) { + var xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { + + try { + var data = JSON.parse(xmlhttp.responseText); + } catch(err) { + console.log(err.message + " in " + xmlhttp.responseText); + return; + } + callback(data); + } + }; + + xmlhttp.open("GET", url, true); + xmlhttp.send(); +} + + +// Ngram fuzzy match +// --- +const createNgram = (word, n) => { // Ngram creation + if (word.length <3) return word; + const vector = []; + for (let i = 0; i < word.length-n+1; ++i) { + vector.push(word.slice(i, i + n)); + } + return vector; +}; +const checkSimilarity = (a, b) => { // Ngram match score + if (!_allowFuzzy) return 0; + + if (a.length > 0 && b.length > 0) { + const aNgram = createNgram(a, _Ngram_base); + const bNgram = createNgram(b, _Ngram_base); + let hits = 0; + for (let x = 0; x < aNgram.length; ++x) { + for (let y = 0; y < bNgram.length; ++y) { + if (aNgram[x] === bNgram[y]) { + hits += 1; + } + } + } + if (hits > 0) { + const union = aNgram.length + bNgram.length; + return (2.0 * hits) / union; + } + } + return 0; +}; + + +// Search Endine's match functions --------------------------------------------- + + +// check if a searching string -> query (string/latin in kb-format) +// matches an item of the array of synonyms -> chkArr (array of utf-8/strings) +// + option to use fyzzy (bigram) match +// *** return: +// matched string (utf-8) --or-- false (if not matched) +// --- +function check_match( query, chkArr, fuzzy = false ) { + var result = ''; + var found = false; + chkArr.forEach( chk => { + if (!found) { + chk_kb = kb_trans(chk); + if ( (chk_kb.indexOf( query ) !== -1) || (fuzzy && (checkSimilarity(chk_kb, query) > _fuzzyLimit)) ) { + found = true; + result = chk; + } + } + }); + return found ? result : false; +} + + +// check if a searching string -> query (string/latin in kb-format) +// matches exactly an item of the array of synonyms -> chkArr (array of utf-8/strings) +// *** return: true|false +// --- +function is_exact_match( query, chkArr ) { + found = false; + chkArr.forEach( w => { if (kb_trans(w) == query) found = true }); + return found; +} + + +// check if at-least-ONE item from an array of query-words -> qArr (array of string/latin in kb-format) +// matches any item of an array of synonyms -> chkArr (array of utf-8/strings) +// *** return: true|false +// --- +function match_one( qArr, chkArr ) { + found = false; + qArr.forEach( query => { + chkArr.forEach( w => { if (kb_trans(w) == query) found = true }); + }); + return found; +} + + +// compact print of search-tracking +// --- +function echo_tracking() { + var actions = []; + var c; + var chs = 0; // number of characters pressed; + var uis = 0; // number of UI actions used (arrows, enters etc.) + var countingStarted = false; // flag + trackSearch.forEach(e => { + switch(e.v) { + // use of ui actions + case 'ArrowDown' : c = '↓'; uis++; break; + case 'ArrowUp' : c = '↑'; uis++; break; + case 'Enter' : c = '↲ '; uis++; break; + case 'ArrowLeft' : c = '←'; uis++; break; + case 'ArrowRight': c = '→'; uis++; break; + case ' ' : c = '· '; uis++; break; + // ignored keys + case 'Alt' : c = 'Alt'; break; + case 'Control' : c = 'Ctrl'; break; + case 'Escape' : c = 'Esc'; break; + case 'Shift' : c = 'Shft'; break; + case 'Home' : c = 'Home'; break; + case 'End' : c = 'End'; break; + // backspace (user's typing errors) + case 'Backspace' : c = 'BkSp'; break; + case 'Delete' : c = 'Del'; break; + // actual typed characters + default: + if (e.v.length == 1) { + c = '<b><u>'+ e.v +'</u></b>'; + chs++; + } + else { // some non important key; no counter increased + c = e.v; // just record the key + } + } + actions.push(c); + }); + + return actions.join(',') +' (<u>'+ chs +' chs</u>, '+ uis +' uis)'; +} + + +// workline object to track status of async svents +// --- +var workline = { + + trackerJL : 0, + set jsonLoaded(x) { + this.trackerJL = x; + + // fire event on certain values + if (x == 2) { + console.log('All streams loaded'); + + // code to execute + // ... + + } + }, + get jsonLoaded() { return this.trackerJL; } + +}; + + + +// LOAD DATA /////////////////////////////////////////////////////////////////// + +ajax_get('results/keywords.json', function(data) { + _kwlinks = data; + workline.jsonLoaded++; console.log('keywords loaded'); +}); + +ajax_get('results/products.json', function(data) { + _products = data; + workline.jsonLoaded++; console.log('products loaded') +}); + + +// on document ready code /////////////////////////////////////////////////////// +$(document).ready(function() { + + // suggestions engine ////////////////////////////////////////////////////// + // --- + function suggestions_engine(qOrig) { + var results = []; // suggestions to respond + var proList = []; // list of products (for all suggestions) + var commonL = []; // list of common products (for multiple suggestions) + var possibleNext = []; // list of possible next suggestions + + var root, last; + + // clean q(uery) string from symbols and multiple spaces + var q = qOrig.replace('+',' ').replace('.',' ') + .replace(' ',' ') + .replace(' ',' '); + + var qAr = q.split(' '); // split to words + + if (qAr.length == 1) { // suggest 1st word //////////////////////// + var kbq = kb_trans(q) + + // loop through root-words + // ... to match all possible suggestions; + _kwlinks.forEach( it => { + chk = check_match(kbq, it.w, true); + + if ( chk != false ) { + results.push({ w: chk }); + + // ... also keep possible products in a list + it.c.forEach( wo => { // for every Word-Link-Node + if (proList.length < _maxResults + 1) { + // concatenate this-suggestion's product sub-list (wo.p) + // to all-suggestions posiible products-list (proList) + proList = proList.concat(wo.p); + // keep unique products in the products-list + proList = proList.filter((item, i, ar) => ar.indexOf(item) === i); + } + }); + } + + }); + } + + if (qAr.length == 2) { // suggest 2nd word /////////////////////////// + root = qAr[0]; + last = qAr[1]; + kbroot = kb_trans(root); + kblast = kb_trans(last); + + _kwlinks.forEach( it => { // locate the ... + if (is_exact_match(kbroot, it.w)) { // exact match of root-word + + it.c.forEach ( wo => { // loop the word-links ... + chk = check_match(kblast, wo.w, true); + if ( chk !== false ) { // if a match is found + results.push({ // keep suggestion + w: root +' '+ chk, + f: wo.f + }); + if (proList.length < _maxResults + 1) { // plus... + proList = proList.concat(wo.p); // keep products-list + proList = proList.filter((item, i, ar) => ar.indexOf(item) === i); + } + } + }); + + } + }); + } + + if (qAr.length > 2) { // suggest N-th word (N>2) ///////////////// + root = qAr.shift(); // isolate first item of qAr + last = qAr.pop(); // isolate last item too + // now qAr includes only the items after root and before last; + // so qAr includes all already selected suggestions (but root) + + // prepare/cache kb-formated string for any key we're going to use + var kb_qAr = []; // array of selected suggestions in kb-format + qAr.forEach( w => { kb_qAr.push(kb_trans(w)); }) + kbroot = kb_trans(root); + kblast = kb_trans(last); + + _kwlinks.forEach( it => { + if (is_exact_match(kbroot, it.w)) { // locate root-word + + // calculate list of common items/products (commonL) + // for selected suggestions + is1stOcc = true; // 1st occurance flag + + it.c.forEach ( swo => { + if (match_one(kb_qAr, swo.w)) { + // swo is one of the already selected suggestions + // so... update the common-(products)-L(ist) + if (is1stOcc) { + commonL = swo.p; // init list (on 1st occurance) + is1stOcc = false; + } + else { + // caclulate list of common products + // = intersection of (so-far) commonL and swo.p + // commonL = commonL.filter(value => swo.p.includes(value)); + commonL = commonL.filter(function(n) { return swo.p.indexOf(n) !== -1; }); + } + } + else { // if swo is not already selected + // then This is a possible NEXT suggestion + possibleNext.push(swo); + } + }); + + // Now that we have all the possible next suggestions + // we'll match them with the last word of the query + + possibleNext.forEach( poss => { // for tthe possible next suggestions + // if last-word-of-query matches possible word(s) + // and list of word's products has commons with commonL + // then it is a valid next suggestion + chk = check_match(kblast, poss.w, true); + if (chk !== false) { + // check intersection of commonL and suggestion's product-lists + tempL = commonL.filter(value => poss.p.includes(value)); + if (tempL.length > 0) { + results.push({ + w: root +' '+ qAr.join(' ') +' '+ chk, + f: poss.f + }); + // update proList too + proList = proList.concat(tempL); + } + } + }); + // NOTE: test before enable + // return false; // escape from forEach + } + + }); + } + + // calculate unique products + let unique = proList.filter((item, i, ar) => ar.indexOf(item) === i); + // credit: https://stackoverflow.com/questions/11246758/ + + // console.log(q, qAr, qAr.length, proList.length); + + if (unique.length < (_maxResults +1)) { + results = []; + unique.forEach( pr => { + _products.forEach( pi => { + if (pi.id == pr) + results.push(pi); + }) + }); + } + + return results; + } + + // simple, fast search products by numeric code property + // --- + function search_by_code( num ) { + const str = num.toString(); + results = []; + + _products.forEach(p => { + if (results.length > _maxResults) return false; + + if ( (p.bp+'-'+p.bc).indexOf(str) !== -1 ) { + results.push(p) + } + }); + return results; + } + + // suggestions caller (router function) + // arguments: + // * qOrig : original query string + // ** list : callback array structure to host results + // --- + var isuggest = function(qOrig, list) { + var results; + + // if query seems to be some king of 'code/id' + if (qOrig.length>2 && qOrig.match(/^[0-9]+$/) != null) { + results = search_by_code(qOrig); + } + else { + // string match procedure with suggestions engine + results = suggestions_engine(qOrig); + + // depricated: + // if no results request again after removing last (key)word + ////// if (results.length == 0) { + ////// console.log('no results found;', qOrig.split(' ')); + ////// // var qAr = qOrig.trim().split(' '); + ////// // qAr.pop(); // remove last word + ////// // results = suggestions_engine(qAr.join(' ')); + ////// } + } + list(results); + } + + // setup suggestions search/input control + // --- + const $tagsInput = $('#tagsInput') + $tagsInput.typeahead( + { + hint: true, + highlight: true, + minLength: 1 + }, + { + limit: _maxResults, + name: 'kwlinks', + displayKey: 'w', + source: isuggest, + templates: { + suggestion: function(data) { + // console.log(data.w); + if (data.id) + return '<div>'+ data.w + '<span>' + data.bp +'-'+ data.bc + '</span></div>'; + return '<div>'+ data.w +'<span>+</span></div>'; + } + } + } + ) + .bind("typeahead:selected", function(obj, datum, name) { + // console.log(datum); + if (datum.hasOwnProperty('id')) { + // final product selected; do whatever ... + // ex. add to selection list + $('#selections').append('<div>'+ datum.w + '<span>' + datum.id + '</span></div>'); + + // then reset search control + $('.typeahead').typeahead('val','').trigger('blur') + .trigger("query"); + setTimeout(() => { $('.typeahead').focus(); }, _timeout_ms); + + // finaly save tracking info; + $('#selections').append('<div class="-info-">'+ echo_tracking() +'</div>'); + trackSearch.length = 0; // ... and reset info to be ready for nextsearch + } + else { + $('.typeahead').typeahead('val','').trigger('blur'); + $('.typeahead').typeahead('val', datum.w +' ') + .trigger("query"); + // give some time to the engine to calculate results + // then fire focus again... + setTimeout(() => { $('.typeahead').focus(); }, _timeout_ms); + } + trackSearch.push({ + e: 'key', + v: 'Enter', + i: $('#tagsInput').val() + }); + }) + .bind("typeahead:cursorchange", function(obj, data) { + // console.log(obj, data); + // var dt = new Date(); + // console.log('triggered cursorchange /'+dt); + }); + + // TRACK user search attempt /////////////////////////////////////////////// + $('.typeahead').on('keyup', function(e) { + trackSearch.push({ + e: 'key', + v: e.key, + i: $('#tagsInput').val() + }); + }); + +}); + + +// NOTE: +// there is a strong chance that list-intersection and list-concatenation algos +// can be optimized further (for CPU and RAM usage); check the following sources ... +// --- +// https://javascript.plainenglish.io/algorithms-101-includes-vs-indexof-in-javascript-7f1b4af04127 +// https://www.measurethat.net/Benchmarks/Show/8221/0/array-indexof-vs-includes-vs-some +// https://www.measurethat.net/Benchmarks/Show/4223/0/array-concat-vs-spread-operator-vs-push +// HINT: +// --- +// array.some() seems to be much faster than array.includes() and array.indexOf() +// also array.push(list) seems to be an awesome alternative vs concat and Set + + + </script> +</html> + +<!-- + oneliner tests + +totl = 0 +_products.forEach(p => { if (kb_trans(p.w).indexOf(kb_trans("xvris")) !=-1) totl++; }) + +totl = 0 +_products.forEach(p => { if (kb_trans(p.w).indexOf(kb_trans("eisagvghs")) !=-1) totl++; }) + +_products.forEach(p => { if (kb_trans(p.w).indexOf(kb_trans("dvro")) !=-1) totl++; }) +_products.forEach(p => { if (kb_trans(p.w).indexOf(kb_trans("geysh")) !=-1) totl++; }) +_products.forEach(p => { if (kb_trans(p.w).indexOf(kb_trans("xvris prosuhkh zaxarhs")) !=-1) totl++; }) + + +χωρίς +εισαγωγής +δώρο +γεύση +γεύσεις +φέτες +Χωρίς-Γλουτένη +Χωρίς-Ζάχαρη +Χωρίς-Αλάτι +Χωρίς-Λακτόζη +Χωρίς-Συντηρητικά +Χωρίς-Αλκοόλ +Χωρίς-Kαφεϊνη +Χωρίς-Γλυκάνισο +Χωρίς-Ανθρακικό +Υψηλής-Παστερίωσης +Ολες-τις-Χρήσεις +Ολικής-Άλεσης +Ολικής-Aλέσεως +Ολικής +Γαϊδούρας +Γαϊδάρου +Ρούχων +Πιάτων +πλύσεις +Πλυντηρίου +Φύλλων +Γάλακτος +Χρήσης +Τύπου +Ολλανδίας +Απορριμμάτων +Medium +Μαλλιά +Μαλλιών +Γενικής +Plus +Classic +Έκπληξη +Μάνης +Ελάτου +Άγριων +Βοτάνων +Λακωνίας + +--> + +<!-- + +-->
\ No newline at end of file |
