Auto Complete Search Functionality in Liferay

blog-banner

The autocomplete search feature gives the user the flexibility to interact with the application quickly as the list of results provides the data with exact matches. Here, we use Liferay's default search engine (Elasticsearch) to get the list of results. We fetch journal articles published on Liferay. Similarly, you can get Liferay's other entities as well.

To implement autocomplete search functionality, we need to create an MVC Portlet.

Creating an MVC portlet:

  • Create a new Liferay Module Project in the existing Liferay workspace.
  • Give the name of the project as journal-article-search-portlet and click next.
    • Component Class Name: JournalArticleSearchPortlet
    • Package name: com.stpl.journal.article.search
  • Open init.jsp from src > main > resources > META-INF > resources and add the following code.
<%
JournalArticleSearchConfiguration journalArticleSearchConfiguration =
    	(JournalArticleSearchConfiguration)
    	renderRequest.getAttribute(JournalArticleSearchConfiguration.class.getName());

	String searchScope = StringPool.BLANK;
	String keywordsParameterName = StringPool.BLANK;
	String scopeParameterName = StringPool.BLANK;
	String destinationPage = StringPool.BLANK;
    
	if (Validator.isNotNull(journalArticleSearchConfiguration)) {
   	 searchScope = portletPreferences.getValue(
            	"searchScope", journalArticleSearchConfiguration.searchScope());
   	 keywordsParameterName=portletPreferences.getValue(
            	"keywordsParameterName", journalArticleSearchConfiguration.keywordsParameterName());
   	 scopeParameterName=portletPreferences.getValue(
            	"scopeParameterName", journalArticleSearchConfiguration.scopeParameterName());
   	 destinationPage=portletPreferences.getValue(
            	"destinationPage", journalArticleSearchConfiguration.destinationPage());
	}
%>
  • In view.jsp create a custom search bar.
<%@ include file="/init.jsp" %>
    <%
    JournalArticleSearchConfiguration configuration = portletDisplay.getPortletInstanceConfiguration( JournalArticleSearchConfiguration.class);
    
    String searchDestinationPage = configuration.destinationPage();
    keywordsParameterName = configuration.keywordsParameterName();
    %>
    
    <liferay-portlet:resourceURL id="searchURL" var="searchURL" />
    
    <aui:form name="fm" cssClass="journal-article-search-form">
        <div class="row">
            <div class="col-md-7">
                <div class="form-group">
                    <div class="input-group">
                        <div class="input-group-item">
                            <input class="form-control input-group-inset input-group-inset-after"
                                id="webcontentSearchKeyword" name="q" type="text" autocomplete="off"
                                placeholder="<%= LanguageUtil.get(request, "search-webcontent") %>"
                            />
                            <span class="input-group-inset-item input-group-inset-item-after" style="display:none;"
                                id="webcontentSearchSpinner">
                                <span class="inline-item inline-item-middle">
                                    <span class="loading-animation" role="presentation"></span>
                                </span>
                            </span>
                            <ul class="autocomplete-dropdown-menu dropdown-menu show" style="display:none;"
                                id="listElements">
        
                            </ul>
                            <div class="input-group-inset-item input-group-inset-item-after">
                                <button class="btn btn-monospaced btn-unstyled" type="submit" id="submitButton">
                                    <liferay-ui:icon icon="search" markupView="lexicon" />
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </aui:form>
    

Adding a keyup event to the search bar:

  • Now we have to implement autocomplete functionality on the search bar, So add the following aui script below the form in view.jsp.
<aui:script>
    Liferay.on('allPortletsReady', function() {
   	 AUI().use('aui-io-request', function(A) {
   	     	A.one('#webcontentSearchKeyword').on('keyup', function() {
   	             	var webcontentSearchKeyword = this.get('value');

   	             	var data = {
   	                 	"articleName": webcontentSearchKeyword
   	             	};

   	             	callServer(data);   					   
   	         	}
   	     	);

   	     	function callServer(data) {
   	         	A.io.request('<%=searchURL%>', {data, on: { success: function() {
   	                         	var response = JSON.parse(this.get('responseData'));
   	                         	var listElements = '';
   	                         	var keyword = A.one('#webcontentSearchKeyword').val();
   	                        	 
   	                         	A.one('#listElements').hide(true);
   	                        	 
   	                         	if (keyword != null && keyword != "" && response != "undefined" && response.length > 0) {
   	                             	response.forEach(function(data, index) {
                               			 listElements += '<li value="' + data.articleTitle + '"><a href="' + data.searchResultViewURL + '">' + data.articleTitle + '</a></li>';
   	                             	});
   	                            	 
   	                             	A.one('#listElements').setHTML(listElements);
   	                            	 
   	                             	A.one('#listElements').show(true);
   	                         	}
   	                     	}
   	                 	}
   	             	}
   	         	);
   	     	}
   	 	}
   	 );
    }
);
</aui:script>

