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.
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
, theAuthor
field does not appear in the Select Fields widget. -
Filters for
Article
objects and returnsnull
if the search result is not anArticle
. For objects where theAuthor
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.
With a query on articles, the Author field is included in the search results.
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.
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.
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 fromImage
andVideo
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 ofImage
objects. -
Implements a
VideoSlide
inner class for slides consisting ofVideo
objects. -
Implements the
fromSelection
method from theSearchResultSelectionGeneratable
interface. The method createsImage
orVideo
slides from theSearchResultSelectionObject
. After usage of theSearchResultSelection
to create a new Content instance, theSearchResultSelection
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 theSearchResultSelection#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.
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 fromMultiMediaGalleryAction
. -
Performs a query to get the
SearchResultSelection
object identified byselectionId
. -
Creates a
MultiMediaGallery
object. TheSearchResultSelection
object can represent selections of various content types. However,MultiMediaGallery
is limited to Image and Video item types, so the implementedfromSelection
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.
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 thatSearchResultSuggestions
needs to determine whichSearchResultSuggester
implementation to use. The default priority level set in theSearchResultSuggester
interface is zero. To use aSearchResultSuggester
implementation other than the defaultSolrSearchResultSuggester
, return a value greater than zero. To no longer use aSearchResultSuggester
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 theArticle
object and theImage
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.
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 @LabelFields
annotation, 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.