/** * This software is licensed to you under the Apache License, Version 2.0 (the * "Apache License"). * * LinkedIn's contributions are made under the Apache License. If you contribute * to the Software, the contributions will be deemed to have been made under the * Apache License, unless you expressly indicate otherwise. Please do not make any * contributions that would be inconsistent with the Apache License. * * You may obtain a copy of the Apache License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, this software * distributed under the Apache License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Apache * License for the specific language governing permissions and limitations for the * software governed under the Apache License. * * © 2012 LinkedIn Corp. All Rights Reserved. */ package com.senseidb.search.relevance.impl; import it.unimi.dsi.fastutil.doubles.DoubleOpenHashSet; import it.unimi.dsi.fastutil.floats.FloatOpenHashSet; import it.unimi.dsi.fastutil.ints.Int2DoubleOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Set; import org.apache.log4j.Logger; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import com.senseidb.search.relevance.ExternalRelevanceDataStorage; import com.senseidb.search.req.ErrorType; import javassist.CannotCompileException; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import javassist.CtField; import javassist.CtMethod; import javassist.CtNewMethod; import javassist.NotFoundException; /** "relevance": { // (1) Model definition part; this json is used to define a model (input variables, // columns/facets, and function parameters and body); "model": { "variables": { "set_int":["c","d"], // supported hashset types: [set_int, set_float, set_string, // set_double, set_long] "map_int_float":["j"], // supported hashmap: [map_int_float, map_int_double, map_int_*...] // [map_string_int, map_string_float, map_string_*] "int":["e","f"], // supported normal variables: [int, double, float, long, bool, string] "long":["g","h"], "custom_obj":["krati","big_cache"] // supported external static big in-memory object (initialized when senseidb starts) }, "facets": { "int":["year","age"], // facet type support: [double, float, int, long, short, string]; "long":["time"] // facet variable has the same name as the facet name, and they // are defined inside this json; }, // (2) Scoring function and function input parameters in Java; A scoring function and its // parameters are the model. A model changes when the function body or signature // changes; // Params for the function. The symbol order matters, and symbols must be those defined // above. innerScore MUST be used, otherwise, makes no sense to use the custom relevance; // reserved keyword for internal parameters are: "_INNER_SCORE" and "_NOW" "function_params":["_INNER_SCORE", "timeVal", "_timeWeight", "_waterworldWeight", "_half_time"], // The value string in the following JSONObject is like this (a return statement MUST // appear as the last one): // // float delta = System.currentTimeMillis() - timeVal; // float t = delta>0 ? delta : 0; // float hour = t/(1000*3600); // float timeScore = (float) Math.exp(-(hour/_half_time)); // float waterworldScore = _INNER_SCORE; // float time = timeScore * _timeWeight; // float water = waterworldScore * _waterworldWeight; // return (time + water); "function":"A LONG JAVA CODE STRING HERE, ONLY AS FUNCTION BODY, NEEDS RETURN STATEMENT." } // (3) Input values for the model above, if the model requires input values; "values": { "c":[1996,1997], "e":0.98, "j":{"1":2.3, "2":3.4, "3":2.9} // A user input hashmap; "jj":{"key":[1,2,3], "value":[2.3, 3.4, 2.9]} // It also supports this method to pass in a map. } } A dummy testing relevance json inside a query request json may look like this: { "query": { "query_string": { "query": "", "relevance":{ "model":{ "variables":{ "set_int":["goodYear"], "int":["thisYear"], "string":["coolTag"], "map_int_float":["mileageWeight"], "map_int_string":["yearcolor"], "map_string_float":["colorweight"], "map_string_string":["categorycolor"] }, "facets":{ "int":["year","mileage"], "long":["groupid"], "string":["color","category"], "mstring":["tags"] }, "function_params":["_INNER_SCORE", "thisYear", "year", "goodYear", "mileageWeight", "mileage", "color", "yearcolor", "colorweight", "category", "categorycolor"], "function":" if (tags.contains(coolTag)) \ return 999999f; \ if (categorycolor.containsKey(category) \ && categorycolor.get(category).equals(color)) \ return 10000f; \ if (colorweight.containsKey(color)) \ return 200f + colorweight.getFloat(color); \ if (yearcolor.containsKey(year) && \ yearcolor.get(year).equals(color)) \ return 200f; \ if (mileageWeight.containsKey(mileage)) \ return 10000+mileageWeight.get(mileage); \ if (goodYear.contains(year)) \ return (float)Math.exp(2d); \ if (year == thisYear) \ return 87f; \ return _INNER_SCORE;" }, "values":{ "goodYear":[1996,1997], "thisYear":2001, "mileageWeight":{"11400":777.9, "11000":10.2}, "yearcolor":{"1998":"red"}, "colorweight":{"red":335.5}, "categorycolor":{"compact":"red"}, "coolTag":"cool" } } } }, "from": 0, "size": 6, "explain": false, "fetchStored": false, "sort":["_score"] } An advanded usage of weighted multi-facet relevance can be: { "query": { "query_string": { "query": "java", "relevance": { "model": { "variables": { "string":["skill"] }, "facets": { "wmstring":["user_skills"] }, "function_params":["_INNER_SCORE", "user_skills", "skill"], "function":" int weight = 0; \ if (user_skills.hasWeight(skill)) \ weight = user_skills.getWeight(); \ return _INNER_SCORE + weight;" }, "values": { "skill":"java" } } } }, "selections": [ { "terms": { "country_code": { "values": ["us"], "excludes": [], "operator": "or" } } }], "from": 0, "size": 10, "explain": false, "fetchStored": false } */ public class CompilationHelper { private static Logger logger = Logger.getLogger(CompilationHelper.class); private static ClassPool pool = ClassPool.getDefault(); // White list of safe classes private static HashSet<String> hs_safe = new HashSet<String>(); // Format strings for relevance model parameters private static String[] PARAM_FORMAT_STRINGS = new String[] { /* 0 */ " int %s = ints[%d];", /* 1 */ " long %s = longs[%d];", /* 2 */ " double %s = doubles[%d];", /* 3 */ " float %s = floats[%d];", /* 4 */ " String %s = strings[%d];", /* 5 */ " short %s = shorts[%d];", /* 6 */ " boolean %s = booleans[%d];", /* 7 */ " it.unimi.dsi.fastutil.ints.IntOpenHashSet %s = (it.unimi.dsi.fastutil.ints.IntOpenHashSet) sets[%d];", /* 8 */ " it.unimi.dsi.fastutil.longs.LongOpenHashSet %s = (it.unimi.dsi.fastutil.longs.LongOpenHashSet) sets[%d];", /* 9 */ " it.unimi.dsi.fastutil.doubles.DoubleOpenHashSet %s = (it.unimi.dsi.fastutil.doubles.DoubleOpenHashSet) sets[%d];", /* 10 */ " it.unimi.dsi.fastutil.floats.FloatOpenHashSet %s = (it.unimi.dsi.fastutil.floats.FloatOpenHashSet) sets[%d];", /* 11 */ " it.unimi.dsi.fastutil.objects.ObjectOpenHashSet %s = (it.unimi.dsi.fastutil.objects.ObjectOpenHashSet) sets[%d];", /* 12 */ " it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap %s = (it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap) maps[%d];", /* 13 */ " it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap %s = (it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap) maps[%d];", /* 14 */ " it.unimi.dsi.fastutil.ints.Int2DoubleOpenHashMap %s = (it.unimi.dsi.fastutil.ints.Int2DoubleOpenHashMap) maps[%d];", /* 15 */ " it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap %s = (it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap) maps[%d];", /* 16 */ " it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap %s = (it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap) maps[%d];", /* 17 */ " it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap %s = (it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap) maps[%d];", /* 18 */ " it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap %s = (it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap) maps[%d];", /* 19 */ " it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap %s = (it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap) maps[%d];", /* 20 */ " it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap %s = (it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap) maps[%d];", /* 21 */ " it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap %s = (it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap) maps[%d];", /* 22 */ " com.senseidb.search.relevance.impl.MFacetInt %s = mFacetInts[%d];", /* 23 */ " com.senseidb.search.relevance.impl.MFacetLong %s = mFacetLongs[%d];", /* 24 */ " com.senseidb.search.relevance.impl.MFacetDouble %s = mFacetDoubles[%d];", /* 25 */ " com.senseidb.search.relevance.impl.MFacetFloat %s = mFacetFloats[%d];", /* 26 */ " com.senseidb.search.relevance.impl.MFacetString %s = mFacetStrings[%d];", /* 27 */ " com.senseidb.search.relevance.impl.MFacetShort %s = mFacetShorts[%d];", /* 28 */ " com.senseidb.search.relevance.impl.WeightedMFacetInt %s = (com.senseidb.search.relevance.impl.WeightedMFacetInt) mFacetInts[%d];", /* 29 */ " com.senseidb.search.relevance.impl.WeightedMFacetLong %s = (com.senseidb.search.relevance.impl.WeightedMFacetLong) mFacetLongs[%d];", /* 30 */ " com.senseidb.search.relevance.impl.WeightedMFacetDouble %s = (com.senseidb.search.relevance.impl.WeightedMFacetDouble) mFacetDoubles[%d];", /* 31 */ " com.senseidb.search.relevance.impl.WeightedMFacetFloat %s = (com.senseidb.search.relevance.impl.WeightedMFacetFloat) mFacetFloats[%d];", /* 32 */ " com.senseidb.search.relevance.impl.WeightedMFacetString %s = (com.senseidb.search.relevance.impl.WeightedMFacetString) mFacetStrings[%d];", /* 33 */ " com.senseidb.search.relevance.impl.WeightedMFacetShort %s = (com.senseidb.search.relevance.impl.WeightedMFacetShort) mFacetShorts[%d];", /* 34 */ " %s %s = (%s) objs[%d];" }; // Map of parameter types to int arrays. For each parameter type, the // int array contains two elements: the first one is the index of // PARAM_FORMAT_STRINGS for the parameter type, and the second one is // the index of input data array for that parameter. private static Map<Integer, int[]> PARAM_INIT_MAP = new HashMap<Integer, int[]>(); private static int TOTAL_INPUT_DATA_ARRAYS = 16; private static String EXP_INT_METHOD = "public double exp(int val) { return Double.longBitsToDouble(((long) (1512775 * val + 1072632447)) << 32); }"; private static String EXP_DOUBLE_METHOD = "public double exp(double val) { return Double.longBitsToDouble(((long) (1512775 * val + 1072632447)) << 32); }"; private static String EXP_FLOAT_METHOD = "public double exp(float val) { return Double.longBitsToDouble(((long) (1512775 * val + 1072632447)) << 32); }"; private static String SCORE_METHOD_HEADER = "public float score(short[] shorts, " + "int[] ints, " + "long[] longs, " + "float[] floats, " + "double[] doubles, " + "boolean[] booleans, " + "String[] strings, " + "Set[] sets, " + "Map[] maps, " + "com.senseidb.search.relevance.impl.MFacetInt[] mFacetInts, " + "com.senseidb.search.relevance.impl.MFacetLong[] mFacetLongs, " + "com.senseidb.search.relevance.impl.MFacetFloat[] mFacetFloats, " + "com.senseidb.search.relevance.impl.MFacetDouble[] mFacetDoubles, " + "com.senseidb.search.relevance.impl.MFacetShort[] mFacetShorts, " + "com.senseidb.search.relevance.impl.MFacetString[] mFacetStrings, " + "java.lang.Object[] objs)"; static { hs_safe.add("it.unimi.dsi.fastutil.ints.IntOpenHashSet"); hs_safe.add("it.unimi.dsi.fastutil.longs.LongOpenHashSet"); hs_safe.add("it.unimi.dsi.fastutil.shorts.ShortOpenHashSet"); hs_safe.add("it.unimi.dsi.fastutil.booleans.BooleanOpenHashSet"); hs_safe.add("it.unimi.dsi.fastutil.doubles.DoubleOpenHashSet"); hs_safe.add("it.unimi.dsi.fastutil.floats.FloatOpenHashSet"); hs_safe.add("it.unimi.dsi.fastutil.objects.ObjectOpenHashSet"); hs_safe.add("it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap"); hs_safe.add("it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap"); hs_safe.add("it.unimi.dsi.fastutil.ints.Int2DoubleOpenHashMap"); hs_safe.add("it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap"); hs_safe.add("it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap"); hs_safe.add("it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap"); hs_safe.add("it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap"); hs_safe.add("it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap"); hs_safe.add("it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap"); hs_safe.add("it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap"); hs_safe.add("it.unimi.dsi.fastutil.objects.AbstractObject2FloatMap"); hs_safe.add("it.unimi.dsi.fastutil.objects.AbstractObject2FloatFunction"); hs_safe.add("com.senseidb.search.relevance.impl.MFacet"); hs_safe.add("com.senseidb.search.relevance.impl.MFacetDouble"); hs_safe.add("com.senseidb.search.relevance.impl.MFacetFloat"); hs_safe.add("com.senseidb.search.relevance.impl.MFacetInt"); hs_safe.add("com.senseidb.search.relevance.impl.MFacetLong"); hs_safe.add("com.senseidb.search.relevance.impl.MFacetShort"); hs_safe.add("com.senseidb.search.relevance.impl.MFacetString"); hs_safe.add("com.senseidb.search.relevance.impl.WeightedMFacet"); hs_safe.add("com.senseidb.search.relevance.impl.WeightedMFacetDouble"); hs_safe.add("com.senseidb.search.relevance.impl.WeightedMFacetFloat"); hs_safe.add("com.senseidb.search.relevance.impl.WeightedMFacetInt"); hs_safe.add("com.senseidb.search.relevance.impl.WeightedMFacetLong"); hs_safe.add("com.senseidb.search.relevance.impl.WeightedMFacetShort"); hs_safe.add("com.senseidb.search.relevance.impl.WeightedMFacetString"); hs_safe.add("java.util.Random"); pool.importPackage("java.util"); for (String cls: hs_safe) { pool.importPackage(cls); } pool.insertClassPath(new ClassClassPath(CompilationHelper.class)); hs_safe.add("com.senseidb.search.relevance.impl.CustomMathModel"); hs_safe.add("com.senseidb.search.relevance.impl.CompilationHelper$CustomLoader"); hs_safe.add("java.lang.Object"); hs_safe.add("java.lang.Exception"); hs_safe.add("java.lang.Boolean"); hs_safe.add("java.lang.Byte"); hs_safe.add("java.lang.Double"); hs_safe.add("java.lang.Float"); hs_safe.add("java.lang.Integer"); hs_safe.add("java.lang.Long"); hs_safe.add("java.lang.Math"); hs_safe.add("java.lang.Number"); hs_safe.add("java.lang.Short"); hs_safe.add("java.lang.String"); hs_safe.add("java.lang.StringBuffer"); hs_safe.add("java.lang.StringBuilder"); hs_safe.add("java.math.BigDecimal"); hs_safe.add("java.math.BigInteger"); hs_safe.add("java.math.MathContext"); hs_safe.add("java.math.RoundingMode"); // Index of input data index for different types: // // 0 int_index 6 m_int_index 12 boolean_index // 1 long_index 7 m_long_index 13 set_index // 2 double_index 8 m_double_index 14 map_index // 3 float_index 9 m_float_index 15 obj_index // 4 string_index 10 m_string_index // 5 short_index 11 m_short_index // the first int in the following int[] is the index of the string template above (PARAM_FORMAT_STRINGS); // the second int in the int[] below is the index of the function parameters; PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_INNER_SCORE, new int[]{ 3, 3}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_NOW, new int[]{ 1, 1}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_INT, new int[]{ 0, 0}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_LONG, new int[]{ 1, 1}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_DOUBLE, new int[]{ 2, 2}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FLOAT, new int[]{ 3, 3}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_STRING, new int[]{ 4, 4}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_BOOLEAN, new int[]{ 6, 12}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_INT, new int[]{ 0, 0}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_LONG, new int[]{ 1, 1}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_DOUBLE, new int[]{ 2, 2}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_FLOAT, new int[]{ 3, 3}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_STRING, new int[]{ 4, 4}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_SHORT, new int[]{ 5, 5}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_SET_INT, new int[]{ 7, 13}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_SET_LONG, new int[]{ 8, 13}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_SET_DOUBLE, new int[]{ 9, 13}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_SET_FLOAT, new int[]{10, 13}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_SET_STRING, new int[]{11, 13}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_MAP_INT_INT, new int[]{12, 14}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_MAP_INT_LONG, new int[]{13, 14}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_MAP_INT_DOUBLE, new int[]{14, 14}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_MAP_INT_FLOAT, new int[]{15, 14}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_MAP_INT_STRING, new int[]{16, 14}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_MAP_STRING_INT, new int[]{17, 14}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_MAP_STRING_LONG, new int[]{18, 14}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_MAP_STRING_DOUBLE, new int[]{19, 14}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_MAP_STRING_FLOAT, new int[]{20, 14}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_MAP_STRING_STRING, new int[]{21, 14}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_M_INT, new int[]{22, 6}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_M_LONG, new int[]{23, 7}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_M_DOUBLE, new int[]{24, 8}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_M_FLOAT, new int[]{25, 9}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_M_STRING, new int[]{26, 10}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_M_SHORT, new int[]{27, 11}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_WM_INT, new int[]{28, 6}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_WM_LONG, new int[]{29, 7}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_WM_DOUBLE, new int[]{30, 8}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_WM_FLOAT, new int[]{31, 9}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_WM_STRING, new int[]{32, 10}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_WM_SHORT, new int[]{33, 11}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_FACET_A_INT, new int[]{0, 0}); PARAM_INIT_MAP.put(RelevanceJSONConstants.TYPENUMBER_CUSTOM_OBJ, new int[]{34, 15}); } private static int MAX_NUM_MODELS = 10000; static HashMap<String, CustomMathModel> hmModels = new HashMap<String, CustomMathModel>(); public static CustomMathModel createCustomMathScorer(JSONObject jsonModel, DataTable dataTable) throws RelevanceException, JSONException { CustomMathModel cMathModel = null; if(jsonModel == null) throw new RelevanceException(ErrorType.JsonParsingError, "No json model is specified."); JSONObject jsonVariables = jsonModel.optJSONObject(RelevanceJSONConstants.KW_VARIABLES); JSONObject jsonFacets = jsonModel.optJSONObject(RelevanceJSONConstants.KW_FACETS); // Process the function body and parameters firstly JSONArray jsonFuncParameter = jsonModel.optJSONArray(RelevanceJSONConstants.KW_FUNC_PARAMETERS); for(int j=0; j<jsonFuncParameter.length(); j++) { String paramName = jsonFuncParameter.optString(j); dataTable.lls_params.add(paramName); } dataTable.funcBody = jsonModel.optString(RelevanceJSONConstants.KW_FUNCTION); // Process facet variables int[] facetIndice = new int[]{0, 0, 0}; // store the facetIndex, facetMultiIndex, and activity engine facet Index; Iterator<String> it_facet = jsonFacets.keys(); while(it_facet.hasNext()) { String facetType = it_facet.next(); JSONArray facetArray = jsonFacets.getJSONArray(facetType); try { handleFacetSymbols(facetType, facetArray, facetIndice, dataTable); } catch (JSONException e) { logger.error("JSON facets are " + jsonFacets); throw e; } } // Process other variables Iterator<String> it_var = jsonVariables.keys(); while(it_var.hasNext()) { String type = it_var.next(); JSONArray varArray = jsonVariables.getJSONArray(type); for (int i = 0; i < varArray.length(); ++i) { String symbol = varArray.getString(i); if (symbol.equals(RelevanceJSONConstants.KW_INNER_SCORE) || symbol.equals(RelevanceJSONConstants.KW_NOW) || symbol.equals(RelevanceJSONConstants.KW_RANDOM)) { throw new RelevanceException(ErrorType.JsonParsingError, "Internal variable name, " + symbol + ", is reserved."); } Integer typeNum = RelevanceJSONConstants.VARIABLE_INFO_MAP.get(type); if (typeNum == null) { throw new RelevanceException(ErrorType.JsonParsingError, "Variable type, " + type + ", is not recognized."); } dataTable.hm_type.put(symbol, typeNum); } } // Add the _NOW variable String symbolNow = RelevanceJSONConstants.KW_NOW; long now = System.currentTimeMillis(); dataTable.hm_var.put(symbolNow, now); dataTable.hm_type.put(symbolNow, RelevanceJSONConstants.TYPENUMBER_NOW); // Add the _INNER_SCORE variable String symbolInnerScore = RelevanceJSONConstants.KW_INNER_SCORE; dataTable.hm_var.put(symbolInnerScore, symbolInnerScore); dataTable.hm_type.put(symbolInnerScore, RelevanceJSONConstants.TYPENUMBER_INNER_SCORE); if(dataTable.funcBody == null || dataTable.funcBody.length()==0) throw new RelevanceException(ErrorType.JsonParsingError, "No function body found."); if(dataTable.funcBody.indexOf("return ")==-1) throw new RelevanceException(ErrorType.JsonParsingError, "No return statement in the function body."); // Check if all the parameters have been defined for(int i=0; i< dataTable.lls_params.size(); i++) { String symbol = dataTable.lls_params.get(i); if( !dataTable.hm_type.containsKey(symbol)) throw new RelevanceException(ErrorType.JsonParsingError, "function parameter: " + symbol + " was not defined."); Integer typeNum = dataTable.hm_type.get(symbol); if (typeNum >= RelevanceJSONConstants.TYPENUMBER_FACET_INT && typeNum <= RelevanceJSONConstants.TYPENUMBER_FACET_WM_STRING) { if( (!dataTable.hm_symbol_facet.containsKey(symbol)) && (!dataTable.hm_symbol_mfacet.containsKey(symbol))) throw new RelevanceException(ErrorType.JsonParsingError, "function parameter: " + symbol + " was not defined."); } } dataTable.lls_params = filterParameters(dataTable); // Compile the math model below String paramString = getParamString(dataTable); dataTable.classIDString = dataTable.funcBody + paramString; String className = "CRel"+ dataTable.classIDString.hashCode(); logger.info("Custom relevance math class name is:"+ className); if(hmModels.containsKey(className)) { cMathModel = hmModels.get(className); logger.info("get math model from hashmap:"+ className); return cMathModel; } else { synchronized(CompilationHelper.class) { if(hmModels.containsKey(className)) { cMathModel = hmModels.get(className); logger.info("get math model from hashmap:"+ className); return cMathModel; } CtClass ch = CompilationHelper.pool.makeClass(className); CtClass ci; try { ci = CompilationHelper.pool.get("com.senseidb.search.relevance.impl.CustomMathModel"); } catch (NotFoundException e) { logger.info(e.getMessage()); throw new RelevanceException(e); } ch.addInterface(ci); String functionString = makeFuncString(dataTable); addStaticFacilityFields(ch); addStaticFacilityMethods(ch); CtMethod m; try { m = CtNewMethod.make(functionString, ch); ch.addMethod(m); } catch (CannotCompileException e) { logger.info(e.getMessage()); throw new RelevanceException(ErrorType.JsonCompilationError, e.getMessage(), e); } Class h; try { h = CompilationHelper.pool.toClass(ch, new CompilationHelper.CustomLoader(CompilationHelper.class.getClassLoader(), className)); } catch (CannotCompileException e) { if(hmModels.containsKey(className)) { cMathModel = hmModels.get(className); logger.info("get math model from hashmap:"+ className); return cMathModel; } else { logger.info(e.getMessage()); throw new RelevanceException(ErrorType.JsonCompilationError, "Compilation error of json relevance model.", e); } } try { cMathModel = (CustomMathModel)h.newInstance(); } catch (InstantiationException e) { logger.info(e.getMessage()); throw new RelevanceException(ErrorType.JsonCompilationError, "Instantiation exception of relevance object.", e); } catch (IllegalAccessException e) { logger.info(e.getMessage()); throw new RelevanceException(ErrorType.JsonCompilationError, "Instantiation exception of relevance object; Illegal access exception", e); } if(hmModels.size() > MAX_NUM_MODELS) hmModels = new HashMap<String, CustomMathModel>(); hmModels.put(className, cMathModel); logger.info("get math model by compilation:"+ className); return cMathModel; } } } public static void initializeValues(JSONObject jsonValues, DataTable dataTable) throws JSONException { HashMap<String, Integer> hm_type = dataTable.hm_type; Iterator it = hm_type.keySet().iterator(); while(it.hasNext()) { String symbol = (String)it.next(); Integer typeNum = dataTable.hm_type.get(symbol); if (typeNum == RelevanceJSONConstants.TYPENUMBER_INNER_SCORE || typeNum == RelevanceJSONConstants.TYPENUMBER_NOW ) continue; if (typeNum >= RelevanceJSONConstants.TYPENUMBER_SET_INT && typeNum <= RelevanceJSONConstants.TYPENUMBER_SET_STRING) { Set hs = null; JSONArray values = jsonValues.optJSONArray(symbol); if (values == null) throw new JSONException("Variable "+ symbol + " does not have value."); switch (typeNum) { case RelevanceJSONConstants.TYPENUMBER_SET_INT: hs = new IntOpenHashSet(); for (int k = 0; k < values.length(); k++) { hs.add(values.getInt(k)); } break; case RelevanceJSONConstants.TYPENUMBER_SET_DOUBLE: hs = new DoubleOpenHashSet(); for (int k = 0; k < values.length(); k++) { hs.add(values.getDouble(k)); } break; case RelevanceJSONConstants.TYPENUMBER_SET_FLOAT: hs = new FloatOpenHashSet(); for (int k = 0; k < values.length(); k++) { hs.add((float) values.getDouble(k)); } break; case RelevanceJSONConstants.TYPENUMBER_SET_LONG: hs = new LongOpenHashSet(); for (int k = 0; k < values.length(); k++) { hs.add(values.getLong(k)); } break; case RelevanceJSONConstants.TYPENUMBER_SET_STRING: hs = new ObjectOpenHashSet(); for (int k = 0; k < values.length(); k++) { hs.add(values.getString(k)); } break; } dataTable.hm_var.put(symbol, hs); } else if (typeNum >= RelevanceJSONConstants.TYPENUMBER_MAP_INT_INT && typeNum <= RelevanceJSONConstants.TYPENUMBER_MAP_STRING_STRING) { JSONObject values = jsonValues.optJSONObject(symbol); if (values == null) throw new JSONException("Variable "+ symbol + " does not have value."); JSONArray keysList = values.names(); int keySize = keysList == null ? 0 : keysList.length(); // denote if the map is represented in a way of combination of key jsonarray and value jsonarray; boolean isKeyValue = isKeyValueArrayMethod(values); JSONArray keysArrayList = null, valuesArrayList = null; int keyArraySize, valueArraySize; if(isKeyValue) { keysArrayList = values.optJSONArray(RelevanceJSONConstants.KW_KEY); valuesArrayList = values.optJSONArray(RelevanceJSONConstants.KW_VALUE); if (keysArrayList == null) throw new JSONException("Variable " + symbol + " is a map, but does not have a key list."); if (valuesArrayList == null) throw new JSONException("Variable " + symbol + "is a map, but does not have a value list."); keyArraySize = keysArrayList.length(); valueArraySize = valuesArrayList.length(); if (keyArraySize != valueArraySize) throw new JSONException("Variable " + symbol + ": key size is different from value size, can not convert to a map." ); keySize = keysArrayList.length(); } Map hm = null; switch (typeNum) { case RelevanceJSONConstants.TYPENUMBER_MAP_INT_INT: hm = new Int2IntOpenHashMap(); for (int j = 0; j < keySize; j++) { if(isKeyValue) ((Int2IntOpenHashMap) hm).put(keysArrayList.getInt(j), valuesArrayList.getInt(j)); else ((Int2IntOpenHashMap) hm).put(keysList.getInt(j), values.getInt(keysList.getString(j))); } break; case RelevanceJSONConstants.TYPENUMBER_MAP_INT_DOUBLE: hm = new Int2DoubleOpenHashMap(); for (int j = 0; j < keySize; j++) { if(isKeyValue) ((Int2DoubleOpenHashMap) hm).put(keysArrayList.getInt(j), valuesArrayList.getDouble(j)); else ((Int2DoubleOpenHashMap) hm).put(keysList.getInt(j), values.getDouble(keysList.getString(j))); } break; case RelevanceJSONConstants.TYPENUMBER_MAP_INT_FLOAT: hm = new Int2FloatOpenHashMap(); for (int j = 0; j < keySize; j++) { if(isKeyValue) ((Int2FloatOpenHashMap) hm).put(keysArrayList.getInt(j), (float) valuesArrayList.getDouble(j)); else ((Int2FloatOpenHashMap) hm).put(keysList.getInt(j), (float) values.getDouble(keysList.getString(j))); } break; case RelevanceJSONConstants.TYPENUMBER_MAP_INT_LONG: hm = new Int2LongOpenHashMap(); for (int j = 0; j < keySize; j++) { if(isKeyValue) ((Int2LongOpenHashMap) hm).put(keysArrayList.getInt(j), Long.parseLong(valuesArrayList.getString(j))); else ((Int2LongOpenHashMap) hm).put(keysList.getInt(j), Long.parseLong(values.getString(keysList.getString(j)))); } break; case RelevanceJSONConstants.TYPENUMBER_MAP_INT_STRING: hm = new Int2ObjectOpenHashMap(); for (int j = 0; j < keySize; j++) { if(isKeyValue) ((Int2ObjectOpenHashMap) hm).put(keysArrayList.getInt(j), valuesArrayList.getString(j)); else ((Int2ObjectOpenHashMap) hm).put(keysList.getInt(j), values.getString(keysList.getString(j))); } break; case RelevanceJSONConstants.TYPENUMBER_MAP_STRING_INT: hm = new Object2IntOpenHashMap(); for (int j = 0; j < keySize; j++) { if(isKeyValue) ((Object2IntOpenHashMap) hm).put(keysArrayList.getString(j), valuesArrayList.getInt(j)); else { String key = keysList.getString(j); ((Object2IntOpenHashMap) hm).put(key, values.getInt(key)); } } break; case RelevanceJSONConstants.TYPENUMBER_MAP_STRING_DOUBLE: hm = new Object2DoubleOpenHashMap(); for (int j = 0; j < keySize; j++) { if(isKeyValue) ((Object2DoubleOpenHashMap) hm).put(keysArrayList.getString(j), valuesArrayList.getDouble(j)); else { String key = keysList.getString(j); ((Object2DoubleOpenHashMap) hm).put(key, values.getDouble(key)); } } break; case RelevanceJSONConstants.TYPENUMBER_MAP_STRING_FLOAT: hm = new Object2FloatOpenHashMap(); for (int j = 0; j < keySize; j++) { if(isKeyValue) ((Object2FloatOpenHashMap) hm).put(keysArrayList.getString(j), (float) valuesArrayList.getDouble(j)); else { String key = keysList.getString(j); ((Object2FloatOpenHashMap) hm).put(key, (float) values.getDouble(key)); } } break; case RelevanceJSONConstants.TYPENUMBER_MAP_STRING_LONG: hm = new Object2LongOpenHashMap(); for (int j = 0; j < keySize; j++) { if(isKeyValue) ((Object2LongOpenHashMap) hm).put(keysArrayList.getString(j), Long.parseLong(valuesArrayList.getString(j))); else { String key = keysList.getString(j); ((Object2LongOpenHashMap) hm).put(key, Long.parseLong(values.getString(keysList.getString(j)))); } } break; case RelevanceJSONConstants.TYPENUMBER_MAP_STRING_STRING: hm = new Object2ObjectOpenHashMap(); for (int j = 0; j < keySize; j++) { if(isKeyValue) ((Object2ObjectOpenHashMap) hm).put(keysArrayList.getString(j), valuesArrayList.getString(j)); else { String key = keysList.getString(j); ((Object2ObjectOpenHashMap) hm).put(key, values.getString(key)); } } break; } dataTable.hm_var.put(symbol, hm); } else if (typeNum >= RelevanceJSONConstants.TYPENUMBER_INT && typeNum <= RelevanceJSONConstants.TYPENUMBER_STRING) { if (!jsonValues.has(symbol)) throw new JSONException("Symbol " + symbol + " was not assigned with a value." ); switch (typeNum) { case RelevanceJSONConstants.TYPENUMBER_INT: dataTable.hm_var.put(symbol, jsonValues.getInt(symbol)); break; case RelevanceJSONConstants.TYPENUMBER_DOUBLE: dataTable.hm_var.put(symbol, jsonValues.getDouble(symbol)); break; case RelevanceJSONConstants.TYPENUMBER_FLOAT: dataTable.hm_var.put(symbol, (float) jsonValues.getDouble(symbol)); break; case RelevanceJSONConstants.TYPENUMBER_LONG: dataTable.hm_var.put(symbol, Long.parseLong(jsonValues.getString(symbol))); break; case RelevanceJSONConstants.TYPENUMBER_STRING: dataTable.hm_var.put(symbol, jsonValues.getString(symbol)); break; case RelevanceJSONConstants.TYPENUMBER_BOOLEAN: dataTable.hm_var.put(symbol, jsonValues.getBoolean(symbol)); break; } } else if (typeNum == RelevanceJSONConstants.TYPENUMBER_CUSTOM_OBJ) { Object obj = ExternalRelevanceDataStorage.getObj(symbol); if(obj != null) dataTable.hm_var.put(symbol, obj); else throw new JSONException("function parameter: " + symbol + " can not be initialized as a custom Object."); } } // Check if all the parameters have been initialized for(int i=0; i< dataTable.lls_params.size(); i++) { String symbol = dataTable.lls_params.get(i); Integer typeNum = dataTable.hm_type.get(symbol); if (typeNum < RelevanceJSONConstants.TYPENUMBER_FACET_INT || typeNum > RelevanceJSONConstants.TYPENUMBER_FACET_A_INT) { if(!dataTable.hm_var.containsKey(symbol)) throw new JSONException("function parameter: " + symbol + " was not initialized."); } } } // End of initializeValues() /** * check if in the JSON values part the map variable is represented in a json map way or two key and value json array; * "JsonMapway":{"1":2.3, "2":3.4, "3":2.9} // A user input hashmap; * "KeyValueJsonArrayPairWay":{"key":[1,2,3], "value":[2.3, 3.4, 2.9]} // It also supports this method to pass in a map. * @param values * @return boolean */ private static boolean isKeyValueArrayMethod(JSONObject mapJSON) { if(mapJSON.has(RelevanceJSONConstants.KW_KEY) && mapJSON.has(RelevanceJSONConstants.KW_VALUE)) { JSONArray keysList = mapJSON.optJSONArray(RelevanceJSONConstants.KW_KEY); JSONArray valuesList = mapJSON.optJSONArray(RelevanceJSONConstants.KW_VALUE); if(keysList != null && valuesList != null) return true; } return false; } private static void addStaticFacilityFields(CtClass ch) throws JSONException { // add a random field in the object; CtField f; try { f = CtField.make("public static java.util.Random _RANDOM = new java.util.Random();", ch); ch.addField(f); } catch (CannotCompileException e) { logger.info(e.getMessage()); throw new JSONException(e); } } private static void addStaticFacilityMethods(CtClass ch) throws JSONException { addMethod(EXP_INT_METHOD, ch); addMethod(EXP_DOUBLE_METHOD, ch); addMethod(EXP_FLOAT_METHOD, ch); } private static void addMethod(String expStr, CtClass ch) throws JSONException { CtMethod m_exp; try { m_exp = CtNewMethod.make(expStr, ch); ch.addMethod(m_exp); } catch (CannotCompileException e) { logger.info(e.getMessage()); throw new JSONException(e); } } private static String getParamString(DataTable dataTable) { StringBuilder sb = new StringBuilder(); for(String param : dataTable.lls_params) { sb.append(param); sb.append("#"); sb.append(dataTable.hm_type.get(param)); sb.append("#"); } return sb.toString(); } private static LinkedList<String> filterParameters(DataTable dataTable) { LinkedList<String> lls_new = new LinkedList<String>(); for(String param : dataTable.lls_params) { if( !(dataTable.funcBody.indexOf(param) == -1)) lls_new.add(param); } return lls_new; } private static <T, V> String mkString(Map<T, V> map) { StringBuffer sb = new StringBuffer().append("{"); int count = 0; for(Map.Entry<T, V> entry : map.entrySet()) { if(count++ != 0) sb.append(", "); sb.append(entry.getKey()).append(": ").append(entry.getValue()); } return sb.append("}").toString(); } private static <T> String mkString(Set<T> set) { StringBuffer sb = new StringBuffer().append("["); int count = 0; for(T elem : set) { if(count++ != 0) sb.append(", "); sb.append(elem); } return sb.append("]").toString(); } private static void handleFacetSymbols(String facetType, JSONArray facetArray, int[] facetIndice, DataTable dataTable) throws JSONException { Integer[] facetInfo = RelevanceJSONConstants.FACET_INFO_MAP.get(facetType); if (facetInfo == null) { String errorString = String.format("Wrong facet type in facet variable definition json: %s. Map contents are %s. Facet array is %s.", facetType, mkString(RelevanceJSONConstants.FACET_INFO_MAP), facetArray); throw new JSONException(errorString); } Integer type = facetInfo[0]; for(int i=0; i < facetArray.length(); i++) { String facetName = facetArray.getString(i); String symbol = facetName; if(dataTable.hm_symbol_facet.containsKey(symbol) || dataTable.hm_symbol_mfacet.containsKey(symbol) || dataTable.hm_symbol_afacet.containsKey(symbol)) throw new JSONException("facet Symbol "+ symbol + " already defined." ); if(dataTable.hm_facet_index.containsKey(facetName) || dataTable.hm_mfacet_index.containsKey(facetName) || dataTable.hm_afacet_index.containsKey(facetName)) throw new JSONException("facet name "+ facetName + " already assigned to a symbol." ); if (facetInfo[1] == 0) { // This facet is a normal facet; dataTable.hm_symbol_facet.put(symbol, facetName); dataTable.hm_facet_index.put(facetName, facetIndice[0]); facetIndice[0] = facetIndice[0]+1; } else if (facetInfo[1] == 1) { // This is a multi-value facet; dataTable.hm_symbol_mfacet.put(symbol, facetName); dataTable.hm_mfacet_index.put(facetName, facetIndice[1]); facetIndice[1] = facetIndice[1]+1; } else if (facetInfo[1] == 2) { // This is an activity engine facet; dataTable.hm_symbol_afacet.put(symbol, facetName); dataTable.hm_afacet_index.put(facetName, facetIndice[2]); facetIndice[2] = facetIndice[2]+1; } dataTable.hm_type.put(symbol, type); } } private static String makeFuncString(DataTable dataTable) throws JSONException { int[] paramIndices = new int[TOTAL_INPUT_DATA_ARRAYS]; for (int i = 0; i < TOTAL_INPUT_DATA_ARRAYS; i++) { paramIndices[i] = 0; } StringBuffer sb = new StringBuffer(); sb.append(SCORE_METHOD_HEADER).append(" {"); dataTable.useInnerScore = false; // set using innerscore to false at the beginning; once we see innerscore is used in the function below, it will be set to true; for(int i=0; i< dataTable.lls_params.size();i++) { String paramName = dataTable.lls_params.get(i); if(!dataTable.hm_type.containsKey(paramName) || (dataTable.hm_type.get(paramName) == null)) throw new JSONException("function parameter " + paramName + " is not defined."); Integer paramType = dataTable.hm_type.get(paramName); int[] paramInfo = PARAM_INIT_MAP.get(paramType); if(paramType.intValue() == RelevanceJSONConstants.TYPENUMBER_CUSTOM_OBJ) { String className = ExternalRelevanceDataStorage.getObjClsName(paramName); if(className == null) throw new JSONException("Custom external object " + paramName + " is not found."); String className2 = className.replace('$', '.'); hs_safe.add(className); pool.importPackage(className); sb.append(String.format(PARAM_FORMAT_STRINGS[paramInfo[0]], className2, paramName, className2, paramIndices[paramInfo[1]]++)); } else sb.append(String.format(PARAM_FORMAT_STRINGS[paramInfo[0]], paramName, paramIndices[paramInfo[1]]++)); if (paramType == RelevanceJSONConstants.TYPENUMBER_INNER_SCORE) { dataTable.useInnerScore = true; } } sb.append(dataTable.funcBody); sb.append("}"); return sb.toString(); } public static class CustomLoader extends ClassLoader { private ClassLoader _cl; private String _target; @Override public Class<?> loadClass(String name) throws ClassNotFoundException { if(hs_safe.contains(name) || name.equals(_target)) return _cl.loadClass(name); else { String message = String.format("Unable to load class %s. Safe classes are %s", name, mkString(hs_safe)); throw new ClassNotFoundException(message); } } public CustomLoader(ClassLoader cl, String target) { _cl = cl; _target = target; } } public static class DataTable { //dynamic data; public HashMap<String, Object> hm_var; //static model data; public HashMap<String, Integer> hm_type; public HashMap<String, String> hm_symbol_facet; public HashMap<String, Integer> hm_facet_index; public HashMap<String, String> hm_symbol_mfacet; //multi-facet public HashMap<String, Integer> hm_mfacet_index; //multi-facet public HashMap<String, String> hm_symbol_afacet; //activity-facet public HashMap<String, Integer> hm_afacet_index; //activity-facet public LinkedList<String> lls_params; public String funcBody = null; public String classIDString = null; public boolean useInnerScore = true; // by default will calculate innerscore value, set this to false will ignore inner score to save time; public DataTable(){ hm_var = new HashMap<String, Object>(); hm_type = new HashMap<String, Integer>(); hm_symbol_facet = new HashMap<String, String>(); hm_facet_index = new HashMap<String, Integer>(); hm_symbol_mfacet = new HashMap<String, String>(); //multi-facet hm_mfacet_index = new HashMap<String, Integer>(); //multi-facet hm_symbol_afacet = new HashMap<String, String>(); //multi-facet hm_afacet_index = new HashMap<String, Integer>(); //multi-facet lls_params = new LinkedList<String>(); useInnerScore = true; } } }