From 6363e8d160ced35c8570b4b38e99ac8b9b1b1cf5 Mon Sep 17 00:00:00 2001 From: Precious Onyenaucheya Date: Thu, 13 Nov 2025 15:14:49 +0000 Subject: [PATCH 1/4] update to use minisearch --- package.json | 1 + src/components/autosuggest/autosuggest.ui.js | 22 ++-------- src/components/autosuggest/fuse-config.js | 44 +++++++++----------- yarn.lock | 5 +++ 4 files changed, 30 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index d3a9ddd12f..ee5464e835 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ }, "dependencies": { "highcharts": "12.1.2", + "minisearch": "^7.2.0", "pym.js": "1.3.2" } } diff --git a/src/components/autosuggest/autosuggest.ui.js b/src/components/autosuggest/autosuggest.ui.js index 871f5662ac..bb2315e725 100644 --- a/src/components/autosuggest/autosuggest.ui.js +++ b/src/components/autosuggest/autosuggest.ui.js @@ -1,6 +1,6 @@ import abortableFetch from '../../js/abortable-fetch'; import { sanitiseAutosuggestText } from './autosuggest.helpers'; -import runFuse from './fuse-config'; +import runMiniSearch from './fuse-config'; import DOMPurify from 'dompurify'; export const baseClass = 'ons-js-autosuggest'; @@ -297,30 +297,16 @@ export default class AutosuggestUI { async fetchSuggestions(sanitisedQuery, data) { this.abortFetch(); - const threshold = - this.customResultsThreshold != null && this.customResultsThreshold >= 0 && this.customResultsThreshold <= 1 - ? this.customResultsThreshold - : 0.2; - - let distance; - if (threshold >= 0.6) { - distance = 500; - } else if (threshold >= 0.4) { - distance = 300; - } else { - distance = 100; - } - - const results = await runFuse(sanitisedQuery, data, this.lang, threshold, distance); + const results = await runMiniSearch(sanitisedQuery, data, this.lang); results.forEach((result) => { - const resultItem = result.item ?? result; - + const resultItem = result || result.item; // MiniSearch returns plain objects result.sanitisedText = sanitiseAutosuggestText( resultItem[this.lang] ?? resultItem['formattedAddress'], this.sanitisedQueryReplaceChars, ); }); + return { status: this.responseStatus, results, diff --git a/src/components/autosuggest/fuse-config.js b/src/components/autosuggest/fuse-config.js index 70dade58b6..1baf904a01 100644 --- a/src/components/autosuggest/fuse-config.js +++ b/src/components/autosuggest/fuse-config.js @@ -1,27 +1,23 @@ -import Fuse from 'fuse.js'; +import MiniSearch from 'minisearch'; -export default function runFuse(query, data, searchFields, threshold, distance) { - const options = { - shouldSort: true, - threshold: threshold, - distance: distance, - keys: [ - { - name: searchFields, - weight: 0.9, - }, - { - name: 'formattedAddress', - weight: 0.9, - }, - { - name: 'tags', - weight: 0.1, - }, - ], - }; +export default function runMiniSearch(query, data, searchField) { + // MiniSearch needs unique IDs, so we generate them if missing + const indexedData = data.map((item, index) => ({ + id: index, + ...item, + })); - const fuse = new Fuse(data, options); - let result = fuse.search(query); - return result; + const miniSearch = new MiniSearch({ + fields: [searchField, 'formattedAddress', 'tags'], // searchable fields + storeFields: [searchField, 'formattedAddress', 'tags'], // returned fields + searchOptions: { + fuzzy: false, // exact + prefix + substring (no fuzzy threshold) + prefix: true, // allows "South Sa" to match full long strings + combineWith: 'AND', // all search tokens must match (same as fuse) + }, + }); + + miniSearch.addAll(indexedData); + + return miniSearch.search(query); } diff --git a/yarn.lock b/yarn.lock index e3b4927e4e..849afa6c89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9465,6 +9465,11 @@ minimist@^1.0.0, minimist@^1.1.0, minimist@^1.2.6, minimist@^1.2.8: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +minisearch@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-7.2.0.tgz#3dc30e41e9464b3836553b6d969b656614f8f359" + integrity sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg== + mitt@3.0.1, mitt@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" From dc94417ce26c510a0b7f6ece84ccddcdf8da9453 Mon Sep 17 00:00:00 2001 From: Precious Onyenaucheya Date: Thu, 13 Nov 2025 16:00:04 +0000 Subject: [PATCH 2/4] update using flex search --- package.json | 1 + src/components/autosuggest/autosuggest.ui.js | 12 ++++-- src/components/autosuggest/fuse-config.js | 39 ++++++++++++++------ yarn.lock | 5 +++ 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index ee5464e835..6e011ea1be 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ "access": "public" }, "dependencies": { + "flexsearch": "^0.8.212", "highcharts": "12.1.2", "minisearch": "^7.2.0", "pym.js": "1.3.2" diff --git a/src/components/autosuggest/autosuggest.ui.js b/src/components/autosuggest/autosuggest.ui.js index bb2315e725..d8400869e3 100644 --- a/src/components/autosuggest/autosuggest.ui.js +++ b/src/components/autosuggest/autosuggest.ui.js @@ -1,6 +1,6 @@ import abortableFetch from '../../js/abortable-fetch'; import { sanitiseAutosuggestText } from './autosuggest.helpers'; -import runMiniSearch from './fuse-config'; +import runFlexSearch from './fuse-config'; import DOMPurify from 'dompurify'; export const baseClass = 'ons-js-autosuggest'; @@ -297,12 +297,16 @@ export default class AutosuggestUI { async fetchSuggestions(sanitisedQuery, data) { this.abortFetch(); - const results = await runMiniSearch(sanitisedQuery, data, this.lang); + // Swap Fuse → FlexSearch + const results = await runFlexSearch( + sanitisedQuery, + data, + this.lang, // searchField + ); results.forEach((result) => { - const resultItem = result || result.item; // MiniSearch returns plain objects result.sanitisedText = sanitiseAutosuggestText( - resultItem[this.lang] ?? resultItem['formattedAddress'], + result[this.lang] ?? result['formattedAddress'], this.sanitisedQueryReplaceChars, ); }); diff --git a/src/components/autosuggest/fuse-config.js b/src/components/autosuggest/fuse-config.js index 1baf904a01..fa4624005c 100644 --- a/src/components/autosuggest/fuse-config.js +++ b/src/components/autosuggest/fuse-config.js @@ -1,23 +1,38 @@ -import MiniSearch from 'minisearch'; +import FlexSearch from 'flexsearch'; -export default function runMiniSearch(query, data, searchField) { - // MiniSearch needs unique IDs, so we generate them if missing +export default function runFlexSearch(query, data, searchField) { + // FlexSearch requires unique IDs const indexedData = data.map((item, index) => ({ id: index, ...item, })); - const miniSearch = new MiniSearch({ - fields: [searchField, 'formattedAddress', 'tags'], // searchable fields - storeFields: [searchField, 'formattedAddress', 'tags'], // returned fields - searchOptions: { - fuzzy: false, // exact + prefix + substring (no fuzzy threshold) - prefix: true, // allows "South Sa" to match full long strings - combineWith: 'AND', // all search tokens must match (same as fuse) + // Correct FlexSearch Document index config + const index = new FlexSearch.Document({ + document: { + id: 'id', + index: [searchField, 'formattedAddress', 'tags'], + store: [searchField, 'formattedAddress', 'tags'], }, + tokenize: 'forward', // supports substring-like matching + context: false, // disable contextual scoring (safer) }); - miniSearch.addAll(indexedData); + // Insert documents + for (const doc of indexedData) { + index.add(doc); + } - return miniSearch.search(query); + // Perform search on all indexed fields + const resultGroups = index.search(query); + + // Flatten results + const flatResults = []; + for (const group of resultGroups) { + for (const id of group.result) { + flatResults.push(indexedData[id]); + } + } + + return flatResults; } diff --git a/yarn.lock b/yarn.lock index 849afa6c89..eddf1b51e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6032,6 +6032,11 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== +flexsearch@^0.8.212: + version "0.8.212" + resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.8.212.tgz#b9509af778a991b938292e36fe0809a4ece4b940" + integrity sha512-wSyJr1GUWoOOIISRu+X2IXiOcVfg9qqBRyCPRUdLMIGJqPzMo+jMRlvE83t14v1j0dRMEaBbER/adQjp6Du2pw== + flush-write-stream@^1.0.2: version "1.1.1" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" From 6ae0a0f4501aaf63a124b7150a53bfb27eeebe47 Mon Sep 17 00:00:00 2001 From: Precious Onyenaucheya Date: Tue, 18 Nov 2025 09:03:46 +0000 Subject: [PATCH 3/4] use raw search --- src/components/autosuggest/autosuggest.ui.js | 15 +++--- src/components/autosuggest/fuse-config.js | 50 +++++++------------- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/src/components/autosuggest/autosuggest.ui.js b/src/components/autosuggest/autosuggest.ui.js index d8400869e3..cdd1021e24 100644 --- a/src/components/autosuggest/autosuggest.ui.js +++ b/src/components/autosuggest/autosuggest.ui.js @@ -1,6 +1,6 @@ import abortableFetch from '../../js/abortable-fetch'; import { sanitiseAutosuggestText } from './autosuggest.helpers'; -import runFlexSearch from './fuse-config'; +import runRawSearch from './fuse-config'; import DOMPurify from 'dompurify'; export const baseClass = 'ons-js-autosuggest'; @@ -298,18 +298,21 @@ export default class AutosuggestUI { this.abortFetch(); // Swap Fuse → FlexSearch - const results = await runFlexSearch( + const results = await runRawSearch( sanitisedQuery, data, this.lang, // searchField ); results.forEach((result) => { - result.sanitisedText = sanitiseAutosuggestText( - result[this.lang] ?? result['formattedAddress'], - this.sanitisedQueryReplaceChars, - ); + result.sanitisedText = sanitiseAutosuggestText(result[this.lang] ?? result.formattedAddress, this.sanitisedQueryReplaceChars); }); + // results.forEach((result) => { + // result.sanitisedText = sanitiseAutosuggestText( + // result[this.lang] ?? result['formattedAddress'], + // this.sanitisedQueryReplaceChars, + // ); + // }); return { status: this.responseStatus, diff --git a/src/components/autosuggest/fuse-config.js b/src/components/autosuggest/fuse-config.js index fa4624005c..ca6c4e58a2 100644 --- a/src/components/autosuggest/fuse-config.js +++ b/src/components/autosuggest/fuse-config.js @@ -1,38 +1,24 @@ -import FlexSearch from 'flexsearch'; +export default function runRawSearch(query, data, searchField) { + if (!query || !query.trim()) return []; -export default function runFlexSearch(query, data, searchField) { - // FlexSearch requires unique IDs - const indexedData = data.map((item, index) => ({ - id: index, - ...item, - })); + const q = query.toLowerCase(); - // Correct FlexSearch Document index config - const index = new FlexSearch.Document({ - document: { - id: 'id', - index: [searchField, 'formattedAddress', 'tags'], - store: [searchField, 'formattedAddress', 'tags'], - }, - tokenize: 'forward', // supports substring-like matching - context: false, // disable contextual scoring (safer) - }); + return data + .map((item) => { + const combined = [item[searchField] ?? '', item.formattedAddress ?? '', Array.isArray(item.tags) ? item.tags.join(' ') : ''] + .join(' ') + .toLowerCase(); - // Insert documents - for (const doc of indexedData) { - index.add(doc); - } + const index = combined.indexOf(q); - // Perform search on all indexed fields - const resultGroups = index.search(query); + if (index === -1) return null; - // Flatten results - const flatResults = []; - for (const group of resultGroups) { - for (const id of group.result) { - flatResults.push(indexedData[id]); - } - } - - return flatResults; + return { + item, + score: index, // earlier match is better + }; + }) + .filter(Boolean) + .sort((a, b) => a.score - b.score) // closest match first + .map((r) => r.item); } From bd0c1688725e01f9d58bf3d7e96ba2078ed73b7b Mon Sep 17 00:00:00 2001 From: Precious Onyenaucheya Date: Tue, 18 Nov 2025 09:08:14 +0000 Subject: [PATCH 4/4] flexsearch index --- src/components/autosuggest/autosuggest.ui.js | 4 +- src/components/autosuggest/fuse-config.js | 39 +++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/components/autosuggest/autosuggest.ui.js b/src/components/autosuggest/autosuggest.ui.js index cdd1021e24..7e163767a6 100644 --- a/src/components/autosuggest/autosuggest.ui.js +++ b/src/components/autosuggest/autosuggest.ui.js @@ -1,6 +1,6 @@ import abortableFetch from '../../js/abortable-fetch'; import { sanitiseAutosuggestText } from './autosuggest.helpers'; -import runRawSearch from './fuse-config'; +import runFlexSearchIndex from './fuse-config'; import DOMPurify from 'dompurify'; export const baseClass = 'ons-js-autosuggest'; @@ -298,7 +298,7 @@ export default class AutosuggestUI { this.abortFetch(); // Swap Fuse → FlexSearch - const results = await runRawSearch( + const results = await runFlexSearchIndex( sanitisedQuery, data, this.lang, // searchField diff --git a/src/components/autosuggest/fuse-config.js b/src/components/autosuggest/fuse-config.js index ca6c4e58a2..a60130eb7f 100644 --- a/src/components/autosuggest/fuse-config.js +++ b/src/components/autosuggest/fuse-config.js @@ -1,24 +1,27 @@ -export default function runRawSearch(query, data, searchField) { - if (!query || !query.trim()) return []; +import FlexSearch from 'flexsearch'; - const q = query.toLowerCase(); +export default function runFlexSearchIndex(query, data, searchField) { + const index = new FlexSearch.Index({ + preset: 'match', // enables raw substring scanning + tokenize: 'forward', // needed for substring matching + cache: true, + encode: false, + depth: 3, // improves substring matching window + }); - return data - .map((item) => { - const combined = [item[searchField] ?? '', item.formattedAddress ?? '', Array.isArray(item.tags) ? item.tags.join(' ') : ''] - .join(' ') - .toLowerCase(); + const documents = []; - const index = combined.indexOf(q); + // Add documents + data.forEach((item, id) => { + const text = + (item[searchField] || '') + ' ' + (item.formattedAddress || '') + ' ' + (Array.isArray(item.tags) ? item.tags.join(' ') : ''); - if (index === -1) return null; + index.add(id, text); + documents[id] = item; + }); - return { - item, - score: index, // earlier match is better - }; - }) - .filter(Boolean) - .sort((a, b) => a.score - b.score) // closest match first - .map((r) => r.item); + // Perform substring search + const resultIds = index.search(query); + + return resultIds.map((id) => documents[id]); }