Solr Enterprise Search Meeting OFBiz (Part – 2)

In my last post Solr Enterprise Search Meeting OFBiz (Part – 1) we saw how to setup Apache Solr on your local computer for development.

In this post we are going to see how to get it to work with Apache OFBiz.

For demonstration purposes, we are going to use example component from OFBiz. The steps shown here will be the same for any other business application. The setup in this tutorial is very basic and will get you on your way to using it with Apache OFBiz. So let’s see how to do it.

Setup core for Apache OFBiz examples in Apache Solr

1. Use solr-4.7.2/example directory as a reference and setup an instance of the Jetty Servlet container to run Solr for your application. Read solr-4.7.2/example/README.txt for more details. The example directory can be copied and renamed as “examples”. The new directory structure should look like:

Solr Enterprise Search Jetty Container Directory

2. Setup the OFBiz example’s core inside the new setup as shown below, again refer to the example core from Solr to do it.

Ofbiz Example Core Structure in Solr

3. Make an entry for the new core that you want to create in $SOLR_HOME/ofbizus/solr/solr.xml as shown below(ofbizus here is the name of a HWM application, you can name it whatever you would like) here we are going to work on the OFBiz example component and want to define the core for it so we are naming it “examples”:

.....
.....  
<cores adminPath="/admin/cores" host="${host:}" hostPort="${jetty.port:8983}" hostContext="${hostContext:solr}">
    <core name="examples" instanceDir="examples" />
    
    <shardHandlerFactory name="shardHandlerFactory" class="HttpShardHandlerFactory">
      <str name="urlScheme">${urlScheme:}</str>
    </shardHandlerFactory>
</cores>
.....
.....

4. Now we define the schema for the examples documents which will be created and indexed for search. Define the schema as mentioned below for the fields in the document in $SOLR_HOME/ofbizus/solr/examples/conf/schema.xml (Code excerpt you can use given below for creating example documents to be indexed):

....
....
 <!-- Only remove the "id" field if you have a very good reason to. While not strictly
     required, it is highly recommended. A <uniqueKey> is present in almost all Solr 
     installations. See the <uniqueKey> declaration below where <uniqueKey> is set to "id".
   -->   
   <field name="exampleId" type="string" indexed="true" stored="true" required="true" multiValued="false" />
   <field name="fullText" type="text_general" indexed="true" stored="false" required="false" multiValued="true" />
   <field name="exampleName" type="string" indexed="true" stored="true" required="true" multiValued="false" />
   <field name="description" type="string" indexed="true" stored="false" required="true" multiValued="false" />
   <field name="statusId" type="string" indexed="true" stored="true" required="false" multiValued="false" />
   <field name="exampleTypeId" type="string" indexed="true" stored="false" required="false" multiValued="false" />
....
....
....
 <!-- Field to use to determine and enforce document uniqueness. 
      Unless this field is marked with required="false", it will be a required field
   -->
 <uniqueKey>exampleId</uniqueKey>
....
....

At this stage we are done with the Solr part, now we have to make it work with OFBiz.

Apache OFBiz Implementation: Preparing example documents as per the schema defined for indexing in Apache Solr

1. If you do not have the example component available within your OFBiz setup, you can get it through SVN from here: http://svn.apache.org/repos/asf/ofbiz/trunk/specialpurpose/example/
Drop it into the hot-deploy directory.
2. Define and implement a service which will create the example document and indexes as they are created in Solr per the defined document schema. Add a service definition to $OFBIZ_HOME/hot-deploy/example/servicedef/services.xml

   <service name="indexExample" engine="java" default-entity-name="Example" location="org.ofbiz.example.ExampleServices" invoke="indexExample">
        <description>Indexing examples</description>
        <auto-attributes mode="IN" include="pk" optional="false"/>
        <auto-attributes mode="IN" include="nonpk" optional="true"/>
    </service>

2. Create a new configuration file as $OFBIZ_HOME/hot-deploy/example/config/Search.properties and put these configurations:

solr.host=http://localhost:8983/solr

