typesense / typesense

Open Source alternative to Algolia + Pinecone and an Easier-to-Use alternative to ElasticSearch ⚡ 🔍 ✨ Fast, typo tolerant, in-memory fuzzy Search Engine for building delightful search experiences
https://typesense.org
GNU General Public License v3.0
19.03k stars 585 forks source link

Can we Implement Autocomplete or Query Suggestions in Typesense using React-Instantsearch #570

Open N-pharmarack opened 2 years ago

N-pharmarack commented 2 years ago

Description

Typesense has made our life so easy by providing fast and free search capability, but there's one thing that is keeping away my searchbar from going into production, and that is Autocomplete, now a days no one wants to type whole name of product, computers must be smart enough to predict that.

I'm trying to implement Autocomplete using @algolia/autocomplete, Autocomplete uses separate collection for query suggestions purpose.

Code for App.js

Please excuse the length of code, I'm attaching just to be 100% sure I'm not wrng anywhere

Main part to look into in const plugins = .......

const typesenseInstantsearchAdapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: 'xyxyxy',
    nodes: [
      {
        host: 'xy.xy.xy.xyy',
        port: 'xyyy',
        protocol: 'http',
      },
    ],
    cacheSearchResultsForSeconds: 2 * 60,
  },
  additionalSearchParameters: {
    queryBy: 'name',
  },
});  

const searchClient = typesenseInstantsearchAdapter.searchClient;

function createURL(searchState) {
  return qs.stringify(searchState, { addQueryPrefix: true });
}

function searchStateToUrl({ location }, searchState) {
  if (Object.keys(searchState).length === 0) {
    return '';
  }

  return `${location.pathname}${createURL(searchState)}`;
}

function urlToSearchState({ search }) {
  return qs.parse(search.slice(1));
}

const VirtualSearchBox = connectSearchBox(() => null);

export function App() {
  const [searchState, setSearchState] = useState(() =>
    urlToSearchState(window.location)
  );
  const timerRef = useRef(null);

  useEffect(() => {
    clearTimeout(timerRef.current);

    timerRef.current = setTimeout(() => {
      window.history.pushState(
        searchState,
        null,
        searchStateToUrl({ location: window.location }, searchState)
      );
    }, 400);
  }, [searchState]);

  const onSubmit = useCallback(({ state }) => {
    setSearchState((searchState) => ({
      ...searchState,
      query: state.query,
    }));
  }, []);
  const onReset = useCallback(() => {
    setSearchState((searchState) => ({
      ...searchState,
      query: '',
    }));
  }, []);
  const plugins = useMemo(() => {
    const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
      key: 'search',
      limit: 3,
      transformSource({ source }) {
        return {
          ...source,
          onSelect(params) {
            setSearchState((searchState) => ({
              ...searchState,
              query: params.item.query,
            }));
          },
        };
      },
    });

    return [
      recentSearchesPlugin,
      createQuerySuggestionsPlugin({
        searchClient,
        indexName: 'query_suggestions1',
        getSearchParams() {
          return recentSearchesPlugin.data.getAlgoliaSearchParams({
            hitsPerPage: 5,
          });
        },
        transformSource({ source }) {
          return {
            ...source,
            onSelect(params) {
              setSearchState((searchState) => ({
                ...searchState,
                query: params.item.query,
              }));
            },
          };
        },
      }),
    ];
  }, []);
  return (
    <div className="container">
      <InstantSearch
        searchClient={searchClient}
        indexName="companies1"
        searchState={searchState}
        onSearchStateChange={setSearchState}
        createURL={createURL}
      >
        {/* A virtual search box is required for InstantSearch to understand the `query` search state property */}
        <VirtualSearchBox />

        <div className="search-panel">

          <div className="search-panel__results">
            <Autocomplete
              placeholder="Search"
              initialState={{
                query: searchState.query,
              }}
              openOnFocus={true}
              onSubmit={onSubmit}
              onReset={onReset}
              plugins={plugins}
            />
            <Hits hitComponent={Hit} />
          </div>
        </div>
      </InstantSearch>
    </div>
  );
}

And comes from below code :

export function Autocomplete(props) {
  const containerRef = useRef(null);

  useEffect(() => {
    if (!containerRef.current) {
      return undefined;
    }

    const search = autocomplete({
      container: containerRef.current,
      renderer: { createElement, Fragment },
      render({ children }, root) {
        render(children, root);
      },
      ...props,
    });

    return () => {
      search.destroy();
    };
  }, []);

  return <div ref={containerRef} />;
}

Actual Behavior

What I'm able to achieve is in below screenshot, I'm able to fetch data 'marked in red boxes' image

Expected Behavior

What I'm not getting is when I type it is not rendering anything image

It should only get me 'Stark Industry' but it not doing anything as I type,

Your help is much needed in this @jasonbosco

jasonbosco commented 2 years ago

@N-pharmarack In this code snippet:

      createQuerySuggestionsPlugin({
        searchClient,
        indexName: 'query_suggestions1',
        getSearchParams() {
          return recentSearchesPlugin.data.getAlgoliaSearchParams({
            hitsPerPage: 5,
          });
        },

Do you have a collection called query_suggestions1 in Typesense? You'd need to create this for this to work, or you might just want to point it to the companies1 collection you already have.

Also do you see any JS errors in the browser console?

N-pharmarack commented 2 years ago

Yes I have created a collection named query_suggestions1, it only has 'query' , 'objectID' and 'name' fields

I was getting error earlier that, you don't have objectID in your schema, so I created a new collection, 'objectID' is basically ID for alogia.

I-dingan commented 1 year ago

Hi I am having the same issue please help

jasonbosco commented 1 year ago

If you're able to share a codesandbox link with a minimal self-contained project that I can run and replicate the issue you're seeing, I can help debug from there...

nestoremgi commented 4 months ago

It's interesting how algolia propose a solution using react instant search and autocomplate, it looks easy not enought but easy https://www.algolia.com/doc/ui-libraries/autocomplete/integrations/with-react-instantsearch/?client=App.tsx but what about to implement something like this with typesense ? i suppose there is not anything build on it for handle it? but it there a good recipe to do it ?