klevultd / frontend-sdk

Monorepository for Klevu headless
https://www.klevu.com
MIT License
21 stars 5 forks source link

How can I get a list of all Facets and values using the headless sdk? #138

Closed mdunbavan closed 1 year ago

mdunbavan commented 2 years ago

Hi,

I am building a headless Shopify site, I am currently looking at Hydrogen and your example of a setup.

Ideally what I would like to do with the headless sdk is get a response with all facets or something similar?

My problem is that it looks like getting data is really tightly coupled with using search query terms that come from a url param or a route that go to Klevu and then help return filters.

I do not want it to be tightly coupled a search term, I just want to return a dumb(non url param dependant) set of facets that I can get into the frontend and then populate as buttons and then only upon selecting those facets does it return the products I need.

Does that make sense?

jerrypena1 commented 2 years ago

Hi @mdunbavan.

What customers typically do in this situation is to perform a search using * (asterisk) as the search term. This will perform the most broad search in order to return every possible facet. Using the SDK you can control or even suppress the initial display of the products so you end up with the scenario you described.

As you mentioned facets are tightly coupled to a search (whether a term or category or recommendations for example), because they are based on the result set that is returned. That is why performing a search against * is a great way to achieve your request.

So for example, you can perform the following

import { listFilters, applyFilterWithManager, KlevuFetch, FilterManager, search, } from "@klevu/core";

const manager = new FilterManager();

