Brightspot CMS Developer Guide

Customizing search results


Brightspot provides several APIs to customize the search results that appear in the search panel. For example, you can set the fields that appear in search results, add actions that can be performed on results, and suggest results based on custom logic. Also, for applicable content types, you can add thumbnail images in the search carousel in the content edit page.


You can select which fields appear in the search results with the Fields list in the results section of the search panel. Brightspot populates the Select Fields widget with default fields.

Select search fields.png
Select fields widget

Using the SearchResultField interface, you can add custom fields to the widget. For example, say that you want the author name to appear for articles listed in the search results. You can implement SearchResultField to return the Author field for Article objects retrieved in a query, similar to the following example.

public class AuthorField implements SearchResultField {

   @Override
   public String getDisplayName() { 
      return "Author";
   }

   @Override
   public boolean isSupported(ObjectType type) { 
      return true;
   }

   @Override
   public String createDataCellText(Object item) { 
      Article article = State.getInstance(item).getOriginalObject() instanceof Article
             ? ((Article) State.getInstance(item).getOriginalObject()) : null;

      if (article != null) {
         if (article.getAuthor() != null)
            return article.getAuthor().getName();
         else return ("No attribution");
      }
      return null;
   }
}
  • Specifies how the field label appears in the search results.
  • Determines if the field appears in the search results. If the method returns false, the Author field does not appear in the Select Fields widget.
  • Filters for Article objects and returns null if the search result is not an Article. For objects where the Author field is set, the author name is returned; otherwise "No attribution" is returned.

With the new implementation, the Author field is now available in the Select Fields widget.

Select fields widget with Author shown.png

With a query on articles, the Author field is included in the search results.

Search results with Author column displayed.png
Search results with Author column displayed


The SearchResultView interface allows you to customize the results section of the search panel. Brightspot includes default implementations, such as the list result view shown below. You can modify these implementations or create your own.

SearchResultView interface.png
SearchResultView interface


The search panel includes a widget for invoking operations on search results. The available actions that appear in the list can vary, depending on the content type filter setting.

Search result actions.png
Search result available actions

Brightspot provides default action implementations as reflected in the above screen shot. You can also create custom actions by implementing the SearchResultAction interface and a page servlet that operates on search results. Brightspot automatically detects new action implementations.

To implement SearchResultAction, override the following methods:

  • getGroup sets a group name in the UI for a set of related action implementations, like Bulk.
  • getPosition sets the top-down position of the action in the UI list of actions. Implementations with the same position settings are arranged in alphabetical order based on the class name.
  • shouldDisplay shows or hides the action in the UI.
  • writeHtml constructs the HTML link that invokes the action when the button is clicked in the UI. The link references the path of the page servlet that processes the search results.

The following snippet shows the writeHtml implementation for the SaveSearchResultAction, which saves the search for the current result set. The method takes ToolPageContext, Search, and SearchResultSelection objects. The ToolPageContext object constructs the HTML link to the action servlet.

@Override
 public void writeHtml(
         ToolPageContext page,
         Search search,
         SearchResultSelection selection)
         throws IOException {

   page.writeStart("div", "class", "searchResult-action-simple");
       page.writeStart("a",
               "class", "button",
               "target", "toolUserSaveSearch",
               "href", page.cmsUrl("/toolUserSaveSearch", 
                       "search", page.url("", Search.NAME_PARAMETER, null)));
           page.writeHtml(page.localize(SaveSearchResultAction.class, "action.saveSearch")); 
       page.writeEnd();
   page.writeEnd();
 }
  • Construct the URL to the page servlet, specified in the servlet routing path as toolUserSaveSearch. This page servlet is located in <project>src/main/java/com/psddev/cms/tool/page.
  • Specifies the label on the action button. If Brightspot is localized, you can pass a resource key to retrieve the label for the applicable UI language. For the search result APIs in the Brightspot project, place resource files in <project>src/main/resource/com/psddev/cms/tool/search.


Leveraging the SearchResultSelectionGeneratable annotation, you can create a complex Content type with a search result action. For example, you can have a MultiMediaGallery type that consists of Image and Video slides. A query for Image or Video types displays an associated action in the search panel. Invoking the action creates a MultiMediaGallery object with any Image or Video objects selected in the search results.

