Advanced Item Search Manager

Advanced Item Search Manager

The advanced item search manager (AISM) gives the developer more control over how the search indexes are traversed.  This document will describe the AISM methods and provide some examples.

The first time you search on your UltraCart account it will return zero results.  A background process will build the underlying indexes of your items within 30 minutes.

 

We're going to walk you through creating a search page for your catalog.  

  • It's going to be complex, but it's necessarily complex.   
  • Take it slow, and read the source code comments.   
  • When changing velocity code, make small changes, test, repeat.  
  • Changing a large chunk of velocity code all at once is a recipe for frustration.

 

In our example, I'll be using some sample data from an UltraCart merchant, Jon from Performance Systems Integration, who was one of the first merchants to use the advanced item search.

Steps

  1. Create your item attributes
  2. Create your catalog template
  3. Map the template to your desired url.

 

Step 1: Create Item Attributes

Jon started with a matrix listing out the attributes relating to each item in one of his product categories Harnesses (automotive parts).  The matrix contained almost four dozen items and seven attributes.  

 

From that matrix, Jon created a spreadsheet (psi_attributes.csv) that could be imported into UltraCart, defining catalog attributes for each item according to the matrix above, which he saved as a .csv file.

 

The item import may be found here:

Home → Items → Batch Item Import

By using the spreadsheet column headers he did, Jon easily imported the file because the import wizard was able to automatically match the attribute columns to the proper field names.   Sure beats manually assigning 20 columns.

 

A cursory review of some items confirms the attributes were successfully loaded into the system.

 

 

Step 2: Create Catalog Template

The next step is to create the template file.

Navigate to:

Home → Catalog → Manage Catalog Templates

and create a new template file.

 

At first glance of the code below, you might be a little overwhelmed. There's quite a few looping blocks, which can lead one to quickly become lost.

Here's what you need to do if you wish to use the code below:

  1. Line 43: Change $attributeNames to be a list of your attributes.
  2. Line 53: Change $attributeChoices to contain your attributes and possible choices.
  3. Line 67: Change $attributeSelections to also be a list of your attributes

Also:

  1. The outputting of the results at the end is done with a simple table (lines 207-215). After that there is some commented out code (lines 216-251) showing a more complex search results.
  2. Change the Look & Feel (and title). The search code has BEGIN and END comments to help you find where it starts and ends. The stuff outside of it is just markup. Change it to whatever you need.

That's it. At the minimum, changing the values of just 3 variables should make the code below work for you.

 

Here is the file (also attached: harness_search.vm)   It is heavily commented, so please read through it carefully.

Advanced Item Search Catalog Screen
## the following comments help smart editors display velocity syntax help.
## you don't have to have them in your file, but if UltraCart developers get involved, they'll add them.
## UltraCart uses Intellij.  Only the premium (pricey) version has velocity support, but it is *AMAZING*.
#* @vtlvariable name="formatHelper" type="com.bpsinfo.ultracart.catalog.tobjects.FormatHelper" *#
#* @vtlvariable name="baseUrl" type="java.lang.String" *#
#* @vtlvariable name="group" type="com.bpsinfo.ultracart.catalog.tobjects.GroupImpl" *#
#* @vtlvariable name="advancedItemSearchManager" type="com.bpsinfo.ultracart.catalog.tobjects.advitemsearch.AdvancedItemSearchManager" *#
#* @vtlvariable name="parameters" type="java.util.HashMap<java.lang.String,java.lang.String>" *#
#* @vtlvariable name="parameters2" type="java.util.HashMap<java.lang.String,java.util.List<java.lang.String>>" *#
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
## TODO - change this to your own title...
  <title>Harness Search - PSI</title>
  <style type='text/css'>
    body {
      font-family: verdana, serif;
      font-size: 0.8em;
    }
    .search_value {
      width: 180px;
      display: inline-block;
      padding: 5px 0;
    }
    .search_category {
      /* float:left; */
      width: 180px;
    }
    .search_header {
      font-weight: bold;
    }

  </style>
  <!-- BEGIN SEARCH CODE -->
