Indexing keywords in a separated index and using them to search in another index (autocomplete)

Hello guys, I am trying to think of a way to search by keywords instead of item attributes, since there might be items with the same attribute value (ie. there can be many “BMW 116” in a single index) and I’d like not to have any duplicates in the search box while you typing.

Theoretically, I know the back-end solution already, which would be pre-building the keywords list by using our internal database and pushing them to Algolia, however how do I sync a searchBox of this “keywords index” with the hits widget of the actual index containing the items?

This is ideally what I am looking for:

Items Index:

[{
    "objectID": 1649,
    "trademark": "BMW",
    "model": "116",
    "price": 5000
},
{
    "objectID": 1604,
    "trademark": "BMW",
    "model": "116",
    "price": 5500,
},
{
    "objectID": 1059,
    "trademark": "BMW",
    "model": "320",
    "price": 10000
},
{
    "objectID": 5943,
    "trademark": "BMW",
    "model": "120",
    "price": 15000
}]

Keywords Index:

[{
    "keyword": "BMW 116"
}, {
    "keyword": "BMW 320"
}, {
    "keyword": "BMW 120"
}]

This way, when you type BMW, you see only one BMW 116 item in the autocomplete, instead of two.

Please note that I am trying to build this with instantsearch.js, and obviously it’s instantiated by using the index containing the items, not the keywords, so I have no idea on how to move myself to achieve this.

Are you using InstantSearch.js?

1 Like

Yes I am, as written in the main thread.

My bad :slight_smile:

Just to be clear about your use case: you want to build an autocomplete so that people can filter out some of the results? Have you considered search for facet values (blog post / doc)? This let you build a search for the values of a facet. If you’re keyword were to be added as a property of your records that would be easy to integrate as it’s built into instantsearch.js.

The problem by adding the keyword as attribute to the items is that the autocomplete is eventually going to give duplicate suggestions, since there could be items using the same combination of keywords.

I want to build a separate index with pre-computed keywords to search the items, so that duplicates are avoided.

That’s the whole point of search for facet values. When this is activated on a specific attribute, the engine creates behind the scene a new index without the duplicates.

On this website, you can search into artists and songs: https://playlist-finder.netlify.com/ but no new index is maintained by hand.

It does not seem to work.

https://gyazo.com/2d711b51a6bc0b9fffdf94907bbb11b4

You can see that there are duplicated values.

This is how the facet attributes are set:

https://gyazo.com/5d0b9aaf6d4ef72a162d2bb41e98716b

I need to use the search_keyword.

And, the search_keyword is not present among the _highlightResult array of attributes.

This is the code I am using:

var search = instantsearch({
    appId:     "{$appId}",
    apiKey:    "{$apiKey}",
    indexName: "vehicles_desc(online_since)",
    urlSync:   true
});

var client = algoliasearch('{$appId}', '{$apiKey}');
var index = client.initIndex('vehicles_desc(online_since)');

autocomplete(
    '#search-box input',
    { hint: true },
    {
        source: autocomplete.sources.hits(index, { hitsPerPage: 10 }),
        displayKey: 'search_keyword',
        templates: {
            suggestion: function(suggestion) {
                return suggestion._highlightResult.search_keyword.value;
            }
        }
    }
).on('autocomplete:selected', function(event, suggestion, dataset) {
    search.helper.setQuery(suggestion.name).search();
});

I have managed to achieve this using another solution select-based.

var customMenuRenderFn = function (renderParams, isFirstRendering) {
    var container          = renderParams.widgetParams.containerNode;
    var title              = renderParams.widgetParams.title || 'dropdownMenu';
    var templates          = renderParams.widgetParams.templates;
    var hideIfIsUnselected = renderParams.widgetParams.hideIfIsUnselected || false;
    var cssClasses         = renderParams.widgetParams.cssClasses || "";

    if (isFirstRendering)
    {
        $(container).append(
            (templates.header || '<h1>' + renderParams.widgetParams.attributeName + '</h1>') +
            '<select class="' + cssClasses.select + '">' +
                '<option value="__EMPTY__">Tutto</option>' +
            '</select>'
        ).hide();

        var refine = renderParams.refine;

        if (! hideIfIsUnselected)
        {
            $(container).show();
        }
        else
        {
            $(hideIfIsUnselected).find('select').on('items:loaded', function () {
                if (isFirstRendering) {
                    var valueToCheck = $(hideIfIsUnselected).find('select').val();

                    $(container).toggle(valueToCheck !== '__EMPTY__');

                    $(container).find('select').off('items:loaded');
                }
            });

            $(hideIfIsUnselected).find('select').on('change', function (event) {
                var value = event.target.value === '__EMPTY__' ? '' : event.target.value;

                if (value === '') {
                    refine();
                }

                $(container).toggle(value !== '');
            });
        }

        $(container).find('select').on('change', function (event) {
            var value = event.target.value === '__EMPTY__' ? '' : event.target.value;

            refine(value);
        });
    }

    function updateHits (hits)
    {
        var items  = renderParams.items;

        optionsHtml = ['<option class="' + cssClasses.item + '" value="__EMPTY__" selected>Tutto</option>']
            .concat(
                items.map(function (item) {
                    return `<option class="${cssClasses.item}" value="${item.value}" ${item.isRefined ? 'selected' : ''}>
                                ${item.label} (${item.count})
                            </option>`;
                })
            );

        $(container).find('select').html(optionsHtml);

        $(container).find('select').trigger('items:loaded');
    }

    if (hideIfIsUnselected && $(hideIfIsUnselected).val() !== '__EMPTY__') {
        updateHits(renderParams.items);
    } else if (! hideIfIsUnselected) {
        updateHits(renderParams.items);
    }
}

var dropdownMenu = instantsearch.connectors.connectMenu(customMenuRenderFn);