typesense / typesense-instantsearch-adapter

A JS adapter library to build rich search interfaces with Typesense and InstantSearch.js
MIT License
361 stars 62 forks source link

how to use multiple instant search client in the same form? #194

Open weiklr opened 5 months ago

weiklr commented 5 months ago

Description

Hi, we were wondering how do we have multiple instant search clients (Typesense Wrapper int he screenshots) co-existing and working in the same form.

One instant search client points to an employee index in the participants field. Another one only instantiates in the modal and is pointing to the receipts collection. We found that the the 'query_by' in the employee index gets overriden by the receipts server adapter options when the modal is activated and then closed.

both instantsearch clients uses their own server adpater options object. How do we prevent overrides like this?

image (1)

initial correct payload: image (4)

After modal is activated and closed, we saw this console error: image (2)

employee index payload after modal is open and closed: Notice how filename is in query_by but this shouldn't be the case, and should be the same as initial correct payload image (3)

jasonbosco commented 5 months ago

If these are separate InstantSearch instances, then you want to use separate typesense-instantsearch-adapter instances with their own settings as well, so there's no interaction between these two instances.

If these are the same InstantSearch instances, then you you want to use this mechanism to specify settings for each collection using collectionSpecificSearchParameters: https://github.com/typesense/typesense-instantsearch-adapter?tab=readme-ov-file#index

weiklr commented 5 months ago

hmm i think there are considered separate instantsearch instances. We created a which contains an InstantSearch react component for each of these components.

sample code:

TypesenseWrapper:
const fullTypesenseAdapterOptions = generateTypesenseInstantsearchAdapterOptions();
const typesenseInstantSearchAdapter = new TypesenseInstantsearchAdapter(fullTypesenseAdapterOptions);
const searchClient = typesenseInstantSearchAdapter.searchClient;

const TypesenseWrapper = () => {
 /** THIN LAYER AROUND INSTANTSEARCH **/
      <InstantSearch
        searchClient={searchClient}
        indexName={indexName}
        {...instantSearchProps}
        future={{ preserveSharedStateOnUnmount: true }}
      >
        <Error />
        {children}
      </InstantSearch>
}

generateTypesenseInstantsearchAdapterOptions - function that returns a new typesense instance search adapter object:

const generateTypesenseInstantsearchAdapterOptions = (): TypesenseInstantsearchAdapterOptions => {
  // Search settings for full search
  return {
    server: {
      apiKey: '',
      nodes: [
        {
          host: ApiSettings.HOST,
          port: ApiSettings.PORT,
          protocol: ApiSettings.PROTOCOL,
        },
      ],
    },
    additionalSearchParameters: {
      query_by: 'q',
      facet_by: '',
      sort_by: '',
    },
  };
};

do you know how do i check if a typesense instantsearch adapter is its own instance or a shared one? cos from this code it seems like it should always create a new instance everytime.

jasonbosco commented 5 months ago

To create a separate typesense-instantsearch-adapter instance, you want to instantiate two separate instances of the class.

const typesenseInstantSearchAdapterEmployees = new TypesenseInstantsearchAdapter(typesenseAdapterOptionsForEmployees);

const typesenseInstantSearchAdapterReceipts = new TypesenseInstantsearchAdapter(typesenseAdapterOptionsForReceipts);

And then in each Instantsearch instance:

<InstantSearch
        searchClient={typesenseInstantSearchAdapterEmployees.searchClient}
        indexName={'employees'}
        {...instantSearchProps}
        future={{ preserveSharedStateOnUnmount: true }}
      >
        <Error />
        {children}
      </InstantSearch>

<InstantSearch
        searchClient={typesenseInstantSearchAdapterReceipts.searchClient}
        indexName={'receipts'}
        {...instantSearchProps}
        future={{ preserveSharedStateOnUnmount: true }}
      >
        <Error />
        {children}
      </InstantSearch>
weiklr commented 5 months ago

Hi Jason, Thanks, we made our TypesenseWrapper such that it now generates new typesenseInstantSearchAdapter instances using useMemo ,which should work in the same principle as your example.

However, we are also using onStateChange instantSearch props to manipulate the uiState and noticed the receipt typesense wrapper instance still references employee index even though they should be 2 separate instances already.