## attributeNames will be used whenever I need to iterate over the list of possible attributes, such as when
## I'm creating all the checkboxes.  You'll notice the two variables below these one are hash tables, which
## do not iterate well (ordering is indeterminate), so I'll use attributeNames as the looper whenever I need to
## use attributeChoices and attributeSelections.
## this variable is a built-in list.
## Beneath the hood, velocity uses a "List" variable for built-in lists.  So here's a documentation page on
## how you may use this variable:  http://docs.oracle.com/javase/6/docs/api/java/util/List.html
  #set($attributeNames = [
  'Crank',
  'Throttle',
  'Transmission Type',
  'Intake Type',
  'Alternator',
  'MAF',
  'CAM'])
## attributeChoices list all of the *possible* choices.  It is used to generate all the checkboxes
## this variable is a hash table of lists ( key = attribute name, value = list of possible choices))
  #set($attributeChoices = {
  'Crank': ['24X','58X'],
  'Throttle': ['DBC','DBW'],
  'Transmission Type': ['4L60E/4L80E','6L80E','T56'],
  'Intake Type':['LS1/LS6','LS2/LS3','Vortec'],
  'Alternator':['Corvette','Truck'],
  'MAF':['LS2','LS3/LS7','07/08 Truck','09 Truck'],
  'CAM':['VTT','No VVT']
  })
## attributeSelections is a hash table of lists.  It contains all of the user's selections.  This variable is
## updated during the routine for determining if a checkbox is checked, and then used later when constructing the
## advanced item search.  It's kind of clunky how this list is populated below, but I wanted to avoid doing a triple
## loop (for each selection ... loop through each possible value ... loop through each http post parameter submitted)
## To avoid the triple, I opportunistically populate it below.
  #set($attributeSelections = {
  'Crank': [],
  'Throttle': [],
  'Transmission Type': [],
  'Intake Type':[],
  'Alternator':[],
  'MAF':[],
  'CAM':[]
  })
</head>
<body>
## I've floated the search to the left, which seems to be a popular place to put it.
<div style='float:left;width:200px;min-height: 100%;margin-right:10px'>
## notice, no action attribute on the form so that it posts back to the document url
  <form method='get'>
  ## if the clear search button was used, set a flag so I can avoid re-checking any checkboxes, effectively clearing them.
    #set($clearForm = false)
  ## the if clause below is a very common way to test for 'null' values.
  ## 1. put the variable inside double quotes.
  ## 2. use the exclamation mark, which prints an empty string "" if the variable doesn't exist or is null
  ## 3. compare that result to an empty string.  If they're equal, the value is null or non-existent.
    #if("$!parameters.get('clearButton')" != "")
      #set($clearForm = true)
    #end
    <input type="text"
           style='width:180px;'
           name="free_search"
           value="$!{parameters.get('free_search')}"/><br>
  ## I have the two buttons right next to each other to make them fix.  you may wish to adjust.
    <input type='submit'
           id="clearButton"
           name="clearButton"
           value='Clear Selections'/><input type='submit'
                                            id="searchButton"
                                            value='Search'/>
    <br>
  ## =========================================================================
  ## begin checkbox code
    #foreach($attr in $attributeNames)
      <div class='search_category'>
        <span class='search_value search_header'>$attr</span>
        #set($attrValues = $attributeChoices.get($attr))
        #foreach($val in $attrValues)
        ## find out if this value is checked by comparing the value with every attr_search parameter submitted.
          #set($paramList = $parameters2.get('attr_search')) ## notice parameters2 (returns list of values)
          #set($valChecked = false)
          #if("$!paramList" != "" && $clearForm == false) ## this is a pretty safe way to see if the paramList is null or not.  don't want to iterate with null object...
            #foreach($param in $paramList)
            ## each checkbox value is a combination of attribute name + values concatenated with a tilde.  To see if a parameter
            ## was submitted, compare the post parameter with the concatenated key string.
            ## if there is a match, I want to do two things.
            ## 1. add the value to my selections variable so I can include it in the advanced search.
            ## 2. set the checkbox to 'checked' so I maintain my state with each page request.
              #if($param == "${attr}~${val}")
              ## add this value to the selected hash of lists which will be used to do the actual search.
              ## first, get a reference to the list of selected values for this attribute
                #set($sel = $attributeSelections.get($attr))
              ## make sure the search attribute exists.  it always should, but this prevents unforeseen change breaks.
              ## unforeseen, such as, you rename an attribute or delete one without properly updating this page.
                #if("$!sel" != "")
                ## it doesn't matter where I put a value in the list
                ## I don't need to put the value at the front, but the method add(pos,element)
                ## returns void, which is preferred to add(element) which returns true/false
                ## If I use just plain add(), I'll print a bunch of 'true' across the page
                  $sel.add(0, $val)
                #end
              ## set the flag so the checkbox is checked.
                #set($valChecked = true)
              #end
            #end ## foreach attr_search parameter
          #end ## if paramList is not null
          <span class='search_value'>
              <label><input type="checkbox"
                            name="attr_search"
                            value="${attr}~$val"
                #if($valChecked) checked='checked' #{end}/> $val</label></span>
        #end ## end-foreach val in attrValues
      </div>
    #end ## end-foreach attr in attributeNames
  ## end checkbox code
  ## =========================================================================

  </form>
