diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 725b71a6ba7..6fa157a09ef 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -190,6 +190,8 @@ New Features
* SOLR-8217: JSON Facet API: add "method" param to terms/field facets to give an execution
hint for what method should be used to facet. (yonik)
+* SOLR-8113: CloneFieldUpdateProcessorFactory now supports choosing a "dest" field name based on a regex
+ pattern and replacement init options. (Gus Heck, hossman)
Bug Fixes
----------------------
diff --git a/solr/core/src/java/org/apache/solr/update/processor/CloneFieldUpdateProcessorFactory.java b/solr/core/src/java/org/apache/solr/update/processor/CloneFieldUpdateProcessorFactory.java
index a68305314b4..290cf9dee49 100644
--- a/solr/core/src/java/org/apache/solr/update/processor/CloneFieldUpdateProcessorFactory.java
+++ b/solr/core/src/java/org/apache/solr/update/processor/CloneFieldUpdateProcessorFactory.java
@@ -21,8 +21,14 @@ import static org.apache.solr.common.SolrException.ErrorCode.SERVER_ERROR;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
@@ -40,18 +46,25 @@ import org.slf4j.LoggerFactory;
/**
* Clones the values found in any matching source
field into
- * the configured dest
field.
+ * a configured dest
field.
*
- * While the dest
field must be a single <str>
,
- * the source
fields can be configured as either:
+ * The source
field(s) can be configured as either:
*
*
* One or more <str>
* An <arr>
of <str>
* A <lst>
containing {@link FieldMutatingUpdateProcessorFactory FieldMutatingUpdateProcessorFactory style selector arguments}
*
- *
- * If the dest
field already exists in the document, then the
+ *
+ *
The dest
field can be a single <str>
+ * containing the literal name of a destination field, or it may be a <lst>
specifying a
+ * regex pattern
and a replacement
string. If the pattern + replacement option
+ * is used the pattern will be matched against all fields matched by the source selector, and the replacement
+ * string (including any capture groups specified from the pattern) will be evaluated a using
+ * {@link Matcher#replaceAll(String)} to generate the literal name of the destination field.
+ *
+ *
+ * If the resolved dest
field already exists in the document, then the
* values from the source
fields will be added to it. The
* "boost" value associated with the dest
will not be changed,
* and any boost specified on the source
fields will be ignored.
@@ -59,14 +72,23 @@ import org.slf4j.LoggerFactory;
* newly created dest
field will have the default boost of 1.0)
*
*
- * In the example below, the category
field will be cloned
- * into the category_s
field, both the authors
and
- * editors
fields will be cloned into the contributors
- * field, and any field with a name ending in _price
-- except for
- * list_price
-- will be cloned into the all_prices
- * field.
+ * In the example below:
*
- *
+ *
+ * The category
field will be cloned into the category_s
field
+ * Both the authors
and editors
fields will be cloned into the
+ * contributors
field
+ *
+ * Any field with a name ending in _price
-- except for
+ * list_price
-- will be cloned into the all_prices
+ *
+ * Any field name beginning with feat and ending in s (i.e. feats or features)
+ * will be cloned into a field prefixed with key_ and not ending in s. (i.e. key_feat or key_feature)
+ *
+ *
+ *
+ *
*
* <updateRequestProcessorChain name="multiple-clones">
* <processor class="solr.CloneFieldUpdateProcessorFactory">
@@ -82,30 +104,87 @@ import org.slf4j.LoggerFactory;
* </processor>
* <processor class="solr.CloneFieldUpdateProcessorFactory">
* <lst name="source">
- * <str name="fieldRegex">.*_price</str>
+ * <str name="fieldRegex">.*_price$</str>
* <lst name="exclude">
* <str name="fieldName">list_price</str>
* </lst>
* </lst>
* <str name="dest">all_prices</str>
* </processor>
+ * <processor class="solr.processor.CloneFieldUpdateProcessorFactory">
+ * <lst name="source">
+ * <str name="fieldRegex">^feat(.*)s$</str>
+ * </lst>
+ * <lst name="dest">
+ * <str name="pattern">^feat(.*)s$</str>
+ * <str name="replacement">key_feat$1</str>
+ * </str>
+ * </processor>
* </updateRequestProcessorChain>
*
+ *
+ *
+ * In common case situations where you wish to use a single regular expression as both a
+ * fieldRegex
selector and a destination pattern
, a "short hand" syntax
+ * is support for convinience: The pattern
and replacement
may be specified
+ * at the top level, omitting source
and dest
declarations completely, and
+ * the pattern
will be used to construct an equivilent source
selector internally.
+ *
+ *
+ * For example, both of the following configurations are equivilent:
+ *
+ *
+ * <!-- full syntax -->
+ * <processor class="solr.processor.CloneFieldUpdateProcessorFactory">
+ * <lst name="source">
+ * <str name="fieldRegex"^gt;$feat(.*)s$</str>
+ * </lst>
+ * <lst name="dest">
+ * <str name="pattern">^feat(.*)s$</str>
+ * <str name="replacement">key_feat$1</str>
+ * </str>
+ * </processor>
+ *
+ * <!-- syntactic sugar syntax -->
+ * <processor class="solr.processor.CloneFieldUpdateProcessorFactory">
+ * <str name="pattern">^feat(.*)s$</str>
+ * <str name="replacement">key_feat$1</str>
+ * </processor>
+ *
+ *
+ *
+ * When cloning multiple fields (or a single multivalued field) into a single valued field, one of the
+ * {@link FieldValueSubsetUpdateProcessorFactory} implementations configured after the
+ * CloneFieldUpdateProcessorFactory
can be useful to reduce the list of values down to a
+ * single value.
+ *
+ *
+ * @see FieldValueSubsetUpdateProcessorFactory
*/
public class CloneFieldUpdateProcessorFactory
extends UpdateRequestProcessorFactory implements SolrCoreAware {
-
+
private final static Logger log = LoggerFactory.getLogger(CloneFieldUpdateProcessorFactory.class);
public static final String SOURCE_PARAM = "source";
public static final String DEST_PARAM = "dest";
-
+ public static final String PATTERN_PARAM = "pattern";
+ public static final String REPLACEMENT_PARAM = "replacement";
+
private SelectorParams srcInclusions = new SelectorParams();
private Collection srcExclusions
= new ArrayList<>();
private FieldNameSelector srcSelector = null;
+
+ /**
+ * If pattern is null, this this is a literal field name. If pattern is non-null then this
+ * is a replacement string that may contain meta-characters (ie: capture group identifiers)
+ * @see #pattern
+ */
private String dest = null;
+ /** @see #dest */
+ private Pattern pattern = null;
protected final FieldNameSelector getSourceSelector() {
if (null != srcSelector) return srcSelector;
@@ -117,72 +196,198 @@ public class CloneFieldUpdateProcessorFactory
@SuppressWarnings("unchecked")
@Override
public void init(NamedList args) {
- Object d = args.remove(DEST_PARAM);
- if (null == d) {
- throw new SolrException
- (SERVER_ERROR, "Init param '" + DEST_PARAM + "' must be specified");
- } else if (! (d instanceof CharSequence) ) {
- throw new SolrException
- (SERVER_ERROR, "Init param '" + DEST_PARAM + "' must be a string (ie: 'str')");
- }
- dest = d.toString();
- List sources = args.getAll(SOURCE_PARAM);
- if (0 == sources.size()) {
- throw new SolrException
- (SERVER_ERROR, "Init param '" + SOURCE_PARAM + "' must be specified");
- }
- if (1 == sources.size() && sources.get(0) instanceof NamedList) {
- // nested set of selector options
- NamedList selectorConfig = (NamedList) args.remove(SOURCE_PARAM);
-
- srcInclusions = parseSelectorParams(selectorConfig);
-
- List excList = selectorConfig.getAll("exclude");
-
- for (Object excObj : excList) {
- if (null == excObj) {
- throw new SolrException
- (SERVER_ERROR, "Init param '" + SOURCE_PARAM +
- "' child 'exclude' can not be null");
- }
- if (! (excObj instanceof NamedList) ) {
- throw new SolrException
- (SERVER_ERROR, "Init param '" + SOURCE_PARAM +
- "' child 'exclude' must be ");
- }
- NamedList exc = (NamedList) excObj;
- srcExclusions.add(parseSelectorParams(exc));
- if (0 < exc.size()) {
- throw new SolrException(SERVER_ERROR, "Init param '" + SOURCE_PARAM +
- "' has unexpected 'exclude' sub-param(s): '"
- + selectorConfig.getName(0) + "'");
- }
- // call once per instance
- selectorConfig.remove("exclude");
- }
-
- if (0 < selectorConfig.size()) {
- throw new SolrException(SERVER_ERROR, "Init param '" + SOURCE_PARAM +
- "' contains unexpected child param(s): '" +
- selectorConfig.getName(0) + "'");
- }
+ // high level (loose) check for which type of config we have.
+ //
+ // individual init methods do more strict syntax checking
+ if (0 <= args.indexOf(SOURCE_PARAM, 0) && 0 <= args.indexOf(DEST_PARAM, 0) ) {
+ initSourceSelectorSyntax(args);
+ } else if (0 <= args.indexOf(PATTERN_PARAM, 0) && 0 <= args.indexOf(REPLACEMENT_PARAM, 0)) {
+ initSimpleRegexReplacement(args);
} else {
- // source better be one or more strings
- srcInclusions.fieldName = new HashSet<>(args.removeConfigArgs("source"));
+ throw new SolrException(SERVER_ERROR, "A combination of either '" + SOURCE_PARAM + "' + '"+
+ DEST_PARAM + "', or '" + REPLACEMENT_PARAM + "' + '" +
+ PATTERN_PARAM + "' init params are mandatory");
}
-
-
if (0 < args.size()) {
- throw new SolrException(SERVER_ERROR,
- "Unexpected init param(s): '" +
- args.getName(0) + "'");
+ throw new SolrException(SERVER_ERROR,
+ "Unexpected init param(s): '" +
+ args.getName(0) + "'");
}
super.init(args);
}
+ /**
+ * init helper method that should only be called when we know for certain that both the
+ * "source" and "dest" init params do not exist.
+ */
+ @SuppressWarnings("unchecked")
+ private void initSimpleRegexReplacement(NamedList args) {
+ // The syntactic sugar for the case where there is only one regex pattern for source and the same pattern
+ // is used for the destination pattern...
+ //
+ // pattern != null && replacement != null
+ //
+ // ...as top level elements, with no other config options specified
+
+ // if we got here we know we had pattern and replacement, now check for the other two so that we can give a better
+ // message than "unexpected"
+ if (0 <= args.indexOf(SOURCE_PARAM, 0) || 0 <= args.indexOf(DEST_PARAM, 0) ) {
+ throw new SolrException(SERVER_ERROR,"Short hand syntax must not be mixed with full syntax. Found " +
+ PATTERN_PARAM + " and " + REPLACEMENT_PARAM + " but also found " + SOURCE_PARAM + " or " + DEST_PARAM);
+ }
+
+ assert args.indexOf(SOURCE_PARAM, 0) < 0;
+
+ Object patt = args.remove(PATTERN_PARAM);
+ Object replacement = args.remove(REPLACEMENT_PARAM);
+
+ if (null == patt || null == replacement) {
+ throw new SolrException(SERVER_ERROR, "Init params '" + PATTERN_PARAM + "' and '" +
+ REPLACEMENT_PARAM + "' are both mandatory if '" + SOURCE_PARAM + "' and '"+
+ DEST_PARAM + "' are not both specified");
+ }
+
+ if (0 != args.size()) {
+ throw new SolrException(SERVER_ERROR, "Init params '" + REPLACEMENT_PARAM + "' and '" +
+ PATTERN_PARAM + "' must be children of '" + DEST_PARAM +
+ "' to be combined with other options.");
+ }
+
+ if (!(replacement instanceof String)) {
+ throw new SolrException(SERVER_ERROR, "Init param '" + REPLACEMENT_PARAM + "' must be a string (i.e. )");
+ }
+ if (!(patt instanceof String)) {
+ throw new SolrException(SERVER_ERROR, "Init param '" + PATTERN_PARAM + "' must be a string (i.e. )");
+ }
+
+ dest = replacement.toString();
+ try {
+ this.pattern = Pattern.compile(patt.toString());
+ } catch (PatternSyntaxException pe) {
+ throw new SolrException(SERVER_ERROR, "Init param " + PATTERN_PARAM +
+ " is not a valid regex pattern: " + patt, pe);
+
+ }
+ srcInclusions = new SelectorParams();
+ srcInclusions.fieldRegex = Collections.singletonList(this.pattern);
+ }
+
+ /**
+ * init helper method that should only be called when we know for certain that both the
+ * "source" and "dest" init params do exist.
+ */
+ @SuppressWarnings("unchecked")
+ private void initSourceSelectorSyntax(NamedList args) {
+ // Full and complete syntax where source and dest are mandatory.
+ //
+ // source may be a single string or a selector.
+ // dest may be a single string or list containing pattern and replacement
+ //
+ // source != null && dest != null
+
+ // if we got here we know we had source and dest, now check for the other two so that we can give a better
+ // message than "unexpected"
+ if (0 <= args.indexOf(PATTERN_PARAM, 0) || 0 <= args.indexOf(REPLACEMENT_PARAM, 0) ) {
+ throw new SolrException(SERVER_ERROR,"Short hand syntax must not be mixed with full syntax. Found " +
+ SOURCE_PARAM + " and " + DEST_PARAM + " but also found " + PATTERN_PARAM + " or " + REPLACEMENT_PARAM);
+ }
+
+ Object d = args.remove(DEST_PARAM);
+ assert null != d;
+
+ List sources = args.getAll(SOURCE_PARAM);
+ assert null != sources;
+
+ if (1 == sources.size()) {
+ if (sources.get(0) instanceof NamedList) {
+ // nested set of selector options
+ NamedList selectorConfig = (NamedList) args.remove(SOURCE_PARAM);
+
+ srcInclusions = parseSelectorParams(selectorConfig);
+
+ List excList = selectorConfig.getAll("exclude");
+
+ for (Object excObj : excList) {
+ if (null == excObj) {
+ throw new SolrException(SERVER_ERROR, "Init param '" + SOURCE_PARAM +
+ "' child 'exclude' can not be null");
+ }
+ if (!(excObj instanceof NamedList)) {
+ throw new SolrException(SERVER_ERROR, "Init param '" + SOURCE_PARAM +
+ "' child 'exclude' must be ");
+ }
+ NamedList exc = (NamedList) excObj;
+ srcExclusions.add(parseSelectorParams(exc));
+ if (0 < exc.size()) {
+ throw new SolrException(SERVER_ERROR, "Init param '" + SOURCE_PARAM +
+ "' has unexpected 'exclude' sub-param(s): '"
+ + selectorConfig.getName(0) + "'");
+ }
+ // call once per instance
+ selectorConfig.remove("exclude");
+ }
+
+ if (0 < selectorConfig.size()) {
+ throw new SolrException(SERVER_ERROR, "Init param '" + SOURCE_PARAM +
+ "' contains unexpected child param(s): '" +
+ selectorConfig.getName(0) + "'");
+ }
+ // consume from the named list so it doesn't interfere with subsequent processing
+ sources.remove(0);
+ }
+ }
+ if (1 <= sources.size()) {
+ // source better be one or more strings
+ srcInclusions.fieldName = new HashSet<>(args.removeConfigArgs("source"));
+ }
+ if (srcInclusions == null) {
+ throw new SolrException(SERVER_ERROR, "Init params do not specify anything to clone, please supply either "
+ + SOURCE_PARAM + " and " + DEST_PARAM + " or " + PATTERN_PARAM + " and " + REPLACEMENT_PARAM + ". See javadocs" +
+ "for CloneFieldUpdateProcessorFactory for further details.");
+ }
+
+ if (d instanceof NamedList) {
+ NamedList destList = (NamedList) d;
+
+ Object patt = destList.remove(PATTERN_PARAM);
+ Object replacement = destList.remove(REPLACEMENT_PARAM);
+
+ if (null == patt || null == replacement) {
+ throw new SolrException(SERVER_ERROR, "Init param '" + DEST_PARAM + "' children '" +
+ PATTERN_PARAM + "' and '" + REPLACEMENT_PARAM +
+ "' are both mandatoryand can not be null");
+ }
+ if (! (patt instanceof String && replacement instanceof String)) {
+ throw new SolrException(SERVER_ERROR, "Init param '" + DEST_PARAM + "' children '" +
+ PATTERN_PARAM + "' and '" + REPLACEMENT_PARAM +
+ "' must both be strings (i.e. )");
+ }
+ if (0 != destList.size()) {
+ throw new SolrException(SERVER_ERROR, "Init param '" + DEST_PARAM + "' has unexpected children: '"
+ + destList.getName(0) + "'");
+ }
+
+ try {
+ this.pattern = Pattern.compile(patt.toString());
+ } catch (PatternSyntaxException pe) {
+ throw new SolrException(SERVER_ERROR, "Init param '" + DEST_PARAM + "' child '" + PATTERN_PARAM +
+ " is not a valid regex pattern: " + patt, pe);
+ }
+ dest = replacement.toString();
+
+ } else if (d instanceof String) {
+ dest = d.toString();
+ } else {
+ throw new SolrException(SERVER_ERROR, "Init param '" + DEST_PARAM + "' must either be a string " +
+ "(i.e. ) or a list (i.e. ) containing '" +
+ PATTERN_PARAM + "' and '" + REPLACEMENT_PARAM);
+ }
+
+ }
+
@Override
public void inform(final SolrCore core) {
@@ -202,32 +407,58 @@ public class CloneFieldUpdateProcessorFactory
public final UpdateRequestProcessor getInstance(SolrQueryRequest req,
SolrQueryResponse rsp,
UpdateRequestProcessor next) {
+ final FieldNameSelector srcSelector = getSourceSelector();
return new UpdateRequestProcessor(next) {
@Override
public void processAdd(AddUpdateCommand cmd) throws IOException {
final SolrInputDocument doc = cmd.getSolrInputDocument();
+ // destination may be regex replace string, which can cause multiple output fields.
+ Map destMap = new HashMap<>();
+
// preserve initial values and boost (if any)
- SolrInputField destField = doc.containsKey(dest) ?
- doc.getField(dest) : new SolrInputField(dest);
-
- boolean modified = false;
for (final String fname : doc.getFieldNames()) {
if (! srcSelector.shouldMutate(fname)) continue;
Collection srcFieldValues = doc.getFieldValues(fname);
if(srcFieldValues == null || srcFieldValues.isEmpty()) continue;
+
+ String resolvedDest = dest;
+
+ if (pattern != null) {
+ Matcher matcher = pattern.matcher(fname);
+ if (matcher.find()) {
+ resolvedDest = matcher.replaceAll(dest);
+ } else {
+ log.debug("CloneFieldUpdateProcessor.srcSelector.shouldMutate(\"{}\") returned true, " +
+ "but replacement pattern did not match, field skipped.", fname);
+ continue;
+ }
+ }
+ SolrInputField destField;
+ if (doc.containsKey(resolvedDest)) {
+ destField = doc.getField(resolvedDest);
+ } else {
+ SolrInputField targetField = destMap.get(resolvedDest);
+ if (targetField == null) {
+ destField = new SolrInputField(resolvedDest);
+ } else {
+ destField = targetField;
+ }
+ }
for (Object val : srcFieldValues) {
// preserve existing dest boost (multiplicitive), ignore src boost
destField.addValue(val, 1.0f);
}
- modified=true;
+ // put it in map to avoid concurrent modification...
+ destMap.put(resolvedDest, destField);
}
- if (modified) doc.put(dest, destField);
-
+ for (String dest : destMap.keySet()) {
+ doc.put(dest, destMap.get(dest));
+ }
super.processAdd(cmd);
}
};
diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig-update-processor-chains.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig-update-processor-chains.xml
index e4f6f8023fd..56402819a7f 100644
--- a/solr/core/src/test-files/solr/collection1/conf/solrconfig-update-processor-chains.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-update-processor-chains.xml
@@ -292,6 +292,17 @@
dest_s
+
+
+
+ source1_s
+
+ source\d(_s)
+ dest$1
+
+
+
+
source1_s
@@ -300,6 +311,17 @@
+
+
+ source1_s
+ source2_s
+
+ source\d(_s)
+ dest$1
+
+
+
+
@@ -310,6 +332,19 @@
+
+
+
+ source1_s
+ source2_s
+
+
+ source\d(_s)
+ dest$1
+
+
+
+
@@ -321,6 +356,41 @@
dest_s
+
+
+
+
+ source\d_.*
+
+ source0_.*
+
+
+
+ source\d(_s)
+ dest$1
+
+
+
+
+
+
+ source\d_.*
+ dest_s
+
+
+
+
+
+
+ foo.*
+
+
+
+ x(\d)
+ y$1
+
+
+
@@ -364,6 +434,16 @@
toField
+
+
+
+ field(.*)
+ toField
+
+
+ toField
+
+
@@ -380,13 +460,27 @@
- .*_price
+ .*_price$
list_price
all_prices
+
+
+ ^feat(.*)s$
+
+
+ ^feat(.*)s$
+ key_feat$1
+
+
+
+
+ ^feat(.*)s$
+ best_feat$1
+
diff --git a/solr/core/src/test/org/apache/solr/update/processor/CloneFieldUpdateProcessorFactoryTest.java b/solr/core/src/test/org/apache/solr/update/processor/CloneFieldUpdateProcessorFactoryTest.java
new file mode 100644
index 00000000000..ac618416228
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/update/processor/CloneFieldUpdateProcessorFactoryTest.java
@@ -0,0 +1,377 @@
+/*
+ * 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.update.processor;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.solr.common.SolrInputDocument;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class CloneFieldUpdateProcessorFactoryTest extends UpdateProcessorTestBase {
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ initCore("solrconfig-update-processor-chains.xml", "schema12.xml");
+ }
+
+ @Test
+ public void testSimpleClone() throws Exception {
+ SolrInputDocument doc = processAdd("clone-single",
+ doc(f("id", "1"),
+ f("source1_s", "foo")
+ ));
+ assertEquals("source1_s should have stringValue", "foo", doc.getFieldValue("source1_s"));
+ assertEquals("dest_s should have stringValue", "foo", doc.getFieldValue("dest_s"));
+ }
+
+ @Test
+ public void testMultiClone() throws Exception {
+ SolrInputDocument doc = processAdd("clone-multi",
+ doc(f("id", "1"),
+ f("source1_s", "foo"),
+ f("source2_s", "bar")));
+
+ assertEquals("source1_s should have stringValue", "foo", doc.getFieldValue("source1_s"));
+ assertEquals("source2_s should have stringValue", "bar", doc.getFieldValue("source2_s"));
+ Collection dest_s = doc.getFieldValues("dest_s");
+ assertTrue(dest_s.contains("foo"));
+ assertTrue(dest_s.contains("bar"));
+ }
+
+ @Test
+ public void testArrayClone() throws Exception {
+ SolrInputDocument doc = processAdd("clone-array",
+ doc(f("id", "1"),
+ f("source1_s", "foo"),
+ f("source2_s", "bar")));
+
+ assertEquals("source1_s should have stringValue", "foo", doc.getFieldValue("source1_s"));
+ assertEquals("source2_s should have stringValue", "bar", doc.getFieldValue("source2_s"));
+ Collection dest_s = doc.getFieldValues("dest_s");
+ assertTrue(dest_s.contains("foo"));
+ assertTrue(dest_s.contains("bar"));
+ }
+
+ @Test
+ public void testSelectorClone() throws Exception {
+ SolrInputDocument doc = processAdd("clone-selector",
+ doc(f("id", "1"),
+ f("source0_s", "nope, not me"),
+ f("source1_s", "foo"),
+ f("source2_s", "bar")));
+
+ assertEquals("source0_s should have stringValue", "nope, not me", doc.getFieldValue("source0_s"));
+ assertEquals("source1_s should have stringValue", "foo", doc.getFieldValue("source1_s"));
+ assertEquals("source2_s should have stringValue", "bar", doc.getFieldValue("source2_s"));
+ Collection dest_s = doc.getFieldValues("dest_s");
+ assertTrue(dest_s.contains("foo"));
+ assertTrue(dest_s.contains("bar"));
+ assertFalse(dest_s.contains("nope, not me"));
+ }
+
+ public void testMultipleClones() throws Exception {
+ SolrInputDocument doc = processAdd("multiple-clones",
+ doc(f("id", "1"),
+ f("category", "test"),
+ f("authors", "author1", "author2"),
+ f("editors", "ed1", "ed2"),
+ f("bfriday_price", 4.00),
+ f("sale_price", 5.00),
+ f("list_price", 6.00),
+ f("features", "hill", "valley", "dune")));
+
+ // the original values should remain
+ assertEquals("category should have a value", "test", doc.getFieldValue("category"));
+
+ Collection auths = doc.getFieldValues("authors");
+ assertTrue(auths.size() == 2);
+ assertTrue(auths.contains("author1"));
+ assertTrue(auths.contains("author2"));
+ Collection eds = doc.getFieldValues("editors");
+ assertTrue(eds.size() == 2);
+ assertTrue(eds.contains("ed1"));
+ assertTrue(eds.contains("ed2"));
+
+ assertEquals("bfriday_price should have a value", 4.0, doc.getFieldValue("bfriday_price"));
+ assertEquals("sale_price should have a value", 5.0, doc.getFieldValue("sale_price"));
+ assertEquals("list_price should have a value", 6.0, doc.getFieldValue("list_price"));
+
+ Collection features = doc.getFieldValues("features");
+ assertTrue(features.size() == 3);
+ assertTrue(features.contains("hill"));
+ assertTrue(features.contains("valley"));
+ assertTrue(features.contains("dune"));
+
+ // and the copied values shoul be added
+ assertEquals("category_s should have a value", "test", doc.getFieldValue("category_s"));
+
+ Collection contribs = doc.getFieldValues("contributors");
+ assertTrue(contribs.size() == 4);
+ assertTrue(contribs.contains("author1"));
+ assertTrue(contribs.contains("author2"));
+ assertTrue(contribs.contains("ed1"));
+ assertTrue(contribs.contains("ed2"));
+
+ Collection prices = doc.getFieldValues("all_prices");
+ assertTrue(prices.size() == 2);
+ assertTrue(prices.contains(5.0));
+ assertTrue(prices.contains(4.0));
+ assertFalse(prices.contains(6.0));
+
+ // n.b. the field names below imply singularity but that would be achieved with a subsequent
+ // FirstFieldValueUpdateProcessorFactory (or similar custom class), and not in clone field itself
+
+ Collection keyf = doc.getFieldValues("key_feature");
+ assertTrue(keyf.size() == 3);
+ assertTrue(keyf.contains("hill"));
+ assertTrue(keyf.contains("valley"));
+ assertTrue(keyf.contains("dune"));
+
+ Collection bestf = doc.getFieldValues("best_feature");
+ assertTrue(bestf.size() == 3);
+ assertTrue(bestf.contains("hill"));
+ assertTrue(bestf.contains("valley"));
+ assertTrue(bestf.contains("dune"));
+ }
+
+ public void testCloneField() throws Exception {
+
+ SolrInputDocument d;
+
+ // regardless of chain, all of these checks should be equivilent
+ for (String chain : Arrays.asList("clone-single", "clone-single-regex",
+ "clone-multi", "clone-multi-regex",
+ "clone-array", "clone-array-regex",
+ "clone-selector", "clone-selector-regex")) {
+
+ // simple clone
+ d = processAdd(chain,
+ doc(f("id", "1111"),
+ f("source0_s", "NOT COPIED"),
+ f("source1_s", "123456789", "", 42, "abcd")));
+ assertNotNull(chain, d);
+ assertEquals(chain,
+ Arrays.asList("123456789", "", 42, "abcd"),
+ d.getFieldValues("source1_s"));
+ assertEquals(chain,
+ Arrays.asList("123456789", "", 42, "abcd"),
+ d.getFieldValues("dest_s"));
+
+ // append to existing values, preserve boost
+ d = processAdd(chain,
+ doc(f("id", "1111"),
+ field("dest_s", 2.3f, "orig1", "orig2"),
+ f("source0_s", "NOT COPIED"),
+ f("source1_s", "123456789", "", 42, "abcd")));
+ assertNotNull(chain, d);
+ assertEquals(chain,
+ Arrays.asList("123456789", "", 42, "abcd"),
+ d.getFieldValues("source1_s"));
+ assertEquals(chain,
+ Arrays.asList("orig1", "orig2", "123456789", "", 42, "abcd"),
+ d.getFieldValues("dest_s"));
+ assertEquals(chain + ": dest boost changed",
+ 2.3f, d.getField("dest_s").getBoost(), 0.0f);
+ }
+
+ // should be equivilent for any chain matching source1_s and source2_s (but not source0_s)
+ for (String chain : Arrays.asList("clone-multi", "clone-multi-regex",
+ "clone-array", "clone-array-regex",
+ "clone-selector", "clone-selector-regex")) {
+
+ // simple clone
+ d = processAdd(chain,
+ doc(f("id", "1111"),
+ f("source0_s", "NOT COPIED"),
+ f("source1_s", "123456789", "", 42, "abcd"),
+ f("source2_s", "xxx", 999)));
+ assertNotNull(chain, d);
+ assertEquals(chain,
+ Arrays.asList("123456789", "", 42, "abcd"),
+ d.getFieldValues("source1_s"));
+ assertEquals(chain,
+ Arrays.asList("xxx", 999),
+ d.getFieldValues("source2_s"));
+ assertEquals(chain,
+ Arrays.asList("123456789", "", 42, "abcd", "xxx", 999),
+ d.getFieldValues("dest_s"));
+
+ // append to existing values, preserve boost
+ d = processAdd(chain,
+ doc(f("id", "1111"),
+ field("dest_s", 2.3f, "orig1", "orig2"),
+ f("source0_s", "NOT COPIED"),
+ f("source1_s", "123456789", "", 42, "abcd"),
+ f("source2_s", "xxx", 999)));
+ assertNotNull(chain, d);
+ assertEquals(chain,
+ Arrays.asList("123456789", "", 42, "abcd"),
+ d.getFieldValues("source1_s"));
+ assertEquals(chain,
+ Arrays.asList("xxx", 999),
+ d.getFieldValues("source2_s"));
+ assertEquals(chain,
+ Arrays.asList("orig1", "orig2",
+ "123456789", "", 42, "abcd",
+ "xxx", 999),
+ d.getFieldValues("dest_s"));
+ assertEquals(chain + ": dest boost changed",
+ 2.3f, d.getField("dest_s").getBoost(), 0.0f);
+ }
+
+ // any chain that copies source1_s to dest_s should be equivilent for these assertions
+ for (String chain : Arrays.asList("clone-simple-regex-syntax",
+ "clone-single", "clone-single-regex",
+ "clone-multi", "clone-multi-regex",
+ "clone-array", "clone-array-regex",
+ "clone-selector", "clone-selector-regex")) {
+
+ // simple clone
+ d = processAdd(chain,
+ doc(f("id", "1111"),
+ f("source1_s", "123456789", "", 42, "abcd")));
+ assertNotNull(chain, d);
+ assertEquals(chain,
+ Arrays.asList("123456789", "", 42, "abcd"),
+ d.getFieldValues("source1_s"));
+ assertEquals(chain,
+ Arrays.asList("123456789", "", 42, "abcd"),
+ d.getFieldValues("dest_s"));
+
+ // append to existing values, preserve boost
+ d = processAdd(chain,
+ doc(f("id", "1111"),
+ field("dest_s", 2.3f, "orig1", "orig2"),
+ f("source1_s", "123456789", "", 42, "abcd")));
+ assertNotNull(chain, d);
+ assertEquals(chain,
+ Arrays.asList("123456789", "", 42, "abcd"),
+ d.getFieldValues("source1_s"));
+ assertEquals(chain,
+ Arrays.asList("orig1", "orig2", "123456789", "", 42, "abcd"),
+ d.getFieldValues("dest_s"));
+ assertEquals(chain + ": dest boost changed",
+ 2.3f, d.getField("dest_s").getBoost(), 0.0f);
+ }
+ }
+
+ public void testCloneFieldRegexReplaceAll() throws Exception {
+ SolrInputDocument d = processAdd("clone-regex-replaceall",
+ doc(f("id", "1111"),
+ f("foo_x2_s", "123456789", "", 42, "abcd"),
+ f("foo_x3_x7_s", "xyz")));
+
+ assertNotNull(d);
+ assertEquals(Arrays.asList("123456789", "", 42, "abcd"),
+ d.getFieldValues("foo_y2_s"));
+ assertEquals("xyz",
+ d.getFieldValue("foo_y3_y7_s"));
+ }
+
+ public void testCloneFieldExample() throws Exception {
+
+ SolrInputDocument d;
+
+ // test example from the javadocs
+ d = processAdd("multiple-clones",
+ doc(f("id", "1111"),
+ f("category", "misc"),
+ f("authors", "Isaac Asimov", "John Brunner"),
+ f("editors", "John W. Campbell"),
+ f("store1_price", 87),
+ f("store2_price", 78),
+ f("store3_price", (Object) null),
+ f("list_price", 1000),
+ f("features", "Pages!", "Binding!"),
+ f("feat_of_strengths", "Pullups")));
+
+ assertNotNull(d);
+ assertEquals("misc", d.getFieldValue("category"));
+ assertEquals("misc", d.getFieldValue("category_s"));
+ assertEquals(Arrays.asList("Isaac Asimov", "John Brunner"),
+ d.getFieldValues("authors"));
+ assertEquals(Collections.singletonList("John W. Campbell"),
+ d.getFieldValues("editors"));
+ assertEquals(Arrays.asList("Isaac Asimov", "John Brunner",
+ "John W. Campbell"),
+ d.getFieldValues("contributors"));
+ assertEquals(87, d.getFieldValue("store1_price"));
+ assertEquals(78, d.getFieldValue("store2_price"));
+ assertEquals(1000, d.getFieldValue("list_price"));
+ assertEquals(Arrays.asList(87, 78),
+ d.getFieldValues("all_prices"));
+
+ assertEquals(Arrays.asList("Pages!", "Binding!"),
+ d.getFieldValues("key_feature"));
+ assertEquals("Pullups", d.getFieldValue("key_feat_of_strength"));
+ }
+
+ public void testCloneCombinations() throws Exception {
+
+ SolrInputDocument d;
+
+ // maxChars
+ d = processAdd("clone-max-chars",
+ doc(f("id", "1111"),
+ f("field1", "text")));
+ assertNotNull(d);
+ assertEquals("text", d.getFieldValue("field1"));
+ assertEquals("tex", d.getFieldValue("toField"));
+
+ // move
+ d = processAdd("clone-move",
+ doc(f("id", "1111"),
+ f("field1", "text")));
+ assertNotNull(d);
+ assertEquals("text", d.getFieldValue("toField"));
+ assertFalse(d.containsKey("field1"));
+
+ // replace
+ d = processAdd("clone-replace",
+ doc(f("id", "1111"),
+ f("toField", "IGNORED"),
+ f("field1", "text")));
+ assertNotNull(d);
+ assertEquals("text", d.getFieldValue("field1"));
+ assertEquals("text", d.getFieldValue("toField"));
+
+ // append
+ d = processAdd("clone-append",
+ doc(f("id", "1111"),
+ f("toField", "aaa"),
+ f("field1", "bbb"),
+ f("field2", "ccc")));
+ assertNotNull(d);
+ assertEquals("bbb", d.getFieldValue("field1"));
+ assertEquals("ccc", d.getFieldValue("field2"));
+ assertEquals("aaa; bbb; ccc", d.getFieldValue("toField"));
+
+ // first value
+ d = processAdd("clone-first",
+ doc(f("id", "1111"),
+ f("field0", "aaa"),
+ f("field1", "bbb"),
+ f("field2", "ccc")));
+ assertNotNull(d);
+ assertEquals("aaa", d.getFieldValue("toField"));
+ }
+
+}
diff --git a/solr/core/src/test/org/apache/solr/update/processor/FieldMutatingUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/FieldMutatingUpdateProcessorTest.java
index 77d3c049a07..1966c698324 100644
--- a/solr/core/src/test/org/apache/solr/update/processor/FieldMutatingUpdateProcessorTest.java
+++ b/solr/core/src/test/org/apache/solr/update/processor/FieldMutatingUpdateProcessorTest.java
@@ -728,120 +728,6 @@ public class FieldMutatingUpdateProcessorTest extends UpdateProcessorTestBase {
}
- public void testCloneField() throws Exception {
-
- SolrInputDocument d = null;
-
- // regardless of chain, all of these should be equivilent
- for (String chain : Arrays.asList("clone-single", "clone-multi",
- "clone-array","clone-selector" )) {
-
- // simple clone
- d = processAdd(chain,
- doc(f("id", "1111"),
- f("source0_s", "NOT COPIED"),
- f("source1_s", "123456789", "", 42, "abcd")));
- assertNotNull(chain, d);
- assertEquals(chain,
- Arrays.asList("123456789", "", 42, "abcd"),
- d.getFieldValues("source1_s"));
- assertEquals(chain,
- Arrays.asList("123456789", "", 42, "abcd"),
- d.getFieldValues("dest_s"));
-
- // append to existing values, preserve boost
- d = processAdd(chain,
- doc(f("id", "1111"),
- field("dest_s", 2.3f, "orig1", "orig2"),
- f("source0_s", "NOT COPIED"),
- f("source1_s", "123456789", "", 42, "abcd")));
- assertNotNull(chain, d);
- assertEquals(chain,
- Arrays.asList("123456789", "", 42, "abcd"),
- d.getFieldValues("source1_s"));
- assertEquals(chain,
- Arrays.asList("orig1", "orig2", "123456789", "", 42, "abcd"),
- d.getFieldValues("dest_s"));
- assertEquals(chain + ": dest boost changed",
- 2.3f, d.getField("dest_s").getBoost(), 0.0f);
- }
-
- // should be equivilent for any chain matching source1_s and source2_s
- for (String chain : Arrays.asList("clone-multi",
- "clone-array","clone-selector" )) {
-
- // simple clone
- d = processAdd(chain,
- doc(f("id", "1111"),
- f("source0_s", "NOT COPIED"),
- f("source1_s", "123456789", "", 42, "abcd"),
- f("source2_s", "xxx", 999)));
- assertNotNull(chain, d);
- assertEquals(chain,
- Arrays.asList("123456789", "", 42, "abcd"),
- d.getFieldValues("source1_s"));
- assertEquals(chain,
- Arrays.asList("xxx", 999),
- d.getFieldValues("source2_s"));
- assertEquals(chain,
- Arrays.asList("123456789", "", 42, "abcd", "xxx", 999),
- d.getFieldValues("dest_s"));
-
- // append to existing values, preserve boost
- d = processAdd(chain,
- doc(f("id", "1111"),
- field("dest_s", 2.3f, "orig1", "orig2"),
- f("source0_s", "NOT COPIED"),
- f("source1_s", "123456789", "", 42, "abcd"),
- f("source2_s", "xxx", 999)));
- assertNotNull(chain, d);
- assertEquals(chain,
- Arrays.asList("123456789", "", 42, "abcd"),
- d.getFieldValues("source1_s"));
- assertEquals(chain,
- Arrays.asList("xxx", 999),
- d.getFieldValues("source2_s"));
- assertEquals(chain,
- Arrays.asList("orig1", "orig2",
- "123456789", "", 42, "abcd",
- "xxx", 999),
- d.getFieldValues("dest_s"));
- assertEquals(chain + ": dest boost changed",
- 2.3f, d.getField("dest_s").getBoost(), 0.0f);
- }
- }
-
- public void testCloneFieldExample() throws Exception {
-
- SolrInputDocument d = null;
-
- // test example from the javadocs
- d = processAdd("multiple-clones",
- doc(f("id", "1111"),
- f("category", "misc"),
- f("authors", "Isaac Asimov", "John Brunner"),
- f("editors", "John W. Campbell"),
- f("store1_price", 87),
- f("store2_price", 78),
- f("store3_price", (Object) null),
- f("list_price", 1000)));
- assertNotNull(d);
- assertEquals("misc",d.getFieldValue("category"));
- assertEquals("misc",d.getFieldValue("category_s"));
- assertEquals(Arrays.asList("Isaac Asimov", "John Brunner"),
- d.getFieldValues("authors"));
- assertEquals(Arrays.asList("John W. Campbell"),
- d.getFieldValues("editors"));
- assertEquals(Arrays.asList("Isaac Asimov", "John Brunner",
- "John W. Campbell"),
- d.getFieldValues("contributors"));
- assertEquals(87,d.getFieldValue("store1_price"));
- assertEquals(78,d.getFieldValue("store2_price"));
- assertEquals(1000,d.getFieldValue("list_price"));
- assertEquals(Arrays.asList(87, 78),
- d.getFieldValues("all_prices"));
-
- }
public void testCountValues() throws Exception {
@@ -893,47 +779,6 @@ public class FieldMutatingUpdateProcessorTest extends UpdateProcessorTestBase {
- }
-
- public void testCloneCombinations() throws Exception {
-
- SolrInputDocument d = null;
-
- // maxChars
- d = processAdd("clone-max-chars",
- doc(f("id", "1111"),
- f("field1", "text")));
- assertNotNull(d);
- assertEquals("text",d.getFieldValue("field1"));
- assertEquals("tex",d.getFieldValue("toField"));
-
- // move
- d = processAdd("clone-move",
- doc(f("id", "1111"),
- f("field1", "text")));
- assertNotNull(d);
- assertEquals("text",d.getFieldValue("toField"));
- assertFalse(d.containsKey("field1"));
-
- // replace
- d = processAdd("clone-replace",
- doc(f("id", "1111"),
- f("toField", "IGNORED"),
- f("field1", "text")));
- assertNotNull(d);
- assertEquals("text", d.getFieldValue("field1"));
- assertEquals("text", d.getFieldValue("toField"));
-
- // append
- d = processAdd("clone-append",
- doc(f("id", "1111"),
- f("toField", "aaa"),
- f("field1", "bbb"),
- f("field2", "ccc")));
- assertNotNull(d);
- assertEquals("bbb", d.getFieldValue("field1"));
- assertEquals("ccc", d.getFieldValue("field2"));
- assertEquals("aaa; bbb; ccc", d.getFieldValue("toField"));
}
public void testConcatDefaults() throws Exception {