export function SearchResultPage() { const [options, setOptions] = useState(manager.options); const [sliders, setSliders] = useState(manager.sliders); const [products, setProducts] = useState([]);

const initialFetch = useCallback(async () => { const res = await KlevuFetch( search( "*", listFilters({ filterManager: manager, }), applyFilterWithManager(manager) ) );

const searchResult = res.queriesById("search");
if (!searchResult) {
  return;
}

// here are all the facets to work with from the search on *
setOptions(manager.options);
setSliders(manager.sliders);

// here are your products to display or not display on initial page load
setProducts(searchResult.records ?? []);

});

// handle the display here return <></>; }

mdunbavan commented 2 years ago

Hi @jerrypena1

I have set this up and I get a response back with products which is great.

The only thing I am really struggling to get back are the facets. They look empty in the response. Do we have to tell Klevu that we explicitly want a certain set of facets back?

rallu commented 2 years ago

No need for explicit listing. To me it feels like it's a problem with data indexing. Can you check from Klevu Merchant Center (box.klevu.com) configuration that you have all facets listed and enabled in "Smart Search" -> "Customization" -> "Facets"

You can also open a support ticket in here https://help.klevu.com/support/tickets/new?cmd=sdk Then our support engineers can check your environment and help you in more detail :)

mdunbavan commented 2 years ago

Hi @rallu

All good above, I got the query working to get the filters in the end. I did the following below:

import {
  listFilters,
  applyFilterWithManager,
  KlevuFetch,
  FilterManager,
  KlevuPackFetchResult,
  KlevuHydratePackedFetchResult,
  search,
} from '@klevu/core';

import {useEffect, useState, useCallback, Fragment} from 'react';
import {useDebounce} from 'react-use';

import {searchQuery} from '~/services/klevu';

const manager = new FilterManager();

export function Facets() {
  const [options, setOptions] = useState(manager.options);
  const [products, setProducts] = useState([]);

  const initialFetch = useCallback(async () => {
    const result = await KlevuFetch(...searchQuery('*', new FilterManager()));

    /** pack query for transfering to client */
    const res = KlevuPackFetchResult(result);

    const searchResult = res.queryResults[0];
    if (!searchResult) {
      return;
    }

    // here are all the facets to work with from the search on *
    setOptions(searchResult.filters);

    // here are your products to display or not display on initial page load
    setProducts(searchResult.records ?? []);
  });

  // does init search to get filters
  const initSearch = useDebounce(initialFetch, 300);

  // should be the click handler that updates the product data
  const filterClick = async (key, name) => {
    manager.toggleOption(key, name);

    const result = await KlevuFetch(...searchQuery('*', manager));

    /** pack query for transfering to client */
    const res = KlevuPackFetchResult(result);
    console.log(res);
  };

  useEffect(() => {
    return () => {
      initSearch;
    };
  }, []);

  // handle the display here
  return (
    <>
      {options.map((o, i) => (
        <Fragment key={i}>
          <p>{o.label}</p>
          <ul key={i}>
            {o.options.map((o2, i2) => (
              <button
                key={i2}
                role={undefined}
                onClick={() => {
                  filterClick(o.key, o2.name);
                }}
              >
                <div>
                  <checkbox checked={o2.selected == true} />
                </div>
                <p>{`${o2.name} (${o2.count})`}</p>
                {o.key === 'color' ? (
                  <div
                    style={{
                      height: '16px',
                      width: '16px',
                      border: '1px solid gray',
                      backgroundColor: o2.name,
                      marginLeft: '8px',
                    }}
                  ></div>
                ) : null}
              </button>
            ))}
          </ul>
        </Fragment>
      ))}
    </>
  );
}

I think the only thing that would be really interesting to find out is how I could use the click function on the buttons to set the filter value and send it back to klevu?

You can see I have started looking at the implementation of it above.

jerrypena1 commented 2 years ago

Hi @mdunbavan

It looks like you are receiving the toggle of the filter option in your filterClick handler and updating the value in the manager instance correctly.

My only callout would be not to repackage the response again in the filterClick event handler since that is only necessary for SSR and by the time a filter has been clicked you are already on the client-side of things. Additionally once you get the result you need to update your state again like you did in the initialFetch function.

const searchResult = response.queryResults[0];
if (!searchResult) {
  return;
}

// here are all the facets to work with from the search on *
setOptions(searchResult.filters);

// here are your products to display or not display on initial page load
setProducts(searchResult.records ?? []);

This will cause the UI to redraw itself.

mdunbavan commented 2 years ago

Hey @jerrypena1 quick query.

I have done the following below:

 const filterClick = async (key, name) => {
    manager.toggleOption(key, name);
    KlevuListenDomEvent(KlevuDomEvents.FilterSelectionUpdate);

    const result = await KlevuFetch(...searchQuery('*', manager));
    const searchResult = result.queriesById('search');
    // saveProducts(searchResult.records);

    // here are all the facets to work with from the search on *

    // console.log(searchResult);

    saveProducts(searchResult.records);
    setOptions(searchResult.filters);
  };

For some odd reason, when I click the button once, it re-runs the request and gets all data like in the initialfetch function, if I hit the button again after that it returns the correct response with updated filters and the set of products that match the selected filter. See below:

{id: 'search', meta: {…}, records: Array(100), filters: Array(7), getSearchClickSendEvent: ƒ, …}
{id: 'search', meta: {…}, records: Array(1), filters: Array(4), getSearchClickSendEvent: ƒ, …}

The top response is the first click of the filter, the second below is the click after that returns the desired result.

Any idea if Klevu has some inner data retrieval that I am not aware of?

jerrypena1 commented 2 years ago

Hi @mdunbavan,

You need to refactor your filterClick function or break it up as we did in our examples. For example, KlevuListenDomEvent(KlevuDomEvents.FilterSelectionUpdate) is unnecessary since the FilterSelectionUpdate event is fired when manager.toggleOption is called. Since you called it in the line before, there isn't a point in registering an event handler. Also KlevuListenDomEvent(KlevuDomEvents.FilterSelectionUpdate) is missing a second parameter which is the handler function that should be called when that event is fired.

I must also apologize for not catching this sooner, but I would not use this code:

// here are all the facets to work with from the search on * setOptions(searchResult.filters);

Instead of trying to access searchResult.filters, it is recommended that you use manager.options (array for the checkboxes), and manager.sliders (array of sliders such as price in most cases, but this can be changed in KMC (Klevu Admin)). Your instance of FilterManager (typcially called manager) is great for managing the state of all the facets since it can be passed in to search to both send and receive the updated filters.

So manager.toggleOption changes the state within the manager instance, that instance gets passed into the search, and once you get the response, the manager is updated with the latest filters which you can then use to update the UI via manger.options & manager.sliders

image

Thank you for the feedback. We will update the documentation to make all of this clearer.

Please let us know if this feedback resolves your issue.