</div>

<div style='float:left'>

## perform the search and display the results.
## ALWAYS GOOD TO RESET JUST TO BE SAFE
  $advancedItemSearchManager.resetManager()
  #foreach($attrName in $attributeSelections.keySet())  ## keySet() docs: http://docs.oracle.com/javase/6/docs/api/java/util/Map.html
  ## get a reference to the list of selected/checked values for this attribute
    #set($searchValues = $attributeSelections.get($attrName))
    #if($searchValues && $searchValues.size() > 0) ## if there are any selections for this attribute
    ## always end in .noop() call to avoid printing junk to the screen
    ## notice that I'm using "and()" below.  If you wished to create a search where *any* checked values returned
    ## a hit, you'd want to use "or()" instead.
      $advancedItemSearchManager.and().resetModifiers().required(true).openGroup().noop()
      #foreach ($searchValue in $searchValues)
        $advancedItemSearchManager.attribute($attrName, $searchValue).noop()
      #end
      $advancedItemSearchManager.closeGroup().noop()
    #end
  #end

## here's price range logic if you wish to include it in your search
## PRICE
##  #if ($selectedPriceslength > 0)
##    $advancedItemSearchManager.and().resetModifiers().required(true).openGroup().noop()
##    #foreach ($selectedPrice in $selectedPrices)
##      #if ($selectedPrice == "0-100")
##        $advancedItemSearchManager.costBetween(0, 100).noop()
##      #elseif ($selectedPrice == "101-200")
##        $advancedItemSearchManager.costBetween(100.01, 200).noop()
##      #elseif ($selectedPrice == "201-300")
##        $advancedItemSearchManager.costBetween(200.01, 300).noop()
##      #elseif ($selectedPrice == "301-400")
##        $advancedItemSearchManager.costBetween(300.01, 400).noop()
##      #elseif ($selectedPrice == "401-500")
##        $advancedItemSearchManager.costBetween(400.01, 500).noop()
##      #end
##    #end
##    $advancedItemSearchManager.closeGroup().noop()
##  #end
## SEARCH FIELD - search over the major fields using the search text box value.
  #set($searchTerm = $!{parameters.get('free_search')})
  #if ("$!searchTerm" != "")
    $advancedItemSearchManager.and().resetModifiers().required(true).openGroup().noop()
    $advancedItemSearchManager.searchEachWord(true).noop()
    $advancedItemSearchManager.or().attributes($searchTerm).noop()
    $advancedItemSearchManager.or().barcode($searchTerm).noop()
    $advancedItemSearchManager.or().description($searchTerm).noop()
    $advancedItemSearchManager.or().extendedDescription($searchTerm).noop()
    $advancedItemSearchManager.or().itemId($searchTerm).noop()
    $advancedItemSearchManager.or().manufacturerName($searchTerm).noop()
    $advancedItemSearchManager.or().manufacturerSKU($searchTerm).noop()
    $advancedItemSearchManager.closeGroup().noop()
  #end
  #set($results = $advancedItemSearchManager.search(1, 50))
