@@ -47,7 +50,32 @@
const { algoliasearch, instantsearch } = window;
const { searchBox } = instantsearch.widgets;
- const searchClient = algoliasearch('{{{env.ALGOLIA_APP_ID}}}', '{{{env.ALGOLIA_API_KEY}}}');
+ // "Exact match" toggle state. When on, we require every term, exact spelling,
+ // and no optional-word fallback. We do this in a search-client wrapper so the
+ // search box keeps showing exactly what the user typed. NOTE: we deliberately
+ // do NOT wrap the query in quotes for a phrase match — the index's searchable
+ // attributes use unordered(...), which disables phrase/proximity matching, so
+ // a quoted phrase returns nothing; strict params give reliable exact matching.
+ let exactMatch = false;
+
+ const baseClient = algoliasearch('{{{env.ALGOLIA_APP_ID}}}', '{{{env.ALGOLIA_API_KEY}}}');
+ const searchClient = {
+ ...baseClient,
+ search(requests) {
+ if (exactMatch) {
+ requests = requests.map((request) => {
+ const params = { ...(request.params || {}) };
+ if (params.query) {
+ params.typoTolerance = false;
+ params.removeWordsIfNoResults = 'none';
+ params.optionalWords = [];
+ }
+ return { ...request, params };
+ });
+ }
+ return baseClient.search(requests);
+ },
+ };
const indexName = '{{{env.ALGOLIA_INDEX_NAME}}}';
const search = instantsearch({
@@ -197,6 +225,15 @@
]);
search.start();
+
+ // Wire the "Exact match" checkbox: flip the flag and re-run the search.
+ const exactMatchCheckbox = document.getElementById('exact-match');
+ if (exactMatchCheckbox) {
+ exactMatchCheckbox.addEventListener('change', (e) => {
+ exactMatch = e.target.checked;
+ if (search.helper) search.helper.search();
+ });
+ }
}
document.addEventListener('DOMContentLoaded', function(event) {
diff --git a/src/partials/algolia-script.hbs b/src/partials/algolia-script.hbs
index fed35a8a..a5ef2eda 100644
--- a/src/partials/algolia-script.hbs
+++ b/src/partials/algolia-script.hbs
@@ -133,6 +133,22 @@ window.__algoliaToggleFilter = function(label, checked) {
}
};
+/**
+ * Global toggle for "exact match" search. When on, docs queries use strict params
+ * (no typo tolerance, no optional words, no remove-words fallback), requiring exact
+ * term spelling while preserving the original user query text — not a quoted phrase
+ * match (see getItems for why phrase matching can't work here). Exposed on `window`
+ * so the checkbox handler in the render template (re-created each render) calls a
+ * stable function. Same rationale as window.__algoliaToggleFilter above.
+ */
+window.__algoliaExactMatch = false;
+window.__algoliaToggleExactMatch = function(checked) {
+ window.__algoliaExactMatch = !!checked;
+ if (window.__algoliaAutocompleteInstance) {
+ window.__algoliaAutocompleteInstance.refresh();
+ }
+};
+
/** Stable refs + listeners for outside-click / Esc handling on the dropdown. */
const __dropdownRefs = { menu: null, toggle: null };
let __dropdownOutsideHandler = null;
@@ -273,14 +289,18 @@ function initAlgolia() {
prevFilters.every((p, i) => p?.label === currFilters[i]?.label);
if (!same || tagsChanged) {
- setContext({ ...state.context, filtersSourceItems: currFilters });
+ // setContext merges into the LIVE context. Never spread ...state.context:
+ // onStateChange can run with a stale state snapshot (e.g. a late async search
+ // response), and spreading it would overwrite current tagsPlugin.tags with old
+ // values — the cause of the filter dropdown "resetting" to a previous count.
+ setContext({ filtersSourceItems: currFilters });
}
// 4) Attach/detach global listeners only while the dropdown is open.
const wasOpen = !!(prevState?.context?.dropdownOpen);
const isOpen = !!(state?.context?.dropdownOpen);
if (isOpen && !wasOpen) {
- const close = () => setContext({ ...state.context, dropdownOpen: false });
+ const close = () => setContext({ dropdownOpen: false });
__dropdownOutsideHandler = (e) => {
const menu = __dropdownRefs.menu;
const toggle = __dropdownRefs.toggle;
@@ -306,11 +326,11 @@ function initAlgolia() {
// 5) Update or seed preview when query/tags change or first load.
if ((nextQ && nextQ !== prevQ) || tagsChanged) {
- setContext({ ...state.context, preview: firstHit });
+ setContext({ preview: firstHit });
return;
}
if (!state.context.preview && firstHit) {
- setContext({ ...state.context, preview: firstHit });
+ setContext({ preview: firstHit });
}
},
@@ -378,7 +398,7 @@ function initAlgolia() {
// Toggle dropdown through Autocomplete context (so onStateChange runs).
const setDropdownOpen = (open) => {
- window.__algoliaAutocompleteInstance.setContext({ ...state.context, dropdownOpen: open });
+ window.__algoliaAutocompleteInstance.setContext({ dropdownOpen: open });
};
// The dropdown UI itself (button + menu)
@@ -508,10 +528,26 @@ function initAlgolia() {
`
: '';
+ // "Exact match" toggle: forces a strict, no-typo, no-fallback query (see getItems).
+ const exactToggle = html`
+
`;
+
// Layout: header (filters/AI), results list, right-side preview, footer tips.
render(
html`
@@ -709,6 +745,16 @@ function initAlgolia() {
{
sourceId: 'docs',
getItems() {
+ // When "Exact match" is on, require every term, exact spelling, and no
+ // optional-word fallback. NOTE: we deliberately do NOT wrap the query in
+ // quotes for a phrase match — the index's searchable attributes use
+ // unordered(...), which disables phrase/proximity matching, so a quoted
+ // phrase returns nothing. Strict params give reliable exact-term matching.
+ // See window.__algoliaToggleExactMatch.
+ const exactMatch = !!window.__algoliaExactMatch;
+ const exactParams = exactMatch
+ ? { typoTolerance: false, removeWordsIfNoResults: 'none', optionalWords: [] }
+ : {};
// getAlgoliaResults: https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/sources/#param-getalgoliaresults
return getAlgoliaResults({
searchClient,
@@ -722,6 +768,7 @@ function initAlgolia() {
attributesToSnippet: ['*:25'], // https://www.algolia.com/doc/api-reference/api-parameters/attributesToSnippet/
snippetEllipsisText: '…', // https://www.algolia.com/doc/api-reference/api-parameters/snippetEllipsisText/
tagFilters: mapToAlgoliaFilters(tagsByFacet), // https://www.algolia.com/doc/api-reference/api-parameters/tagFilters/
+ ...exactParams,
},
},
],