featureDetails = new ArrayList<>();
+ featureDetails.add(Explanation.match(featureToWeight[index],
+ "weight on feature [would be cool to have the name :)]"));
+ featureDetails.add(featureExplain);
+
+ details.add(Explanation.match(featureExplain.getValue()
+ * featureToWeight[index], "prod of:", featureDetails));
+ index++;
+ }
+
+ return Explanation.match(finalScore, getName() + " [ " + getType()
+ + " ] model applied to features, sum of:", details);
+ }
+}
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/ranking/package-info.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/ranking/package-info.java
new file mode 100644
index 000000000000..32d1f22ec4cf
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/ranking/package-info.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ *
+ * This package contains the main logic for performing the reranking using
+ * a LTR model .
+ *
+ *
+ * A LTR model is plugged into the ranking through the {@link org.apache.solr.ltr.ranking.LTRQParserPlugin},
+ * a {@link org.apache.solr.search.QParserPlugin}. The plugin will
+ * read from the request the model (instance of {@link org.apache.solr.ltr.ranking.ModelQuery})
+ * used to perform the request plus other
+ * parameters. The plugin will generate a {@link org.apache.solr.ltr.ranking.LTRQuery}:
+ * a particular {@link org.apache.solr.search.RankQuery}
+ * that will encapsulate the given model and use it to
+ * rescore and rerank the document (by using an {@link org.apache.solr.ltr.ranking.LTRCollector}).
+ *
+ *
+ * A model will be applied on each document through a {@link org.apache.solr.ltr.ranking.ModelQuery}, a
+ * subclass of {@link org.apache.lucene.search.Query}. As a normal query,
+ * the learned model will produce a new score
+ * for each document reranked.
+ *
+ *
+ * A {@link org.apache.solr.ltr.ranking.ModelQuery} is created by providing an instance of
+ * {@link org.apache.solr.ltr.feature.ModelMetadata}. An instance of
+ * {@link org.apache.solr.ltr.feature.ModelMetadata}
+ * defines how to combine the features in order to create a new
+ * score for a document. A new learning to rank model is plugged
+ * into the framework by extending {@link org.apache.solr.ltr.feature.ModelMetadata},
+ * (see for example {@link org.apache.solr.ltr.ranking.LambdaMARTModel} and {@link org.apache.solr.ltr.ranking.RankSVMModel}).
+ *
+ *
+ * The {@link org.apache.solr.ltr.ranking.ModelQuery} will take care of computing the values of
+ * all the features (see {@link org.apache.solr.ltr.ranking.Feature}) and then will delegate the final score
+ * generation to the {@link org.apache.solr.ltr.feature.ModelMetadata}, by calling the method
+ * {@link org.apache.solr.ltr.feature.ModelMetadata#score(float[] modelFeatureValuesNormalized) score(float[] modelFeatureValuesNormalized)}.
+ *
+ *
+ * Finally, a {@link org.apache.solr.ltr.ranking.Feature} will produce a particular value for each document, so
+ * it is modeled as a {@link org.apache.lucene.search.Query}. The package org.apache.solr.ltr.feature.impl contains several examples
+ * of features. One benefit of extending the Query object is that we can reuse
+ * Query as a feature, see for example {@link org.apache.solr.ltr.feature.impl.SolrFeature}.
+ *
+ *
+ *
+ *
+ *
+ */
+package org.apache.solr.ltr.ranking;
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/rest/ManagedFeatureStore.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/rest/ManagedFeatureStore.java
new file mode 100644
index 000000000000..7a634132ed69
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/rest/ManagedFeatureStore.java
@@ -0,0 +1,223 @@
+package org.apache.solr.ltr.rest;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.SolrResourceLoader;
+import org.apache.solr.ltr.feature.FeatureStore;
+import org.apache.solr.ltr.ranking.Feature;
+import org.apache.solr.ltr.util.FeatureException;
+import org.apache.solr.ltr.util.InvalidFeatureNameException;
+import org.apache.solr.ltr.util.NameValidator;
+import org.apache.solr.ltr.util.NamedParams;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.rest.BaseSolrResource;
+import org.apache.solr.rest.ManagedResource;
+import org.apache.solr.rest.ManagedResourceStorage.StorageIO;
+import org.apache.solr.rest.RestManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Managed resource for a storing a feature.
+ */
+public class ManagedFeatureStore extends ManagedResource implements
+ ManagedResource.ChildResourceSupport {
+
+ private Map stores = new HashMap<>();
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ public static final String FEATURES_JSON_FIELD = "features";
+ public static final String FEATURE_STORE_JSON_FIELD = "featureStores";
+ public static final String DEFAULT_FSTORE = "_DEFAULT_";
+
+ public ManagedFeatureStore(String resourceId, SolrResourceLoader loader,
+ StorageIO storageIO) throws SolrException {
+ super(resourceId, loader, storageIO);
+
+ }
+
+ public synchronized FeatureStore getFeatureStore(String name) {
+ if (name == null) {
+ name = DEFAULT_FSTORE;
+ }
+ if (!stores.containsKey(name)) {
+ stores.put(name, new FeatureStore(name));
+ }
+ return stores.get(name);
+ }
+
+ @Override
+ protected void onManagedDataLoadedFromStorage(NamedList> managedInitArgs,
+ Object managedData) throws SolrException {
+
+ stores.clear();
+ logger.info("------ managed feature ~ loading ------");
+ if (managedData instanceof List) {
+ @SuppressWarnings("unchecked")
+ List> up = (List>) managedData;
+ for (Map u : up) {
+ update(u);
+ }
+ }
+ }
+
+ public void update(Map map) {
+ String name = (String) map.get("name");
+ String type = (String) map.get("type");
+ String store = (String) map.get("store");
+
+ NamedParams params = null;
+
+ if (map.containsKey("params")) {
+ @SuppressWarnings("unchecked")
+ Map np = (Map) map.get("params");
+ params = new NamedParams(np);
+ }
+
+ try {
+
+ addFeature(name, type, store, params);
+ } catch (InvalidFeatureNameException e) {
+ throw new SolrException(ErrorCode.BAD_REQUEST, e);
+ } catch (FeatureException e) {
+ logger.error(e.getMessage());
+ e.printStackTrace();
+ throw new SolrException(ErrorCode.BAD_REQUEST, e);
+ }
+ }
+
+ public synchronized void addFeature(String name, String type,
+ String featureStore, NamedParams params)
+ throws InvalidFeatureNameException, FeatureException {
+ if (featureStore == null) {
+ featureStore = DEFAULT_FSTORE;
+ }
+
+ logger.info("register feature {} -> {} in store [" + featureStore + "]",
+ name, type);
+ if (!NameValidator.check(name)) {
+ throw new InvalidFeatureNameException(name);
+ }
+
+ FeatureStore fstore = getFeatureStore(featureStore);
+
+ if (fstore.containsFeature(name)) {
+ logger.error(
+ "feature {} yet contained in the store, please use a different name",
+ name);
+ throw new InvalidFeatureNameException(name
+ + " yet contained in the store");
+ }
+
+ if (params == null) {
+ params = NamedParams.EMPTY;
+ }
+
+ Feature feature = createFeature(name, type, params, fstore.size());
+
+ fstore.add(feature);
+ }
+
+ /**
+ * generates an instance this feature.
+ */
+ private Feature createFeature(String name, String type, NamedParams params,
+ int id) throws FeatureException {
+ try {
+ Class> c = Class.forName(type);
+
+ Feature f = (Feature) c.newInstance();
+ f.init(name, params, id);
+ return f;
+
+ } catch (Exception e) {
+ throw new FeatureException(e.getMessage(), e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Object applyUpdatesToManagedData(Object updates) {
+ if (updates instanceof List) {
+ List> up = (List>) updates;
+ for (Map u : up) {
+ update(u);
+ }
+ }
+
+ if (updates instanceof Map) {
+ // a unique feature
+ update((Map) updates);
+ }
+
+ // logger.info("fstore updated, features: ");
+ // for (String s : store.getFeatureNames()) {
+ // logger.info(" - {}", s);
+ //
+ // }
+ List features = new ArrayList<>();
+ for (FeatureStore fs : stores.values()) {
+ features.addAll(fs.featuresAsManagedResources());
+ }
+ return features;
+ }
+
+ @Override
+ public void doDeleteChild(BaseSolrResource endpoint, String childId) {
+ if (childId.equals("*")) {
+ stores.clear();
+ return;
+ }
+ if (stores.containsKey(childId)) {
+ stores.remove(childId);
+ }
+ }
+
+ /**
+ * Called to retrieve a named part (the given childId) of the resource at the
+ * given endpoint. Note: since we have a unique child feature store we ignore
+ * the childId.
+ */
+ @Override
+ public void doGet(BaseSolrResource endpoint, String childId) {
+ SolrQueryResponse response = endpoint.getSolrResponse();
+
+ // If no feature store specified, show all the feature stores available
+ if (childId == null) {
+ response.add(FEATURE_STORE_JSON_FIELD, stores.keySet());
+ } else {
+ FeatureStore store = getFeatureStore(childId);
+ if (store == null) {
+ throw new SolrException(ErrorCode.BAD_REQUEST,
+ "missing feature store [" + childId + "]");
+ }
+ response.add(FEATURES_JSON_FIELD, store.featuresAsManagedResources());
+ }
+ }
+
+}
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/rest/ManagedModelStore.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/rest/ManagedModelStore.java
new file mode 100644
index 000000000000..897f98155c70
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/rest/ManagedModelStore.java
@@ -0,0 +1,304 @@
+package org.apache.solr.ltr.rest;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.SolrResourceLoader;
+import org.apache.solr.ltr.feature.FeatureStore;
+import org.apache.solr.ltr.feature.ModelMetadata;
+import org.apache.solr.ltr.feature.ModelStore;
+import org.apache.solr.ltr.feature.norm.Normalizer;
+import org.apache.solr.ltr.feature.norm.impl.IdentityNormalizer;
+import org.apache.solr.ltr.ranking.Feature;
+import org.apache.solr.ltr.util.FeatureException;
+import org.apache.solr.ltr.util.ModelException;
+import org.apache.solr.ltr.util.NamedParams;
+import org.apache.solr.ltr.util.NormalizerException;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.rest.BaseSolrResource;
+import org.apache.solr.rest.ManagedResource;
+import org.apache.solr.rest.ManagedResourceStorage.StorageIO;
+import org.apache.solr.rest.RestManager;
+import org.noggit.ObjectBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Menaged resource for storing a model
+ */
+public class ManagedModelStore extends ManagedResource implements
+ ManagedResource.ChildResourceSupport {
+
+ ModelStore store;
+ private ManagedFeatureStore featureStores;
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final String MODELS_JSON_FIELD = "models";
+
+ public ManagedModelStore(String resourceId, SolrResourceLoader loader,
+ StorageIO storageIO) throws SolrException {
+ super(resourceId, loader, storageIO);
+
+ store = new ModelStore();
+
+ }
+
+ public void init(ManagedFeatureStore featureStores) {
+ logger.info("INIT model store");
+ this.featureStores = featureStores;
+ }
+
+ private Object managedData;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void onManagedDataLoadedFromStorage(NamedList> managedInitArgs,
+ Object managedData) throws SolrException {
+ store.clear();
+ // the managed models on the disk or on zookeeper will be loaded in a lazy
+ // way, since we need to set the managed features first (unfortunately
+ // managed resources do not
+ // decouple the creation of a managed resource with the reading of the data
+ // from the storage)
+ this.managedData = managedData;
+
+ }
+
+ public void loadStoredModels() {
+ logger.info("------ managed models ~ loading ------");
+
+ if (managedData != null && managedData instanceof List) {
+ List> up = (List>) managedData;
+ for (Map u : up) {
+ try {
+ update(u);
+ } catch (ModelException e) {
+ throw new SolrException(ErrorCode.BAD_REQUEST, e);
+ }
+ }
+ }
+ }
+
+ public static Normalizer getNormalizerInstance(String type, NamedParams params)
+ throws NormalizerException {
+ Normalizer f;
+ Class> c;
+ try {
+ c = Class.forName(type);
+
+ f = (Normalizer) c.newInstance();
+ } catch (ClassNotFoundException | InstantiationException
+ | IllegalAccessException e) {
+ throw new NormalizerException("missing normalizer " + type, e);
+ }
+ f.setType(type);
+ if (params == null) {
+ params = NamedParams.EMPTY;
+ }
+ f.init(params);
+ return f;
+
+ }
+
+ @SuppressWarnings("unchecked")
+ private Feature parseFeature(Map featureMap,
+ FeatureStore featureStore) throws NormalizerException, FeatureException,
+ CloneNotSupportedException {
+ // FIXME name shouldn't be be null, exception?
+ String name = (String) featureMap.get("name");
+
+ Normalizer norm = IdentityNormalizer.INSTANCE;
+ if (featureMap.containsKey("norm")) {
+ logger.info("adding normalizer {}", featureMap);
+ Map normMap = (Map) featureMap.get("norm");
+ // FIXME type shouldn't be be null, exception?
+ String type = ((String) normMap.get("type"));
+ NamedParams params = null;
+ if (normMap.containsKey("params")) {
+ Object paramsObj = normMap.get("params");
+ if (paramsObj != null) {
+ params = new NamedParams((Map) paramsObj);
+ }
+ }
+ norm = getNormalizerInstance(type, params);
+ }
+ if (featureStores == null) {
+ throw new FeatureException("missing feature store");
+ }
+
+ Feature meta = featureStore.get(name);
+ meta = (Feature) meta.clone();
+ meta.setNorm(norm);
+
+ return meta;
+ }
+
+ @SuppressWarnings("unchecked")
+ public ModelMetadata makeModelMetaData(String json) throws ModelException {
+ Object parsedJson = null;
+ try {
+ parsedJson = ObjectBuilder.fromJSON(json);
+ } catch (IOException ioExc) {
+ throw new ModelException("ObjectBuilder failed parsing json", ioExc);
+ }
+ return makeModelMetaData((Map) parsedJson);
+ }
+
+ @SuppressWarnings("unchecked")
+ public ModelMetadata makeModelMetaData(Map map)
+ throws ModelException {
+ String name = (String) map.get("name");
+ Object o = map.get("store");
+ String featureStoreName = (o == null) ? ManagedFeatureStore.DEFAULT_FSTORE
+ : (String) o;
+ NamedParams params = null;
+ FeatureStore fstore = featureStores.getFeatureStore(featureStoreName);
+ if (!map.containsKey("features")) {
+ throw new SolrException(ErrorCode.BAD_REQUEST,
+ "Missing mandatory field features");
+ }
+ List featureList = (List) map.get("features");
+
+ List features = new ArrayList<>();
+
+ for (Object f : featureList) {
+ try {
+ Feature feature = parseFeature((Map) f, fstore);
+ if (!fstore.containsFeature(feature.getName())) {
+ throw new ModelException("missing feature " + feature.getName()
+ + " in model " + name);
+ }
+ features.add(feature);
+ } catch (NormalizerException | FeatureException e) {
+ throw new SolrException(ErrorCode.BAD_REQUEST, e);
+ } catch (CloneNotSupportedException e) {
+ throw new SolrException(ErrorCode.BAD_REQUEST, e);
+ }
+ }
+
+ if (map.containsKey("params")) {
+ Map paramsMap = (Map) map.get("params");
+ params = new NamedParams(paramsMap);
+ }
+
+ String type = (String) map.get("type");
+ ModelMetadata meta = null;
+ try {
+ Class> cl = Class.forName(type);
+ Constructor> cons = cl.getDeclaredConstructor(String.class,
+ String.class, List.class, String.class, Collection.class,
+ NamedParams.class);
+ meta = (ModelMetadata) cons.newInstance(name, type, features,
+ featureStoreName, fstore.getFeatures(), params);
+ } catch (Exception e) {
+ throw new ModelException("Model type does not exist " + type, e);
+ }
+
+ return meta;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void update(Map map) throws ModelException {
+
+ ModelMetadata meta = makeModelMetaData(map);
+ try {
+ addMetadataModel(meta);
+ } catch (ModelException e) {
+ throw new SolrException(ErrorCode.BAD_REQUEST, e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected Object applyUpdatesToManagedData(Object updates) {
+ if (updates instanceof List) {
+ List> up = (List>) updates;
+ for (Map u : up) {
+ try {
+ update(u);
+ } catch (ModelException e) {
+ throw new SolrException(ErrorCode.BAD_REQUEST, e);
+ }
+ }
+ }
+
+ if (updates instanceof Map) {
+ Map map = (Map) updates;
+ try {
+ update(map);
+ } catch (ModelException e) {
+ throw new SolrException(ErrorCode.BAD_REQUEST, e);
+ }
+ }
+
+ return store.modelAsManagedResources();
+ }
+
+ @Override
+ public void doDeleteChild(BaseSolrResource endpoint, String childId) {
+ if (childId.equals("*")) store.clear();
+ if (store.containsModel(childId)) store.delete(childId);
+ }
+
+ /**
+ * Called to retrieve a named part (the given childId) of the resource at the
+ * given endpoint. Note: since we have a unique child managed store we ignore
+ * the childId.
+ */
+ @Override
+ public void doGet(BaseSolrResource endpoint, String childId) {
+
+ SolrQueryResponse response = endpoint.getSolrResponse();
+ response.add(MODELS_JSON_FIELD, store.modelAsManagedResources());
+
+ }
+
+ public synchronized void addMetadataModel(ModelMetadata modeldata)
+ throws ModelException {
+ logger.info("adding model {}", modeldata.getName());
+ store.addModel(modeldata);
+ }
+
+ public ModelMetadata getModel(String modelName) throws ModelException {
+ // this function replicates getModelStore().getModel(modelName), but
+ // it simplifies the testing (we can avoid to mock also a ModelStore).
+ return store.getModel(modelName);
+ }
+
+ public ModelStore getModelStore() {
+ return store;
+ }
+
+ @Override
+ public String toString() {
+ return "ManagedModelStore [store=" + store + ", featureStores="
+ + featureStores + "]";
+ }
+
+}
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/rest/package-info.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/rest/package-info.java
new file mode 100644
index 000000000000..f61019e5f59d
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/rest/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Contains the {@link org.apache.solr.rest.ManagedResource} that encapsulate
+ * the feature and the model stores.
+ */
+package org.apache.solr.ltr.rest;
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/FeatureException.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/FeatureException.java
new file mode 100644
index 000000000000..7bf57885572b
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/FeatureException.java
@@ -0,0 +1,32 @@
+package org.apache.solr.ltr.util;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class FeatureException extends LtrException {
+
+ private static final long serialVersionUID = 1L;
+
+ public FeatureException(String msg) {
+ super(msg);
+ }
+
+ public FeatureException(String msg, Exception parent) {
+ super(msg, parent);
+ }
+
+}
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/InvalidFeatureNameException.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/InvalidFeatureNameException.java
new file mode 100644
index 000000000000..854428907ea7
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/InvalidFeatureNameException.java
@@ -0,0 +1,32 @@
+package org.apache.solr.ltr.util;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class InvalidFeatureNameException extends LtrException {
+
+ private static final long serialVersionUID = 1L;
+
+ public InvalidFeatureNameException(String featureName) {
+ super("Invalid feature name " + featureName);
+ }
+
+ public InvalidFeatureNameException(String featureName, Exception parent) {
+ super("Invalid feature name " + featureName, parent);
+ }
+
+}
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/LtrException.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/LtrException.java
new file mode 100644
index 000000000000..0d6141518fa9
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/LtrException.java
@@ -0,0 +1,34 @@
+package org.apache.solr.ltr.util;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+
+public class LtrException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ public LtrException(String message) {
+ super(message);
+ }
+
+ public LtrException(String message, Exception parent) {
+ super(message, parent);
+ }
+
+}
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/MacroExpander.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/MacroExpander.java
new file mode 100644
index 000000000000..f88d4fc8e72e
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/MacroExpander.java
@@ -0,0 +1,104 @@
+package org.apache.solr.ltr.util;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.Map;
+
+// TODO This should be replaced with the MacroExpander inside Solr 5.2
+public class MacroExpander {
+ public static final String MACRO_START = "${";
+
+ private Map orig;
+ private String macroStart = MACRO_START;
+ private char escape = '\\';
+
+ public MacroExpander(Map orig) {
+ this.orig = orig;
+ }
+
+ public static String expand(String val, Map params) {
+ MacroExpander mc = new MacroExpander(params);
+ return mc.expand(val);
+ }
+
+ public String expand(String val) {
+ // quickest short circuit
+ int idx = val.indexOf(macroStart.charAt(0));
+ if (idx < 0) return val;
+
+ int start = 0; // start of the unprocessed part of the string
+ int end = 0;
+ StringBuilder sb = null;
+ for (;;) {
+ idx = val.indexOf(macroStart, idx);
+ int matchedStart = idx;
+
+ // check if escaped
+ if (idx > 0) {
+ // check if escaped...
+ // TODO: what if you *want* to actually have a backslash... perhaps
+ // that's when we allow changing
+ // of the escape character?
+
+ char ch = val.charAt(idx - 1);
+ if (ch == escape) {
+ idx += macroStart.length();
+ continue;
+ }
+ } else if (idx < 0) {
+ if (sb == null) return val;
+ sb.append(val.substring(start));
+ return sb.toString();
+ }
+
+ // found unescaped "${"
+ idx += macroStart.length();
+
+ int rbrace = val.indexOf('}', idx);
+ if (rbrace == -1) {
+ // no matching close brace...
+ continue;
+ }
+
+ if (sb == null) {
+ sb = new StringBuilder(val.length() * 2);
+ }
+
+ if (matchedStart > 0) {
+ sb.append(val.substring(start, matchedStart));
+ }
+
+ // update "start" to be at the end of ${...}
+ start = rbrace + 1;
+
+ String paramName = val.substring(idx, rbrace);
+
+ // in the event that expansions become context dependent... consult
+ // original?
+ String replacement = orig.get(paramName);
+
+ // TODO - handle a list somehow...
+ if (replacement != null) {
+ sb.append(replacement);
+ } else {
+ sb.append(val.substring(matchedStart, start));
+ }
+
+ }
+ }
+}
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/ModelException.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/ModelException.java
new file mode 100644
index 000000000000..47c5a6754f4d
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/ModelException.java
@@ -0,0 +1,32 @@
+package org.apache.solr.ltr.util;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class ModelException extends LtrException {
+
+ private static final long serialVersionUID = 1L;
+
+ public ModelException(String message) {
+ super(message);
+ }
+
+ public ModelException(String message, Exception parent) {
+ super(message, parent);
+ }
+
+}
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/NameValidator.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/NameValidator.java
new file mode 100644
index 000000000000..b97d5be1594d
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/NameValidator.java
@@ -0,0 +1,35 @@
+package org.apache.solr.ltr.util;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class NameValidator {
+ private static Pattern pattern = Pattern
+ .compile("^[a-zA-Z0-9][a-zA-Z0-9_.\\-/\\(/\\)]*$");
+
+ public static boolean check(String name) {
+ if (name == null) {
+ return false;
+ }
+ Matcher matcher = pattern.matcher(name);
+ return matcher.find();
+ }
+
+}
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/NamedParams.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/NamedParams.java
new file mode 100644
index 000000000000..0446805c9946
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/NamedParams.java
@@ -0,0 +1,97 @@
+package org.apache.solr.ltr.util;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class NamedParams extends HashMap {
+
+ private static final long serialVersionUID = 1L;
+ public static final NamedParams EMPTY = new NamedParams();
+
+ public static float convertToFloat(Object o) {
+ float f = 0;
+ if (o instanceof Double) {
+ double d = (Double) o;
+ f = (float) d;
+ return f;
+ }
+ if (o instanceof Integer) {
+ int d = (Integer) o;
+ f = (float) d;
+ return f;
+ }
+ if (o instanceof Long) {
+ long l = (Long) o;
+ f = (float) l;
+ return f;
+ }
+ if (o instanceof Float) {
+ Float ff = (Float) o;
+ f = (float) ff;
+ return f;
+ }
+
+ throw new NumberFormatException(o.getClass().getName()
+ + " cannot be converted to float");
+ }
+
+ public NamedParams() {}
+
+ public NamedParams(Map params) {
+ for (Map.Entry p : params.entrySet()) {
+ add(p.getKey(), p.getValue());
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public NamedParams(Object o) {
+ this((Map) o);
+ }
+
+ public NamedParams add(String name, Object value) {
+ put(name, value);
+ return this;
+ }
+
+ public double getDouble(String key, double defValue) {
+ if (containsKey(key)) {
+ return (double) get(key);
+ }
+ return defValue;
+ }
+
+ public List getList(String key) {
+ if (containsKey(key)) {
+ return (List) get(key);
+ }
+ return null;
+ }
+
+ public float getFloat(String key) {
+ Object o = get(key);
+ return convertToFloat(o);
+ }
+
+ public float getFloat(String key, float value) {
+ return (containsKey(key)) ? getFloat(key) : value;
+ }
+
+}
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/NormalizerException.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/NormalizerException.java
new file mode 100644
index 000000000000..ee34ba794e3d
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/NormalizerException.java
@@ -0,0 +1,32 @@
+package org.apache.solr.ltr.util;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+public class NormalizerException extends LtrException {
+
+ private static final long serialVersionUID = 1L;
+
+ public NormalizerException(String msg) {
+ super(msg);
+ }
+
+ public NormalizerException(String message, Exception parent) {
+ super(message, parent);
+ }
+
+}
diff --git a/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/package-info.java b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/package-info.java
new file mode 100644
index 000000000000..55d1f4405c01
--- /dev/null
+++ b/solr/contrib/ltr/src/java/org/apache/solr/ltr/util/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Some utilities
+ */
+package org.apache.solr.ltr.util;
diff --git a/solr/contrib/ltr/src/java/overview.html b/solr/contrib/ltr/src/java/overview.html
new file mode 100644
index 000000000000..a04b97798e4c
--- /dev/null
+++ b/solr/contrib/ltr/src/java/overview.html
@@ -0,0 +1,21 @@
+
+
+
+Apache Solr Search Server: Learning to Rank Contrib
+
+
diff --git a/solr/contrib/ltr/src/test-files/featureExamples/external_features.json b/solr/contrib/ltr/src/test-files/featureExamples/external_features.json
new file mode 100644
index 000000000000..71ca99cac084
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/featureExamples/external_features.json
@@ -0,0 +1,13 @@
+[ {
+ "name" : "matchedTitle",
+ "type" : "org.apache.solr.ltr.feature.impl.SolrFeature",
+ "params" : {
+ "q" : "{!terms f=title}${user_query}"
+ }
+}, {
+ "name" : "titlePhraseMatch",
+ "type" : "org.apache.solr.ltr.feature.impl.SolrFeature",
+ "params" : {
+ "q" : "{!field f=title}${user_query}"
+ }
+} ]
\ No newline at end of file
diff --git a/solr/contrib/ltr/src/test-files/featureExamples/features-file-simple-params.json b/solr/contrib/ltr/src/test-files/featureExamples/features-file-simple-params.json
new file mode 100644
index 000000000000..023f25e452f1
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/featureExamples/features-file-simple-params.json
@@ -0,0 +1,12 @@
+[
+ {
+ "name": "constant",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "params": {
+ "value": 100,
+ "complex":{
+ "map":0.1
+ }
+ }
+ }
+]
\ No newline at end of file
diff --git a/solr/contrib/ltr/src/test-files/featureExamples/features-file-simple.json b/solr/contrib/ltr/src/test-files/featureExamples/features-file-simple.json
new file mode 100644
index 000000000000..601d3dd5f3b0
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/featureExamples/features-file-simple.json
@@ -0,0 +1,6 @@
+[
+{
+ "name":"constant";
+ "type":"org.apache.solr.ltr.feature.impl.ValueFeature"
+}
+]
\ No newline at end of file
diff --git a/solr/contrib/ltr/src/test-files/featureExamples/features-ranksvm.json b/solr/contrib/ltr/src/test-files/featureExamples/features-ranksvm.json
new file mode 100644
index 000000000000..b3405fc6f091
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/featureExamples/features-ranksvm.json
@@ -0,0 +1,51 @@
+[
+ {
+ "name": "title",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "params": {
+ "value": 1
+ }
+ },
+ {
+ "name": "description",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "params": {
+ "value": 2
+ }
+ },
+ {
+ "name": "keywords",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "params": {
+ "value": 2
+ }
+ },
+ {
+ "name": "popularity",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "params": {
+ "value": 3
+ }
+ },
+ {
+ "name": "text",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "params": {
+ "value": 4
+ }
+ },
+ {
+ "name": "queryIntentPerson",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "params": {
+ "value": 5
+ }
+ },
+ {
+ "name": "queryIntentCompany",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "params": {
+ "value": 5
+ }
+ }
+]
\ No newline at end of file
diff --git a/solr/contrib/ltr/src/test-files/featureExamples/features-store-test-model.json b/solr/contrib/ltr/src/test-files/featureExamples/features-store-test-model.json
new file mode 100644
index 000000000000..4161c68d997a
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/featureExamples/features-store-test-model.json
@@ -0,0 +1,51 @@
+[
+ {
+ "name": "constant1",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "store":"test",
+ "params": {
+ "value": 1
+ }
+ },
+ {
+ "name": "constant2",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "store":"test",
+ "params": {
+ "value": 2
+ }
+ },
+ {
+ "name": "constant3",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "store":"test",
+ "params": {
+ "value": 3
+ }
+ },
+ {
+ "name": "constant4",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "store":"test",
+ "params": {
+ "value": 4
+ }
+ },
+ {
+ "name": "constant5",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "store":"test",
+ "params": {
+ "value": 5
+ }
+ },
+ {
+ "name": "pop",
+ "type": "org.apache.solr.ltr.feature.impl.FieldValueFeature",
+ "store":"test",
+ "params": {
+ "field": "popularity"
+ }
+ }
+
+]
\ No newline at end of file
diff --git a/solr/contrib/ltr/src/test-files/featureExamples/features.json b/solr/contrib/ltr/src/test-files/featureExamples/features.json
new file mode 100644
index 000000000000..624f6100d599
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/featureExamples/features.json
@@ -0,0 +1,6 @@
+[
+
+{ "name": "comp_industryTermScorer", "type": "org.apache.solr.ltr.feature.impl.SolrFeature", "params": {"q": "${user_query}","df": "comp_industry"}},
+{ "name": "comp_strongKeywordsTermScorer", "type": "org.apache.solr.ltr.feature.impl.SolrFeature", "params": {"q": "${user_query}","df": "comp_strongKeywords"}}
+
+]
\ No newline at end of file
diff --git a/solr/contrib/ltr/src/test-files/featureExamples/lambdamart_features.json b/solr/contrib/ltr/src/test-files/featureExamples/lambdamart_features.json
new file mode 100644
index 000000000000..6f6400a6aa2c
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/featureExamples/lambdamart_features.json
@@ -0,0 +1,16 @@
+[
+ {
+ "name": "matchedTitle",
+ "type": "org.apache.solr.ltr.feature.impl.SolrFeature",
+ "params": {
+ "q": "{!terms f=title}${user_query}"
+ }
+ },
+ {
+ "name": "constantScoreToForceLambdaMARTScoreAllDocs",
+ "type": "org.apache.solr.ltr.feature.impl.ValueFeature",
+ "params": {
+ "value": 1
+ }
+ }
+]
diff --git a/solr/contrib/ltr/src/test-files/log4j.properties b/solr/contrib/ltr/src/test-files/log4j.properties
new file mode 100644
index 000000000000..d86c6988d5ed
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/log4j.properties
@@ -0,0 +1,32 @@
+# Logging level
+log4j.rootLogger=INFO, CONSOLE
+
+log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
+log4j.appender.CONSOLE.Target=System.err
+log4j.appender.CONSOLE.layout=org.apache.log4j.EnhancedPatternLayout
+log4j.appender.CONSOLE.layout.ConversionPattern=%-4r %-5p (%t) [%X{node_name} %X{collection} %X{shard} %X{replica} %X{core}] %c{1.} %m%n
+log4j.logger.org.apache.zookeeper=WARN
+log4j.logger.org.apache.hadoop=WARN
+log4j.logger.org.apache.directory=WARN
+log4j.logger.org.apache.solr.hadoop=INFO
+log4j.logger.org.apache.solr.client.solrj.embedded.JettySolrRunner=DEBUG
+org.apache.solr.client.solrj.embedded.JettySolrRunner=DEBUG
+
+#log4j.logger.org.apache.solr.update.processor.LogUpdateProcessor=DEBUG
+#log4j.logger.org.apache.solr.update.processor.DistributedUpdateProcessor=DEBUG
+#log4j.logger.org.apache.solr.update.PeerSync=DEBUG
+#log4j.logger.org.apache.solr.core.CoreContainer=DEBUG
+#log4j.logger.org.apache.solr.cloud.RecoveryStrategy=DEBUG
+#log4j.logger.org.apache.solr.cloud.SyncStrategy=DEBUG
+#log4j.logger.org.apache.solr.handler.admin.CoreAdminHandler=DEBUG
+#log4j.logger.org.apache.solr.cloud.ZkController=DEBUG
+#log4j.logger.org.apache.solr.update.DefaultSolrCoreState=DEBUG
+#log4j.logger.org.apache.solr.common.cloud.ConnectionManager=DEBUG
+#log4j.logger.org.apache.solr.update.UpdateLog=DEBUG
+#log4j.logger.org.apache.solr.cloud.ChaosMonkey=DEBUG
+#log4j.logger.org.apache.solr.update.TransactionLog=DEBUG
+#log4j.logger.org.apache.solr.handler.ReplicationHandler=DEBUG
+#log4j.logger.org.apache.solr.handler.IndexFetcher=DEBUG
+
+#log4j.logger.org.apache.solr.common.cloud.ClusterStateUtil=DEBUG
+#log4j.logger.org.apache.solr.cloud.OverseerAutoReplicaFailoverThread=DEBUG
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/external_model.json b/solr/contrib/ltr/src/test-files/modelExamples/external_model.json
new file mode 100644
index 000000000000..ca3a18666280
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/external_model.json
@@ -0,0 +1,12 @@
+{
+ "type":"org.apache.solr.ltr.ranking.RankSVMModel",
+ "name":"externalmodel",
+ "features":[
+ { "name": "matchedTitle"}
+ ],
+ "params":{
+ "weights": {
+ "matchedTitle": 0.999
+ }
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model.json b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model.json
new file mode 100644
index 000000000000..91b1e047ea14
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model.json
@@ -0,0 +1,38 @@
+{
+ "type":"org.apache.solr.ltr.ranking.LambdaMARTModel",
+ "name":"lambdamartmodel",
+ "features":[
+ { "name": "matchedTitle"},
+ { "name": "constantScoreToForceLambdaMARTScoreAllDocs"}
+ ],
+ "params":{
+ "trees": [
+ {
+ "weight" : 1,
+ "tree": {
+ "feature": "matchedTitle",
+ "threshold": 0.5,
+ "left" : {
+ "value" : -100
+ },
+ "right": {
+ "feature" : "this_feature_doesnt_exist",
+ "threshold": 10.0,
+ "left" : {
+ "value" : 50
+ },
+ "right" : {
+ "value" : 75
+ }
+ }
+ }
+ },
+ {
+ "weight" : 2,
+ "tree": {
+ "value" : -10
+ }
+ }
+ ]
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_feature.json b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_feature.json
new file mode 100644
index 000000000000..24499890f58c
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_feature.json
@@ -0,0 +1,24 @@
+{
+ "type":"org.apache.solr.ltr.ranking.LambdaMARTModel",
+ "name":"lambdamartmodel_no_feature",
+ "features":[
+ { "name": "matchedTitle"},
+ { "name": "constantScoreToForceLambdaMARTScoreAllDocs"}
+ ],
+ "params":{
+ "trees": [
+ {
+ "weight" : 1,
+ "tree": {
+ "threshold": 0.5,
+ "left" : {
+ "value" : -100
+ },
+ "right": {
+ "value" : 75
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_features.json b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_features.json
new file mode 100644
index 000000000000..e0164c4bc7f6
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_features.json
@@ -0,0 +1,14 @@
+{
+ "type":"org.apache.solr.ltr.ranking.LambdaMARTModel",
+ "name":"lambdamartmodel_no_features",
+ "params":{
+ "trees": [
+ {
+ "weight" : 2,
+ "tree": {
+ "value" : -10
+ }
+ }
+ ]
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_left.json b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_left.json
new file mode 100644
index 000000000000..3c3f89588b0b
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_left.json
@@ -0,0 +1,22 @@
+{
+ "type":"org.apache.solr.ltr.ranking.LambdaMARTModel",
+ "name":"lambdamartmodel_no_left",
+ "features":[
+ { "name": "matchedTitle"},
+ { "name": "constantScoreToForceLambdaMARTScoreAllDocs"}
+ ],
+ "params":{
+ "trees": [
+ {
+ "weight" : 1,
+ "tree": {
+ "feature": "matchedTitle",
+ "threshold": 0.5,
+ "right": {
+ "value" : 75
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_params.json b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_params.json
new file mode 100644
index 000000000000..b11e0a8652f5
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_params.json
@@ -0,0 +1,8 @@
+{
+ "type":"org.apache.solr.ltr.ranking.LambdaMARTModel",
+ "name":"lambdamartmodel_no_params",
+ "features":[
+ { "name": "matchedTitle"},
+ { "name": "constantScoreToForceLambdaMARTScoreAllDocs"}
+ ]
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_right.json b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_right.json
new file mode 100644
index 000000000000..70f78ce8f6ac
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_right.json
@@ -0,0 +1,22 @@
+{
+ "type":"org.apache.solr.ltr.ranking.LambdaMARTModel",
+ "name":"lambdamartmodel_no_right",
+ "features":[
+ { "name": "matchedTitle"},
+ { "name": "constantScoreToForceLambdaMARTScoreAllDocs"}
+ ],
+ "params":{
+ "trees": [
+ {
+ "weight" : 1,
+ "tree": {
+ "feature": "matchedTitle",
+ "threshold": 0.5,
+ "left" : {
+ "value" : -100
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_threshold.json b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_threshold.json
new file mode 100644
index 000000000000..3982e06f5199
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_threshold.json
@@ -0,0 +1,24 @@
+{
+ "type":"org.apache.solr.ltr.ranking.LambdaMARTModel",
+ "name":"lambdamartmodel_no_threshold",
+ "features":[
+ { "name": "matchedTitle"},
+ { "name": "constantScoreToForceLambdaMARTScoreAllDocs"}
+ ],
+ "params":{
+ "trees": [
+ {
+ "weight" : 1,
+ "tree": {
+ "feature": "matchedTitle",
+ "left" : {
+ "value" : -100
+ },
+ "right": {
+ "value" : 75
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_tree.json b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_tree.json
new file mode 100644
index 000000000000..148e2f056eec
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_tree.json
@@ -0,0 +1,15 @@
+{
+ "type":"org.apache.solr.ltr.ranking.LambdaMARTModel",
+ "name":"lambdamartmodel_no_tree",
+ "features":[
+ { "name": "matchedTitle"},
+ { "name": "constantScoreToForceLambdaMARTScoreAllDocs"}
+ ],
+ "params":{
+ "trees": [
+ {
+ "weight" : 2
+ }
+ ]
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_trees.json b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_trees.json
new file mode 100644
index 000000000000..72d744965b58
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_trees.json
@@ -0,0 +1,10 @@
+{
+ "type":"org.apache.solr.ltr.ranking.LambdaMARTModel",
+ "name":"lambdamartmodel_no_trees",
+ "features":[
+ { "name": "matchedTitle"},
+ { "name": "constantScoreToForceLambdaMARTScoreAllDocs"}
+ ],
+ "params":{
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_weight.json b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_weight.json
new file mode 100644
index 000000000000..bd5ffefb54f4
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/lambdamart_model_no_weight.json
@@ -0,0 +1,24 @@
+{
+ "type":"org.apache.solr.ltr.ranking.LambdaMARTModel",
+ "name":"lambdamartmodel_no_weight",
+ "features":[
+ { "name": "matchedTitle"},
+ { "name": "constantScoreToForceLambdaMARTScoreAllDocs"}
+ ],
+ "params":{
+ "trees": [
+ {
+ "tree": {
+ "feature": "matchedTitle",
+ "threshold": 0.5,
+ "left" : {
+ "value" : -100
+ },
+ "right": {
+ "value" : 75
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/ranksvm-model.json b/solr/contrib/ltr/src/test-files/modelExamples/ranksvm-model.json
new file mode 100644
index 000000000000..9e3c815a4ca5
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/ranksvm-model.json
@@ -0,0 +1,30 @@
+{
+ "type":"org.apache.solr.ltr.ranking.RankSVMModel",
+ "name":"6029760550880411648",
+ "features":[
+ {"name":"title"},
+ {"name":"description"},
+ {"name":"keywords"},
+ {
+ "name":"popularity",
+ "norm": {
+ "type":"org.apache.solr.ltr.feature.norm.impl.MinMaxNormalizer",
+ "params":{ "min":0.0, "max":10.0 }
+ }
+ },
+ {"name":"text"},
+ {"name":"queryIntentPerson"},
+ {"name":"queryIntentCompany"}
+ ],
+ "params":{
+ "weights": {
+ "title": 0.0000000000,
+ "description": 0.1000000000,
+ "keywords": 0.2000000000,
+ "popularity": 0.3000000000,
+ "text": 0.4000000000,
+ "queryIntentPerson":0.1231231,
+ "queryIntentCompany":0.12121211
+ }
+ }
+}
\ No newline at end of file
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/svm-model-normalized.json b/solr/contrib/ltr/src/test-files/modelExamples/svm-model-normalized.json
new file mode 100644
index 000000000000..1aec25098531
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/svm-model-normalized.json
@@ -0,0 +1,22 @@
+{
+ "type":"org.apache.solr.ltr.ranking.RankSVMModel",
+ "name":"norm2",
+ "features":[
+ {
+ "name":"feature2normalize",
+ "norm": {
+ "type":"org.apache.solr.ltr.feature.norm.impl.StandardNormalizer",
+
+ "params":{
+ "avg":0.0,
+ "std":2.0
+ }
+ }
+ }
+ ],
+ "params":{
+ "weights":{
+ "feature2normalize":1.0
+ }
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/svm-model.json b/solr/contrib/ltr/src/test-files/modelExamples/svm-model.json
new file mode 100644
index 000000000000..f22f87ff0962
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/svm-model.json
@@ -0,0 +1,20 @@
+{
+ "type":"org.apache.solr.ltr.ranking.RankSVMModel",
+ "name":"svm",
+ "features":[
+ {"name":"constant1"},
+ {"name":"constant2"},
+ {"name":"constant3"},
+ {"name":"constant4"},
+ {"name":"constant5"}
+ ],
+ "params":{
+ "weights":{
+ "constant1":1,
+ "constant2":2,
+ "constant3":3,
+ "constant4":4,
+ "constant5":5
+ }
+ }
+}
\ No newline at end of file
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/svm-model1.json b/solr/contrib/ltr/src/test-files/modelExamples/svm-model1.json
new file mode 100644
index 000000000000..b24d32e8cb54
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/svm-model1.json
@@ -0,0 +1,14 @@
+{
+ "type":"org.apache.solr.ltr.ranking.RankSVMModel,
+ "name":"svm1",
+ "features":[
+ {"name":"constant2"},
+ {"name":"constant4"}
+ ],
+ "params":{
+ "weights":{
+ "constant2":3,
+ "constant4":6,
+ }
+ }
+}
\ No newline at end of file
diff --git a/solr/contrib/ltr/src/test-files/modelExamples/svm-sum-model.json b/solr/contrib/ltr/src/test-files/modelExamples/svm-sum-model.json
new file mode 100644
index 000000000000..733e73886cb4
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/modelExamples/svm-sum-model.json
@@ -0,0 +1,20 @@
+{
+ "type":"org.apache.solr.ltr.ranking.RankSVMModel",
+ "name":"sum",
+ "features":[
+ {"name":"constant1"},
+ {"name":"constant2"},
+ {"name":"constant3"},
+ {"name":"constant4"},
+ {"name":"constant5"}
+ ],
+ "params":{
+ "weights":{
+ "constant1":1,
+ "constant2":1,
+ "constant3":1,
+ "constant4":1,
+ "constant5":1
+ }
+ }
+}
diff --git a/solr/contrib/ltr/src/test-files/solr/collection1/conf/indexSynonyms.txt b/solr/contrib/ltr/src/test-files/solr/collection1/conf/indexSynonyms.txt
new file mode 100644
index 000000000000..af55e6efd779
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/solr/collection1/conf/indexSynonyms.txt
@@ -0,0 +1,18 @@
+# the asf licenses this file to you under the apache license, version 2.0
+# (the "license"); you may not use this file except in compliance with
+# the license. you may obtain a copy of the license at
+#
+# http://www.apache.org/licenses/license-2.0
+#
+# unless required by applicable law or agreed to in writing, software
+# distributed under the license is distributed on an "as is" basis,
+# without warranties or conditions of any kind, either express or implied.
+# see the license for the specific language governing permissions and
+# limitations under the license.
+
+#-----------------------------------------------------------------------
+
+# some synonym groups specific to this example
+gb,gib,gigabyte,gigabytes
+mb,mib,megabyte,megabytes
+television, televisions, tv, tvs
diff --git a/solr/contrib/ltr/src/test-files/solr/collection1/conf/protwords.txt b/solr/contrib/ltr/src/test-files/solr/collection1/conf/protwords.txt
new file mode 100644
index 000000000000..02cb4ac23bdb
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/solr/collection1/conf/protwords.txt
@@ -0,0 +1,20 @@
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#-----------------------------------------------------------------------
+# Use a protected word file to protect against the stemmer reducing two
+# unrelated words to the same base word.
+
+# Some non-words that normally won't be encountered,
+# just to test that they won't be stemmed.
+
+offical
diff --git a/solr/contrib/ltr/src/test-files/solr/collection1/conf/schema-ltr.xml b/solr/contrib/ltr/src/test-files/solr/collection1/conf/schema-ltr.xml
new file mode 100644
index 000000000000..949250899e41
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/solr/collection1/conf/schema-ltr.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/solr/contrib/ltr/src/test-files/solr/collection1/conf/solrconfig-ltr.xml b/solr/contrib/ltr/src/test-files/solr/collection1/conf/solrconfig-ltr.xml
new file mode 100644
index 000000000000..628a84bbc08d
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/solr/collection1/conf/solrconfig-ltr.xml
@@ -0,0 +1,72 @@
+
+
+
+
+ 6.0.0
+ ${solr.data.dir:}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 15000
+ false
+
+
+ 1000
+
+
+ ${solr.data.dir:}
+
+
+
+
+
+
+
+ explicit
+ json
+ true
+ id
+
+
+ ltrComponent
+
+
+
+
diff --git a/solr/contrib/ltr/src/test-files/solr/collection1/conf/stemdict.txt b/solr/contrib/ltr/src/test-files/solr/collection1/conf/stemdict.txt
new file mode 100644
index 000000000000..78f05c223a85
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/solr/collection1/conf/stemdict.txt
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#-----------------------------------------------------------------------
+# test that we can override the stemming algorithm with our own mappings
+# these must be tab-separated
+salty salt
diff --git a/solr/contrib/ltr/src/test-files/solr/collection1/conf/stopwords.txt b/solr/contrib/ltr/src/test-files/solr/collection1/conf/stopwords.txt
new file mode 100644
index 000000000000..eabae3b7c0dd
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/solr/collection1/conf/stopwords.txt
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+a
diff --git a/solr/contrib/ltr/src/test-files/solr/collection1/conf/synonyms.txt b/solr/contrib/ltr/src/test-files/solr/collection1/conf/synonyms.txt
new file mode 100644
index 000000000000..0ef0e8daabaf
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/solr/collection1/conf/synonyms.txt
@@ -0,0 +1,28 @@
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#-----------------------------------------------------------------------
+#some test synonym mappings unlikely to appear in real input text
+aaafoo => aaabar
+bbbfoo => bbbfoo bbbbar
+cccfoo => cccbar cccbaz
+fooaaa,baraaa,bazaaa
+
+# Some synonym groups specific to this example
+GB,gib,gigabyte,gigabytes
+MB,mib,megabyte,megabytes
+Television, Televisions, TV, TVs
+#notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming
+#after us won't split it into two words.
+
+# Synonym mappings can be used for spelling correction too
+pixima => pixma
diff --git a/solr/contrib/ltr/src/test-files/solr/collection1/conf/wdfftypes.txt b/solr/contrib/ltr/src/test-files/solr/collection1/conf/wdfftypes.txt
new file mode 100644
index 000000000000..52e60f8643f2
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/solr/collection1/conf/wdfftypes.txt
@@ -0,0 +1,11 @@
+# A customized type mapping for WordDelimiterFilterFactory
+# the allowable types are: LOWER, UPPER, ALPHA, DIGIT, ALPHANUM, SUBWORD_DELIM
+#
+# the default for any character without a mapping is always computed from
+# Unicode character properties
+
+# Map the $, % characters to DIGIT
+# This might be useful for financial data.
+$ => DIGIT
+% => DIGIT
+& => ALPHA
diff --git a/solr/contrib/ltr/src/test-files/solr/solr.xml b/solr/contrib/ltr/src/test-files/solr/solr.xml
new file mode 100644
index 000000000000..c8c3ebeb30a5
--- /dev/null
+++ b/solr/contrib/ltr/src/test-files/solr/solr.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+ ${shareSchema:false}
+ ${configSetBaseDir:configsets}
+ ${coreRootDirectory:.}
+
+
+ ${urlScheme:}
+ ${socketTimeout:90000}
+ ${connTimeout:15000}
+
+
+
+ 127.0.0.1
+ ${hostPort:8983}
+ ${hostContext:solr}
+ ${solr.zkclienttimeout:30000}
+ ${genericCoreNodeNames:true}
+ ${leaderVoteWait:10000}
+ ${distribUpdateConnTimeout:45000}
+ ${distribUpdateSoTimeout:340000}
+
+
+
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/TestRerankBase.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/TestRerankBase.java
new file mode 100644
index 000000000000..c4b92070405b
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/TestRerankBase.java
@@ -0,0 +1,422 @@
+package org.apache.solr.ltr;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Scanner;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.ContentStreamBase;
+import org.apache.solr.ltr.feature.ModelMetadata;
+import org.apache.solr.ltr.feature.impl.ValueFeature;
+import org.apache.solr.ltr.feature.impl.ValueFeature.ValueFeatureWeight;
+import org.apache.solr.ltr.ranking.Feature;
+import org.apache.solr.ltr.ranking.LTRComponent.LTRParams;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.apache.solr.ltr.rest.ManagedFeatureStore;
+import org.apache.solr.ltr.rest.ManagedModelStore;
+import org.apache.solr.ltr.util.FeatureException;
+import org.apache.solr.ltr.util.ModelException;
+import org.apache.solr.ltr.util.NamedParams;
+import org.apache.solr.request.SolrQueryRequestBase;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.util.RestTestBase;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.noggit.ObjectBuilder;
+import org.restlet.ext.servlet.ServerServlet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressSSL
+public class TestRerankBase extends RestTestBase {
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ protected static File tmpSolrHome;
+ protected static File tmpConfDir;
+
+ public static final String FEATURE_ENDPOINT = LTRParams.FSTORE_END_POINT;
+ public static final String MODEL_ENDPOINT = LTRParams.MSTORE_END_POINT;
+ public static final String FEATURE_FILE_NAME = "_schema_fstore.json";
+ public static final String MODEL_FILE_NAME = "_schema_mstore.json";
+ public static final String PARENT_ENDPOINT = "/schema/*";
+
+ protected static final String collection = "collection1";
+ protected static final String confDir = collection + "/conf";
+
+ protected static File fstorefile = null;
+ protected static File mstorefile = null;
+
+ public static void setuptest() throws Exception {
+ setuptest("solrconfig-ltr.xml", "schema-ltr.xml");
+ bulkIndex();
+ }
+
+ public static void setupPersistenttest() throws Exception {
+ setupPersistentTest("solrconfig-ltr.xml", "schema-ltr.xml");
+ bulkIndex();
+ }
+
+ // NOTE: this will return a new rest manager since the getCore() method
+ // returns a new instance of a restManager.
+ public static ManagedFeatureStore getNewManagedFeatureStore() {
+ ManagedFeatureStore fs = (ManagedFeatureStore) h.getCore().getRestManager()
+ .getManagedResource(FEATURE_ENDPOINT);
+ return fs;
+ }
+
+ public static ManagedModelStore getNewManagedModelStore() {
+
+ ManagedModelStore fs = (ManagedModelStore) h.getCore().getRestManager()
+ .getManagedResource(MODEL_ENDPOINT);
+ return fs;
+ }
+
+ public static void setuptest(String solrconfig, String schema)
+ throws Exception {
+ initCore(solrconfig, schema);
+
+ tmpSolrHome = createTempDir().toFile();
+ tmpConfDir = new File(tmpSolrHome, confDir);
+ tmpConfDir.deleteOnExit();
+ FileUtils.copyDirectory(new File(TEST_HOME()),
+ tmpSolrHome.getAbsoluteFile());
+ File fstore = new File(tmpConfDir, FEATURE_FILE_NAME);
+ File mstore = new File(tmpConfDir, MODEL_FILE_NAME);
+
+ if (fstore.exists()) {
+ logger.info("remove feature store config file in {}",
+ fstore.getAbsolutePath());
+ Files.delete(fstore.toPath());
+ }
+ if (mstore.exists()) {
+ logger.info("remove model store config file in {}",
+ mstore.getAbsolutePath());
+ Files.delete(mstore.toPath());
+ }
+ if (!solrconfig.equals("solrconfig.xml")) FileUtils.copyFile(new File(
+ tmpSolrHome.getAbsolutePath() + "/collection1/conf/" + solrconfig),
+ new File(tmpSolrHome.getAbsolutePath()
+ + "/collection1/conf/solrconfig.xml"));
+ if (!schema.equals("schema.xml")) FileUtils
+ .copyFile(new File(tmpSolrHome.getAbsolutePath() + "/collection1/conf/"
+ + schema), new File(tmpSolrHome.getAbsolutePath()
+ + "/collection1/conf/schema.xml"));
+
+ final SortedMap extraServlets = new TreeMap<>();
+ final ServletHolder solrRestApi = new ServletHolder("SolrSchemaRestApi",
+ ServerServlet.class);
+ solrRestApi.setInitParameter("org.restlet.application",
+ "org.apache.solr.rest.SolrSchemaRestApi");
+ solrRestApi.setInitParameter("storageIO",
+ "org.apache.solr.rest.ManagedResourceStorage$InMemoryStorageIO");
+ extraServlets.put(solrRestApi, PARENT_ENDPOINT);
+
+ System.setProperty("managed.schema.mutable", "true");
+ System.setProperty("enable.update.log", "false");
+
+ createJettyAndHarness(tmpSolrHome.getAbsolutePath(), solrconfig, schema,
+ "/solr", true, extraServlets);
+ }
+
+ public static void setupPersistentTest(String solrconfig, String schema)
+ throws Exception {
+ initCore(solrconfig, schema);
+
+ tmpSolrHome = createTempDir().toFile();
+ tmpConfDir = new File(tmpSolrHome, confDir);
+ tmpConfDir.deleteOnExit();
+ FileUtils.copyDirectory(new File(TEST_HOME()),
+ tmpSolrHome.getAbsoluteFile());
+ fstorefile = new File(tmpConfDir, FEATURE_FILE_NAME);
+ mstorefile = new File(tmpConfDir, MODEL_FILE_NAME);
+
+ if (fstorefile.exists()) {
+ logger.info("remove feature store config file in {}",
+ fstorefile.getAbsolutePath());
+ Files.delete(fstorefile.toPath());
+ }
+ if (mstorefile.exists()) {
+ logger.info("remove model store config file in {}",
+ mstorefile.getAbsolutePath());
+ Files.delete(mstorefile.toPath());
+ }
+ // clearModelStore();
+
+ final SortedMap extraServlets = new TreeMap<>();
+ final ServletHolder solrRestApi = new ServletHolder("SolrSchemaRestApi",
+ ServerServlet.class);
+ solrRestApi.setInitParameter("org.restlet.application",
+ "org.apache.solr.rest.SolrSchemaRestApi");
+ solrRestApi.setInitParameter("storageIO",
+ "org.apache.solr.rest.ManagedResourceStorage$JsonStorageIO");
+
+ extraServlets.put(solrRestApi, PARENT_ENDPOINT); // '/schema/*' matches
+ // '/schema',
+ // '/schema/', and
+ // '/schema/whatever...'
+
+ System.setProperty("managed.schema.mutable", "true");
+ // System.setProperty("enable.update.log", "false");
+
+ createJettyAndHarness(tmpSolrHome.getAbsolutePath(), solrconfig, schema,
+ "/solr", true, extraServlets);
+ }
+
+ protected static void aftertest() throws Exception {
+
+ jetty.stop();
+ jetty = null;
+ FileUtils.deleteDirectory(tmpSolrHome);
+ System.clearProperty("managed.schema.mutable");
+ // System.clearProperty("enable.update.log");
+
+ restTestHarness = null;
+ }
+
+ public static void makeRestTestHarnessNull() {
+ restTestHarness = null;
+ }
+
+ /** produces a model encoded in json **/
+ public static String getModelInJson(String name, String type,
+ String[] features, String fstore, String params) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{\n");
+ sb.append("\"name\":").append('"').append(name).append('"').append(",\n");
+ sb.append("\"store\":").append('"').append(fstore).append('"')
+ .append(",\n");
+ sb.append("\"type\":").append('"').append(type).append('"').append(",\n");
+ sb.append("\"features\":").append('[');
+ for (String feature : features) {
+ sb.append("\n\t{ ");
+ sb.append("\"name\":").append('"').append(feature).append('"')
+ .append("},");
+ }
+ sb.deleteCharAt(sb.length() - 1);
+ sb.append("\n]\n");
+ if (params != null) {
+ sb.append(",\n");
+ sb.append("\"params\":").append(params);
+ }
+ sb.append("\n}\n");
+ return sb.toString();
+ }
+
+ /** produces a model encoded in json **/
+ public static String getFeatureInJson(String name, String type,
+ String fstore, String params) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{\n");
+ sb.append("\"name\":").append('"').append(name).append('"').append(",\n");
+ sb.append("\"store\":").append('"').append(fstore).append('"')
+ .append(",\n");
+ sb.append("\"type\":").append('"').append(type).append('"');
+ if (params != null) {
+ sb.append(",\n");
+ sb.append("\"params\":").append(params);
+ }
+ sb.append("\n}\n");
+ return sb.toString();
+ }
+
+ protected static void loadFeature(String name, String type, String params)
+ throws Exception {
+ String feature = getFeatureInJson(name, type, "test", params);
+ logger.info("loading feauture \n{} ", feature);
+ assertJPut(FEATURE_ENDPOINT, feature, "/responseHeader/status==0");
+ }
+
+ protected static void loadFeature(String name, String type, String fstore,
+ String params) throws Exception {
+ String feature = getFeatureInJson(name, type, fstore, params);
+ logger.info("loading feauture \n{} ", feature);
+ assertJPut(FEATURE_ENDPOINT, feature, "/responseHeader/status==0");
+ }
+
+ protected static void loadModel(String name, String type, String[] features,
+ String params) throws Exception {
+ loadModel(name, type, features, "test", params);
+ }
+
+ protected static void loadModel(String name, String type, String[] features,
+ String fstore, String params) throws Exception {
+ String model = getModelInJson(name, type, features, fstore, params);
+ logger.info("loading model \n{} ", model);
+ assertJPut(MODEL_ENDPOINT, model, "/responseHeader/status==0");
+ }
+
+ public static void loadModels(String fileName) throws Exception {
+ URL url = TestRerankBase.class.getResource("/modelExamples/" + fileName);
+ String multipleModels = FileUtils.readFileToString(new File(url.toURI()), "UTF-8");
+
+ assertJPut(MODEL_ENDPOINT, multipleModels, "/responseHeader/status==0");
+ }
+
+ public static void createModelFromFiles(String modelFileName,
+ String featureFileName) throws ModelException, Exception {
+ URL url = TestRerankBase.class.getResource("/modelExamples/"
+ + modelFileName);
+ String modelJson = FileUtils.readFileToString(new File(url.toURI()), "UTF-8");
+ ManagedModelStore ms = getNewManagedModelStore();
+
+ url = TestRerankBase.class.getResource("/featureExamples/"
+ + featureFileName);
+ String featureJson = FileUtils.readFileToString(new File(url.toURI()),"UTF-8");
+
+ Object parsedFeatureJson = null;
+ try {
+ parsedFeatureJson = ObjectBuilder.fromJSON(featureJson);
+ } catch (IOException ioExc) {
+ throw new ModelException("ObjectBuilder failed parsing json", ioExc);
+ }
+
+ ManagedFeatureStore fs = getNewManagedFeatureStore();
+ // fs.getFeatureStore(null).clear();
+ fs.doDeleteChild(null, "*"); // is this safe??
+ // based on my need to call this I dont think that
+ // "getNewManagedFeatureStore()"
+ // is actually returning a new feature store each time
+ fs.applyUpdatesToManagedData(parsedFeatureJson);
+ ms.init(fs);
+
+ ModelMetadata meta = ms.makeModelMetaData(modelJson);
+ ms.addMetadataModel(meta);
+ }
+
+ public static void loadFeatures(String fileName) throws Exception {
+ URL url = TestRerankBase.class.getResource("/featureExamples/" + fileName);
+ String multipleFeatures = FileUtils.readFileToString(new File(url.toURI()),"UTF-8");
+ logger.info("send \n{}", multipleFeatures);
+
+ assertJPut(FEATURE_ENDPOINT, multipleFeatures, "/responseHeader/status==0");
+ }
+
+ protected List getFeatures(List names)
+ throws FeatureException {
+ List features = new ArrayList<>();
+ int pos = 0;
+ for (String name : names) {
+ ValueFeature f = new ValueFeature();
+ f.init(name, new NamedParams().add("value", 10), pos);
+ features.add(f);
+ ++pos;
+ }
+ return features;
+ }
+
+ protected List getFeatures(String[] names) throws FeatureException {
+ return getFeatures(Arrays.asList(names));
+ }
+
+ protected static void loadModelAndFeatures(String name, int allFeatureCount,
+ int modelFeatureCount) throws Exception {
+ String[] features = new String[modelFeatureCount];
+ String[] weights = new String[modelFeatureCount];
+ for (int i = 0; i < allFeatureCount; i++) {
+ String featureName = "c" + i;
+ if (i < modelFeatureCount) {
+ features[i] = featureName;
+ weights[i] = "\"" + featureName + "\":1.0";
+ }
+ loadFeature(featureName, ValueFeatureWeight.class.getCanonicalName(),
+ "{\"value\":" + i + "}");
+ }
+
+ loadModel(name, RankSVMModel.class.getCanonicalName(), features,
+ "{\"weights\":{" + StringUtils.join(weights, ",") + "}}");
+ }
+
+ protected static void bulkIndex() throws Exception {
+ System.out.println("-----------index ---------------------");
+ assertU(adoc("title", "bloomberg different bla", "description",
+ "bloomberg", "id", "6", "popularity", "1"));
+ assertU(adoc("title", "bloomberg bloomberg ", "description", "bloomberg",
+ "id", "7", "popularity", "2"));
+ assertU(adoc("title", "bloomberg bloomberg bloomberg", "description",
+ "bloomberg", "id", "8", "popularity", "3"));
+ assertU(adoc("title", "bloomberg bloomberg bloomberg bloomberg",
+ "description", "bloomberg", "id", "9", "popularity", "5"));
+ assertU(commit());
+ }
+
+ protected static void bulkIndex(String filePath) throws Exception {
+ SolrQueryRequestBase req = lrf.makeRequest(CommonParams.STREAM_CONTENTTYPE,
+ "application/xml");
+
+ List streams = new ArrayList();
+ File file = new File(filePath);
+ streams.add(new ContentStreamBase.FileStream(file));
+ req.setContentStreams(streams);
+
+ try {
+ SolrQueryResponse res = new SolrQueryResponse();
+ h.updater.handleRequest(req, res);
+ } catch (Throwable ex) {
+ // Ignore. Just log the exception and go to the next file
+ logger.error(ex.getMessage());
+ ex.printStackTrace();
+ }
+ assertU(commit());
+
+ }
+
+ protected static void buildIndexUsingAdoc(String filepath)
+ throws FileNotFoundException {
+ Scanner scn = new Scanner(new File(filepath),"UTF-8");
+ StringBuffer buff = new StringBuffer();
+ scn.nextLine();
+ scn.nextLine();
+ scn.nextLine(); // Skip the first 3 lines then add everything else
+ ArrayList docsToAdd = new ArrayList();
+ while (scn.hasNext()) {
+ String curLine = scn.nextLine();
+ if (curLine.contains("")) {
+ buff.append(curLine + "\n");
+ docsToAdd.add(buff.toString().replace("", "")
+ .replace("", "\n")
+ .replace(" ", " \n"));
+ if (!scn.hasNext()) break;
+ else curLine = scn.nextLine();
+ buff = new StringBuffer();
+ }
+ buff.append(curLine + "\n");
+ }
+ for (String doc : docsToAdd) {
+ assertU(doc.trim());
+ }
+ assertU(commit());
+ scn.close();
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestFeatureLogging.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestFeatureLogging.java
new file mode 100644
index 000000000000..337400da0cef
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestFeatureLogging.java
@@ -0,0 +1,182 @@
+package org.apache.solr.ltr.feature;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.feature.impl.FieldValueFeature;
+import org.apache.solr.ltr.feature.impl.SolrFeature;
+import org.apache.solr.ltr.feature.impl.ValueFeature;
+import org.apache.solr.ltr.ranking.LTRComponent;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+@SuppressSSL
+public class TestFeatureLogging extends TestRerankBase {
+
+ @BeforeClass
+ public static void setup() throws Exception {
+ setuptest();
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ @Test
+ public void testGeneratedFeatures() throws Exception {
+ loadFeature("c1", ValueFeature.class.getCanonicalName(), "test1",
+ "{\"value\":1.0}");
+ loadFeature("c2", ValueFeature.class.getCanonicalName(), "test1",
+ "{\"value\":2.0}");
+ loadFeature("c3", ValueFeature.class.getCanonicalName(), "test1",
+ "{\"value\":3.0}");
+ loadFeature("pop", FieldValueFeature.class.getCanonicalName(), "test1",
+ "{\"field\":\"popularity\"}");
+ loadFeature("nomatch", SolrFeature.class.getCanonicalName(), "test1",
+ "{\"q\":\"{!terms f=title}foobarbat\"}");
+ loadFeature("yesmatch", SolrFeature.class.getCanonicalName(), "test1",
+ "{\"q\":\"{!terms f=popularity}2\"}");
+
+ loadModel("sum1", RankSVMModel.class.getCanonicalName(), new String[] {
+ "c1", "c2", "c3"}, "test1",
+ "{\"weights\":{\"c1\":1.0,\"c2\":1.0,\"c3\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:bloomberg");
+ query.add("fl", "title,description,id,popularity,[fv]");
+ query.add("rows", "3");
+ query.add("debugQuery", "on");
+ query.add(LTRComponent.LTRParams.FV, "true");
+ query.add("rq", "{!ltr reRankDocs=3 model=sum1}");
+
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ(
+ "/query" + query.toQueryString(),
+ "/response/docs/[0]/=={'title':'bloomberg bloomberg ', 'description':'bloomberg','id':'7', 'popularity':2, '[fv]':'c1:1.0;c2:2.0;c3:3.0;pop:2.0;yesmatch:1.0'}");
+
+ query.remove("fl");
+ query.add("fl", "[fv]");
+ query.add("rows", "3");
+ query.add(LTRComponent.LTRParams.FV, "true");
+ query.add("rq", "{!ltr reRankDocs=3 model=sum1}");
+
+ res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[0]/=={'[fv]':'c1:1.0;c2:2.0;c3:3.0;pop:2.0;yesmatch:1.0'}");
+ }
+
+ @Test
+ public void testGeneratedOnlyFeatures() throws Exception {
+ loadFeature("c1", ValueFeature.class.getCanonicalName(), "test3",
+ "{\"value\":1.0}");
+ loadFeature("c2", ValueFeature.class.getCanonicalName(), "test3",
+ "{\"value\":2.0}");
+ loadFeature("c3", ValueFeature.class.getCanonicalName(), "test3",
+ "{\"value\":3.0}");
+ loadFeature("pop", FieldValueFeature.class.getCanonicalName(), "test3",
+ "{\"field\":\"popularity\"}");
+
+ loadModel("sumonly", RankSVMModel.class.getCanonicalName(), new String[] {
+ "c1", "c2", "c3"}, "test3",
+ "{\"weights\":{\"c1\":1.0,\"c2\":1.0,\"c3\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:bloomberg");
+ query.add("fl", "title,description,id,popularity,[fv]");
+ query.add("rows", "3");
+ query.add("debugQuery", "on");
+ query.add(LTRComponent.LTRParams.FV, "true");
+ query.add("rq", "{!ltr reRankDocs=3 model=sumonly}");
+
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ(
+ "/query" + query.toQueryString(),
+ "/response/docs/[0]/=={'title':'bloomberg bloomberg ', 'description':'bloomberg','id':'7', 'popularity':2, '[fv]':'c1:1.0;c2:2.0;c3:3.0;pop:2.0'}");
+
+ query.remove("fl");
+ query.add("fl", "fv:[fv]");
+ query.add("rows", "3");
+
+ query.add(LTRComponent.LTRParams.FV, "true");
+ query.add("rq", "{!ltr reRankDocs=3 model=sumonly}");
+
+ res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[0]/=={'fv':'c1:1.0;c2:2.0;c3:3.0;pop:2.0'}");
+
+ }
+
+ @Test
+ public void testGeneratedGroup() throws Exception {
+ loadFeature("c1", ValueFeature.class.getCanonicalName(), "testgroup",
+ "{\"value\":1.0}");
+ loadFeature("c2", ValueFeature.class.getCanonicalName(), "testgroup",
+ "{\"value\":2.0}");
+ loadFeature("c3", ValueFeature.class.getCanonicalName(), "testgroup",
+ "{\"value\":3.0}");
+ loadFeature("pop", FieldValueFeature.class.getCanonicalName(), "testgroup",
+ "{\"field\":\"popularity\"}");
+
+ loadModel("sumgroup", RankSVMModel.class.getCanonicalName(), new String[] {
+ "c1", "c2", "c3"}, "testgroup",
+ "{\"weights\":{\"c1\":1.0,\"c2\":1.0,\"c3\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:bloomberg");
+ query.add("fl", "*,[fv]");
+ query.add("debugQuery", "on");
+
+ query.remove("fl");
+ query.add("fl", "fv:[fv]");
+ query.add("rows", "3");
+ query.add("group", "true");
+ query.add("group.field", "title");
+
+ query.add(LTRComponent.LTRParams.FV, "true");
+ query.add("rq", "{!ltr reRankDocs=3 model=sumgroup}");
+
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ(
+ "/query" + query.toQueryString(),
+ "/grouped/title/groups/[0]/doclist/docs/[0]/=={'fv':'c1:1.0;c2:2.0;c3:3.0;pop:5.0'}");
+
+ query.add("fvwt", "json");
+ res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ(
+ "/query" + query.toQueryString(),
+ "/grouped/title/groups/[0]/doclist/docs/[0]/fv/=={'c1':1.0,'c2':2.0,'c3':3.0,'pop':5.0}");
+ query.remove("fl");
+ query.add("fl", "fv:[fv]");
+
+ assertJQ(
+ "/query" + query.toQueryString(),
+ "/grouped/title/groups/[0]/doclist/docs/[0]/fv/=={'c3':3.0,'pop':5.0,'c1':1.0,'c2':2.0}");
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestFeatureMetadata.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestFeatureMetadata.java
new file mode 100644
index 000000000000..5590553ec325
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestFeatureMetadata.java
@@ -0,0 +1,76 @@
+package org.apache.solr.ltr.feature;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.feature.impl.OriginalScoreFeature;
+import org.apache.solr.ltr.feature.impl.ValueFeature;
+import org.apache.solr.ltr.ranking.Feature;
+import org.apache.solr.ltr.rest.ManagedFeatureStore;
+import org.apache.solr.ltr.util.FeatureException;
+import org.apache.solr.ltr.util.InvalidFeatureNameException;
+import org.apache.solr.ltr.util.NamedParams;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestFeatureMetadata extends TestRerankBase {
+
+ static ManagedFeatureStore store = null;
+
+ @BeforeClass
+ public static void setup() throws Exception {
+ setuptest();
+ store = getNewManagedFeatureStore();
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ @Test
+ public void getInstanceTest() throws FeatureException,
+ InvalidFeatureNameException {
+
+ store.addFeature("test", OriginalScoreFeature.class.getCanonicalName(),
+ "testFstore", NamedParams.EMPTY);
+ Feature feature = store.getFeatureStore("testFstore").get("test");
+ assertEquals("test", feature.getName());
+ assertEquals(OriginalScoreFeature.class.getCanonicalName(), feature
+ .getClass().getCanonicalName());
+ }
+
+ @Test(expected = FeatureException.class)
+ public void getInvalidInstanceTest() throws FeatureException,
+ InvalidFeatureNameException {
+ store.addFeature("test", "org.apache.solr.ltr.feature.LOLFeature",
+ "testFstore2", NamedParams.EMPTY);
+
+ }
+
+ @Test(expected = InvalidFeatureNameException.class)
+ public void getInvalidNameTest() throws FeatureException,
+ InvalidFeatureNameException {
+
+ store.addFeature("!!!??????????", ValueFeature.class.getCanonicalName(),
+ "testFstore3", NamedParams.EMPTY);
+
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestFeatureStore.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestFeatureStore.java
new file mode 100644
index 000000000000..068fa4a8f8d0
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestFeatureStore.java
@@ -0,0 +1,103 @@
+package org.apache.solr.ltr.feature;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.feature.impl.FieldValueFeature;
+import org.apache.solr.ltr.feature.impl.OriginalScoreFeature;
+import org.apache.solr.ltr.feature.impl.ValueFeature;
+import org.apache.solr.ltr.ranking.Feature;
+import org.apache.solr.ltr.rest.ManagedFeatureStore;
+import org.apache.solr.ltr.util.FeatureException;
+import org.apache.solr.ltr.util.InvalidFeatureNameException;
+import org.apache.solr.ltr.util.NamedParams;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestFeatureStore extends TestRerankBase {
+
+ static ManagedFeatureStore fstore = null;
+
+ @BeforeClass
+ public static void setup() throws Exception {
+ setuptest();
+ fstore = getNewManagedFeatureStore();
+ }
+
+ @Test
+ public void testFeatureStoreAdd() throws InvalidFeatureNameException,
+ FeatureException {
+ FeatureStore fs = fstore.getFeatureStore("fstore-testFeature");
+ for (int i = 0; i < 5; i++) {
+ fstore.addFeature("c" + i, OriginalScoreFeature.class.getCanonicalName(),
+ "fstore-testFeature", NamedParams.EMPTY);
+
+ assertTrue(fs.containsFeature("c" + i));
+
+ }
+ assertEquals(5, fs.size());
+
+ }
+
+ @Test
+ public void testFeatureStoreGet() throws FeatureException,
+ InvalidFeatureNameException {
+ FeatureStore fs = fstore.getFeatureStore("fstore-testFeature2");
+ for (int i = 0; i < 5; i++) {
+
+ fstore.addFeature("c" + (float) i, ValueFeature.class.getCanonicalName(),
+ "fstore-testFeature2", new NamedParams().add("value", i));
+
+ }
+
+ for (float i = 0; i < 5; i++) {
+ Feature f = fs.get("c" + (float) i);
+ assertEquals("c" + i, f.getName());
+ assertEquals(i, f.getParams().getFloat("value"), 0.0001);
+ }
+ }
+
+ @Test(expected = FeatureException.class)
+ public void testMissingFeature() throws InvalidFeatureNameException,
+ FeatureException {
+ FeatureStore fs = fstore.getFeatureStore("fstore-testFeature3");
+ for (int i = 0; i < 5; i++) {
+ fstore.addFeature("testc" + (float) i,
+ ValueFeature.class.getCanonicalName(), "fstore-testFeature3",
+ new NamedParams().add("value", i));
+
+ }
+ fs.get("missing_feature_name");
+ }
+
+ @Test(expected = FeatureException.class)
+ public void testMissingFeature2() throws InvalidFeatureNameException,
+ FeatureException {
+ FeatureStore fs = fstore.getFeatureStore("fstore-testFeature4");
+ for (int i = 0; i < 5; i++) {
+ fstore.addFeature("testc" + (float) i,
+ ValueFeature.class.getCanonicalName(), "fstore-testFeature4",
+ new NamedParams().add("value", i));
+
+ }
+ fstore.addFeature("invalidparam",
+ FieldValueFeature.class.getCanonicalName(), "fstore-testFeature4",
+ NamedParams.EMPTY);
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestModelMetadata.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestModelMetadata.java
new file mode 100644
index 000000000000..2575981a1567
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/TestModelMetadata.java
@@ -0,0 +1,154 @@
+package org.apache.solr.ltr.feature;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.apache.solr.ltr.rest.ManagedModelStore;
+import org.apache.solr.ltr.util.FeatureException;
+import org.apache.solr.ltr.util.ModelException;
+import org.apache.solr.ltr.util.NamedParams;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestModelMetadata extends TestRerankBase {
+
+ static ManagedModelStore store = null;
+ static FeatureStore fstore = null;
+
+ @BeforeClass
+ public static void setup() throws Exception {
+ setuptest();
+ // loadFeatures("features-store-test-model.json");
+ store = getNewManagedModelStore();
+ fstore = getNewManagedFeatureStore().getFeatureStore("test");
+
+ }
+
+ @Test
+ public void getInstanceTest() throws FeatureException, ModelException {
+ Map weights = new HashMap<>();
+ weights.put("constant1", 1d);
+ weights.put("constant5", 1d);
+
+ ModelMetadata meta = new RankSVMModel("test1",
+ RankSVMModel.class.getCanonicalName(), getFeatures(new String[] {
+ "constant1", "constant5"}), "test", fstore.getFeatures(),
+ new NamedParams().add("weights", weights));
+
+ store.addMetadataModel(meta);
+ ModelMetadata m = store.getModel("test1");
+ assertEquals(meta, m);
+ }
+
+ @Test(expected = ModelException.class)
+ public void getInvalidTypeTest() throws ModelException, FeatureException {
+ ModelMetadata meta = new RankSVMModel("test2",
+ "org.apache.solr.ltr.model.LOLModel", getFeatures(new String[] {
+ "constant1", "constant5"}), "test", fstore.getFeatures(), null);
+ store.addMetadataModel(meta);
+ ModelMetadata m = store.getModel("test38290156821076");
+ }
+
+ @Test(expected = ModelException.class)
+ public void getInvalidNameTest() throws ModelException, FeatureException {
+ ModelMetadata meta = new RankSVMModel("!!!??????????",
+ RankSVMModel.class.getCanonicalName(), getFeatures(new String[] {
+ "constant1", "constant5"}), "test", fstore.getFeatures(), null);
+ store.addMetadataModel(meta);
+ store.getModel("!!!??????????");
+ }
+
+ @Test(expected = ModelException.class)
+ public void existingNameTest() throws ModelException, FeatureException {
+ Map weights = new HashMap<>();
+ weights.put("constant1", 1d);
+ weights.put("constant5", 1d);
+
+ ModelMetadata meta = new RankSVMModel("test3",
+ RankSVMModel.class.getCanonicalName(), getFeatures(new String[] {
+ "constant1", "constant5"}), "test", fstore.getFeatures(),
+ new NamedParams().add("weights", weights));
+ store.addMetadataModel(meta);
+ ModelMetadata m = store.getModel("test3");
+ assertEquals(meta, m);
+ store.addMetadataModel(meta);
+ }
+
+ @Test(expected = ModelException.class)
+ public void duplicateFeatureTest() throws ModelException, FeatureException {
+ Map weights = new HashMap<>();
+ weights.put("constant1", 1d);
+ weights.put("constant5", 1d);
+
+ ModelMetadata meta = new RankSVMModel("test4",
+ RankSVMModel.class.getCanonicalName(), getFeatures(new String[] {
+ "constant1", "constant1"}), "test", fstore.getFeatures(),
+ new NamedParams().add("weights", weights));
+ store.addMetadataModel(meta);
+
+ }
+
+ @Test(expected = ModelException.class)
+ public void missingFeatureTest() throws ModelException, FeatureException {
+ Map weights = new HashMap<>();
+ weights.put("constant1", 1d);
+ weights.put("constant5missing", 1d);
+
+ ModelMetadata meta = new RankSVMModel("test5",
+ RankSVMModel.class.getCanonicalName(), getFeatures(new String[] {
+ "constant1", "constant1"}), "test", fstore.getFeatures(),
+ new NamedParams().add("weights", weights));
+ store.addMetadataModel(meta);
+
+ }
+
+ @Test(expected = ModelException.class)
+ public void notExistingClassTest() throws ModelException, FeatureException {
+ Map weights = new HashMap<>();
+ weights.put("constant1", 1d);
+ weights.put("constant5missing", 1d);
+
+ ModelMetadata meta = new RankSVMModel("test6",
+ "com.hello.im.a.bad.model.class", getFeatures(new String[] {
+ "constant1", "constant5"}), "test", fstore.getFeatures(),
+ new NamedParams().add("weights", weights));
+ store.addMetadataModel(meta);
+
+ }
+
+ private class WrongClass {};
+
+ @Test(expected = ModelException.class)
+ public void badModelClassTest() throws ModelException, FeatureException {
+ Map weights = new HashMap<>();
+ weights.put("constant1", 1d);
+ weights.put("constant5missing", 1d);
+
+ ModelMetadata meta = new RankSVMModel("test7",
+ WrongClass.class.getCanonicalName(), getFeatures(new String[] {
+ "constant1", "constant5"}), "test", fstore.getFeatures(),
+ new NamedParams().add("weights", weights));
+ store.addMetadataModel(meta);
+
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestEdisMaxSolrFeature.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestEdisMaxSolrFeature.java
new file mode 100644
index 000000000000..90e7aaa04af9
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestEdisMaxSolrFeature.java
@@ -0,0 +1,46 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.Test;
+
+public class TestEdisMaxSolrFeature extends TestQueryFeature {
+ @Test
+ public void testEdisMaxSolrFeature() throws Exception {
+ loadFeature(
+ "SomeEdisMax",
+ SolrFeature.class.getCanonicalName(),
+ "{\"q\":\"{!edismax qf='title description' pf='description' mm=100% boost='pow(popularity, 0.1)' v='w1' tie=0.1}\"}");
+
+ loadModel("EdisMax-model", RankSVMModel.class.getCanonicalName(),
+ new String[] {"SomeEdisMax"}, "{\"weights\":{\"SomeEdisMax\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+
+ query.add("rq", "{!ltr model=EdisMax-model reRankDocs=4}");
+ query.set("debugQuery", "on");
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ }
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestExternalFeatures.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestExternalFeatures.java
new file mode 100644
index 000000000000..163745df6114
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestExternalFeatures.java
@@ -0,0 +1,119 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.ranking.LTRComponent;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+@SuppressCodecs({"Lucene3x", "Lucene41", "Lucene40", "Appending"})
+public class TestExternalFeatures extends TestRerankBase {
+
+ @BeforeClass
+ public static void before() throws Exception {
+ setuptest("solrconfig-ltr.xml", "schema-ltr.xml");
+
+ assertU(adoc("id", "1", "title", "w1", "description", "w1", "popularity",
+ "1"));
+ assertU(adoc("id", "2", "title", "w2", "description", "w2", "popularity",
+ "2"));
+ assertU(adoc("id", "3", "title", "w3", "description", "w3", "popularity",
+ "3"));
+ assertU(adoc("id", "4", "title", "w4", "description", "w4", "popularity",
+ "4"));
+ assertU(adoc("id", "5", "title", "w5", "description", "w5", "popularity",
+ "5"));
+ assertU(commit());
+
+ loadFeatures("external_features.json");
+ loadModels("external_model.json");
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ @Test
+ public void externalTest1() throws Exception {
+ SolrQuery query = new SolrQuery();
+ query.setQuery("*:*");
+ query.add("fl", "*,score");
+ query.add("rows", "3");
+ query.add(LTRComponent.LTRParams.FV, "true");
+
+ // Regular scores
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==1.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='2'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/score==1.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='3'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/score==1.0");
+
+ query.add("fl", "[fv]");
+ // Model is not specified so we should get a model does not exist exception
+ assertJQ("/query" + query.toQueryString(), "/error/msg=='model is null'");
+
+ // No match scores since user_query not passed in to external feature info
+ // and feature depended on it.
+ query.add("rq", "{!ltr reRankDocs=3 model=externalmodel}");
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==0.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='2'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/score==0.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='3'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/score==0.0");
+
+ // Matched user query since it was passed in
+ query.remove("rq");
+ query
+ .add("rq", "{!ltr reRankDocs=3 model=externalmodel efi.user_query=w3}");
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='3'");
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[0]/score==0.999");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='1'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/score==0.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='2'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/score==0.0");
+
+ System.out.println(restTestHarness.query("/query" + query.toQueryString()));
+ }
+
+ @Test
+ public void externalStopwordTest() throws Exception {
+ SolrQuery query = new SolrQuery();
+ query.setQuery("*:*");
+ query.add("fl", "*,score,fv:[fv]");
+ query.add("rows", "1");
+ query.add(LTRComponent.LTRParams.FV, "true");
+ // Stopword only query passed in
+ query.add("rq",
+ "{!ltr reRankDocs=3 model=externalmodel efi.user_query='a'}");
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/fv==''");
+
+ System.out.println(restTestHarness.query("/query" + query.toQueryString()));
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestFieldLengthFeature.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestFieldLengthFeature.java
new file mode 100644
index 000000000000..7f66377259ce
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestFieldLengthFeature.java
@@ -0,0 +1,120 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+@SuppressCodecs({"Lucene3x", "Lucene41", "Lucene40", "Appending"})
+public class TestFieldLengthFeature extends TestRerankBase {
+
+ @BeforeClass
+ public static void before() throws Exception {
+ setuptest("solrconfig-ltr.xml", "schema-ltr.xml");
+
+ assertU(adoc("id", "1", "title", "w1", "description", "w1"));
+ assertU(adoc("id", "2", "title", "w2 2asd asdd didid", "description",
+ "w2 2asd asdd didid"));
+ assertU(adoc("id", "3", "title", "w3", "description", "w3"));
+ assertU(adoc("id", "4", "title", "w4", "description", "w4"));
+ assertU(adoc("id", "5", "title", "w5", "description", "w5"));
+ assertU(adoc("id", "6", "title", "w1 w2", "description", "w1 w2"));
+ assertU(adoc("id", "7", "title", "w1 w2 w3 w4 w5", "description",
+ "w1 w2 w3 w4 w5 w8"));
+ assertU(adoc("id", "8", "title", "w1 w1 w1 w2 w2 w8", "description",
+ "w1 w1 w1 w2 w2"));
+ assertU(commit());
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ @Test
+ public void testRanking() throws Exception {
+ loadFeature("title-length", FieldLengthFeature.class.getCanonicalName(),
+ "{\"field\":\"title\"}");
+
+ loadModel("title-model", RankSVMModel.class.getCanonicalName(),
+ new String[] {"title-length"}, "{\"weights\":{\"title-length\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+
+ String res;
+ // res = restTestHarness.query("/query" + query.toQueryString());
+ // System.out.println(res);
+ // Normal term match
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='8'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='7'");
+ // Normal term match
+
+ query.add("rq", "{!ltr model=title-model reRankDocs=4}");
+ res = restTestHarness.query("/query" + query.toQueryString());
+
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='8'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='7'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='1'");
+
+ query.setQuery("*:*");
+ query.remove("rows");
+ query.add("rows", "8");
+ query.remove("rq");
+ query.add("rq", "{!ltr model=title-model reRankDocs=8}");
+ res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='8'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='7'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='2'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='6'");
+
+ loadFeature("description-length",
+ FieldLengthFeature.class.getCanonicalName(),
+ "{\"field\":\"description\"}");
+ loadModel("description-model", RankSVMModel.class.getCanonicalName(),
+ new String[] {"description-length"},
+ "{\"weights\":{\"description-length\":1.0}}");
+ query.setQuery("title:w1");
+ query.remove("rq");
+ query.remove("rows");
+ query.add("rows", "4");
+ query.add("rq", "{!ltr model=description-model reRankDocs=4}");
+ res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='7'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='8'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='1'");
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestFieldValueFeature.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestFieldValueFeature.java
new file mode 100644
index 000000000000..da206ae23577
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestFieldValueFeature.java
@@ -0,0 +1,105 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+@SuppressCodecs({"Lucene3x", "Lucene41", "Lucene40", "Appending"})
+public class TestFieldValueFeature extends TestRerankBase {
+
+ @BeforeClass
+ public static void before() throws Exception {
+ setuptest("solrconfig-ltr.xml", "schema-ltr.xml");
+
+ assertU(adoc("id", "1", "title", "w1", "description", "w1", "popularity",
+ "1"));
+ assertU(adoc("id", "2", "title", "w2 2asd asdd didid", "description",
+ "w2 2asd asdd didid", "popularity", "2"));
+ assertU(adoc("id", "3", "title", "w3", "description", "w3", "popularity",
+ "3"));
+ assertU(adoc("id", "4", "title", "w4", "description", "w4", "popularity",
+ "4"));
+ assertU(adoc("id", "5", "title", "w5", "description", "w5", "popularity",
+ "5"));
+ assertU(adoc("id", "6", "title", "w1 w2", "description", "w1 w2",
+ "popularity", "6"));
+ assertU(adoc("id", "7", "title", "w1 w2 w3 w4 w5", "description",
+ "w1 w2 w3 w4 w5 w8", "popularity", "7"));
+ assertU(adoc("id", "8", "title", "w1 w1 w1 w2 w2 w8", "description",
+ "w1 w1 w1 w2 w2", "popularity", "8"));
+ assertU(commit());
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ @Test
+ public void testRanking() throws Exception {
+ loadFeature("popularity", FieldValueFeature.class.getCanonicalName(),
+ "{\"field\":\"popularity\"}");
+
+ loadModel("popularity-model", RankSVMModel.class.getCanonicalName(),
+ new String[] {"popularity"}, "{\"weights\":{\"popularity\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+
+ String res;
+ // res = restTestHarness.query("/query" + query.toQueryString());
+ // System.out.println(res);
+ // Normal term match
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='8'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='7'");
+ // Normal term match
+
+ query.add("rq", "{!ltr model=popularity-model reRankDocs=4}");
+ res = restTestHarness.query("/query" + query.toQueryString());
+
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='8'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='7'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='1'");
+
+ query.setQuery("*:*");
+ query.remove("rows");
+ query.add("rows", "8");
+ query.remove("rq");
+ query.add("rq", "{!ltr model=popularity-model reRankDocs=8}");
+ res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='8'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='7'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='5'");
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestFilterSolrFeature.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestFilterSolrFeature.java
new file mode 100644
index 000000000000..5957b8d0aa1d
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestFilterSolrFeature.java
@@ -0,0 +1,90 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+@SuppressCodecs({"Lucene3x", "Lucene41", "Lucene40", "Appending"})
+public class TestFilterSolrFeature extends TestRerankBase {
+ @BeforeClass
+ public static void before() throws Exception {
+ setuptest("solrconfig-ltr.xml", "schema-ltr.xml");
+
+ assertU(adoc("id", "1", "title", "w1", "description", "w1", "popularity",
+ "1"));
+ assertU(adoc("id", "2", "title", "w2 2asd asdd didid", "description",
+ "w2 2asd asdd didid", "popularity", "2"));
+ assertU(adoc("id", "3", "title", "w1", "description", "w1", "popularity",
+ "3"));
+ assertU(adoc("id", "4", "title", "w1", "description", "w1", "popularity",
+ "4"));
+ assertU(adoc("id", "5", "title", "w5", "description", "w5", "popularity",
+ "5"));
+ assertU(adoc("id", "6", "title", "w6 w2", "description", "w1 w2",
+ "popularity", "6"));
+ assertU(adoc("id", "7", "title", "w1 w2 w3 w4 w5", "description",
+ "w6 w2 w3 w4 w5 w8", "popularity", "88888"));
+ assertU(adoc("id", "8", "title", "w1 w1 w1 w2 w2 w8", "description",
+ "w1 w1 w1 w2 w2", "popularity", "88888"));
+ assertU(commit());
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ @Test
+ public void testUserTermScoreWithFQ() throws Exception {
+ loadFeature("SomeTermFQ", SolrFeature.class.getCanonicalName(),
+ "{\"fq\":[\"{!terms f=popularity}88888\"]}");
+ loadFeature("SomeEfiFQ", SolrFeature.class.getCanonicalName(),
+ "{\"fq\":[\"{!terms f=title}${user_query}\"]}");
+ loadModel("Term-modelFQ", RankSVMModel.class.getCanonicalName(),
+ new String[] {"SomeTermFQ", "SomeEfiFQ"},
+ "{\"weights\":{\"SomeTermFQ\":1.6, \"SomeEfiFQ\":2.0}}");
+ SolrQuery query = new SolrQuery();
+ query.setQuery("*:*");
+ query.add("fl", "*, score");
+ query.add("rows", "3");
+ query.add("fq", "{!terms f=title}w1");
+ query.add("rq",
+ "{!ltr model=Term-modelFQ reRankDocs=5 efi.user_query='w5'}");
+
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==5");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==3.6");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/score==1.6");
+ }
+
+ @Test
+ public void testBadFeature() throws Exception {
+ // Missing q/fq
+ String feature = getFeatureInJson("badFeature", "test",
+ SolrFeature.class.getCanonicalName(), "{\"df\":\"foo\"]}");
+ assertJPut(FEATURE_ENDPOINT, feature, "/responseHeader/status==500");
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestLambdaMARTModel.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestLambdaMARTModel.java
new file mode 100644
index 000000000000..9256b4d3c1e2
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestLambdaMARTModel.java
@@ -0,0 +1,219 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//import static org.junit.internal.matchers.StringContains.containsString;
+
+import java.lang.invoke.MethodHandles;
+
+import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.ranking.LTRComponent;
+import org.apache.solr.ltr.util.ModelException;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressCodecs({"Lucene3x", "Lucene41", "Lucene40", "Appending"})
+public class TestLambdaMARTModel extends TestRerankBase {
+
+ @SuppressWarnings("unused")
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ @BeforeClass
+ public static void before() throws Exception {
+ setuptest("solrconfig-ltr.xml", "schema-ltr.xml");
+
+ h.update(adoc("id", "1", "title", "w1", "description", "w1", "popularity",
+ "1"));
+ h.update(adoc("id", "2", "title", "w2", "description", "w2", "popularity",
+ "2"));
+ h.update(adoc("id", "3", "title", "w3", "description", "w3", "popularity",
+ "3"));
+ h.update(adoc("id", "4", "title", "w4", "description", "w4", "popularity",
+ "4"));
+ h.update(adoc("id", "5", "title", "w5", "description", "w5", "popularity",
+ "5"));
+ h.update(commit());
+
+ loadFeatures("lambdamart_features.json"); // currently needed to force
+ // scoring on all docs
+ loadModels("lambdamart_model.json");
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ @Ignore
+ @Test
+ public void lambdaMartTest1() throws Exception {
+ SolrQuery query = new SolrQuery();
+ query.setQuery("*:*");
+ query.add("rows", "3");
+ query.add(LTRComponent.LTRParams.FV, "true");
+ query.add("fl", "*,score");
+
+ // Regular scores
+ // System.out.println(restTestHarness.query(request)
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==1.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='2'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/score==1.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='3'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/score==1.0");
+
+ // No match scores since user_query not passed in to external feature info
+ // and feature depended on it.
+ query.add("fl", "[fv]");
+ query.add("rq", "{!ltr reRankDocs=3 model=lambdamartmodel}");
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[0]/score==-120.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='2'");
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[1]/score==-120.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='3'");
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[2]/score==-120.0");
+
+ // Matched user query since it was passed in
+ query.remove("rq");
+ query.add("rq",
+ "{!ltr reRankDocs=3 model=lambdamartmodel efi.user_query=w3}");
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='3'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==30.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='1'");
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[1]/score==-120.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='2'");
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[2]/score==-120.0");
+
+ System.out.println(restTestHarness.query("/query" + query.toQueryString()));
+
+ }
+
+ @Ignore
+ @Test
+ public void lambdaMartTestExplain() throws Exception {
+ SolrQuery query = new SolrQuery();
+ query.setQuery("*:*");
+ query.add("fl", "*,score,[fv]");
+ query.add("rows", "3");
+ query.add(LTRComponent.LTRParams.FV, "true");
+
+ query.add("rq",
+ "{!ltr reRankDocs=3 model=lambdamartmodel efi.user_query=w3}");
+
+ // test out the explain feature, make sure it returns something
+ query.setParam("debugQuery", "on");
+ String qryResult = JQ("/query" + query.toQueryString());
+
+ System.out.println(qryResult);
+
+ qryResult = qryResult.replaceAll("\n", " ");
+ // FIXME containsString doesn't exist.
+ // assertThat(qryResult, containsString("\"debug\":{"));
+ // qryResult = qryResult.substring(qryResult.indexOf("debug"));
+ //
+ // assertThat(qryResult, containsString("\"explain\":{"));
+ // qryResult = qryResult.substring(qryResult.indexOf("explain"));
+ //
+ // assertThat(qryResult, containsString("lambdamartmodel"));
+ // assertThat(qryResult,
+ // containsString("org.apache.solr.ltr.ranking.LambdaMARTModel"));
+ //
+ // assertThat(qryResult, containsString("-100.0 = tree 0"));
+ // assertThat(qryResult, containsString("50.0 = tree 0"));
+ // assertThat(qryResult, containsString("-20.0 = tree 1"));
+ // assertThat(qryResult, containsString("'matchedTitle':1.0 > 0.5"));
+ // assertThat(qryResult, containsString("'matchedTitle':0.0 <= 0.5"));
+ //
+ // assertThat(qryResult, containsString(" Go Right "));
+ // assertThat(qryResult, containsString(" Go Left "));
+ // assertThat(qryResult,
+ // containsString("'this_feature_doesnt_exist' does not exist in FV"));
+
+ System.out.println(restTestHarness.query("/query" + query.toQueryString()));
+ }
+
+ @Test(expected = ModelException.class)
+ public void lambdaMartTestNoParams() throws Exception {
+ createModelFromFiles("lambdamart_model_no_params.json",
+ "lambdamart_features.json");
+
+ }
+
+ @Test(expected = ModelException.class)
+ public void lambdaMartTestNoTrees() throws Exception {
+ createModelFromFiles("lambdamart_model_no_trees.json",
+ "lambdamart_features.json");
+ }
+
+ @Test(expected = ModelException.class)
+ public void lambdaMartTestNoWeight() throws Exception {
+ createModelFromFiles("lambdamart_model_no_weight.json",
+ "lambdamart_features.json");
+ }
+
+ @Test(expected = ModelException.class)
+ public void lambdaMartTestNoTree() throws Exception {
+ createModelFromFiles("lambdamart_model_no_tree.json",
+ "lambdamart_features.json");
+ }
+
+ @Test(expected = SolrException.class)
+ public void lambdaMartTestNoFeatures() throws Exception {
+ createModelFromFiles("lambdamart_model_no_features.json",
+ "lambdamart_features.json");
+ }
+
+ @Test(expected = ModelException.class)
+ public void lambdaMartTestNoRight() throws Exception {
+ createModelFromFiles("lambdamart_model_no_right.json",
+ "lambdamart_features.json");
+
+ }
+
+ @Test(expected = ModelException.class)
+ public void lambdaMartTestNoLeft() throws Exception {
+ createModelFromFiles("lambdamart_model_no_left.json",
+ "lambdamart_features.json");
+ }
+
+ @Test(expected = ModelException.class)
+ public void lambdaMartTestNoThreshold() throws Exception {
+ createModelFromFiles("lambdamart_model_no_threshold.json",
+ "lambdamart_features.json");
+
+ }
+
+ @Test(expected = ModelException.class)
+ public void lambdaMartTestNoFeature() throws Exception {
+ createModelFromFiles("lambdamart_model_no_feature.json",
+ "lambdamart_features.json");
+ }
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestNoMatchSolrFeature.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestNoMatchSolrFeature.java
new file mode 100644
index 000000000000..5d0e97ef5535
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestNoMatchSolrFeature.java
@@ -0,0 +1,206 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.ArrayList;
+import java.util.Map;
+
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.ranking.LambdaMARTModel;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.noggit.ObjectBuilder;
+
+public class TestNoMatchSolrFeature extends TestRerankBase {
+
+ @BeforeClass
+ public static void before() throws Exception {
+ setuptest("solrconfig-ltr.xml", "schema-ltr.xml");
+
+ assertU(adoc("id", "1", "title", "w1", "description", "w1", "popularity",
+ "1"));
+ assertU(adoc("id", "2", "title", "w2 2asd asdd didid", "description",
+ "w2 2asd asdd didid", "popularity", "2"));
+ assertU(adoc("id", "3", "title", "w3", "description", "w3", "popularity",
+ "3"));
+ assertU(adoc("id", "4", "title", "w4", "description", "w4", "popularity",
+ "4"));
+ assertU(adoc("id", "5", "title", "w5", "description", "w5", "popularity",
+ "5"));
+ assertU(adoc("id", "6", "title", "w1 w2", "description", "w1 w2",
+ "popularity", "6"));
+ assertU(adoc("id", "7", "title", "w1 w2 w3 w4 w5", "description",
+ "w1 w2 w3 w4 w5 w8", "popularity", "7"));
+ assertU(adoc("id", "8", "title", "w1 w1 w1 w2 w2 w8", "description",
+ "w1 w1 w1 w2 w2", "popularity", "8"));
+ assertU(commit());
+
+ loadFeature("nomatchfeature", SolrFeature.class.getCanonicalName(),
+ "{\"q\":\"foobarbat12345\",\"df\":\"title\"}");
+ loadFeature("yesmatchfeature", SolrFeature.class.getCanonicalName(),
+ "{\"q\":\"w1\",\"df\":\"title\"}");
+ loadFeature("nomatchfeature2", SolrFeature.class.getCanonicalName(),
+ "{\"q\":\"foobarbat12345\",\"df\":\"title\"}");
+ loadModel(
+ "nomatchmodel",
+ RankSVMModel.class.getCanonicalName(),
+ new String[] {"nomatchfeature", "yesmatchfeature", "nomatchfeature2"},
+ "{\"weights\":{\"nomatchfeature\":1.0,\"yesmatchfeature\":1.1,\"nomatchfeature2\":1.1}}");
+
+ loadFeature("nomatchfeature3", SolrFeature.class.getCanonicalName(),
+ "{\"q\":\"foobarbat12345\",\"df\":\"title\"}");
+ loadModel("nomatchmodel2", RankSVMModel.class.getCanonicalName(),
+ new String[] {"nomatchfeature3"},
+ "{\"weights\":{\"nomatchfeature3\":1.0}}");
+
+ loadFeature("nomatchfeature4", SolrFeature.class.getCanonicalName(),
+ "noMatchFeaturesStore", "{\"q\":\"foobarbat12345\",\"df\":\"title\"}");
+ loadModel("nomatchmodel3", RankSVMModel.class.getCanonicalName(),
+ new String[] {"nomatchfeature4"}, "noMatchFeaturesStore",
+ "{\"weights\":{\"nomatchfeature4\":1.0}}");
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ @Test
+ public void testNoMatchSolrFeat1() throws Exception {
+ // Tests model with all no matching features but 1
+ SolrQuery query = new SolrQuery();
+ query.setQuery("*:*");
+ query.add("fl", "*, score,fv:[fv]");
+ query.add("rows", "4");
+ query.add("fv", "true");
+ query.add("rq", "{!ltr model=nomatchmodel reRankDocs=4}");
+
+ SolrQuery yesMatchFeatureQuery = new SolrQuery();
+ yesMatchFeatureQuery.setQuery("title:w1");
+ yesMatchFeatureQuery.add("fl", "score");
+ yesMatchFeatureQuery.add("rows", "4");
+ String res = restTestHarness.query("/query"
+ + yesMatchFeatureQuery.toQueryString());
+ System.out.println(res);
+ Map jsonParse = (Map) ObjectBuilder
+ .fromJSON(res);
+ Double doc0Score = (Double) ((Map) ((ArrayList) ((Map) jsonParse
+ .get("response")).get("docs")).get(0)).get("score");
+
+ res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score=="
+ + doc0Score * 1.1);
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[0]/fv=='yesmatchfeature:" + doc0Score + "'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='2'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/score==0.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/fv==''");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='3'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/score==0.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/fv==''");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='4'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/score==0.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/fv==''");
+ }
+
+ @Test
+ public void testNoMatchSolrFeat2() throws Exception {
+ // Tests model with all no matching features, but 1 non-modal matching
+ // feature for logging
+ SolrQuery query = new SolrQuery();
+ query.setQuery("*:*");
+ query.add("fl", "*, score,fv:[fv]");
+ query.add("rows", "4");
+ query.add("fv", "true");
+ query.add("rq", "{!ltr model=nomatchmodel2 reRankDocs=4}");
+
+ SolrQuery yesMatchFeatureQuery = new SolrQuery();
+ yesMatchFeatureQuery.setQuery("title:w1");
+ yesMatchFeatureQuery.add("fl", "score");
+ yesMatchFeatureQuery.add("rows", "4");
+ String res = restTestHarness.query("/query"
+ + yesMatchFeatureQuery.toQueryString());
+ System.out.println(res);
+ Map jsonParse = (Map) ObjectBuilder
+ .fromJSON(res);
+ Double doc0Score = (Double) ((Map) ((ArrayList) ((Map) jsonParse
+ .get("response")).get("docs")).get(0)).get("score");
+
+ res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==0.0");
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[0]/fv=='yesmatchfeature:" + doc0Score + "'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/score==0.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/fv==''");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/score==0.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/fv==''");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/score==0.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/fv==''");
+ }
+
+ @Test
+ public void testNoMatchSolrFeat3() throws Exception {
+ // Tests model with all no matching features
+ SolrQuery query = new SolrQuery();
+ query.setQuery("*:*");
+ query.add("fl", "*, score,fv:[fv]");
+ query.add("rows", "4");
+ query.add("fv", "true");
+ query.add("rq", "{!ltr model=nomatchmodel3 reRankDocs=4}");
+
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==0.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/fv==''");
+ }
+
+ @Test
+ public void testNoMatchSolrFeat4() throws Exception {
+ // Tests model with all no matching features but expects a non 0 score
+ loadModel(
+ "nomatchmodel4",
+ LambdaMARTModel.class.getCanonicalName(),
+ new String[] {"nomatchfeature4"},
+ "noMatchFeaturesStore",
+ "{\"trees\":[{\"weight\":1.0, \"tree\":{\"feature\": \"matchedTitle\",\"threshold\": 0.5,\"left\":{\"value\" : -10},\"right\":{\"value\" : 9}}}]}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("*:*");
+ query.add("fl", "*, score,fv:[fv]");
+ query.add("rows", "4");
+ query.add("fv", "true");
+ query.add("rq", "{!ltr model=nomatchmodel4 reRankDocs=4}");
+
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[0]/score==-10.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/fv==''");
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestOriginalScoreFeature.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestOriginalScoreFeature.java
new file mode 100644
index 000000000000..c90243815061
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestOriginalScoreFeature.java
@@ -0,0 +1,169 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
+import org.apache.solr.JSONTestUtil;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.common.util.StrUtils;
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.ranking.LTRComponent;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.noggit.ObjectBuilder;
+
+@SuppressCodecs({"Lucene3x", "Lucene41", "Lucene40", "Appending"})
+public class TestOriginalScoreFeature extends TestRerankBase {
+
+ @BeforeClass
+ public static void before() throws Exception {
+ setuptest("solrconfig-ltr.xml", "schema-ltr.xml");
+
+ assertU(adoc("id", "1", "title", "w1"));
+ assertU(adoc("id", "2", "title", "w2"));
+ assertU(adoc("id", "3", "title", "w3"));
+ assertU(adoc("id", "4", "title", "w4"));
+ assertU(adoc("id", "5", "title", "w5"));
+ assertU(adoc("id", "6", "title", "w1 w2"));
+ assertU(adoc("id", "7", "title", "w1 w2 w3 w4 w5"));
+ assertU(adoc("id", "8", "title", "w1 w1 w1 w2 w2"));
+ assertU(commit());
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ @Test
+ public void testOriginalScore() throws Exception {
+ loadFeature("score", OriginalScoreFeature.class.getCanonicalName(), "{}");
+
+ loadModel("originalScore", RankSVMModel.class.getCanonicalName(),
+ new String[] {"score"}, "{\"weights\":{\"score\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+ query.add("wt", "json");
+
+ // String res = restTestHarness.query("/query" + query.toQueryString());
+ // System.out.println(res);
+
+ // Normal term match
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='8'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='7'");
+
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ Map jsonParse = (Map) ObjectBuilder
+ .fromJSON(res);
+ String doc0Score = ((Double) ((Map) ((ArrayList) ((Map) jsonParse
+ .get("response")).get("docs")).get(0)).get("score")).toString();
+ String doc1Score = ((Double) ((Map) ((ArrayList) ((Map) jsonParse
+ .get("response")).get("docs")).get(1)).get("score")).toString();
+ String doc2Score = ((Double) ((Map) ((ArrayList) ((Map) jsonParse
+ .get("response")).get("docs")).get(2)).get("score")).toString();
+ String doc3Score = ((Double) ((Map) ((ArrayList) ((Map) jsonParse
+ .get("response")).get("docs")).get(3)).get("score")).toString();
+
+ query.add("fl", "[fv]");
+ query.add(LTRComponent.LTRParams.FV, "true");
+ query.add("rq", "{!ltr model=originalScore reRankDocs=4}");
+
+ // res = restTestHarness.query("/query" + query.toQueryString());
+ // System.out.println(res);
+
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score=="
+ + doc0Score);
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='8'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/score=="
+ + doc1Score);
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/score=="
+ + doc2Score);
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='7'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/score=="
+ + doc3Score);
+ }
+
+ @Test
+ public void testOriginalScoreWithNonScoringFeatures() throws Exception {
+ loadFeature("origScore", OriginalScoreFeature.class.getCanonicalName(),
+ "store2", "{}");
+ loadFeature("c2", ValueFeature.class.getCanonicalName(), "store2",
+ "{\"value\":2.0}");
+
+ loadModel("origScore", RankSVMModel.class.getCanonicalName(),
+ new String[] {"origScore"}, "store2",
+ "{\"weights\":{\"origScore\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score, fv:[fv]");
+ query.add("rows", "4");
+ query.add("wt", "json");
+ query.add(LTRComponent.LTRParams.FV, "true");
+ query.add("rq", "{!ltr model=origScore reRankDocs=4}");
+
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ Map jsonParse = (Map) ObjectBuilder
+ .fromJSON(res);
+ String doc0Score = ((Double) ((Map) ((ArrayList) ((Map) jsonParse
+ .get("response")).get("docs")).get(0)).get("score")).toString();
+ String doc1Score = ((Double) ((Map) ((ArrayList) ((Map) jsonParse
+ .get("response")).get("docs")).get(1)).get("score")).toString();
+ String doc2Score = ((Double) ((Map) ((ArrayList) ((Map) jsonParse
+ .get("response")).get("docs")).get(2)).get("score")).toString();
+ String doc3Score = ((Double) ((Map) ((ArrayList) ((Map) jsonParse
+ .get("response")).get("docs")).get(3)).get("score")).toString();
+ System.out.println(doc0Score);
+
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[0]/fv=='origScore:" + doc0Score + ";c2:2.0'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='8'");
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[1]/fv=='origScore:" + doc1Score + ";c2:2.0'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[2]/fv=='origScore:" + doc2Score + ";c2:2.0'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='7'");
+ assertJQ("/query" + query.toQueryString(),
+ "/response/docs/[3]/fv=='origScore:" + doc3Score + ";c2:2.0'");
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestQueryFeature.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestQueryFeature.java
new file mode 100644
index 000000000000..ecd3b01dc313
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestQueryFeature.java
@@ -0,0 +1,99 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
+import org.apache.solr.ltr.TestRerankBase;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+@SuppressCodecs({"Lucene3x", "Lucene41", "Lucene40", "Appending"})
+public class TestQueryFeature extends TestRerankBase {
+
+ @BeforeClass
+ public static void before() throws Exception {
+ setuptest("solrconfig-ltr.xml", "schema-ltr.xml");
+
+ assertU(adoc("id", "1", "title", "w1", "description", "w1", "popularity",
+ "1"));
+ assertU(adoc("id", "2", "title", "w2 2asd asdd didid", "description",
+ "w2 2asd asdd didid", "popularity", "2"));
+ assertU(adoc("id", "3", "title", "w3", "description", "w3", "popularity",
+ "3"));
+ assertU(adoc("id", "4", "title", "w4", "description", "w4", "popularity",
+ "4"));
+ assertU(adoc("id", "5", "title", "w5", "description", "w5", "popularity",
+ "5"));
+ assertU(adoc("id", "6", "title", "w1 w2", "description", "w1 w2",
+ "popularity", "6"));
+ assertU(adoc("id", "7", "title", "w1 w2 w3 w4 w5", "description",
+ "w1 w2 w3 w4 w5 w8", "popularity", "7"));
+ assertU(adoc("id", "8", "title", "w1 w1 w1 w2 w2 w8", "description",
+ "w1 w1 w1 w2 w2", "popularity", "8"));
+ assertU(commit());
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ /*
+ * @Test public void testEdisMaxSolrFeatureFilterQ() throws Exception {
+ * //before(); loadFeature("SomeEdisMaxFQ",
+ * SolrFeature.class.getCanonicalName(),
+ * "{\"fq\":[\"{!edismax qf='title description' pf='description' mm=100% boost='pow(popularity, 0.1)' v='w1' tie=0.1}\"]}"
+ * );
+ *
+ * loadModel("EdisMax-modelFQ", RankSVMModel.class.getCanonicalName(), new
+ * String[] { "SomeEdisMaxFQ" }, "{\"weights\":{\"SomeEdisMaxFQ\":1.0}}");
+ *
+ * SolrQuery query = new SolrQuery(); query.setQuery("title:w1");
+ * query.add("fl", "*, score"); query.add("rows", "4"); query.add("rq",
+ * "{!ltr model=EdisMax-modelFQ reRankDocs=4}"); query.set("debugQuery",
+ * "on"); String res = restTestHarness.query("/query?" + query.toString());
+ * System.out.println(res); assertJQ("/query?" + query.toString(),
+ * "/response/numFound/==4"); //aftertest(); }
+ *
+ *
+ * public void testUserTermScoreWithFQ() throws Exception { before();
+ * loadFeature("SomeTermFQ", SolrFeature.class.getCanonicalName(),
+ * "{\"fq\":[\"{!terms f=popularity}88888\"]}"); loadModel("Term-modelFQ",
+ * RankSVMModel.class.getCanonicalName(), new String[] { "SomeTermFQ" },
+ * "{\"weights\":{\"SomeTermFQ\":1.5}}"); SolrQuery query = new SolrQuery();
+ * query.setQuery("title:w1"); query.add("fl", "*, score"); query.add("rows",
+ * "4"); query.add("rq", "{!ltr model=Term-modelFQ reRankDocs=4}");
+ * query.set("debugQuery", "on"); String res = restTestHarness.query("/query?"
+ * + query.toString()); System.out.println(res); assertJQ("/query?" +
+ * query.toString(), "/response/numFound/==4"); assertJQ("/query?" +
+ * query.toString(), "/response/docs/[0]/score==0.0"); assertJQ("/query?" +
+ * query.toString(), "/response/docs/[1]/score==0.0"); aftertest(); }
+ *
+ * public void testUserTermScorerQWithQuery() throws Exception { before();
+ * loadFeature("matchedTitle", SolrFeature.class.getCanonicalName(),
+ * "{\"q\":\"title:QUERY\"}"); loadModel("Term-matchedTitle",
+ * RankSVMModel.class.getCanonicalName(), new String[] { "matchedTitle" },
+ * "{\"weights\":{\"matchedTitle\":1.0}}"); SolrQuery query = new SolrQuery();
+ * query.setQuery("title:w1"); query.add("fl", "*, score"); query.add("rows",
+ * "4"); query.add("rq", "{!ltr model=Term-matchedTitle reRankDocs=4}");
+ * query.set("debugQuery", "on"); String res = restTestHarness.query("/query?"
+ * + query.toString()); System.out.println(res); assertJQ("/query?" +
+ * query.toString(), "/response/numFound/==4"); aftertest(); }
+ */
+
+}
\ No newline at end of file
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestRankingFeature.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestRankingFeature.java
new file mode 100644
index 000000000000..79f0630b75a4
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestRankingFeature.java
@@ -0,0 +1,80 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.Test;
+
+public class TestRankingFeature extends TestQueryFeature {
+ @Test
+ public void testRankingSolrFeature() throws Exception {
+ // before();
+ loadFeature("powpularityS", SolrFeature.class.getCanonicalName(),
+ "{\"q\":\"{!func}pow(popularity,2)\"}");
+ loadFeature("unpopularityS", SolrFeature.class.getCanonicalName(),
+ "{\"q\":\"{!func}div(1,popularity)\"}");
+
+ loadModel("powpularityS-model", RankSVMModel.class.getCanonicalName(),
+ new String[] {"powpularityS"}, "{\"weights\":{\"powpularityS\":1.0}}");
+ loadModel("unpopularityS-model", RankSVMModel.class.getCanonicalName(),
+ new String[] {"unpopularityS"}, "{\"weights\":{\"unpopularityS\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='8'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='7'");
+ // Normal term match
+
+ query.add("rq", "{!ltr model=powpularityS-model reRankDocs=4}");
+ query.set("debugQuery", "on");
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='8'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==64.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='7'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/score==49.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='6'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/score==36.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='1'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/score==1.0");
+
+ query.remove("rq");
+ query.add("rq", "{!ltr model=unpopularityS-model reRankDocs=4}");
+
+ query.set("debugQuery", "on");
+ res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='1'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==1.0");
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/id=='6'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[2]/id=='7'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[3]/id=='8'");
+ // aftertest();
+
+ }
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestUserTermScoreWithQ.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestUserTermScoreWithQ.java
new file mode 100644
index 000000000000..d978a6bcf370
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestUserTermScoreWithQ.java
@@ -0,0 +1,45 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.Test;
+
+public class TestUserTermScoreWithQ extends TestQueryFeature {
+ @Test
+ public void testUserTermScoreWithQ() throws Exception {
+ // before();
+ loadFeature("SomeTermQ", SolrFeature.class.getCanonicalName(),
+ "{\"q\":\"{!terms f=popularity}88888\"}");
+ loadModel("Term-modelQ", RankSVMModel.class.getCanonicalName(),
+ new String[] {"SomeTermQ"}, "{\"weights\":{\"SomeTermQ\":1.0}}");
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+ query.add("rq", "{!ltr model=Term-modelQ reRankDocs=4}");
+ query.set("debugQuery", "on");
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==0.0");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/score==0.0");
+ // aftertest();
+ }
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestUserTermScorerQuery.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestUserTermScorerQuery.java
new file mode 100644
index 000000000000..4e3a0a7e731d
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestUserTermScorerQuery.java
@@ -0,0 +1,45 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.Test;
+
+public class TestUserTermScorerQuery extends TestQueryFeature {
+ @Test
+ public void testUserTermScorerQuery() throws Exception {
+ // before();
+ loadFeature("matchedTitleDFExt", SolrFeature.class.getCanonicalName(),
+ "{\"q\":\"${user_query}\",\"df\":\"title\"}");
+ loadModel("Term-matchedTitleDFExt", RankSVMModel.class.getCanonicalName(),
+ new String[] {"matchedTitleDFExt"},
+ "{\"weights\":{\"matchedTitleDFExt\":1.1}}");
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+ query.add("rq",
+ "{!ltr model=Term-matchedTitleDFExt reRankDocs=4 efi.user_query=w8}");
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='8'");
+ // aftertest();
+ }
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestUserTermScorereQDF.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestUserTermScorereQDF.java
new file mode 100644
index 000000000000..a7928d703f38
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestUserTermScorereQDF.java
@@ -0,0 +1,46 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.Test;
+
+public class TestUserTermScorereQDF extends TestQueryFeature {
+ @Test
+ public void testUserTermScorerQWithDF() throws Exception {
+ // before();
+ loadFeature("matchedTitleDF", SolrFeature.class.getCanonicalName(),
+ "{\"q\":\"w5\",\"df\":\"title\"}");
+ loadModel("Term-matchedTitleDF", RankSVMModel.class.getCanonicalName(),
+ new String[] {"matchedTitleDF"},
+ "{\"weights\":{\"matchedTitleDF\":1.0}}");
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score");
+ query.add("rows", "2");
+ query.add("rq", "{!ltr model=Term-matchedTitleDF reRankDocs=4}");
+ query.set("debugQuery", "on");
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==4");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/id=='7'");
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[1]/score==0.0");
+ // aftertest();
+ }
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestValueFeature.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestValueFeature.java
new file mode 100644
index 000000000000..82283e18e590
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/impl/TestValueFeature.java
@@ -0,0 +1,149 @@
+package org.apache.solr.ltr.feature.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.ranking.LTRComponent;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+@SuppressCodecs({"Lucene3x", "Lucene41", "Lucene40", "Appending"})
+public class TestValueFeature extends TestRerankBase {
+
+ @BeforeClass
+ public static void before() throws Exception {
+ setuptest("solrconfig-ltr.xml", "schema-ltr.xml");
+
+ assertU(adoc("id", "1", "title", "w1"));
+ assertU(adoc("id", "2", "title", "w2"));
+ assertU(adoc("id", "3", "title", "w3"));
+ assertU(adoc("id", "4", "title", "w4"));
+ assertU(adoc("id", "5", "title", "w5"));
+ assertU(adoc("id", "6", "title", "w1 w2"));
+ assertU(adoc("id", "7", "title", "w1 w2 w3 w4 w5"));
+ assertU(adoc("id", "8", "title", "w1 w1 w1 w2 w2"));
+ assertU(commit());
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ @Test(expected = Exception.class)
+ public void testValueFeature1() throws Exception {
+ loadFeature("c1", ValueFeature.class.getCanonicalName(), "{}");
+ }
+
+ @Test(expected = Exception.class)
+ public void testValueFeature2() throws Exception {
+ loadFeature("c2", ValueFeature.class.getCanonicalName(), "{\"value\":\"\"}");
+ }
+
+ @Test(expected = Exception.class)
+ public void testValueFeature3() throws Exception {
+ loadFeature("c2", ValueFeature.class.getCanonicalName(),
+ "{\"value\":\" \"}");
+ }
+
+ @Test
+ public void testValueFeature4() throws Exception {
+ loadFeature("c3", ValueFeature.class.getCanonicalName(), "c3",
+ "{\"value\":2}");
+ loadModel("m3", RankSVMModel.class.getCanonicalName(), new String[] {"c3"},
+ "c3", "{\"weights\":{\"c3\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+ query.add("wt", "json");
+ query.add("rq", "{!ltr model=m3 reRankDocs=4}");
+
+ // String res = restTestHarness.query("/query" + query.toQueryString());
+ // System.out.println("\n\n333333\n\n" + res);
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==2.0");
+ }
+
+ @Test
+ public void testValueFeature5() throws Exception {
+ loadFeature("c4", ValueFeature.class.getCanonicalName(), "c4",
+ "{\"value\":\"2\"}");
+ loadModel("m4", RankSVMModel.class.getCanonicalName(), new String[] {"c4"},
+ "c4", "{\"weights\":{\"c4\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+ query.add("wt", "json");
+ query.add("rq", "{!ltr model=m4 reRankDocs=4}");
+
+ // String res = restTestHarness.query("/query" + query.toQueryString());
+ // System.out.println("\n\n44444\n\n" + res);
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==2.0");
+ }
+
+ @Test
+ public void testValueFeature6() throws Exception {
+ loadFeature("c5", ValueFeature.class.getCanonicalName(), "c5",
+ "{\"value\":\"${val5}\"}");
+ loadModel("m5", RankSVMModel.class.getCanonicalName(), new String[] {"c5"},
+ "c5", "{\"weights\":{\"c5\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score,fvonly:[fvonly]");
+ query.add("rows", "4");
+ query.add("wt", "json");
+ query.add(LTRComponent.LTRParams.FV, "true");
+ query.add("rq", "{!ltr model=m5 reRankDocs=4}");
+
+ // String res = restTestHarness.query("/query" + query.toQueryString());
+ // System.out.println(res);
+
+ // No efi.val passed in
+ assertJQ("/query" + query.toQueryString(), "/responseHeader/status==400");
+ }
+
+ @Test
+ public void testValueFeature7() throws Exception {
+ loadFeature("c6", ValueFeature.class.getCanonicalName(), "c6",
+ "{\"value\":\"${val6}\"}");
+ loadModel("m6", RankSVMModel.class.getCanonicalName(), new String[] {"c6"},
+ "c6", "{\"weights\":{\"c6\":1.0}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:w1");
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+ query.add("wt", "json");
+ query.add("rq", "{!ltr model=m6 reRankDocs=4 efi.val6='2'}");
+
+ // String res = restTestHarness.query("/query" + query.toQueryString());
+ // System.out.println(res);
+
+ assertJQ("/query" + query.toQueryString(), "/response/docs/[0]/score==2.0");
+ }
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/norm/impl/TestMinMaxNormalizer.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/norm/impl/TestMinMaxNormalizer.java
new file mode 100644
index 000000000000..b06c09afaf41
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/norm/impl/TestMinMaxNormalizer.java
@@ -0,0 +1,89 @@
+package org.apache.solr.ltr.feature.norm.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.solr.ltr.feature.norm.Normalizer;
+import org.apache.solr.ltr.rest.ManagedModelStore;
+import org.apache.solr.ltr.util.NamedParams;
+import org.apache.solr.ltr.util.NormalizerException;
+import org.junit.Test;
+
+public class TestMinMaxNormalizer {
+
+ @Test(expected = NormalizerException.class)
+ public void testInvalidMinMaxNoParams() throws NormalizerException {
+ ManagedModelStore.getNormalizerInstance(
+ MinMaxNormalizer.class.getCanonicalName(), new NamedParams());
+
+ }
+
+ @Test(expected = NormalizerException.class)
+ public void testInvalidMinMaxMissingMax() throws NormalizerException {
+
+ ManagedModelStore.getNormalizerInstance(
+ MinMaxNormalizer.class.getCanonicalName(),
+ new NamedParams().add("min", 0f));
+
+ }
+
+ @Test(expected = NormalizerException.class)
+ public void testInvalidMinMaxMissingMin() throws NormalizerException {
+
+ ManagedModelStore.getNormalizerInstance(
+ MinMaxNormalizer.class.getCanonicalName(),
+ new NamedParams().add("max", 0f));
+
+ }
+
+ @Test(expected = NormalizerException.class)
+ public void testInvalidMinMaxMissingInvalidDelta() throws NormalizerException {
+ ManagedModelStore.getNormalizerInstance(
+ MinMaxNormalizer.class.getCanonicalName(),
+ new NamedParams().add("max", 0f).add("min", 10f));
+ }
+
+ @Test(expected = NormalizerException.class)
+ public void testInvalidMinMaxMissingInvalidDelta2()
+ throws NormalizerException {
+
+ ManagedModelStore.getNormalizerInstance(
+ "org.apache.solr.ltr.feature.norm.impl.MinMaxNormalizer",
+ new NamedParams().add("min", 10f).add("max", 10f));
+ // min == max
+ }
+
+ @Test
+ public void testNormalizer() throws NormalizerException {
+ Normalizer n = ManagedModelStore.getNormalizerInstance(
+ MinMaxNormalizer.class.getCanonicalName(),
+ new NamedParams().add("min", 5f).add("max", 10f));
+
+ float value = 8;
+ assertEquals((value - 5f) / (10f - 5f), n.normalize(value), 0.0001);
+ value = 100;
+ assertEquals((value - 5f) / (10f - 5f), n.normalize(value), 0.0001);
+ value = 150;
+ assertEquals((value - 5f) / (10f - 5f), n.normalize(value), 0.0001);
+ value = -1;
+ assertEquals((value - 5f) / (10f - 5f), n.normalize(value), 0.0001);
+ value = 5;
+ assertEquals((value - 5f) / (10f - 5f), n.normalize(value), 0.0001);
+ }
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/norm/impl/TestStandardNormalizer.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/norm/impl/TestStandardNormalizer.java
new file mode 100644
index 000000000000..2178ff064168
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/feature/norm/impl/TestStandardNormalizer.java
@@ -0,0 +1,81 @@
+package org.apache.solr.ltr.feature.norm.impl;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.solr.ltr.feature.norm.Normalizer;
+import org.apache.solr.ltr.rest.ManagedModelStore;
+import org.apache.solr.ltr.util.NamedParams;
+import org.apache.solr.ltr.util.NormalizerException;
+import org.junit.Test;
+
+public class TestStandardNormalizer {
+
+ @Test(expected = NormalizerException.class)
+ public void testNormalizerNoParams() throws NormalizerException {
+ ManagedModelStore.getNormalizerInstance(
+ StandardNormalizer.class.getCanonicalName(), new NamedParams());
+
+ }
+
+ @Test(expected = NormalizerException.class)
+ public void testInvalidSTD() throws NormalizerException {
+
+ ManagedModelStore.getNormalizerInstance(
+ StandardNormalizer.class.getCanonicalName(),
+ new NamedParams().add("std", 0f));
+
+ }
+
+ @Test(expected = NormalizerException.class)
+ public void testInvalidSTD2() throws NormalizerException {
+
+ ManagedModelStore.getNormalizerInstance(
+ StandardNormalizer.class.getCanonicalName(),
+ new NamedParams().add("std", -1f));
+
+ }
+
+ @Test(expected = NormalizerException.class)
+ public void testInvalidSTD3() throws NormalizerException {
+ ManagedModelStore.getNormalizerInstance(
+ StandardNormalizer.class.getCanonicalName(),
+ new NamedParams().add("avg", 1f).add("std", 0f));
+ }
+
+ @Test
+ public void testNormalizer() throws NormalizerException {
+ Normalizer identity = ManagedModelStore.getNormalizerInstance(
+ StandardNormalizer.class.getCanonicalName(),
+ new NamedParams().add("avg", 0f).add("std", 1f));
+
+ float value = 8;
+ assertEquals(value, identity.normalize(value), 0.0001);
+ value = 150;
+ assertEquals(value, identity.normalize(value), 0.0001);
+ Normalizer norm = ManagedModelStore.getNormalizerInstance(
+ StandardNormalizer.class.getCanonicalName(),
+ new NamedParams().add("avg", 10f).add("std", 1.5f));
+
+ for (float v : new float[] {10f, 20f, 25f, 30f, 31f, 40f, 42f, 100f,
+ 10000000f}) {
+ assertEquals((v - 10f) / (1.5f), norm.normalize(v), 0.0001);
+ }
+ }
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestLTRQParserExplain.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestLTRQParserExplain.java
new file mode 100644
index 000000000000..645c010993f8
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestLTRQParserExplain.java
@@ -0,0 +1,142 @@
+package org.apache.solr.ltr.ranking;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.lang.invoke.MethodHandles;
+
+import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.TestRerankBase;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressSSL
+public class TestLTRQParserExplain extends TestRerankBase {
+
+ @SuppressWarnings("unused")
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ @BeforeClass
+ public static void setup() throws Exception {
+ setuptest();
+ loadFeatures("features-store-test-model.json");
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ }
+
+ @Test
+ public void checkReranked() throws Exception {
+
+ loadModel("svm", RankSVMModel.class.getCanonicalName(), new String[] {
+ "constant1", "constant2"},
+ "{\"weights\":{\"constant1\":1.5,\"constant2\":3.5}}");
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:bloomberg");
+ query.setParam("debugQuery", "on");
+ query.add("rows", "2");
+ query.add("rq", "{!ltr reRankDocs=2 model=svm}");
+ query.add("fl", "*,score");
+ // query.add("wt","xml");
+ // System.out.println(restTestHarness.query("/query" +
+ // query.toQueryString()));
+ // query.add("wt","json");
+ // assertJQ(
+ // "/query" + query.toQueryString(),
+ // "/debug/explain/7=='\n8.5 = svm [ org.apache.solr.ltr.ranking.RankSVMModel ] model applied to features, sum of:\n 1.5 = prod of:\n 1.5 = weight on feature [would be cool to have the name :)]\n 1.0 = ValueFeature [name=constant1 value=1.0]\n 7.0 = prod of:\n 3.5 = weight on feature [would be cool to have the name :)]\n 2.0 = ValueFeature [name=constant2 value=2.0]\n'");
+ query.add("wt", "xml");
+ System.out.println(restTestHarness.query("/query" + query.toQueryString()));
+ }
+
+ @Test
+ public void checkReranked2() throws Exception {
+ loadModel("svm2", RankSVMModel.class.getCanonicalName(), new String[] {
+ "constant1", "constant2", "pop"},
+ "{\"weights\":{\"pop\":1.0,\"constant1\":1.5,\"constant2\":3.5}}");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:bloomberg");
+ query.setParam("debugQuery", "on");
+ query.add("rows", "2");
+ query.add("rq", "{!ltr reRankDocs=2 model=svm2}");
+ query.add("fl", "*,score");
+
+ assertJQ(
+ "/query" + query.toQueryString(),
+ "/debug/explain/9=='\n13.5 = svm2 [ org.apache.solr.ltr.ranking.RankSVMModel ] model applied to features, sum of:\n 1.5 = prod of:\n 1.5 = weight on feature [would be cool to have the name :)]\n 1.0 = ValueFeature [name=constant1 value=1.0]\n 7.0 = prod of:\n 3.5 = weight on feature [would be cool to have the name :)]\n 2.0 = ValueFeature [name=constant2 value=2.0]\n 5.0 = prod of:\n 1.0 = weight on feature [would be cool to have the name :)]\n 5.0 = FieldValueFeature [name=pop fields=[popularity]]\n'");
+ query.add("wt", "xml");
+ System.out.println(restTestHarness.query("/query" + query.toQueryString()));
+ }
+
+ @Test
+ public void checkReranked3() throws Exception {
+ loadFeatures("features-ranksvm.json");
+ loadModels("ranksvm-model.json");
+
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:bloomberg");
+ query.setParam("debugQuery", "on");
+ query.add("rows", "4");
+ query.add("rq", "{!ltr reRankDocs=4 model=6029760550880411648}");
+ query.add("fl", "*,score");
+ query.add("wt", "xml");
+
+ System.out.println(restTestHarness.query("/query" + query.toQueryString()));
+ query.remove("wt");
+ query.add("wt", "json");
+ assertJQ(
+ "/query" + query.toQueryString(),
+ "/debug/explain/7=='\n3.5116758 = 6029760550880411648 [ org.apache.solr.ltr.ranking.RankSVMModel ] model applied to features, sum of:\n 0.0 = prod of:\n 0.0 = weight on feature [would be cool to have the name :)]\n 1.0 = ValueFeature [name=title value=1.0]\n 0.2 = prod of:\n 0.1 = weight on feature [would be cool to have the name :)]\n 2.0 = ValueFeature [name=description value=2.0]\n 0.4 = prod of:\n 0.2 = weight on feature [would be cool to have the name :)]\n 2.0 = ValueFeature [name=keywords value=2.0]\n 0.09 = prod of:\n 0.3 = weight on feature [would be cool to have the name :)]\n 0.3 = normalized using org.apache.solr.ltr.feature.norm.impl.MinMaxNormalizer [params {min=0.0, max=10.0}]\n 3.0 = ValueFeature [name=popularity value=3.0]\n 1.6 = prod of:\n 0.4 = weight on feature [would be cool to have the name :)]\n 4.0 = ValueFeature [name=text value=4.0]\n 0.6156155 = prod of:\n 0.1231231 = weight on feature [would be cool to have the name :)]\n 5.0 = ValueFeature [name=queryIntentPerson value=5.0]\n 0.60606056 = prod of:\n 0.12121211 = weight on feature [would be cool to have the name :)]\n 5.0 = ValueFeature [name=queryIntentCompany value=5.0]\n'}");
+ assertJQ(
+ "/query" + query.toQueryString(),
+ "/debug/explain/9=='\n3.5116758 = 6029760550880411648 [ org.apache.solr.ltr.ranking.RankSVMModel ] model applied to features, sum of:\n 0.0 = prod of:\n 0.0 = weight on feature [would be cool to have the name :)]\n 1.0 = ValueFeature [name=title value=1.0]\n 0.2 = prod of:\n 0.1 = weight on feature [would be cool to have the name :)]\n 2.0 = ValueFeature [name=description value=2.0]\n 0.4 = prod of:\n 0.2 = weight on feature [would be cool to have the name :)]\n 2.0 = ValueFeature [name=keywords value=2.0]\n 0.09 = prod of:\n 0.3 = weight on feature [would be cool to have the name :)]\n 0.3 = normalized using org.apache.solr.ltr.feature.norm.impl.MinMaxNormalizer [params {min=0.0, max=10.0}]\n 3.0 = ValueFeature [name=popularity value=3.0]\n 1.6 = prod of:\n 0.4 = weight on feature [would be cool to have the name :)]\n 4.0 = ValueFeature [name=text value=4.0]\n 0.6156155 = prod of:\n 0.1231231 = weight on feature [would be cool to have the name :)]\n 5.0 = ValueFeature [name=queryIntentPerson value=5.0]\n 0.60606056 = prod of:\n 0.12121211 = weight on feature [would be cool to have the name :)]\n 5.0 = ValueFeature [name=queryIntentCompany value=5.0]\n'}");
+ }
+
+ // @Test
+ // public void checkfq() throws Exception {
+ //
+ // System.out.println("after: \n" + restTestHarness.query("/config/managed"));
+ //
+ // FunctionQueryFeature fq = new FunctionQueryFeature("log(popularity)");
+ // assertJPut(featureEndpoint, gson.toJson(fq), "/responseHeader/status==0");
+ // fq = new FunctionQueryFeature("tf_title_bloomberg",
+ // "tf(title,'bloomberg')");
+ // assertJPut(featureEndpoint, gson.toJson(fq), "/responseHeader/status==0");
+ // // fq.(new NamedParams().add("fq", "log(popularity)"));
+ //
+ // ModelMetadata model = new ModelMetadata("sum3",
+ // SumModel.class.getCanonicalName(), getFeatures(new String[] {
+ // "log(popularity)", "tf_title_bloomberg", "t1", "t2" }));
+ //
+ // assertJPut(modelEndpoint, gson.toJson(model), "/responseHeader/status==0");
+ //
+ // SolrQuery query = new SolrQuery();
+ // query.setQuery("title:bloomberg");
+ // query.setParam("debugQuery", "on");
+ // query.add("rows", "4");
+ // query.add("rq", "{!ltr reRankDocs=4 model=sum3}");
+ // query.add("fl", "*,score");
+ // System.out.println(restTestHarness.query("/query" +
+ // query.toQueryString()));
+ // }
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestLTRQParserPlugin.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestLTRQParserPlugin.java
new file mode 100644
index 000000000000..607d6bff74e4
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestLTRQParserPlugin.java
@@ -0,0 +1,139 @@
+package org.apache.solr.ltr.ranking;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.lang.invoke.MethodHandles;
+
+import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.ltr.TestRerankBase;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressCodecs({"Lucene3x", "Lucene41", "Lucene40", "Appending"})
+public class TestLTRQParserPlugin extends TestRerankBase {
+
+ @SuppressWarnings("unused")
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ @BeforeClass
+ public static void before() throws Exception {
+ setuptest("solrconfig-ltr.xml", "schema-ltr.xml");
+ // store = getModelStore();
+ bulkIndex();
+
+ loadFeatures("features-ranksvm.json");
+ loadModels("ranksvm-model.json");
+ }
+
+ @AfterClass
+ public static void after() throws Exception {
+ aftertest();
+ // store.clear();
+ }
+
+ @Test
+ public void ltrModelIdMissingTest() throws Exception {
+ String solrQuery = "_query_:{!edismax qf='title' mm=100% v='bloomberg' tie=0.1}";
+ // SolrQueryRequest req = req("q", solrQuery, "rows", "4", "fl", "*,score",
+ // "fv", "true", "rq", "{!ltr reRankDocs=100}");
+ SolrQuery query = new SolrQuery();
+ query.setQuery(solrQuery);
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+ query.add("fv", "true");
+ query.add("rq", "{!ltr reRankDocs=100}");
+
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ assert (res.contains("Must provide model in the request"));
+
+ // h.query("/query", req);
+
+ /*
+ * String solrQuery =
+ * "_query_:{!edismax qf='title' mm=100% v='bloomberg' tie=0.1}";
+ * SolrQueryRequest req = req("q", solrQuery, "rows", "4", "fl", "*,score",
+ * "fv", "true", "rq", "{!ltr reRankDocs=100}");
+ *
+ * h.query("/query", req);
+ */
+ }
+
+ @Test
+ public void ltrModelIdDoesNotExistTest() throws Exception {
+ String solrQuery = "_query_:{!edismax qf='title' mm=100% v='bloomberg' tie=0.1}";
+ SolrQuery query = new SolrQuery();
+ query.setQuery(solrQuery);
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+ query.add("fv", "true");
+ query.add("rq", "{!ltr model=-1 reRankDocs=100}");
+
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ assert (res.contains("cannot find model"));
+ /*
+ * String solrQuery =
+ * "_query_:{!edismax qf='title' mm=100% v='bloomberg' tie=0.1}";
+ * SolrQueryRequest req = req("q", solrQuery, "rows", "4", "fl", "*,score",
+ * "fv", "true", "rq", "{!ltr model=-1 reRankDocs=100}");
+ *
+ * h.query("/query", req);
+ */
+ }
+
+ @Test
+ public void ltrMoreResultsThanReRankedTest() throws Exception {
+ String solrQuery = "_query_:{!edismax qf='title' mm=100% v='bloomberg' tie=0.1}";
+ SolrQuery query = new SolrQuery();
+ query.setQuery(solrQuery);
+ query.add("fl", "*, score");
+ query.add("rows", "4");
+ query.add("fv", "true");
+ query.add("rq", "{!ltr model=6029760550880411648 reRankDocs=3}");
+
+ String res = restTestHarness.query("/query" + query.toQueryString());
+ System.out.println(res);
+ assert (res.contains("Requesting more documents than being reranked."));
+ /*
+ * String solrQuery =
+ * "_query_:{!edismax qf='title' mm=100% v='bloomberg' tie=0.1}";
+ * SolrQueryRequest req = req("q", solrQuery, "rows", "999999", "fl",
+ * "*,score", "fv", "true", "rq",
+ * "{!ltr model=6029760550880411648 reRankDocs=100}");
+ *
+ * h.query("/query", req);
+ */
+ }
+
+ @Test
+ public void ltrNoResultsTest() throws Exception {
+ SolrQuery query = new SolrQuery();
+ query.setQuery("title:bloomberg23");
+ query.add("fl", "*,[fv]");
+ query.add("rows", "3");
+ query.add("debugQuery", "on");
+ query.add(LTRComponent.LTRParams.FV, "true");
+ query.add("rq", "{!ltr reRankDocs=3 model=6029760550880411648}");
+ assertJQ("/query" + query.toQueryString(), "/response/numFound/==0");
+ // assertJQ("/query?" + query.toString(), "/response/numFound/==0");
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestModelQuery.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestModelQuery.java
new file mode 100644
index 000000000000..e2f3d508ac3e
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestModelQuery.java
@@ -0,0 +1,234 @@
+package org.apache.solr.ltr.ranking;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FloatDocValuesField;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.ReaderUtil;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause.Occur;
+import org.apache.lucene.search.BooleanQuery.Builder;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.search.Weight;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
+import org.apache.solr.ltr.feature.impl.ValueFeature;
+import org.apache.solr.ltr.feature.norm.Normalizer;
+import org.apache.solr.ltr.util.FeatureException;
+import org.apache.solr.ltr.util.ModelException;
+import org.apache.solr.ltr.util.NamedParams;
+import org.junit.Test;
+
+@SuppressCodecs("Lucene3x")
+public class TestModelQuery extends LuceneTestCase {
+
+ private IndexSearcher getSearcher(IndexReader r) {
+ IndexSearcher searcher = newSearcher(r, false, false);
+ return searcher;
+ }
+
+ private static List makeFeatures(int[] featureIds) {
+ List features = new ArrayList<>();
+ for (int i : featureIds) {
+ ValueFeature f = new ValueFeature();
+ try {
+ f.init("f" + i, new NamedParams().add("value", i), i);
+ } catch (FeatureException e) {
+ e.printStackTrace();
+ }
+ features.add(f);
+ }
+ return features;
+ }
+
+ private static List makeNormalizedFeatures(int[] featureIds) {
+ List features = new ArrayList<>();
+ for (int i : featureIds) {
+ ValueFeature f = new ValueFeature();
+ f.name = "f" + i;
+ f.params = new NamedParams().add("value", i);
+ f.id = i;
+ f.norm = new Normalizer() {
+
+ @Override
+ public float normalize(float value) {
+ return 42.42f;
+ }
+ };
+ features.add(f);
+ }
+ return features;
+ }
+
+ private static NamedParams makeFeatureWeights(List features) {
+ NamedParams nameParams = new NamedParams();
+ HashMap modelWeights = new HashMap();
+ for (Feature feat : features) {
+ modelWeights.put(feat.name, 0.1);
+ }
+ if (modelWeights.isEmpty()) modelWeights.put("", 0.0);
+ nameParams.add("weights", modelWeights);
+ return nameParams;
+ }
+
+ private ModelQuery.ModelWeight performQuery(TopDocs hits,
+ IndexSearcher searcher, int docid, ModelQuery model) throws IOException,
+ ModelException {
+ List leafContexts = searcher.getTopReaderContext()
+ .leaves();
+ int n = ReaderUtil.subIndex(hits.scoreDocs[0].doc, leafContexts);
+ final LeafReaderContext context = leafContexts.get(n);
+ int deBasedDoc = hits.scoreDocs[0].doc - context.docBase;
+
+ Weight weight = searcher.createNormalizedWeight(model, true);
+ Scorer scorer = weight.scorer(context);
+
+ // rerank using the field final-score
+ scorer.iterator().advance(deBasedDoc);
+ float score = scorer.score();
+
+ // assertEquals(42.0f, score, 0.0001);
+ // assertTrue(weight instanceof AssertingWeight);
+ // (AssertingIndexSearcher)
+ assertTrue(weight instanceof ModelQuery.ModelWeight);
+ ModelQuery.ModelWeight modelWeight = (ModelQuery.ModelWeight) weight;
+ return modelWeight;
+
+ }
+
+ @Test
+ public void testModelQuery() throws IOException, ModelException {
+ Directory dir = newDirectory();
+ RandomIndexWriter w = new RandomIndexWriter(random(), dir);
+
+ Document doc = new Document();
+ doc.add(newStringField("id", "0", Field.Store.YES));
+ doc.add(newTextField("field", "wizard the the the the the oz",
+ Field.Store.NO));
+ doc.add(new FloatDocValuesField("final-score", 1.0f));
+
+ w.addDocument(doc);
+ doc = new Document();
+ doc.add(newStringField("id", "1", Field.Store.YES));
+ // 1 extra token, but wizard and oz are close;
+ doc.add(newTextField("field", "wizard oz the the the the the the",
+ Field.Store.NO));
+ doc.add(new FloatDocValuesField("final-score", 2.0f));
+ w.addDocument(doc);
+
+ IndexReader r = w.getReader();
+ w.close();
+
+ // Do ordinary BooleanQuery:
+ Builder bqBuilder = new Builder();
+ bqBuilder.add(new TermQuery(new Term("field", "wizard")), Occur.SHOULD);
+ bqBuilder.add(new TermQuery(new Term("field", "oz")), Occur.SHOULD);
+ IndexSearcher searcher = getSearcher(r);
+ // first run the standard query
+ TopDocs hits = searcher.search(bqBuilder.build(), 10);
+ assertEquals(2, hits.totalHits);
+ assertEquals("0", searcher.doc(hits.scoreDocs[0].doc).get("id"));
+ assertEquals("1", searcher.doc(hits.scoreDocs[1].doc).get("id"));
+
+ List features = makeFeatures(new int[] {0, 1, 2});
+ List allFeatures = makeFeatures(new int[] {0, 1, 2, 3, 4, 5, 6, 7,
+ 8, 9});
+ RankSVMModel meta = new RankSVMModel("test",
+ RankSVMModel.class.getCanonicalName(), features, "test", allFeatures,
+ makeFeatureWeights(features));
+
+ ModelQuery.ModelWeight modelWeight = performQuery(hits, searcher,
+ hits.scoreDocs[0].doc, new ModelQuery(meta));
+ assertEquals(3, modelWeight.modelFeatureValuesNormalized.length);
+ assertEquals(10, modelWeight.allFeatureValues.length);
+
+ for (int i = 0; i < 3; i++) {
+ assertEquals((float) i, modelWeight.modelFeatureValuesNormalized[i],
+ 0.0001);
+ }
+ for (int i = 0; i < 10; i++) {
+ assertEquals((float) i, modelWeight.allFeatureValues[i], 0.0001);
+ }
+
+ for (int i = 0; i < 10; i++) {
+ assertEquals("f" + i, modelWeight.allFeatureNames[i]);
+
+ }
+
+ int[] mixPositions = new int[] {8, 2, 4, 9, 0};
+ features = makeFeatures(mixPositions);
+ meta = new RankSVMModel("test", RankSVMModel.class.getCanonicalName(),
+ features, "test", allFeatures, makeFeatureWeights(features));
+
+ modelWeight = performQuery(hits, searcher, hits.scoreDocs[0].doc,
+ new ModelQuery(meta));
+ assertEquals(mixPositions.length,
+ modelWeight.modelFeatureValuesNormalized.length);
+
+ for (int i = 0; i < mixPositions.length; i++) {
+ assertEquals((float) mixPositions[i],
+ modelWeight.modelFeatureValuesNormalized[i], 0.0001);
+ }
+ for (int i = 0; i < 10; i++) {
+ assertEquals((float) i, modelWeight.allFeatureValues[i], 0.0001);
+ }
+
+ int[] noPositions = new int[] {};
+ features = makeFeatures(noPositions);
+ meta = new RankSVMModel("test", RankSVMModel.class.getCanonicalName(),
+ features, "test", allFeatures, makeFeatureWeights(features));
+
+ modelWeight = performQuery(hits, searcher, hits.scoreDocs[0].doc,
+ new ModelQuery(meta));
+ assertEquals(0, modelWeight.modelFeatureValuesNormalized.length);
+
+ // test normalizers
+ features = makeNormalizedFeatures(mixPositions);
+ RankSVMModel normMeta = new RankSVMModel("test",
+ RankSVMModel.class.getCanonicalName(), features, "test", allFeatures,
+ makeFeatureWeights(features));
+
+ modelWeight = performQuery(hits, searcher, hits.scoreDocs[0].doc,
+ new ModelQuery(normMeta));
+ assertEquals(mixPositions.length,
+ modelWeight.modelFeatureValuesNormalized.length);
+ for (int i = 0; i < mixPositions.length; i++) {
+ assertEquals(42.42f, modelWeight.modelFeatureValuesNormalized[i], 0.0001);
+ }
+ for (int i = 0; i < 10; i++) {
+ assertEquals((float) i, modelWeight.allFeatureValues[i], 0.0001);
+ }
+ r.close();
+ dir.close();
+
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestReRankingPipeline.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestReRankingPipeline.java
new file mode 100644
index 000000000000..cb90d1fc0886
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/ranking/TestReRankingPipeline.java
@@ -0,0 +1,286 @@
+package org.apache.solr.ltr.ranking;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FloatDocValuesField;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause.Occur;
+import org.apache.lucene.search.BooleanQuery.Builder;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.Scorer.ChildScorer;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
+import org.apache.solr.ltr.feature.ModelMetadata;
+import org.apache.solr.ltr.feature.impl.FieldValueFeature;
+import org.apache.solr.ltr.ranking.ModelQuery.ModelWeight;
+import org.apache.solr.ltr.ranking.ModelQuery.ModelWeight.ModelScorer;
+import org.apache.solr.ltr.util.NamedParams;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@SuppressCodecs("Lucene3x")
+public class TestReRankingPipeline extends LuceneTestCase {
+
+ private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private IndexSearcher getSearcher(IndexReader r) {
+ IndexSearcher searcher = newSearcher(r);
+
+ return searcher;
+ }
+
+ private static List makeFieldValueFeatures(int[] featureIds,
+ String field) {
+ List features = new ArrayList<>();
+ for (int i : featureIds) {
+ FieldValueFeature f = new FieldValueFeature();
+ f.name = "f" + i;
+ f.params = new NamedParams().add("field", field);
+ features.add(f);
+ }
+ return features;
+ }
+
+ private class MockModel extends ModelMetadata {
+
+ public MockModel(String name, String type, List features,
+ String featureStoreName, Collection allFeatures,
+ NamedParams params) {
+ super(name, type, features, featureStoreName, allFeatures, params);
+ }
+
+ @Override
+ public float score(float[] modelFeatureValuesNormalized) {
+ return modelFeatureValuesNormalized[2];
+ }
+
+ @Override
+ public Explanation explain(LeafReaderContext context, int doc,
+ float finalScore, List featureExplanations) {
+ return null;
+ }
+
+ }
+
+ @Ignore
+ @Test
+ public void testRescorer() throws IOException {
+ Directory dir = newDirectory();
+ RandomIndexWriter w = new RandomIndexWriter(random(), dir);
+
+ Document doc = new Document();
+ doc.add(newStringField("id", "0", Field.Store.YES));
+ doc.add(newTextField("field", "wizard the the the the the oz",
+ Field.Store.NO));
+ doc.add(new FloatDocValuesField("final-score", 1.0f));
+
+ w.addDocument(doc);
+ doc = new Document();
+ doc.add(newStringField("id", "1", Field.Store.YES));
+ // 1 extra token, but wizard and oz are close;
+ doc.add(newTextField("field", "wizard oz the the the the the the",
+ Field.Store.NO));
+ doc.add(new FloatDocValuesField("final-score", 2.0f));
+ w.addDocument(doc);
+
+ IndexReader r = w.getReader();
+ w.close();
+
+ // Do ordinary BooleanQuery:
+ Builder bqBuilder = new Builder();
+ bqBuilder.add(new TermQuery(new Term("field", "wizard")), Occur.SHOULD);
+ bqBuilder.add(new TermQuery(new Term("field", "oz")), Occur.SHOULD);
+ IndexSearcher searcher = getSearcher(r);
+ // first run the standard query
+ TopDocs hits = searcher.search(bqBuilder.build(), 10);
+ assertEquals(2, hits.totalHits);
+ assertEquals("0", searcher.doc(hits.scoreDocs[0].doc).get("id"));
+ assertEquals("1", searcher.doc(hits.scoreDocs[1].doc).get("id"));
+
+ List features = makeFieldValueFeatures(new int[] {0, 1, 2},
+ "final-score");
+ List allFeatures = makeFieldValueFeatures(new int[] {0, 1, 2, 3,
+ 4, 5, 6, 7, 8, 9}, "final-score");
+ RankSVMModel meta = new RankSVMModel("test",
+ MockModel.class.getCanonicalName(), features, "test", allFeatures, null);
+
+ LTRRescorer rescorer = new LTRRescorer(new ModelQuery(meta));
+ hits = rescorer.rescore(searcher, hits, 2);
+
+ // rerank using the field final-score
+ assertEquals("1", searcher.doc(hits.scoreDocs[0].doc).get("id"));
+ assertEquals("0", searcher.doc(hits.scoreDocs[1].doc).get("id"));
+
+ r.close();
+ dir.close();
+
+ }
+
+ @Ignore
+ @Test
+ public void testDifferentTopN() throws IOException {
+ Directory dir = newDirectory();
+ RandomIndexWriter w = new RandomIndexWriter(random(), dir);
+
+ Document doc = new Document();
+ doc.add(newStringField("id", "0", Field.Store.YES));
+ doc.add(newTextField("field", "wizard oz oz oz oz oz", Field.Store.NO));
+ doc.add(new FloatDocValuesField("final-score", 1.0f));
+ w.addDocument(doc);
+
+ doc = new Document();
+ doc.add(newStringField("id", "1", Field.Store.YES));
+ doc.add(newTextField("field", "wizard oz oz oz oz the", Field.Store.NO));
+ doc.add(new FloatDocValuesField("final-score", 2.0f));
+ w.addDocument(doc);
+ doc = new Document();
+ doc.add(newStringField("id", "2", Field.Store.YES));
+ doc.add(newTextField("field", "wizard oz oz oz the the ", Field.Store.NO));
+ doc.add(new FloatDocValuesField("final-score", 3.0f));
+ w.addDocument(doc);
+ doc = new Document();
+ doc.add(newStringField("id", "3", Field.Store.YES));
+ doc.add(newTextField("field", "wizard oz oz the the the the ",
+ Field.Store.NO));
+ doc.add(new FloatDocValuesField("final-score", 4.0f));
+ w.addDocument(doc);
+ doc = new Document();
+ doc.add(newStringField("id", "4", Field.Store.YES));
+ doc.add(newTextField("field", "wizard oz the the the the the the",
+ Field.Store.NO));
+ doc.add(new FloatDocValuesField("final-score", 5.0f));
+ w.addDocument(doc);
+
+ IndexReader r = w.getReader();
+ w.close();
+
+ // Do ordinary BooleanQuery:
+ Builder bqBuilder = new Builder();
+ bqBuilder.add(new TermQuery(new Term("field", "wizard")), Occur.SHOULD);
+ bqBuilder.add(new TermQuery(new Term("field", "oz")), Occur.SHOULD);
+ IndexSearcher searcher = getSearcher(r);
+
+ // first run the standard query
+ TopDocs hits = searcher.search(bqBuilder.build(), 10);
+ assertEquals(5, hits.totalHits);
+ for (int i = 0; i < 5; i++) {
+ System.out.print(hits.scoreDocs[i].doc + " -> ");
+ System.out.println(searcher.doc(hits.scoreDocs[i].doc).get("id"));
+ }
+
+ assertEquals("0", searcher.doc(hits.scoreDocs[0].doc).get("id"));
+ assertEquals("1", searcher.doc(hits.scoreDocs[1].doc).get("id"));
+ assertEquals("2", searcher.doc(hits.scoreDocs[2].doc).get("id"));
+ assertEquals("3", searcher.doc(hits.scoreDocs[3].doc).get("id"));
+ assertEquals("4", searcher.doc(hits.scoreDocs[4].doc).get("id"));
+
+ List features = makeFieldValueFeatures(new int[] {0, 1, 2},
+ "final-score");
+ List allFeatures = makeFieldValueFeatures(new int[] {0, 1, 2, 3,
+ 4, 5, 6, 7, 8, 9}, "final-score");
+ RankSVMModel meta = new RankSVMModel("test",
+ MockModel.class.getCanonicalName(), features, "test", allFeatures, null);
+
+ LTRRescorer rescorer = new LTRRescorer(new ModelQuery(meta));
+
+ // rerank @ 0 should not change the order
+ hits = rescorer.rescore(searcher, hits, 0);
+ assertEquals("0", searcher.doc(hits.scoreDocs[0].doc).get("id"));
+ assertEquals("1", searcher.doc(hits.scoreDocs[1].doc).get("id"));
+ assertEquals("2", searcher.doc(hits.scoreDocs[2].doc).get("id"));
+ assertEquals("3", searcher.doc(hits.scoreDocs[3].doc).get("id"));
+ assertEquals("4", searcher.doc(hits.scoreDocs[4].doc).get("id"));
+
+ // test rerank with different topN cuts
+
+ for (int topN = 1; topN <= 5; topN++) {
+ logger.info("rerank {} documents ", topN);
+ hits = searcher.search(bqBuilder.build(), 10);
+ // meta = new MockModel();
+ // rescorer = new LTRRescorer(new ModelQuery(meta));
+ ScoreDoc[] slice = new ScoreDoc[topN];
+ System.arraycopy(hits.scoreDocs, 0, slice, 0, topN);
+ hits = new TopDocs(hits.totalHits, slice, hits.getMaxScore());
+ hits = rescorer.rescore(searcher, hits, topN);
+ for (int i = topN - 1, j = 0; i >= 0; i--, j++) {
+ logger.info("doc {} in pos {}", searcher.doc(hits.scoreDocs[j].doc)
+ .get("id"), j);
+
+ assertEquals(i,
+ Integer.parseInt(searcher.doc(hits.scoreDocs[j].doc).get("id")));
+ assertEquals(i + 1, hits.scoreDocs[j].score, 0.00001);
+
+ }
+ }
+
+ r.close();
+ dir.close();
+
+ }
+
+ @Test
+ public void testDocParam() throws Exception {
+ NamedParams test = new NamedParams();
+ test.add("fake", 2);
+ List features = makeFieldValueFeatures(new int[] {0},
+ "final-score");
+ List allFeatures = makeFieldValueFeatures(new int[] {0},
+ "final-score");
+ MockModel meta = new MockModel("test", MockModel.class.getCanonicalName(),
+ features, "test", allFeatures, null);
+ ModelQuery query = new ModelQuery(meta);
+ ModelWeight wgt = query.createWeight(null, true);
+ ModelScorer modelScr = wgt.scorer(null);
+ modelScr.setDocInfoParam("ORIGINAL_SCORE", 1);
+ for (ChildScorer feat : modelScr.getChildren()) {
+ assert (((FeatureScorer) feat.child).hasDocParam("ORIGINAL_SCORE"));
+ }
+
+ features = makeFieldValueFeatures(new int[] {0, 1, 2}, "final-score");
+ allFeatures = makeFieldValueFeatures(new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8,
+ 9}, "final-score");
+ meta = new MockModel("test", MockModel.class.getCanonicalName(), features,
+ "test", allFeatures, null);
+ query = new ModelQuery(meta);
+ wgt = query.createWeight(null, true);
+ modelScr = wgt.scorer(null);
+ modelScr.setDocInfoParam("ORIGINAL_SCORE", 1);
+ for (ChildScorer feat : modelScr.getChildren()) {
+ assert (((FeatureScorer) feat.child).hasDocParam("ORIGINAL_SCORE"));
+ }
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/rest/TestModelManager.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/rest/TestModelManager.java
new file mode 100644
index 000000000000..3fd4bbc55b05
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/rest/TestModelManager.java
@@ -0,0 +1,170 @@
+package org.apache.solr.ltr.rest;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.SolrResourceLoader;
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.ranking.LTRComponent;
+import org.apache.solr.ltr.ranking.LTRComponent.LTRParams;
+import org.apache.solr.rest.ManagedResource;
+import org.apache.solr.rest.ManagedResourceStorage;
+import org.apache.solr.rest.RestManager;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+@SuppressSSL
+public class TestModelManager extends TestRerankBase {
+
+ @BeforeClass
+ public static void init() throws Exception {
+ setuptest();
+ }
+
+ @Before
+ public void restart() throws Exception {
+ restTestHarness.delete(LTRParams.MSTORE_END_POINT + "/*");
+ restTestHarness.delete(LTRParams.FSTORE_END_POINT + "/*");
+
+ }
+
+ @Test
+ public void test() throws Exception {
+ SolrResourceLoader loader = new SolrResourceLoader(tmpSolrHome.toPath());
+
+ RestManager.Registry registry = loader.getManagedResourceRegistry();
+ assertNotNull(
+ "Expected a non-null RestManager.Registry from the SolrResourceLoader!",
+ registry);
+
+ String resourceId = "/schema/fstore1";
+ registry.registerManagedResource(resourceId, ManagedFeatureStore.class,
+ new LTRComponent());
+
+ String resourceId2 = "/schema/mstore1";
+ registry.registerManagedResource(resourceId2, ManagedModelStore.class,
+ new LTRComponent());
+
+ NamedList initArgs = new NamedList<>();
+
+ RestManager restManager = new RestManager();
+ restManager.init(loader, initArgs,
+ new ManagedResourceStorage.InMemoryStorageIO());
+
+ ManagedResource res = restManager.getManagedResource(resourceId);
+ assertTrue(res instanceof ManagedFeatureStore);
+ assertEquals(res.getResourceId(), resourceId);
+
+ }
+
+ @Test
+ public void testRestManagerEndpoints() throws Exception {
+ // relies on these ManagedResources being activated in the
+ // schema-rest.xml used by this test
+ assertJQ("/schema/managed", "/responseHeader/status==0");
+ System.out.println(restTestHarness.query("/schema/managed"));
+
+ System.out.println("after: \n" + restTestHarness.query("/schema/managed"));
+
+ // Add features
+ String feature = "{\"name\": \"test1\", \"type\": \"org.apache.solr.ltr.feature.impl.ValueFeature\", \"params\": {\"value\": 1} }";
+ assertJPut(TestRerankBase.FEATURE_ENDPOINT, feature,
+ "/responseHeader/status==0");
+
+ feature = "{\"name\": \"test2\", \"type\": \"org.apache.solr.ltr.feature.impl.ValueFeature\", \"params\": {\"value\": 1} }";
+ assertJPut(TestRerankBase.FEATURE_ENDPOINT, feature,
+ "/responseHeader/status==0");
+
+ feature = "{\"name\": \"test3\", \"type\": \"org.apache.solr.ltr.feature.impl.ValueFeature\", \"params\": {\"value\": 1} }";
+ assertJPut(TestRerankBase.FEATURE_ENDPOINT, feature,
+ "/responseHeader/status==0");
+
+ feature = "{\"name\": \"test33\", \"store\": \"TEST\", \"type\": \"org.apache.solr.ltr.feature.impl.ValueFeature\", \"params\": {\"value\": 1} }";
+ assertJPut(TestRerankBase.FEATURE_ENDPOINT, feature,
+ "/responseHeader/status==0");
+
+ String multipleFeatures = "[{\"name\": \"test4\", \"type\": \"org.apache.solr.ltr.feature.impl.ValueFeature\", \"params\": {\"value\": 1} }"
+ + ",{\"name\": \"test5\", \"type\": \"org.apache.solr.ltr.feature.impl.ValueFeature\", \"params\": {\"value\": 1} } ]";
+ assertJPut(TestRerankBase.FEATURE_ENDPOINT, multipleFeatures,
+ "/responseHeader/status==0");
+
+ // Add bad feature (wrong params)_
+ String badfeature = "{\"name\": \"fvalue\", \"type\": \"org.apache.solr.ltr.feature.impl.FieldValueFeature\", \"params\": {\"value\": 1} }";
+ assertJPut(TestRerankBase.FEATURE_ENDPOINT, badfeature,
+ "/responseHeader/status==400");
+
+ // Add models
+ String model = "{ \"name\":\"testmodel1\", \"type\":\"org.apache.solr.ltr.ranking.RankSVMModel\", \"features\":[] }";
+ // fails since it does not have features
+ assertJPut(TestRerankBase.MODEL_ENDPOINT, model,
+ "/responseHeader/status==400");
+ // fails since it does not have weights
+ model = "{ \"name\":\"testmodel2\", \"type\":\"org.apache.solr.ltr.ranking.RankSVMModel\", \"features\":[{\"name\":\"test1\"}, {\"name\":\"test2\"}] }";
+ assertJPut(TestRerankBase.MODEL_ENDPOINT, model,
+ "/responseHeader/status==400");
+ // success
+ model = "{ \"name\":\"testmodel3\", \"type\":\"org.apache.solr.ltr.ranking.RankSVMModel\", \"features\":[{\"name\":\"test1\"}, {\"name\":\"test2\"}],\"params\":{\"weights\":{\"test1\":1.5,\"test2\":2.0}}}";
+ assertJPut(TestRerankBase.MODEL_ENDPOINT, model,
+ "/responseHeader/status==0");
+ // success
+ String multipleModels = "[{ \"name\":\"testmodel4\", \"type\":\"org.apache.solr.ltr.ranking.RankSVMModel\", \"features\":[{\"name\":\"test1\"}, {\"name\":\"test2\"}],\"params\":{\"weights\":{\"test1\":1.5,\"test2\":2.0}} }\n"
+ + ",{ \"name\":\"testmodel5\", \"type\":\"org.apache.solr.ltr.ranking.RankSVMModel\", \"features\":[{\"name\":\"test1\"}, {\"name\":\"test2\"}],\"params\":{\"weights\":{\"test1\":1.5,\"test2\":2.0}} } ]";
+ assertJPut(TestRerankBase.MODEL_ENDPOINT, multipleModels,
+ "/responseHeader/status==0");
+ String qryResult = JQ(LTRParams.MSTORE_END_POINT);
+
+ assert (qryResult.contains("\"name\":\"testmodel3\"")
+ && qryResult.contains("\"name\":\"testmodel4\"") && qryResult
+ .contains("\"name\":\"testmodel5\""));
+ /*
+ * assertJQ(LTRParams.MSTORE_END_POINT, "/models/[0]/name=='testmodel3'");
+ * assertJQ(LTRParams.MSTORE_END_POINT, "/models/[1]/name=='testmodel4'");
+ * assertJQ(LTRParams.MSTORE_END_POINT, "/models/[2]/name=='testmodel5'");
+ */
+ assertJQ(LTRParams.FSTORE_END_POINT, "/featureStores==['TEST','_DEFAULT_']");
+ assertJQ(LTRParams.FSTORE_END_POINT + "/_DEFAULT_",
+ "/features/[0]/name=='test1'");
+ assertJQ(LTRParams.FSTORE_END_POINT + "/TEST",
+ "/features/[0]/name=='test33'");
+ }
+
+ @Test
+ public void testEndpointsFromFile() throws Exception {
+ loadFeatures("features-ranksvm.json");
+ loadModels("ranksvm-model.json");
+
+ assertJQ(LTRParams.MSTORE_END_POINT,
+ "/models/[0]/name=='6029760550880411648'");
+ assertJQ(LTRParams.FSTORE_END_POINT + "/_DEFAULT_",
+ "/features/[1]/name=='description'");
+ }
+
+ @Test
+ public void testLoadInvalidFeature() throws Exception {
+ // relies on these ManagedResources being activated in the
+ // schema-rest.xml used by this test
+ assertJQ("/schema/managed", "/responseHeader/status==0");
+ String newEndpoint = LTRParams.FSTORE_END_POINT;
+ String feature = "{\"name\": \"^&test1\", \"type\": \"org.apache.solr.ltr.feature.impl.ValueFeature\", \"params\": {\"value\": 1} }";
+ assertJPut(newEndpoint, feature, "/responseHeader/status==400");
+
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/rest/TestModelManagerPersistence.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/rest/TestModelManagerPersistence.java
new file mode 100644
index 000000000000..6a528783bb20
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/rest/TestModelManagerPersistence.java
@@ -0,0 +1,86 @@
+package org.apache.solr.ltr.rest;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.commons.io.FileUtils;
+import org.apache.solr.SolrTestCaseJ4.SuppressSSL;
+import org.apache.solr.ltr.TestRerankBase;
+import org.apache.solr.ltr.feature.impl.ValueFeature;
+import org.apache.solr.ltr.ranking.LTRComponent.LTRParams;
+import org.apache.solr.ltr.ranking.RankSVMModel;
+import org.junit.Before;
+import org.junit.Test;
+
+@SuppressSSL
+public class TestModelManagerPersistence extends TestRerankBase {
+
+ @Before
+ public void init() throws Exception {
+ setupPersistenttest();
+ }
+
+ // executed first
+ @Test
+ public void testFeaturePersistence() throws Exception {
+
+ loadFeature("feature", ValueFeature.class.getCanonicalName(), "test",
+ "{\"value\":2}");
+ System.out.println(restTestHarness.query(LTRParams.FSTORE_END_POINT
+ + "/test"));
+ assertJQ(LTRParams.FSTORE_END_POINT + "/test",
+ "/features/[0]/name=='feature'");
+ restTestHarness.reload();
+ assertJQ(LTRParams.FSTORE_END_POINT + "/test",
+ "/features/[0]/name=='feature'");
+ loadFeature("feature1", ValueFeature.class.getCanonicalName(), "test1",
+ "{\"value\":2}");
+ loadFeature("feature2", ValueFeature.class.getCanonicalName(), "test",
+ "{\"value\":2}");
+ loadFeature("feature3", ValueFeature.class.getCanonicalName(), "test2",
+ "{\"value\":2}");
+ assertJQ(LTRParams.FSTORE_END_POINT + "/test",
+ "/features/[0]/name=='feature'");
+ assertJQ(LTRParams.FSTORE_END_POINT + "/test",
+ "/features/[1]/name=='feature2'");
+ assertJQ(LTRParams.FSTORE_END_POINT + "/test1",
+ "/features/[0]/name=='feature1'");
+ assertJQ(LTRParams.FSTORE_END_POINT + "/test2",
+ "/features/[0]/name=='feature3'");
+ restTestHarness.reload();
+ assertJQ(LTRParams.FSTORE_END_POINT + "/test",
+ "/features/[0]/name=='feature'");
+ assertJQ(LTRParams.FSTORE_END_POINT + "/test",
+ "/features/[1]/name=='feature2'");
+ assertJQ(LTRParams.FSTORE_END_POINT + "/test1",
+ "/features/[0]/name=='feature1'");
+ assertJQ(LTRParams.FSTORE_END_POINT + "/test2",
+ "/features/[0]/name=='feature3'");
+ loadModel("test-model", RankSVMModel.class.getCanonicalName(),
+ new String[] {"feature"}, "test", "{\"weights\":{\"feature\":1.0}}");
+ String fstorecontent = FileUtils.readFileToString(fstorefile,"UTF-8");
+ String mstorecontent = FileUtils.readFileToString(mstorefile,"UTF-8");
+
+ System.out.println("fstore:\n");
+ System.out.println(fstorecontent);
+
+ System.out.println("mstore:\n");
+ System.out.println(mstorecontent);
+
+ }
+
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/util/TestMacroExpander.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/util/TestMacroExpander.java
new file mode 100644
index 000000000000..ffbd6dc2178c
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/util/TestMacroExpander.java
@@ -0,0 +1,58 @@
+package org.apache.solr.ltr.util;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class TestMacroExpander {
+
+ @Test
+ public void testEmptyExpander() {
+ Map efi = new HashMap();
+ MacroExpander macroExpander = new MacroExpander(efi);
+
+ assertEquals("", macroExpander.expand(""));
+ assertEquals("foo", macroExpander.expand("foo"));
+ assertEquals("$foo", macroExpander.expand("$foo"));
+ assertEquals("${foo}", macroExpander.expand("${foo}"));
+ assertEquals("{foo}", macroExpander.expand("{foo}"));
+ assertEquals("${foo}", MacroExpander.expand("${foo}", efi));
+ }
+
+ @Test
+ public void testExpander() {
+ Map efi = new HashMap();
+ efi.put("foo", "bar");
+ efi.put("baz", "bat");
+ MacroExpander macroExpander = new MacroExpander(efi);
+
+ assertEquals("", macroExpander.expand(""));
+ assertEquals("foo", macroExpander.expand("foo"));
+ assertEquals("$foo", macroExpander.expand("$foo"));
+ assertEquals("bar", macroExpander.expand("${foo}"));
+ assertEquals("{foo}", macroExpander.expand("{foo}"));
+ assertEquals("bar", MacroExpander.expand("${foo}", efi));
+ assertEquals("foo bar baz bat",
+ macroExpander.expand("foo ${foo} baz ${baz}"));
+ }
+}
diff --git a/solr/contrib/ltr/src/test/org/apache/solr/ltr/util/TestNameValidator.java b/solr/contrib/ltr/src/test/org/apache/solr/ltr/util/TestNameValidator.java
new file mode 100644
index 000000000000..c54bb30b1d32
--- /dev/null
+++ b/solr/contrib/ltr/src/test/org/apache/solr/ltr/util/TestNameValidator.java
@@ -0,0 +1,48 @@
+package org.apache.solr.ltr.util;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class TestNameValidator {
+
+ @Test
+ public void testValidator() {
+ assertTrue(NameValidator.check("test"));
+ assertTrue(NameValidator.check("constant"));
+ assertTrue(NameValidator.check("test_test"));
+ assertTrue(NameValidator.check("TEst"));
+ assertTrue(NameValidator.check("TEST"));
+ assertTrue(NameValidator.check("328195082960784"));
+ assertFalse(NameValidator.check(" "));
+ assertFalse(NameValidator.check(""));
+ assertFalse(NameValidator.check("test?"));
+ assertFalse(NameValidator.check("??????"));
+ assertFalse(NameValidator.check("_____-----"));
+ assertFalse(NameValidator.check("12345,67890.31"));
+ assertFalse(NameValidator.check("aasdasdadasdzASADADSAZ01239()[]|_-"));
+ assertFalse(NameValidator.check(null));
+ assertTrue(NameValidator.check("a"));
+ assertTrue(NameValidator.check("test()"));
+ assertTrue(NameValidator.check("test________123"));
+
+ }
+}