Customizing Liferay search results with a JSP hook & a content template

I'd like to point out at the outset that the content of this post may be mundane for the seasoned Liferay developer. What I really want to demonstrate is the ease with which the problem (defined below) can be solved quickly and easily through use of the portal's architectural mechanisms and APIs.

The Problem

Customize the out-of-the-box Search portlet's presentation of results to show each row as a course card; note that our searches are restricted to course content only.

A duly smudged-out page specification is shown below.

 

Each course card in the above is one search resultWhen I first came across this requirement, I hopped onto my local portal instance and dropped the Liferay out-of-the-box Search portlet onto a page. Here's what you would see (taken from the Liferay Developer Network).

 

Two major gaps that needed bridging jumped out at me:

  • The search criteria had to be moved to the right-hand-side.
  • The list of results was row-wise, but had to be transformed into a course card view, with each course item corresponding to a row.

A quick examination of the portlet configuration did not seem helpful. So I made the foregone conclusion that we would need to write a custom search portlet. We had the HTML from the designers, we had the necessary APIs at our disposal. We even had the codebase of the search portlet as a reference for the faceted search implementation as well as the search criteria presentation. All set to get started.

But our estimates were far less than ideal. We needed this thing implemented fast!

Hooked on a Solution

And then I read up on hooks, in particular the JSP hook. Using a JSP hook, I could alter any code in the actively deployed search portlet, running here: [liferay]/webapps/ROOT/html/portlet/search

So I created the hook and added search.jsp to my META-INF (more on hooks here). After about 30 minutes of studying search.jsp and one simple tweak, the search criteria was on the right-hand-side. Great! 

So, the only challenge left now was the resultset presentation.

A Template called Course Card

I had to disengage from the search portlet and focus on the content architecture of the site. An increased familiary with structures and templates ensued. Among these, I had a course structure and a course template.

Pretty soon, I found myself coding up an Application Display Template (ADT) for use with an Asset Publisher portlet instance. The goal was to show courses that matched certain critera. In this template, I iterate through the assets (essentially course content items) and for each asset, I make xpath calls to acquire the various attributes of the course into a bunch of variables. And then, using an HTML fragment I was handed by the designers, I replaced the data placeholders with the corresponding variables. I called the ADT Course Card. It looked something like the below.

The ADT

<div class="row-fluid">
<div class="span12">
<h2 class="heading-secondary">Courses</h2>
</div>
</div>

#set ($rowcounter = 0)
#if (!$entries.isEmpty())
    <div class="row-fluid">
#foreach ($curEntry in $entries)
   #set ($rowcounter = $rowcounter + 1)
   #set( $renderer = $curEntry.getAssetRenderer() )
   #set( $link = $renderer.getURLViewInContext($renderRequest, $renderResponse, ''))
   #set( $journalArticle = $renderer.getArticle() )
   #set ($articlePrimKey = $journalArticle.getResourcePrimKey())
   #set ($catLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryLocalService"))
   #set ($catPropertyLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryPropertyLocalService"))
   #set ($articleCats = $catLocalService.getCategories("com.liferay.portlet.journal.model.JournalArticle", $articlePrimKey))
   #set ($qualificationLevelAttribute = $journalArticle.getExpandoBridge().getAttribute("qualificationLevel"))
   #set ($studyModeAttribute = $journalArticle.getExpandoBridge().getAttribute("studyMode"))
   #set ($locationAttribute = $journalArticle.getExpandoBridge().getAttribute("location"))
   #set ($primaryFaculty = $journalArticle.getExpandoBridge().getAttribute("primaryFaculty"))
   #set( $document = $saxReaderUtil.read($journalArticle.getContent()) )
   #set( $rootElement = $document.getRootElement() ) 

   ### get any content fields you need using XPath
   #set( $courseNameSelector =  $saxReaderUtil.createXPath("dynamic-element[@name='txtCourseName']") )
   #set( $courseName =  $courseNameSelector.selectSingleNode($rootElement).getStringValue() )

   ### get any custom fields as below
   #set ($durationMax = $journalArticle.getExpandoBridge().getAttribute("durationMax"))
   #set ($durationMin = $journalArticle.getExpandoBridge().getAttribute("durationMin"))

####### skipping a bunch of irrelevant code that extracts several other fields and custom fields into various variables. ########

