Contributing Custom Content to the Similar Results Widget
Subscribers
Availability: This functionality relies on a Service Provider Interface (SPI) that’s bundled with Liferay DXP 7.3+. It’s available in Liferay DXP 7.2, from Fix Pack 5+, via installation of the Similar Results widget from Liferay Marketplace.
You can display your application’s custom content in the Similar Results widget by implementing a SimilarResultsContributor
. Note that for the contributor to work, the Similar Results widget must be able to detect your content as the main asset on a page. That means it must be displayable via a URL in a “Display Widget”, like the supported Liferay DXP assets (e.g., blogs entries and wiki pages). Keep in mind that the Similar Results widget can already be used with any content displayed in Lifery DXP’s Asset Publisher, without the need for a custom contributor.
Since the Knowledge Base application does not implement a SimilarResultsContributor
for KB Articles out of the box, this example implements one. For simplicity, only KB Articles in the root folder of the application are dealt with here.
Deploy a SimilarResultsContributor for Knowledge Base Articles
Start a new Liferay DXP instance by running
docker run -it -m 8g -p 8080:8080 liferay/dxp:2024.q2.11
Sign in to Liferay at http://localhost:8080 using the email address test@liferay.com and the password test. When prompted, change the password to learn.
Then, follow these steps to get an example SimilarResultsContributor
up and running on your Liferay DXP instance:
-
Download and unzip Acme Similar Results Contributor.
curl https://resources.learn.liferay.com/dxp/latest/en/using-search/developer-guide/liferay-r1s1.zip -O
unzip liferay-r1s1.zip
-
From the module root, build and deploy.
./gradlew deploy -Ddeploy.docker.container.id=$(docker ps -lq)
noteThis command is the same as copying the deployed jars to /opt/liferay/osgi/modules on the Docker container.
-
Confirm the deployment in the Liferay Docker container console.
STARTED com.acme.r1s1.impl_1.0.0 [1009]
-
Verify that the example contributor is working. Begin by opening your browser to
https://localhost:8080
-
Add some KB Articles at Site Menu → Content → Knowledge Base.
Make sure they have similar Title and Content fields. You can use these Strings to create three articles (use the same string for title and content):
Test KB Article one
Test KB Article two
Test KB Article three
-
Add the Knowledge Base Display widget to a page, followed by the Similar Results widget.
-
Open the widget configuration of the Similar Results widget, and make sure to set a value of 1 for these settings:
Minimum Term Frequency: 1 Minimum Document Frequency: 1
-
Click on one of the KB Articles to select it for display, as the main asset.
The Similar Results widget now shows other related KB Articles.
Now that you verified that the example behaves properly, learn how it works.
Examine the SimilarResultsContributor
Review the deployed example. It contains just one class: the contributor that enables custom content for the Similar Results widget.
Annotate the Contributor Class for OSGi Registration
The R1S1SimilarResultsContributor
implements the SimilarResultsContributor
interface:
@Component(service = SimilarResultsContributor.class)
public class R1S1SimilarResultsContributor implements SimilarResultsContributor {
The service
component property registers your implementation as a SimilarResultsContributor
service.
Review the SimilarResultsContributor
Interface
Implement the three methods from the interface.
public void detectRoute(RouteBuilder routeBuilder, RouteHelper routeHelper);
Implement detectRoute
to provide a distinctive portion of your entity’s URL pattern, so that the Similar Results widget can detect if your contributor should be invoked. The URL pattern is added as an attribute of the RouteBuilder
object. The RouteHelper
is useful for retrieving the whole URL String for parsing.
Only one SimilarResultsContributor
is supported for each display widget.
public void resolveCriteria(
CriteriaBuilder criteriaBuilder, CriteriaHelper criteriaHelper);
Implement resolveCriteria
to use the main entity on the page to look up the corresponding search engine document. This will be invoked if the route detected indicates that your contributor is the appropriate one.
public void writeDestination(
DestinationBuilder destinationBuilder,
DestinationHelper destinationHelper);
Implement writeDestination
to update the main asset when a User clicks a link in the similar results widget.
Complete the Similar Results Contributor
Implement the detectRoute
Method
@Override
public void detectRoute(
RouteBuilder routeBuilder, RouteHelper routeHelper) {
String[] pathParts = StringUtil.split(
_http.getPath(routeHelper.getURLString()),
Portal.FRIENDLY_URL_SEPARATOR);
String[] parameters = StringUtil.split(
pathParts[pathParts.length - 1], CharPool.FORWARD_SLASH);
if (!parameters[0].matches("knowledge_base")) {
throw new RuntimeException(
"Knowledge base article was not detected");
}
routeBuilder.addAttribute("urlTitle", parameters[1]);
}
Implement detectRoute
to inject logic checking for a distinctive portion of your entity’s URL pattern. The Similar results widget uses this check to find the correct SimilarResultsContributor
. If your entity’s display URL is detected, add at least one attribute to the URL route for use later. Here we’re checking for "knowledge_base"
in the Friendly URL, and adding "urlTitle"
as an attribute to the RouteBuilder
passed in the method signature if it’s detected.
The routeHelper.getUrlString
call is important, as it can be used to retrieve the relative URL of the detected asset within the virtual instance. For example,
/web/guest/page-title/-/knowledge_base/kb-article-url-title
The ID being added as an attribute to the RouteBuilder
is used to fetch the entity and the corresponding search engine document in the resolveCriteria
method.
Implement the resolveCriteria
Method
@Override
public void resolveCriteria(
CriteriaBuilder criteriaBuilder, CriteriaHelper criteriaHelper) {
String urlTitle = (String)criteriaHelper.getRouteParameter("urlTitle");
KBArticle kbArticle = _kbArticleLocalService.fetchKBArticleByUrlTitle(
criteriaHelper.getGroupId(),
KBFolderConstants.DEFAULT_PARENT_FOLDER_ID, urlTitle);
if (kbArticle == null) {
return;
}
AssetEntry assetEntry = _assetEntryLocalService.fetchEntry(
criteriaHelper.getGroupId(), kbArticle.getUuid());
if (assetEntry == null) {
return;
}
String uidField = String.valueOf(kbArticle.getPrimaryKeyObj());
if (ReleaseInfo.getBuildNumber() ==
ReleaseInfo.RELEASE_7_2_10_BUILD_NUMBER) {
uidField = String.valueOf(kbArticle.getResourcePrimKey());
}
criteriaBuilder.uid(Field.getUID(assetEntry.getClassName(), uidField));
}
Look up the search engine document corresponding to the page’s displayed entity. You must provide the criteriaBuilder.uid
method the value of the appropriate search engine document’s uid
field (this is usually equal to the Elasticsearch-specified _id
field in the document). In the Liferay DXP index, this field is a composition of the entry class name and the class primary key. Pass both as Strings to Field.getUID
to obtain the value. Our example starts by fetching the model entity using the ID you added to the attribute in the detectRoute
method (the urlTitle
), and then uses it to retrieve the asset entry.
There’s a difference between Liferay DXP 7.2 and Liferay DXP 7.3, so a condition to check the version, with logic for each, is provided here. In Liferay DXP 7.3, getPrimaryKeyObj
is used in conjunction with the class name, whereas in Liferay DXP 7.2, getResourcePrimKey
is needed.
Now that matching documents can be found, write the destination URL so the similar results are updated.
Implement the writeDestination
Method
@Override
public void writeDestination(
DestinationBuilder destinationBuilder,
DestinationHelper destinationHelper) {
String urlTitle = (String)destinationHelper.getRouteParameter(
"urlTitle");
AssetRenderer<?> assetRenderer = destinationHelper.getAssetRenderer();
KBArticle kbArticle = (KBArticle)assetRenderer.getAssetObject();
destinationBuilder.replace(urlTitle, kbArticle.getUrlTitle());
}
Implement writeDestination
to update the main asset when a user clicks a link in the Similar Results widget. The More Like This query is re-sent to the search engine, and the Similar Results list is re-rendered to match the new main asset. For KB Articles, the entirety of the work is to replace the urlTitle
in the original URL (for the main asset) with the urlTitle
of the matched entity.
The destinationHelper.getRouteParameter
call is important. As the only method from the DestinationHelper
that is a pre-search operator, it will always return data from the currently selected main asset, prior to re-rendering the main asset or the Similar Results links. The remainder of the DestinationHelper
methods, including the other one shown here, getAssetRenderer
, return data for a matched asset. This method is run iteratively for each matched result.
Declare the Service Dependencies
This code relies on services deployed to an OSGi container: AssetEntryLocalService
, KBArticleLocalService
, and Http
. Declare your need for them using the Declarative Services @Reference
annotation, provided by org.osgi.service.component.annotations.Reference
. Set them into private fields.
@Reference
private AssetEntryLocalService _assetEntryLocalService;
@Reference
private Http _http;
@Reference
private KBArticleLocalService _kbArticleLocalService;
Additional Details
Since each implementation of an entity’s URLs is likely to differ significantly, see the SimilarResultsContributor
interface and the bundled implementations on GitHub if you need more inspiration when writing your own application’s contributor.
Much of the work involved in contributing your application’s custom content to the Similar Results widget is in working with the display URL. To learn how Liferay’s own assets create their display URLs, inspect the getURLView
method of an entity’s *AssetRenderer
class.
JournalArticleAssetRenderer#getURLView
, Liferay DXP 7.3.2 GA3WikiPageAssetRenderer#getURLView
, Liferay DXP 7.3.2 GA3BlogsEntryAssetRenderer#getURLView
, Liferay DXP 7.3.2 GA3DLFileEntryAssetRenderer#getURLView
, Liferay DXP 7.3.2 GA3
As mentioned earlier, this example demonstrates creating a SimilarResultsModelDocumentContributor
that will work with KB Articles in the root folder of the application. Adding support for KB Folders is possible, and is an interesting exercise for the motivated reader. Look at the source code for the DocumentLibrarySimilarResultsContributor
for inspiration.
Troubleshooting: Asset UID Architecture
The uid
is constructed in a standard way as of Liferay DXP 7.3. The com.liferay.portal.search.internal.model.uid.UIDFactoryImpl
class is responsible for setting the uid
for all documents under control by Liferay’s indexing architecture. Since it’s now standardized, there need be no guesswork on your part.
Similarly, in versions 7.2 and 7.1, if an entity is indexed with the Composite Indexer APIs (i.e., it has a ModelDocumentContributor
class), the uid
is set by Liferay’s implementation and is standardized.
However, entities indexed with the legacy Indexer API (i.e., the entity has a *Indexer
class that extends Liferay’s BaseIndexer
) may have overridden the logic that sets the uid
, so it’s worth looking into an entity’s indexing implementation.