This is an automated email from the ASF dual-hosted git repository.
dsmiley pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_9x by this push:
new 3b9c7819dde SOLR-5707: Lucene Expressions via
ExpressionValueSourceParser (#1244)
3b9c7819dde is described below
commit 3b9c7819ddeb7cde1ac96acab2df9a5c02d4b4e1
Author: Kevin Risden <[email protected]>
AuthorDate: Sun Jul 13 16:40:34 2025 -0400
SOLR-5707: Lucene Expressions via ExpressionValueSourceParser (#1244)
New ExpressionValueSourceParser that allows custom function queries / VSPs
to be defined in a
subset of JavaScript, pre-compiled, and that which can access the score
and fields. It's powered by
the Lucene Expressions module.
ValueSourceAugmenter: score propagation
---------
Co-authored-by: Chris Hostetter <[email protected]>
Co-authored-by: David Smiley <[email protected]>
Co-authored-by: Ryan Ernst <[email protected]>
(cherry picked from commit c3b5f57cd068a362a5992e57f855e90c34cf329a)
---
gradle/documentation/pull-lucene-javadocs.gradle | 9 +-
solr/CHANGES.txt | 12 +-
.../response/transform/ValueSourceAugmenter.java | 82 ++++-
.../solr/search/ExpressionValueSourceParser.java | 155 +++++++++
.../collection1/conf/solrconfig-expressions-vs.xml | 76 +++++
.../search/ExpressionValueSourceParserTest.java | 346 +++++++++++++++++++++
.../pages/expression-value-source-parser.adoc | 56 ++++
.../query-guide/pages/function-queries.adoc | 2 +
.../modules/query-guide/querying-nav.adoc | 1 +
9 files changed, 725 insertions(+), 14 deletions(-)
diff --git a/gradle/documentation/pull-lucene-javadocs.gradle
b/gradle/documentation/pull-lucene-javadocs.gradle
index 85490b31901..7e58735f722 100644
--- a/gradle/documentation/pull-lucene-javadocs.gradle
+++ b/gradle/documentation/pull-lucene-javadocs.gradle
@@ -41,11 +41,12 @@ configure(project(":solr:documentation")) {
// - For now this list is focused solely on the javadocs needed for
ref-guide link validation.
// - If/when additional links are added from the ref-guide to additional
lucene modules not listed here,
// they can be added.
- // - If/when we need the lucene javadocs for "all" lucene depdencies in
Solr (ie: to do link checking
- // from all Solr javadocs?) then perhaps we can find a way to build this
list programatically?
- // - If these javadocs are (only every) consumed by the ref guide only,
then these deps & associated tasks
+ // - If/when we need the lucene javadocs for "all" lucene dependencies in
Solr (ie: to do link checking
+ // from all Solr javadocs?) then perhaps we can find a way to build this
list programmatically?
+ // - If these javadocs are only consumed by the ref guide, then these deps
& associated tasks
// should just be moved to the ref-guide build.gradle
javadocs group: 'org.apache.lucene', name: 'lucene-core', classifier:
'javadoc'
+ javadocs group: 'org.apache.lucene', name: 'lucene-expressions',
classifier: 'javadoc'
javadocs group: 'org.apache.lucene', name: 'lucene-analysis-common',
classifier: 'javadoc'
javadocs group: 'org.apache.lucene', name: 'lucene-analysis-stempel',
classifier: 'javadoc'
javadocs group: 'org.apache.lucene', name: 'lucene-queryparser',
classifier: 'javadoc'
@@ -65,7 +66,7 @@ configure(project(":solr:documentation")) {
def resolved = configurations.javadocs.resolvedConfiguration
resolved.resolvedArtifacts.each { artifact ->
def id = artifact.moduleVersion.id
- // This mimics the directory stucture used on lucene.apache.org for
the javadocs of all modules.
+ // This mimics the directory structure used on lucene.apache.org for
the javadocs of all modules.
//
// HACK: the lucene.apache.org javadocs are organized to match the
module directory structure in the repo,
// not the "flat" artifact names -- so there is no one size fits all
way to determine the directory name.
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 52e8fb0dae5..9259ce173f3 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -32,10 +32,6 @@ Other Changes
================== 9.9.0 ==================
New Features
---------------------
-* SOLR-17582: The CLUSTERSTATUS API will now stream each collection's status
to the response,
- fetching and computing it on the fly. To avoid a backwards compatibility
concern, this won't work
- for wt=javabin. (Matthew Biscocho, David Smiley)
-
* SOLR-17626: Add RawTFSimilarityFactory class. (Christine Poerschke)
* SOLR-17656: New 'skipLeaderRecovery' replica property allows PULL replicas
with existing indexes to immediately become ACTIVE (hossman)
@@ -52,6 +48,10 @@ New Features
* SOLR-17749: Added linear function support for RankField via
RankQParserPlugin. (Christine Poerschke)
+* SOLR-5707: New ExpressionValueSourceParser that allows custom function
queries / VSPs to be defined in a
+ subset of JavaScript, pre-compiled, and that which can access the score and
fields. It's powered by
+ the Lucene Expressions module. (hossman, David Smiley, Ryan Ernst, Kevin
Risden)
+
Improvements
---------------------
* SOLR-15751: The v2 API now has parity with the v1 "COLSTATUS" and "segments"
APIs, which can be used to fetch detailed information about
@@ -100,6 +100,10 @@ Improvements
Optimizations
---------------------
+* SOLR-17582: The CLUSTERSTATUS API will now stream each collection's status
to the response,
+ fetching and computing it on the fly. To avoid a backwards compatibility
concern, this won't work
+ for wt=javabin. (Matthew Biscocho, David Smiley)
+
* SOLR-17578: Remove ZkController internal core supplier, for slightly faster
reconnection after Zookeeper session loss. (Pierre Salagnac)
* SOLR-17669: Reduced memory usage in SolrJ getBeans() method when handling
dynamic fields with wildcards. (Martin Anzinger)
diff --git
a/solr/core/src/java/org/apache/solr/response/transform/ValueSourceAugmenter.java
b/solr/core/src/java/org/apache/solr/response/transform/ValueSourceAugmenter.java
index 4bd9f4eb12b..825c1b405a6 100644
---
a/solr/core/src/java/org/apache/solr/response/transform/ValueSourceAugmenter.java
+++
b/solr/core/src/java/org/apache/solr/response/transform/ValueSourceAugmenter.java
@@ -22,9 +22,11 @@ import java.util.List;
import java.util.Map;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.ReaderUtil;
+import org.apache.lucene.internal.hppc.IntFloatHashMap;
import org.apache.lucene.internal.hppc.IntObjectHashMap;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.Scorable;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.response.ResultContext;
@@ -67,20 +69,44 @@ public class ValueSourceAugmenter extends DocTransformer {
fcontext = ValueSource.newContext(searcher);
this.valueSource.createWeight(fcontext, searcher);
final var docList = context.getDocList();
- if (docList == null) {
+ final int prefetchSize = docList == null ? 0 : Math.min(docList.size(),
maxPrefetchSize);
+ if (prefetchSize == 0) {
return;
}
- final int prefetchSize = Math.min(docList.size(), maxPrefetchSize);
+ // Check if scores are wanted and initialize the Scorable if so
+ final MutableScorable scorable; // stored in fcontext (when not null)
+ final IntFloatHashMap docToScoreMap;
+ if (context.wantsScores()) { // TODO switch to ValueSource.needsScores
once it exists
+ docToScoreMap = new IntFloatHashMap(prefetchSize);
+ scorable =
+ new MutableScorable() {
+ @Override
+ public float score() throws IOException {
+ return docToScoreMap.get(docBase + localDocId);
+ }
+ };
+ fcontext.put("scorer", scorable);
+ } else {
+ scorable = null;
+ docToScoreMap = null;
+ }
+
+ // Get the IDs and scores
final int[] ids = new int[prefetchSize];
int i = 0;
var iter = docList.iterator();
while (iter.hasNext() && i < prefetchSize) {
- ids[i++] = iter.nextDoc();
+ ids[i] = iter.nextDoc();
+ if (docToScoreMap != null) {
+ docToScoreMap.put(ids[i], iter.score());
+ }
+ i++;
}
Arrays.sort(ids);
- cachedValuesById = new IntObjectHashMap<>(ids.length);
+ // Get the values in docId order. Store in cachedValuesById
+ cachedValuesById = new IntObjectHashMap<>(ids.length);
FunctionValues values = null;
int docBase = -1;
int nextDocBase = 0; // i.e. this segment's maxDoc
@@ -94,9 +120,16 @@ public class ValueSourceAugmenter extends DocTransformer {
}
int localId = docid - docBase;
- var value = values.objectVal(localId);
+
+ if (scorable != null) {
+ scorable.docBase = docBase;
+ scorable.localDocId = localId;
+ }
+ var value = values.objectVal(localId); // note: might use the Scorable
+
cachedValuesById.put(docid, value != null ? value : NULL_SENTINEL);
}
+ fcontext.remove("scorer"); // remove ours; it was there only for
prefetching
} catch (IOException e) {
throw new SolrException(
SolrException.ErrorCode.SERVER_ERROR, "exception for valuesource " +
valueSource, e);
@@ -118,8 +151,13 @@ public class ValueSourceAugmenter extends DocTransformer {
try {
int idx = ReaderUtil.subIndex(docid, readerContexts);
LeafReaderContext rcontext = readerContexts.get(idx);
- FunctionValues values = valueSource.getValues(fcontext, rcontext);
int localId = docid - rcontext.docBase;
+
+ if (context.wantsScores()) {
+ fcontext.put("scorer", new ScoreAndDoc(localId, (float)
doc.get("score")));
+ }
+
+ FunctionValues values = valueSource.getValues(fcontext, rcontext);
setValue(doc, values.objectVal(localId));
} catch (IOException e) {
throw new SolrException(
@@ -130,6 +168,17 @@ public class ValueSourceAugmenter extends DocTransformer {
}
}
+ private abstract static class MutableScorable extends Scorable {
+
+ int docBase;
+ int localDocId;
+
+ @Override
+ public int docID() {
+ return localDocId;
+ }
+ }
+
/** Always returns true */
@Override
public boolean needsSolrIndexSearcher() {
@@ -141,4 +190,25 @@ public class ValueSourceAugmenter extends DocTransformer {
doc.setField(name, val);
}
}
+
+ /** Fake scorer for a single document */
+ protected static class ScoreAndDoc extends Scorable {
+ final int docid;
+ final float score;
+
+ ScoreAndDoc(int docid, float score) {
+ this.docid = docid;
+ this.score = score;
+ }
+
+ @Override
+ public int docID() {
+ return docid;
+ }
+
+ @Override
+ public float score() throws IOException {
+ return score;
+ }
+ }
}
diff --git
a/solr/core/src/java/org/apache/solr/search/ExpressionValueSourceParser.java
b/solr/core/src/java/org/apache/solr/search/ExpressionValueSourceParser.java
new file mode 100644
index 00000000000..3812357c3d9
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/search/ExpressionValueSourceParser.java
@@ -0,0 +1,155 @@
+/*
+ * 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.
+ */
+package org.apache.solr.search;
+
+import static org.apache.solr.common.SolrException.ErrorCode.SERVER_ERROR;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.lucene.expressions.Bindings;
+import org.apache.lucene.expressions.Expression;
+import org.apache.lucene.expressions.js.JavascriptCompiler;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.DoubleValuesSource;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.schema.SchemaField;
+
+/**
+ * A ValueSource parser configured with a pre-compiled expression that can
then be evaluated at
+ * request time. It's powered by the Lucene Expressions module, which is a
subset of JavaScript.
+ */
+public class ExpressionValueSourceParser extends ValueSourceParser {
+
+ public static final String SCORE_KEY = "score-name"; // TODO get rid of
this? Why have it?
+ public static final String EXPRESSION_KEY = "expression";
+
+ private Expression expression;
+ private String scoreKey;
+ private int numPositionalArgs = 0; // Number of positional arguments in the
expression
+
+ @Override
+ public void init(NamedList<?> args) {
+ initConfiguredExpression(args);
+ initScoreKey(args);
+ super.init(args);
+ }
+
+ /** Checks for optional scoreKey override */
+ private void initScoreKey(NamedList<?> args) {
+ scoreKey = Optional.ofNullable((String)
args.remove(SCORE_KEY)).orElse(SolrReturnFields.SCORE);
+ }
+
+ /** Parses the pre-configured expression */
+ private void initConfiguredExpression(NamedList<?> args) {
+ String expressionStr =
+ Optional.ofNullable((String) args.remove(EXPRESSION_KEY))
+ .orElseThrow(
+ () ->
+ new SolrException(
+ SERVER_ERROR, EXPRESSION_KEY + " must be configured
with an expression"));
+
+ // Find the highest positional argument in the expression
+ Pattern pattern = Pattern.compile("\\$(\\d+)");
+ Matcher matcher = pattern.matcher(expressionStr);
+ while (matcher.find()) {
+ int argNum = Integer.parseInt(matcher.group(1));
+ numPositionalArgs = Math.max(numPositionalArgs, argNum);
+ }
+
+ // TODO add way to register additional functions
+ try {
+ this.expression = JavascriptCompiler.compile(expressionStr);
+ } catch (ParseException e) {
+ throw new SolrException(
+ SERVER_ERROR, "Unable to parse javascript expression: " +
expressionStr, e);
+ }
+ }
+
+ // TODO: support dynamic expressions: expr("foo * bar / 32") ??
+
+ @Override
+ public ValueSource parse(FunctionQParser fp) throws SyntaxError {
+ assert null != fp;
+
+ // Parse positional arguments if any
+ List<DoubleValuesSource> positionalArgs = new ArrayList<>();
+ for (int i = 0; i < numPositionalArgs; i++) {
+ ValueSource vs = fp.parseValueSource();
+ positionalArgs.add(vs.asDoubleValuesSource());
+ }
+
+ IndexSchema schema = fp.getReq().getSchema();
+ SolrBindings b = new SolrBindings(scoreKey, schema, positionalArgs);
+ return
ValueSource.fromDoubleValuesSource(expression.getDoubleValuesSource(b));
+ }
+
+ /**
+ * A bindings class that uses schema fields to resolve variables.
+ *
+ * @lucene.internal
+ */
+ public static class SolrBindings extends Bindings {
+ private final String scoreKey;
+ private final IndexSchema schema;
+ private final List<DoubleValuesSource> positionalArgs;
+
+ /**
+ * @param scoreKey The binding name that should be used to represent the
score, may be null
+ * @param schema IndexSchema for field bindings
+ * @param positionalArgs List of positional arguments
+ */
+ public SolrBindings(
+ String scoreKey, IndexSchema schema, List<DoubleValuesSource>
positionalArgs) {
+ this.scoreKey = scoreKey;
+ this.schema = schema;
+ this.positionalArgs = positionalArgs != null ? positionalArgs : new
ArrayList<>();
+ }
+
+ @Override
+ public DoubleValuesSource getDoubleValuesSource(String key) {
+ assert null != key;
+
+ if (Objects.equals(scoreKey, key)) {
+ return DoubleValuesSource.SCORES;
+ }
+
+ // Check for positional arguments like $1, $2, etc.
+ if (key.startsWith("$")) {
+ try {
+ int position = Integer.parseInt(key.substring(1));
+ return positionalArgs.get(position - 1); // Convert to 0-based index
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException("Not a valid positional argument:
" + key, e);
+ }
+ }
+
+ SchemaField field = schema.getFieldOrNull(key);
+ if (null != field) {
+ return field.getType().getValueSource(field,
null).asDoubleValuesSource();
+ }
+
+ throw new IllegalArgumentException("No binding or schema field for key:
" + key);
+ }
+ }
+}
diff --git
a/solr/core/src/test-files/solr/collection1/conf/solrconfig-expressions-vs.xml
b/solr/core/src/test-files/solr/collection1/conf/solrconfig-expressions-vs.xml
new file mode 100644
index 00000000000..9081d634eff
--- /dev/null
+++
b/solr/core/src/test-files/solr/collection1/conf/solrconfig-expressions-vs.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" ?>
+
+<!--
+ 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.
+-->
+
+<config>
+
<luceneMatchVersion>${tests.luceneMatchVersion:LUCENE_CURRENT}</luceneMatchVersion>
+
+ <directoryFactory name="DirectoryFactory"
class="${solr.directoryFactory:solr.RAMDirectoryFactory}"/>
+ <schemaFactory class="ClassicIndexSchemaFactory"/>
+
+ <xi:include href="solrconfig.snippet.randomindexconfig.xml"
xmlns:xi="http://www.w3.org/2001/XInclude"/>
+
+ <requestHandler name="standard" class="solr.StandardRequestHandler"/>
+ <requestHandler name="/update" class="solr.UpdateRequestHandler" />
+
+ <valueSourceParser name="sin1" class="solr.ExpressionValueSourceParser">
+ <str name="expression">sin(1)</str>
+ </valueSourceParser>
+
+ <valueSourceParser name="cos_sin1" class="solr.ExpressionValueSourceParser">
+ <str name="expression">cos(sin(1))</str>
+ </valueSourceParser>
+
+ <valueSourceParser name="sqrt_int1_i"
class="solr.ExpressionValueSourceParser">
+ <str name="expression">sqrt(int1_i)</str>
+ </valueSourceParser>
+
+ <valueSourceParser name="sqrt_double1_d"
class="solr.ExpressionValueSourceParser">
+ <str name="expression">sqrt(double1_d)</str>
+ </valueSourceParser>
+
+ <valueSourceParser name="date1_dt_minus_1990"
class="solr.ExpressionValueSourceParser">
+ <str name="expression">date1_dt - 631036800000</str>
+ </valueSourceParser>
+
+ <valueSourceParser name="one_plus_score"
class="solr.ExpressionValueSourceParser">
+ <str name="expression">1 + score</str>
+ </valueSourceParser>
+
+ <valueSourceParser name="two_plus_score"
class="solr.ExpressionValueSourceParser">
+ <str name="expression">1 + score + 1</str>
+ </valueSourceParser>
+
+ <valueSourceParser name="sqrt_int1_i_plus_one_plus_score"
class="solr.ExpressionValueSourceParser">
+ <str name="expression">sqrt(int1_i) + 1 + score</str>
+ </valueSourceParser>
+
+ <valueSourceParser name="mixed_expr"
class="solr.ExpressionValueSourceParser">
+ <str name="expression">(1 + score)*ln(cos(sin(1)))</str>
+ </valueSourceParser>
+
+ <valueSourceParser name="expr_ssccoorree"
class="solr.ExpressionValueSourceParser">
+ <!-- unclear if anyone needs this capability -->
+ <str name="score-name">ssccoorree</str>
+ <str name="expression">1 + ssccoorree</str>
+ </valueSourceParser>
+
+ <valueSourceParser name="positional_args"
class="solr.ExpressionValueSourceParser">
+ <str name="expression">$1 + $2 + $3</str>
+ </valueSourceParser>
+</config>
diff --git
a/solr/core/src/test/org/apache/solr/search/ExpressionValueSourceParserTest.java
b/solr/core/src/test/org/apache/solr/search/ExpressionValueSourceParserTest.java
new file mode 100644
index 00000000000..97f9c4d7418
--- /dev/null
+++
b/solr/core/src/test/org/apache/solr/search/ExpressionValueSourceParserTest.java
@@ -0,0 +1,346 @@
+/*
+ * 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.
+ */
+package org.apache.solr.search;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.DoubleValuesSource;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.search.ExpressionValueSourceParser.SolrBindings;
+import org.apache.solr.util.DateMathParser;
+import org.junit.BeforeClass;
+
+public class ExpressionValueSourceParserTest extends SolrTestCaseJ4 {
+
+ private final List<DoubleValuesSource> positionalArgs = new ArrayList<>();
+
+ @BeforeClass
+ public static void beforeTests() throws Exception {
+ initCore("solrconfig-expressions-vs.xml", "schema15.xml");
+
+ assertU(
+ adoc("id", "1", "int1_i", "50", "double1_d", "-2.5", "date1_dt",
"1996-12-19T16:39:57Z"));
+ assertU(
+ adoc("id", "2", "int1_i", "-30", "double1_d", "10.3", "date1_dt",
"1999-12-19T16:39:57Z"));
+ assertU(
+ adoc("id", "3", "int1_i", "10", "double1_d", "500.3", "date1_dt",
"1995-12-19T16:39:57Z"));
+ assertU(adoc("id", "4", "int1_i", "40", "double1_d", "-1", "date1_dt",
"1994-12-19T16:39:57Z"));
+ assertU(
+ adoc("id", "5", "int1_i", "20", "double1_d", "2.1", "date1_dt",
"1997-12-19T16:39:57Z"));
+
+ assertU(commit());
+ }
+
+ public void testValidBindings() throws ParseException {
+ IndexSchema schema = h.getCore().getLatestSchema();
+ SolrBindings bindings = new SolrBindings("ScOrE", schema,
this.positionalArgs);
+
+ assertEquals(
+ "foo_i from bindings is wrong",
+ schema
+ .getFieldType("foo_i")
+ .getValueSource(schema.getField("foo_i"), null)
+ .asDoubleValuesSource(),
+ bindings.getDoubleValuesSource("foo_i"));
+ ValueSource scoreBind =
+
ValueSource.fromDoubleValuesSource(bindings.getDoubleValuesSource("ScOrE"));
+ assertNotNull("ScOrE bindings failed", scoreBind);
+
+ try {
+ bindings.getDoubleValuesSource("not_a_expr_and_not_in_schema");
+ fail("no exception from bogus binding");
+ } catch (IllegalArgumentException e) {
+ assertTrue(
+ "wrong exception message: " + e.getMessage(),
+ e.getMessage().contains("not_a_expr_and_not_in_schema"));
+ }
+
+ // change things up a bit
+ bindings = new SolrBindings(null, schema, this.positionalArgs);
+ try {
+ bindings.getDoubleValuesSource("ScOrE");
+ fail("ScOrE should not have bindings");
+ } catch (IllegalArgumentException e) {
+ // NOOP
+ }
+ }
+
+ public void testBogusBindings() throws ParseException {
+ IndexSchema schema = h.getCore().getLatestSchema();
+ SolrBindings bindings = new SolrBindings("ScOrE", schema,
this.positionalArgs);
+
+ try {
+ bindings.getDoubleValuesSource("yak");
+ fail("sanity check failed: yak has a binding?");
+ } catch (IllegalArgumentException e) {
+ // NOOP
+ }
+
+ // change things up a bit
+ bindings = new SolrBindings(null, schema, this.positionalArgs);
+ try {
+ bindings.getDoubleValuesSource("ScOrE");
+ fail("ScOrE should not have bindings");
+ } catch (IllegalArgumentException e) {
+ // NOOP
+ }
+ try {
+ bindings.getDoubleValuesSource("score");
+ fail("score should not have bindings");
+ } catch (IllegalArgumentException e) {
+ // NOOP
+ }
+ }
+
+ /** tests an expression referring to a score field using an overridden score
binding */
+ public void testSortSsccoorree() {
+ assertQ(
+ "sort",
+ req(
+ "fl", "id",
+ "q", "{!func}field(int1_i)",
+ "sort", "expr_ssccoorree() desc,id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/str[@name='id'][.='1']",
+ "//result/doc[2]/str[@name='id'][.='4']",
+ "//result/doc[3]/str[@name='id'][.='5']",
+ "//result/doc[4]/str[@name='id'][.='3']",
+ "//result/doc[5]/str[@name='id'][.='2']");
+ }
+
+ /** tests a constant expression */
+ public void testSortConstant() {
+ assertQ(
+ "sort",
+ req("fl", "id", "q", "*:*", "sort", "sin1() desc,id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/str[@name='id'][.='1']",
+ "//result/doc[2]/str[@name='id'][.='2']",
+ "//result/doc[3]/str[@name='id'][.='3']",
+ "//result/doc[4]/str[@name='id'][.='4']",
+ "//result/doc[5]/str[@name='id'][.='5']");
+ }
+
+ // Removed testSortExpression as expressions can no longer reference other
expressions
+
+ /** tests an expression referring to an int field */
+ public void testSortInt() {
+ assertQ(
+ "sort",
+ req("fl", "id", "q", "*:*", "sort", "sqrt_int1_i() desc,id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/str[@name='id'][.='2']", // NaN
+ "//result/doc[2]/str[@name='id'][.='1']",
+ "//result/doc[3]/str[@name='id'][.='4']",
+ "//result/doc[4]/str[@name='id'][.='5']",
+ "//result/doc[5]/str[@name='id'][.='3']");
+ }
+
+ /** tests an expression referring to a double field */
+ public void testSortDouble() {
+ assertQ(
+ "sort",
+ req("fl", "id", "q", "*:*", "sort", "sqrt_double1_d() desc,id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/str[@name='id'][.='1']", // NaN
+ "//result/doc[2]/str[@name='id'][.='4']", // NaN
+ "//result/doc[3]/str[@name='id'][.='3']",
+ "//result/doc[4]/str[@name='id'][.='2']",
+ "//result/doc[5]/str[@name='id'][.='5']");
+ }
+
+ /** tests an expression referring to a date field */
+ public void testSortDate() {
+ assertQ(
+ "sort",
+ req("fl", "id", "q", "*:*", "sort", "date1_dt_minus_1990() desc,id
asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/str[@name='id'][.='2']",
+ "//result/doc[2]/str[@name='id'][.='5']",
+ "//result/doc[3]/str[@name='id'][.='1']",
+ "//result/doc[4]/str[@name='id'][.='3']",
+ "//result/doc[5]/str[@name='id'][.='4']");
+ }
+
+ /** tests an expression referring to a score field */
+ public void testSortScore() {
+ assertQ(
+ "sort",
+ req("fl", "id", "q", "{!func}field(int1_i)", "sort", "one_plus_score()
desc,id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/str[@name='id'][.='1']",
+ "//result/doc[2]/str[@name='id'][.='4']",
+ "//result/doc[3]/str[@name='id'][.='5']",
+ "//result/doc[4]/str[@name='id'][.='3']",
+ "//result/doc[5]/str[@name='id'][.='2']");
+ }
+
+ /** tests a constant expression */
+ public void testReturnConstant() {
+ final float expected = (float) Math.sin(1);
+ assertQ(
+ "return",
+ req("fl", "sin1:sin1()", "q", "*:*", "sort", "id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/float[@name='sin1'][.='" + expected + "']",
+ "//result/doc[2]/float[@name='sin1'][.='" + expected + "']",
+ "//result/doc[3]/float[@name='sin1'][.='" + expected + "']",
+ "//result/doc[4]/float[@name='sin1'][.='" + expected + "']",
+ "//result/doc[5]/float[@name='sin1'][.='" + expected + "']");
+ }
+
+ // Removed testReturnExpression as expressions can no longer reference other
expressions
+
+ /** tests an expression referring to an int field */
+ public void testReturnInt() {
+ assertQ(
+ "return",
+ req("fl", "foo:sqrt_int1_i()", "q", "*:*", "sort", "id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/float[@name='foo'][.=" + (float) Math.sqrt(50) + "]",
+ "//result/doc[2]/float[@name='foo'][.='NaN']",
+ "//result/doc[3]/float[@name='foo'][.=" + (float) Math.sqrt(10) + "]",
+ "//result/doc[4]/float[@name='foo'][.=" + (float) Math.sqrt(40) + "]",
+ "//result/doc[5]/float[@name='foo'][.=" + (float) Math.sqrt(20) + "]");
+ }
+
+ /** tests an expression referring to a double field */
+ public void testReturnDouble() {
+ assertQ(
+ "return",
+ req("fl", "bar:sqrt_double1_d()", "q", "*:*", "sort", "id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/float[@name='bar'][.='NaN']",
+ "//result/doc[2]/float[@name='bar'][.=" + (float) Math.sqrt(10.3d) +
"]",
+ "//result/doc[3]/float[@name='bar'][.=" + (float) Math.sqrt(500.3d) +
"]",
+ "//result/doc[4]/float[@name='bar'][.='NaN']",
+ "//result/doc[5]/float[@name='bar'][.=" + (float) Math.sqrt(2.1d) +
"]");
+ }
+
+ /** tests an expression referring to a date field */
+ public void testReturnDate() {
+ assertQ(
+ "return",
+ req("fl", "date1_dt_minus_1990:date1_dt_minus_1990()", "q", "*:*",
"sort", "id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/float[@name='date1_dt_minus_1990'][.='"
+ + (float)
+ (DateMathParser.parseMath(null,
"1996-12-19T16:39:57Z").getTime() - 631036800000D)
+ + "']",
+ "//result/doc[2]/float[@name='date1_dt_minus_1990'][.='"
+ + (float)
+ (DateMathParser.parseMath(null,
"1999-12-19T16:39:57Z").getTime() - 631036800000D)
+ + "']",
+ "//result/doc[3]/float[@name='date1_dt_minus_1990'][.='"
+ + (float)
+ (DateMathParser.parseMath(null,
"1995-12-19T16:39:57Z").getTime() - 631036800000D)
+ + "']",
+ "//result/doc[4]/float[@name='date1_dt_minus_1990'][.='"
+ + (float)
+ (DateMathParser.parseMath(null,
"1994-12-19T16:39:57Z").getTime() - 631036800000D)
+ + "']",
+ "//result/doc[5]/float[@name='date1_dt_minus_1990'][.='"
+ + (float)
+ (DateMathParser.parseMath(null,
"1997-12-19T16:39:57Z").getTime() - 631036800000D)
+ + "']");
+ }
+
+ /** tests an expression referring to score */
+ public void testReturnScores() {
+ assertQ(
+ "return",
+ // unfortunately, need to add fl=score for ValueSourceAugmenter to
access it
+ req(
+ "fl", "one_plus_score(),score",
+ "q", "{!func}field(int1_i)",
+ "sort", "id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/float[@name='one_plus_score()'][.='51.0']",
+ "//result/doc[2]/float[@name='one_plus_score()'][.='1.0']",
+ "//result/doc[3]/float[@name='one_plus_score()'][.='11.0']",
+ "//result/doc[4]/float[@name='one_plus_score()'][.='41.0']",
+ "//result/doc[5]/float[@name='one_plus_score()'][.='21.0']");
+ }
+
+ public void testReturnScores2() {
+ assertQ(
+ "return",
+ // unfortunately, need to add fl=score for ValueSourceAugmenter to
access it
+ req(
+ "fl", "two_plus_score:two_plus_score(),score",
+ "q", "{!func}field(int1_i)",
+ "sort", "id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/float[@name='two_plus_score'][.='52.0']",
+ "//result/doc[2]/float[@name='two_plus_score'][.='2.0']",
+ "//result/doc[3]/float[@name='two_plus_score'][.='12.0']",
+ "//result/doc[4]/float[@name='two_plus_score'][.='42.0']",
+ "//result/doc[5]/float[@name='two_plus_score'][.='22.0']");
+ }
+
+ public void testReturnScores3() {
+ assertQ(
+ "return",
+ // unfortunately, need to add fl=score for ValueSourceAugmenter to
access it
+ req(
+ "fl", "foo:sqrt_int1_i_plus_one_plus_score(),score",
+ "q", "{!func}field(int1_i)",
+ "sort", "id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/float[@name='foo'][.='" + (float) (Math.sqrt(50) + 1
+ 50) + "']",
+ "//result/doc[2]/float[@name='foo'][.='NaN']",
+ "//result/doc[3]/float[@name='foo'][.='" + (float) (Math.sqrt(10) + 1
+ 10) + "']",
+ "//result/doc[4]/float[@name='foo'][.='" + (float) (Math.sqrt(40) + 1
+ 40) + "']",
+ "//result/doc[5]/float[@name='foo'][.='" + (float) (Math.sqrt(20) + 1
+ 20) + "']");
+ }
+
+ /** tests an expression with positional arguments */
+ public void testPositionalArgs() {
+ assertQ(
+ "return",
+ req(
+ "fl", "sum:positional_args(10,20,30)",
+ "q", "*:*",
+ "sort", "id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/float[@name='sum'][.='60.0']",
+ "//result/doc[2]/float[@name='sum'][.='60.0']",
+ "//result/doc[3]/float[@name='sum'][.='60.0']",
+ "//result/doc[4]/float[@name='sum'][.='60.0']",
+ "//result/doc[5]/float[@name='sum'][.='60.0']");
+ }
+
+ /** tests embedding an expression in another value source */
+ public void testEmbeddedExpression() {
+ final float expected = (float) Math.sin(1);
+ assertQ(
+ "return",
+ req(
+ "fl", "embedded:sum(sin1(),5)",
+ "q", "*:*",
+ "sort", "id asc"),
+ "//*[@numFound='5']",
+ "//result/doc[1]/float[@name='embedded'][.='" + (expected + 5.0f) +
"']",
+ "//result/doc[2]/float[@name='embedded'][.='" + (expected + 5.0f) +
"']",
+ "//result/doc[3]/float[@name='embedded'][.='" + (expected + 5.0f) +
"']",
+ "//result/doc[4]/float[@name='embedded'][.='" + (expected + 5.0f) +
"']",
+ "//result/doc[5]/float[@name='embedded'][.='" + (expected + 5.0f) +
"']");
+ }
+}
diff --git
a/solr/solr-ref-guide/modules/query-guide/pages/expression-value-source-parser.adoc
b/solr/solr-ref-guide/modules/query-guide/pages/expression-value-source-parser.adoc
new file mode 100644
index 00000000000..ae9a9989a4e
--- /dev/null
+++
b/solr/solr-ref-guide/modules/query-guide/pages/expression-value-source-parser.adoc
@@ -0,0 +1,56 @@
+= Expression Value Source Parser
+// 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.
+
+The `ExpressionValueSourceParser` allows you to implement a custom valueSource
merely by adding a concise JavaScript expression to your `solrconfig.xml`.
+The expression is precompiled and offers competitive performance to those
written in Java.
+The syntax is a limited subset of JavaScript that is purely numerically
oriented, and only certain built-in functions can be called.
+The implementation is based on the Lucene Expressions module, which you can
learn more about in the
{lucene-javadocs}/expressions/org/apache/lucene/expressions/js/package-summary.html[Lucene
Expressions documentation].
+
+== Examples
+
+Expressions can reference field values directly by field name, the document's
score using the special name `score`, and positional arguments as `$1`, `$2`,
etc.
+The arguments might be constants, fields, or other functions supplying a
result.
+
+Here are some example definitions designed to illustrate these features:
+
+[source,xml]
+----
+<valueSourceParser name="sqrt_popularity"
class="solr.ExpressionValueSourceParser">
+ <str name="expression">sqrt(popularity)</str>
+</valueSourceParser>
+
+<valueSourceParser name="weighted_sum"
class="solr.ExpressionValueSourceParser">
+ <str name="expression">$1 * 0.8 + $2 * 0.2</str>
+</valueSourceParser>
+
+<valueSourceParser name="complex_score"
class="solr.ExpressionValueSourceParser">
+ <str name="expression">log(sum(popularity,recency)) * max(score,0.1)</str>
+</valueSourceParser>
+----
+
+Here is one unrealistic query using multiple function queries to illustrate
its features:
+
+[source,text]
+----
+&q=my query
+&fq={!frange l=1000}sqrt_popularity()
+&sort=complex_score() desc
+&fl=id,weighted_sum(field1,field2)
+----
+
+Using this VSP to boost/manipulate scores is more efficient than using the
`query()` function query, since the latter would execute the underlying `q` an
additional time.
diff --git
a/solr/solr-ref-guide/modules/query-guide/pages/function-queries.adoc
b/solr/solr-ref-guide/modules/query-guide/pages/function-queries.adoc
index 7c6f1a9d0ea..05abe1a114f 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/function-queries.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/function-queries.adoc
@@ -86,6 +86,8 @@ Only functions with fast random access are recommended.
The table below summarizes the functions available for function queries.
+Additionally, you can write a custom one using a tiny bit of JavaScript with
the xref:expression-value-source-parser.adoc[Expression Value Source Parser].
+
=== abs Function
Returns the absolute value of the specified value or function.
diff --git a/solr/solr-ref-guide/modules/query-guide/querying-nav.adoc
b/solr/solr-ref-guide/modules/query-guide/querying-nav.adoc
index d64e6be6c6b..9890f089047 100644
--- a/solr/solr-ref-guide/modules/query-guide/querying-nav.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/querying-nav.adoc
@@ -23,6 +23,7 @@
** xref:dismax-query-parser.adoc[]
** xref:edismax-query-parser.adoc[]
** xref:function-queries.adoc[]
+*** xref:expression-value-source-parser.adoc[]
** xref:local-params.adoc[]
** xref:json-request-api.adoc[]
*** xref:json-query-dsl.adoc[]