################# Display the course data in a card format ###########################
<div class="span4 block-vspace">
<div class="card-container $cardCss.value">
<a href="$link" class="card">
      <div class="card-content-container">
       <p class="card-category-title">$faculty</p>
       <div class="card-content">
      <h4 class="card-title fade in">$courseName</h4>
      <p><strong>$qualificationLevels</strong></p>
      <p class="small">NQF Level $nqfLevel, SAQA No. $saqaNumber</p>
      <p>$curEntry.summary</p>
      <hr>
      <p class="small">Offered as: <strong>$studyModes</strong></p>
      <p class="small">Minimum duration: <strong>$durationMin</strong><br>
      Maximum duration: <strong>$durationMax</strong></p>
      <p class="small"><strong>$locations</strong></p>
     </div>
     <hr class="card-horizontal-rule">
     <button class="card-btn-link btn btn-link btn-block"><strong>View course <i class="fa fa-chevron-right fa-sm"></i></strong></button>
   </div>
    </a>
  </div>
</div>
    #if ($rowcounter % 3 == 0)
       </div>
        <div class="row-fluid"> &nbsp; </div>
        <div class="row-fluid">
    #end
#end
</div> 
#end

Hmm. Now, that was what I wanted each of my search results to look like. The difference was I wouldn't be working within the Asset Publisher's query framework, but rather within the search portlet's.

I already had web content items (aka JournalArticles) for the courses. And I already had a Course Detail template (a web content template, not to be confused with ADT) that showed ALL of the course details in one presentation. I could now craft a new Course Card content template that shadowed my Course Card ADT. You can see how this was a bit backwards in that the ADT was more complex to code up. The content template to display the course card was more straightforward because I had easy acces to the fields in the course type structure. This is what my Course Card content template looked like. Notice the similarity barring the XPATH syntax in the ADT to retrieve content fields.

The content template

############### get vocabulary ID's ####################
#set ($vocabularyLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetVocabularyLocalService"))
#set ($vocabularies = $vocabularyLocalService.getGroupVocabularies($getterUtil.getLong($groupId)))

### we are interested in 2 specific vocabularies - Schools and StudyLevels
#foreach($voc in $vocabularies)
    #if ($voc.name == "Faculty")
        #set ($facultyVocabularyId = $voc.vocabularyId)
    #end
#end

#set ($journalArticleLocalService = $serviceLocator.findService("com.liferay.portlet.journal.service.JournalArticleLocalService"))
#set ($journalArticle = $journalArticleLocalService.getArticle($getterUtil.getLong($groupId), $reserved-article-id.data))
#set ($articlePrimKey = $journalArticle.getResourcePrimKey())
#set ($catLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryLocalService"))
#set ($catPropertyLocalService = $serviceLocator.findService("com.liferay.portlet.asset.service.AssetCategoryPropertyLocalService"))
#set ($articleCats = $catLocalService.getCategories("com.liferay.portlet.journal.model.JournalArticle", $articlePrimKey))
#set ($qualificationLevelAttribute = $journalArticle.getExpandoBridge().getAttribute("qualificationLevel"))
#set ($studyModeAttribute = $journalArticle.getExpandoBridge().getAttribute("studyMode"))
#set ($locationAttribute = $journalArticle.getExpandoBridge().getAttribute("location"))
#set ($primaryFaculty = $journalArticle.getExpandoBridge().getAttribute("primaryFaculty"))
#set ($durationMin = $journalArticle.getExpandoBridge().getAttribute("durationMin"))
#set ($durationMax = $journalArticle.getExpandoBridge().getAttribute("durationMax"))

####### skipping a bunch of irrelevant code that extracts several other fields and custom fields into various variables. ########

################# Display the course card data ###########################

