Search non-catalog data

You can index and search data that is not part of the product catalog.

For example, you may want the Help page on your site to be included in a search. If a customer searches for “help”, the Help page is returned along with any products that match. Note the Help page will be returned in addition to the matching product catalog results.

To index and search additional data, you must take the following steps:

  1. Set up the search attributes by performing the following:
    • Add attributes using Attributes API

      The properties/dimensions used for adding records to newly created record collections, are either already defined in the application or can be imported manually using /gsadmin/v1 rest end-point.

    • Add attributes to Search interface using Search Interface API
    • Create page definition with record filter
  2. Update the index by taking the following steps:
    • Upload records using the Search Data API endpoints
    • Manually Trigger Baseline/Partial update
  3. Query the index by doing the following:

    Make request to new page.

Create a data record collection

You use REST endpoints exposed at /gsdata/v1/cloud/data/{recordCollection} to create / populate a data record collection.

POST /gsdata/v1/cloud/data/{recordCollection}
Content-Type: application/vnd.oracle.resource+json; type=collection - if creating/updating bulk of records
<json data>
  1. recordCollection - A valid record collection name, as defined by the regular expression
  2. [a-zA-Z][a-zA-Z0-9_-]*. CAS has a limitation that the recordStore name cannot have more than 128 characters.

Example: Import records to the record collection

You can use the following call to import the specified records to the record collection.

POST /gsdata/v1/cloud/data/storeLocations
"Content-Type: application/vnd.oracle.resource+json; type=collection"
{
  links :[{
    "rel" : "self",
    "href" : "/gsdata/v1/cloud/data/storeLocations"
  }],
  items : [
    {
      "record.action": "deleteAll"
    },
    {
      links :[{
        "rel" : "self",
        "href" : "/gsdata/v1/cloud/data/storeLocations/store1000"
      }],
      "record.id": "store1000",
      "record.action": "upsert",
      "store.state": "CA",
      "store.amenities": [
        "Coffee Shop",
        "Pharmacy"
      ]
    },
    {
      "record.action": "delete",
      "record.id": "store1011"
    },
    ...
  ]
}

Use Case: Search buying guides

For the sample store, Company A has a range of cameras and camcorders for sale. To help their customers find the right camera, they have some buying guides that walk through the features, and they want to include summary information on this so the information are returned along with the products when relevant.

  1. Call the gsdata endpoint to create the new buying guide items.

    The following are sample data representing two of these buying guides:

    {
    	    "content.type": "buyingGuide",
    		"guides.department": "Electrical",
    		"guides.productType": "DSLR Cameras",
    		"guides.keywords": "DSLR, Camera, Professional, Zoom, interchangeable lens",
    		"record.locale": "en-US",
    		"record.id": "guide1001",
    		"guides.description": "Looking for more information on our DSLR cameras?  Read on to learn how to pick the right camera for you."
    	},
    	{
    	    "content.type": "buyingGuide",
    		"guides.department": "Electrical",
    		"guides.productType": "Compact Cameras",
    		"guides.keywords": "compact, camera, holiday, small, point-and-shoot, automatic",
    		"record.locale": "en-US",
    		"record.id": "guide1002",
    		"guides.description": "Looking for a compact camera to take on holiday?  We compare the best compacts!"
    	}
    First they upload the following JSON to the server using the new gsdata endpoint. They use POST to create a new resource, and use the syntax of /gsdata/v1/cloud/data/buyingGuides, where buyingGuides is the name for the new entry.
    POST /gsdata/v1/cloud/data/buyingGuides
    {
    	"items": [{
    	    "content.type": "buyingGuide",
    		"guides.department": "Electrical",
    		"guides.productType": "DSLR Cameras",
    		"guides.keywords": "DSLR, Camera, Professional, Zoom, interchangeable lens",
    		"record.locale": "en-US",
    		"record.id": "guide1001",
    		"guides.description": "Looking for more information on our DSLR cameras?  Read on to learn how to pick the right camera for you."
    	},
    	{
    	    "content.type": "buyingGuide",
    		"guides.department": "Electrical",
    		"guides.productType": "Compact Cameras",		"guides.keywords": "compact, camera, holiday, small, point-and-shoot, automatic",
    		"record.locale": "en-US",
    		"record.id": "guide1002",
    		"guides.description": "Looking for a compact camera to take on holiday?  We compare the best compacts!"
    	},
    	{
    		"record.action": "OCCForceFlush"
    	}]
    }
  2. They create new attributes.

    After the data have been created, they define the attributes that were used. To do this, they use the standard Attributes API, and use POST to create new attributes corresponding to the data they uploaded previously. This is the code to create new attributes of:

    • guides.department (as a facet)
    • guides.productType (as a searchable property)
    • guides.description (as a searchable property)
    • content.type (as a filterable property)
    POST /gsadmin/v1/cloud/attributes/system/guides.department
    {
    "ecr:lastModifiedBy": "admin",
    "ecr:type": "dimension",
    "isAutogen": true
    }
    
    POST /gsadmin/v1/cloud/attributes/system/guides.productType
    {
    "ecr:lastModifiedBy": "admin",
    "ecr:type": "property",
    "isRecordSearchEnabled": true
    }
    
    POST /gsadmin/v1/cloud/attributes/system/guides.description
    {
    "ecr:lastModifiedBy": "admin",
    "ecr:type": "property",
    "isRecordSearchEnabled": true
    }
    
    
    POST /gsadmin/v1/cloud/attributes/system/content.type
    {
    "ecr:lastModifiedBy": "admin",
    "ecr:type": "property",
    "isRecordFilterable": true
    }
  3. They create new services using the pages endpoint, and add the new fields as being returned.

    They want to define a separate endpoint that they can call to return this information. This is done using the Pages Admin API. They name the page “buyingGuide”, and define that they want to return four properties:

    guides.description

    guides.productType

    guides.department

    record.id

    POST /gsadmin/v1/cloud/pages/Default/buyingGuide
    {
      "contentType": "Page",
      "ecr:type": "page",
      "contentItem": {
        "@name": "Content Search Service",
        "@type": "GuidedSearchService",
        "@appFilterState": {
          "@type": "FilterState",
          "recordFilters": [
            "content.type:buyingGuide"
          ]
        },
        "navigation": {
          "@type": "NavigationContainer",
          "contentPaths": [
            "/content/facets"
          ]
        },
        "resultsList": {
          "@type": "ResultsList",
    	  "fieldNames": [
                    "guides.description",
                    "guides.productType",
                    "guides.department",
    				"record.id"
                ],
          "rankingRules": {
            "merchRulePaths": [
              "/content/rankingRules"
            ],
            "systemRulePaths": [
              "/content/system/rankingRules"
            ],
            "systemRuleLimit": 10
          }
        },
        "searchAdjustments": {
          "@type": "SearchAdjustments"
        }
      }
    }
  4. Add the properties created above to the “All” search interface.
  5. Add the searchable attributes they have just added to the “All” searchable field ranking. To do this:
    1. Open the Oracle Commerce administration interface.
    2. Click to open the Search section.
    3. Click Searchable Field Rankings.
    4. Click All.
    5. Use the dropdown to add the new attributes to the end of the list, in this order:

      guides.department

      guides.productType

      guides.description

  6. Index the data. They use the following API endpoint to run a search index to update the data:
    POST /ccadmin/v1/search/index?op=baseline

    They monitor the progress of this using:

    GET /ccadmin/v1/search

    When success: true is displayed, the indexing has completed.

  7. Verify that this works correctly. Query the data by calling the following URL on their storefront:
    /ccstore/v1/assembler/pages/Default/services/buyingGuide?Ntt=camera

Create a widget to support searching data

Custom product listing widget can support searching data that is not part of the product catalog. For detailed information about creating widgets, see Create a Widget.

Create the widget structure for the product listing sample widget

Widgets that include user interface elements must include display templates. The following shows an example of the files and directories in a product listing widget.

Widget/
    ext.json
    widget/
        ProductistingForAdditionalContent
            widget.json
            js/
                product-listing.js
            less/
                widget.less
            locales/
            en/
                ns.multicart.json
            templates/
                display.template
                paginationControls.template

The JavaScript code you write extends the createsearchViewModel class. For more information about the widget structure and the contents of the ext.json and widget.json files, see Understand widgets .

Create the JavaScript file for the product listing sample widget

The widget’s JavaScript file includes functions that let shoppers search data that is not part of the product catalog.

The following example shows sample JavaScript that implements the createsearch functionality:

////Non-catalog content
        widget.assemblerPagesPath = "services/storeLocations"; //This should be services/searchService
        widget.ntk = "stores"; //This should point to the search interface created
		//Created a function to process the non-catalog content to display them
        widget.amenitiesToText = function(obj){
          var amenitiestext = obj[0];
          for(var i=1; i < obj.length; i++ ){
                amenitiestext = amenitiestext + ", " +obj[i];
              }
          return amenitiestext;
        };
///////End non-catalog content    	}

Create template files for the product listing sample widget

The widget’s display.template file contains the following code for rendering the page (the following is an excerpt of the display.template):

