Type Ahead Concepts
The type ahead functionality in the enterprise search module uses a custom Solr solution for delivering type ahead
suggestions. This is primarily done through using advanced filter factories, highlighting, and boosting.
The first aspect of the type ahead functionality comes from the use of advanced filter factories such as Solr's
EdgeNGramFilterFactory
. This filter processes a term hot
into edge ngrams h
, ho
, and hot
. This is used by our
type ahead so that when the user types ho
they end up matching a field that has the word hot
, hoop
, or horn
.
This functionality is fairly powerful and allows for some complicated matching scenarios. For instance, ho sa
would match
hot sauce
and also match saddle horn
.
The second aspect of the type ahead functionality comes from the use of Solr's highlighting engine. Simply put,
highlighting in Solr allows us to specify a field name and a query, which Solr processes and then returns the contents of that field
with the words matching that query surrounded with a denominator, typically ***
. We then take these words and use them
as the suggestions for our type ahead.
The third aspect of the type ahead functionality comes from the use of boosting within Solr. We allow an admin user to
specify a boost value for each field we query against. This boost value influences the score (ordering) of the documents
(and highlighting suggestions) returned by Solr. Using boost values allows the user to give more weight to a match
against a product's name than a product's description.
Here is an example gif of the type ahead functionality in action on our private demo site.
Note: The Heat Clinic demo site has only a handful of products. Our larger catalog clients end up with a significantly
larger amount of suggestions.
Type Ahead Configuration
If you are using the type ahead functionality in your frontend website you will need to follow these steps:
Add Type Ahead Configuration through admin or sql:
If you choose to add the configuration through the admin or want to modify your type ahead configuration later, navigate to
/admin/type-ahead
.
If you prefer to add the configuration using sql, here is an example insert from our demo data:INSERT INTO BLC_TYPE_AHEAD_CONFIG (TYPE_AHEAD_CONFIG_ID, ACTIVE_END_DATE, ACTIVE_START_DATE, CATEGORY_LIMIT, CATEGORY_SUGGESTIONS, FRAGMENT_SIZE, NAME, PHRASE_PROXIMITY, SEARCH_URL, SUGGESTION_LIMIT) VALUES (-33000, NULL, CURRENT_TIMESTAMP, 5, TRUE, 30, 'Demo Type Ahead', 2, '/search', 8);
Add type ahead fields to the set of indexed fields
You will need to add in some index field entities to your application to be targeted by the type ahead search.
This can be done withing the admin by going to/admin/search-field
, or by sql like the following:-- Product Name Field INSERT INTO BLC_INDEX_FIELD (INDEX_FIELD_ID, FIELD_ID, SEARCHABLE) VALUES (-33001, 4, FALSE); INSERT INTO BLC_INDEX_FIELD_TYPE (INDEX_FIELD_TYPE_ID, INDEX_FIELD_ID, FIELD_TYPE) VALUES (-33001, -33001, 'tta'); -- Manufacturer Field INSERT INTO BLC_INDEX_FIELD (INDEX_FIELD_ID, FIELD_ID, SEARCHABLE) VALUES (-33002, 1, FALSE); INSERT INTO BLC_INDEX_FIELD_TYPE (INDEX_FIELD_TYPE_ID, INDEX_FIELD_ID, FIELD_TYPE) VALUES (-33002, -33002, 'tta'); -- Long Description Field INSERT INTO BLC_INDEX_FIELD (INDEX_FIELD_ID, FIELD_ID, SEARCHABLE) VALUES (-33003, 7, FALSE); INSERT INTO BLC_INDEX_FIELD_TYPE (INDEX_FIELD_TYPE_ID, INDEX_FIELD_ID, FIELD_TYPE) VALUES (-33003, -33003, 'tta');
Note: The field type
tta
corresponds to the solr field type fortext_type_ahead
defined in yourschema.xml
.Add the query field and highlight field relationships to the configuration
In order for the configuration to know which fields to query against and highlight against, you must add
relationships between your configuration and your index fields. You can do this through the admin, or by sql:-- name hl field INSERT INTO BLC_TYPE_AHEAD_HLT_FIELD (TYPE_AHEAD_HLT_FIELD_ID, TYPE_AHEAD_CONFIG_ID, INDEX_FIELD_TYPE_ID) VALUES (-33000, -33000, -33001); -- mfg hl field INSERT INTO BLC_TYPE_AHEAD_HLT_FIELD (TYPE_AHEAD_HLT_FIELD_ID, TYPE_AHEAD_CONFIG_ID, INDEX_FIELD_TYPE_ID) VALUES (-33001, -33000, -33002); -- name query field INSERT INTO BLC_TYPE_AHEAD_QUERY_FIELD (TYPE_AHEAD_QUERY_FIELD_ID, TYPE_AHEAD_CONFIG_ID, INDEX_FIELD_TYPE_ID, BOOST) VALUES (-33000, -33000, -33001, 10); -- mfg query field INSERT INTO BLC_TYPE_AHEAD_QUERY_FIELD (TYPE_AHEAD_QUERY_FIELD_ID, TYPE_AHEAD_CONFIG_ID, INDEX_FIELD_TYPE_ID, BOOST) VALUES (-33001, -33000, -33002, 3); -- desc query field INSERT INTO BLC_TYPE_AHEAD_QUERY_FIELD (TYPE_AHEAD_QUERY_FIELD_ID, TYPE_AHEAD_CONFIG_ID, INDEX_FIELD_TYPE_ID, BOOST) VALUES (-33002, -33000, -33003, 1);
Add a new controller
TypeAheadController
to your site to process type ahead requests:@RestController @RequestMapping("/type-ahead") public class TypeAheadController extends com.broadleafcommerce.search.web.controller.TypeAheadController { @Override @RequestMapping(value = "/search", produces = "application/json") public List<TypeAheadSuggestion> typeAhead(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "q") String query, @RequestParam(value = "name", required = false) String name, @RequestParam(value = "contributor", required = false) Set<String> includedContributorKeys) { return super.typeAhead(request, response, query, name, includedContributorKeys); } }
All that is left to set up the frontend portion of the application. This typically entails creating html for
displaying the type ahead results underneath the search box in the header, as well as javascript that is responsible for
making the ajax to get new type ahead results when they user types characters. Here is an ajax call to get suggestions:BLC.ajax({ type: 'GET', url: '/type-ahead/search', traditional: true, // so that the contributor request parameters are excluded array brackets data: { q: query, name: 'Demo Type Ahead', contributor: ['categories', 'keywords', 'manufacturers', 'products'], 'contributor.products.limit': 5 // specify a custom limit for the product suggestions }, }, function(data) { // update page with type ahead results });
TypeAheadSuggestionContributor
If you want to extend or contribute to the typeahead search, this can be done through creating your own implementation
of the TypeAheadSuggestionContributor
interface. Here is an example implementation of this interface:
@Service
public class ManufacturerTypeAheadSuggestionContributorImpl implements TypeAheadSuggestionContributor {
protected static final String CONTRIBUTOR_KEY = "manufacturers";
protected static final String MANUFACTURER_LIMIT_OPTION_KEY = "limit";
protected static final String MANUFACTURER_MINCOUNT_OPTION_KEY = "mincount";
protected final Environment environment;
public ManufacturerTypeAheadSuggestionContributorImpl(Environment environment) {
this.environment = environment;
}
@Override
public boolean canHandle(Set<String> includedContributorKeys, TypeAheadConfiguration config) {
return includedContributorKeys != null && includedContributorKeys.contains(CONTRIBUTOR_KEY);
}
@Override
public void contribute(SolrQuery solrQuery, TypeAheadConfiguration config, TypeAheadCriteria typeAheadCriteria) {
String fieldName = getManufacturerFieldName();
solrQuery.addFacetField(fieldName);
solrQuery.set("f." + fieldName + ".facet.mincount", getFacetMinCount(config, typeAheadCriteria));
solrQuery.set("f." + fieldName + ".facet.limit", getFacetLimit(config, typeAheadCriteria));
}
@Override
public void process(TypeAheadResponse typeAheadResponse, QueryResponse queryResponse, TypeAheadConfiguration config, TypeAheadCriteria typeAheadCriteria) throws TypeAheadException {
FacetField facetField = queryResponse.getFacetField(getManufacturerFieldName());
if (facetField != null) {
List<TypeAheadSuggestion> suggestions = new ArrayList<>();
for (FacetField.Count count : facetField.getValues()) {
suggestions.add(buildManufacturerSuggestion(count, typeAheadCriteria.getQuery(), config));
}
typeAheadResponse.suggestions(CONTRIBUTOR_KEY, suggestions);
} else {
throw new TypeAheadException("Category suggestion facet not found on Solr response", typeAheadCriteria.getQuery(), config.getName());
}
}
protected TypeAheadSuggestion buildManufacturerSuggestion(FacetField.Count count, String query, TypeAheadConfiguration config) throws TypeAheadException {
String manufacturer = count.getName();
return new TypeAheadSuggestion()
.suggestion(manufacturer)
.url(config.getSearchUrl() + "?" + URLEncodedUtils.format(Arrays.asList(
new BasicNameValuePair(SearchCriteria.QUERY_STRING, query),
new BasicNameValuePair(getManufacturerFieldKey(), manufacturer)), "UTF-8"))
.put("count", count.getCount());
}
protected int getFacetLimit(TypeAheadConfiguration config, TypeAheadCriteria typeAheadCriteria) {
Map<String, Object> options = typeAheadCriteria.getContributorOptions().get(CONTRIBUTOR_KEY);
if (MapUtils.isNotEmpty(options)) {
if (options.containsKey(MANUFACTURER_LIMIT_OPTION_KEY)) {
try {
return Integer.parseInt((String) options.get(MANUFACTURER_LIMIT_OPTION_KEY));
} catch (NumberFormatException e) {
// do nothing, we want the default behavior below to happen
}
}
}
return environment.getProperty("typeahead.manufacturers.limit", Integer.class, 5);
}
protected int getFacetMinCount(TypeAheadConfiguration config, TypeAheadCriteria typeAheadCriteria) {
Map<String, Object> options = typeAheadCriteria.getContributorOptions().get(CONTRIBUTOR_KEY);
if (MapUtils.isNotEmpty(options)) {
if (options.containsKey(MANUFACTURER_MINCOUNT_OPTION_KEY)) {
try {
return Integer.parseInt((String) options.get(MANUFACTURER_MINCOUNT_OPTION_KEY));
} catch (NumberFormatException e) {
// do nothing, we want the default behavior below to happen
}
}
}
return environment.getProperty("typeahead.manufacturers.mincount", Integer.class, 1);
}
protected String getManufacturerFieldKey() {
return getManufacturerFieldName().split("_")[0];
}
protected String getManufacturerFieldName() {
return "mfg_s";
}
}
We provide several contributors out of box, including KeywordsTypeAheadSuggestionContributorImpl
, ProductTypeAheadSuggestionContributorImpl
,
and CategoryTypeAheadSuggestionContributorImpl
. You can activate a contributor for a type ahead search by passing along
the request parameter contributor
with the contributor key, e.g. contributor=keywords
.
Managed Synonyms
EnterpriseSearch includes support for managing synonyms in the admin using Solr's RestManager API. This functionality is easy to enable within your project. All you need to do is ensure that your schema.xml
is up to date and includes the field types denoted in Data Changes above, and ensure you include the admin security configuration from load_enterprise_search_admin_security.sql
mentioned earlier.
Once your schema.xml
is configured and your admin security is setup correctly, you should be able to manage synonyms at /admin/synonyms
.
Reloading Collections
One of the most important steps to remember when managing synonyms is to reload the collections after doing so. This action ensures that the cores are aware of the current state of the managed resources. Once the cores/collections reload, you can test making a search on your site and see if the synonym mapping is working.
One thing to remember is that by default we are using query-time analyzers to include the managed synonyms, however, if you are using index-time analyzers, you MUST reindex your Solr collections before the synonyms will work.