<div class="span4 block-vspace">
<div class="card-container $cardCss.value">
<a href="/-/$reserved-article-url-title.data" class="card">
      <div class="card-content-container">
       <p class="card-category-title">$faculty</p>
       <div class="card-content">
      <h4 class="card-title fade in">$txtCourseName.getData()</h4>
      <p><strong>$qualificationLevels</strong></p>
      <p class="small">NQF Level $txtNqfLevel.getData(), SAQA No. $txtSaqaNumber.getData()</p>
      <p>Designed to offer well-balanced exposure to the knowledge, skills and attitudes required to operate effectively in a general business environment.  This qualification offers three majors, namely, Business Management, Marketing and Human Resources.</p>
      <hr>
      <p class="small">Offered as: <strong>$studyModes</strong></p>
      <p class="small">Minimum duration: <strong>$durationMin</strong><br>
      Maximum duration: <strong>$durationMax</strong></p>
      <p class="small"><strong>$locations</strong></p>
     </div>
     <hr class="card-horizontal-rule">
   <button class="card-btn-link btn btn-link btn-block" onclick="window.location.href='/-/$reserved-article-url-title.data;'"><strong>View course <i class="icon icon-chevron-right fa-sm"></i></strong></button>
   </div>
    </a>
  </div>
</div>

With that in place, we were left with the task of using it, i.e retrieving the content (easy) and rendering it using this template from within the search portlet.

Back to the Search JSP hook, and Making it all Work

The only file I needed to touch was main_search.jspf. These were the tweaks.

  1. Fixed the search container delta to 9 so each page had no more than 9 results, after which default pagination would kick in.
  2. Added a scriptlet into the row container to load the JournalArticle for each course in the iteration.
  3. Retrieved the content string after transforming the course JournalArticle using the Course Card content template. The API method that does this for you is JournalArticleLocalServiceUtil.getArticleContent.

  4. Write out the content to the response.
 
These tweaks are highlighted in the below excerpt from main_search.jspf.
 
<%
SearchContainer mainSearchSearchContainer = new SearchContainer(renderRequest, null, null, SearchContainer.DEFAULT_CUR_PARAM, 9, portletURL, null, null);

SearchContext searchContext = SearchContextFactory.getInstance(request);

mainSearchSearchContainer.setEmptyResultsMessage(LanguageUtil.format(pageContext, "no-results-were-found-that-matched-the-keywords-x", "<strong>" + HtmlUtil.escape(searchContext.getKeywords()) + "</strong>"));
...
...
...
<aui:col cssClass="result" first="<%= !showMenu %>" span="10">
<%@ include file="/html/portlet/search/main_search_suggest.jspf" %>

<liferay-ui:search-container
searchContainer="<%= mainSearchSearchContainer %>"
total="<%= hits.getLength() %>"
>

<liferay-ui:search-container-results
results="<%= hits.toList() %>"
/>

<liferay-ui:search-container-row
className="com.liferay.portal.kernel.search.Document"
escapedModel="<%= false %>"
keyProperty="UID"
modelVar="document"
stringKey="<%= true %>"
>

</liferay-ui:search-container-row>
<div class="row-fluid">
           <c:forEach items="${hitsVar}" var="doc"  varStatus="counter">
               <%
 int counter = 0;
 Document docItem = (Document) pageContext.getAttribute("doc");
 String idString = docItem.getField(com.liferay.portal.kernel.search.Field.ROOT_ENTRY_CLASS_PK).getValues()[0];
 long articleId = new Long(idString).longValue();
 com.liferay.portlet.journal.model.JournalArticle article = com.liferay.portlet.journal.service.JournalArticleLocalServiceUtil.getLatestArticle(articleId);
 String ddmTemplateKey = com.liferay.portal.kernel.util.PropsUtil.get("search.coursecard.templatekey");
 String content = null;
 try{
 content = com.liferay.portlet.journal.service.JournalArticleLocalServiceUtil.getArticleContent(article, ddmTemplateKey, "view", "en_US", themeDisplay);
 }catch(com.liferay.portlet.journal.NoSuchArticleException e){
 System.out.println("---> no article found with id " + idString);
 }
               %>
 <%= content %>
 <c:if test="${counter.count mod 3 == 0}">
 </div>
 <div class="row-fluid"> &nbsp; </div>
 <div class="row-fluid">
 </c:if>
           </c:forEach>
       </div>
<br style="clear:both"/>
<liferay-ui:search-iterator type="more" />
...
...
</liferay-ui:search-container>
</aui:col>

That's it!

In Conclusion

The bulk of the work done here was to:

  • code a content template for each result (corresponding to a row in the search results)
  • modify main_search.jspf (through a hook) to retrieve the content markup from the JournalArticle, apply the new content template to it and include the result in the response

What you need to know

This is a quick (very quick) solution and should be good for the most part. But you need to know that Liferay 7 will not support JSP hooks. There is talk of (hopefully) a Migration Assistant that will transform the hook into its own OSGI module. However, my impression is that we should plan ahead and design/implement a custom portlet that incorporates the functionality of our hook(s). That way, when an upgrade to Liferay 7 knocks on our door, we'll actually be ready to invite it in.

Hope this was useful.

Blogs
Hi All,

I am facing the below mentioned issue related to the Search hook. Please provide some solution for that

I have uploaded the excel file containing data in the Document and Media portlet section . And after that I have deployed my search hook which is reading the data from excel and displaying upon search operation performed. The issue I am facing is what ever search result is displayed in the search result page i.e page.jsp , the same data is now displaying in every section of the control panel for eg. Web Content, wiki , sites etc... Please refer the screen shots attached for the issue faced.


The solution i am expecting is it should display searched data only in the search result page not any where else like it is displaying in web content , wiki.

Please respond .... Its urgent.
Thanks in Advance

Avinash
Attachment

Attachment
Hi Avinash. First off, you should post your question on the forums to get a much larger readership.
https://web.liferay.com/community/forums
If you have not done so yet, please read my note below before you do.

I don't see any screenshots here. In any case, from what you describe, it might be of more interest to see the specific changes you made to the search hook, down to the detail of what you added to the page.jsp and other related files. This will really help forum users point out your issue.

It certainly looks like the change you made is getting applied to the larger markup surrounding the search portlet. Possibilities that come to mind are: using an ID or name that is not unique to the search portlet, broken markup (such as a missing closing tag for a DIV), etc.

Hope you figure it out.
Sorry, I'm new in liferay, I can't understand how associate the ADT, content template and hook without structure.
How can I show this example? thank you
Hi Armando. Thanks for the comment.

First off, your site would need some sort of content. That content may (or may not) have a specific structure. In my case, the structure is called "Course". I had defined a template for that structure, also called "Course".

The hook I refer to was specific to the search portlet. Instead of coding a duplicate front end for each course in the hook's JSP, it made much more sense to simply load the course content (i.e. journal article XML) and then transform the content xml to XHTML using the template associated with the course structure. So the hook code makes a call to JournalArticleLocateServiceUtil.getArticleContent() which takes the template key as one of its parameters.

Next, the ADT. The ADT is not used by the hook, rather it is picked as the Display Template from the Display Settings tab of an Asset Publisher portlet. This may not be relevant to your site, but it was for mine. All that course content I was showing through my search portlet also had to be shown for each school using an Asset Publisher portlet instance. So I coded an ADT that could be shared on each School's landing page. All we did was configure the Asset Publisher instance to bring back content having a category that matched the corresponding school name. E.g. Business School.

Hope that helps.
Javeed, thank you for the answer, Do you have the project in github?
or you can show the structure? tank you!!
I'm not sure what you're after here. The structure I used is pretty involved and specific to our business requirements. I pasted the XML for it here, you can paste it into the Source view of a new structure to see what it looks like.


