oo

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.

The Blogs display widget works with Similar Results because of its 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.q1.1

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:

  1. 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
    
  2. From the module root, build and deploy.

    ./gradlew deploy -Ddeploy.docker.container.id=$(docker ps -lq)
    
    note

    This command is the same as copying the deployed jars to /opt/liferay/osgi/modules on the Docker container.

  3. Confirm the deployment in the Liferay Docker container console.

    STARTED com.acme.r1s1.impl_1.0.0 [1009]
    
  4. Verify that the example contributor is working. Begin by opening your browser to https://localhost:8080

  5. Add some KB Articles at Site MenuContentKnowledge 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

  6. Add the Knowledge Base Display widget to a page, followed by the Similar Results widget.

  7. 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

  8. 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.

The Similar Results widget can display 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.

note

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.

important

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.

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.

Capability: