Grouping Instantsearch results under a common hierarchy/category

Hello, I’ve been amazed by Algolia and the implementation on my site has been quite smooth so far, great product! I am having trouble customizing my search results using Instantsearch.js, and hopefully someone can help out. Here is my problem:

Example search query: “Phone”

How my results currently display using Instantsearch.js:

  • Apple
    • iPhone
      • iPhone 1 content snippet (link)
  • Apple
    • iPhone
      • iPhone 2 content snippet (link)
  • Apple
    • iPhone
      • iPhone 3 content snippet (link)
  • Apple
    • iPhone
      • iPhone 4 content snippet (link)

How I want my results to display using Instantsearch.js: I want to eliminate the redundancy of repeating the same top-level hierarchy (i.e., Apple, iPhone).

  • Apple
    • iPhone
      • iPhone 1 content snippet (link)
      • iPhone 2 content snippet (link)
      • iPhone 3 content snippet (link)
      • iPhone 4 content snippet (link)

Or put another way, I want to replicate the Dropdown Search-UI in Docsearch, but using the Instantsearch.js API instead:

Screen Shot 2020-10-24 at 7.04.20 PM

I just can’t seem to find the right widget in the Instantsearch API that would do this. Thank you very much for any help!

My set up:
I am using docsearch + Docker to create my index. Here’s my config.json that I am using to crawl my site.

{
  "index_name": "index",
  "start_urls": [
    "https://www.website.com/"
  ],
  "selectors": {
    "lvl0": "#content h1",
    "lvl1": "#content h2",
    "text": "#content p"
  },
  "only_content_level": true,
}

And here is my client-side instantsearch.js set up

const search = instantsearch({
  indexName: 'instant_search',
  searchClient: algoliasearch(
    'APPID',
    'key'
  ),
});

search.addWidgets([
  instantsearch.widgets.searchBox({
    container: '#searchbox',
    placeholder: 'Search for a topic...',
    cssClasses: {
      root: 'MyCustomSearchBox',
      form: [
        'algolia-fix',
      ],
    },
  }),

  instantsearch.widgets.infiniteHits({
    container: '#infinite-hits',
    templates: {
      item: `
      <a href="{{url}}">
        <div class="hit-name">
            {{#helpers.highlight}}
               { "attribute": "hierarchy.lvl0" }
            {{/helpers.highlight}}
          <span class="badge">{{hierarchy.lvl1}}</span>
        </div>
        <div class="hit-description">
          {{#helpers.snippet}}
             { "attribute": "content" }
          {{/helpers.snippet}}
        </div>
      </a>
      `,
    },
  }),
]);
search.start();

Hi @tony,

Are you trying to create an improved filtering menu such as our hierarchical menu?

Not quite… (unless I’m misunderstanding the function :sweat_smile:). I don’t really want a filter or user selected option, but rather want to replicate the Dropdown Search-UI from Algolia’s Docsearch

Perhaps the live demo below from the Bootstrap 4 search that uses Algolia Docsearch Dropdown UI would help explain, because I want to replicate this exact display:

Thanks again for the help!

Hi @tony,

If I understood correctly the issue is only about displaying the element “grouped”. You should be able to group the results with the transformItems function. The option is called with the list of results retrieved from Algolia and lets you transform the structure before rendering.

If it’s not enough to solve your problem, could you please provide the structure of your records?

Thank you!

Hi Samuel,

Thanks! I think this is may be what I am looking for! The following is my general data structure:

"hits": [
    {
      "content": "Heading 1 Summary Lorem ipsum dolor sit amet",
      "hierarchy": {
        "lvl0": "Chapter 1",
        "lvl1": "Heading 1",
        "lvl2": null,
      }

  {
    "content": "Heading 2 Summary Lorem ipsum dolor sit amet",
    "hierarchy": {
      "lvl0": "Chapter 1",
      "lvl1": "Heading 2",
      "lvl2": null,
    }

{
    "content": "Heading 3 Summary Lorem ipsum dolor sit amet",
     "hierarchy": {
          "lvl0": "Chapter 1",
          "lvl1": "Heading 3",
          "lvl2": null,
    }
]

I think what I need to achieve is to transform the infinitehits widget https://www.algolia.com/doc/api-reference/widgets/infinite-hits/js/#widget-param-transformitems but am a bit lost on how to use the code:

I am hoping to have the following search display structure:

  • lvl0
    • lvl1
      • content
    • lvl1
      • content
    • lvl
      • content

Thanks so much!

Hi @tony,

Thank you for the structure of the data, it helps a lot!

Here is a snippet that group the data by the attribute hierarchy.lvl0 (see below). Note that we don’t use the the helper for the highlighting. It doesn’t work with this structure but you can fallback on directly accessing the value. The three {{{ are required to render the value as HTML. You can find details about the _highlightResult in the documentation. You can find the complete example on CodeSandbox.

instantsearch.widgets.hits({
    container: '#hits',
    templates: {
      item: `
        <ul>
          <h3>{{level}}</h3>
          {{#items}}
            <li>
              <h4>{{hierarchy.lvl1}}</h4>
              <div class="hit-description">
                {{{_highlightResult.content.value}}}
              </div>
            </li>
          {{/items}}
        <ul>
      `,
    },
    transformItems(items) {
      const groupBy = items.reduce((acc, item) => {
        const list = acc[item.hierarchy.lvl0] || [];

        return {
          ...acc,
          [item.hierarchy.lvl0]: list.concat(item),
        };
      }, {});

      return Object.keys(groupBy).map((level) => ({
        items: groupBy[level],
        level,
      }));
    },
  }),

Hope that helps!

2 Likes

Hi Samuel,

Yes this is amazing, and exactly what I was looking for! Thanks for getting me started with the sandbox :grinning: . However, I am encountering 1 error when I run your sandbox:

Steps to reproduce error:
Type in query in search box: Chapter

Error produced:

How might I correct this error?

Thanks again so much, Algolia support is awesome!

the error happened because one of your items does not have a hierarchy attribute.

You can prevent that to happen in two ways:

  1. add hierarchy to all items
  2. add a fallback to display nothing for that item, if there’s no hierarchy:
  transformItems(items) {
    const groupBy = items.reduce((acc, item) => {
      if (!item.hierarchy) {
        return acc;
      }

      const list = acc[item.hierarchy.lvl0] || [];

      return {
        ...acc,
        [item.hierarchy.lvl0]: list.concat(item),
      };
    }, {});

    return Object.keys(groupBy).map((level) => ({
      items: groupBy[level],
      level,
    }));
  },