<root available-locales="en_US" default-locale="en_US">
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtCourseName" readOnly="false" repeatable="false" required="true" showLabel="true" type="text" width="small">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Course Name]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="html" fieldNamespace="ddm" indexType="keyword" localizable="true" name="htmlCourseLead" readOnly="false" repeatable="false" required="false" showLabel="true" type="ddm-text-html" width="small">
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtTabTitle" readOnly="false" repeatable="true" required="false" showLabel="true" type="text" width="large">
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtTabId" readOnly="false" repeatable="false" required="false" showLabel="true" type="text" width="small">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Tab Identifier]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="html" fieldNamespace="ddm" indexType="keyword" localizable="true" name="htmlTabContent" readOnly="false" repeatable="false" required="false" showLabel="true" type="ddm-text-html" width="large">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Course Content]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Course Tab Title]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Course Lead Text]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtFeesFundingUrl" readOnly="false" repeatable="false" required="false" showLabel="true" type="text" width="small">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Fees & Funding URL for this course]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="boolean" indexType="keyword" localizable="true" name="blnShowFeesFunding" readOnly="false" repeatable="false" required="false" showLabel="true" type="checkbox" width="">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Show 'Fees & Funding' tab]]>
</entry>
<entry name="predefinedValue">
<![CDATA[false]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="boolean" indexType="keyword" localizable="true" name="blnShowHowToApply" readOnly="false" repeatable="false" required="false" showLabel="true" type="checkbox" width="">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Show 'How to Apply' Tab]]>
</entry>
<entry name="predefinedValue">
<![CDATA[false]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtNqfLevel" readOnly="false" repeatable="false" required="false" showLabel="true" type="text" width="small">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[NQF Level]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtSaqaNumber" readOnly="false" repeatable="false" required="false" showLabel="true" type="text" width="small">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[SAQA Number]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="document-library" fieldNamespace="ddm" indexType="keyword" localizable="true" name="docBanner" readOnly="false" repeatable="false" required="false" showLabel="true" type="ddm-documentlibrary" width="">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Course Banner]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtActivePageBreadcrumb" readOnly="false" repeatable="false" required="false" showLabel="true" type="text" width="small">
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtBreadcrumbTitle" readOnly="false" repeatable="true" required="false" showLabel="true" type="text" width="small">
<dynamic-element dataType="link-to-page" fieldNamespace="ddm" indexType="keyword" localizable="true" name="linkPageBreadcrumb" readOnly="false" repeatable="false" required="false" showLabel="true" type="ddm-link-to-page" width="small">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Breadcrumb Link to Page]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Breadcrumb Title]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Active Page Breadcrumb Title]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtTabularContentHeading" readOnly="false" repeatable="false" required="false" showLabel="true" type="text" width="small">
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtLabel" readOnly="false" repeatable="true" required="false" showLabel="true" type="text" width="small">
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtValue" readOnly="false" repeatable="false" required="false" showLabel="true" type="text" width="small">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Value]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Label]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Tabular Content Heading]]>
</entry>
<entry name="predefinedValue">
<![CDATA[Key dates]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtDownloadLinksHeading" readOnly="false" repeatable="false" required="false" showLabel="true" type="text" width="small">
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtDownloadableLabel" readOnly="false" repeatable="true" required="false" showLabel="true" type="text" width="small">
<dynamic-element dataType="document-library" fieldNamespace="ddm" indexType="keyword" localizable="true" name="docDownloadable" readOnly="false" repeatable="false" required="false" showLabel="true" type="ddm-documentlibrary" width="">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Documents and Media]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="string" indexType="keyword" localizable="true" name="txtExternalLink" readOnly="false" repeatable="false" required="false" showLabel="true" type="text" width="small">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[External Link]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[If an external link is specified, the Documents and Media value is ignored]]>
</entry>
</meta-data>
</dynamic-element>
<dynamic-element dataType="boolean" indexType="keyword" localizable="true" name="blnNewWindow" readOnly="false" repeatable="false" required="false" showLabel="true" type="checkbox" width="">
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Open in new window]]>
</entry>
<entry name="predefinedValue">
<![CDATA[true]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Downloadable Document]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[]]>
</entry>
</meta-data>
</dynamic-element>
<meta-data locale="en_US">
<entry name="label">
<![CDATA[Download Links Heading]]>
</entry>
<entry name="predefinedValue">
<![CDATA[]]>
</entry>
<entry name="tip">
<![CDATA[Leave this blank if you don't need a heading]]>
</entry>
</meta-data>
</dynamic-element>
</root>
thank you for your answer, I need something like this:
https://s17.postimg.org/qb7gjwngf/busqueda_Truqueada.png
Basically I try to add styles to the results in a search
Ah! So, then what you need is to write a JSP hook to replace the code running under
/webapps/ROOT/html/portlet/search/
You may have to tweak one or more of the jspf fragments in that folder. As a quick experiment, try tweaking those files to see how your search results get altered.

Lifeary's documentation on hooks is here:
https://www.liferay.com/documentation/liferay-portal/6.0/development/-/ai/overriding-a-jsp?_ga=1.75370449.2126301223.1479500233

Keep in mind that my approach to locating the display logic for each search result in a content template is just one idea. I did that because each search result is guaranteed to be a content item of type course (for my business requirements). if you're getting various types of content back, then you need to think afresh on how best to achieve that separation. In essence, however, it all comes down to the code you put in your JSP hook to replace the existing search results functionality.

Hope that helps. And good luck!