Perform a full text search in AEM 6.5
Goal
Create a component that performs a full text search. The example will show only the first step of a search, that is the search bar. We won’t cover the “view all results” case/the result page and its pagination, but there will be some hints inside the code to achieve it. This is what we are going to develop:

Procedure
Let’s create our component as follows:
- The Dialog XML File:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Search"
sling:resourceType="cq/gui/components/authoring/dialog">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<tabs
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/tabs"
maximized="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<properties
jcr:primaryType="nt:unstructured"
jcr:title="Properties"
sling:resourceType="granite/ui/components/coral/foundation/container"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<columns
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<searchRoot
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser"
rootPath="/content/"
fieldLabel="Search Root Link"
name="./searchRoot"/>
<minChar
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Min char to start search"
name="./minChar"/>
<previewLimit
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Enter the title text for this search component"
fieldLabel="Preview Limit in search bar"
name="./previewLimit"/>
<resultSize
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Result size (for query)"
name="./resultSize"/>
<searchResultUrl
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser"
rootPath="/content/"
fieldLabel="Result Page Link"
name="./searchResultUrl"/>
</items>
</column>
</items>
</columns>
</items>
</properties>
</items>
</tabs>
</items>
</content>
</jcr:root>
And configure:
- Search Root: path used to hit the servlet and used by the query builder to search the results. (I used “/content/we-retail/us/en”, if you have the We.Retail project, you can try it as well)
- Min Char: minimum number of user digits to start the search (e.g. 3)
- Preview Limit: number of items shown under the search bar (e.g. 5)
- Result Size (not used – hint): Items per page if you configure a result page with all the result
- Search Result (not used – hint): URL of the result page
2. The HTML file:
<div class="container">
<template id="bc-search-results--hint__template">
<div class='bc-searchbar-preview--line' data-url='{{url}}' tabindex="0">{{{title}}}</div>
</template>
<div class="s01" id="searchbar" data-search-url="${properties.searchRoot}.search.json" data-search-min-char="${properties.minChar}"
data-search-items-per-page="${properties.resultSize}" data-search-preview-limit="${properties.previewLimit}" data-search-delay-ms="500"
data-view-all-search-results-url="${properties.searchResultUrl}">
<div class="flex">
<div class="input-field first-wrap" aria-label="Search">
<input placeholder="Search" id="search" aria-label="To search the site, type a search term" />
</div>
<div class="input-field third-wrap">
<button class="btn-search" type="button">Search</button>
</div>
</div>
<!-- Search results preview -->
<div class="bc-searchbar-preview d-none" tabindex="0">
<div class="bc-searchbar-viewall">
<span>VIEW ALL SEARCH RESULTS</span><!--/* NOT USED*/-->
</div>
</div>
</div>
<!-- Search bar -->
<sly data-sly-use.clientlib="/libs/granite/sightly/templates/clientlib.html">
<sly data-sly-call="${clientlib.all @ categories='clientlib.Lab2020-search'}"/>
</sly>
</div>
The front end code is really basic. It’s just to show you a preview of the component. You will adapt it based on your project and your requirements. Indeed I won’t share the CSS. As you have seen, I used Handlebars to render the list of results.
3. Create your clientlibs and in particular the JS file (as I said, you will need Handlebars and jQuery as well to have your component working):
(function (window, document, $) {
function undefToBlank(val){
return typeof val !== "undefined"?val:"";
}
function simpleLocationToHash() {
var location = {};
window.location.search.replace(/\?/i, '').split('&').map(function (o) {
var sp = o.split('=');
location[sp[0]] = sp[1];
});
return location;
}
'use strict'; // Require dependencies here
var $searchBar = $("#searchbar");
var perPage = $searchBar.attr('data-search-items-per-page');
var resultsPageUrl = $searchBar.attr("data-view-all-search-results-url");
var searchUrl = $searchBar.attr("data-search-url");
var previewLimit = $searchBar.attr("data-search-preview-limit");
var minChar = $searchBar.attr("data-search-min-char");
var searchDelay = $searchBar.attr("data-search-delay-ms");
var searchVal = $searchBar.find('input').val();
var kw = '';
var query = simpleLocationToHash();
var offset = typeof query.resultsOffset != 'undefined' ? parseInt(query.resultsOffset) : 0; //var $searchBar = $('.bc-searchbar');
var $searchPreview = $searchBar.find('.bc-searchbar-preview');
var $searchInput = $searchBar.find("input");
var queryText = decodeURI(query.fulltext);
var KEY_ENTER = 13,
KEY_ESC = 27;
// not used - to go to the result page
var $viewAllSearchResults = $searchBar.find('.bc-searchbar-viewall');
var hintTemplate = Handlebars.compile(document.getElementById("bc-search-results--hint__template").innerHTML);
function populateSearchResultsPreview(searchResults, kw) {
if (!Array.isArray(searchResults.results) || searchResults.results.length <= 0) return $searchPreview.hide();
var $searchPreviewLine = $searchBar.find('.bc-searchbar-preview--line');
$searchPreviewLine.remove();
$searchPreview.prepend(searchResults.results.map(function (o, i) {
if (i < previewLimit) {
o.title = "<span class=''>" + o.title + "</span>";
return hintTemplate(o);
}
}));
$searchPreviewLine = $searchBar.find('.bc-searchbar-preview--line'); // navigate to a certain page upon clicking the search results
$searchPreviewLine.on("click", function () {
window.location = $(this).attr("data-url");
});
$searchPreview.show();
} // we wait for 100ms before we issue a search, so that the user gets time to finish typing
var delayedSearch;
$searchInput.on("keyup", function (e) {
if (delayedSearch) {
clearTimeout(delayedSearch);
delayedSearch = null;
}
kw = $(this).val();
if (kw.length < minChar) return populateSearchResultsPreview([]);
var url = searchUrl + createUrl(0, undefined, kw);
delayedSearch = setTimeout(function () {
var response = $.ajax({
url: url,
dataType: 'JSON',
type: 'GET',
headers: {
"Content-Type": "application/json"
}
});
response.done(function(data){
populateSearchResultsPreview(data, kw)
});
}, searchDelay);
}).on("keydown", function (e) {
if (e.which === KEY_ENTER) {
gotoResults(); //could be used
}
});
/* view all results */
// initialize the module here
var urlTemplate = Handlebars.compile("?fulltext={{fulltext}}&resultsOffset={{resultsOffset}}");
function createUrl(page, resultsPage, url) {
var ret = typeof resultsPage === 'undefined' ? urlTemplate({
fulltext: typeof url === 'undefined' ? undefToBlank(query.fulltext) : url,
resultsOffset: page * perPage
}) : urlMaker.mergeParams({
fulltext: typeof url === 'undefined' ? undefToBlank(query.fulltext) : url,
resultsOffset: page * perPage
}).serialize(true);
return ret;
}
//not used
function gotoResults() {
var searchVal = $searchBar.find('input').val();
window.location = resultsPageUrl + urlTemplate({
fulltext: encodeURI(searchVal),
resultsOffset: 0
});
}
})(window, document, jQuery);
4. Create the Servlet:
package com.adobe.training.core.servlets;
import com.day.cq.search.PredicateConverter;
import com.day.cq.search.PredicateGroup;
import com.day.cq.search.Query;
import com.day.cq.search.QueryBuilder;
import com.day.cq.search.result.Hit;
import com.day.cq.search.result.SearchResult;
import com.day.cq.wcm.api.NameConstants;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.Servlet;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author riccardo teruzzi
*/
@Component(
service = Servlet.class,
property = {
"sling.servlet.selectors=" + SearchResultServlet.DEFAULT_SELECTOR,
"sling.servlet.resourceTypes=cq:Page",
"sling.servlet.extensions=json",
"sling.servlet.methods=GET"
})
public class SearchResultServlet extends SlingAllMethodsServlet {
public static final String PREDICATE_FULLTEXT = "fulltext";
public static final String PREDICATE_TYPE = "type";
public static final String PREDICATE_PATH = "path";
public static final String UTF8 = "UTF-8";
public static final String APPLICATION_JSON = "application/json";
protected static final String DEFAULT_SELECTOR = "search";
protected static final String PARAM_FULLTEXT = "fulltext";
private static final String PARAM_RESULTS_OFFSET = "resultsOffset";
private static final Logger LOGGER = LoggerFactory.getLogger(SearchResultServlet.class);
@Reference
private QueryBuilder queryBuilder;
@Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws IOException {
try {
JSONObject jsonObject = getResults(request);
response.setContentType(APPLICATION_JSON);
response.setCharacterEncoding(UTF8);
response.getWriter().print(jsonObject);
} catch (IOException | JSONException e) {
LOGGER.error(e.getMessage());
}
}
private JSONObject getResults(SlingHttpServletRequest request) throws JSONException {
int searchTermMinimumLength = 3;
int resultsSize = 10; //For pagination
String searchRoot = request.getResource().getPath();
JSONObject response = new JSONObject();
JSONArray results = new JSONArray();
String fulltext = request.getParameter(PARAM_FULLTEXT);
if (fulltext == null || fulltext.length() < searchTermMinimumLength) {
return response;
}
long resultsOffset = 0; //For pagination
if (request.getParameter(PARAM_RESULTS_OFFSET) != null) {
resultsOffset = Long.parseLong(request.getParameter(PARAM_RESULTS_OFFSET));
}
Map<String, String> predicatesMap = new HashMap<>();
predicatesMap.put(PREDICATE_FULLTEXT, fulltext);
predicatesMap.put(PREDICATE_PATH, searchRoot);
predicatesMap.put(PREDICATE_TYPE, NameConstants.NT_PAGE);
PredicateGroup predicates = PredicateConverter.createPredicates(predicatesMap);
ResourceResolver resourceResolver = request.getResource().getResourceResolver();
Query query = queryBuilder.createQuery(predicates, resourceResolver.adaptTo(Session.class));
query.setHitsPerPage(resultsSize);
if (resultsOffset != 0) {
query.setStart(resultsOffset);
}
SearchResult searchResult = query.getResult();
long totalMatches = searchResult.getTotalMatches();
PageManager pageManager = resourceResolver.adaptTo(PageManager.class);
List<Hit> hits = searchResult.getHits();
if (hits != null && pageManager != null) {
for (Hit hit : hits) {
try {
JSONObject result = new JSONObject();
Resource hitRes = hit.getResource();
Page page = pageManager.getContainingPage(hitRes);
if (page != null) {
result.put("title", page.getTitle());
result.put("url", resourceResolver.map(page.getPath()) + ".html");
results.put(result);
}
} catch (RepositoryException e) {
LOGGER.error("Unable to retrieve search results for query.", e);
}
}
}
response.put("totalMatches", totalMatches);
response.put("results", results);
return response;
}
}
Everything should be clear, I hope. The resultSize and the offSet could be used for the second step of your search (the result page and the pagination).
As I said, you will probably adapt a lot of the HTML/JS based on your project, but I think this could be a good start if you need to implement a full text search .
Cheers! 🍻