Creating MVCResourceCommand:

  • The above script will add a keyup event on the search bar and navigate the control to the MVCResourceCommand. So let's create an MVCResourceCommand to get journal articles from elasticsearch.
  • Create a class called JournalArticleSearchMVCResourceCommand under the com.stpl.journal.article.search.portlet.action package.
  • This class should implement the MVCResourceCommand and override its serveResource method.
  • Add @Component and the necessary properties and services to the class.
  • Let's break down the implementation into smaller methods.
  • Get the keywords from the request and set them in the SearchContext.
private SearchContext getSearchcontext(ResourceRequest resourceRequest) {
    HttpServletRequest httpServletRequest = PortalUtil.getHttpServletRequest(resourceRequest);
    HttpServletRequest httpRequest = PortalUtil.getOriginalServletRequest(httpServletRequest);

    String keywords = ParamUtil.getString(httpRequest, "articleName");

    SearchContext searchContext = SearchContextFactory.getInstance(httpRequest);
    searchContext.setKeywords(keywords);
    searchContext.setAttribute("paginationType", "more");
    searchContext.setStart(0);
    searchContext.setEnd(6);

    return searchContext;
}
  • To get the title and view URL of the journal article from the Asset Entry, create a custom WebContent model under the com.stpl.journal.article.search.model package.
public class WebContent implements Serializable {

    private String articleTitle;
    private String searchResultViewURL;

    [getters…]
    [setters…]
    [hashCode()]
    [equals()]
    [toString()]
}
  • Using IndexerRegistryUtil we can get the Indexer of JournalArticle. After that, from the Indexer<JournalArticle> object, we can search for the journal articles.
private List<WebContent> searchWebContent(SearchContext searchContext,
    ResourceRequest request, ResourceResponse response, ThemeDisplay themeDisplay) {

        User user = themeDisplay.getUser();
        String languageId = user.getLanguageId();
    
        Indexer<JournalArticle> indexer = IndexerRegistryUtil.getIndexer(JournalArticle.class);
        Hits hits = indexer.search(searchContext);
        List<WebContent> webcontentList = new ArrayList<>();
    
        for (int i = 0; i < hits.getDocs().length; i++) {
            Document doc = hits.doc(i);
    
            String articleTitle = GetterUtil.getString(doc.get("title_" + languageId));
            String entryClassPK = GetterUtil.getString(doc.get("entryClassPK"));
            String entryClassName = GetterUtil.getString(doc.get("entryClassName"));
    
            AssetEntry assetEntry = _assetEntryLocalService.getEntry(entryClassName, Long.valueOf(entryClassPK));
            String searchResultViewURL = getSearchResultViewURL(assetEntry.getEntryId(), request, response, themeDisplay);
    
            WebContent webContent = new WebContent();
            webContent.setArticleTitle(articleTitle);
            webContent.setSearchResultViewURL(searchResultViewURL);
    
            webcontentList.add(webContent);
        }
        return webcontentList;
    }
  • Get the reference for AssetEntryLocalService.
@Reference
AssetEntryLocalService _assetEntryLocalService;
  • Here we get the list of journal articles. Now we have to set the view url as well, so create the view url.
private static String getSearchResultViewURL(long articleId, ResourceRequest resourceRequest,
    ResourceResponse resourceResponse, ThemeDisplay themeDisplay) {

        PortletURL viewContentURL = resourceResponse.createRenderURL();
    
        viewContentURL.setParameter("mvcPath", "/view_content.jsp");
        viewContentURL.setWindowState(WindowState.MAXIMIZED);
        viewContentURL.setPortletMode(PortletMode.VIEW);
        viewContentURL.setParameter("assetEntryId", String.valueOf(articleId));
        viewContentURL.setParameter("type", "content");
    
        String viewURL = viewContentURL.toString();
        String currentURL = themeDisplay.getURLPortal();
    
        return HttpUtil.setParameter(viewURL, "p_l_back_url", currentURL);
    }
  • Convert the list of journal articles into JSON and return it.
private JSONArray getResponse(List<WebContent> webContentList) {
    
    JSONArray jsonArray = JSONFactoryUtil.createJSONArray();
    
    for (WebContent webContent : webContentList) {
   	 JSONObject jsonObject = JSONFactoryUtil.createJSONObject();
   	 jsonObject.put("articleTitle", webContent.getArticleTitle());
   	 jsonObject.put("searchResultViewURL", webContent.getSearchResultViewURL());
   	 jsonArray.put(jsonObject);
    }
    
    return jsonArray;
}

  • Override the serveResource method and pass the response to the view layer.
@Override
    public boolean serveResource(ResourceRequest resourceRequest, ResourceResponse resourceResponse)
    throws PortletException {

SearchContext searchContext = getSearchcontext(resourceRequest);

ThemeDisplay themeDisplay = (ThemeDisplay) resourceRequest.getAttribute(WebKeys.THEME_DISPLAY);

List<WebContent> webContentList = searchWebContent(searchContext, resourceRequest, resourceResponse, themeDisplay);

JSONArray response = getResponse(webContentList);

resourceResponse.setContentType("application/json");
try {
    PrintWriter writer = resourceResponse.getWriter();
    writer.println(response.toString());
    return true;
} catch (IOException ioException) {
    ioException.printStackTrace();
    return false;
}
}

Creating a configuration interface:

  • The portlet is ready to search but the portlet configuration is required as this is similar to Liferay's default search bar.
  • Create an interface called JournalArticleSearchConfiguration under com.stpl.journal.article.search.configuration package.
@Meta.OCD(
	id = "com.stpl.journal.article.search.configuration.JournalArticleSearchConfiguration," 
	localization = "content/Language", name = "journal-article-search-configuration"
)
public interface JournalArticleSearchConfiguration {

    @Meta.AD (deflt = "everything", name = "searchScope", required = false)
    public String searchScope();

    @Meta.AD (deflt = "q", name = "keywordsParameterName", required = false)
    public String keywordsParameterName();

    @Meta.AD(deflt = "scope", name = "scopeParameterName", required = false)
    public String scopeParameterName();

    @Meta.AD(deflt = "/search", name = "destinationPage", required = false)
    public String destinationPage();
}
  • Add configurationPid attribute to @Component in the JournalArticleSearchPortlet.
configurationPid="com.stpl.journal.article.search.configuration.JournalArticleSearchConfiguration"
  • Create a configuration.jsp file in src > main > resources > META-INF > resources and obtain the configuration object from the request object and read the desired configuration value from it.
<%@ include file="/init.jsp" %>

    <liferay-portlet:actionURL portletConfiguration="<%= true %>"
	var="configurationActionURL" />

<liferay-portlet:renderURL portletConfiguration="<%= true %>"
	var="configurationRenderURL" />

<aui:form action="<%= configurationActionURL %>" method="post" name="fm" cssClass="container-view">
	<div class="sheet sheet-lg">
    	<aui:input name="<%= Constants.CMD %>" type="hidden"
        	value="<%= Constants.UPDATE %>" />

    	<aui:input name="redirect" type="hidden"
        	value="<%= configurationRenderURL %>" />
       	 
    	<aui:input label="keywords-parameter-name" name="keywordsParameterName" type="text"
        	value="<%= keywordsParameterName %>" />
       	 
    	<aui:input label="scope-parameter-name" name="scopeParameterName" type="text"
        	value="<%= scopeParameterName %>" />   	 

    	<aui:fieldset>
        	<aui:select label="search-scope" name="searchScope"
            	value="<%= searchScope %>">
            	<aui:option label="this-site" value="this-site" />
            	<aui:option label="everything" value="everything" />
        	</aui:select>

    	</aui:fieldset>

    	<aui:input label="destination-page" name="destinationPage" type="text"
         	value="<%= destinationPage %>" />
   	 
    	<aui:button-row>
        	<aui:button type="submit"></aui:button>
    	</aui:button-row>
	</div>
</aui:form>

Implementing a configuration action:

  • To implement a configuration action, you should create a class that extends Liferay's DefaultConfigurationAction class. So, create a class called JournalArticleSearchActionConfiguration that extends DefaultConfigurationAction.
@Component(
	configurationPid = "com.stpl.journal.article.search.configuration.JournalArticleSearchConfiguration",
	configurationPolicy = ConfigurationPolicy.OPTIONAL,
	immediate = true,
	property = {
    	"javax.portlet.name="+ JournalArticleSearchPortletKeys.JOURNALARTICLESEARCH,
	},
	service = ConfigurationAction.class
)
public class JournalArticleSearchActionConfiguration extends DefaultConfigurationAction {

    @Override
    public void processAction(PortletConfig portletConfig, ActionRequest actionRequest,
   		 ActionResponse actionResponse) throws Exception {

   	 String searchScope = ParamUtil.getString(actionRequest, "searchScope");
   	 String keywordsParameterName = ParamUtil.getString(actionRequest,
   			 "keywordsParameterName");
   	 String scopeParameterName = ParamUtil.getString(actionRequest, "scopeParameterName");
   	 String destinationPage = ParamUtil.getString(actionRequest, "destinationPage");

   	 setPreference(actionRequest, "searchScope", searchScope);
   	 setPreference(actionRequest, "keywordsParameterName", keywordsParameterName);
   	 setPreference(actionRequest, "scopeParameterName", scopeParameterName);
   	 setPreference(actionRequest, "destinationPage", destinationPage);

   	 super.processAction(portletConfig, actionRequest, actionResponse);
    }

    @Override
    public void include(PortletConfig portletConfig, HttpServletRequest httpServletRequest,
   		 HttpServletResponse httpServletResponse) throws Exception {

   	 httpServletRequest.setAttribute(JournalArticleSearchConfiguration.class.getName(),
   			 journalArticleSearchConfiguration);

   	 super.include(portletConfig, httpServletRequest, httpServletResponse);
    }

    @Activate
    @Modified
    protected void activate(Map<Object, Object> properties) {
   	 journalArticleSearchConfiguration = ConfigurableUtil
   			 .createConfigurable(JournalArticleSearchConfiguration.class, properties);
    }

    private volatile JournalArticleSearchConfiguration journalArticleSearchConfiguration;

}
  • This will take all the configuration values from JournalArticleSearchConfiguration and set them in the preferences.
  • Our portlet is ready to deploy and use.

Journal Article Search

Creating a view for the journal article:

  • We have created a view url for the journal article, and it navigates to the view_content.jsp. It will display a journal article in full view.
  • Create the view_content.jsp file under src > main > resources > META-INF > resources.
<%@ include file="/init.jsp" %>

    <%
    portletDisplay.setShowBackIcon(true);
    
    SearchResultContentDisplayBuilder searchResultContentDisplayBuilder = new SearchResultContentDisplayBuilder();
    
    searchResultContentDisplayBuilder.setAssetEntryId(ParamUtil.getLong(request, "assetEntryId"));
    searchResultContentDisplayBuilder.setLocale(locale);
    searchResultContentDisplayBuilder.setPermissionChecker(permissionChecker);
    searchResultContentDisplayBuilder.setPortal(PortalUtil.getPortal());
    searchResultContentDisplayBuilder.setRenderRequest(renderRequest);
    searchResultContentDisplayBuilder.setRenderResponse(renderResponse);
    searchResultContentDisplayBuilder.setType(ParamUtil.getString(request, "type"));
    
    SearchResultContentDisplayContext searchResultContentDisplayContext = searchResultContentDisplayBuilder.build();
    %>
    
    <c:if test="<%= searchResultContentDisplayContext.isVisible() %>">
        <div class="mb-2">
            <h4 class="component-title">
                <span class="asset-title d-inline">
                    <%= HtmlUtil.escape(searchResultContentDisplayContext.getHeaderTitle()) %>
                </span>
    
                <c:if test="<%= searchResultContentDisplayContext.hasEditPermission() %>">
                    <span class="d-inline-flex">
                        <liferay-ui:icon
                            cssClass="visible-interaction"
                            icon="pencil"
                            label="<%= false %>"
                            markupView="lexicon"
                            message='<%= LanguageUtil.format(request, "edit-x-x", new Object[] {"hide-accessible", HtmlUtil.escape(searchResultContentDisplayContext.getIconEditTarget())}, false) %>'
                            method="get"
                            url="<%= searchResultContentDisplayContext.getIconURLString() %>"
                        />
                    </span>
                </c:if>
            </h4>
        </div>
    
        <liferay-asset:asset-display
            assetEntry="<%= searchResultContentDisplayContext.getAssetEntry() %>"
            assetRenderer="<%= searchResultContentDisplayContext.getAssetRenderer() %>"
            assetRendererFactory="<%= searchResultContentDisplayContext.getAssetRendererFactory() %>"
        />
    </c:if>
    
  • The SearchResultContentDisplayBuilder is used to set the fields required for the Asset Entry and is being built using the SearchResultContentDisplayContext.
  • SearchResultContentDisplayContext provides meta data for the SearchResultContentDisplayBuilder.
