Skip to content

Commit 47da5c4

Browse files
authored
Merge pull request #4546 from ClickHouse/search-limits
Debouncing to reduce search requests
2 parents b9d678e + 487c60a commit 47da5c4

File tree

1 file changed

+159
-93
lines changed

1 file changed

+159
-93
lines changed

src/theme/SearchBar/index.js

Lines changed: 159 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useMemo, useRef, useState } from 'react';
1+
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
22
import { DocSearchButton, useDocSearchKeyboardEvents } from '@docsearch/react';
33
import Head from '@docusaurus/Head';
44
import { useHistory } from '@docusaurus/router';
@@ -9,7 +9,6 @@ import {
99
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
1010
import { createPortal } from 'react-dom';
1111
import translations from '@theme/SearchTranslations';
12-
import { useEffect } from 'react';
1312
import {useAskAI} from '@site/src/hooks/useAskAI'
1413
import { shouldPreventSearchAction, handleSearchKeyboardConflict } from './utils/aiConflictHandler';
1514
import { initializeSearchAnalytics, createEnhancedSearchClient } from './utils/searchAnalytics';
@@ -25,17 +24,17 @@ import { DocTypeSelector } from './docTypeSelector';
2524

2625
function DocSearch({ contextualSearch, externalUrlRegex, ...props }) {
2726
const queryIDRef = useRef(null);
27+
const lastQueryRef = useRef('');
2828
const { siteMetadata, i18n: { currentLocale } } = useDocusaurusContext();
2929
const processSearchResultUrl = useSearchResultUrlProcessor();
3030
const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
31-
const { isAskAIOpen, currentMode } = useAskAI();
31+
const { isAskAIOpen } = useAskAI();
3232
const history = useHistory();
3333
const searchButtonRef = useRef(null);
3434

35-
// Doc type filtering state
3635
const [selectedDocTypes, setSelectedDocTypes] = useState(null);
36+
const searchParametersRef = useRef(null);
3737

38-
// Use the modal management hook
3938
const {
4039
isOpen,
4140
initialQuery,
@@ -47,26 +46,63 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) {
4746
importDocSearchModalIfNeeded
4847
} = useDocSearchModal();
4948

50-
// Configure search parameters with doc_type filter
51-
const searchParameters = createSearchParameters(
52-
props,
53-
contextualSearch,
54-
contextualSearchFacetFilters,
55-
selectedDocTypes
56-
);
49+
// Update searchParameters ref instead of creating new object
50+
useEffect(() => {
51+
const newParams = createSearchParameters(
52+
props,
53+
contextualSearch,
54+
contextualSearchFacetFilters,
55+
selectedDocTypes
56+
);
57+
58+
if (!searchParametersRef.current) {
59+
searchParametersRef.current = newParams;
60+
} else {
61+
Object.keys(newParams).forEach(key => {
62+
searchParametersRef.current[key] = newParams[key];
63+
});
64+
}
65+
}, [props, contextualSearch, contextualSearchFacetFilters, selectedDocTypes]);
66+
67+
// Initialize on mount
68+
if (!searchParametersRef.current) {
69+
searchParametersRef.current = createSearchParameters(
70+
props,
71+
contextualSearch,
72+
contextualSearchFacetFilters,
73+
selectedDocTypes
74+
);
75+
}
76+
77+
// Track input changes to capture the query
78+
useEffect(() => {
79+
if (!isOpen) return;
80+
81+
const handleInput = (e) => {
82+
const input = e.target;
83+
if (input.classList.contains('DocSearch-Input')) {
84+
lastQueryRef.current = input.value;
85+
}
86+
};
87+
88+
document.addEventListener('input', handleInput, true);
89+
return () => document.removeEventListener('input', handleInput, true);
90+
}, [isOpen]);
5791

5892
useEffect(() => {
5993
initializeSearchAnalytics(props.appId, props.apiKey);
6094
}, [props.appId, props.apiKey]);
6195

62-
// Create navigator for handling result clicks
6396
const navigator = useMemo(
6497
() => createSearchNavigator(history, externalUrlRegex),
6598
[history, externalUrlRegex]
6699
);
67100

68-
// Transform search items with metadata
69101
const transformItems = useCallback((items, state) => {
102+
if (state?.query) {
103+
lastQueryRef.current = state.query;
104+
}
105+
70106
return transformSearchItems(items, {
71107
transformItems: props.transformItems,
72108
processSearchResultUrl,
@@ -75,36 +111,74 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) {
75111
});
76112
}, [props.transformItems, processSearchResultUrl, currentLocale]);
77113

78-
const handleDocTypeChange = useCallback((docTypes) => {
79-
setSelectedDocTypes(docTypes);
80-
}, []);
114+
const handleDocTypeChange = useCallback((docTypes) => {
115+
setSelectedDocTypes(docTypes);
116+
117+
// Re-trigger search with updated filters after state update completes
118+
setTimeout(() => {
119+
const input = document.querySelector('.DocSearch-Input');
120+
const query = lastQueryRef.current;
121+
122+
if (input && query) {
123+
// Access React's internal value setter to bypass readonly property
124+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
125+
window.HTMLInputElement.prototype,
126+
'value'
127+
).set;
128+
129+
// Clear input to trigger change detection
130+
nativeInputValueSetter.call(input, '');
131+
input.dispatchEvent(new Event('input', { bubbles: true }));
132+
133+
// Restore original query to execute search with new filters
134+
setTimeout(() => {
135+
nativeInputValueSetter.call(input, query);
136+
input.dispatchEvent(new Event('input', { bubbles: true }));
137+
input.focus();
138+
}, 0);
139+
}
140+
}, 100);
141+
}, []);
81142

82143
const resultsFooterComponent = useMemo(
83-
() =>
84-
// eslint-disable-next-line react/no-unstable-nested-components
85-
(footerProps) =>
86-
<SearchResultsFooter {...footerProps} onClose={onClose} />,
87-
[onClose],
144+
() => (footerProps) => <SearchResultsFooter {...footerProps} onClose={onClose} />,
145+
[onClose]
88146
);
89147

90148
const transformSearchClient = useCallback((searchClient) => {
91-
return createEnhancedSearchClient(searchClient, siteMetadata.docusaurusVersion, queryIDRef);
149+
const enhancedClient = createEnhancedSearchClient(
150+
searchClient,
151+
siteMetadata.docusaurusVersion,
152+
queryIDRef
153+
);
154+
155+
const originalSearch = enhancedClient.search.bind(enhancedClient);
156+
157+
let debounceTimeout;
158+
enhancedClient.search = (...args) => {
159+
return new Promise((resolve, reject) => {
160+
clearTimeout(debounceTimeout);
161+
debounceTimeout = setTimeout(() => {
162+
originalSearch(...args)
163+
.then(resolve)
164+
.catch(reject);
165+
}, 200);
166+
});
167+
};
168+
169+
return enhancedClient;
92170
}, [siteMetadata.docusaurusVersion]);
93171

94172
const handleOnOpen = useCallback(() => {
95-
console.log('handleOnOpen called', { isAskAIOpen });
96-
97173
if (shouldPreventSearchAction(isAskAIOpen)) {
98-
console.log('handleOnOpen - preventing search modal');
99174
return;
100175
}
101-
102176
onOpen();
103177
}, [isAskAIOpen, onOpen]);
104178

105179
const handleOnInput = useCallback((event) => {
106180
if (shouldPreventSearchAction(isAskAIOpen)) {
107-
return; // Prevent search input handling
181+
return;
108182
}
109183
onInput(event);
110184
}, [isAskAIOpen, onInput]);
@@ -118,68 +192,64 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) {
118192
});
119193

