Skip to content

feat(autocomplete): add Recommend API sources to EXPERIMENTAL_autocomplete#7008

Draft
Haroenv wants to merge 6 commits into
masterfrom
feat/autocomplete-recommend-sources
Draft

feat(autocomplete): add Recommend API sources to EXPERIMENTAL_autocomplete#7008
Haroenv wants to merge 6 commits into
masterfrom
feat/autocomplete-recommend-sources

Conversation

@Haroenv
Copy link
Copy Markdown
Contributor

@Haroenv Haroenv commented May 4, 2026

Closes #7006

Summary

  • Adds a unified sources array to EXPERIMENTAL_autocomplete that can contain both search index sources and Recommend sources (trendingItems, frequentlyBoughtTogether, relatedProducts, lookingSimilar), replacing the deprecated indices array
  • Per-source showWhen: '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)
  • Source order in the sources array directly controls the rendering order, eliminating the need for a custom transformItems

Design decisions

Why a unified sources array instead of separate indices + recommend options?
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 shared Map<sourceId, RecommendHitsState>. A forceRender callback (stored in mutable render state) re-renders the AutocompleteWrapper component when recommend results arrive outside the normal search lifecycle.

ScopedResult.results type widening
Widened to SearchResults | RecommendResponse<any> | null. clearRefinements and currentRefinements connectors guard with instanceof SearchResults so they correctly return no refinements for recommend sources.

sendEvent
Reuses createSendEventForHits, the same helper used by all other recommend connectors.

addAbsolutePosition
Called 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

EXPERIMENTAL_autocomplete({
  container: '#autocomplete',
  sources: [
    // Recommend source (shown only when query is empty)
    {
      sourceType: 'recommend',
      model: 'trendingItems',
      limit: 5,
      showWhen: 'empty',   // default for recommend sources
      templates: {
        item({ item }) { /* ... */ },
      },
    },
    // Search index source (always shown)
    {
      sourceType: 'index',
      indexName: 'instant_search',
      showWhen: 'always',  // default for index sources
    },
  ],
});

Test plan

  • Verify recommend sources appear when showWhen: 'empty' and query is empty
  • Verify recommend sources are hidden when a query is typed (with showWhen: 'empty')
  • Verify index sources always appear (with showWhen: 'always')
  • Verify sources order controls panel rendering order
  • Verify deprecated indices option still works (backwards compatibility)
  • Verify clearRefinements and currentRefinements work alongside recommend sources
  • Verify sendEvent fires correctly for recommend hits

🤖 Generated with Claude Code

…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>
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 4, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 204 complexity · 0 duplication

Metric Results
Complexity 204
Duplication 0

View in Codacy

TIP This summary will be updated as you push new changes.

Haroenv and others added 3 commits May 4, 2026 16:16
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>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 4, 2026

More templates

algoliasearch-helper

npm i https://pkg.pr.new/algolia/instantsearch/algoliasearch-helper@7008

instantsearch-ui-components

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch-ui-components@7008

instantsearch.css

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch.css@7008

instantsearch.js

npm i https://pkg.pr.new/algolia/instantsearch/instantsearch.js@7008

react-instantsearch

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch@7008

react-instantsearch-core

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-core@7008

react-instantsearch-nextjs

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-nextjs@7008

react-instantsearch-router-nextjs

npm i https://pkg.pr.new/algolia/instantsearch/react-instantsearch-router-nextjs@7008

vue-instantsearch

npm i https://pkg.pr.new/algolia/instantsearch/vue-instantsearch@7008

commit: 1bef21a

…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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 sources array (index + recommend) and updates transformItems typing to operate on sources rather than indices.
  • Wires Recommend connectors into the autocomplete widget and exposes recommend entries in panel rendering (with showWhen visibility controls).
  • Updates connector types/tests to include sources while keeping indices as 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 sources exists, but there’s no coverage for the new behavior added in the connector: merging recommend sources from _recommendSources and 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 thread packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx Outdated
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 thread packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx Outdated
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);

Comment thread packages/instantsearch.js/src/connectors/autocomplete/connectAutocomplete.ts Outdated
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants