blob: 711e08fe09e01609c764a8500c306ca50d2928b3 [file] [log] [blame]
/*
* 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.rest.schema;
import java.io.File;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.apache.commons.io.FileUtils;
import org.apache.lucene.misc.SweetSpotSimilarity;
import org.apache.lucene.search.similarities.BM25Similarity;
import org.apache.lucene.search.similarities.DFISimilarity;
import org.apache.lucene.search.similarities.PerFieldSimilarityWrapper;
import org.apache.lucene.search.similarities.Similarity;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.request.schema.SchemaRequest;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.SolrCore;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SimilarityFactory;
import org.apache.solr.search.similarities.SchemaSimilarityFactory;
import org.apache.solr.util.RESTfulServerProvider;
import org.apache.solr.util.RestTestBase;
import org.apache.solr.util.RestTestHarness;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.common.util.Utils.fromJSONString;
public class TestBulkSchemaAPI extends RestTestBase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static File tmpSolrHome;
@Before
public void before() throws Exception {
tmpSolrHome = createTempDir().toFile();
FileUtils.copyDirectory(new File(TEST_HOME()), tmpSolrHome.getAbsoluteFile());
System.setProperty("managed.schema.mutable", "true");
System.setProperty("enable.update.log", "false");
createJettyAndHarness(tmpSolrHome.getAbsolutePath(), "solrconfig-managed-schema.xml", "schema-rest.xml",
"/solr", true, null);
if (random().nextBoolean()) {
log.info("These tests are run with V2 API");
restTestHarness.setServerProvider(new RESTfulServerProvider() {
@Override
public String getBaseURL() {
return jetty.getBaseUrl().toString() + "/____v2/cores/" + DEFAULT_TEST_CORENAME;
}
});
}
}
@After
public void after() throws Exception {
if (jetty != null) {
jetty.stop();
jetty = null;
}
if (restTestHarness != null) {
restTestHarness.close();
}
restTestHarness = null;
}
public void testMultipleAddFieldWithErrors() throws Exception {
String payload = "{\n" +
" 'add-field' : {\n" +
" 'name':'a1',\n" +
" 'type': 'string1',\n" +
" 'stored':true,\n" +
" 'indexed':false\n" +
" },\n" +
" 'add-field' : {\n" +
" 'type': 'string',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" }\n" +
" }";
String response = restTestHarness.post("/schema", json(payload));
@SuppressWarnings({"rawtypes"})
Map map = (Map) fromJSONString(response);
@SuppressWarnings({"rawtypes"})
Map error = (Map)map.get("error");
assertNotNull("No errors", error);
@SuppressWarnings({"rawtypes"})
List details = (List)error.get("details");
assertNotNull("No details", details);
assertEquals("Wrong number of details", 2, details.size());
@SuppressWarnings({"rawtypes"})
List firstErrorList = (List)((Map)details.get(0)).get("errorMessages");
assertEquals(1, firstErrorList.size());
assertTrue (((String)firstErrorList.get(0)).contains("Field 'a1': Field type 'string1' not found.\n"));
@SuppressWarnings({"rawtypes"})
List secondErrorList = (List)((Map)details.get(1)).get("errorMessages");
assertEquals(1, secondErrorList.size());
assertTrue (((String)secondErrorList.get(0)).contains("is a required field"));
}
public void testAnalyzerClass() throws Exception {
String addFieldTypeAnalyzerWithClass = "{\n" +
"'add-field-type' : {" +
" 'name' : 'myNewTextFieldWithAnalyzerClass',\n" +
" 'class':'solr.TextField',\n" +
" 'analyzer' : {\n" +
" 'luceneMatchVersion':'5.0.0',\n" +
" 'class':'org.apache.lucene.analysis.core.WhitespaceAnalyzer'\n";
String charFilters =
" 'charFilters' : [{\n" +
" 'class':'solr.PatternReplaceCharFilterFactory',\n" +
" 'replacement':'$1$1',\n" +
" 'pattern':'([a-zA-Z])\\\\\\\\1+'\n" +
" }],\n";
String tokenizer =
" 'tokenizer' : { 'class':'solr.WhitespaceTokenizerFactory' },\n";
String filters =
" 'filters' : [{ 'class':'solr.ASCIIFoldingFilterFactory' }]\n";
String suffix =
" }\n"+
"}}";
String response = restTestHarness.post("/schema",
json(addFieldTypeAnalyzerWithClass + ',' + charFilters + tokenizer + filters + suffix));
@SuppressWarnings({"rawtypes"})
Map map = (Map) fromJSONString(response);
@SuppressWarnings({"rawtypes"})
Map error = (Map)map.get("error");
assertNotNull("No errors", error);
@SuppressWarnings({"rawtypes"})
List details = (List)error.get("details");
assertNotNull("No details", details);
assertEquals("Wrong number of details", 1, details.size());
@SuppressWarnings({"rawtypes"})
List errorList = (List)((Map)details.get(0)).get("errorMessages");
assertEquals(1, errorList.size());
assertTrue (((String)errorList.get(0)).contains
("An analyzer with a class property may not define any char filters!"));
response = restTestHarness.post("/schema",
json(addFieldTypeAnalyzerWithClass + ',' + tokenizer + filters + suffix));
map = (Map) fromJSONString(response);
error = (Map)map.get("error");
assertNotNull("No errors", error);
details = (List)error.get("details");
assertNotNull("No details", details);
assertEquals("Wrong number of details", 1, details.size());
errorList = (List)((Map)details.get(0)).get("errorMessages");
assertEquals(1, errorList.size());
assertTrue (((String)errorList.get(0)).contains
("An analyzer with a class property may not define a tokenizer!"));
response = restTestHarness.post("/schema",
json(addFieldTypeAnalyzerWithClass + ',' + filters + suffix));
map = (Map) fromJSONString(response);
error = (Map)map.get("error");
assertNotNull("No errors", error);
details = (List)error.get("details");
assertNotNull("No details", details);
assertEquals("Wrong number of details", 1, details.size());
errorList = (List)((Map)details.get(0)).get("errorMessages");
assertEquals(1, errorList.size());
assertTrue (((String)errorList.get(0)).contains
("An analyzer with a class property may not define any filters!"));
response = restTestHarness.post("/schema", json(addFieldTypeAnalyzerWithClass + suffix));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
map = getObj(restTestHarness, "myNewTextFieldWithAnalyzerClass", "fieldTypes");
assertNotNull(map);
@SuppressWarnings({"rawtypes"})
Map analyzer = (Map)map.get("analyzer");
assertEquals("org.apache.lucene.analysis.core.WhitespaceAnalyzer", String.valueOf(analyzer.get("class")));
assertEquals("5.0.0", String.valueOf(analyzer.get(IndexSchema.LUCENE_MATCH_VERSION_PARAM)));
}
public void testAddFieldMatchingExistingDynamicField() throws Exception {
RestTestHarness harness = restTestHarness;
String newFieldName = "attr_non_dynamic";
@SuppressWarnings({"rawtypes"})
Map map = getObj(harness, newFieldName, "fields");
assertNull("Field '" + newFieldName + "' already exists in the schema", map);
map = getObj(harness, "attr_*", "dynamicFields");
assertNotNull("'attr_*' dynamic field does not exist in the schema", map);
map = getObj(harness, "boolean", "fieldTypes");
assertNotNull("'boolean' field type does not exist in the schema", map);
String payload = "{\n" +
" 'add-field' : {\n" +
" 'name':'" + newFieldName + "',\n" +
" 'type':'boolean',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" }\n" +
" }";
String response = harness.post("/schema", json(payload));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
map = getObj(harness, newFieldName, "fields");
assertNotNull("Field '" + newFieldName + "' is not in the schema", map);
}
public void testAddIllegalDynamicField() throws Exception {
RestTestHarness harness = restTestHarness;
String newFieldName = "illegal";
String payload = "{\n" +
" 'add-dynamic-field' : {\n" +
" 'name':'" + newFieldName + "',\n" +
" 'type':'string',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" }\n" +
" }";
String response = harness.post("/schema", json(payload));
@SuppressWarnings({"rawtypes"})
Map map = (Map) fromJSONString(response);
assertNotNull(response, map.get("error"));
map = getObj(harness, newFieldName, "dynamicFields");
assertNull(newFieldName + " illegal dynamic field should not have been added to schema", map);
}
@SuppressWarnings({"rawtypes"})
public void testAddIllegalFields() throws Exception {
RestTestHarness harness = restTestHarness;
// 1. Make sure you can't create a new field with an asterisk in its name
String newFieldName = "asterisk*";
String payload = "{\n" +
" 'add-field' : {\n" +
" 'name':'" + newFieldName + "',\n" +
" 'type':'string',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" }\n" +
"}";
String response = harness.post("/schema", json(payload));
Map map = (Map) fromJSONString(response);
assertNotNull(response, map.get("error"));
map = getObj(harness, newFieldName, "fields");
assertNull(newFieldName + " illegal dynamic field should not have been added to schema", map);
// 2. Make sure you get an error when you try to create a field that already exists
// Make sure 'wdf_nocase' field exists
newFieldName = "wdf_nocase";
Map m = getObj(harness, newFieldName, "fields");
assertNotNull("'" + newFieldName + "' field does not exist in the schema", m);
payload = "{\n" +
" 'add-field' : {\n" +
" 'name':'" + newFieldName + "',\n" +
" 'type':'string',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" }\n" +
"}";
response = harness.post("/schema", json(payload));
map = (Map) fromJSONString(response);
assertNotNull(response, map.get("error"));
}
@SuppressWarnings({"rawtypes"})
public void testAddFieldWithExistingCatchallDynamicField() throws Exception {
RestTestHarness harness = restTestHarness;
String newFieldName = "NewField1";
Map map = getObj(harness, newFieldName, "fields");
assertNull("Field '" + newFieldName + "' already exists in the schema", map);
map = getObj(harness, "*", "dynamicFields");
assertNull("'*' dynamic field already exists in the schema", map);
map = getObj(harness, "string", "fieldTypes");
assertNotNull("'boolean' field type does not exist in the schema", map);
map = getObj(harness, "boolean", "fieldTypes");
assertNotNull("'boolean' field type does not exist in the schema", map);
String payload = "{\n" +
" 'add-dynamic-field' : {\n" +
" 'name':'*',\n" +
" 'type':'string',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" }\n" +
"}";
String response = harness.post("/schema", json(payload));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
map = getObj(harness, "*", "dynamicFields");
assertNotNull("Dynamic field '*' is not in the schema", map);
payload = "{\n" +
" 'add-field' : {\n" +
" 'name':'" + newFieldName + "',\n" +
" 'type':'boolean',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" }\n" +
" }";
response = harness.post("/schema", json(payload));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
map = getObj(harness, newFieldName, "fields");
assertNotNull("Field '" + newFieldName + "' is not in the schema", map);
}
@SuppressWarnings({"unchecked", "rawtypes"})
public void testMultipleCommands() throws Exception{
RestTestHarness harness = restTestHarness;
Map m = getObj(harness, "wdf_nocase", "fields");
assertNotNull("'wdf_nocase' field does not exist in the schema", m);
m = getObj(harness, "wdf_nocase", "fieldTypes");
assertNotNull("'wdf_nocase' field type does not exist in the schema", m);
m = getObj(harness, "boolean", "fieldTypes");
assertNotNull("'boolean' field type does not exist in the schema", m);
assertNull(m.get("sortMissingFirst"));
assertTrue((Boolean)m.get("sortMissingLast"));
m = getObj(harness, "name", "fields");
assertNotNull("'name' field does not exist in the schema", m);
assertEquals("nametext", m.get("type"));
m = getObj(harness, "bind", "fields");
assertNotNull("'bind' field does not exist in the schema", m);
assertEquals("boolean", m.get("type"));
m = getObj(harness, "attr_*", "dynamicFields");
assertNotNull("'attr_*' dynamic field does not exist in the schema", m);
assertEquals("text", m.get("type"));
List l = getSourceCopyFields(harness, "*_i");
Set s = new HashSet();
assertEquals(4, l.size());
s.add(((Map)l.get(0)).get("dest"));
s.add(((Map)l.get(1)).get("dest"));
s.add(((Map) l.get(2)).get("dest"));
s.add(((Map) l.get(3)).get("dest"));
assertTrue(s.contains("title"));
assertTrue(s.contains("*_s"));
String payload = "{\n" +
" 'add-field' : {\n" +
" 'name':'a1',\n" +
" 'type': 'string',\n" +
" 'stored':true,\n" +
" 'indexed':false\n" +
" },\n" +
" 'add-field' : {\n" +
" 'name':'a2',\n" +
" 'type': 'string',\n" +
" 'stored':true,\n" +
" 'indexed':true,\n" +
" 'uninvertible':true,\n" +
" },\n" +
" 'add-dynamic-field' : {\n" +
" 'name' :'*_lol',\n" +
" 'type':'string',\n" +
" 'stored':true,\n" +
" 'indexed':true,\n" +
" 'uninvertible':false,\n" +
" },\n" +
" 'add-copy-field' : {\n" +
" 'source' :'a1',\n" +
" 'dest':['a2','hello_lol']\n" +
" },\n" +
" 'add-field-type' : {\n" +
" 'name' :'mystr',\n" +
" 'class' : 'solr.StrField',\n" +
" 'sortMissingLast':'true'\n" +
" },\n" +
" 'add-field-type' : {" +
" 'name' : 'myNewTxtField',\n" +
" 'class':'solr.TextField',\n" +
" 'positionIncrementGap':'100',\n" +
" 'indexAnalyzer' : {\n" +
" 'charFilters':[\n" +
" {\n" +
" 'class':'solr.PatternReplaceCharFilterFactory',\n" +
" 'replacement':'$1$1',\n" +
" 'pattern':'([a-zA-Z])\\\\\\\\1+'\n" +
" }\n" +
" ],\n" +
" 'tokenizer':{'class':'solr.WhitespaceTokenizerFactory'},\n" +
" 'filters':[\n" +
" {\n" +
" 'class':'solr.WordDelimiterGraphFilterFactory',\n" +
" 'preserveOriginal':'0'\n" +
" },\n" +
" {\n" +
" 'class':'solr.StopFilterFactory',\n" +
" 'words':'stopwords.txt',\n" +
" 'ignoreCase':'true'\n" +
" },\n" +
" {'class':'solr.LowerCaseFilterFactory'},\n" +
" {'class':'solr.ASCIIFoldingFilterFactory'},\n" +
" {'class':'solr.KStemFilterFactory'},\n" +
" {'class':'solr.FlattenGraphFilterFactory'}\n" +
" ]\n" +
" },\n" +
" 'queryAnalyzer' : {\n" +
" 'charFilters':[\n" +
" {\n" +
" 'class':'solr.PatternReplaceCharFilterFactory',\n" +
" 'replacement':'$1$1',\n" +
" 'pattern':'([a-zA-Z])\\\\\\\\1+'\n" +
" }\n" +
" ],\n" +
" 'tokenizer':{'class':'solr.WhitespaceTokenizerFactory'},\n" +
" 'filters':[\n" +
" {\n" +
" 'class':'solr.WordDelimiterGraphFilterFactory',\n" +
" 'preserveOriginal':'0'\n" +
" },\n" +
" {\n" +
" 'class':'solr.StopFilterFactory',\n" +
" 'words':'stopwords.txt',\n" +
" 'ignoreCase':'true'\n" +
" },\n" +
" {'class':'solr.LowerCaseFilterFactory'},\n" +
" {'class':'solr.ASCIIFoldingFilterFactory'},\n" +
" {'class':'solr.KStemFilterFactory'}\n" +
" ]\n" +
" }\n" +
" },\n"+
" 'add-field' : {\n" +
" 'name':'a3',\n" +
" 'type': 'myNewTxtField',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" },\n" +
" 'add-field-type' : {" +
" 'name' : 'myWhitespaceTxtField',\n" +
" 'class':'solr.TextField',\n" +
" 'uninvertible':false,\n" +
" 'analyzer' : {'class' : 'org.apache.lucene.analysis.core.WhitespaceAnalyzer'}\n" +
" },\n"+
" 'add-field' : {\n" +
" 'name':'a5',\n" +
" 'type': 'myWhitespaceTxtField',\n" +
" 'stored':true\n" +
" },\n" +
" 'add-field-type' : {" +
" 'name' : 'mySimField',\n" +
" 'class':'solr.TextField',\n" +
" 'analyzer' : {'tokenizer':{'class':'solr.WhitespaceTokenizerFactory'}},\n" +
" 'similarity' : {'class':'org.apache.lucene.misc.SweetSpotSimilarity'}\n" +
" },\n"+
" 'add-field' : {\n" +
" 'name':'a4',\n" +
" 'type': 'mySimField',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" },\n" +
" 'delete-field' : {'name':'wdf_nocase'},\n" +
" 'delete-field-type' : {'name':'wdf_nocase'},\n" +
" 'delete-dynamic-field' : {'name':'*_tt'},\n" +
" 'delete-copy-field' : {'source':'a1', 'dest':'a2'},\n" +
" 'delete-copy-field' : {'source':'*_i', 'dest':['title', '*_s']},\n" +
" 'replace-field-type' : {\n" +
" 'name':'boolean',\n" +
" 'class':'solr.BoolField',\n" +
" 'sortMissingFirst':true\n" +
" },\n" +
" 'replace-field' : {\n" +
" 'name':'name',\n" +
" 'type':'string',\n" +
" 'indexed':true,\n" +
" 'stored':true\n" +
" },\n" +
" 'replace-dynamic-field' : {\n" +
" 'name':'attr_*',\n" +
" 'type':'string',\n" +
" 'indexed':true,\n" +
" 'stored':true,\n" +
" 'multiValued':true\n" +
" }\n" +
" }\n";
String response = harness.post("/schema", json(payload));
Map map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
m = getObj(harness, "a1", "fields");
assertNotNull("field a1 not created", m);
assertEquals("string", m.get("type"));
assertEquals(Boolean.TRUE, m.get("stored"));
assertEquals(Boolean.FALSE, m.get("indexed"));
m = getObj(harness,"a2", "fields");
assertNotNull("field a2 not created", m);
assertEquals("string", m.get("type"));
assertEquals(Boolean.TRUE, m.get("stored"));
assertEquals(Boolean.TRUE, m.get("indexed"));
assertEquals(Boolean.TRUE, m.get("uninvertible"));
m = getObj(harness,"*_lol", "dynamicFields");
assertNotNull("field *_lol not created", m);
assertEquals("string", m.get("type"));
assertEquals(Boolean.TRUE, m.get("stored"));
assertEquals(Boolean.TRUE, m.get("indexed"));
assertEquals(Boolean.FALSE, m.get("uninvertible"));
l = getSourceCopyFields(harness, "a1");
s = new HashSet();
assertEquals(1, l.size());
s.add(((Map) l.get(0)).get("dest"));
assertTrue(s.contains("hello_lol"));
l = getSourceCopyFields(harness, "*_i");
s = new HashSet();
assertEquals(2, l.size());
s.add(((Map)l.get(0)).get("dest"));
s.add(((Map) l.get(1)).get("dest"));
assertFalse(s.contains("title"));
assertFalse(s.contains("*_s"));
m = getObj(harness, "mystr", "fieldTypes");
assertNotNull(m);
assertEquals("solr.StrField", m.get("class"));
assertEquals("true", String.valueOf(m.get("sortMissingLast")));
m = getObj(harness, "myNewTxtField", "fieldTypes");
assertNotNull(m);
m = getObj(harness, "a3", "fields");
assertNotNull("field a3 not created", m);
assertEquals("myNewTxtField", m.get("type"));
m = getObj(harness, "mySimField", "fieldTypes");
assertNotNull(m);
m = (Map)m.get("similarity");
assertNotNull(m);
assertEquals(SweetSpotSimilarity.class.getName(), m.get("class"));
m = getObj(harness, "a4", "fields");
assertNotNull("field a4 not created", m);
assertEquals("mySimField", m.get("type"));
assertFieldSimilarity("a4", SweetSpotSimilarity.class);
m = getObj(harness, "myWhitespaceTxtField", "fieldTypes");
assertNotNull(m);
assertEquals(Boolean.FALSE, m.get("uninvertible"));
assertNull(m.get("similarity")); // unspecified, expect default
m = getObj(harness, "a5", "fields");
assertNotNull("field a5 not created", m);
assertEquals("myWhitespaceTxtField", m.get("type"));
assertNull(m.get("uninvertible")); // inherited, but API shouldn't return w/o explicit showDefaults
assertFieldSimilarity("a5", BM25Similarity.class); // unspecified, expect default
m = getObj(harness, "wdf_nocase", "fields");
assertNull("field 'wdf_nocase' not deleted", m);
m = getObj(harness, "wdf_nocase", "fieldTypes");
assertNull("field type 'wdf_nocase' not deleted", m);
m = getObj(harness, "*_tt", "dynamicFields");
assertNull("dynamic field '*_tt' not deleted", m);
m = getObj(harness, "boolean", "fieldTypes");
assertNotNull("'boolean' field type does not exist in the schema", m);
assertNull(m.get("sortMissingLast"));
assertTrue((Boolean)m.get("sortMissingFirst"));
m = getObj(harness, "bind", "fields"); // this field will be rebuilt when "boolean" field type is replaced
assertNotNull("'bind' field does not exist in the schema", m);
m = getObj(harness, "name", "fields");
assertNotNull("'name' field does not exist in the schema", m);
assertEquals("string", m.get("type"));
m = getObj(harness, "attr_*", "dynamicFields");
assertNotNull("'attr_*' dynamic field does not exist in the schema", m);
assertEquals("string", m.get("type"));
}
public void testCopyFieldRules() throws Exception {
RestTestHarness harness = restTestHarness;
@SuppressWarnings({"rawtypes"})
Map m = getObj(harness, "name", "fields");
assertNotNull("'name' field does not exist in the schema", m);
m = getObj(harness, "bind", "fields");
assertNotNull("'bind' field does not exist in the schema", m);
@SuppressWarnings({"rawtypes"})
List l = getSourceCopyFields(harness, "bleh_s");
assertTrue("'bleh_s' copyField rule exists in the schema", l.isEmpty());
String payload = "{\n" +
" 'add-copy-field' : {\n" +
" 'source' :'bleh_s',\n" +
" 'dest':'name'\n" +
" }\n" +
" }\n";
String response = harness.post("/schema", json(payload));
@SuppressWarnings({"rawtypes"})
Map map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
l = getSourceCopyFields(harness, "bleh_s");
assertFalse("'bleh_s' copyField rule doesn't exist", l.isEmpty());
assertEquals("bleh_s", ((Map)l.get(0)).get("source"));
assertEquals("name", ((Map)l.get(0)).get("dest"));
// delete copy field rule
payload = "{\n" +
" 'delete-copy-field' : {\n" +
" 'source' :'bleh_s',\n" +
" 'dest':'name'\n" +
" }\n" +
" }\n";
response = harness.post("/schema", json(payload));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
l = getSourceCopyFields(harness, "bleh_s");
assertTrue("'bleh_s' copyField rule exists in the schema", l.isEmpty());
// copy and delete with multiple destination
payload = "{\n" +
" 'add-copy-field' : {\n" +
" 'source' :'bleh_s',\n" +
" 'dest':['name','bind']\n" +
" }\n" +
" }\n";
response = harness.post("/schema", json(payload));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
l = getSourceCopyFields(harness, "bleh_s");
assertEquals(2, l.size());
payload = "{\n" +
" 'delete-copy-field' : {\n" +
" 'source' :'bleh_s',\n" +
" 'dest':['name','bind']\n" +
" }\n" +
" }\n";
response = harness.post("/schema", json(payload));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
l = getSourceCopyFields(harness, "bleh_s");
assertTrue("'bleh_s' copyField rule exists in the schema", l.isEmpty());
}
@SuppressWarnings({"rawtypes"})
public void testCopyFieldWithReplace() throws Exception {
RestTestHarness harness = restTestHarness;
String newFieldName = "test_solr_14950";
// add-field-type
String addFieldTypeAnalyzer = "{\n" +
"'add-field-type' : {" +
" 'name' : 'myNewTextField',\n" +
" 'class':'solr.TextField',\n" +
" 'analyzer' : {\n" +
" 'charFilters' : [{\n" +
" 'class':'solr.PatternReplaceCharFilterFactory',\n" +
" 'replacement':'$1$1',\n" +
" 'pattern':'([a-zA-Z])\\\\\\\\1+'\n" +
" }],\n" +
" 'tokenizer' : { 'class':'solr.WhitespaceTokenizerFactory' },\n" +
" 'filters' : [{ 'class':'solr.ASCIIFoldingFilterFactory' }]\n" +
" }\n"+
"}}";
String response = restTestHarness.post("/schema", json(addFieldTypeAnalyzer));
Map map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
map = getObj(harness, "myNewTextField", "fieldTypes");
assertNotNull("'myNewTextField' field type does not exist in the schema", map);
// add-field
String payload = "{\n" +
" 'add-field' : {\n" +
" 'name':'" + newFieldName + "',\n" +
" 'type':'myNewTextField',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" }\n" +
" }";
response = harness.post("/schema", json(payload));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
Map m = getObj(harness, newFieldName, "fields");
assertNotNull("'"+ newFieldName + "' field does not exist in the schema", m);
// add copy-field with explicit source and destination
List l = getSourceCopyFields(harness, "bleh_s");
assertTrue("'bleh_s' copyField rule exists in the schema", l.isEmpty());
payload = "{\n" +
" 'add-copy-field' : {\n" +
" 'source' :'bleh_s',\n" +
" 'dest':'"+ newFieldName + "'\n" +
" }\n" +
" }\n";
response = harness.post("/schema", json(payload));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
l = getSourceCopyFields(harness, "bleh_s");
assertFalse("'bleh_s' copyField rule doesn't exist", l.isEmpty());
assertEquals("bleh_s", ((Map)l.get(0)).get("source"));
assertEquals(newFieldName, ((Map)l.get(0)).get("dest"));
// replace-field-type
String replaceFieldTypeAnalyzer = "{\n" +
"'replace-field-type' : {" +
" 'name' : 'myNewTextField',\n" +
" 'class':'solr.TextField',\n" +
" 'analyzer' : {\n" +
" 'tokenizer' : { 'class':'solr.WhitespaceTokenizerFactory' },\n" +
" 'filters' : [{ 'class':'solr.ASCIIFoldingFilterFactory' }]\n" +
" }\n"+
"}}";
response = restTestHarness.post("/schema", json(replaceFieldTypeAnalyzer));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
map = getObj(restTestHarness, "myNewTextField", "fieldTypes");
assertNotNull(map);
Map analyzer = (Map)map.get("analyzer");
assertNull("'myNewTextField' shouldn't contain charFilters", analyzer.get("charFilters"));
l = getSourceCopyFields(harness, "bleh_s");
assertFalse("'bleh_s' copyField rule doesn't exist", l.isEmpty());
assertEquals("bleh_s", ((Map)l.get(0)).get("source"));
assertEquals(newFieldName, ((Map)l.get(0)).get("dest"));
// with replace-field
String replaceField = "{'replace-field' : {'name':'" + newFieldName + "', 'type':'string'}}";
response = harness.post("/schema", json(replaceField));
map = (Map) fromJSONString(response);
assertNull(map.get("error"));
l = getSourceCopyFields(harness, "bleh_s");
assertFalse("'bleh_s' copyField rule doesn't exist", l.isEmpty());
assertEquals("bleh_s", ((Map)l.get(0)).get("source"));
assertEquals(newFieldName, ((Map)l.get(0)).get("dest"));
}
@SuppressWarnings({"unchecked", "rawtypes"})
public void testDeleteAndReplace() throws Exception {
RestTestHarness harness = restTestHarness;
Map map = getObj(harness, "NewField1", "fields");
assertNull("Field 'NewField1' already exists in the schema", map);
map = getObj(harness, "NewField2", "fields");
assertNull("Field 'NewField2' already exists in the schema", map);
map = getObj(harness, "NewFieldType", "fieldTypes");
assertNull("'NewFieldType' field type already exists in the schema", map);
List list = getSourceCopyFields(harness, "NewField1");
assertEquals("There is already a copy field with source 'NewField1' in the schema", 0, list.size());
map = getObj(harness, "NewDynamicField1*", "dynamicFields");
assertNull("Dynamic field 'NewDynamicField1*' already exists in the schema", map);
map = getObj(harness, "NewDynamicField2*", "dynamicFields");
assertNull("Dynamic field 'NewDynamicField2*' already exists in the schema", map);
String cmds = "{\n" +
" 'add-field-type': { 'name':'NewFieldType', 'class':'solr.StrField' },\n" +
" 'add-field': [{ 'name':'NewField1', 'type':'NewFieldType' },\n" +
" { 'name':'NewField2', 'type':'NewFieldType' },\n" +
" { 'name':'NewField3', 'type':'NewFieldType' },\n" +
" { 'name':'NewField4', 'type':'NewFieldType' }],\n" +
" 'add-dynamic-field': [{ 'name':'NewDynamicField1*', 'type':'NewFieldType' },\n" +
" { 'name':'NewDynamicField2*', 'type':'NewFieldType' },\n" +
" { 'name':'NewDynamicField3*', 'type':'NewFieldType' }],\n" +
" 'add-copy-field': [{'source':'NewField1', 'dest':['NewField2', 'NewDynamicField1A']},\n" +
" {'source':'NewDynamicField1*', 'dest':'NewField2' },\n" +
" {'source':'NewDynamicField2*', 'dest':'NewField2' },\n" +
" {'source':'NewDynamicField3*', 'dest':'NewField3' },\n" +
" {'source':'NewField4', 'dest':'NewField3' },\n" +
" {'source':'NewField4', 'dest':'NewField2', maxChars: 100 },\n" +
" {'source':'NewField4', 'dest':['NewField1'], maxChars: 3333 }]\n" +
"}\n";
String response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
map = getObj(harness, "NewFieldType", "fieldTypes");
assertNotNull("'NewFieldType' is not in the schema", map);
map = getObj(harness, "NewField1", "fields");
assertNotNull("Field 'NewField1' is not in the schema", map);
map = getObj(harness, "NewField2", "fields");
assertNotNull("Field 'NewField2' is not in the schema", map);
map = getObj(harness, "NewField3", "fields");
assertNotNull("Field 'NewField3' is not in the schema", map);
map = getObj(harness, "NewField4", "fields");
assertNotNull("Field 'NewField4' is not in the schema", map);
list = getSourceCopyFields(harness, "NewField1");
Set set = new HashSet();
for (Object obj : list) {
set.add(((Map)obj).get("dest"));
}
assertEquals(2, list.size());
assertTrue(set.contains("NewField2"));
assertTrue(set.contains("NewDynamicField1A"));
list = getSourceCopyFields(harness, "NewDynamicField1*");
assertEquals(1, list.size());
assertEquals("NewField2", ((Map)list.get(0)).get("dest"));
list = getSourceCopyFields(harness, "NewDynamicField2*");
assertEquals(1, list.size());
assertEquals("NewField2", ((Map)list.get(0)).get("dest"));
list = getSourceCopyFields(harness, "NewDynamicField3*");
assertEquals(1, list.size());
assertEquals("NewField3", ((Map)list.get(0)).get("dest"));
list = getSourceCopyFields(harness, "NewField4");
assertEquals(3, list.size());
map.clear();
for (Object obj : list) {
map.put(((Map)obj).get("dest"), ((Map)obj).get("maxChars"));
}
assertTrue(map.containsKey("NewField1"));
assertEquals(3333L, map.get("NewField1"));
assertTrue(map.containsKey("NewField2"));
assertEquals(100L, map.get("NewField2"));
assertTrue(map.containsKey("NewField3"));
assertNull(map.get("NewField3"));
cmds = "{'delete-field-type' : {'name':'NewFieldType'}}";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
Object errors = map.get("error");
assertNotNull(errors);
assertTrue(errors.toString().contains("Can't delete 'NewFieldType' because it's the field type of "));
cmds = "{'delete-field' : {'name':'NewField1'}}";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
errors = map.get("error");
assertNotNull(errors);
assertTrue(errors.toString().contains
("Can't delete field 'NewField1' because it's referred to by at least one copy field directive"));
cmds = "{'delete-field' : {'name':'NewField2'}}";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
errors = map.get("error");
assertNotNull(errors);
assertTrue(errors.toString().contains
("Can't delete field 'NewField2' because it's referred to by at least one copy field directive"));
cmds = "{'replace-field' : {'name':'NewField1', 'type':'string'}}";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
assertNull(map.get("error"));
// Make sure the copy field directives with source NewField1 are preserved
list = getSourceCopyFields(harness, "NewField1");
set = new HashSet();
for (Object obj : list) {
set.add(((Map)obj).get("dest"));
}
assertEquals(2, list.size());
assertTrue(set.contains("NewField2"));
assertTrue(set.contains("NewDynamicField1A"));
cmds = "{'delete-dynamic-field' : {'name':'NewDynamicField1*'}}";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
errors = map.get("error");
assertNotNull(errors);
assertTrue(errors.toString().contains
("copyField dest :'NewDynamicField1A' is not an explicit field and doesn't match a dynamicField."));
cmds = "{'replace-field' : {'name':'NewField2', 'type':'string'}}";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
errors = map.get("error");
assertNull(errors);
// Make sure the copy field directives with destination NewField2 are preserved
list = getDestCopyFields(harness, "NewField2");
set = new HashSet();
for (Object obj : list) {
set.add(((Map)obj).get("source"));
}
assertEquals(4, list.size());
assertTrue(set.contains("NewField1"));
assertTrue(set.contains("NewField4"));
assertTrue(set.contains("NewDynamicField1*"));
assertTrue(set.contains("NewDynamicField2*"));
cmds = "{'replace-dynamic-field' : {'name':'NewDynamicField2*', 'type':'string'}}";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
errors = map.get("error");
assertNull(errors);
// Make sure the copy field directives with source NewDynamicField2* are preserved
list = getSourceCopyFields(harness, "NewDynamicField2*");
assertEquals(1, list.size());
assertEquals("NewField2", ((Map) list.get(0)).get("dest"));
cmds = "{'replace-dynamic-field' : {'name':'NewDynamicField1*', 'type':'string'}}";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
errors = map.get("error");
assertNull(errors);
// Make sure the copy field directives with destinations matching NewDynamicField1* are preserved
list = getDestCopyFields(harness, "NewDynamicField1A");
assertEquals(1, list.size());
assertEquals("NewField1", ((Map) list.get(0)).get("source"));
cmds = "{'replace-field-type': {'name':'NewFieldType', 'class':'solr.BinaryField'}}";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
assertNull(map.get("error"));
// Make sure the copy field directives with sources and destinations of type NewFieldType are preserved
list = getDestCopyFields(harness, "NewField3");
assertEquals(2, list.size());
set = new HashSet();
for (Object obj : list) {
set.add(((Map)obj).get("source"));
}
assertTrue(set.contains("NewField4"));
assertTrue(set.contains("NewDynamicField3*"));
cmds = "{\n" +
" 'delete-copy-field': [{'source':'NewField1', 'dest':['NewField2', 'NewDynamicField1A'] },\n" +
" {'source':'NewDynamicField1*', 'dest':'NewField2' },\n" +
" {'source':'NewDynamicField2*', 'dest':'NewField2' },\n" +
" {'source':'NewDynamicField3*', 'dest':'NewField3' },\n" +
" {'source':'NewField4', 'dest':['NewField1', 'NewField2', 'NewField3']}]\n" +
"}\n";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
assertNull(map.get("error"));
list = getSourceCopyFields(harness, "NewField1");
assertEquals(0, list.size());
list = getSourceCopyFields(harness, "NewDynamicField1*");
assertEquals(0, list.size());
list = getSourceCopyFields(harness, "NewDynamicField2*");
assertEquals(0, list.size());
list = getSourceCopyFields(harness, "NewDynamicField3*");
assertEquals(0, list.size());
list = getSourceCopyFields(harness, "NewField4");
assertEquals(0, list.size());
cmds = "{'delete-field': [{'name':'NewField1'},{'name':'NewField2'},{'name':'NewField3'},{'name':'NewField4'}]}";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
assertNull(map.get("error"));
cmds = "{'delete-dynamic-field': [{'name':'NewDynamicField1*'}," +
" {'name':'NewDynamicField2*'},\n" +
" {'name':'NewDynamicField3*'}]\n" +
"}\n";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
assertNull(map.get("error"));
cmds = "{'delete-field-type':{'name':'NewFieldType'}}";
response = harness.post("/schema", json(cmds));
map = (Map) fromJSONString(response);
assertNull(map.get("error"));
}
public void testSortableTextFieldWithAnalyzer() throws Exception {
String fieldTypeName = "sort_text_type";
String fieldName = "sort_text";
String payload = "{\n" +
" 'add-field-type' : {" +
" 'name' : '" + fieldTypeName + "',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" 'maxCharsForDocValues':6\n" +
" 'class':'solr.SortableTextField',\n" +
" 'analyzer' : {'tokenizer':{'class':'solr.WhitespaceTokenizerFactory'}},\n" +
" },\n"+
" 'add-field' : {\n" +
" 'name':'" + fieldName + "',\n" +
" 'type': '"+fieldTypeName+"',\n" +
" }\n" +
"}\n";
String response = restTestHarness.post("/schema", json(payload));
@SuppressWarnings({"rawtypes"})
Map map = (Map) fromJSONString(response);
assertNull(response, map.get("errors"));
@SuppressWarnings({"rawtypes"})
Map fields = getObj(restTestHarness, fieldName, "fields");
assertNotNull("field " + fieldName + " not created", fields);
assertEquals(0,
getSolrClient().add(Arrays.asList(sdoc("id","1",fieldName,"xxx aaa"),
sdoc("id","2",fieldName,"xxx bbb aaa"),
sdoc("id","3",fieldName,"xxx bbb zzz"))).getStatus());
assertEquals(0, getSolrClient().commit().getStatus());
{
SolrDocumentList docs = getSolrClient().query
(params("q",fieldName+":xxx","sort", fieldName + " asc, id desc")).getResults();
assertEquals(3L, docs.getNumFound());
assertEquals(3L, docs.size());
assertEquals("1", docs.get(0).getFieldValue("id"));
assertEquals("3", docs.get(1).getFieldValue("id"));
assertEquals("2", docs.get(2).getFieldValue("id"));
}
{
SolrDocumentList docs = getSolrClient().query
(params("q",fieldName+":xxx", "sort", fieldName + " desc, id asc")).getResults();
assertEquals(3L, docs.getNumFound());
assertEquals(3L, docs.size());
assertEquals("2", docs.get(0).getFieldValue("id"));
assertEquals("3", docs.get(1).getFieldValue("id"));
assertEquals("1", docs.get(2).getFieldValue("id"));
}
}
@Test
public void testAddNewFieldAndQuery() throws Exception {
getSolrClient().add(Arrays.asList(
sdoc("id", "1", "term_s", "tux")));
getSolrClient().commit(true, true);
Map<String,Object> attrs = new HashMap<>();
attrs.put("name", "newstringtestfield");
attrs.put("type", "string");
new SchemaRequest.AddField(attrs).process(getSolrClient());
SolrQuery query = new SolrQuery("*:*");
query.addFacetField("newstringtestfield");
int size = getSolrClient().query(query).getResults().size();
assertEquals(1, size);
}
public void testSimilarityParser() throws Exception {
RestTestHarness harness = restTestHarness;
final float k1 = 2.25f;
final float b = 0.33f;
String fieldTypeName = "MySimilarityField";
String fieldName = "similarityTestField";
String payload = "{\n" +
" 'add-field-type' : {" +
" 'name' : '" + fieldTypeName + "',\n" +
" 'class':'solr.TextField',\n" +
" 'analyzer' : {'tokenizer':{'class':'solr.WhitespaceTokenizerFactory'}},\n" +
" 'similarity' : {'class':'org.apache.solr.search.similarities.BM25SimilarityFactory', 'k1':"+k1+", 'b':"+b+" }\n" +
" },\n"+
" 'add-field' : {\n" +
" 'name':'" + fieldName + "',\n" +
" 'type': 'MySimilarityField',\n" +
" 'stored':true,\n" +
" 'indexed':true\n" +
" }\n" +
"}\n";
String response = harness.post("/schema", json(payload));
@SuppressWarnings({"rawtypes"})
Map map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
@SuppressWarnings({"rawtypes"})
Map fields = getObj(harness, fieldName, "fields");
assertNotNull("field " + fieldName + " not created", fields);
assertFieldSimilarity(fieldName, BM25Similarity.class,
sim -> assertEquals("Unexpected k1", k1, sim.getK1(), .001),
sim -> assertEquals("Unexpected b", b, sim.getB(), .001));
final String independenceMeasure = "Saturated";
final boolean discountOverlaps = false;
payload = "{\n" +
" 'replace-field-type' : {" +
" 'name' : '" + fieldTypeName + "',\n" +
" 'class':'solr.TextField',\n" +
" 'analyzer' : {'tokenizer':{'class':'solr.WhitespaceTokenizerFactory'}},\n" +
" 'similarity' : {\n" +
" 'class':'org.apache.solr.search.similarities.DFISimilarityFactory',\n" +
" 'independenceMeasure':'" + independenceMeasure + "',\n" +
" 'discountOverlaps':" + discountOverlaps + "\n" +
" }\n" +
" }\n"+
"}\n";
response = harness.post("/schema", json(payload));
map = (Map) fromJSONString(response);
assertNull(response, map.get("error"));
fields = getObj(harness, fieldName, "fields");
assertNotNull("field " + fieldName + " not created", fields);
assertFieldSimilarity(fieldName, DFISimilarity.class,
sim -> assertEquals("Unexpected independenceMeasure", independenceMeasure, sim.getIndependence().toString()),
sim -> assertEquals("Unexpected discountedOverlaps", discountOverlaps, sim.getDiscountOverlaps()));
}
@SuppressWarnings({"rawtypes"})
public static Map getObj(RestTestHarness restHarness, String fld, String key) throws Exception {
Map map = getRespMap(restHarness);
List l = (List) ((Map)map.get("schema")).get(key);
for (Object o : l) {
@SuppressWarnings({"rawtypes"})Map m = (Map) o;
if (fld.equals(m.get("name")))
return m;
}
return null;
}
@SuppressWarnings({"rawtypes"})
public static Map getRespMap(RestTestHarness restHarness) throws Exception {
return getAsMap("/schema", restHarness);
}
@SuppressWarnings({"rawtypes"})
public static Map getAsMap(String uri, RestTestHarness restHarness) throws Exception {
String response = restHarness.query(uri);
return (Map) fromJSONString(response);
}
@SuppressWarnings({"unchecked", "rawtypes"})
public static List getSourceCopyFields(RestTestHarness harness, String src) throws Exception {
Map map = getRespMap(harness);
List l = (List) ((Map)map.get("schema")).get("copyFields");
List result = new ArrayList();
for (Object o : l) {
Map m = (Map) o;
if (src.equals(m.get("source"))) result.add(m);
}
return result;
}
@SuppressWarnings({"unchecked", "rawtypes"})
public static List getDestCopyFields(RestTestHarness harness, String dest) throws Exception {
Map map = getRespMap(harness);
List l = (List) ((Map)map.get("schema")).get("copyFields");
List result = new ArrayList();
for (Object o : l) {
Map m = (Map) o;
if (dest.equals(m.get("dest"))) result.add(m);
}
return result;
}
/**
* whitebox checks the Similarity for the specified field according to {@link SolrCore#getLatestSchema}
*
* Executes each of the specified Similarity-accepting validators.
*/
@SafeVarargs
@SuppressWarnings({"unchecked", "varargs"})
private static <T extends Similarity> void assertFieldSimilarity(String fieldname, Class<T> expected, Consumer<T>... validators) {
CoreContainer cc = jetty.getCoreContainer();
try (SolrCore core = cc.getCore("collection1")) {
SimilarityFactory simfac = core.getLatestSchema().getSimilarityFactory();
assertNotNull(simfac);
assertTrue("test only works with SchemaSimilarityFactory",
simfac instanceof SchemaSimilarityFactory);
Similarity mainSim = core.getLatestSchema().getSimilarity();
assertNotNull(mainSim);
// sanity check simfac vs sim in use - also verify infom called on simfac, otherwise exception
assertEquals(mainSim, simfac.getSimilarity());
assertTrue("test only works with PerFieldSimilarityWrapper, SchemaSimilarityFactory redefined?",
mainSim instanceof PerFieldSimilarityWrapper);
Similarity fieldSim = ((PerFieldSimilarityWrapper)mainSim).get(fieldname);
assertEquals("wrong sim for field=" + fieldname, expected, fieldSim.getClass());
Arrays.asList(validators).forEach(v -> v.accept((T)fieldSim));
}
}
}