3. Put the implementation of the service you just defined in the src directory at org.ofbiz.example.ExampleServices as mentioned below. This service implementation will take care of connecting to the Solr server, and supply the field values to SOLR to create/update or delete the document for indexing. We have used the Solrj API to work with Solr, make sure to copy it from $ SOLR_HOME/dist/solr-solrj-4.7.2.jar to $OFBIZ_HOME/hot-deploy/example/lib.

package org.ofbiz.example;

import java.io.IOException;
import java.util.Map;

import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrServer;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrDocument;
import org.ofbiz.base.util.UtilProperties;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.entity.Delegator;
import org.ofbiz.service.DispatchContext;
import org.ofbiz.service.ServiceUtil;

public class ExampleServices {

    public static Map<string, object=""> indexExample(DispatchContext dctx, Map<string, ?="" extends="" object=""> context) {
        Delegator delegator = dctx.getDelegator();
        String exampleId = (String) context.get("exampleId");

        // examples below is the name of the core we have defined in solr
        String solrHost = getSolrHost(delegator, "examples");

        HttpSolrServer server = new HttpSolrServer(solrHost);
        SolrDocument solrDocument = new SolrDocument();
        solrDocument.addField("exampleId", exampleId);
        solrDocument.addField("exampleName", context.get("exampleName"));
        solrDocument.addField("description", context.get("description"));
        solrDocument.addField("fullText", exampleId);
        solrDocument.addField("fullText", context.get("exampleName"));
        solrDocument.addField("fullText", context.get("description"));
        solrDocument.addField("exampleTypeId", context.get("exampleTypeId"));
        solrDocument.addField("statusId", context.get("statusId"));
        try {
            server.add(ClientUtils.toSolrInputDocument(solrDocument));
            server.commit();
        } catch (SolrServerException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return ServiceUtil.returnSuccess();
    }

    public static String getSolrHost(Delegator delegator, String indexName) {
        String solrHost = UtilProperties.getPropertyValue("Search", "solr.host");
        if (UtilValidate.isNotEmpty(solrHost)) {
            solrHost += "/" + indexName;
        }
        return solrHost;
    }
}

4.  We want this service to add/update/delete the example document(being indexed or already indexed) as any of these operations is performed on the example. To do this, we are using an Entity ECA. Create the EECA file as $ OFBIZ_HOME/hot-deploy/example/entitydef/eecas.xml as shown below:

<entity-eca xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="http://ofbiz.apache.org/dtds/entity-eca.xsd">
    <eca entity="Example" operation="create-store-remove" event="return">
        <action service="indexExample" mode="sync"/>
    </eca>
</entity-eca>

 

Testing Solr Enterprise Search with Apache OFBiz at this stage

At this stage to test your work
1. Recompile and start OFBiz project with demo data using command-

$./ant clean load-demo start

2. Now start Solr server using:

$cd SOLR_HOME/ofbizus/
$ java -jar start.jar

3. Direct your browser to the OFBiz Example application and create some new examples, observe the activities on the Solr console log. You will see that it has started creating and indexing these documents.

4. Now, direct your browser to the Solr admin console, it should now show the examples core that you defined similar to the image below. Solr Enterprise Search Admin Console
5. Now you can play with it for sometime and check if the new examples that you created in OFBiz are visible here at Solr side. For this you can continue adding examples in OFBiz and find them here in Solr admin console.

Apache Solr Enterprise Search in Action with Apache OFBiz

After starting the Solr and OFBiz servers, you will create a new example, a document will be created and indexed by Solr for it. Now that Solr Search is working with OFBiz, you can follow these steps to build a UI for it and start using Solr Search.

1. Implement a new screen named “FindExampleSolr” in file $ OFBIZ_HOME/hot-deploy/example/widget/ExampleScreens.xml as:

<screen name="FindExampleSolr">
    <section>
        <actions>
            <set field="titleProperty" value="PageTitleFindExamples"/>
            <set field="headerItem" value="FindExampleSolr"/>
            <script location="component://example/webapp/example/WEB-INF/actions/GetExampleSearchFacets.groovy"/>
            <script location="component://example/webapp/example/WEB-INF/actions/SearchExamplesByFilters.groovy"/>
        </actions>
        <widgets>
             <decorator-screen name="main-decorator"   location="${parameters.mainDecoratorLocation}">
                 <decorator-section name="left-column">
                     <screenlet title="Search Example">
                        <platform-specific>
                            <html><html-template location="component://example/webapp/example/example/FindExample.ftl"/></html>
                        </platform-specific>
                    </screenlet>
                </decorator-section>
                <decorator-section name="body">
                    <screenlet title="Search Results">
                        <platform-specific>
                            <html><html-template location="component://example/webapp/example/example/ListFilteredExamples.ftl"/></html>
                        </platform-specific>
                    </screenlet>
                </decorator-section>
            </decorator-screen>
        </widgets>
    </section>
</screen>

2. Implement GetExampleSearchFacets.groovy(For facets preparation for Example Type and Status on your search view) included in your screen’s action tag at the mentioned location(component://example/webapp/example/WEB-INF/actions/) in your example component as:

import org.apache.solr.client.solrj.impl.HttpSolrServer;
import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrRequest;

import org.ofbiz.example.ExampleServices;

HttpSolrServer server = new HttpSolrServer(ExampleServices.getSolrHost(delegator, "examples"));
SolrQuery query = new SolrQuery();
facetExampleTypes = [];
keyword = parameters.keyword?.trim() ?: "*";

exampleTypes = delegator.findList("ExampleType", null, null, null, null, false);

// calculation of facets
query.setParam("q", "*:*");
query.addFilterQuery("fullText:*" + keyword + "* OR " + "exampleId:*" + keyword + "*");
query.setFacet(true);

//exampleType facets for getting the count of example in each exampleType.
exampleTypes.each { exampleType ->
    query.addFacetQuery("exampleTypeId:" + exampleType.exampleTypeId);
}

statusQueryString = "";
if(parameters.statusId){
    searchedStatusesString = parameters.statusId;
    searchedStatuses = searchedStatusesString.split(":");
    searchedStatuses.each { status ->
        if(status) {
            if(statusQueryString) {
                statusQueryString = statusQueryString + " OR " + status;
            } else {
                statusQueryString = status;
            }
        }
    }
}
if (statusQueryString){
    query.addFilterQuery("statusId:(" + statusQueryString + ")");
}

QueryRequest qryReq = new QueryRequest(query, SolrRequest.METHOD.POST);
QueryResponse rsp = qryReq.process(server);
Map facetQuery = rsp.getFacetQuery();

searchedExampleTypesString = parameters.exampleType;
searchedExampleTypes = searchedExampleTypesString?.split(":");

if(parameters.keyword){
    urlParam = "keyword=" + keyword + "&exampleType=";
} else {
    urlParam = "exampleType=";
}
// ExampleType URL parameters calculation
exampleTypes.each { exampleType ->
    exampleTypeInfo = [:];
    exampleTypeParam = urlParam;
    checked = true;
    exampleTypeCount = facetQuery.get("exampleTypeId:" + exampleType.exampleTypeId);
    if(exampleTypeCount > 0 || searchedExampleTypes?.contains(exampleType.exampleTypeId)) {
        exampleTypeInfo.description = exampleType.description;
        exampleTypeInfo.exampleTypeId = exampleType.exampleTypeId;
        exampleTypeInfo.exampleTypeCount = exampleTypeCount;

        if(searchedExampleTypes){
            urlExampleTypes = [];
            searchedExampleTypes.each { searchExampleType ->
                if(searchExampleType){
                    if(!(searchExampleType.equals(exampleType.exampleTypeId))){
                        urlExampleTypes.add(searchExampleType);
                    }
                }
            }
            urlExampleTypes.each { urlExampleType->
                exampleTypeParam = exampleTypeParam + ":"+ urlExampleType;
            }
            if(!(searchedExampleTypes.contains(exampleType.exampleTypeId))) {
                checked = false;
                exampleTypeParam = exampleTypeParam + ":"+ exampleType.exampleTypeId;
            }
        } else {
            exampleTypeParam = exampleTypeParam + exampleType.exampleTypeId;
        }
        if(parameters.statusId){
            exampleTypeParam = exampleTypeParam + "&statusId=" + parameters.statusId;
        }
        exampleTypeInfo.checked = checked;
        exampleTypeInfo.urlParam = exampleTypeParam;
        facetExampleTypes.add(exampleTypeInfo);
    }
}
context.clearExampleTypeUrl = urlParam + (parameters.statusId ? "&statusId=" + parameters.statusId : "");;

//example status facets
query.clear();
query.setParam("q", "*:*");
query.addFilterQuery("fullText:*" + keyword + "* OR " + "exampleId:*" + keyword + "*");
query.setFacet(true);

exampleStatuses = delegator.findByAnd("StatusItem", [statusTypeId : "EXAMPLE_STATUS"], null, false);
exampleStatuses.each { exampleStatus ->
    query.addFacetQuery("statusId:" + exampleStatus.statusId);
}

queryString = "";
statusUrlExampleTypeParam = urlParam;
if(searchedExampleTypes){
    searchedExampleTypes.each { searchExampleType ->
        if(searchExampleType){
            if(queryString) {
                queryString = queryString +" OR " + searchExampleType;
            } else {
                queryString = searchExampleType;
            }
            statusUrlExampleTypeParam = statusUrlExampleTypeParam + ":" + searchExampleType;
        }
    }
    if (queryString){
        query.addFilterQuery("exampleTypeId:" + "(" + queryString +")");
    }
}

qryReq = new QueryRequest(query, SolrRequest.METHOD.POST);
rsp = qryReq.process(server);
facetQuery = rsp.getFacetQuery();
facetExampleStatuses = [];

context.clearStatusUrl = statusUrlExampleTypeParam;

searchedExampleStatusesString = parameters.statusId;
searchedExampleStatuses = searchedExampleStatusesString?.split(":");

exampleStatuses.each { exampleStatus ->
    exampleStatusInfo = [:];
    checked = true;
    exampleStatusCount= facetQuery.get("statusId:" + exampleStatus.statusId);
    if(exampleStatusCount > 0) {
        exampleStatusUrlParam = statusUrlExampleTypeParam + "&statusId=";
        exampleStatusInfo.statusDesc = exampleStatus.description;
        exampleStatusInfo.statusId = exampleStatus.statusId;
        exampleStatusInfo.exampleStatusCount = exampleStatusCount;

        if(searchedExampleStatuses){
            urlExampleStatuses = [];
            searchedExampleStatuses.each { searchExampleStatus ->
                if(searchExampleStatus){
                    if(!(searchExampleStatus.equals(exampleStatus.statusId))){
                        urlExampleStatuses.add(searchExampleStatus);
                    }
                }
            }
            urlExampleStatuses.each { urlExampleStatus->
                exampleStatusUrlParam = exampleStatusUrlParam + ":"+ urlExampleStatus;
            }
            if(!(searchedExampleStatuses.contains(exampleStatus.statusId))) {
                checked = false;
                exampleStatusUrlParam = exampleStatusUrlParam + ":" + exampleStatus.statusId;
            }
        } else {
            exampleStatusUrlParam = exampleStatusUrlParam + exampleStatus.statusId;
        }
        exampleStatusInfo.checked = checked;
        exampleStatusInfo.urlParam = exampleStatusUrlParam;
        facetExampleStatuses.add(exampleStatusInfo);
    }
}
context.facetExampleTypes = facetExampleTypes;
context.facetExampleStatuses = facetExampleStatuses;

3. Implement SearchExamplesByFilters.groovy(Filter search results based on selection) included in FindExampleSolr screen’s action tag at the mentioned location(component://example/webapp/example/WEB-INF/actions/):

import org.apache.solr.client.solrj.impl.HttpSolrServer;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.response.QueryResponse;

import org.ofbiz.entity.util.EntityUtil;
import org.ofbiz.example.ExampleServices;

HttpSolrServer server = new HttpSolrServer(ExampleServices.getSolrHost(delegator, "examples"));
SolrQuery query = new SolrQuery();

// filtered queries on facets.
keyword = parameters.keyword?.trim() ?: "*";
viewSize = Integer.valueOf(context.viewSize ?: 30);
viewIndex = Integer.valueOf(context.viewIndex ?: 0);

query.setParam("q", "*:*");
query.addFilterQuery("fullText:*" + keyword + "*");
queryString = "";

if(parameters.keyword){
    completeUrl = "keyword="+ keyword + "&exampleTypeId=";
} else {
    completeUrl = "exampleTypeId=";
}

if(parameters.exampleType) {
    searchedExampleTypesString = parameters.exampleType;
    searchedExampleTypes = searchedExampleTypesString.split(":");
    searchedExampleTypes.each { exampleType ->
        if(exampleType) {
            if(queryString) {
                queryString = queryString + " OR " + exampleType;
            } else {
                queryString = exampleType;
            }
            completeUrl = completeUrl + ":" + exampleType;
        }
    }
}

if (queryString){
    query.addFilterQuery("exampleTypeId:(" + queryString + ")");
}

statusQueryString = "";
if(parameters.statusId){
    searchedStatusesString = parameters.statusId;
    searchedStatuses = searchedStatusesString.split(":");
    searchedStatuses.each { status ->
        if(status) {
            if(statusQueryString) {
                statusQueryString = statusQueryString + " OR " + status;
            } else {
                statusQueryString = status;
            }
            completeUrl = completeUrl + "&statusId=" + status;
        }
    }
}
if (statusQueryString){
    query.addFilterQuery("statusId:(" + statusQueryString + ")");
}
QueryRequest qryReq = new QueryRequest(query, SolrRequest.METHOD.POST);
QueryResponse rsp = qryReq.process(server);
listSize = Integer.valueOf(rsp.getResults().getNumFound().toString());

query.setRows(viewSize);
query.setStart(0);
qryReq = new QueryRequest(query, SolrRequest.METHOD.POST);
rsp = qryReq.process(server);
SolrDocumentList docs = rsp.getResults();

exampleInfoList = [] as Set;
for (SolrDocument doc : docs) {
    exampleInfoList.add(["exampleId": (String) doc.get("exampleId"), "statusId": (String) doc.get("statusId"),"exampleTypeId": (String) doc.get("exampleTypeId"), "exampleName": (String) doc.get("exampleName")]);
}
exampleDetailList = [];
exampleInfoList.each { exampleInfo ->
    example = EntityUtil.getFirst(delegator.findByAnd("Example", [exampleId: exampleInfo.exampleId], null, true));
    if (example) {
        exampleDetailList.add(["exampleId": exampleInfo.exampleId, "exampleName": exampleInfo.exampleName]);
    }
}
context.exampleDetailList = exampleDetailList;
context.completeUrl = completeUrl;
context.viewIndex = viewIndex;
context.viewSize = viewSize;
context.lowIndex = 0;
context.listSize = listSize;

4. Implement the FindExample.ftl included in the FindExampleSolr screen’s left column at location component://example/webapp/example/example/ This will be used to show the search box and facets for Example Type and Status.

<div>
  <form method="get" action="<@ofbizUrl>FindExampleSolr</@ofbizUrl>">
    <input type="hidden" name="showAll" value="Y" />
    <fieldset>
      <div>
        <input type="text" class="form-control" value="${parameters.keyword?if_exists}" name="keyword" placeholder="${uiLabelMap.CommonSearch}"/>
      </div>
    </fieldset>
    <input type="submit" value="Search"/>
  </form>
  <#if facetExampleTypes?has_content>
    <div>
      <h3><span>${uiLabelMap.CommonType}</span><#if parameters.exampleType?has_content><a href="<@ofbizUrl>FindExampleSolr</@ofbizUrl>?${clearExampleTypeUrl}"><span> (${uiLabelMap.CommonClear})</span></a></#if></h3>
    </div>
    <div>
      <#list facetExampleTypes as exampleType>
        <div>
          <label for="exampleType">
            <input type="checkbox" value="${exampleType.exampleTypeId}"<#if parameters.exampleType?has_content><#if exampleType.checked!>checked</#if></#if>>
            <a href="<@ofbizUrl>FindExampleSolr</@ofbizUrl>?${exampleType.urlParam}">
              <#if exampleType.description?exists>${exampleType.description}<#else>${exampleType.exampleTypeId}</#if> (${exampleType.exampleTypeCount?if_exists})
            </a>
          </label>
        </div>
      </#list>
    </div>
  </#if>
  <#if facetExampleStatuses?has_content>
    <div>
      <h3><span>${uiLabelMap.CommonStatus}</span><#if parameters.statusId?has_content><a href="<@ofbizUrl>FindExampleSolr</@ofbizUrl>?${clearStatusUrl}"><span> (${uiLabelMap.CommonClear})</span></a></#if></h3>
    </div>
    <div>
      <#list facetExampleStatuses as exampleStatus>
        <div class="checkbox">
          <label for="exampleStatus">
            <input type="checkbox" value="${exampleStatus.statusId}"<#if parameters.statusId?has_content><#if exampleStatus.checked!>checked</#if></#if>>
            <a href="<@ofbizUrl>FindExampleSolr</@ofbizUrl>?${exampleStatus.urlParam}">
              <#if exampleStatus.statusDesc?exists>${exampleStatus.statusDesc}<#else>${exampleStatus.statusId}</#if> (${exampleStatus.exampleStatusCount?if_exists})
            </a>
          </label>
        </div>
      </#list>
    </div>
  </#if>
</div>

 

5. Now implement ListFilteredExamples.ftl included in your FindExampleSolr screen at location component://example/webapp/example/example/ This file will be used to show the search results:

<#if exampleDetailList?has_content>
  <#list exampleDetailList as example>
    <ul>
      <li>
        <a class="asmListItemLabel" href="<@ofbizUrl>EditExample?exampleId=${example.exampleId!}</@ofbizUrl>" target="_blank">
          <label>${example.exampleName!(example.exampleId!)}</label>
        </a>
      </li>
    <ul>
  </#list>
<#else>
  <h2>No results found<h2>
</#if>

6. Make the new request and view mapping entry in the example app controller.xml as:

....
....
<request-map uri="FindExampleSolr"><security https="true" auth="true"/><response name="success" type="view" value="FindExampleSolr"/></request-map>
....
....
<view-map name="FindExampleSolr" type="screen" page="component://example/widget/example/ExampleScreens.xml#FindExampleSolr"/>
....
....

 

7. Add a new menu item for accessing this screen with title “Solr Search Example” in $ OFBIZ_HOME/hot-deploy/example/widget/example/ExampleMenus.xml under ExampleAppBar as:

....
....
<menu-item name="FindExampleSolr" title="Solr Search Example">
    <link target="FindExampleSolr"/>
</menu-item>
....
.....

8. Now let’s test the Solr search implementation done for OFBiz. Direct your browser to https://localhost:8443/example/control/FindExampleSolr to test it. Make sure the Solr server is running as well. The UI for this implementation should look like:
Solr Enterprise Search In Action with Apache OFBiz

Now you can play with it further and do some experiments and enhancements, or you can start implementing Solr search on Products, Customers or Orders. As you move further along to defining a new core document schema, you will need to have a thorough understanding of the search requirements from a buisness aspect. Review the OFBiz data model carefully prior to making decisions on defining the document Schema, because this is what is indexed and searched.

Learn more from Apache Solr Enterprise Search Platform Space.

Keep Learning…


DATE: Jul 03, 2014
AUTHOR: Pranay Pandey
OFBiz Tutorials, Enterprise Search, OFBiz