The following steps show how to construct a search result action that creates a MultiMediaGallery content type.

Step 1: Create the content type

@SearchResultSelectionGeneratable.ItemTypes({Image.class, Video.class}) 
public class MultiMediaGallery extends Content {

   private List<Slide> slides; 
   public List<Slide> getSlides() { return slides; }
   public void setSlides(List<Slide> slides) { this.slides = slides; }

   @Embedded
   private static abstract class Slide extends Record { } 

   private static class ImageSlide extends Slide { 
      private Image image;

      public Image getImage() {
         return image;
      }

      public void setImage(Image image) {
         this.image = image;
      }
   }

   private static class VideoSlide extends Slide { 
      private Video video;

      public Video getVideo() {
         return video;
      }

      public void setVideo(Video video) {
         this.video = video;
      }
   }

   public void fromSelection(SearchResultSelection selection) { 

      for (Object object : selection.createItemsQuery().selectAll()) {
         if (object instanceof Image) {
             // If the selected object is an Image, create a new ImageSlide to wrap it.
             ImageSlide imageSlide = new ImageSlide();
             imageSlide.setImage((Image) object);
             getSlides().add(imageSlide);

         } else if (object instanceof Video) {
             // If the selected object is a Video, create a new VideoSlide to wrap it.
             VideoSlide videoSlide = new VideoSlide();
             videoSlide.setVideo((Video) object);
             getSlides().add(videoSlide);
         }
         // Ignore any other objects that are not Images or Videos
      }
   }
}
  • Class annotation that enables MultiMediaGallery objects to be created from Image and Video objects returned in search results.
  • Defines a list for a Slide type and associated getter and setter methods.
  • Embeds an abstract Slide class.
  • Implements an ImageSlide inner class for slides consisting of Image objects.
  • Implements a VideoSlide inner class for slides consisting of Video objects.
  • Implements the fromSelection method from the SearchResultSelectionGeneratable interface. The method creates Image or Video slides from the SearchResultSelectionObject. After usage of the SearchResultSelection to create a new Content instance, the SearchResultSelection is destroyed.

Step 2: Implement SearchResultAction

The SearchResultAction implementation displays the applicable action button in the search panel.

public class MultiMediaGalleryAction implements SearchResultAction {

   @Override
   public int getPosition() {
      return 0;
   }

   @Override
   public boolean shouldDisplay(ToolPageContext page, Search search, SearchResultSelection selection) {
      return true;
   }

   // @Override
   public void writeHtml(
      ToolPageContext page,
      Search search,
      SearchResultSelection selection)
      throws IOException {

      if (selection == null) { 
         return;
      }

      page.writeStart("div", "class", "searchResult-action-simple");

      page.writeStart("a",
             "class", "button",
             "target", "toolUserMultiMedia",
             "href", new UrlBuilder(page.getRequest())  
                     .absolutePath(page.toolPath(CmsTool.class, "toolUserMultiMedia"))
                     .parameter("selectionId", selection.getId()));
      page.writeHtml(page.localize(MultiMediaGalleryAction.class, "action.MultiMediaGalleryAction"));
      page.writeEnd();
      page.writeEnd();
   }
}
  • Checks for search results that are selected in the UI. If there are no selections, then the implementation does not display the action button.
  • Constructs the URL to the page servlet, specified in the servlet routing path as tooUserMultiMedia. Only one parameter is passed to the servlet, selectionId, returned by the SearchResultSelection#getId() method.
  • Specifies the label on the action button. The label is retrieved from a localization resource file.

When results are selected in the search panel, the Create MultiMediaGallery button appears.

Create MultiMediaGallery button.png
Create MultiMediaGallery button displayed

Step 3: Implement page servlet

The page servlet invoked from the search result action creates the MultiMediaGallery objects from the search result selections.

