Implement Predictive Search in Liferay (II)

Part 2: Search as You Type

In the sequel for Implement Predictive Search in Liferay Part 1: Autocomplete I’ll demonstrate how to create a predictive search in another flavor: search as you type. It works like this: the user starts entering keywords in the search bar and gets instant content suggestions. The suggestions are direct links to contents.

In the previous demonstration I used a headless approach with the help of Liferay REST builder. In this example I’m however going to use another approach because I need portal links to the suggested contents.

Asset links within Liferay portal are created by asset type specific asset renderers. To be able to create an URL for an asset, the renderer needs a portlet request and a portlet response as seen in the API. Here it becomes tricky because REST APIs obviously don’t have those at hand because of not being portlets. If you want to build completely headless predictive search and only need headless API URLs to contents you’re good to continue with the REST approach but if you need portal URLs, links to contents on portal pages, it gets difficult. If you still want to have it all, both REST and portal links, some possible options are:

  • Have the links statically indexed within index documents

  • “Fake” the portlet request and response in the REST API to be able to generate URLs

Both are possible approaches as such but some challenges of their own, I’m not going here deeper into. For the sake of demonstration, to achieve my goals, I’m here taking the simplest approach and will simply create a Liferay MVC portlet project having a custom search bar portlet and a MVCResourceCommand serving suggestions. I'll then replace the out of the box search bar portlet with this custom one. No REST this time.

While this is a simple example I'm once again going to focus on key points of implementation only. The full source code is available in the link at the bottom.

Implementation steps outline:

  1. Create a MVC portlet project

  2. Implement the portlet class

  3. Implement the MVCResourceCommand for serving suggestions

  4. Implement a portlet shared search contributor

  5. Implement the user interface

  6. Test and have fun!

Tools used were:

  1. Liferay Developer Studio 3.9.5

  2. Liferay CE 7.4 GA10

  3. Liferay Workspace environment

1. Create a MVC Portlet Project

I'll first create an MVC Portlet module project predictive-search-web on my Liferay Workspace

2. Implement the Portlet Class

I'm going to use this portlet instead of the out of the box search bar so I have to make it to play together with the other out of the box ones. For that purpose I'm getting the keyword parameter name and value from shared search settings and make them available for the view JSP:


@Override
public void render(
		RenderRequest renderRequest, RenderResponse renderResponse)
	throws IOException, PortletException {

	PortletSharedSearchResponse portletSharedSearchResponse =
		_portletSharedSearchRequest.search(renderRequest);

	Optional keywordsOptional =
		portletSharedSearchResponse.getKeywordsOptional();

	renderRequest.setAttribute(
		"keywords", keywordsOptional.orElse(StringPool.BLANK));

	SearchSettings searchSettings =
		portletSharedSearchResponse.getSearchSettings();

	Optional keywordsParameterNameOptional =
		searchSettings.getKeywordsParameterName();

	renderRequest.setAttribute(
		"keywordsParameterName", keywordsParameterNameOptional.orElse("q"));

	super.render(renderRequest, renderResponse);
}    
 

3. Implement the MVCResourceCommand for Serving Suggestions

The logic of the MVC resource command here is very much the same as in the REST endpoint implemented in the prequel blog. Notably, I’m operating directly with JSON and adding an extra attributes array with a link to content and asset type fields to the response. Asset type field is used to group suggestions by type on the user interface.

Also this time  I’m not even creating the search query manually, but using the default Liferay search. The only things I'm setting there are the scope (groupId) and asset types to be searched:


private SearchRequest _getSearchRequest(ResourceRequest resourceRequest) {
	ThemeDisplay themeDisplay = (ThemeDisplay)resourceRequest.getAttribute(
		WebKeys.THEME_DISPLAY);

	return _searchRequestBuilderFactory.builder(
	).companyId(
		themeDisplay.getCompanyId()
	).groupIds(
		new long[] {themeDisplay.getScopeGroupId()}
	).modelIndexerClassNames(
		BlogsEntry.class.getName(), DLFileEntry.class.getName(),
		JournalArticle.class.getName()
	).queryString(
		ParamUtil.getString(resourceRequest, "keywords")
	).size(
		10
	).build();
}

You could here build your query as in the part 1 and just set that to the search request builder.

4. Implement a Portlet Shared Search Contributor

I need to wire my custom portlet to the shared search request. Otherwise, out of the box search bars removed from page,  it would just hang there on the page and would not do search on submit. The wiring is done with the help of PortletSharedSearchContributor API:


@Override
public void contribute(
	PortletSharedSearchSettings portletSharedSearchSettings) {

	SearchRequestBuilder searchRequestBuilder =
		portletSharedSearchSettings.getSearchRequestBuilder();

	Optional keywordsParameterNameOptional =
		portletSharedSearchSettings.getKeywordsParameterName();

	Optional keywordsOptional =
		portletSharedSearchSettings.getParameterOptional(
			keywordsParameterNameOptional.orElse("q"));

	searchRequestBuilder.queryString(
		keywordsOptional.orElse(StringPool.BLANK));
}

Note that I'm leaving some work behind for a complete implementation. Notably, I'm ignoring  federated search requests. To see how it's done in out of the box search bar, please take a look its source code.

5. Implement the User Interface

For the sake of demonstration and simplicity I’m again using the DevBridge Autocomplete UI component, but at this point you might want to take a look at Liferay Clay components and React based approach to make this more robust and better integrate to Liferay frontend architecture. 

I'm using the widget template code from part 1 but slightly better organized. As the biggest difference to widget template I'm now using NPM to manage JS dependencies.

In addition to setting up NPM the files modified were:

I’m also grouping the suggestions by asset type as that's nicely supported by the DevBridge Autocomplete component

6. Test and Have fun!

That’s it for coding. Go to your search page and remove the standard search bar. Then add our custom search bar Predictive Search Bar Widget to the page.

Feed your portal with some contents and start typing in search box and something like this should happen:


 

Closing Notes

What next? While either of the predictive search approaches I demonstrated might be good starting points for your own implementation, it would be good to have a single predictive search service to be able to serve suggestions from several data sources in a single stream. Also using Search Blueprints to control queries to get suggestions would set us completely free from having to hardcode any query logic in our service. Sounds like there’s another sequel blog to come…

Thanks for reading! hope this gave some ideas for creating your own predictive search on Liferay.

Source code here. 

0