public class SearchResultContentDisplayContext implements Serializable {

    private AssetEntry assetEntry;
    private AssetRenderer<?> assetRenderer;
    private AssetRendererFactory<?> assetRendererFactory;
    private boolean hasEditPermission;
    private String headerTitle;
    private String iconEditTarget;
    private String iconURLString;
    private boolean visible;

    [getters…]
    [setters…]
    [hashCode()]
    [equals()]
    [toString()]

}
public class SearchResultContentDisplayBuilder {

    public SearchResultContentDisplayContext build() throws Exception {

   	 SearchResultContentDisplayContext searchResultContentDisplayContext = new SearchResultContentDisplayContext();

   	 AssetRendererFactory<?> assetRendererFactory = getAssetRendererFactoryByType(_type);

   	 searchResultContentDisplayContext.setAssetRendererFactory(assetRendererFactory);

   	 AssetEntry assetEntry;

   	 if (assetRendererFactory != null) {
   		 assetEntry = assetRendererFactory.getAssetEntry(_assetEntryId);
   	 } else {
   		 assetEntry = null;
   	 }

   	 searchResultContentDisplayContext.setAssetEntry(assetEntry);

   	 AssetRenderer<?> assetRenderer;

   	 if (assetEntry != null) {
   		 assetRenderer = assetEntry.getAssetRenderer();
   	 } else {
   		 assetRenderer = null;
   	 }

   	 searchResultContentDisplayContext.setAssetRenderer(assetRenderer);

   	 final boolean visible;

   	 if ((assetEntry != null) && (assetRenderer != null) && assetEntry.isVisible()
   			 && assetRenderer.hasViewPermission(_permissionChecker)) {

   		 visible = true;
   	 } else {
   		 visible = false;
   	 }

   	 searchResultContentDisplayContext.setVisible(visible);

   	 if (visible) {
   		 String title = assetRenderer.getTitle(_locale);

   		 searchResultContentDisplayContext.setHeaderTitle(title);

   		 boolean hasEditPermission = assetRenderer.hasEditPermission(_permissionChecker);

   		 searchResultContentDisplayContext.setHasEditPermission(hasEditPermission);

   		 if (hasEditPermission) {
   			 ThemeDisplay themeDisplay = (ThemeDisplay) _renderRequest
   					 .getAttribute(WebKeys.THEME_DISPLAY);

   			 searchResultContentDisplayContext.setIconEditTarget(title);

   			 PortletURL editPortletURL = assetRenderer.getURLEdit(
   					 _portal.getLiferayPortletRequest(_renderRequest),
   					 _portal.getLiferayPortletResponse(_renderResponse));

   			 editPortletURL.setParameter("redirect", themeDisplay.getURLCurrent());

   			 searchResultContentDisplayContext.setIconURLString(editPortletURL.toString());
   		 }
   	 }

   	 return searchResultContentDisplayContext;
    }

    [setters…]

    protected AssetRendererFactory<?> getAssetRendererFactoryByType(String type) {
   	 if (_assetRendererFactoryLookup != null) {
   		 return _assetRendererFactoryLookup.getAssetRendererFactoryByType(type);
   	 }

   	 return AssetRendererFactoryRegistryUtil.getAssetRendererFactoryByType(type);
    }

    private long _assetEntryId;
    private AssetRendererFactoryLookup _assetRendererFactoryLookup;
    private Locale _locale;
    private PermissionChecker _permissionChecker;
    private Portal _portal;
    private RenderRequest _renderRequest;
    private RenderResponse _renderResponse;
    private String _type;
}

Conclusion:

This implementation provides search as you type functionality with quick result summaries. Also, the searched keyword can appear at any position in the journal article title. We are a Liferay development company  that provides consulting, upgrade, and DXP portal development services.

Contact us

For Your Business Requirements

Text to Identify Refresh CAPTCHA
Background Image Close Button

2 - 4 October 2024

Hall: 10, Booth: #B8 Brussels, Belgium