From 456bc59240b631ffcfe1d22f99e2d6a3bf047973 Mon Sep 17 00:00:00 2001 From: IceSandwich Date: Mon, 14 Jul 2025 14:36:01 +0800 Subject: [PATCH 01/12] improve performance by changing the search engine to flexsearch --- web/js/autocomplete.js | 121 +++++++++++------- web/js/data.js | 40 ++++++ web/js/settings.js | 2 +- .../flexsearch.bundle.module.min.js | 106 +++++++++++++++ web/js/utils.js | 4 +- 5 files changed, 224 insertions(+), 49 deletions(-) create mode 100644 web/js/thirdparty/flexsearch.bundle.module.min.js diff --git a/web/js/autocomplete.js b/web/js/autocomplete.js index 91901fe..e4d8402 100644 --- a/web/js/autocomplete.js +++ b/web/js/autocomplete.js @@ -103,59 +103,85 @@ function searchCompletionCandidates(textareaElement) { const sources = getEnabledTagSourceInPriorityOrder(); for (const source of sources) { - // Search in sortedTags (already sorted by count) - for (const tagData of autoCompleteData[source].sortedTags) { - let matched = false; - let isExactMatch = false; - let matchedAlias = null; + if (autoCompleteData[source].use_fuse && autoCompleteData[source].fuse) { + const searchResults = autoCompleteData[source].fuse.search(partialTag, { + limit: settingValues.maxSuggestions, + suggest: false, + cache: true, + }); + if (settingValues._logprocessingTime) { + const endTime = performance.now(); + const duration = endTime - startTime; + console.debug(`[Autocomplete-Plus] Fast Search for "${partialTag}" in ${source} took ${duration.toFixed(2)}ms. Found ${searchResults.length} candidates.`); + } + let result = []; + searchResults.sort((a, b) => { + if (matchWord(autoCompleteData[source].sortedTags[b].tag, queryVariations).matched) { + return 999999999999; + } + if (matchWord(autoCompleteData[source].sortedTags[a].tag, queryVariations).matched) { + return -999999999999; + } + return autoCompleteData[source].sortedTags[b].count - autoCompleteData[source].sortedTags[a].count; + }).forEach(seachResult => { + result.push(autoCompleteData[source].sortedTags[seachResult]); + }); + return result; + } else { + // Search in sortedTags (already sorted by count) + for (const tagData of autoCompleteData[source].sortedTags) { + let matched = false; + let isExactMatch = false; + let matchedAlias = null; - // Check primary tag against all variations for exact/partial match - const tagMatch = matchWord(tagData.tag, queryVariations); - matched = tagMatch.matched; - isExactMatch = tagMatch.isExactMatch; + // Check primary tag against all variations for exact/partial match + const tagMatch = matchWord(tagData.tag, queryVariations); + matched = tagMatch.matched; + isExactMatch = tagMatch.isExactMatch; - // If primary tag didn't match, check aliases against all variations - if (!matched && tagData.alias && Array.isArray(tagData.alias) && tagData.alias.length > 0) { - for (const alias of tagData.alias) { - const lowerAlias = alias.toLowerCase(); - const aliasMatch = matchWord(lowerAlias, queryVariations); - if (aliasMatch.matched) { - matched = true; - isExactMatch = aliasMatch.isExactMatch; - matchedAlias = alias; - break; - } - } - } + // If primary tag didn't match, check aliases against all variations + if (!matched && tagData.alias && Array.isArray(tagData.alias) && tagData.alias.length > 0) { + for (const alias of tagData.alias) { + const lowerAlias = alias.toLowerCase(); + const aliasMatch = matchWord(lowerAlias, queryVariations); + if (aliasMatch.matched) { + matched = true; + isExactMatch = aliasMatch.isExactMatch; + matchedAlias = alias; + break; + } + } + } - const tagSetKey = tagData.tag; + const tagSetKey = tagData.tag; - // Add candidate if matched and not already added - if (matched && !addedTags.has(tagSetKey)) { - // Add to exact matches or partial matches based on match type - if (isExactMatch) { - exactMatches.push(tagData); - } else { - partialMatches.push(tagData); - } + // Add candidate if matched and not already added + if (matched && !addedTags.has(tagSetKey)) { + // Add to exact matches or partial matches based on match type + if (isExactMatch) { + exactMatches.push(tagData); + } else { + partialMatches.push(tagData); + } - addedTags.add(tagSetKey); + addedTags.add(tagSetKey); - // Check if we've reached the maximum suggestions limit combining both arrays - if (exactMatches.length + partialMatches.length >= settingValues.maxSuggestions) { - // Return the combined results, prioritizing exact matches - const result = [...exactMatches, ...partialMatches].slice(0, settingValues.maxSuggestions); + // Check if we've reached the maximum suggestions limit combining both arrays + if (exactMatches.length + partialMatches.length >= settingValues.maxSuggestions) { + // Return the combined results, prioritizing exact matches + const result = [...exactMatches, ...partialMatches].slice(0, settingValues.maxSuggestions); - if (settingValues._logprocessingTime) { - const endTime = performance.now(); - const duration = endTime - startTime; - console.debug(`[Autocomplete-Plus] Search for "${partialTag}" took ${duration.toFixed(2)}ms. Found ${result.length} candidates (max reached).`); - } + if (settingValues._logprocessingTime) { + const endTime = performance.now(); + const duration = endTime - startTime; + console.debug(`[Autocomplete-Plus] Search for "${partialTag}" took ${duration.toFixed(2)}ms. Found ${result.length} candidates (max reached).`); + } - return result; // Early exit - } - } - } + return result; // Early exit + } + } + } + } } // Combine results, with exact matches first @@ -182,9 +208,10 @@ function getCurrentPartialTag(inputElement) { // Find the last newline or comma before the cursor const lastNewLine = text.lastIndexOf('\n', cursorPos - 1); const lastComma = text.lastIndexOf(',', cursorPos - 1); + const lastPeriod = text.lastIndexOf('.', cursorPos - 1); // Get the position of the last separator (newline or comma) before cursor - const lastSeparator = Math.max(lastNewLine, lastComma); + const lastSeparator = Math.max(lastNewLine, lastComma, lastPeriod); const start = lastSeparator === -1 ? 0 : lastSeparator + 1; // Check if the cursor is inside a prompt weight modifier (e.g., :1.2, :.5, :1.) @@ -918,4 +945,4 @@ export class AutocompleteEventHandler { */ handleClick(event) { } -} \ No newline at end of file +} diff --git a/web/js/data.js b/web/js/data.js index 0f57bfb..46e419a 100644 --- a/web/js/data.js +++ b/web/js/data.js @@ -1,3 +1,4 @@ +import { Index } from './thirdparty/flexsearch.bundle.module.min.js' import { settingValues } from "./settings.js"; // --- Constants --- @@ -63,6 +64,12 @@ export class TagData { class AutocompleteData { constructor() { + /** @type {Index} */ + this.fuse = null; + + /** @type {boolean} */ + this.use_fuse = false; + /** @type {TagData[]} */ this.sortedTags = []; @@ -182,6 +189,39 @@ async function loadTags(csvUrl, siteName) { } }); + autoCompleteData[siteName].fuse = new Index({ + tokenize: "forward", + }); + + let startIdx = 0; + const startTime = performance.now(); + function processChunkTasks() { + const chunkSize = 1000; + const end = Math.min(startIdx + chunkSize, autoCompleteData[siteName].sortedTags.length); + for (; startIdx < end; startIdx++) { + const tagData = autoCompleteData[siteName].sortedTags[startIdx]; + let allTags = []; + allTags.push(tagData.tag); + if (tagData.alias && Array.isArray(tagData.alias)) { + tagData.alias.forEach(alias => { + allTags.push(alias); + }); + } + allTags = [...new Set(allTags)]; + autoCompleteData[siteName].fuse.add(startIdx, allTags.join(',')); + } + + if (startIdx < autoCompleteData[siteName].sortedTags.length) { + setTimeout(processChunkTasks, 0); + // console.log(`[Autocomplete-Plus] Current porcess: ${startIdx}`); + } else { + const endTime = performance.now(); + const duration = endTime - startTime; + autoCompleteData[siteName].use_fuse = true; + console.debug(`[Autocomplete-Plus] Building ${autoCompleteData[siteName].sortedTags.length} index for ${siteName} took ${duration.toFixed(2)}ms.`); + } + } + processChunkTasks(); } catch (error) { console.error(`[Autocomplete-Plus] Failed to fetch or process tags from ${csvUrl}:`, error); } diff --git a/web/js/settings.js b/web/js/settings.js index 6b93e98..3155288 100644 --- a/web/js/settings.js +++ b/web/js/settings.js @@ -21,4 +21,4 @@ export const settingValues = { // Debugging settings (use internally) _hideWhenOutofFocus: true, // Hide UI when the input is out of focus _logprocessingTime: false, // Log processing time for debugging -} \ No newline at end of file +} diff --git a/web/js/thirdparty/flexsearch.bundle.module.min.js b/web/js/thirdparty/flexsearch.bundle.module.min.js new file mode 100644 index 0000000..10a4fda --- /dev/null +++ b/web/js/thirdparty/flexsearch.bundle.module.min.js @@ -0,0 +1,106 @@ +/**! + * FlexSearch.js v0.8.200 (Bundle/Module) + * Author and Copyright: Thomas Wilkerling + * Licence: Apache-2.0 + * Hosted by Nextapps GmbH + * https://github.com/nextapps-de/flexsearch + */ +var w;function H(a,c,b){const e=typeof b,d=typeof a;if("undefined"!==e){if("undefined"!==d){if(b){if("function"===d&&e===d)return function(h){return a(b(h))};c=a.constructor;if(c===b.constructor){if(c===Array)return b.concat(a);if(c===Map){var f=new Map(b);for(var g of a)f.set(g[0],g[1]);return f}if(c===Set){g=new Set(b);for(f of a.values())g.add(f);return g}}}return a}return b}return"undefined"===d?c:a}function aa(a,c){return"undefined"===typeof a?c:a}function I(){return Object.create(null)} +function M(a){return"string"===typeof a}function ba(a){return"object"===typeof a}function ca(a){const c=[];for(const b of a.keys())c.push(b);return c}function ea(a,c){if(M(c))a=a[c];else for(let b=0;a&&b"a1a".split(b).length; +this.numeric=H(a.numeric,e)}else{try{this.split=H(this.split,fa)}catch(d){this.split=/\s+/}this.numeric=H(a.numeric,H(this.numeric,!0))}this.prepare=H(a.prepare,null,this.prepare);this.finalize=H(a.finalize,null,this.finalize);b=a.filter;this.filter="function"===typeof b?b:H(b&&new Set(b),null,this.filter);this.dedupe=H(a.dedupe,!0,this.dedupe);this.matcher=H((b=a.matcher)&&new Map(b),null,this.matcher);this.mapper=H((b=a.mapper)&&new Map(b),null,this.mapper);this.stemmer=H((b=a.stemmer)&&new Map(b), +null,this.stemmer);this.replacer=H(a.replacer,null,this.replacer);this.minlength=H(a.minlength,1,this.minlength);this.maxlength=H(a.maxlength,1024,this.maxlength);this.rtl=H(a.rtl,!1,this.rtl);if(this.cache=b=H(a.cache,!0,this.cache))this.I=null,this.R="number"===typeof b?b:2E5,this.B=new Map,this.H=new Map,this.M=this.L=128;this.h="";this.N=null;this.A="";this.O=null;if(this.matcher)for(const d of this.matcher.keys())this.h+=(this.h?"|":"")+d;if(this.stemmer)for(const d of this.stemmer.keys())this.A+= +(this.A?"|":"")+d;return this};w.addStemmer=function(a,c){this.stemmer||(this.stemmer=new Map);this.stemmer.set(a,c);this.A+=(this.A?"|":"")+a;this.O=null;this.cache&&Q(this);return this};w.addFilter=function(a){"function"===typeof a?this.filter=a:(this.filter||(this.filter=new Set),this.filter.add(a));this.cache&&Q(this);return this}; +w.addMapper=function(a,c){if("object"===typeof a)return this.addReplacer(a,c);if(1a.length&&(this.dedupe||this.mapper))return this.addMapper(a,c);this.matcher||(this.matcher=new Map);this.matcher.set(a,c);this.h+=(this.h?"|":"")+a;this.N=null;this.cache&&Q(this);return this}; +w.addReplacer=function(a,c){if("string"===typeof a)return this.addMatcher(a,c);this.replacer||(this.replacer=[]);this.replacer.push(a,c);this.cache&&Q(this);return this}; +w.encode=function(a,c){if(this.cache&&a.length<=this.L)if(this.I){if(this.B.has(a))return this.B.get(a)}else this.I=setTimeout(Q,50,this);this.normalize&&("function"===typeof this.normalize?a=this.normalize(a):a=ka?a.normalize("NFKD").replace(ka,"").toLowerCase():a.toLowerCase());this.prepare&&(a=this.prepare(a));this.numeric&&3this.maxlength)){if(c){if(d[l])continue;d[l]=1}else{if(f===l)continue;f=l}if(b)e.push(l);else if(!this.filter||("function"===typeof this.filter?this.filter(l):!this.filter.has(l))){if(this.cache&&l.length<=this.M)if(this.I){var k=this.H.get(l);if(k||""===k){k&&e.push(k);continue}}else this.I=setTimeout(Q,50,this);if(this.stemmer){this.O||(this.O=new RegExp("(?!^)("+ +this.A+")$"));let u;for(;u!==l&&2this.stemmer.get(p))}if(l&&(this.mapper||this.dedupe&&1this.matcher.get(u)));if(l&&this.replacer)for(k=0;l&&kthis.R&&(this.H.clear(),this.M=this.M/1.1|0));if(l){if(l!==n)if(c){if(d[l])continue;d[l]=1}else{if(g===l)continue;g=l}e.push(l)}}}this.finalize&&(e=this.finalize(e)||e);this.cache&&a.length<=this.L&&(this.B.set(a,e),this.B.size>this.R&&(this.B.clear(),this.L=this.L/1.1|0));return e};function Q(a){a.I=null;a.B.clear();a.H.clear()};function ma(a,c,b){b||(c||"object"!==typeof a?"object"===typeof c&&(b=c,c=0):b=a);b&&(a=b.query||a,c=b.limit||c);let e=""+(c||0);b&&(e+=(b.offset||0)+!!b.context+!!b.suggest+(!1!==b.resolve)+(b.resolution||this.resolution)+(b.boost||0));a=(""+a).toLowerCase();this.cache||(this.cache=new R);let d=this.cache.get(a+e);if(!d){const f=b&&b.cache;f&&(b.cache=!1);d=this.search(a,c,b);f&&(b.cache=f);this.cache.set(a+e,d)}return d}function R(a){this.limit=a&&!0!==a?a:1E3;this.cache=new Map;this.h=""} +R.prototype.set=function(a,c){this.cache.set(this.h=a,c);this.cache.size>this.limit&&this.cache.delete(this.cache.keys().next().value)};R.prototype.get=function(a){const c=this.cache.get(a);c&&this.h!==a&&(this.cache.delete(a),this.cache.set(this.h=a,c));return c};R.prototype.remove=function(a){for(const c of this.cache){const b=c[0];c[1].includes(a)&&this.cache.delete(b)}};R.prototype.clear=function(){this.cache.clear();this.h=""};const na={normalize:!1,numeric:!1,dedupe:!1};const qa={};const ra=new Map([["b","p"],["v","f"],["w","f"],["z","s"],["x","s"],["d","t"],["n","m"],["c","k"],["g","k"],["j","k"],["q","k"],["i","e"],["y","e"],["u","o"]]);const sa=new Map([["ae","a"],["oe","o"],["sh","s"],["kh","k"],["th","t"],["ph","f"],["pf","f"]]),ta=[/([^aeo])h(.)/g,"$1$2",/([aeo])h([^aeo]|$)/g,"$1$2",/(.)\1+/g,"$1"];const ua={a:"",e:"",i:"",o:"",u:"",y:"",b:1,f:1,p:1,v:1,c:2,g:2,j:2,k:2,q:2,s:2,x:2,z:2,"\u00df":2,d:3,t:3,l:4,m:5,n:5,r:6};var va={Exact:na,Default:qa,Normalize:qa,LatinBalance:{mapper:ra},LatinAdvanced:{mapper:ra,matcher:sa,replacer:ta},LatinExtra:{mapper:ra,replacer:ta.concat([/(?!^)[aeo]/g,""]),matcher:sa},LatinSoundex:{dedupe:!1,include:{letter:!0},finalize:function(a){for(let b=0;b=g.length)c-=g.length;else{c=g[e?"splice":"slice"](c,b);const h=c.length;if(h&&(d=d.length?d.concat(c):c,b-=h,e&&(a.length-=h),!b))break;c=0}return d} +function za(a){if(!this||this.constructor!==za)return new za(a);this.index=a?[a]:[];this.length=a?a.length:0;const c=this;return new Proxy([],{get(b,e){if("length"===e)return c.length;if("push"===e)return function(d){c.index[c.index.length-1].push(d);c.length++};if("pop"===e)return function(){if(c.length)return c.length--,c.index[c.index.length-1].pop()};if("indexOf"===e)return function(d){let f=0;for(let g=0,h,k;g=this.priority*this.priority*3):(Ha=setTimeout(Ka,0),Ia=Date.now());if(Ja){const f=this;return new Promise(g=>{setTimeout(function(){g(f[a+"Async"].apply(f,c))},0)})}const d=this[a].apply(this,c);b=d.then?d:new Promise(f=>f(d));e&&b.then(e);return b}};let W=0; +function La(a={},c){function b(h){function k(m){m=m.data||m;const l=m.id,n=l&&f.h[l];n&&(n(m.msg),delete f.h[l])}this.worker=h;this.h=I();if(this.worker){d?this.worker.on("message",k):this.worker.onmessage=k;if(a.config)return new Promise(function(m){1E9c?a.slice(b,b+c):a,e?Ta.call(this,a):a;let d=[];for(let f=0,g,h;f=h){b-=h;continue}g=g.slice(b,b+c);h=g.length;b=0}h>c&&(g=g.slice(0,c),h=c);if(!d.length&&h>=c)return e?Ta.call(this,g):g;d.push(g);c-=h;if(!c)break}d=1a.length?this.result=a[0]:(this.result=Ya(a,c,b,!1,this.h),b=0));d&&(this.await=null);return d?this.resolve(c,b,e):this};Y.prototype.and=function(){return Ua(this,"and",Za,arguments)};function Za(a,c,b,e,d,f){if(!f&&!this.result.length)return d?this.result:this;let g;if(a.length)if(this.result.length&&a.unshift(this.result),2>a.length)this.result=a[0];else{let h=0;for(let k=0,m,l;ka.length)this.result=a[0];else{a:{f=b;var g=this.h;const h=[],k=I();let m=0;for(let l=0,n;lb||e?k.slice(e,b+e):k;else{if(ab||e)k=k.slice(e,b+ +e)}else{d=[];for(let n=0,u;ne)e-=u.length;else{if(b&&u.length>b||e)u=u.slice(e,b+e),b-=u.length,e&&(e-=u.length);d.push(u);if(!b)break}k=d}}return k} +function Ya(a,c,b,e,d){const f=[],g=I();let h;var k=a.length;let m;if(e)for(d=k-1;0<=d;d--){if(m=(e=a[d])&&e.length)for(k=0;kA&&(A=q.length+(q?1:0)),B=q.length+(q?1:0)+G.length,E+=D,ya.push(v.length),v.push({match:G})),q+=(q?" ":"")+G)}if(!x)F=y[C],q+=(q?" ":"")+F,k&&v.push({text:F});else if(k&&E>=k)break}E=ya.length*(f.length-2);if(p||t||k&&q.length-E>k)if(E=k+E-2*u,C=B-A,0y&&(y=0)),v=v.length-1){if(J>= +v.length){C[x+1]=1;J>=y.length&&(E[x+1]=1);continue}D-=u}q=v[J].text;if(L=t&&z[x])if(0L)if(C[x+1]=1,m)q=q.substring(0,L);else continue;(L-=q.length)||(L=-1);z[x]=L}else{C[x+1]=1;continue}if(D+q.length+1<=k)q=" "+q,B[x]+=q;else if(m)U=k-D-1,0=J){if(0>J){C[x]=1;E[x]=1;continue}D-=u}q=v[J].text;if(L=p&&F[x])if(0L)if(C[x]=1,m)q=q.substring(q.length- +L);else continue;(L-=q.length)||(L=-1);F[x]=L}else{C[x]=1;continue}if(D+q.length+1<=k)q+=" ",B[x]=q+B[x];else if(m)U=q.length+1-(k-D),0<=U&&U=y.length-1?Pa=1:Jb||e)a=a.slice(e,e+b);d&&(a=Ta.call(this,a))}return a} +function Ta(a){if(!this||!this.store)return a;if(this.db)return this.index.get(this.field[0]).db.enrich(a);const c=Array(a.length);for(let b=0,e;br;f--){g=t.substring(r,f);q=this.rtl?d-1-r:r;var h=this.score?this.score(c,t,p,g,q):wb(u, +m,p,d,q);xb(this,n,g,h,a,b)}break}case "bidirectional":case "reverse":if(1g?0:1),m,p,h-1,k-1),q=this.bidirectional&&t>f;xb(this,l,q?f:t,r,a,b,q?t:f)}}}}this.fastupdate||this.reg.add(a)}else c=""}this.db&&(c||this.commit_task.push({del:a}),this.S&&ub(this));return this}; +function xb(a,c,b,e,d,f,g){let h=g?a.ctx:a.map,k;if(!c[b]||g&&!(k=c[b])[g])if(g?(c=k||(c[b]=I()),c[g]=1,(k=h.get(g))?h=k:h.set(g,h=new Map)):c[b]=1,(k=h.get(b))?h=k:h.set(b,h=k=[]),h=h[e]||(h[e]=[]),!f||!h.includes(d)){if(h.length===2**31-1){c=new za(h);if(a.fastupdate)for(let m of a.reg.values())m.includes(h)&&(m[m.indexOf(h)]=c);k[e]=h=c}h.push(d);a.fastupdate&&((e=a.reg.get(d))?e.push(h):a.reg.set(d,[h]))}} +function wb(a,c,b,e,d){return b&&1b)&&(k=b,b=c,c=k);if(a.db)return a.db.get(c,b,e,d,f,g,h);a=b?(a=a.ctx.get(b))&&a.get(c):a.map.get(c);return a};function V(a,c){if(!this||this.constructor!==V)return new V(a);if(a){var b=M(a)?a:a.preset;b&&(a=Object.assign({},vb[b],a))}else a={};b=a.context;const e=!0===b?{depth:1}:b||{},d=M(a.encoder)?va[a.encoder]:a.encode||a.encoder||{};this.encoder=d.encode?d:"object"===typeof d?new la(d):{encode:d};this.resolution=a.resolution||9;this.tokenize=b=(b=a.tokenize)&&"default"!==b&&"exact"!==b&&b||"strict";this.depth="strict"===b&&e.depth||0;this.bidirectional=!1!==e.bidirectional;this.fastupdate=!!a.fastupdate; +this.score=a.score||null;(b=a.keystore||0)&&(this.keystore=b);this.map=b?new S(b):new Map;this.ctx=b?new S(b):new Map;this.reg=c||(this.fastupdate?b?new S(b):new Map:b?new T(b):new Set);this.T=e.resolution||3;this.rtl=d.rtl||a.rtl||!1;this.cache=(b=a.cache||null)&&new R(b);this.resolve=!1!==a.resolve;if(b=a.db)this.db=this.mount(b);this.S=!1!==a.commit;this.commit_task=[];this.commit_timer=null;this.priority=a.priority||4}w=V.prototype; +w.mount=function(a){this.commit_timer&&(clearTimeout(this.commit_timer),this.commit_timer=null);return a.mount(this)};w.commit=function(a,c){this.commit_timer&&(clearTimeout(this.commit_timer),this.commit_timer=null);return this.db.commit(this,a,c)};w.destroy=function(){this.commit_timer&&(clearTimeout(this.commit_timer),this.commit_timer=null);return this.db.destroy()};function ub(a){a.commit_timer||(a.commit_timer=setTimeout(function(){a.commit_timer=null;a.db.commit(a,void 0,void 0)},1))} +w.clear=function(){this.map.clear();this.ctx.clear();this.reg.clear();this.cache&&this.cache.clear();this.db&&(this.commit_timer&&clearTimeout(this.commit_timer),this.commit_timer=null,this.commit_task=[{clear:!0}]);return this};w.append=function(a,c){return this.add(a,c,!0)};w.contain=function(a){return this.db?this.db.has(a):this.reg.has(a)};w.update=function(a,c){const b=this,e=this.remove(a);return e&&e.then?e.then(()=>b.add(a,c)):this.add(a,c)}; +w.cleanup=function(){if(!this.fastupdate)return this;tb(this.map);this.depth&&tb(this.ctx);return this};w.searchCache=ma;w.export=function(a,c,b=0,e=0){let d,f;switch(e){case 0:d="reg";f=pb(this.reg);break;case 1:d="cfg";f=null;break;case 2:d="map";f=lb(this.map,this.reg.size);break;case 3:d="ctx";f=nb(this.ctx,this.reg.size);break;default:return}return rb.call(this,a,c,d,f,b,e)}; +w.import=function(a,c){if(c)switch("string"===typeof c&&(c=JSON.parse(c)),a=a.split("."),"json"===a[a.length-1]&&a.pop(),3===a.length&&a.shift(),a=1=l.length){e-=l.length;continue}const n=b?e+Math.min(l.length-e,b):l.length;for(let u=e;u=f.length)return[];if(!c&&!b)return f;f=f.slice(b,b+c);return e?d.enrich(f):f})}; +w.enrich=function(a){"object"!==typeof a&&(a=[a]);const c=this.db.transaction("reg","readonly").objectStore("reg"),b=[];for(let e=0;e{a.onsuccess=a.oncomplete=function(){c&&c(this.result);c=null;b(this.result)};a.onerror=a.onblocked=e;a=null})};export default {Index:V,Charset:va,Encoder:la,Document:Na,Worker:La,Resolver:Y,IndexedDB:Fb,Language:{}}; +export const Index=V;export const Charset=va;export const Encoder=la;export const Document=Na;export const Worker=La;export const Resolver=Y;export const IndexedDB=Fb;export const Language={}; \ No newline at end of file diff --git a/web/js/utils.js b/web/js/utils.js index ac0246d..2a9e3de 100644 --- a/web/js/utils.js +++ b/web/js/utils.js @@ -349,11 +349,13 @@ export function findAllTagPositions(text) { // Find the end of this tag (next comma or newline) let endPosComma = text.indexOf(',', startPos); let endPosNewline = text.indexOf('\n', startPos); + let endPosPeriod = text.indexOf('.', startPos); if (endPosComma === -1) endPosComma = text.length; if (endPosNewline === -1) endPosNewline = text.length; + if (endPosPeriod === -1) endPosPeriod = text.length; - const endPos = Math.min(endPosComma, endPosNewline); + const endPos = Math.min(endPosComma, endPosNewline, endPosPeriod); const tagText = text.substring(startPos, endPos); if (tagText.trim().length > 0) { From 8750b5dcacbfa4bb0bf38af190b41fbc1fdf5d81 Mon Sep 17 00:00:00 2001 From: IceSandwich Date: Mon, 14 Jul 2025 14:48:20 +0800 Subject: [PATCH 02/12] fix indents --- web/js/autocomplete.js | 144 ++++++++++++++++++++--------------------- web/js/data.js | 70 ++++++++++---------- web/js/settings.js | 2 +- web/js/utils.js | 4 +- 4 files changed, 110 insertions(+), 110 deletions(-) diff --git a/web/js/autocomplete.js b/web/js/autocomplete.js index e4d8402..998ceef 100644 --- a/web/js/autocomplete.js +++ b/web/js/autocomplete.js @@ -103,85 +103,85 @@ function searchCompletionCandidates(textareaElement) { const sources = getEnabledTagSourceInPriorityOrder(); for (const source of sources) { - if (autoCompleteData[source].use_fuse && autoCompleteData[source].fuse) { - const searchResults = autoCompleteData[source].fuse.search(partialTag, { - limit: settingValues.maxSuggestions, - suggest: false, - cache: true, - }); - if (settingValues._logprocessingTime) { - const endTime = performance.now(); - const duration = endTime - startTime; - console.debug(`[Autocomplete-Plus] Fast Search for "${partialTag}" in ${source} took ${duration.toFixed(2)}ms. Found ${searchResults.length} candidates.`); - } - let result = []; - searchResults.sort((a, b) => { - if (matchWord(autoCompleteData[source].sortedTags[b].tag, queryVariations).matched) { - return 999999999999; - } - if (matchWord(autoCompleteData[source].sortedTags[a].tag, queryVariations).matched) { - return -999999999999; - } - return autoCompleteData[source].sortedTags[b].count - autoCompleteData[source].sortedTags[a].count; - }).forEach(seachResult => { - result.push(autoCompleteData[source].sortedTags[seachResult]); - }); - return result; - } else { - // Search in sortedTags (already sorted by count) - for (const tagData of autoCompleteData[source].sortedTags) { - let matched = false; - let isExactMatch = false; - let matchedAlias = null; + if (autoCompleteData[source].use_fuse && autoCompleteData[source].fuse) { + const searchResults = autoCompleteData[source].fuse.search(partialTag, { + limit: settingValues.maxSuggestions, + suggest: false, + cache: true, + }); + if (settingValues._logprocessingTime) { + const endTime = performance.now(); + const duration = endTime - startTime; + console.debug(`[Autocomplete-Plus] Fast Search for "${partialTag}" in ${source} took ${duration.toFixed(2)}ms. Found ${searchResults.length} candidates.`); + } + let result = []; + searchResults.sort((a, b) => { + if (matchWord(autoCompleteData[source].sortedTags[b].tag, queryVariations).matched) { + return 999999999999; + } + if (matchWord(autoCompleteData[source].sortedTags[a].tag, queryVariations).matched) { + return -999999999999; + } + return autoCompleteData[source].sortedTags[b].count - autoCompleteData[source].sortedTags[a].count; + }).forEach(seachResult => { + result.push(autoCompleteData[source].sortedTags[seachResult]); + }); + return result; + } else { + // Search in sortedTags (already sorted by count) + for (const tagData of autoCompleteData[source].sortedTags) { + let matched = false; + let isExactMatch = false; + let matchedAlias = null; - // Check primary tag against all variations for exact/partial match - const tagMatch = matchWord(tagData.tag, queryVariations); - matched = tagMatch.matched; - isExactMatch = tagMatch.isExactMatch; + // Check primary tag against all variations for exact/partial match + const tagMatch = matchWord(tagData.tag, queryVariations); + matched = tagMatch.matched; + isExactMatch = tagMatch.isExactMatch; - // If primary tag didn't match, check aliases against all variations - if (!matched && tagData.alias && Array.isArray(tagData.alias) && tagData.alias.length > 0) { - for (const alias of tagData.alias) { - const lowerAlias = alias.toLowerCase(); - const aliasMatch = matchWord(lowerAlias, queryVariations); - if (aliasMatch.matched) { - matched = true; - isExactMatch = aliasMatch.isExactMatch; - matchedAlias = alias; - break; - } - } - } + // If primary tag didn't match, check aliases against all variations + if (!matched && tagData.alias && Array.isArray(tagData.alias) && tagData.alias.length > 0) { + for (const alias of tagData.alias) { + const lowerAlias = alias.toLowerCase(); + const aliasMatch = matchWord(lowerAlias, queryVariations); + if (aliasMatch.matched) { + matched = true; + isExactMatch = aliasMatch.isExactMatch; + matchedAlias = alias; + break; + } + } + } - const tagSetKey = tagData.tag; + const tagSetKey = tagData.tag; - // Add candidate if matched and not already added - if (matched && !addedTags.has(tagSetKey)) { - // Add to exact matches or partial matches based on match type - if (isExactMatch) { - exactMatches.push(tagData); - } else { - partialMatches.push(tagData); - } + // Add candidate if matched and not already added + if (matched && !addedTags.has(tagSetKey)) { + // Add to exact matches or partial matches based on match type + if (isExactMatch) { + exactMatches.push(tagData); + } else { + partialMatches.push(tagData); + } - addedTags.add(tagSetKey); + addedTags.add(tagSetKey); - // Check if we've reached the maximum suggestions limit combining both arrays - if (exactMatches.length + partialMatches.length >= settingValues.maxSuggestions) { - // Return the combined results, prioritizing exact matches - const result = [...exactMatches, ...partialMatches].slice(0, settingValues.maxSuggestions); + // Check if we've reached the maximum suggestions limit combining both arrays + if (exactMatches.length + partialMatches.length >= settingValues.maxSuggestions) { + // Return the combined results, prioritizing exact matches + const result = [...exactMatches, ...partialMatches].slice(0, settingValues.maxSuggestions); - if (settingValues._logprocessingTime) { - const endTime = performance.now(); - const duration = endTime - startTime; - console.debug(`[Autocomplete-Plus] Search for "${partialTag}" took ${duration.toFixed(2)}ms. Found ${result.length} candidates (max reached).`); - } + if (settingValues._logprocessingTime) { + const endTime = performance.now(); + const duration = endTime - startTime; + console.debug(`[Autocomplete-Plus] Search for "${partialTag}" took ${duration.toFixed(2)}ms. Found ${result.length} candidates (max reached).`); + } - return result; // Early exit - } - } - } - } + return result; // Early exit + } + } + } + } } // Combine results, with exact matches first @@ -208,7 +208,7 @@ function getCurrentPartialTag(inputElement) { // Find the last newline or comma before the cursor const lastNewLine = text.lastIndexOf('\n', cursorPos - 1); const lastComma = text.lastIndexOf(',', cursorPos - 1); - const lastPeriod = text.lastIndexOf('.', cursorPos - 1); + const lastPeriod = text.lastIndexOf('.', cursorPos - 1); // Get the position of the last separator (newline or comma) before cursor const lastSeparator = Math.max(lastNewLine, lastComma, lastPeriod); diff --git a/web/js/data.js b/web/js/data.js index 46e419a..42e7376 100644 --- a/web/js/data.js +++ b/web/js/data.js @@ -64,11 +64,11 @@ export class TagData { class AutocompleteData { constructor() { - /** @type {Index} */ - this.fuse = null; + /** @type {Index} */ + this.fuse = null; - /** @type {boolean} */ - this.use_fuse = false; + /** @type {boolean} */ + this.use_fuse = false; /** @type {TagData[]} */ this.sortedTags = []; @@ -189,39 +189,39 @@ async function loadTags(csvUrl, siteName) { } }); - autoCompleteData[siteName].fuse = new Index({ - tokenize: "forward", - }); + autoCompleteData[siteName].fuse = new Index({ + tokenize: "forward", + }); - let startIdx = 0; - const startTime = performance.now(); - function processChunkTasks() { - const chunkSize = 1000; - const end = Math.min(startIdx + chunkSize, autoCompleteData[siteName].sortedTags.length); - for (; startIdx < end; startIdx++) { - const tagData = autoCompleteData[siteName].sortedTags[startIdx]; - let allTags = []; - allTags.push(tagData.tag); - if (tagData.alias && Array.isArray(tagData.alias)) { - tagData.alias.forEach(alias => { - allTags.push(alias); - }); - } - allTags = [...new Set(allTags)]; - autoCompleteData[siteName].fuse.add(startIdx, allTags.join(',')); - } + let startIdx = 0; + const startTime = performance.now(); + function processChunkTasks() { + const chunkSize = 1000; + const end = Math.min(startIdx + chunkSize, autoCompleteData[siteName].sortedTags.length); + for (; startIdx < end; startIdx++) { + const tagData = autoCompleteData[siteName].sortedTags[startIdx]; + let allTags = []; + allTags.push(tagData.tag); + if (tagData.alias && Array.isArray(tagData.alias)) { + tagData.alias.forEach(alias => { + allTags.push(alias); + }); + } + allTags = [...new Set(allTags)]; + autoCompleteData[siteName].fuse.add(startIdx, allTags.join(',')); + } - if (startIdx < autoCompleteData[siteName].sortedTags.length) { - setTimeout(processChunkTasks, 0); - // console.log(`[Autocomplete-Plus] Current porcess: ${startIdx}`); - } else { - const endTime = performance.now(); - const duration = endTime - startTime; - autoCompleteData[siteName].use_fuse = true; - console.debug(`[Autocomplete-Plus] Building ${autoCompleteData[siteName].sortedTags.length} index for ${siteName} took ${duration.toFixed(2)}ms.`); - } - } - processChunkTasks(); + if (startIdx < autoCompleteData[siteName].sortedTags.length) { + setTimeout(processChunkTasks, 0); + // console.log(`[Autocomplete-Plus] Current porcess: ${startIdx}`); + } else { + const endTime = performance.now(); + const duration = endTime - startTime; + autoCompleteData[siteName].use_fuse = true; + console.debug(`[Autocomplete-Plus] Building ${autoCompleteData[siteName].sortedTags.length} index for ${siteName} took ${duration.toFixed(2)}ms.`); + } + } + processChunkTasks(); } catch (error) { console.error(`[Autocomplete-Plus] Failed to fetch or process tags from ${csvUrl}:`, error); } diff --git a/web/js/settings.js b/web/js/settings.js index 3155288..6b93e98 100644 --- a/web/js/settings.js +++ b/web/js/settings.js @@ -21,4 +21,4 @@ export const settingValues = { // Debugging settings (use internally) _hideWhenOutofFocus: true, // Hide UI when the input is out of focus _logprocessingTime: false, // Log processing time for debugging -} +} \ No newline at end of file diff --git a/web/js/utils.js b/web/js/utils.js index 2a9e3de..2c816ba 100644 --- a/web/js/utils.js +++ b/web/js/utils.js @@ -349,11 +349,11 @@ export function findAllTagPositions(text) { // Find the end of this tag (next comma or newline) let endPosComma = text.indexOf(',', startPos); let endPosNewline = text.indexOf('\n', startPos); - let endPosPeriod = text.indexOf('.', startPos); + let endPosPeriod = text.indexOf('.', startPos); if (endPosComma === -1) endPosComma = text.length; if (endPosNewline === -1) endPosNewline = text.length; - if (endPosPeriod === -1) endPosPeriod = text.length; + if (endPosPeriod === -1) endPosPeriod = text.length; const endPos = Math.min(endPosComma, endPosNewline, endPosPeriod); const tagText = text.substring(startPos, endPos); From a374ca6c118227ae2d41e3f76d03666425f13bd1 Mon Sep 17 00:00:00 2001 From: newtextdoc1111 Date: Wed, 16 Jul 2025 08:00:24 +0900 Subject: [PATCH 03/12] feat: Refactor autocomplete data handling to build FlexSearch index after tags are loaded --- web/js/autocomplete.js | 4 ++-- web/js/data.js | 44 ++++++++++++++++++++++++++++-------------- web/js/settings.js | 1 + 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/web/js/autocomplete.js b/web/js/autocomplete.js index 998ceef..62d4aaa 100644 --- a/web/js/autocomplete.js +++ b/web/js/autocomplete.js @@ -103,8 +103,8 @@ function searchCompletionCandidates(textareaElement) { const sources = getEnabledTagSourceInPriorityOrder(); for (const source of sources) { - if (autoCompleteData[source].use_fuse && autoCompleteData[source].fuse) { - const searchResults = autoCompleteData[source].fuse.search(partialTag, { + if (settingValues.useFastSearch && autoCompleteData[source].flexSearchIndex) { + const searchResults = autoCompleteData[source].flexSearchIndex.search(partialTag, { limit: settingValues.maxSuggestions, suggest: false, cache: true, diff --git a/web/js/data.js b/web/js/data.js index 42e7376..91b97ab 100644 --- a/web/js/data.js +++ b/web/js/data.js @@ -1,4 +1,4 @@ -import { Index } from './thirdparty/flexsearch.bundle.module.min.js' +import { Charset, Index } from './thirdparty/flexsearch.bundle.module.min.js' import { settingValues } from "./settings.js"; // --- Constants --- @@ -65,10 +65,7 @@ export class TagData { class AutocompleteData { constructor() { /** @type {Index} */ - this.fuse = null; - - /** @type {boolean} */ - this.use_fuse = false; + this.flexSearchIndex = null; /** @type {TagData[]} */ this.sortedTags = []; @@ -189,8 +186,20 @@ async function loadTags(csvUrl, siteName) { } }); - autoCompleteData[siteName].fuse = new Index({ + } catch (error) { + console.error(`[Autocomplete-Plus] Failed to fetch or process tags from ${csvUrl}:`, error); + } +} + +/** + * Build FlexSearch index for the given site name. + * @param {string} siteName + */ +async function buildFlexSearchIndex(siteName) { + try { + const index = new Index({ tokenize: "forward", + // encoder: Charset.CJK, // FIXME: English search doesn't work properly when using CJK }); let startIdx = 0; @@ -208,7 +217,7 @@ async function loadTags(csvUrl, siteName) { }); } allTags = [...new Set(allTags)]; - autoCompleteData[siteName].fuse.add(startIdx, allTags.join(',')); + index.add(startIdx, allTags.join(',')); } if (startIdx < autoCompleteData[siteName].sortedTags.length) { @@ -217,13 +226,13 @@ async function loadTags(csvUrl, siteName) { } else { const endTime = performance.now(); const duration = endTime - startTime; - autoCompleteData[siteName].use_fuse = true; + autoCompleteData[siteName].flexSearchIndex = index; console.debug(`[Autocomplete-Plus] Building ${autoCompleteData[siteName].sortedTags.length} index for ${siteName} took ${duration.toFixed(2)}ms.`); } } processChunkTasks(); } catch (error) { - console.error(`[Autocomplete-Plus] Failed to fetch or process tags from ${csvUrl}:`, error); + console.error(`[Autocomplete-Plus] Failed to building flexSearch index`, error); } } @@ -415,12 +424,17 @@ export async function initializeData(csvListData, source) { // Now, execute all promise factories and wait for their completion. // The actual loading (fetch calls) will start when the factories are invoked here. await Promise.all([ - Promise.all(tagsLoadPromiseFactories.map(factory => factory())).then(() => { - const endTime = performance.now(); - if (csvListData[source].base_tags) { - console.log(`[Autocomplete-Plus] "${source}" Tags loading complete in ${(endTime - startTime).toFixed(2)}ms`); - } - }), + Promise.all(tagsLoadPromiseFactories.map(factory => factory())) + .then(() => { + // Build FlexSearch index after tags are loaded + return buildFlexSearchIndex(source); + }) + .then(() => { + const endTime = performance.now(); + if (csvListData[source].base_tags) { + console.log(`[Autocomplete-Plus] "${source}" Tags loading complete in ${(endTime - startTime).toFixed(2)}ms`); + } + }), Promise.all(cooccurrenceLoadPromiseFactories.map(factory => factory())).then(() => { const endTime = performance.now(); if (csvListData[source].base_cooccurrence) { diff --git a/web/js/settings.js b/web/js/settings.js index 6b93e98..8882bed 100644 --- a/web/js/settings.js +++ b/web/js/settings.js @@ -7,6 +7,7 @@ export const settingValues = { // Autocomplete feature settings enabled: true, maxSuggestions: 10, + useFastSearch: false, // Related tags feature settings enableRelatedTags: true, From e0728322162cfdd65bfa5ea4c32550b1857b3d0a Mon Sep 17 00:00:00 2001 From: newtextdoc1111 Date: Wed, 16 Jul 2025 08:01:11 +0900 Subject: [PATCH 04/12] fix: Improve sorting logic in searchCompletionCandidates for better tag matching --- web/js/autocomplete.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/js/autocomplete.js b/web/js/autocomplete.js index 62d4aaa..ce0b484 100644 --- a/web/js/autocomplete.js +++ b/web/js/autocomplete.js @@ -116,13 +116,17 @@ function searchCompletionCandidates(textareaElement) { } let result = []; searchResults.sort((a, b) => { - if (matchWord(autoCompleteData[source].sortedTags[b].tag, queryVariations).matched) { + const aTag = autoCompleteData[source].sortedTags[a]; + const bTag = autoCompleteData[source].sortedTags[b]; + if (matchWord(bTag.tag, queryVariations).isExactMatch || + (bTag.alias && bTag.alias.some(alias => matchWord(alias, queryVariations).isExactMatch))) { return 999999999999; } - if (matchWord(autoCompleteData[source].sortedTags[a].tag, queryVariations).matched) { + if (matchWord(aTag.tag, queryVariations).isExactMatch || + (aTag.alias && aTag.alias.some(alias => matchWord(alias, queryVariations).isExactMatch))) { return -999999999999; } - return autoCompleteData[source].sortedTags[b].count - autoCompleteData[source].sortedTags[a].count; + return bTag.count - aTag.count; }).forEach(seachResult => { result.push(autoCompleteData[source].sortedTags[seachResult]); }); From 1d85f3e284b6aaec49a13dcc680fc0c4bea50246 Mon Sep 17 00:00:00 2001 From: newtextdoc1111 Date: Wed, 16 Jul 2025 08:01:45 +0900 Subject: [PATCH 05/12] feat: Add 'Use Fast Search' setting to autocomplete options --- web/js/main.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/js/main.js b/web/js/main.js index 376a2cc..b99dd10 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -286,6 +286,16 @@ app.registerExtension({ } }, // --- Autocomplete Settings --- + { + id: id + ".use_fast_search", + name: "Use Fast Search", + type: "boolean", + defaultValue: false, + category: [name, "Autocompletion", "Use Fast Search"], + onChange: (newVal, oldVal) => { + settingValues.useFastSearch = newVal; + } + }, { id: id + ".max_suggestions", name: "Max suggestions", From f019960d3408a9a7555adb1ba500ffe1f010ebf2 Mon Sep 17 00:00:00 2001 From: newtextdoc1111 Date: Wed, 16 Jul 2025 08:14:33 +0900 Subject: [PATCH 06/12] fix: Remove unneccesary period handling in findAllTagPositions --- web/js/utils.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/js/utils.js b/web/js/utils.js index 2c816ba..ac0246d 100644 --- a/web/js/utils.js +++ b/web/js/utils.js @@ -349,13 +349,11 @@ export function findAllTagPositions(text) { // Find the end of this tag (next comma or newline) let endPosComma = text.indexOf(',', startPos); let endPosNewline = text.indexOf('\n', startPos); - let endPosPeriod = text.indexOf('.', startPos); if (endPosComma === -1) endPosComma = text.length; if (endPosNewline === -1) endPosNewline = text.length; - if (endPosPeriod === -1) endPosPeriod = text.length; - const endPos = Math.min(endPosComma, endPosNewline, endPosPeriod); + const endPos = Math.min(endPosComma, endPosNewline); const tagText = text.substring(startPos, endPos); if (tagText.trim().length > 0) { From 385f9d1c4f5f4d6d6cabfe9514b7d8705a1a5768 Mon Sep 17 00:00:00 2001 From: IceSandwich Date: Wed, 16 Jul 2025 10:58:03 +0800 Subject: [PATCH 07/12] remove period handling completely --- web/js/autocomplete.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/js/autocomplete.js b/web/js/autocomplete.js index ce0b484..e989538 100644 --- a/web/js/autocomplete.js +++ b/web/js/autocomplete.js @@ -212,10 +212,9 @@ function getCurrentPartialTag(inputElement) { // Find the last newline or comma before the cursor const lastNewLine = text.lastIndexOf('\n', cursorPos - 1); const lastComma = text.lastIndexOf(',', cursorPos - 1); - const lastPeriod = text.lastIndexOf('.', cursorPos - 1); // Get the position of the last separator (newline or comma) before cursor - const lastSeparator = Math.max(lastNewLine, lastComma, lastPeriod); + const lastSeparator = Math.max(lastNewLine, lastComma); const start = lastSeparator === -1 ? 0 : lastSeparator + 1; // Check if the cursor is inside a prompt weight modifier (e.g., :1.2, :.5, :1.) From b6c03233ba8c60cf99e7a2675a1c1ac01d6bf1ca Mon Sep 17 00:00:00 2001 From: IceSandwich Date: Wed, 16 Jul 2025 14:20:51 +0800 Subject: [PATCH 08/12] fix the behavior of matching words --- web/js/autocomplete.js | 17 ++++++++++++----- web/js/data.js | 27 ++++++++++++++++----------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/web/js/autocomplete.js b/web/js/autocomplete.js index e989538..8b0817a 100644 --- a/web/js/autocomplete.js +++ b/web/js/autocomplete.js @@ -105,7 +105,7 @@ function searchCompletionCandidates(textareaElement) { for (const source of sources) { if (settingValues.useFastSearch && autoCompleteData[source].flexSearchIndex) { const searchResults = autoCompleteData[source].flexSearchIndex.search(partialTag, { - limit: settingValues.maxSuggestions, + limit: settingValues.maxSuggestions * 10, suggest: false, cache: true, }); @@ -114,8 +114,13 @@ function searchCompletionCandidates(textareaElement) { const duration = endTime - startTime; console.debug(`[Autocomplete-Plus] Fast Search for "${partialTag}" in ${source} took ${duration.toFixed(2)}ms. Found ${searchResults.length} candidates.`); } + let result = []; - searchResults.sort((a, b) => { + result = searchResults.map((index) => { + return autoCompleteData[source].flexSearchMapping[index]; + }); + result = [...new Set(result)]; + result = result.sort((a, b) => { const aTag = autoCompleteData[source].sortedTags[a]; const bTag = autoCompleteData[source].sortedTags[b]; if (matchWord(bTag.tag, queryVariations).isExactMatch || @@ -126,9 +131,11 @@ function searchCompletionCandidates(textareaElement) { (aTag.alias && aTag.alias.some(alias => matchWord(alias, queryVariations).isExactMatch))) { return -999999999999; } - return bTag.count - aTag.count; - }).forEach(seachResult => { - result.push(autoCompleteData[source].sortedTags[seachResult]); + return a - b; + }); + result = result.slice(0, Math.min(result.length, settingValues.maxSuggestions)); + result = result.map((index) => { + return autoCompleteData[source].sortedTags[index]; }); return result; } else { diff --git a/web/js/data.js b/web/js/data.js index 91b97ab..6f26a4d 100644 --- a/web/js/data.js +++ b/web/js/data.js @@ -67,6 +67,9 @@ class AutocompleteData { /** @type {Index} */ this.flexSearchIndex = null; + /** @type {number[]} */ + this.flexSearchMapping = []; + /** @type {TagData[]} */ this.sortedTags = []; @@ -197,9 +200,12 @@ async function loadTags(csvUrl, siteName) { */ async function buildFlexSearchIndex(siteName) { try { + if (autoCompleteData[siteName].sortedTags.length === 0) { + return; + } + const index = new Index({ - tokenize: "forward", - // encoder: Charset.CJK, // FIXME: English search doesn't work properly when using CJK + tokenize: "bidirectional", }); let startIdx = 0; @@ -209,15 +215,14 @@ async function buildFlexSearchIndex(siteName) { const end = Math.min(startIdx + chunkSize, autoCompleteData[siteName].sortedTags.length); for (; startIdx < end; startIdx++) { const tagData = autoCompleteData[siteName].sortedTags[startIdx]; - let allTags = []; - allTags.push(tagData.tag); - if (tagData.alias && Array.isArray(tagData.alias)) { - tagData.alias.forEach(alias => { - allTags.push(alias); - }); - } - allTags = [...new Set(allTags)]; - index.add(startIdx, allTags.join(',')); + + index.add(autoCompleteData[siteName].flexSearchMapping.length, tagData.tag); + autoCompleteData[siteName].flexSearchMapping.push(startIdx); + + tagData.alias.forEach(alias => { + index.add(autoCompleteData[siteName].flexSearchMapping.length, alias); + autoCompleteData[siteName].flexSearchMapping.push(startIdx); + }) } if (startIdx < autoCompleteData[siteName].sortedTags.length) { From b4175e700b9f5ddfc1788f131a369285f077684b Mon Sep 17 00:00:00 2001 From: IceSandwich Date: Wed, 16 Jul 2025 15:46:22 +0800 Subject: [PATCH 09/12] fix the issue when input in tag and alias at the same time --- web/js/autocomplete.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/web/js/autocomplete.js b/web/js/autocomplete.js index 8b0817a..130d1ff 100644 --- a/web/js/autocomplete.js +++ b/web/js/autocomplete.js @@ -123,12 +123,16 @@ function searchCompletionCandidates(textareaElement) { result = result.sort((a, b) => { const aTag = autoCompleteData[source].sortedTags[a]; const bTag = autoCompleteData[source].sortedTags[b]; - if (matchWord(bTag.tag, queryVariations).isExactMatch || - (bTag.alias && bTag.alias.some(alias => matchWord(alias, queryVariations).isExactMatch))) { + if (matchWord(bTag.tag, queryVariations).isExactMatch) { return 999999999999; } - if (matchWord(aTag.tag, queryVariations).isExactMatch || - (aTag.alias && aTag.alias.some(alias => matchWord(alias, queryVariations).isExactMatch))) { + if (matchWord(aTag.tag, queryVariations).isExactMatch) { + return -999999999999; + } + if (bTag.alias && bTag.alias.some(alias => matchWord(alias, queryVariations).isExactMatch)) { + return 999999999999; + } + if (aTag.alias && aTag.alias.some(alias => matchWord(alias, queryVariations).isExactMatch)) { return -999999999999; } return a - b; From 9c6215139be70d47d805e82133d73e5950b0d7f4 Mon Sep 17 00:00:00 2001 From: IceSandwich Date: Thu, 17 Jul 2025 10:12:15 +0800 Subject: [PATCH 10/12] display actual candidate count when using fast search --- web/js/autocomplete.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/web/js/autocomplete.js b/web/js/autocomplete.js index 97c8781..dd1ad9f 100644 --- a/web/js/autocomplete.js +++ b/web/js/autocomplete.js @@ -112,11 +112,6 @@ function searchCompletionCandidates(textareaElement) { suggest: false, cache: true, }); - if (settingValues._logprocessingTime) { - const endTime = performance.now(); - const duration = endTime - startTime; - console.debug(`[Autocomplete-Plus] Fast Search for "${partialTag}" in ${source} took ${duration.toFixed(2)}ms. Found ${searchResults.length} candidates.`); - } let result = []; result = searchResults.map((index) => { @@ -144,6 +139,12 @@ function searchCompletionCandidates(textareaElement) { result = result.map((index) => { return autoCompleteData[source].sortedTags[index]; }); + + if (settingValues._logprocessingTime) { + const endTime = performance.now(); + const duration = endTime - startTime; + console.debug(`[Autocomplete-Plus] Fast Search for "${partialTag}" in ${source} took ${duration.toFixed(2)}ms. Found ${result.length} candidates within ${searchResults.length} searches with aliases.`); + } return result; } else { // Search in sortedTags (already sorted by count) From 59938c08d0d3cbd52f5da8cba9d6df473c719fdb Mon Sep 17 00:00:00 2001 From: IceSandwich Date: Thu, 17 Jul 2025 11:12:24 +0800 Subject: [PATCH 11/12] calculate the search limit from CSV files --- web/js/autocomplete.js | 2 +- web/js/data.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/web/js/autocomplete.js b/web/js/autocomplete.js index dd1ad9f..9b57d67 100644 --- a/web/js/autocomplete.js +++ b/web/js/autocomplete.js @@ -108,7 +108,7 @@ function searchCompletionCandidates(textareaElement) { for (const source of sources) { if (settingValues.useFastSearch && autoCompleteData[source].flexSearchIndex) { const searchResults = autoCompleteData[source].flexSearchIndex.search(partialTag, { - limit: settingValues.maxSuggestions * 10, + limit: autoCompleteData[source].flexSearchLimit, suggest: false, cache: true, }); diff --git a/web/js/data.js b/web/js/data.js index e0adc3c..2351758 100644 --- a/web/js/data.js +++ b/web/js/data.js @@ -70,6 +70,10 @@ class AutocompleteData { /** @type {number[]} */ this.flexSearchMapping = []; + /** @type {number} */ + // The actual number will be calculated later when loading CSV files + this.flexSearchLimit = settingValues.maxSuggestions * 10; + /** @type {TagData[]} */ this.sortedTags = []; @@ -211,6 +215,7 @@ async function buildFlexSearchIndex(siteName) { }); let startIdx = 0; + let maxCountOfAlias = 0; const startTime = performance.now(); function processChunkTasks() { const chunkSize = 1000; @@ -225,6 +230,8 @@ async function buildFlexSearchIndex(siteName) { index.add(autoCompleteData[siteName].flexSearchMapping.length, alias); autoCompleteData[siteName].flexSearchMapping.push(startIdx); }) + + maxCountOfAlias = Math.max(maxCountOfAlias, tagData.alias.length); } if (startIdx < autoCompleteData[siteName].sortedTags.length) { @@ -234,6 +241,7 @@ async function buildFlexSearchIndex(siteName) { const endTime = performance.now(); const duration = endTime - startTime; autoCompleteData[siteName].flexSearchIndex = index; + autoCompleteData[siteName].flexSearchLimit = settingValues.maxSuggestions * Math.min(10, maxCountOfAlias + 1); console.debug(`[Autocomplete-Plus] Building ${autoCompleteData[siteName].sortedTags.length} index for ${siteName} took ${duration.toFixed(2)}ms.`); } } From 6b34f3876f5c4f2c4b110fda97dea4cdcdb20c0f Mon Sep 17 00:00:00 2001 From: IceSandwich Date: Thu, 17 Jul 2025 20:51:59 +0800 Subject: [PATCH 12/12] change `flexSearchLimit` to `flexSearchLimitMultiplier` --- web/js/autocomplete.js | 2 +- web/js/data.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/js/autocomplete.js b/web/js/autocomplete.js index 9b57d67..98d11bf 100644 --- a/web/js/autocomplete.js +++ b/web/js/autocomplete.js @@ -108,7 +108,7 @@ function searchCompletionCandidates(textareaElement) { for (const source of sources) { if (settingValues.useFastSearch && autoCompleteData[source].flexSearchIndex) { const searchResults = autoCompleteData[source].flexSearchIndex.search(partialTag, { - limit: autoCompleteData[source].flexSearchLimit, + limit: settingValues.maxSuggestions * autoCompleteData[source].flexSearchLimitMultiplier, suggest: false, cache: true, }); diff --git a/web/js/data.js b/web/js/data.js index 2351758..d3aa762 100644 --- a/web/js/data.js +++ b/web/js/data.js @@ -72,7 +72,7 @@ class AutocompleteData { /** @type {number} */ // The actual number will be calculated later when loading CSV files - this.flexSearchLimit = settingValues.maxSuggestions * 10; + this.flexSearchLimitMultiplier = 10; /** @type {TagData[]} */ this.sortedTags = []; @@ -241,7 +241,7 @@ async function buildFlexSearchIndex(siteName) { const endTime = performance.now(); const duration = endTime - startTime; autoCompleteData[siteName].flexSearchIndex = index; - autoCompleteData[siteName].flexSearchLimit = settingValues.maxSuggestions * Math.min(10, maxCountOfAlias + 1); + autoCompleteData[siteName].flexSearchLimitMultiplier = Math.min(10, maxCountOfAlias + 1); console.debug(`[Autocomplete-Plus] Building ${autoCompleteData[siteName].sortedTags.length} index for ${siteName} took ${duration.toFixed(2)}ms.`); } }