SOLR-3026: eDismax: Locking down which fields can be explicitly queried (user fields aka uf)

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1299172 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Jan Høydahl 2012-03-10 09:42:41 +00:00
parent b9dfe2836e
commit 2b65fd00e6
3 changed files with 446 additions and 65 deletions

View File

@ -536,6 +536,9 @@ New Features
* SOLR-2202: Currency FieldType, whith support for currencies and exchange rates
(Greg Fodor & Andrew Morrison via janhoy, rmuir, Uwe Schindler)
* SOLR-3026: eDismax: Locking down which fields can be explicitly queried (user fields aka uf)
(janhoy, hossmann, Tomás Fernández Löbbe)
Optimizations
----------------------
* SOLR-1931: Speedup for LukeRequestHandler and admin/schema browser. New parameter

View File

@ -22,6 +22,16 @@
package org.apache.solr.search;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.queries.function.BoostedQuery;
import org.apache.lucene.queries.function.FunctionQuery;
import org.apache.lucene.queries.function.ValueSource;
@ -30,7 +40,6 @@ import org.apache.lucene.queries.function.valuesource.QueryValueSource;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.analysis.Analyzer;
import org.apache.solr.common.params.DisMaxParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
@ -39,8 +48,6 @@ import org.apache.solr.schema.FieldType;
import org.apache.solr.util.SolrPluginUtils;
import org.apache.solr.analysis.*;
import java.util.*;
/**
* An advanced multi-field query parser.
* @lucene.experimental
@ -74,7 +81,10 @@ class ExtendedDismaxQParser extends QParser {
/** shorten the class references for utilities */
private static interface DMP extends DisMaxParams {
/* :NOOP */
/**
* User fields. The fields that can be used by the end user to create field-specific queries.
*/
public static String UF = "uf";
}
@ -82,15 +92,30 @@ class ExtendedDismaxQParser extends QParser {
super(qstr, localParams, params, req);
}
Map<String,Float> queryFields;
Query parsedUserQuery;
/**
* The field names specified by 'qf' that (most) clauses will
* be queried against
*/
private Map<String,Float> queryFields;
/**
* The field names specified by 'uf' that users are
* allowed to include literally in their query string. The Float
* boost values will be applied automaticly to any clause using that
* field name. '*' will be treated as an alias for any
* field that exists in the schema. Wildcards are allowed to
* express dynamicFields.
*/
private UserFields userFields;
private Query parsedUserQuery;
private String[] boostParams;
private String[] multBoosts;
private List<Query> boostQueries;
private Query altUserQuery;
private QParser altQParser;
private SolrParams solrParams;
@Override
@ -98,11 +123,13 @@ class ExtendedDismaxQParser extends QParser {
SolrParams localParams = getLocalParams();
SolrParams params = getParams();
SolrParams solrParams = SolrParams.wrapDefaults(localParams, params);
solrParams = SolrParams.wrapDefaults(localParams, params);
final String minShouldMatch =
DisMaxQParser.parseMinShouldMatch(req.getSchema(), solrParams);
userFields = new UserFields(U.parseFieldBoosts(solrParams.getParams(DMP.UF)));
queryFields = SolrPluginUtils.parseFieldBoosts(solrParams.getParams(DisMaxParams.QF));
if (0 == queryFields.size()) {
queryFields.put(req.getSchema().getDefaultSearchFieldName(), 1.0f);
@ -158,75 +185,59 @@ class ExtendedDismaxQParser extends QParser {
new ExtendedSolrQueryParser(this, IMPOSSIBLE_FIELD_NAME);
up.addAlias(IMPOSSIBLE_FIELD_NAME,
tiebreaker, queryFields);
addAliasesFromRequest(up, tiebreaker);
up.setPhraseSlop(qslop); // slop for explicit user phrase queries
up.setAllowLeadingWildcard(true);
// defer escaping and only do if lucene parsing fails, or we need phrases
// parsing fails. Need to sloppy phrase queries anyway though.
List<Clause> clauses = null;
boolean specialSyntax = false;
int numPluses = 0;
int numMinuses = 0;
int numOptional = 0;
int numAND = 0;
int numOR = 0;
int numNOT = 0;
boolean sawLowerAnd=false;
boolean sawLowerOr=false;
clauses = splitIntoClauses(userQuery, false);
for (Clause clause : clauses) {
if (!clause.isPhrase && clause.hasSpecialSyntax) {
specialSyntax = true;
}
if (clause.must == '+') numPluses++;
if (clause.must == '-') numMinuses++;
if (clause.isBareWord()) {
String s = clause.val;
if ("AND".equals(s)) {
numAND++;
} else if ("OR".equals(s)) {
if ("OR".equals(s)) {
numOR++;
} else if ("NOT".equals(s)) {
numNOT++;
} else if (lowercaseOperators) {
if ("and".equals(s)) {
numAND++;
sawLowerAnd=true;
} else if ("or".equals(s)) {
numOR++;
sawLowerOr=true;
}
} else if (lowercaseOperators && "or".equals(s)) {
numOR++;
}
}
}
numOptional = clauses.size() - (numPluses + numMinuses);
// convert lower or mixed case operators to uppercase if we saw them.
// Always rebuild mainUserQuery from clauses to catch modifications from splitIntoClauses
// This was necessary for userFields modifications to get propagated into the query.
// Convert lower or mixed case operators to uppercase if we saw them.
// only do this for the lucene query part and not for phrase query boosting
// since some fields might not be case insensitive.
// We don't use a regex for this because it might change and AND or OR in
// a phrase query in a case sensitive field.
if (sawLowerAnd || sawLowerOr) {
StringBuilder sb = new StringBuilder();
for (int i=0; i<clauses.size(); i++) {
Clause clause = clauses.get(i);
String s = clause.raw;
// and and or won't be operators at the start or end
if (i>0 && i+1<clauses.size()) {
if ("AND".equalsIgnoreCase(s)) {
s="AND";
} else if ("OR".equalsIgnoreCase(s)) {
s="OR";
}
StringBuilder sb = new StringBuilder();
for (int i=0; i<clauses.size(); i++) {
Clause clause = clauses.get(i);
String s = clause.raw;
// and and or won't be operators at the start or end
if (i>0 && i+1<clauses.size()) {
if ("AND".equalsIgnoreCase(s)) {
s="AND";
} else if ("OR".equalsIgnoreCase(s)) {
s="OR";
}
sb.append(s);
sb.append(' ');
}
mainUserQuery = sb.toString();
sb.append(s);
sb.append(' ');
}
mainUserQuery = sb.toString();
// For correct lucene queries, turn off mm processing if there
// were explicit operators (except for AND).
boolean doMinMatched = (numOR + numNOT + numPluses + numMinuses) == 0;
@ -252,9 +263,8 @@ class ExtendedDismaxQParser extends QParser {
}
}
if (parsedUserQuery == null) {
StringBuilder sb = new StringBuilder();
sb = new StringBuilder();
for (Clause clause : clauses) {
boolean doQuote = clause.isPhrase;
@ -278,6 +288,12 @@ class ExtendedDismaxQParser extends QParser {
if (doQuote) {
sb.append('"');
}
if (clause.field != null) {
// Add the default user field boost, if any
Float boost = userFields.getBoost(clause.field);
if(boost != null)
sb.append("^").append(boost);
}
sb.append(' ');
}
String escapedUserQuery = sb.toString();
@ -393,6 +409,28 @@ class ExtendedDismaxQParser extends QParser {
return topQuery;
}
/**
* Extracts all the alised fields from the requests and adds them to up
* @param up
* @param tiebreaker
* @throws ParseException
*/
private void addAliasesFromRequest(ExtendedSolrQueryParser up, float tiebreaker) throws ParseException {
Iterator<String> it = solrParams.getParameterNamesIterator();
while(it.hasNext()) {
String param = it.next();
if(param.startsWith("f.") && param.endsWith(".qf")) {
// Add the alias
String fname = param.substring(2,param.length()-3);
String qfReplacement = solrParams.get(param);
Map<String,Float> parsedQf = SolrPluginUtils.parseFieldBoosts(qfReplacement);
if(parsedQf.size() == 0)
return;
up.addAlias(fname, tiebreaker, parsedQf);
}
}
}
/**
* Modifies the main query by adding a new optional Query consisting
* of shingled phrase queries across the specified clauses using the
@ -598,6 +636,7 @@ class ExtendedDismaxQParser extends QParser {
int end=s.length();
char ch=0;
int start;
boolean disallowUserField = false;
outer: while (pos < end) {
ch = s.charAt(pos);
@ -614,6 +653,10 @@ class ExtendedDismaxQParser extends QParser {
}
clause.field = getFieldName(s, pos, end);
if(clause.field != null && !userFields.isAllowed(clause.field)) {
disallowUserField = true;
clause.field = null;
}
if (clause.field != null) {
pos += clause.field.length(); // skip the field name
pos++; // skip the ':'
@ -709,15 +752,31 @@ class ExtendedDismaxQParser extends QParser {
}
if (clause != null) {
clause.raw = s.substring(start, pos);
if(disallowUserField) {
clause.raw = clause.val;
} else {
clause.raw = s.substring(start, pos);
// Add default userField boost if no explicit boost exists
if(userFields.isAllowed(clause.field) && !clause.raw.contains("^")) {
Float boost = userFields.getBoost(clause.field);
if(boost != null)
clause.raw += "^" + boost;
}
}
lst.add(clause);
}
clause = new Clause();
disallowUserField = false;
}
return lst;
}
/**
* returns a field name or legal field alias from the current
* position of the string
* @param solrParams
*/
public String getFieldName(String s, int pos, int end) {
if (pos >= end) return null;
int p=pos;
@ -731,10 +790,12 @@ class ExtendedDismaxQParser extends QParser {
if (!(Character.isJavaIdentifierPart(ch) || ch=='-' || ch=='.')) return null;
}
String fname = s.substring(pos, p);
return getReq().getSchema().getFieldTypeNoEx(fname) == null ? null : fname;
boolean isInSchema = getReq().getSchema().getFieldTypeNoEx(fname) != null;
boolean isAlias = solrParams.get("f."+fname+".qf") != null;
return (isInSchema || isAlias) ? fname : null;
}
public static List<String> split(String s, boolean ignoreQuote) {
ArrayList<String> lst = new ArrayList<String>(4);
int pos=0, start=0, end=s.length();
@ -792,7 +853,7 @@ class ExtendedDismaxQParser extends QParser {
* A subclass of SolrQueryParser that supports aliasing fields for
* constructing DisjunctionMaxQueries.
*/
class ExtendedSolrQueryParser extends SolrQueryParser {
static class ExtendedSolrQueryParser extends SolrQueryParser {
/** A simple container for storing alias info
@ -870,6 +931,16 @@ class ExtendedDismaxQParser extends QParser {
a.fields = fieldBoosts;
aliases.put(field, a);
}
/**
* Returns the aliases found for a field.
* Returns null if there are no aliases for the field
* @param field
* @return
*/
public Alias getAlias(String field) {
return aliases.get(field);
}
QType type;
@ -980,9 +1051,9 @@ class ExtendedDismaxQParser extends QParser {
* DisjunctionMaxQuery. (so yes: aliases which point at other
* aliases should work)
*/
protected Query getAliasedQuery()
throws ParseException {
protected Query getAliasedQuery() throws ParseException {
Alias a = aliases.get(field);
this.validateCyclicAliasing(field);
if (a != null) {
List<Query> lst = getQueries(a);
if (lst == null || lst.size()==0)
@ -1018,15 +1089,43 @@ class ExtendedDismaxQParser extends QParser {
}
}
/**
* Validate there is no cyclic referencing in the aliasing
*/
private void validateCyclicAliasing(String field) throws ParseException {
Set<String> set = new HashSet<String>();
set.add(field);
if(validateField(field, set)) {
throw new ParseException("Field aliases lead to a cycle");
}
}
private boolean validateField(String field, Set<String> set) {
if(this.getAlias(field) == null) {
return false;
}
boolean hascycle = false;
for(String referencedField:this.getAlias(field).fields.keySet()) {
if(!set.add(referencedField)) {
hascycle = true;
} else {
if(validateField(referencedField, set)) {
hascycle = true;
}
set.remove(referencedField);
}
}
return hascycle;
}
protected List<Query> getQueries(Alias a) throws ParseException {
protected List<Query> getQueries(Alias a) throws ParseException {
if (a == null) return null;
if (a.fields.size()==0) return null;
List<Query> lst= new ArrayList<Query>(4);
for (String f : a.fields.keySet()) {
this.field = f;
Query sub = getQuery();
Query sub = getAliasedQuery();
if (sub != null) {
Float boost = a.fields.get(f);
if (boost != null) {
@ -1127,4 +1226,129 @@ class ExtendedDismaxQParser extends QParser {
if (q instanceof BooleanQuery && ((BooleanQuery)q).clauses().size()==0) return true;
return false;
}
/**
* Class that encapsulates the input from userFields parameter and can answer whether
* a field allowed or disallowed as fielded query in the query string
*/
static class UserFields {
private Map<String,Float> userFieldsMap;
private DynamicField[] dynamicUserFields;
private DynamicField[] negativeDynamicUserFields;
UserFields(Map<String,Float> ufm) {
userFieldsMap = ufm;
if (0 == userFieldsMap.size()) {
userFieldsMap.put("*", null);
}
// Process dynamic patterns in userFields
ArrayList<DynamicField> dynUserFields = new ArrayList<DynamicField>();
ArrayList<DynamicField> negDynUserFields = new ArrayList<DynamicField>();
for(String f : userFieldsMap.keySet()) {
if(f.contains("*")) {
if(f.startsWith("-"))
negDynUserFields.add(new DynamicField(f.substring(1)));
else
dynUserFields.add(new DynamicField(f));
}
}
Collections.sort(dynUserFields);
dynamicUserFields = dynUserFields.toArray(new DynamicField[dynUserFields.size()]);
Collections.sort(negDynUserFields);
negativeDynamicUserFields = negDynUserFields.toArray(new DynamicField[negDynUserFields.size()]);
// System.out.println("** userF="+userFieldsMap+", dynUF="+Arrays.toString(dynamicUserFields)+", negDynUF="+Arrays.toString(negativeDynamicUserFields));
}
/**
* Is the given field name allowed according to UserFields spec given in the uf parameter?
* @param fname the field name to examine
* @return true if the fielded queries are allowed on this field
*/
public boolean isAllowed(String fname) {
boolean res = ((userFieldsMap.containsKey(fname) || isDynField(fname, false)) &&
!userFieldsMap.containsKey("-"+fname) &&
!isDynField(fname, true));
return res;
}
private boolean isDynField(String field, boolean neg) {
return getDynFieldForName(field, neg) == null ? false : true;
}
private String getDynFieldForName(String f, boolean neg) {
for( DynamicField df : neg?negativeDynamicUserFields:dynamicUserFields ) {
if( df.matches( f ) ) return df.wildcard;
}
return null;
}
/**
* Finds the default user field boost associated with the given field.
* This is parsed from the uf parameter, and may be specified as wildcards, e.g. *name^2.0 or *^3.0
* @param field the field to find boost for
* @return the float boost value associated with the given field or a wildcard matching the field
*/
public Float getBoost(String field) {
return (userFieldsMap.containsKey(field)) ?
userFieldsMap.get(field) : // Exact field
userFieldsMap.get(getDynFieldForName(field, false)); // Dynamic field
}
}
/* Represents a dynamic field, for easier matching, inspired by same class in IndexSchema */
static class DynamicField implements Comparable<DynamicField> {
final static int STARTS_WITH=1;
final static int ENDS_WITH=2;
final static int CATCHALL=3;
final String wildcard;
final int type;
final String str;
protected DynamicField(String wildcard) {
this.wildcard = wildcard;
if (wildcard.equals("*")) {
type=CATCHALL;
str=null;
}
else if (wildcard.startsWith("*")) {
type=ENDS_WITH;
str=wildcard.substring(1);
}
else if (wildcard.endsWith("*")) {
type=STARTS_WITH;
str=wildcard.substring(0,wildcard.length()-1);
}
else {
throw new RuntimeException("dynamic field name must start or end with *");
}
}
/*
* Returns true if the regex wildcard for this DynamicField would match the input field name
*/
public boolean matches(String name) {
if (type==CATCHALL) return true;
else if (type==STARTS_WITH && name.startsWith(str)) return true;
else if (type==ENDS_WITH && name.endsWith(str)) return true;
else return false;
}
/**
* Sort order is based on length of regex. Longest comes first.
* @param other The object to compare to.
* @return a negative integer, zero, or a positive integer
* as this object is less than, equal to, or greater than
* the specified object.
*/
public int compareTo(DynamicField other) {
return other.wildcard.length() - wildcard.length();
}
public String toString() {
return this.wildcard;
}
}
}

View File

@ -17,6 +17,9 @@
package org.apache.solr.search;
import java.io.IOException;
import org.apache.solr.common.SolrException;
import org.apache.solr.util.AbstractSolrTestCase;
public class TestExtendedDismaxParser extends AbstractSolrTestCase {
@ -31,16 +34,6 @@ public class TestExtendedDismaxParser extends AbstractSolrTestCase {
// if you override setUp or tearDown, you better call
// the super classes version
super.setUp();
}
@Override
public void tearDown() throws Exception {
// if you override setUp or tearDown, you better call
// the super classes version
super.tearDown();
}
// test the edismax query parser based on the dismax parser
public void testFocusQueryParser() {
assertU(adoc("id", "42", "trait_ss", "Tool", "trait_ss", "Obnoxious",
"name", "Zapp Brannigan"));
assertU(adoc("id", "43" ,
@ -63,6 +56,16 @@ public class TestExtendedDismaxParser extends AbstractSolrTestCase {
assertU(adoc("id", "50", "text_sw", "start new big city end"));
assertU(commit());
}
@Override
public void tearDown() throws Exception {
// if you override setUp or tearDown, you better call
// the super classes version
super.tearDown();
}
// test the edismax query parser based on the dismax parser
public void testFocusQueryParser() {
String allq = "id:[42 TO 50]";
String allr = "*[count(//doc)=9]";
String oner = "*[count(//doc)=1]";
@ -249,4 +252,155 @@ public class TestExtendedDismaxParser extends AbstractSolrTestCase {
}
public void testUserFields() {
String oner = "*[count(//doc)=1]";
String nor = "*[count(//doc)=0]";
// User fields
// Default is allow all "*"
// If a list of fields are given, only those are allowed "foo bar"
// Possible to invert with "-" syntax:
// Disallow all: "-*"
// Allow all but id: "* -id"
// Also supports "dynamic" field name wildcarding
assertQ(req("defType","edismax", "q","id:42"),
oner);
assertQ(req("defType","edismax", "uf","*", "q","id:42"),
oner);
assertQ(req("defType","edismax", "uf","id", "q","id:42"),
oner);
assertQ(req("defType","edismax", "uf","-*", "q","id:42"),
nor);
assertQ(req("defType","edismax", "uf","loremipsum", "q","id:42"),
nor);
assertQ(req("defType","edismax", "uf","* -id", "q","id:42"),
nor);
assertQ(req("defType","edismax", "uf","* -loremipsum", "q","id:42"),
oner);
assertQ(req("defType","edismax", "uf","id^5.0", "q","id:42"),
oner);
assertQ(req("defType","edismax", "uf","*^5.0", "q","id:42"),
oner);
assertQ(req("defType","edismax", "uf","id^5.0", "q","id:42^10.0"),
oner);
assertQ(req("defType","edismax", "uf","na*", "q","name:Zapp"),
oner);
assertQ(req("defType","edismax", "uf","*me", "q","name:Zapp"),
oner);
assertQ(req("defType","edismax", "uf","* -na*", "q","name:Zapp"),
nor);
assertQ(req("defType","edismax", "uf","*me -name", "q","name:Zapp"),
nor);
assertQ(req("defType","edismax", "uf","*ame -*e", "q","name:Zapp"),
nor);
// Boosts from user fields
assertQ(req("defType","edismax", "debugQuery","true", "rows","0", "q","id:42"),
"//str[@name='parsedquery_toString'][.='+id:42']");
assertQ(req("defType","edismax", "debugQuery","true", "rows","0", "uf","*^5.0", "q","id:42"),
"//str[@name='parsedquery_toString'][.='+id:42^5.0']");
assertQ(req("defType","edismax", "debugQuery","true", "rows","0", "uf","*^2.0 id^5.0 -xyz", "q","name:foo"),
"//str[@name='parsedquery_toString'][.='+name:foo^2.0']");
assertQ(req("defType","edismax", "debugQuery","true", "rows","0", "uf","i*^5.0", "q","id:42"),
"//str[@name='parsedquery_toString'][.='+id:42^5.0']");
assertQ(req("defType","edismax", "uf","-*", "q","cannons"),
oner);
assertQ(req("defType","edismax", "uf","* -id", "q","42", "qf", "id"), oner);
}
public void testAliasing() throws IOException, Exception {
String oner = "*[count(//doc)=1]";
String twor = "*[count(//doc)=2]";
String nor = "*[count(//doc)=0]";
// Aliasing
// Single field
assertQ(req("defType","edismax", "q","myalias:Zapp"),
nor);
assertQ(req("defType","edismax", "q","myalias:Zapp", "f.myalias.qf","name"),
oner);
// Multi field
assertQ(req("defType","edismax", "uf", "myalias", "q","myalias:(Zapp Obnoxious)", "f.myalias.qf","name^2.0 mytrait_ss^5.0", "mm", "50%"),
oner);
// Multi field
assertQ(req("defType","edismax", "q","Zapp Obnoxious", "f.myalias.qf","name^2.0 mytrait_ss^5.0"),
nor);
assertQ(req("defType","edismax", "q","Zapp Obnoxious", "qf","myalias^10.0", "f.myalias.qf","name^2.0 mytrait_ss^5.0"), oner);
assertQ(req("defType","edismax", "q","Zapp Obnoxious", "qf","myalias^10.0", "f.myalias.qf","name^2.0 trait_ss^5.0"), twor);
assertQ(req("defType","edismax", "q","Zapp Obnoxious", "qf","myalias^10.0", "f.myalias.qf","name^2.0 trait_ss^5.0", "mm", "100%"), oner);
assertQ(req("defType","edismax", "q","Zapp Obnoxious", "qf","who^10.0 where^3.0", "f.who.qf","name^2.0", "f.where.qf", "mytrait_ss^5.0"), oner);
assertQ(req("defType","edismax", "q","Zapp Obnoxious", "qf","myalias", "f.myalias.qf","name mytrait_ss", "uf", "myalias"), oner);
assertQ(req("defType","edismax", "uf","who", "q","who:(Zapp Obnoxious)", "f.who.qf", "name^2.0 trait_ss^5.0", "qf", "id"), twor);
assertQ(req("defType","edismax", "uf","* -name", "q","who:(Zapp Obnoxious)", "f.who.qf", "name^2.0 trait_ss^5.0"), twor);
}
public void testAliasingBoost() throws IOException, Exception {
assertQ(req("defType","edismax", "q","Zapp Pig", "qf","myalias", "f.myalias.qf","name trait_ss^0.5"), "//result/doc[1]/str[@name='id']=42", "//result/doc[2]/str[@name='id']=47");//doc 42 should score higher than 46
assertQ(req("defType","edismax", "q","Zapp Pig", "qf","myalias^100 name", "f.myalias.qf","trait_ss^0.5"), "//result/doc[1]/str[@name='id']=47", "//result/doc[2]/str[@name='id']=42");//Now the order should be inverse
}
public void testCyclicAliasing() throws IOException, Exception {
try {
h.query(req("defType","edismax", "q","Zapp Pig", "qf","who", "f.who.qf","name","f.name.qf","who"));
fail("Simple cyclic alising");
} catch (SolrException e) {
assertTrue(e.getCause().getMessage().contains("Field aliases lead to a cycle"));
}
try {
h.query(req("defType","edismax", "q","Zapp Pig", "qf","who", "f.who.qf","name","f.name.qf","myalias", "f.myalias.qf","who"));
fail();
} catch (SolrException e) {
assertTrue(e.getCause().getMessage().contains("Field aliases lead to a cycle"));
}
try {
h.query(req("defType","edismax", "q","Zapp Pig", "qf","field1", "f.field1.qf","field2 field3","f.field2.qf","field4 field5", "f.field4.qf","field5", "f.field5.qf","field6", "f.field3.qf","field6"));
} catch (SolrException e) {
fail("This is not cyclic alising");
}
try {
h.query(req("defType","edismax", "q","Zapp Pig", "qf","field1", "f.field1.qf","field2 field3", "f.field2.qf","field4 field5", "f.field4.qf","field5", "f.field5.qf","field4"));
fail();
} catch (SolrException e) {
assertTrue(e.getCause().getMessage().contains("Field aliases lead to a cycle"));
}
try {
h.query(req("defType","edismax", "q","who:(Zapp Pig)", "qf","field1", "f.who.qf","name","f.name.qf","myalias", "f.myalias.qf","who"));
fail();
} catch (SolrException e) {
assertTrue(e.getCause().getMessage().contains("Field aliases lead to a cycle"));
}
}
}