Home > magnolia-cms, resources > Resources: The Code for Faceted Search Navigation

Resources: The Code for Faceted Search Navigation

In my last post, I discussed the data-capture portion of our new Resources page on our corporate site. Now I’d like to get into the code for the page to show you how things are done.

The main template for the page is pretty much a standard STK template:

Template Definition

Template Definition

There are a couple of interesting things going on in the mainArea and extrasArea, though. Have a look:

Auto-Generated Paragraphs

Auto-Generated Paragraphs

You can see that, in both sections, we’re adding an automatically-generated paragraph. The main area adds the paragraph that’s responsible for rendering search results, and the extras area automatically includes the search navigation. Because we continue to rely on the normal structure of STK pages, we don’t have to do a lot more to build the page structure. The one remaining important piece we do need to put in place, however, is custom CSS and Javascript for the page template. (We’re using the excellent PrettyPhoto jQuery lightbox script and need to include its supporting files.) Here’s how we include the extra files in the page template:

Including CSS and Javascript files

Including CSS and Javascript files

As you can see, we can easily specify whether or not to use Far Future Caching (which improves caching performance significantly) and what media the CSS should apply to.

Now that our page template is built up, let’s take a look at the navigation paragraph. This paragraph’s job is to present a list of all of the different categories for which we have assets, and to allow site visitors to choose from that list. It’s dynamic, in that if a category is already selected, we only display the categories that have items with both that category and the selected category. Here’s the code from the Freemarker template:

        [#list model.categories as category]
            <li class="category">${category.displayName!category}</li>
            [#if category?children?size > 0]
                    [#list category?children?sort as subCategory]
                        [#if model.isCategoryActive( subCategory )]
                            <li class="selected">
                                ${subCategory} (${model.getCategoryCount( subCategory )})
                                <a href="${model.getClearCategoryLink( subCategory )}">${unselectCharacters}</a>
                            [#if model.getCategoryCount( subCategory ) > 0]
                                    <a href="${model.getCategoryLink( subCategory )}">[#if subCategory.displayName?has_content]${subCategory.displayName}[#else]${subCategory}[/#if] (${model.getCategoryCount( subCategory )})</a>

You should be able to see that the Freemarker template gets a list of categories from the model object and iterates through them. It then iterates through the subcategories, displaying each as a link (if it’s not yet selected), or as plain text with a cancel option (if it’s already selected). But obviously, the model class is doing a fair bit of work under the covers to generate the data that Freemarker uses. Let’s take a look at some of the code there. The first thing that the template relies on the list of categories:

    public List<Content> getCategories() throws AccessDeniedException, PathNotFoundException, RepositoryException {
        if ( this.categories == null ) {
            Content getCategoriesFromNode = content;
            //The model is called form the searchResult paragraph, so the categories are on the auto generated navigation paragraph in extras.
            if(!content.hasNodeData("categories")) {
                //Getting the searchNavigation paragraph's node in extras
                getCategoriesFromNode = getRoot().getContent().getContent("extras").getContent("extras1").getContent("extrasItem0");
            this.categories = CategoryUtil.getCategories( getCategoriesFromNode, "categories" );
        return this.categories;

We use lazy initialization for all of the model’s properties, which keeps us from having to compute things unnecessarily or too often. We then use the CategoryUtil class (which is part of the Categorization module) to get the list of Categories that have been specified in the search configuration dialog and return that list.

Next up, we want to determine if a particular subcategory is active — that is, whether the site visitor has already chosen to search on that category. Here’s the code for that:

public boolean isCategoryActive( Content category ) {
    String existingCategories = FCSSelectorUtil.getSelectorValue( CATEGORY_PARAMETER );
    if ( !StringUtils.isEmpty( existingCategories ) ) {
        List<String> categoriesList = Arrays.asList( StringUtils.split( existingCategories, CATEGORY_DELIMITER ) );
        return categoriesList.contains( getAbbreviatedPath( category ) );
    return false;

The FCKSelectorUtil class is what we use to extract search parameters from the current URL string. It’s a bit outside the scope of this article, but we built it to allow us to use Selectors instead of query strings to determine what results to show. (Using Selectors allows us to cache the results, while using query strings turns off caching by default in Magnolia.)

Next up, we need to count the number of results available for a given category. This is pretty database-intensive, as we have to count the number of results for videos, slideshows, DMS documents, and webpages, and then add them all together. Note that we have here used Openmind’s very nice Criteria API to simplify the queries as much as possible:

    public int getCategoryCount( Content category ) {
        Criteria websiteCriteria = getWebsiteCriteria();
        websiteCriteria = addCurrentRestrictions( websiteCriteria );
        websiteCriteria.add( Restrictions.contains( "@categories", category.getUUID() ) );
        AdvancedResult websiteResult = websiteCriteria.execute();

        Criteria dmsCriteria = getDmsCriteria();
        dmsCriteria = addCurrentRestrictions( dmsCriteria );
        dmsCriteria.add( Restrictions.contains( "@categories", category.getUUID() ) );
        AdvancedResult dmsResult = dmsCriteria.execute();

        Criteria videoCriteria = getVideoCriteria();
        videoCriteria = addCurrentRestrictions( videoCriteria );
        videoCriteria.add( Restrictions.contains( "@categories", category.getUUID() ) );
        AdvancedResult videoResult = videoCriteria.execute();

        Criteria slideshowCriteria = getSlideshowCriteria();
        slideshowCriteria = addCurrentRestrictions( slideshowCriteria );
        slideshowCriteria.add( Restrictions.contains( "@categories", category.getUUID() ) );
        AdvancedResult slideshowResult = slideshowCriteria.execute();

        return websiteResult.getTotalSize() + dmsResult.getTotalSize() + videoResult.getTotalSize() + slideshowResult.getTotalSize();

This method relies on a couple of supporting utility methods. One set is responsible for setting up the basics of a certain type of query. For example:

private Criteria getDmsCriteria() {
    Criteria criteria = JCRCriteriaFactory.createCriteria()
    .setBasePath( getSearchRoot() )
    .add( Restrictions.eq( Criterion.JCR_PRIMARYTYPE, "mgnl:contentNode" ) );
    return criteria;

And the addCurrentRestrictions() method modifies the query to only return results from the currently-selected categories:

private Criteria addCurrentRestrictions(Criteria criteria) {
    String categoriesString = FCSSelectorUtil.getSelectorValue( CATEGORY_PARAMETER );
    if ( !StringUtils.isEmpty( categoriesString ) ) {
        HierarchyManager hm = MgnlContext.getHierarchyManager("data");
        for ( String category : StringUtils.split( categoriesString, CATEGORY_DELIMITER ) ) {
            Content categoryContent;
            try {
                categoryContent = hm.getContent( getFullPath( category ) );
                criteria.add( Restrictions.contains( "@categories", categoryContent.getUUID() ) );
            } catch (RepositoryException e) {
                log.warn( "Unable to locate category: " + category );
                criteria.add( Restrictions.contains( "@categories", "NO_MATCH" ) );
    return criteria;

There are some more details, but that should give you a basic understanding of how the navigation paragraph works. In the next post, we’ll take a look at the search results paragraph and how it works.

Categories: magnolia-cms, resources
  1. No comments yet.
  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: