Code Samples to Create Similarity Query Prefix Handlers

The standard use of #attrsimilar is inside a query template using the ELLQL language. For advanced Exalead CloudView users who want to manage similarity queries in UQL, you can adapt the following code samples.

See Also
Configure the Index for Similarity Queries
Use the #attrsimilar Function in the Search API

To create your similarity query prefix handler, adapt the following code samples to your use case and package your custom prefix handler as a CVPlugin. For more information, see Packaging Custom Components as Plugins in the Exalead CloudView Programmer's Guide.

Code for simple attrsimilar prefix handler (SimpleAttrSimilarPrefixHandler.java)

package com.exalead.example.search;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import com.exalead.mercury.component.CVComponent;
import com.exalead.mercury.component.config.CVComponentConfigClass;
import com.exalead.search.query.QueryContext;
import com.exalead.search.query.QueryProcessingException;
import com.exalead.search.query.node.AttrSimilar;
import com.exalead.search.query.node.If;
import com.exalead.search.query.node.IndexOptions;
import com.exalead.search.query.node.Node;
import com.exalead.search.query.node.NodeVisitor;
import com.exalead.search.query.node.PrefixNode;
import com.exalead.search.query.node.UserQueryChunk;
import com.exalead.search.query.prefix.CustomPrefixHandler;
import com.exalead.search.query.util.LongOrDouble;

@CVComponentConfigClass(configClass=SimpleAttrSimilarPrefixHandlerConfig.class)
public class SimpleAttrSimilarPrefixHandler extends CustomPrefixHandler implements
	CVComponent {
   private static final Logger log = Logger.getLogger(SimpleAttrSimilarPrefixHandler.class);
   private final SimpleAttrSimilarPrefixHandlerConfig config;
public SimpleAttrSimilarPrefixHandler(SimpleAttrSimilarPrefixHandlerConfig config) {
	super(config);
	this.config = config;
}

@Override
public Node handlePrefix(Phase phase, PrefixNode node,
		NodeVisitor parentVisitor, QueryContext queryContext)
		throws QueryProcessingException {
	if(phase == Phase.POST_PARSE){
		if (node.content instanceof UserQueryChunk) {
			UserQueryChunk uqc = (UserQueryChunk) node.content;
			String[] tokens = uqc.value.split(",");
			IndexOptions options = new IndexOptions();
			LongOrDouble filterValue = null;
						
			String signatureField = config.getIndexField();
     String signatureContext = null;
     String function = config.getDistance();
                
     //let's parse node options to override config ones   
     if(uqc.indexOptions != null && uqc.indexOptions.getRawOptions() != null){
       for(Map.Entry<String, String> entry : uqc.indexOptions.getRawOptions().entrySet()){
         if("filter_value=".equals(entry.getKey())){
           filterValue = new LongOrDouble(Double.parseDouble(entry.getValue()));
         } else if("index_field=".equals(entry.getKey())){
           signatureField = entry.getValue();
         } else if("function=".equals(entry.getKey())){
             function = entry.getValue();
         } else {
             options.addRawOptions(entry.getKey(), entry.getValue());
         }
       }
     }
                
          if(filterValue == null && config.getFilterValue() != null){
            filterValue = new LongOrDouble(config.getFilterValue());
               }
               queryContext.query.hitOrder.clone();
			   options.addRawOptions("function=", function);
                
               String vfName = tokens[0];
               List<LongOrDouble> signature = parseSignature(tokens[1]);

               if(signatureField != null && signatureField.contains("@")){
                   String[] signatureFieldTokens = signatureField.split("@");
                   signatureField = signatureFieldTokens[1];
                   signatureContext = signatureFieldTokens[0];
               }
                
			return createAttrSimilarNode(vfName, options, signatureField, signatureContext, 
     signature, filterValue);
		}
	}
	return node;
}
	
private Node createAttrSimilarNode(String vfName, IndexOptions options,
	     String signatureField, String signatureContext,List<LongOrDouble> signature, 
      LongOrDouble filterValue) throws QueryProcessingException{
	   Node res = null;
	   options.addRawOptions("name=",vfName);
	   if(signatureField == null){
	     log.error("Missing signature field config or option");
	     throw new QueryProcessingException("Missing signature field config or option");
	   }
	   //the #attrsimilar node
	   res = new AttrSimilar( signatureField, signatureContext, signature, null, null, null, options);
            
       if(filterValue != null){
         //here we create the surrounding #filter node
         String filter = "@"+vfName+".value>="+filterValue.toString();
         IndexOptions opts = new IndexOptions();
            res = new If(res,filter, opts);
       }
       return res;
}
	
/**
	* 
	* @param signature a space-separated list of double
	* @return The List<LongOrDouble> containing the signature
	*/
private List<LongOrDouble> parseSignature(String signature){
	String[] tokens = signature.trim().split(" ");
	List<LongOrDouble> res = new ArrayList<LongOrDouble>(tokens.length);
	for(int i = 0; i<tokens.length; i++){
		res.add(new LongOrDouble(Double.parseDouble(tokens[i])));
	}
	return res; 
 }

}

Code for simple attrsimilar prefix handler configuration (SimpleAttrSimilarPrefixHandlerConfig.java)

package com.exalead.example.search;

import com.exalead.config.bean.IsHidden;
import com.exalead.config.bean.IsMandatory;
import com.exalead.config.bean.PropertyDescription;
import com.exalead.mercury.component.config.CVComponentConfig;
import com.exalead.search.query.util.LongOrDouble;

public class SimpleAttrSimilarPrefixHandlerConfig implements
		CVComponentConfig {

	public SimpleAttrSimilarPrefixHandlerConfig() {
	}

	private String indexField;
	private String distance = "manhattan_normed";
	private LongOrDouble filterValue;
	
   @IsMandatory(false)
   @PropertyDescription("The binary index field that contains the signatures."
     + "If it's a dynamic field (multi valued and storing meta names) " 
     + "use the following syntax: signatureName@indexFieldName. "
     + "This value can be overridden in query option \"index_field\".")
	public void setIndexField(String indexField) {
		this.indexField = indexField;
	}
	
	public String getIndexField() {
		return indexField;
	}
	
	@PropertyDescription("The distance function to use. "
     + "Some possible values: manhattan, manhattan_normed, euclidian, "
     + "euclidian_normed, cosine. "
	    + "Normed versions of the distance must be used when the signatures "
     + "in the index have not been normed before indexing. "
     + "This value can be overridden in query option \"function\".")
    public void setDistance(String distance) {
		this.distance = distance;
	}
	
	public String getDistance() {
		return distance;
	}
	
	
	@IsMandatory(false)
	@PropertyDescription("The minimum similarity score that a hit must have "
     + "to match the query. "
	    + "This value will generally between 0 and 1. "
     + "It can be overridden in query option \"filter_value\". "
	    + "If empty, there will be no filtering based on the score.")
	public void setFilterValue(Double filterValue) {
        this.filterValue = new LongOrDouble(filterValue);
    }
	
	public Double getFilterValue() {
	    if(filterValue == null){
	        return null;
	    }
        return filterValue.getDouble();
    }
	
	@IsHidden
	public LongOrDouble getLongOrDoubleFilterValue() {
        return filterValue;
    }
		
}