120194
return (
121-
<>
122-
<Head>
123-
{/* This hints the browser that the website will load data from Algolia,
124-
and allows it to preconnect to the DocSearch cluster. It makes the first
125-
query faster, especially on mobile. */}
126-
<link
127-
rel="preconnect"
128-
href={`https://${props.appId}-dsn.algolia.net`}
129-
crossOrigin="anonymous"
130-
/>
131-
</Head>
132-
133-
<DocSearchButton
134-
onTouchStart={importDocSearchModalIfNeeded}
135-
onFocus={importDocSearchModalIfNeeded}
136-
onMouseOver={importDocSearchModalIfNeeded}
137-
onClick={onOpen}
138-
ref={searchButtonRef}
139-
translations={translations.button}
195+
<>
196+
<Head>
197+
<link
198+
rel="preconnect"
199+
href={`https://${props.appId}-dsn.algolia.net`}
200+
crossOrigin="anonymous"
140201
/>
202+
</Head>
141203

142-
{isOpen &&
143-
DocSearchModal &&
144-
searchContainer &&
145-
createPortal(
146-
<>
147-
<DocSearchModal
148-
onClose={onClose}
149-
initialScrollY={window.scrollY}
150-
initialQuery={initialQuery}
151-
navigator={navigator}
152-
transformItems={transformItems}
153-
hitComponent={SearchHit}
154-
transformSearchClient={transformSearchClient}
155-
{...(props.searchPagePath && {
156-
resultsFooterComponent,
157-
})}
158-
{...props}
159-
insights={true}
160-
searchParameters={searchParameters}
161-
placeholder={translations.placeholder}
162-
translations={translations.modal}
163-
/>
164-
165-
{/* Selector positioned as overlay */}
166-
<div style={{
167-
position: 'fixed',
168-
top: window.innerWidth < 768 ? '55px' : '120px',
169-
right: window.innerWidth < 768 ? 'calc(50% - 185px)' : 'calc(50% - 255px)',
170-
zIndex: 10000,
171-
backgroundColor: 'var(--docsearch-modal-background)',
172-
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
173-
}}>
174-
<DocTypeSelector
175-
selectedDocTypes={selectedDocTypes}
176-
onSelectionChange={handleDocTypeChange}
177-
/>
178-
</div>
179-
</>,
180-
searchContainer,
181-
)}
182-
</>
204+
<DocSearchButton
205+
onTouchStart={importDocSearchModalIfNeeded}
206+
onFocus={importDocSearchModalIfNeeded}
207+
onMouseOver={importDocSearchModalIfNeeded}
208+
onClick={onOpen}
209+
ref={searchButtonRef}
210+
translations={translations.button}
211+
/>
212+
213+
{isOpen &&
214+
DocSearchModal &&
215+
searchContainer &&
216+
createPortal(
217+
<>
218+
<DocSearchModal
219+
onClose={onClose}
220+
initialScrollY={window.scrollY}
221+
initialQuery={initialQuery}
222+
navigator={navigator}
223+
transformItems={transformItems}
224+
hitComponent={SearchHit}
225+
transformSearchClient={transformSearchClient}
226+
{...(props.searchPagePath && {
227+
resultsFooterComponent,
228+
})}
229+
{...props}
230+
insights={true}
231+
searchParameters={searchParametersRef.current}
232+
placeholder={translations.placeholder}
233+
translations={translations.modal}
234+
/>
235+
236+
<div style={{
237+
position: 'fixed',
238+
top: window.innerWidth < 768 ? '55px' : '120px',
239+
right: window.innerWidth < 768 ? 'calc(50% - 185px)' : 'calc(50% - 255px)',
240+
zIndex: 10000,
241+
backgroundColor: 'var(--docsearch-modal-background)',
242+
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
243+
}}>
244+
<DocTypeSelector
245+
selectedDocTypes={selectedDocTypes}
246+
onSelectionChange={handleDocTypeChange}
247+
/>
248+
</div>
249+
</>,
250+
searchContainer
251+
)}
252+
</>
183253
);
184254
}
185255

@@ -192,12 +262,8 @@ export default function SearchBar() {
192262
handleSearchKeyboardConflict(event, isAskAIOpen);
193263
};
194264

195-
// Add listener with capture phase to intercept before DocSearch
196265
document.addEventListener('keydown', handleKeyDown, true);
197-
198-
return () => {
199-
document.removeEventListener('keydown', handleKeyDown, true);
200-
};
266+
return () => document.removeEventListener('keydown', handleKeyDown, true);
201267
}, [isAskAIOpen]);
202268

203269
return <DocSearch {...siteConfig.themeConfig.algolia} />;

0 commit comments

Comments
 (0)