## Simple output.
<table>
  #foreach ($record in $results.getPageRecords())
    <tr>
      <td>$record.getItemId()</td>
      <td>$record.getDescription()</td>
      <td>$record.getScore()</td>
    </tr>
  #end
</table>
  ## More complex outputting.  Uses some custom PSI snippet includes, so examine carefully before trying to use.
##  #if($results.getPageRecordCount() == 0)
##  ## No items found message here.
##  #else
##
##    #set($groupItems = [])
##    #foreach($rec in $results.getPageRecords())
##      #set($searchItem = $group.getItem($rec.getItemId()))
##    ## sanity check - make sure the item can be retrieved completely -- don't blindly add it to groupItems.
##      #if("$!searchItem" != "")
##        #set($foo = $groupItems.add($group.getItem($rec.getItemId()))) ## set result to foo to avoid outputting 'true'
##      #end
##    #end ##foreach records in search results.
##
##  ## ============================================
##  ## standard group logic
##  ## ============================================
##
##    #set($groupItemsTable = $formatHelper.getTable($groupItems, 4))
##    #if($groupItems && $groupItems.size() > 0)
##      #foreach($row in $groupItemsTable.getRows())
##        <div class="group_row">
##          #foreach($column in $row.getColumns())
##            #if($column)
##              #set($columnUrl = "${baseUrl}$group.path${column.getMerchantItemID()}.html")
##              #ucTemplate("snip_group-column")
##            #end
##          #end
##        </div>
##        <!--/group_row-->
##      #end
##    #end
##
##
##
##  #end ## if there were records
  <!-- END SEARCH CODE -->
</div>
</body>
</html> 

 

 

 

Step 3: Map template to url

Once the template was created, all that was left was to map it to a url.  This was done in the  Home → Catalog → Manage Catalog Groups

 

 

Reference

Context

The AISM object that you will interact with on your catalog page is referenced by:

 

$advancedItemSearchManager

Methods

The AISM has a large number of methods on it that are used to build up the query and then execute it.  Below are the methods on the object.

Method GroupMethod SignatureNotes
Managementvoid resetManager()Clears all the settings inside of the search manager to prepare for another query.
   
Direct MatchingAdvancedItemSearchManager actualSearch(String actualSearch)Set the text of the actual search query entered by the user to check if they entered a direct item id.
 AdvancedItemSearchManager singleResultDirectMatch(boolean singleResultDirectMatch)If the actual search matches the item id or manufacturer SKU for a result then return a single result.
   
Term ModifiersAdvancedItemSearchManager resetModifiers()Reset the modifiers for the next search term.
 AdvancedItemSearchManager boost(String boost)Set the boost for the search term if there is a match.  Effects the ordering of results.
 AdvancedItemSearchManager fuzzy(Boolean fuzzy)Make search terms fuzzy.
 AdvancedItemSearchManager required(Boolean required)Make the next operation in the search query required
 AdvancedItemSearchManager searchEachWord(boolean searchEachWord)If multiple words are provided in the next term then break them up and search each one.
 

AdvancedItemSearchManager similarity(String similarity)
AdvancedItemSearchManager similarity(double similarity)
AdvancedItemSearchManager similarity(BigDecimal similarity)

Set the similarity for search terms.
 AdvancedItemSearchManager wildcard(Boolean wildcard)Make search terms a wildcard search.
   
TermsAdvancedItemSearchManager attribute(String attributeName, String term)Search a specific attribute for a term.
 AdvancedItemSearchManager attributes(String term)Search all attributes for a term
 

AdvancedItemSearchManager barcode(String term)

Search the barcode field
 

AdvancedItemSearchManager cost(String comparison, String value)
AdvancedItemSearchManager cost(String comparison, double value)
AdvancedItemSearchManager cost(String comparison, BigDecimal value)

Search the cost field.  Valid values for the comparison parameter are:
  • <
  • <=
  • =
  • >=
  • =
 

AdvancedItemSearchManager costBetween(String valueLow, String valueHigh)
AdvancedItemSearchManager costBetween(double valueLow, double valueHigh)
AdvancedItemSearchManager costBetween(BigDecimal valueLow, BigDecimal valueHigh)

Search the cost field to find items between a range.
 

AdvancedItemSearchManager description(String term)

Search the description field
 

AdvancedItemSearchManager extendedDescription(String term)

Search the extended description field.
 

AdvancedItemSearchManager itemId(String term)

Search the item id field.
 AdvancedItemSearchManager manufacturerName(String term)

Search the manufacturer name field.

 AdvancedItemSearchManager manufacturerSKU(String term)Search the manufacturer SKU field.
 

AdvancedItemSearchManager simple(String search)

Performs a "simple" search which is the same behavior that the regular built in search engine of UltraCart uses.
 AdvancedItemSearchManager simple(String search, boolean expanded)Performs a "simple" search which is the same behavior that the regular built in search engine of UltraCart uses, but expand the results further with fuzzy logic.
   
 AdvancedItemSearchManager openGroup()Opens a logic group (think open parenthesis in a programming language)
 AdvancedItemSearchManager closeGroup()Closes a logic group (think closing parenthesis in a programming language)
 AdvancedItemSearchManager and()Separate terms or groups with an AND operation.
 AdvancedItemSearchManager or()Separate terms or groups with an OR operation.
   
Conveniencevoid noop()Consumes the output of the last daisy chained method call to make sure nothing outputs from the Velocity rendering.
   
SearchAdvancedItemSearchResult search()Returns up to 50 results
 AdvancedItemSearchResult search(int pageNumber)Returns specific page number of results with 50 items per page.
 AdvancedItemSearchResult search(int pageNumber, int itemsPerPage)Return specific page number of results with up to 200 items per page.

 

Results

The search methods return an AdvancedItemSearchResult object.  This object contains a child array of AdvancedItemSearchResultRecord which ultimately contains the catalog Item objects.  Below is the object method for each result object.

AdvancedItemSearchResult

MethodNote

String getErrorMessage()

Error message if there was a problem with the query.

int getTotalRecordCount()

Total number of results for the search.

int getPages()

Number of pages in the total result set.

int getPage()

Current page number from the result set.

AdvancedItemSearchResultRecord[] getPageRecords()

Records for this page.

int getPageRecordCount()

Number of records per page.

AdvancedItemSearchResultRecord

MethodNote

String getDescription()

Description of the item
String getItemId()Item ID of the item
float getScore()Score between 0 (lowest) - 1 (highest) of the result
Item getItem()Catalog item object of the item

Sample Queries

In this section we'll look at how to build up some sample queries based a fictional user interface.

Let's break down the user interface and see what searches are have from a high level.

  • The text field box needs to search across item id, description, extended description, manufacturer name, manufacturer SKU, and all attributes.
  • The checkboxes for Color Group, Collection, Construction, and Content search various attributes that are on the item.
  • The price range box filters items to certain price ranges.

Now let's look at what the Velocity looks like to build up the query.

Building the Query
## You will have to parse out the information from the $parameters map object.  That is beyond the scope of this tutorial.
#set ($selectedColorGroups = ["white", "black"])
#set ($selectedCollections = ["classic", "sheer"])
#set ($selectedConstructions = [])
#set ($selectedContents = ["silk"])
#set ($selectedPrices = ["0-100", "101-200"])
#set ($searchTerm = "my search")