here's the typesensewrapper code for your reference:

const TypesenseWrapper2: React.FC<ITypesenseWrapper> = (props) => {
  const { children, onStateChange, initialUiState, typesenseAdapterOptions, scopedKeyUrl } = props;

  // created to create stable reference for useEffects
  const { data: typesenseServerConfig } = useQuery({
    queryKey: ['typesenseServerConfig'],
    queryFn: async () => await getTypesenseServerConfig(scopedKeyUrl),
    refetchOnWindowFocus: false,
  });

  const typesenseInstantsearchAdapter: TypesenseInstantsearchAdapter | null =
    useMemo((): TypesenseInstantsearchAdapter | null => {
      const {
        key: apiKey,
        index,
        host,
        port,
        protocol,
      } = typesenseServerConfig ?? {
        key: '',
        host: 'HOST',
        port: 1234,
        protocol: 'https'
      };

      if (!apiKey) return null;

      return new TypesenseInstantsearchAdapter({
        server: {
          apiKey,
          nodes: [{ host, port, protocol }],
        },
        additionalSearchParameters: { ...typesenseAdapterOptions.additionalSearchParameters },
        collectionSpecificSearchParameters: { ...typesenseAdapterOptions.collectionSpecificSearchParameters },
      });
    }, [typesenseServerConfig, typesenseAdapterOptions]);

  // Get the index name for instant search
  const instantSearchIndexName = useMemo(() => typesenseServerConfig?.index ?? '', [typesenseServerConfig]);

  const genInitialUiState = {
    [instantSearchIndexName]: { ...initialUiState },
  } as unknown as UiState;
  // Create a instant search client

  const instantSearchClient = useMemo(
    () => typesenseInstantsearchAdapter?.searchClient,
    [typesenseInstantsearchAdapter]
  );

  // Function that runs when users close idle modal and continue to use the page

  const instantSearchOnStateChange: InstantSearchProps['onStateChange'] = ({ uiState, setUiState }) => {
    if (!onStateChange) {
      return;
    }
    console.log('instant index:', instantSearchIndexName);
    onStateChange({ uiState, setUiState }, instantSearchIndexName);
  };

  const instantSearchProps = { onStateChange: instantSearchOnStateChange, initialUiState: genInitialUiState };
  if (!typesenseInstantsearchAdapter || !instantSearchClient) {
    return null;
  }

  return (
    <>
      <InstantSearch
        searchClient={instantSearchClient}
        indexName={instantSearchIndexName}
        {...instantSearchProps}
        future={{ preserveSharedStateOnUnmount: true }}
      >
        <Error />
        {children}
      </InstantSearch>
)

how we use this wrapper - receipts typesensewrapper

<TypesenseWrapper2
            scopedKeyUrl={SCOPED_KEY_URL}
            typesenseAdapterOptions={getTypesenseAdapterOptions()}
            onStateChange={onTypesenseStateChange2}
          >
            <AddReceiptModal isOpen onToggleModal={handleToggleReceiptModal} onUpdateReceipts={handleUpdateReceipts} />
</TypesenseWrapper2>

employee typesensewrapper as hoc. note that both are using different typesenseAdapterOptions object already.

export const withEmpListData = <T,>(Component: React.ComponentType<T>): React.FC<T> => {
  const WrappedComponent: React.FC<any> = (props) => (
    <TypesenseWrapper2
      scopedKeyUrl={SCOPED_KEY_URL}
      typesenseAdapterOptions={typesenseAdapterOptions}
      onStateChange={onTypesenseStateChange}
    >
      <Component {...props} />
    </TypesenseWrapper2>
  );

  WrappedComponent.displayName = `WithEmpListData(${Component.displayName ?? Component.name})`;

  return WrappedComponent;
};

now the issue is when i click on receipts modal, i still see that it's referencing employee index when it should be referencing receipt index when onStateChange is invoked. on subsequent clicks it behaves correctly. screenshot as follows:

image
jasonbosco commented 5 months ago

I'm not too familiar with React, so I can't speak to the use of useMemo. But it sounds like the instantsearch-adapter instances are still being shared somehow...