<!-- ko if: (listingViewModel().display) -->
<div id="CC-productListing" role="alert">
  <!--  ko if: listType() == 'search' -->
  <!--  ko with: listingViewModel -->
  <div class="sr-only" data-bind="text :pageLoadedText"></div>
  <!-- /ko -->
  <!-- /ko -->
  <div id="CC-product-listing-controls" class="row">
    <div class="col-sm-12">
      <!-- ko with: listingViewModel -->
      <!-- ko if: $parent.listType() == 'search' -->
      <h2 id="search-results" class="sr-only" role="alert" data-bind="widgetLocaleText: 'searchResultsText'"></h2>
      <!-- /ko -->
      <!-- ko if: titleText -->
      <div class="row">
        <div class="col-xs-12">
          <h2 id="cc-product-listing-title" data-bind="text: titleText"></h2>
        </div>
      </div>
      <!-- /ko -->
      <!-- ko if: $parent.listType() == 'search' -->
      <!-- ko if: noSearchResultsText -->
      <div class="row">
        <div id="cc-productlisting-noSearchResults" class="col-xs-12">
          <span data-bind="text: noSearchResultsText"></span>
        </div>
      </div>
      <!-- /ko -->
      <!-- ko if: suggestedSearches().length > 0 -->
      <div id="cc-productlisting-didYouMean">
        <span data-bind="widgetLocaleText:'didYouMeanText'"></span>
        <div id="cc-productlisting-didYouMeanTerms" data-bind="foreach : suggestedSearches">
          <a data-bind="attr: {id: 'cc-productlisting-didYouMean-Suggestion-'+$index()}, widgetLocaleText: {value:'dYMTermAriaLabel', attr:'aria-label'}, click: $data.clickSuggestion" href="#">
            <!-- ko if: ( $index() < ($parent.suggestedSearches().length - 1)) -->
            <span data-bind="widgetLocaleText : {value:'dYMTermTextHasNext', attr:'innerText', params: {label: $data.label}}"></span>
            <!-- /ko -->
            <!-- ko if: ( $index() == ($parent.suggestedSearches().length - 1)) -->
            <span data-bind="widgetLocaleText : {value:'dYMTermText', attr:'innerText', params: {label: $data.label}}"></span>
            <!-- /ko -->
          </a>
        </div>
      </div>
      <!-- /ko -->
Pro      <!-- /ko -->
      <!-- /ko -->
      <!-- ko if: (listingViewModel().totalNumber() > 0) -->
      <div data-bind="text: resultsText" class="sr-only" role="alert"></div>
      <!-- ko if: listType() == 'search' -->
      <h3 class="sr-only" role="alert" data-bind="widgetLocaleText: 'viewingOptionsText'"></h3>
      <!-- /ko -->
      <div class="row">
        <div class="col-sm-12" id="cc-area-controls">
          <div class="row">

The widget’s display.template calls another template file, paginationControls.template. This template file contains the following code for rendering multiple pages when the list of products is long:

<div class="btn-group">
    <a class="btn btn-default" data-bind="ccNavigation: '',  attr : {href: firstPage()}, widgetLocaleText : {value:'goToFirstPageText', attr:'aria-label'}, css: { disabled: $data.currentPage() == 1 }" ><span data-bind="widgetLocaleText: 'goToFirstPagePaginationSymbol'"></span></a>
    <a href="#" class="btn btn-default" data-bind="ccNavigation: '', attr: {href: previousPage()}, widgetLocaleText : {value:'goToPreviousPageText', attr:'aria-label'}, css: { disabled: $data.currentPage() == 1 }" rel="prev"><span data-bind="widgetLocaleText: 'goToPreviousPagePaginationSymbol'"></span></a>

    <!-- ko foreach: pages -->
      <a href="#" class="btn btn-default" data-bind="ccNavigation: '', attr: {href: $parent.goToPage($data)}, css: {active: $data.pageNumber===$parent.currentPage() }">
      <!-- ko if: $data.selected === true -->
          <span class="sr-only" data-bind="widgetLocaleText : 'activePageText'"></span>
        <!-- /ko -->
        <!-- ko if: $data.selected === false -->
          <span class="sr-only" data-bind="widgetLocaleText : 'goToPageText'"></span>
        <!-- /ko -->
        <span data-bind="ccNumber: $data.pageNumber"></span>
       </a>
      <!-- /ko -->

    <a href="#" class="btn btn-default" data-bind="ccNavigation: '', attr: {href: nextPage()}, widgetLocaleText : {value:'goToNextPageText', attr:'aria-label'}, css: { disabled: currentPage() == $data.totalNumberOfPages() }" rel="next"><span data-bind="widgetLocaleText: 'goToNextPagePaginationSymbol'"></span></a>
    <a href="#" class="btn btn-default" data-bind="ccNavigation: '', attr: {href: lastPage()}, widgetLocaleText : {value:'goToLastPageText', attr:'aria-label'}, css: { disabled: currentPage() == $data.totalNumberOfPages() }"><span data-bind="widgetLocaleText: 'goToLastPagePaginationSymbol'"></span></a>

</div>