@RoutingFilter.Path(application = "cms", value = "toolUserMultiMedia") public class ToolUserMultiMedia extends PageServlet {

   @Override
   protected String getPermissionId() {
      return null;
   }

   @Override
   protected void doService(ToolPageContext page) throws IOException, ServletException {

      UUID selectionId = page.param(UUID.class, "selectionId"); 
      SearchResultSelection selection = Query.from(SearchResultSelection.class).where("_id = ?", selectionId).first(); 

      MultiMediaGallery gallery = new MultiMediaGallery(); 
      gallery.fromSelection(selection);
      gallery.save();
   }
}
  • Specifies the annotation @RoutingFilter.Path of the servlet as toolUserMultiMedia.
  • Gets the value of the selectionId parameter passed from MultiMediaGalleryAction.
  • Performs a query to get the SearchResultSelection object identified by selectionId.
  • Creates a MultiMediaGallery object. The SearchResultSelection object can represent selections of various content types. However, MultiMediaGallery is limited to Image and Video item types, so the implemented fromSelection method creates gallery slides from only those types.


When you click a selection field in the content edit form, the content picker appears with a list of search results applicable to the object type. For example, if an Article contains fields for Author and Image, clicking either of those fields invokes the Content Picker with a list of Author or Image search results. Obviously you want to select a result appropriate to the context of the containing article.

To assist the user in selecting a search result that’s most appropriate to the containing object type, you can implement suggestions to appear in the Content Picker search results. Say that you have an Article subclass for environmental content, which contains an Image field. To guide writers to set applicable images for the content type, you can implement filtering of search results and display suggestions for environmentally-themed images. In the following screen shot, Brightspot suggests images that are related to the environment.

Select Image.png
Select Image widget

How suggestions work

As search results are rendered in the content picker, the search renderer calls the page servlet SearchResultSuggestions. This servlet passes control to a SearchResultSuggester implementation with the highest priority. Based on the object on which the content picker was invoked (such as Article), and on the search results returned (such as Images), the SearchResultSuggester implementation determines if there are suggestions to offer. If so, the implementation passes the suggestions to the search renderer.

Brightspot includes a default SearchResultSuggester implementation, SolrSearchResultSuggester. This implementation uses Solr database scoring to check for suggestions.

Implementing SearchResultSuggester

Override the following methods:

  • getPriority returns a numerical priority level that SearchResultSuggestions needs to determine which SearchResultSuggester implementation to use. The default priority level set in the SearchResultSuggester interface is zero. To use a SearchResultSuggester implementation other than the default SolrSearchResultSuggester, return a value greater than zero. To no longer use a SearchResultSuggester implementation, return a value less than zero.
  • writeHtml contains the logic for determining suggestions. To continue with the above example, this method would evaluate the context of the Article object and the Image search results and pass suggestions, if any, to the search renderer.

In Brightspot, non-content objects derive directly from Record and typically support Content-derived objects. For example, you can have a content-carrying Article class that references a Record-derived WriterContract class. This class represents the personal information of freelance writers who contribute articles.

public class WriterContract extends Record {

   @Indexed
   private String name;

   @Indexed
   private String address;

   private StorageItem contract;

   // getters and setters
}

Record-derived objects can only be searched by content type in the content picker, not in the search panel. In the content picker, only the first indexed field of a Record-derived object is searchable. All other indexed fields are ignored by default.

For example, if you click the Contract field in the content edit form for Article, the content picker shows all of the WriterContract objects. Because name is the first indexed field in the WriterContract class, you can only search on name in the content picker.

Content picker searches only on name.png
Content picker searches only on name

You can change Brightspot’s default search behavior for non-content objects in the following ways:

  • Use the @Content.Searchable annotation, which makes all indexed fields of a Record-derived class searchable, from both the search panel and the content picker.
  • Use the @Recordable.LabelFields annotation in conjunction with the @Indexed annotation.

All indexed fields listed in the @LabelFields annotation are full-text searchable in the content picker.

In the following example, the indexed fields of name and address are listed in the @LabelFieldsannotation, thereby making them both searchable.

@Recordable.LabelFields({"name", "address"})
public class WriterContract extends Record {

   @Indexed
   private String name;

   @Indexed
   private String address;

   private StorageItem contract;

   // getters and setters
}

In the content picker, you can now search on the address field as well as the name field to narrow the selection of WriterContract objects.

Searches on name or address.png
Searches on name or address

Previous Topic
Customizing search panel settings
Next Topic
Visibility labels
Was this topic helpful?
Thanks for your feedback.