## ALWAYS GOOD TO RESET JUST TO BE SAFE
$advancedItemSearchManager.resetManager()
## COLOR GROUP ATTRIBUTE
#if ($selectedColorGroups.length > 0)
  $advancedItemSearchManager.and().resetModifiers().required(true).openGroup().noop()
  #foreach ($selectedColorGroup in $selectedColorGroups)
      $advancedItemSearchManager.attribute("colorGroup", $selectedColorGroup).noop()
  #end
  $advancedItemSearchManager.closeGroup().noop()
#end

## COLLECTION ATTRIBUTE
#if ($selectedCollections.length > 0)
  $advancedItemSearchManager.and().resetModifiers().required(true).openGroup().noop()
  #foreach ($selectedCollection in $selectedCollections)
      $advancedItemSearchManager.attribute("collection", $selectedCollection).noop()
  #end
  $advancedItemSearchManager.closeGroup().noop()
#end

## CONSTRUCTION ATTRIBUTE
#if ($selectedConstructions.length > 0)
  $advancedItemSearchManager.and().resetModifiers().required(true).openGroup().noop()
  #foreach ($selectedConstruction in $selectedConstructions)
      $advancedItemSearchManager.attribute("construction", $selectedConstruction).noop()
  #end
  $advancedItemSearchManager.closeGroup().noop()
#end

## CONTENT ATTRIBUTE
#if ($selectedContents.length > 0)
  $advancedItemSearchManager.and().resetModifiers().required(true).openGroup().noop()
  #foreach ($selectedContent in $selectedContents)
      $advancedItemSearchManager.attribute("content", $selectedContent).noop()
  #end
  $advancedItemSearchManager.closeGroup().noop()
#end

## PRICE
#if ($selectedPriceslength > 0)
  $advancedItemSearchManager.and().resetModifiers().required(true).openGroup().noop()
  #foreach ($selectedPrice in $selectedPrices)
    #if ($selectedPrice == "0-100")
      $advancedItemSearchManager.costBetween(0, 100).noop()
    #elseif ($selectedPrice == "101-200")
      $advancedItemSearchManager.costBetween(100.01, 200).noop()
    #elseif ($selectedPrice == "201-300")
      $advancedItemSearchManager.costBetween(200.01, 300).noop()
    #elseif ($selectedPrice == "301-400")
      $advancedItemSearchManager.costBetween(300.01, 400).noop()
    #elseif ($selectedPrice == "401-500")
      $advancedItemSearchManager.costBetween(400.01, 500).noop()
    #end
  #end
  $advancedItemSearchManager.closeGroup().noop()
#end

## SEARCH FIELD
#if ($searchTerm != "") 
  $advancedItemSearchManager.and().resetModifiers().required(true).openGroup().noop()
  $advancedItemSearchManager.searchEachWord(true).noop()
  $advancedItemSearchManager.or().attributes($searchTerm).noop()
  $advancedItemSearchManager.or().barcode($searchTerm).noop()
  $advancedItemSearchManager.or().description($searchTerm).noop()
  $advancedItemSearchManager.or().extendedDescription($searchTerm).noop()
  $advancedItemSearchManager.or().itemId($searchTerm).noop()
  $advancedItemSearchManager.or().manufacturerName($searchTerm).noop()
  $advancedItemSearchManager.or().manufacturerSKU($searchTerm).noop()  
  $advancedItemSearchManager.closeGroup().noop()
  
#end

Now we need to execute the search and print out the results.

#set($results = $advancedItemSearchManager.search(1, 50))
<table>
#foreach ($record in $results.getPageRecords())
  <tr>
    <td>$record.getItemId()</td>
    <td>$record.getDescription()</td>
    <td>$record.getScore()</td>
  </tr>
#end
</table>

Other things to consider in your coding.

  1. Storing away the search parameters for the subsequent pages of the result set.
  2. Paging through results
  3. Check the error message in the result and displaying it.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Â