feat(autocomplete): add Recommend API sources to EXPERIMENTAL_autocomplete#7008
Draft
Haroenv wants to merge 6 commits into
Draft
feat(autocomplete): add Recommend API sources to EXPERIMENTAL_autocomplete#7008Haroenv wants to merge 6 commits into
Haroenv wants to merge 6 commits into
Conversation
…plete Allows recommend models (trendingItems, frequentlyBoughtTogether, relatedProducts, lookingSimilar) to appear as sources in the autocomplete panel alongside search index results. - Introduces unified `sources` array (replacing the deprecated `indices` array) in `AutocompleteRenderState`, where each entry carries a `sourceType: 'index' | 'recommend'` discriminant - Adds `IndexSourceConfig` and `RecommendSourceConfig` widget param types so callers can mix search and recommend sources and control their display order explicitly - Adds per-source `showWhen: 'always' | 'empty' | 'querying'` — index sources default to `'always'`, recommend sources default to `'empty'` (show trending items only when the query is empty) - Recommend widgets are created as children of the isolated index; a `forceRender` callback stored in mutable render state re-renders the panel when recommend results arrive outside the search lifecycle - Widens `ScopedResult.results` to `SearchResults | RecommendResponse<any> | null` and adds `instanceof SearchResults` guards in `clearRefinements` and `currentRefinements` connectors so they gracefully ignore recommend results - `sendEvent` reuses `createSendEventForHits` (same as other recommend connectors); `addAbsolutePosition` is called as page 0 with `hitsPerPage = hits.length` to support future pagination without changing the public interface Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 204 |
| Duplication | 0 |
TIP This summary will be updated as you push new changes.
Previously, recommend widgets triggered an independent re-render when their results arrived, causing a double-paint whenever search results and recommend results resolved at different times. Recommend widgets now only write into a shared Map; the render is exclusively driven by the search lifecycle. The connector reads both scopedResults (search) and the Map (recommend) in a single getWidgetRenderState call, producing the unified sources array in one pass. This guarantees one render per keystroke regardless of which response arrives first. Side-effects: - ScopedResult.results reverts to SearchResults | null — no widening needed - connectClearRefinements / connectCurrentRefinements revert their instanceof guards (recommend results never appear in scopedResults) - forceRender and lastConnectorParams are removed from renderState Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…me for recommend - Panel elements are now rebuilt in allSources order after both index and recommend forEach loops populate them, so a recommend source listed before an index source in the config actually appears above it in the panel - RecommendSourceConfig.indexName is now honoured: when set, the recommend widget is wrapped in an index() widget for that indexName so recommendations are fetched from the correct index instead of the isolated parent - Remove redundant addAbsolutePosition call on recommend hits in the connector; recommend connectors already set __position and __queryID on their items - Remove recommendResults from RendererParams (no longer passed to AutocompleteWrapper; recommend state is read from connector sources) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…formItems - Annotate onInput event with native browser type to prevent implicit-any in declaration build context (where React JSX types may not resolve) - Update react-instantsearch transformItems prop from deprecated TransformItemsIndicesConfig to AutocompleteSource Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
More templates
algoliasearch-helper
instantsearch-ui-components
instantsearch.css
instantsearch.js
react-instantsearch
react-instantsearch-core
react-instantsearch-nextjs
react-instantsearch-router-nextjs
vue-instantsearch
commit: |
…ring - Replace for-of loops with forEach to satisfy no-for-of lint rule - Remove unnecessary type assertions after discriminant narrowing - Move createSendEventForHits to type-only import - Fix onInput event type to avoid Event& incompatibility with React synthetic events - Fix panel element ordering to use transformed sources order so transformItems can reorder query suggestions relative to other indices - Build _sourcesOrder from indicesConfig (includes showQuerySuggestions at front) instead of allSources to preserve correct default ordering - Update connectAutocomplete tests to expect sources alongside indices Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds support for Recommend-powered sources (e.g., trending items / related products) to EXPERIMENTAL_autocomplete by introducing a unified sources model, updating connector/render-state types, and adapting rendering so panel sections can be controlled per source.
Changes:
- Introduces a unified
sourcesarray (index + recommend) and updatestransformItemstyping to operate on sources rather than indices. - Wires Recommend connectors into the autocomplete widget and exposes recommend entries in panel rendering (with
showWhenvisibility controls). - Updates connector types/tests to include
sourceswhile keepingindicesas deprecated compatibility.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/common/widgets/autocomplete/options.tsx | Updates TS annotations in transformItems tests to accommodate the new sources-based signature. |
| packages/react-instantsearch/src/widgets/Autocomplete.tsx | Updates React Autocomplete prop typing for transformItems to use AutocompleteSource[]. |
| packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx | Implements unified sources + Recommend source rendering and adds showWhen filtering and panel ordering logic. |
| packages/instantsearch.js/src/connectors/index.ts | Re-exports AutocompleteSource type from the autocomplete connector. |
| packages/instantsearch.js/src/connectors/autocomplete/connectAutocomplete.ts | Adds sources to render state, updates transformItems to apply to sources, and merges recommend sources via private params. |
| packages/instantsearch.js/src/connectors/autocomplete/tests/connectAutocomplete-test.ts | Updates expectations to include the new sources field in render state. |
Comments suppressed due to low confidence (1)
packages/instantsearch.js/src/connectors/autocomplete/tests/connectAutocomplete-test.ts:150
- The updated tests assert that
sourcesexists, but there’s no coverage for the new behavior added in the connector: merging recommend sources from_recommendSourcesand respecting_sourcesOrder(including transformItems reordering). Adding targeted tests for these code paths would help prevent regressions in source ordering and recommend integration.
it('renders during init and render', () => {
const searchClient = createSearchClient();
const render = jest.fn();
const makeWidget = connectAutocomplete(render);
const widget = makeWidget({});
expect(render).toHaveBeenCalledTimes(0);
const helper = algoliasearchHelper(searchClient, '', {});
helper.search = jest.fn();
widget.init!(createInitOptions({ helper }));
expect(render).toHaveBeenCalledTimes(1);
expect(render).toHaveBeenLastCalledWith(
expect.objectContaining({
currentRefinement: '',
sources: [],
indices: [],
refine: expect.any(Function),
instantSearchInstance: expect.any(Object),
widgetParams: expect.any(Object),
}),
true
);
widget.render!(createRenderOptions());
expect(render).toHaveBeenCalledTimes(2);
expect(render).toHaveBeenLastCalledWith(
expect.objectContaining({
currentRefinement: '',
sources: expect.any(Array),
indices: expect.any(Array),
refine: expect.any(Function),
instantSearchInstance: expect.any(Object),
widgetParams: expect.any(Object),
}),
false
);
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+613
to
+625
| // Apply showWhen filtering for index sources. Index sources default to 'always'. | ||
| const indicesForPanel = indicesForPanelRaw.filter((autocompleteIndex) => { | ||
| const config = find( | ||
| sourcesConfig, | ||
| (c): c is IndexSourceConfig<TItem> => | ||
| c.sourceType !== 'recommend' && | ||
| c.indexName === autocompleteIndex.indexName | ||
| ); | ||
| const showWhen = config?.showWhen ?? 'always'; | ||
| if (showWhen === 'empty') return !localQuery; | ||
| if (showWhen === 'querying') return Boolean(localQuery); | ||
| return true; | ||
| }); |
Comment on lines
+1587
to
+1598
| _sourcesOrder: [ | ||
| // indicesConfig already has showQuerySuggestions/showPromptSuggestions | ||
| // in their correct positions (unshift/push respectively). | ||
| ...indicesConfig.map(({ indexName }) => ({ | ||
| sourceId: indexName, | ||
| sourceType: 'index' as const, | ||
| })), | ||
| ...recommendSources.map((s) => ({ | ||
| sourceId: s.sourceId || s.model, | ||
| sourceType: 'recommend' as const, | ||
| })), | ||
| ], |
Comment on lines
+1532
to
+1549
| } else if (config.model === 'frequentlyBoughtTogether') { | ||
| widget = connectFrequentlyBoughtTogether(storeResults)({ | ||
| objectIDs: config.objectID ? [config.objectID] : [], | ||
| limit: config.limit, | ||
| threshold: config.threshold, | ||
| queryParameters: config.queryParameters, | ||
| }); | ||
| } else if (config.model === 'relatedProducts') { | ||
| widget = connectRelatedProducts(storeResults)({ | ||
| objectIDs: config.objectID ? [config.objectID] : [], | ||
| limit: config.limit, | ||
| threshold: config.threshold, | ||
| queryParameters: config.queryParameters, | ||
| }); | ||
| } else if (config.model === 'lookingSimilar') { | ||
| widget = connectLookingSimilar(storeResults)({ | ||
| objectIDs: config.objectID ? [config.objectID] : [], | ||
| limit: config.limit, |
Comment on lines
+1514
to
+1565
| const recommendWidgets = recommendSources.map((config) => { | ||
| const sourceId = config.sourceId || config.model; | ||
|
|
||
| const storeResults = (renderState: { items: any[]; sendEvent: any }) => { | ||
| recommendResults.set(sourceId, { | ||
| hits: renderState.items as Hit[], | ||
| sendEvent: renderState.sendEvent, | ||
| }); | ||
| }; | ||
|
|
||
| // Map model to the appropriate connector. | ||
| let widget = null; | ||
| if (config.model === 'trendingItems') { | ||
| widget = connectTrendingItems(storeResults)({ | ||
| limit: config.limit, | ||
| threshold: config.threshold, | ||
| queryParameters: config.queryParameters, | ||
| }); | ||
| } else if (config.model === 'frequentlyBoughtTogether') { | ||
| widget = connectFrequentlyBoughtTogether(storeResults)({ | ||
| objectIDs: config.objectID ? [config.objectID] : [], | ||
| limit: config.limit, | ||
| threshold: config.threshold, | ||
| queryParameters: config.queryParameters, | ||
| }); | ||
| } else if (config.model === 'relatedProducts') { | ||
| widget = connectRelatedProducts(storeResults)({ | ||
| objectIDs: config.objectID ? [config.objectID] : [], | ||
| limit: config.limit, | ||
| threshold: config.threshold, | ||
| queryParameters: config.queryParameters, | ||
| }); | ||
| } else if (config.model === 'lookingSimilar') { | ||
| widget = connectLookingSimilar(storeResults)({ | ||
| objectIDs: config.objectID ? [config.objectID] : [], | ||
| limit: config.limit, | ||
| threshold: config.threshold, | ||
| queryParameters: config.queryParameters, | ||
| }); | ||
| } | ||
|
|
||
| if (!widget) return null; | ||
|
|
||
| // Wrap in an index widget when the user specifies a different indexName, | ||
| // so recommendations are fetched from that index rather than the parent. | ||
| if (config.indexName) { | ||
| return index({ indexName: config.indexName }).addWidgets([widget]); | ||
| } | ||
|
|
||
| return widget; | ||
| }).filter((w): w is NonNullable<typeof w> => w !== null); | ||
|
|
- Use SendEventForHits type instead of ReturnType<typeof createSendEventForHits> - Use stable template props indices (position in indicesConfig, not indicesForPanel) to prevent template prop drift when showWhen hides/shows sources - Use indicesConfig.length as stable base offset for recommend template props - Derive _sourcesOrder from allSources to preserve user-defined interleaving of index and recommend sources (showQuerySuggestions/showPromptSuggestions still anchored at front/end respectively) - Warn in dev when objectID is missing for models that require it - Warn in dev when multiple recommend sources share the same sourceId - Pass indexName through RecommendHitsState so connector can populate AutocompleteSource.indexName for recommend sources Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #7006
Summary
sourcesarray toEXPERIMENTAL_autocompletethat can contain both search index sources and Recommend sources (trendingItems, frequentlyBoughtTogether, relatedProducts, lookingSimilar), replacing the deprecatedindicesarrayshowWhen: 'always' | 'empty' | 'querying'controls when each source's panel section is shown — index sources default to'always', recommend sources default to'empty'(e.g. trending items visible only when the search box is empty)sourcesarray directly controls the rendering order, eliminating the need for a customtransformItemsDesign decisions
Why a unified
sourcesarray instead of separateindices+recommendoptions?Ordering between search and recommend results needs to be explicit. A single array is the simplest way to allow arbitrary interleaving (e.g. trending items before search results, or after).
How recommend results reach the panel
Recommend widgets don't flow through
scopedResults(which is search-only). Instead, each recommend source creates a connector widget as a child of the autocomplete's isolated index. Results are stored in a sharedMap<sourceId, RecommendHitsState>. AforceRendercallback (stored in mutable render state) re-renders theAutocompleteWrappercomponent when recommend results arrive outside the normal search lifecycle.ScopedResult.resultstype wideningWidened to
SearchResults | RecommendResponse<any> | null.clearRefinementsandcurrentRefinementsconnectors guard withinstanceof SearchResultsso they correctly return no refinements for recommend sources.sendEventReuses
createSendEventForHits, the same helper used by all other recommend connectors.addAbsolutePositionCalled as
(hits, 0, hits.length || 1)— treats recommend results as page 0, so position numbers are meaningful and the interface can support pagination in a future change without breaking changes.API sketch
Test plan
showWhen: 'empty'and query is emptyshowWhen: 'empty')showWhen: 'always')sourcesorder controls panel rendering orderindicesoption still works (backwards compatibility)clearRefinementsandcurrentRefinementswork alongside recommend sourcessendEventfires correctly for recommend hits🤖 Generated with Claude Code