mirror of https://github.com/apache/lucene.git
SOLR-11244: Query DSL for Solr
This commit is contained in:
parent
63a0c8d92f
commit
d3013ab600
|
@ -81,6 +81,8 @@ New Features
|
||||||
|
|
||||||
* SOLR-11215: Make a metric accessible through a single param. (ab)
|
* SOLR-11215: Make a metric accessible through a single param. (ab)
|
||||||
|
|
||||||
|
* SOLR-11244: Query DSL for Solr (Cao Manh Dat)
|
||||||
|
|
||||||
Bug Fixes
|
Bug Fixes
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* 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.request.json;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.solr.common.SolrException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert json query object to local params.
|
||||||
|
*
|
||||||
|
* @lucene.internal
|
||||||
|
*/
|
||||||
|
class JsonQueryConverter {
|
||||||
|
private int numParams = 0;
|
||||||
|
|
||||||
|
String toLocalParams(Object jsonQueryObject, Map<String, String[]> additionalParams) {
|
||||||
|
if (jsonQueryObject instanceof String) return jsonQueryObject.toString();
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
buildLocalParams(builder, jsonQueryObject, true, additionalParams);
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String putParam(String val, Map<String, String[]> additionalParams) {
|
||||||
|
String name = "_tt"+(numParams++);
|
||||||
|
additionalParams.put(name, new String[]{val});
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildLocalParams(StringBuilder builder, Object val, boolean isQParser, Map<String, String[]> additionalParams) {
|
||||||
|
if (!isQParser && !(val instanceof Map)) {
|
||||||
|
// val is value of a query parser, and it is not a map
|
||||||
|
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
||||||
|
"Error when parsing json query, expect a json object here, but found : "+val);
|
||||||
|
}
|
||||||
|
if (val instanceof String) {
|
||||||
|
builder.append('$').append(putParam(val.toString(), additionalParams));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (val instanceof Number) {
|
||||||
|
builder.append(val);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(val instanceof Map)) {
|
||||||
|
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
||||||
|
"Error when parsing json query, expect a json object here, but found : "+val);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String,Object> map = (Map<String, Object>) val;
|
||||||
|
if (isQParser) {
|
||||||
|
if (map.size() != 1) {
|
||||||
|
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
||||||
|
"Error when parsing json query, expect only one query parser here, but found : "+map.keySet());
|
||||||
|
}
|
||||||
|
String qtype = map.keySet().iterator().next();
|
||||||
|
Object subVal = map.get(qtype);
|
||||||
|
|
||||||
|
// We don't want to introduce unnecessary variable at root level
|
||||||
|
boolean useSubBuilder = builder.length() > 0;
|
||||||
|
StringBuilder subBuilder = builder;
|
||||||
|
|
||||||
|
if (useSubBuilder) subBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
subBuilder = subBuilder.append("{!").append(qtype).append(' ');;
|
||||||
|
buildLocalParams(subBuilder, subVal, false, additionalParams);
|
||||||
|
subBuilder.append("}");
|
||||||
|
|
||||||
|
if (useSubBuilder) builder.append('$').append(putParam(subBuilder.toString(), additionalParams));
|
||||||
|
} else {
|
||||||
|
for (Map.Entry<String, Object> entry : map.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
if (entry.getValue() instanceof List) {
|
||||||
|
if (key.equals("query")) {
|
||||||
|
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
|
||||||
|
"Error when parsing json query, value of query field should not be a list, found : " + entry.getValue());
|
||||||
|
}
|
||||||
|
List l = (List) entry.getValue();
|
||||||
|
for (Object subVal : l) {
|
||||||
|
builder.append(key).append("=");
|
||||||
|
buildLocalParams(builder, subVal, true, additionalParams);
|
||||||
|
builder.append(" ");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (key.equals("query")) {
|
||||||
|
key = "v";
|
||||||
|
}
|
||||||
|
builder.append(key).append("=");
|
||||||
|
buildLocalParams(builder, entry.getValue(), true, additionalParams);
|
||||||
|
builder.append(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -190,16 +190,20 @@ public class RequestUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
// implement compat for existing components...
|
// implement compat for existing components...
|
||||||
|
JsonQueryConverter jsonQueryConverter = new JsonQueryConverter();
|
||||||
if (json != null && !isShard) {
|
if (json != null && !isShard) {
|
||||||
for (Map.Entry<String,Object> entry : json.entrySet()) {
|
for (Map.Entry<String,Object> entry : json.entrySet()) {
|
||||||
String key = entry.getKey();
|
String key = entry.getKey();
|
||||||
String out = null;
|
String out = null;
|
||||||
|
boolean isQuery = false;
|
||||||
boolean arr = false;
|
boolean arr = false;
|
||||||
if ("query".equals(key)) {
|
if ("query".equals(key)) {
|
||||||
out = "q";
|
out = "q";
|
||||||
|
isQuery = true;
|
||||||
} else if ("filter".equals(key)) {
|
} else if ("filter".equals(key)) {
|
||||||
out = "fq";
|
out = "fq";
|
||||||
arr = true;
|
arr = true;
|
||||||
|
isQuery = true;
|
||||||
} else if ("fields".equals(key)) {
|
} else if ("fields".equals(key)) {
|
||||||
out = "fl";
|
out = "fl";
|
||||||
arr = true;
|
arr = true;
|
||||||
|
@ -230,14 +234,14 @@ public class RequestUtil {
|
||||||
if (lst != null) {
|
if (lst != null) {
|
||||||
for (int i = 0; i < jsonSize; i++) {
|
for (int i = 0; i < jsonSize; i++) {
|
||||||
Object v = lst.get(i);
|
Object v = lst.get(i);
|
||||||
newval[existingSize + i] = v.toString();
|
newval[existingSize + i] = isQuery ? jsonQueryConverter.toLocalParams(v, newMap) : v.toString();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
newval[newval.length-1] = val.toString();
|
newval[newval.length-1] = isQuery ? jsonQueryConverter.toLocalParams(val, newMap) : val.toString();
|
||||||
}
|
}
|
||||||
newMap.put(out, newval);
|
newMap.put(out, newval);
|
||||||
} else {
|
} else {
|
||||||
newMap.put(out, new String[]{val.toString()});
|
newMap.put(out, new String[]{isQuery ? jsonQueryConverter.toLocalParams(val, newMap) : val.toString()});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.apache.solr.search;
|
||||||
|
|
||||||
|
import org.apache.lucene.search.BooleanClause;
|
||||||
|
import org.apache.lucene.search.BooleanQuery;
|
||||||
|
import org.apache.lucene.search.Query;
|
||||||
|
import org.apache.solr.common.params.SolrParams;
|
||||||
|
import org.apache.solr.request.SolrQueryRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a boolean query from sub queries.
|
||||||
|
* Sub queries can be marked as must, must_not, filter or should
|
||||||
|
*
|
||||||
|
* <p>Example: <code>{!bool should=title:lucene should=title:solr must_not=id:1}</code>
|
||||||
|
*/
|
||||||
|
public class BoolQParserPlugin extends QParserPlugin {
|
||||||
|
public static final String NAME = "bool";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
|
||||||
|
return new QParser(qstr, localParams, params, req) {
|
||||||
|
@Override
|
||||||
|
public Query parse() throws SyntaxError {
|
||||||
|
BooleanQuery.Builder builder = new BooleanQuery.Builder();
|
||||||
|
SolrParams solrParams = SolrParams.wrapDefaults(localParams, params);
|
||||||
|
addQueries(builder, solrParams.getParams("must"), BooleanClause.Occur.MUST);
|
||||||
|
addQueries(builder, solrParams.getParams("must_not"), BooleanClause.Occur.MUST_NOT);
|
||||||
|
addQueries(builder, solrParams.getParams("filter"), BooleanClause.Occur.FILTER);
|
||||||
|
addQueries(builder, solrParams.getParams("should"), BooleanClause.Occur.SHOULD);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addQueries(BooleanQuery.Builder builder, String[] subQueries, BooleanClause.Occur occur) throws SyntaxError {
|
||||||
|
if (subQueries != null) {
|
||||||
|
for (String subQuery : subQueries) {
|
||||||
|
builder.add(subQuery(subQuery, null).parse(), occur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -81,6 +81,7 @@ public abstract class QParserPlugin implements NamedListInitializedPlugin, SolrI
|
||||||
map.put(SignificantTermsQParserPlugin.NAME, SignificantTermsQParserPlugin.class);
|
map.put(SignificantTermsQParserPlugin.NAME, SignificantTermsQParserPlugin.class);
|
||||||
map.put(PayloadScoreQParserPlugin.NAME, PayloadScoreQParserPlugin.class);
|
map.put(PayloadScoreQParserPlugin.NAME, PayloadScoreQParserPlugin.class);
|
||||||
map.put(PayloadCheckQParserPlugin.NAME, PayloadCheckQParserPlugin.class);
|
map.put(PayloadCheckQParserPlugin.NAME, PayloadCheckQParserPlugin.class);
|
||||||
|
map.put(BoolQParserPlugin.NAME, BoolQParserPlugin.class);
|
||||||
|
|
||||||
standardPlugins = Collections.unmodifiableMap(map);
|
standardPlugins = Collections.unmodifiableMap(map);
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ public class TestSmileRequest extends SolrTestCaseJ4 {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
client.queryDefaults().set("shards", servers.getShards());
|
client.queryDefaults().set("shards", servers.getShards());
|
||||||
TestJsonRequest.doJsonRequest(client);
|
TestJsonRequest.doJsonRequest(client, false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ public class TestJsonRequest extends SolrTestCaseHS {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testLocalJsonRequest() throws Exception {
|
public void testLocalJsonRequest() throws Exception {
|
||||||
doJsonRequest(Client.localClient);
|
doJsonRequest(Client.localClient, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -62,11 +62,10 @@ public class TestJsonRequest extends SolrTestCaseHS {
|
||||||
initServers();
|
initServers();
|
||||||
Client client = servers.getClient( random().nextInt() );
|
Client client = servers.getClient( random().nextInt() );
|
||||||
client.queryDefaults().set( "shards", servers.getShards() );
|
client.queryDefaults().set( "shards", servers.getShards() );
|
||||||
doJsonRequest(client);
|
doJsonRequest(client, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void doJsonRequest(Client client, boolean isDistrib) throws Exception {
|
||||||
public static void doJsonRequest(Client client) throws Exception {
|
|
||||||
client.deleteByQuery("*:*", null);
|
client.deleteByQuery("*:*", null);
|
||||||
client.add(sdoc("id", "1", "cat_s", "A", "where_s", "NY"), null);
|
client.add(sdoc("id", "1", "cat_s", "A", "where_s", "NY"), null);
|
||||||
client.add(sdoc("id", "2", "cat_s", "B", "where_s", "NJ"), null);
|
client.add(sdoc("id", "2", "cat_s", "B", "where_s", "NJ"), null);
|
||||||
|
@ -217,6 +216,178 @@ public class TestJsonRequest extends SolrTestCaseHS {
|
||||||
, "debug/json=={query:'cat_s:A', filter:'where_s:NY'}"
|
, "debug/json=={query:'cat_s:A', filter:'where_s:NY'}"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// test query dsl
|
||||||
|
client.testJQ( params("json", "{'query':'{!lucene}id:1'}")
|
||||||
|
, "response/numFound==1"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.testJQ( params("json", "{" +
|
||||||
|
" 'query': {" +
|
||||||
|
" 'bool' : {" +
|
||||||
|
" 'should' : [" +
|
||||||
|
" {'lucene' : {'query' : 'id:1'}}," +
|
||||||
|
" 'id:2'" +
|
||||||
|
" ]" +
|
||||||
|
" }" +
|
||||||
|
" }" +
|
||||||
|
"}")
|
||||||
|
, "response/numFound==2"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.testJQ( params("json", "{" +
|
||||||
|
" 'query': {" +
|
||||||
|
" 'bool' : {" +
|
||||||
|
" 'should' : [" +
|
||||||
|
" 'id:1'," +
|
||||||
|
" 'id:2'" +
|
||||||
|
" ]" +
|
||||||
|
" }" +
|
||||||
|
" }" +
|
||||||
|
"}")
|
||||||
|
, "response/numFound==2"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.testJQ( params("json", "{ " +
|
||||||
|
" query : {" +
|
||||||
|
" boost : {" +
|
||||||
|
" query : {" +
|
||||||
|
" lucene : { " +
|
||||||
|
" df : cat_s, " +
|
||||||
|
" query : A " +
|
||||||
|
" }" +
|
||||||
|
" }, " +
|
||||||
|
" b : 1.5 " +
|
||||||
|
" } " +
|
||||||
|
" } " +
|
||||||
|
"}")
|
||||||
|
, "response/numFound==2"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.testJQ( params("json","{ " +
|
||||||
|
" query : {" +
|
||||||
|
" bool : {" +
|
||||||
|
" must : {" +
|
||||||
|
" lucene : {" +
|
||||||
|
" q.op : AND," +
|
||||||
|
" df : cat_s," +
|
||||||
|
" query : A" +
|
||||||
|
" }" +
|
||||||
|
" }" +
|
||||||
|
" must_not : {lucene : {query:'id: 1'}}" +
|
||||||
|
" }" +
|
||||||
|
" }" +
|
||||||
|
"}")
|
||||||
|
, "response/numFound==1"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.testJQ( params("json","{ " +
|
||||||
|
" query : {" +
|
||||||
|
" bool : {" +
|
||||||
|
" must : {" +
|
||||||
|
" lucene : {" +
|
||||||
|
" q.op : AND," +
|
||||||
|
" df : cat_s," +
|
||||||
|
" query : A" +
|
||||||
|
" }" +
|
||||||
|
" }" +
|
||||||
|
" must_not : [{lucene : {query:'id: 1'}}]" +
|
||||||
|
" }" +
|
||||||
|
" }" +
|
||||||
|
"}")
|
||||||
|
, "response/numFound==1"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.testJQ( params("json","{ " +
|
||||||
|
" query : {" +
|
||||||
|
" bool : {" +
|
||||||
|
" must : '{!lucene q.op=AND df=cat_s}A'" +
|
||||||
|
" must_not : '{!lucene v=\\'id:1\\'}'" +
|
||||||
|
" }" +
|
||||||
|
" }" +
|
||||||
|
"}")
|
||||||
|
, "response/numFound==1"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
client.testJQ( params("json","{" +
|
||||||
|
" query : '*:*'," +
|
||||||
|
" filter : {" +
|
||||||
|
" collapse : {" +
|
||||||
|
" field : cat_s" +
|
||||||
|
" } " +
|
||||||
|
" } " +
|
||||||
|
"}")
|
||||||
|
, isDistrib ? "" : "response/numFound==2"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.testJQ( params("json","{" +
|
||||||
|
" query : {" +
|
||||||
|
" edismax : {" +
|
||||||
|
" query : 'A'," +
|
||||||
|
" qf : 'cat_s'," +
|
||||||
|
" bq : {" +
|
||||||
|
" edismax : {" +
|
||||||
|
" query : 'NJ'" +
|
||||||
|
" qf : 'where_s'" +
|
||||||
|
" }" +
|
||||||
|
" }" +
|
||||||
|
" }" +
|
||||||
|
" }, " +
|
||||||
|
" fields : id" +
|
||||||
|
"}")
|
||||||
|
, "response/numFound==2", isDistrib? "" : "response/docs==[{id:'4'},{id:'1'}]"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.testJQ( params("json","{" +
|
||||||
|
" query : {" +
|
||||||
|
" edismax : {" +
|
||||||
|
" query : 'A'," +
|
||||||
|
" qf : 'cat_s'," +
|
||||||
|
" bq : {" +
|
||||||
|
" edismax : {" +
|
||||||
|
" query : 'NY'" +
|
||||||
|
" qf : 'where_s'" +
|
||||||
|
" }" +
|
||||||
|
" }" +
|
||||||
|
" }" +
|
||||||
|
" }, " +
|
||||||
|
" fields : id" +
|
||||||
|
"}")
|
||||||
|
, "response/numFound==2", isDistrib? "" : "response/docs==[{id:'1'},{id:'4'}]"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.testJQ( params("json","{" +
|
||||||
|
" query : {" +
|
||||||
|
" dismax : {" +
|
||||||
|
" query : 'A NJ'" +
|
||||||
|
" qf : 'cat_s^0.1 where_s^100'" +
|
||||||
|
" } " +
|
||||||
|
" }, " +
|
||||||
|
" filter : '-id:2'," +
|
||||||
|
" fields : id" +
|
||||||
|
"}")
|
||||||
|
, "response/numFound==3", isDistrib? "" : "response/docs==[{id:'4'},{id:'5'},{id:'1'}]"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.testJQ( params("json","{" +
|
||||||
|
" query : {" +
|
||||||
|
" dismax : {" +
|
||||||
|
" query : 'A NJ'" +
|
||||||
|
" qf : ['cat_s^100', 'where_s^0.1']" +
|
||||||
|
" } " +
|
||||||
|
" }, " +
|
||||||
|
" filter : '-id:2'," +
|
||||||
|
" fields : id" +
|
||||||
|
"}")
|
||||||
|
, "response/numFound==3", isDistrib? "" : "response/docs==[{id:'4'},{id:'1'},{id:'5'}]"
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.testJQ(params("json", "{query:{'lucene':'id:1'}}"));
|
||||||
|
fail();
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertTrue(e.getMessage().contains("id:1"));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// test failure on unknown parameter
|
// test failure on unknown parameter
|
||||||
|
|
Loading…
Reference in New Issue