Bootstrap a Query using the Fluent Search API
Introduction
Goal
Bootstrap a search query using the Fluent Query API in Bloomreach Experience Manager's delivery tier.
Background
The Fluent Search API in Bloomreach Experience Manager's delivery tier (HST) provides an intuitive way to construct search queries. This page explains how to bootstrap a query using HstQueryBuilder.
Create an HstQuery using HstQueryBuilder
The HstQueryBuilder can create an HstQuery instance from the current request context automatically.
In either HST components or JAX-RS Service Resources, you can apply the same practice:
-
In an HST component,
public class LatestItems extends BaseComponent { @Override public void doBeforeRender(HstRequest request, HstResponse response) { final HippoBean scope = RequestContextProvider.get().getSiteContentBaseBean(); final HstQuery hstQuery = HstQueryBuilder.create(scope) .ofTypes(BaseDocument.class) .build(); // ... } }
-
In a JAXRS Service Resource,
ProductPlainResource extending AbstractResource:
@Path("/products/") public class ProductPlainResource extends AbstractResource { @GET @Path("/{productType}/") public List<ProductRepresentation> getProductResources(....) { final HippoBean scope = RequestContextProvider.get().getSiteContentBaseBean(); final HstQuery hstQuery = HstQueryBuilder.create(scope) .ofTypes(BaseDocument.class) .build(); // ... } }
In cases where you don't have a HstRequestContext (for example for during some background process or for external application integration that does not involve an HTTP request), you can get hold of the HstQueryManager to build an HstQuery with it (through HstQueryBuilder#build(HstQueryManager) method:
Repository repo = HstServices.getComponentManager().getComponent(Repository.class.getName()); Credentials creds = HstServices.getComponentManager().getComponent(Credentials.class.getName() + ".default"); ContentBeansTool cbt = HstServices.getComponentManager().getComponent(ContentBeansTool.class.getName()); Session session = null; try { session = repo.login(creds); HstQueryManager queryManager = cbt.createQueryManager(session); final Node scope = JcrUtils.getNodeIfExists("/content/documents/myhippoproject", session); final HstQuery hstQuery = HstQueryBuilder.create(scope) .ofTypes("myhippoproject:news") .build(queryManager); // ... } finally { if (session != null) session.logout(); }
Scope the Query with HstQueryBuilder
An HstQuery can search content in one or multiple scopes. The scope(s) must be set through HstQueryBuilder#create(HippoBean ... scopeBeans) or HstQueryBuilder#create(Node ... scopeNodes)method. As you can see, it is a variable argument (varargs). So you can use a single scope or multiple scopes.
For example,
final HippoBean base = RequestContextProvider.get().getSiteContentBaseBean(); final HippoBean newsFolder = base.getBean("news"); final HippoBean eventsFolder = base.getBean("events"); // Set multiple search scopes including news/ and events/ folders. final HstQuery hstQuery = HstQueryBuilder.create(newsFolder, eventsFolder) .ofTypes(BaseDocument.class) .build();
or
final Session session = RequestContextProvider.get().getSession(); final Node newsFolderNode = JcrUtils.getNodeIfExists("/content/documents/myhippoproject/news", session); final Node eventsFolderNode = JcrUtils.getNodeIfExists("/content/documents/myhippoproject/events", session); // Set multiple search scopes including news/ and events/ folders. final HstQuery hstQuery = HstQueryBuilder.create(newsFolderNode, eventsFolderNode) .ofTypes(BaseDocument.class) .build();
Please note that HstQueryBuilder#create(...) requires at least one non-null argument. Otherwise, it will throw a RuntimeQueryException due to the illegal scope argument(s).
You can also exclude some scopes in the query execution through HstQueryBuilder#excludeScopes(HippoBean ... excludeScopeBeans) or HstQueryBuilder#excludeScopes(Node ... excludeScopeNodes). These methods also allow variable arguments (varargs). So you can set a single exclusion scope or multiple exclusion scopes.
For example,
final HippoBean base = RequestContextProvider.get().getSiteContentBaseBean(); final HippoBean eventsFolder = base.getBean("events"); // Set a search scope and exclude events/ folder. final HstQuery hstQuery = HstQueryBuilder.create(base) .excludeScopes(eventsFolder) .ofTypes(BaseDocument.class) .build();
or
final Session session = RequestContextProvider.get().getSession(); final Node baseNode = JcrUtils.getNodeIfExists("/content/documents/myhippoproject", session); final Node eventsFolderNode = JcrUtils.getNodeIfExists("/content/documents/myhippoproject/events", session); // Set a search scope and exclude events/ folder. final HstQuery hstQuery = HstQueryBuilder.create(baseNode) .excludeScopes(eventsFolderNode) .ofTypes(BaseDocument.class) .build();
Please note that if you add a bean or node in both scopes (through #create(...) and excludeScopes (through #excludeScopes(...)), then the excludeScopes will take the precedence over scopes. Any of excludeScopes will be excluded in the search query execution as a result.
Set a Limit and Offset
In general, it is always best to set a limit. If you do not specify a limit, the HST by default will set a limit of 1000. The best way is to set the limit equal to the number of items you want to show, for example pageSize. If you combine this with offset, you can easily always fetch the correct items.
For example, if pageSize = 10, and you want to show the 3rd page ( pageIndex = 3), use:
final HippoBean base = RequestContextProvider.get().getSiteContentBaseBean(); final HstQuery hstQuery = HstQueryBuilder.create(base) .ofTypes(BaseDocument.class) .limit(pageSize) .offset(pageSize * (pageIndex - 1)) .build();
Now, there is one more catch. If you have set a limit of, say, 10, then, HstQueryResult#getSize() will return at most 10. So, what if you need to know how many pages there are? For this, you can use getTotalSize(). This method returns the total number of hits.
Always set a limit, preferably to just the number you need to show
If you don't set a limit, HST sets a limit of 1000 for safety. This is already large, and might result in slower searches. For optimal performance, set a limit (and optionally an offset).
Sort Results with HstQueryBuilder
The HstQuery can "order by" / "sort by" multiple properties, ascending or descending. The sorting is done according to the order in which the properties to sort on are added. Sorting can only be done on properties (meta-data) which are directly stored on the document. Properties which are part of a Compound in a document can not be used for sorting.
For example:
Sorting / Ordering:
final HippoBean scope = RequestContextProvider.get().getSiteContentBaseBean(); final HstQuery hstQuery = HstQueryBuilder.create(scope) .ofTypes(BaseDocument.class) // first sort the results on the "example:date" descending (newest first) .orderByDescending("example:date") // if there are hits with the same "example:date", we then sort ascending by // "example:title" case insensitive .orderByAscendingCaseInsensitive("example:title") // if there are hits with the same date and title, we sort descending by // "hippostdpubwf:publicationDate" .orderByDescending("hippostdpubwf:publicationDate") .build() // ...
Please note that HstQueryBuilder#orderByDescending(String... fieldNames), HstQueryBuilder#orderByDescendingCaseInsensitive(String... fieldNames), HstQueryBuilder#orderByAscending(String... fieldNames) and HstQueryBuilder#orderByAscendingCaseInsensitive(String... fieldNames) are defined with variable arguments (varargs) as well. If you put multiple field names with those methods, it should be equivalent to multiple invocations of the method with each field name argument.
Sorting can only be done on properties directly on the documents
See Sorting search results in a query can only be done on direct properties of Documents.