Skip to content

Instantly share code, notes, and snippets.

@dav-rob
Created August 27, 2012 09:20
Show Gist options
  • Save dav-rob/3486870 to your computer and use it in GitHub Desktop.
Save dav-rob/3486870 to your computer and use it in GitHub Desktop.
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.elasticsearch.script.AbstractFloatSearchScript;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.NativeScriptFactory;
/**
* This class is run in isolation on the ElasticSearch Server, so can have no dependencies on other classes in CMT.
*
* Custom sorting (and, also I think, custom fields) are defined by passing a score to the Lucene sort collector.
*
* Each document stands alone and doesn't know anything about other document's scores, so a comparator based sort
* is not possible. The way sorting happens is that each document has its own score and then the lucene collector sorts
* according to these float (32 bit) scores.
*
* Although ElasticSearch has these script classes:
*
* AbstractFloatSearchScript
* AbstractDoubleSearchScript
* AbstractLongSearchScript
*
* The return value from all of these is cast to a float when the Score is passed to the Lucene sort collector.
* i.e. the "runAsFloat()" method is called no matter what script type is implemented.
*
* Effectively that means that we only ever have 32 bits of scores to return in order to sort the documents.
*
* In Reality it appears that we have even less than 32 bits of scores to play with: the largest range of value is
*
* 1*2^6 to 63*2^24
* i.e. 2^6 to 2^32
*
* Which is really a range of about 24 bits.
*
* @author drober20
*
*/
public class CustomColumnSorterFactory implements NativeScriptFactory{
private final static Log LOG = LogFactory.getLog(CustomColumnSorterFactory.class);
private static HashMap<Character, Integer> charMap;
static {
HashMap<Character, Integer> charMap = new HashMap<Character, Integer>();
charMap.put('0', 1);
charMap.put('1', 2);
charMap.put('2', 3);
charMap.put('3', 4);
charMap.put('4', 5);
charMap.put('5', 6);
charMap.put('6', 7);
charMap.put('7', 8);
charMap.put('8', 9);
charMap.put('9', 10);
charMap.put('A', 11);
charMap.put('B', 12);
charMap.put('C', 13);
charMap.put('D', 14);
charMap.put('E', 15);
charMap.put('F', 16);
charMap.put('G', 17);
charMap.put('H', 18);
charMap.put('I', 19);
charMap.put('J', 20);
charMap.put('K', 21);
charMap.put('L', 22);
charMap.put('M', 23);
charMap.put('N', 24);
charMap.put('O', 25);
charMap.put('P', 26);
charMap.put('Q', 27);
charMap.put('R', 28);
charMap.put('S', 29);
charMap.put('T', 30);
charMap.put('U', 31);
charMap.put('V', 32);
charMap.put('W', 33);
charMap.put('X', 34);
charMap.put('Y', 35);
charMap.put('Z', 36);
CustomColumnSorterFactory.charMap = charMap;
/*
* We have up to 2^6 -1 = 63 entries, if needed.
*
* charMap.put("1", 0);
charMap.put("1", 0);
*
*/
}
@Override
public ExecutableScript newScript(Map<String, Object> params) {
LOG.info("Last string float value = " + order0_last);
return new ColumnSortScript(params);
}
private static String lastString = "Z";// 8 bit last string
//"\uffff";// 16 bit last string
/**
* This method is to allow us to sort on more then one letter in a string when we only have the
* option of returning a single Float (or Int or Float) value from 'sortValue' method for each
* document, rather than being able to compare one document with another.
*
* @param custAttrValue
* @return
*/
private static Float getFirstThreeLettersFloatValue(String custAttrValue, int order) {
if (custAttrValue.length() == 0){
throw new IllegalStateException("Null or empty strings should be initialised to 'z'");
}
custAttrValue = custAttrValue.toUpperCase();
float threeCharValue = 0;
int singleCharValue;
int sortCharLength = custAttrValue.length() < 2 ? custAttrValue.length() : 2;
for (int i = 0; i < sortCharLength;i++ ){
Integer value = charMap.get(custAttrValue.charAt(i));
if (value == null){
singleCharValue = 0;
}else {
singleCharValue = value;
}
if (order == 0){
if (i==0){
threeCharValue += singleCharValue << 24;
} else if (i == 1) {
threeCharValue += singleCharValue << 18;
}
} else {
if (i==0){
threeCharValue += singleCharValue << 12;
} else if (i == 1) {
threeCharValue += singleCharValue << 6;
}
}
}
return threeCharValue;
}
protected static Float order0_last = 1056964608f; // 63*2^24
protected static Float order0_first = 10777216f; // much less than 1*2^24 (16777216)
protected static Float order1_last = 258050f; // greater than 63*2^12
protected static Float order1_first = 0f; // less than 1*2^6
private static Float getAsFloat(String custAttValue1, int order) {
try {
Float theFloat = Float.valueOf(custAttValue1);
return theFloat;
}catch (NumberFormatException nfe){
return null;
}
}
public static Float getScore(List<Map> custColsMapList, String indexFieldName, boolean isASC, int order){
String custAttrValue = getCustomAttrValue(custColsMapList, indexFieldName);
Float score = getFloatScore(custAttrValue, isASC, order);
String logDesc = String.format("Cust Column Value [%s], Score[%f]", custAttrValue, score);
LOG.info(logDesc);
return score;
}
public static String getCustomAttrValue(final List<Map> custColsMapList, String indexFieldName) {
String custAttValue = null;
for (Map custColsMap : custColsMapList) {
String ccId = (String)custColsMap.get("ccId");
if (ccId.equals(indexFieldName)){
//LOG.info("ccId = " + ccId );
Object value = custColsMap.get("value");
//LOG.info("value = " + value );
if (value != null){
return value.toString();
}
}
}
return custAttValue;
}
public static Float getFloatScore(String custAttrValue, boolean isASC, int order) {
boolean isEmpty = custAttrValue == null || custAttrValue.trim().equals("");
if (isEmpty && order == 0){
return isASC ? order0_last : 0;
} else if (isEmpty && order == 1){
return isASC ? order1_last : 0;
}
Float floatValue = null; //getAsFloat(custAttrValue, order); // we have to sacrifice true numbers to get two column sorting in the
// available 32 bits of column sorting space.
if (floatValue == null){
floatValue = getFirstThreeLettersFloatValue(custAttrValue, order);
}
return floatValue;
}
private static Float getScoreForDirection(boolean isASC, Float score) {
if (isASC){
score = 0- score;
}
return score;
}
static class ColumnSortScript extends AbstractFloatSearchScript {
private Map<String, Object> params;
public ColumnSortScript(Map<String, Object> params) {
this.params = params;
}
@Override
public float runAsFloat() {
// TODO Auto-generated method stub
return doRunAsFloat();
}
public float doRunAsFloat() {
try {
float score = 0;
if (params.get("isCustomColumn") != null){
score = getCustomColumnFloatValue("sortDirection", "indexFieldName", 0);
} else if (params.get("isNormalColumn") != null){
score = getNormalColumnFloatValue("sortDirection", "indexFieldName", 0);
} else {
throw new IllegalStateException("First Sort Column must exist.");
}
//make first sort score into its' equivalent float score
float totalScore = score;
float score1 = 0;
if (params.get("isCustomColumn1") != null){
score1 = getCustomColumnFloatValue("sortDirection1", "indexFieldName1", 1);
} else if (params.get("isNormalColumn1") != null){
score1 = getNormalColumnFloatValue("sortDirection1", "indexFieldName1", 1);
}
totalScore += score1;
String logDesc = String.format("score[%f], score1[%f], totalScore[%f]", score, score1, totalScore);
LOG.info(logDesc);
return totalScore;
} catch (Exception e) {
LOG.error("Unexpected error sorting custom columns.", e);
return 0;
}
}
private float getNormalColumnFloatValue(String sortDirectionKey, String indexFieldNameKey, int order) {
Object sortFieldObj = params.get(indexFieldNameKey);
String sortField = sortFieldObj == null ? null : sortFieldObj.toString();
if (sortField == null){
throw new IllegalStateException("Field Name not mapped to key[" + indexFieldNameKey + "]");
}
boolean isASC = isASC(sortDirectionKey);
Object fieldValueObj = source().get(sortField);
String fieldValue;
if (fieldValueObj == null){
fieldValue = null;
}
else if (fieldValueObj instanceof Date){
fieldValue = ((Date)fieldValueObj).getTime() + "";
} else {
fieldValue = fieldValueObj.toString();
}
Float score = getFloatScore(fieldValue, isASC, order);
score = getScoreForDirection(isASC, score);
String logDesc = String.format("Normal Column Value [%s], Score[%f]", fieldValue, score);
LOG.info(logDesc);
return score;
}
private Float getCustomColumnFloatValue(String sortDirectionKey, String indexFieldNameKey, int order) {
boolean isASC = isASC(sortDirectionKey);
Object customColsMapList = source().get("customColumns");
//LOG.info(" Source = " + customColsMapList.getClass());
if (customColsMapList == null ){
float defaultReturn = defaultReturn(isASC, order);
//LOG.info("Default return = " + defaultReturn);
return defaultReturn;
//return getScoreForDirection(isASC, last);
}
List<Map> custColsMapList = (List<Map>) customColsMapList;
if (custColsMapList == null || custColsMapList.size() == 0){
float defaultReturn = defaultReturn(isASC, order);
//LOG.info("Default return = " + defaultReturn);
return defaultReturn;
}
String indexFieldName = (String)params.get(indexFieldNameKey);
Float score = getScore(custColsMapList, indexFieldName, isASC, order);
score = getScoreForDirection(isASC, score);
return score;
}
private boolean isASC(String sortDirectionKey) {
String sortDirection = (String)params.get(sortDirectionKey);
boolean isASC = sortDirection == null || (sortDirection != null && !sortDirection.toUpperCase().trim().equals("DESC"));
return isASC;
}
private float defaultReturn(boolean isASC, int order) {
if (isASC && order == 0){
return 0 - order0_last;
} if (isASC && order == 1){
return 0 - order1_last;
} else if (order == 0) {
return order0_first;
} else { //if (order == 1){
return order1_first;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment