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/search.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/search.html')
| -rw-r--r-- | html/search.html | 403 |
1 files changed, 294 insertions, 109 deletions
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 |
