Faceted Search

This document explains how to use the Botfuel FacetedSearch module to connect to a database and help users search for items.

Overview

This module implements a SearchDialog, which is a subclass of PromptDiaglog, where entities are linked to facets of a database.

The questions asked by the SearchDialog and their order will depend on the user input and on the data contained in the database. SearchDialog will query the database when it receives new user input, it will determine the next question to ask to best narrow down the remaining possibilities (using a min-max algorithm by default), or will return the results if the done condition has been met (for example, if there is a single result).

See this demo for an example.

How to use the FacetedSearch module

Follow the following steps to use the FacetedSearch module in your bot.

Configuration

Specify the module in the bot configuration file.

module.exports = {
  modules: ['botfuel-module-facetedsearch'],
};

See Module Overview for more information on how to create and use a module.

Database

The SearchDialog requires one of the subclasses of FacetDb (which are exported by the FacetedSearch module).

The example below defines a clothing database where Brand, Color and Size are the facets that we would like to ask questions about. This example uses The PlainFacetDb which is an in-memory database that implements the FacetDb interface.

const { PlainFacetDb } = require('botfuel-module-facetedsearch');

const metadata = {
    filter: PlainFacetDb.DEFAULTFILTER({
        brand: PlainFacetDb.EQUAL,
        color: (value, param) => hexToColor(value) === param,
        size: PlainFacetDb.IN
    }),
    done: rows => rows && rows.length <= 2,
};

const data = [
  {
    "brand": "Polo Raph Laurent",
    "color": "White",
    "size": "[S,M,L,XL,XXL]",
    "price": 65,
    "image": "G_48929177_85_ZP_1.jpg"
  },
  {
    "brand": "Lacoste",
    "color": "Red",
    "size": "[S,M,L,XL]",
    "price": 95,
    "image": "G_80704372_401_ZP_1.jpg"
  },
  ...
];

const ArticleDb = new PlainFacetDb(data, metadata);
module.exports = ArticleDb;

The PlainFacetDb constructor takes 2 arguments:

  • data: the data array,
  • metadata: object containing a filter and a done function.

filter is a function which takes a query and a data row and returns a boolean indicating whether the row matches the query. A query is an object mapping facet names to facet values, eg {brand: 'Lacoste', color: '#00FFFF'}. PlainFacetDb provides a default filter which checks that all conditions on each facet are met.

done is a function indicating whether or not to stop the search based on the results of the search and the query.

Dialog

The SearchDialog is a subclass of PromptDialog. It takes a database (for example, the one defined previously) as a parameter.

const { SearchDialog } = require('botfuel-module-facetedsearch');
const ArticleDb = require('../db/article-db');

class ArticleDialog extends SearchDialog {}

ArticleDialog.params = {
  namespace: 'article',
  db: ArticleDb,
  entities: {
    brand: {
      dim: 'brand',
    },
    size: {
      dim: 'size',
      priority: 10,
    },
    color: {
      dim: 'color',
    },
  },
};

module.exports = ArticleDialog;

The entities defined in the dialog have to have the same names than the facets of the database.

Based on matched entities, the SearchDialog will determine the remaining facets to be asked as follow:

  • Facets with positive priorities will always be asked first provided that they are not already fulfilled (i.e answered in the previous user input).
  • For the remaining (still missing) facets, SearchDialog will select the one which optimizes the strategy (the default strategy, min-max, consists in minimizing the number of possibilities in the worst case).

For example, if the remaining facets are Brand and Color with the following counts:

brand: [
  { value:'Lacoste', count: 10 },
  { value:'Polo Raph Laurent', count: 4 }
],
color: [
  { value:'White', count: 7 },
  { value:'Red', count: 7 }
]

Then the next facet to be asked will be Color.

Thus, the missingEntities object that will be passed to the view (see the next section) will have the Color entity before the Brand entity.

View

The view should inherit SearchView and implement the method render(userMessage, data) as in the following example:

const { SearchView } = require('botfuel-module-facetedsearch');

class ArticleView extends SearchView {
  render(userMessage, data){
    const { matchedEntities, missingEntities, data, facetValueCounts } = data;
    const messages = [];
    if (missingEntities.size !== 0) {
      const facet = missingEntities.keys().next().value;
      // possible choices for facet
      const facetValues = Object.keys(facetValueCounts[facet])
      return [
        new BotTextMessage(`What ${facet} do you like?`),
        new QuickrepliesMessage(facetValues),
      ];
    }
    //...
  }
}

module.exports = ArticleView;
  • If you don't override the dialogWillDisplay function in your dialog then the data parameter passed to the render function will have the following properties:

    • facetValueCounts for the first missing entity so that you can propose possible choices for your question
    • data if there is no missing entity left so that you can return the search results to your user
  • If you want to override the dialogWillDisplay function in your dialog, you can call super.dialogWillDisplay to get the data and facetValueCounts defined above and pass them to your view.

Example

See this demo bot and its source code.