blob: 48f2816d22ebaecdf3369f03f6817d50d40ff7dc [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.response.transform;
import java.util.Collection;
import java.util.Iterator;
import org.apache.lucene.util.TestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.BasicResultContext;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.hamcrest.core.StringContains.containsString;
public class TestChildDocTransformer extends SolrTestCaseJ4 {
private static String ID_FIELD = "id";
private String[] titleVals = new String[2];
@BeforeClass
public static void beforeClass() throws Exception {
initCore("solrconfig.xml","schema-root.xml"); // *not* the "nest" schema
}
@After
public void cleanup() throws Exception {
assertU(delQ("*:*"));
assertU(commit());
}
@Test
public void testParentFilter() throws Exception {
for(int i=0; i<titleVals.length; i++) {
titleVals[i] = TestUtil.randomSimpleString(random(), 1, 20);
}
createIndex(titleVals);
testParentFilterJSON();
testParentFilterXML();
testSubQueryParentFilterJSON();
testSubQueryParentFilterXML();
}
@Test
public void testAllParams() throws Exception {
createSimpleIndex();
testChildDoctransformerJSON();
testChildDoctransformerXML();
testSubQueryXML();
testSubQueryJSON();
testChildDocNonStoredDVFields();
testChildReturnFields();
}
private void testChildDoctransformerXML() throws Exception {
String test1[] = new String[] {
"//*[@numFound='1']",
"/response/result/doc[1]/doc[1]/str[@name='id']='2'" ,
"/response/result/doc[1]/doc[2]/str[@name='id']='3'" ,
"/response/result/doc[1]/doc[3]/str[@name='id']='4'" ,
"/response/result/doc[1]/doc[4]/str[@name='id']='5'" ,
"/response/result/doc[1]/doc[5]/str[@name='id']='6'" ,
"/response/result/doc[1]/doc[6]/str[@name='id']='7'"};
String test2[] = new String[] {
"//*[@numFound='1']",
"/response/result/doc[1]/doc[1]/str[@name='id']='2'" ,
"/response/result/doc[1]/doc[2]/str[@name='id']='4'" ,
"/response/result/doc[1]/doc[3]/str[@name='id']='6'" };
String test3[] = new String[] {
"//*[@numFound='1']",
"count(/response/result/doc[1]/doc)=2",
"/response/result/doc[1]/doc[1]/str[@name='id']='3'" ,
"/response/result/doc[1]/doc[2]/str[@name='id']='5'"};
assertQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,[child]"), test1);
// shows parentFilter specified (not necessary any more) and also child
assertQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "id, subject,[child childFilter=\"title:foo\"]"), test2);
assertQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "id, subject,[child childFilter=\"title:bar\" limit=2]"), test3);
SolrException e = expectThrows(SolrException.class, () -> {
h.query(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "id, subject,[child parentFilter=\"subject:bleh\" childFilter=\"title:bar\" limit=2]"));
});
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
assertThat(e.getMessage(),
containsString("Parent filter 'QueryBitSetProducer(subject:bleh)' doesn't match any parent documents"));
e = expectThrows(SolrException.class, () -> {
h.query(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "id, subject,[child parentFilter=e childFilter=\"title:bar\" limit=2]"));
});
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
assertThat(e.getMessage(),
containsString("Parent filter 'QueryBitSetProducer(text:e)' doesn't match any parent documents"));
e = expectThrows(SolrException.class, () -> {
h.query(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "id, subject,[child parentFilter=\"\"]"));
});
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, e.code());
assertThat(e.getMessage(), containsString("Invalid Parent filter '', resolves to null"));
}
private void testSubQueryXML() {
String test1[];
{
final String subqueryPath = "/result[@name='children'][@numFound='6']";
test1 = new String[] {
"//*[@numFound='1']",
"/response/result/doc[1]" + subqueryPath + "/doc[1]/str[@name='id']='2'" ,
"/response/result/doc[1]" + subqueryPath + "/doc[2]/str[@name='id']='3'" ,
"/response/result/doc[1]" + subqueryPath + "/doc[3]/str[@name='id']='4'" ,
"/response/result/doc[1]" + subqueryPath + "/doc[4]/str[@name='id']='5'" ,
"/response/result/doc[1]" + subqueryPath + "/doc[5]/str[@name='id']='6'" ,
"/response/result/doc[1]" + subqueryPath + "/doc[6]/str[@name='id']='7'"};
}
assertQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,children:[subquery]",
"children.q","{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
"children.rows","10"),
test1);
String test2[] = new String[] {
"//*[@numFound='1']",
"/response/result/doc[1]/result[@name='children'][@numFound='3']/doc[1]/str[@name='id']='2'" ,
"/response/result/doc[1]/result[@name='children'][@numFound='3']/doc[2]/str[@name='id']='4'" ,
"/response/result/doc[1]/result[@name='children'][@numFound='3']/doc[3]/str[@name='id']='6'" };
assertQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,children:[subquery]",
"children.q","{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
"children.rows","10",
"children.fq","title:foo"
), test2);
String test3[] = new String[] {
"//*[@numFound='1']",
"/response/result/doc[1]/result[@name='children'][@numFound='3']/doc[1]/str[@name='id']='3'" ,
"/response/result/doc[1]/result[@name='children'][@numFound='3']/doc[2]/str[@name='id']='5'" };
assertQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,children:[subquery]",
"children.q","{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
"children.rows","2",
"children.fq","title:bar",
"children.sort","_docid_ asc"
), test3);
}
private void testSubQueryJSON() throws Exception {
String[] test1 = new String[] {
"/response/docs/[0]/children/docs/[0]/id=='2'",
"/response/docs/[0]/children/docs/[1]/id=='3'",
"/response/docs/[0]/children/docs/[2]/id=='4'",
"/response/docs/[0]/children/docs/[3]/id=='5'",
"/response/docs/[0]/children/docs/[4]/id=='6'",
"/response/docs/[0]/children/docs/[5]/id=='7'"
};
String[] test2 = new String[] {
"/response/docs/[0]/children/docs/[0]/id=='2'",
"/response/docs/[0]/children/docs/[1]/id=='4'",
"/response/docs/[0]/children/docs/[2]/id=='6'"
};
String[] test3 = new String[] {
"/response/docs/[0]/children/docs/[0]/id=='3'",
"/response/docs/[0]/children/docs/[1]/id=='5'"
};
assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,children:[subquery]",
"children.q","{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
"children.rows","10"), test1);
assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,children:[subquery]",
"children.q","{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
"children.rows","10",
"children.fq","title:foo"), test2);
assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,children:[subquery]",
"children.q","{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
"children.rows","2",
"children.fq","title:bar",
"children.sort","_docid_ asc"), test3);
}
private void testChildDoctransformerJSON() throws Exception {
String[] test1 = new String[] {
"/response/docs/[0]/_childDocuments_/[0]/id=='2'",
"/response/docs/[0]/_childDocuments_/[1]/id=='3'",
"/response/docs/[0]/_childDocuments_/[2]/id=='4'",
"/response/docs/[0]/_childDocuments_/[3]/id=='5'",
"/response/docs/[0]/_childDocuments_/[4]/id=='6'",
"/response/docs/[0]/_childDocuments_/[5]/id=='7'"
};
String[] test2 = new String[] {
"/response/docs/[0]/_childDocuments_/[0]/id=='2'",
"/response/docs/[0]/_childDocuments_/[1]/id=='4'",
"/response/docs/[0]/_childDocuments_/[2]/id=='6'"
};
String[] test3 = new String[] {
"/response/docs/[0]/_childDocuments_/[0]/id=='3'",
"/response/docs/[0]/_childDocuments_/[1]/id=='5'"
};
assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,[child]"), test1);
assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "id, subject,[child childFilter=\"title:foo\"]"), test2);
assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "id, subject,[child childFilter=\"title:bar\" limit=3]"), test3);
}
private void testChildDocNonStoredDVFields() throws Exception {
String[] test1 = new String[] {
"/response/docs/[0]/_childDocuments_/[0]/intDvoDefault==42",
"/response/docs/[0]/_childDocuments_/[1]/intDvoDefault==42",
"/response/docs/[0]/_childDocuments_/[2]/intDvoDefault==42",
"/response/docs/[0]/_childDocuments_/[3]/intDvoDefault==42",
"/response/docs/[0]/_childDocuments_/[4]/intDvoDefault==42",
"/response/docs/[0]/_childDocuments_/[5]/intDvoDefault==42"
};
String[] test2 = new String[] {
"/response/docs/[0]/_childDocuments_/[0]/intDvoDefault==42",
"/response/docs/[0]/_childDocuments_/[1]/intDvoDefault==42",
"/response/docs/[0]/_childDocuments_/[2]/intDvoDefault==42"
};
String[] test3 = new String[] {
"/response/docs/[0]/_childDocuments_/[0]/intDvoDefault==42",
"/response/docs/[0]/_childDocuments_/[1]/intDvoDefault==42"
};
assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,[child]"), test1);
assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "intDvoDefault, subject,[child childFilter=\"title:foo\"]"), test2);
assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "intDvoDefault, subject,[child childFilter=\"title:bar\" limit=2]"), test3);
}
private void testChildReturnFields() throws Exception {
assertJQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,[child fl=\"intDvoDefault,child_fl:[value v='child_fl_test']\"]"),
"/response/docs/[0]/intDefault==42",
"/response/docs/[0]/_childDocuments_/[0]/intDvoDefault==42",
"/response/docs/[0]/_childDocuments_/[0]/child_fl=='child_fl_test'");
try(SolrQueryRequest req = req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "intDefault,[child fl=\"intDvoDefault, [docid]\"]")) {
BasicResultContext res = (BasicResultContext) h.queryAndResponse("/select", req).getResponse();
Iterator<SolrDocument> docsStreamer = res.getProcessedDocuments();
while (docsStreamer.hasNext()) {
SolrDocument doc = docsStreamer.next();
assertFalse("root docs should not contain fields specified in child return fields", doc.containsKey("intDvoDefault"));
assertTrue("root docs should contain fields specified in query return fields", doc.containsKey("intDefault"));
Collection<SolrDocument> childDocs = doc.getChildDocuments();
for(SolrDocument childDoc: childDocs) {
assertEquals("child doc should only have 2 keys", 2, childDoc.keySet().size());
assertTrue("child docs should contain fields specified in child return fields", childDoc.containsKey("intDvoDefault"));
assertEquals("child docs should contain fields specified in child return fields",
42, childDoc.getFieldValue("intDvoDefault"));
assertTrue("child docs should contain fields specified in child return fields", childDoc.containsKey("[docid]"));
}
}
}
}
private void createSimpleIndex() {
SolrInputDocument parentDocument = new SolrInputDocument();
parentDocument.addField(ID_FIELD, "1");
parentDocument.addField("subject", "parentDocument");
for(int i=0; i< 6; i++) {
SolrInputDocument childDocument = new SolrInputDocument();
childDocument.addField(ID_FIELD, Integer.toString(i+2));
if(i%2==0) {
childDocument.addField("title", "foo");
} else {
childDocument.addField("title", "bar");
}
parentDocument.addChildDocument(childDocument);
}
try {
Long version = addAndGetVersion(parentDocument, null);
assertNotNull(version);
} catch (Exception e) {
fail("Failed to add document to the index");
}
assertU(commit());
assertQ(req("q", "*:*"), "//*[@numFound='" + 7 + "']");
}
private static void createIndex(String[] titleVals) {
String[] parentIDS = new String[] {"1", "4"};
String[] childDocIDS = new String[] {"2", "5"};
String[] grandChildIDS = new String[] {"3", "6"};
for(int i=0; i< parentIDS.length; i++) {
SolrInputDocument parentDocument = new SolrInputDocument();
parentDocument.addField(ID_FIELD, parentIDS[i]);
parentDocument.addField("subject", "parentDocument");
parentDocument.addField("title", titleVals[i]);
SolrInputDocument childDocument = new SolrInputDocument();
childDocument.addField(ID_FIELD, childDocIDS[i]);
childDocument.addField("cat", "childDocument");
childDocument.addField("title", titleVals[i]);
SolrInputDocument grandChildDocument = new SolrInputDocument();
grandChildDocument.addField(ID_FIELD, grandChildIDS[i]);
childDocument.addChildDocument(grandChildDocument);
parentDocument.addChildDocument(childDocument);
try {
Long version = addAndGetVersion(parentDocument, null);
assertNotNull(version);
} catch (Exception e) {
fail("Failed to add document to the index");
}
if (random().nextBoolean()) {
assertU(commit());
}
}
assertU(commit());
assertQ(req("q", "*:*"), "//*[@numFound='" + (parentIDS.length + childDocIDS.length + grandChildIDS.length) + "']");
}
private void testParentFilterJSON() throws Exception {
String[] tests = new String[] {
"/response/docs/[0]/id=='1'",
"/response/docs/[0]/_childDocuments_/[0]/id=='2'",
"/response/docs/[0]/_childDocuments_/[0]/cat=='childDocument'",
"/response/docs/[0]/_childDocuments_/[0]/title=='" + titleVals[0] + "'",
"/response/docs/[1]/id=='4'",
"/response/docs/[1]/_childDocuments_/[0]/id=='5'",
"/response/docs/[1]/_childDocuments_/[0]/cat=='childDocument'",
"/response/docs/[1]/_childDocuments_/[0]/title=='" + titleVals[1] + "'"
};
assertJQ(req("q", "*:*",
"sort", "id asc",
"fq", "subject:\"parentDocument\" ",
"fl", "*,[child childFilter='cat:childDocument' parentFilter=\"subject:parentDocument\"]"),
tests);
assertJQ(req("q", "*:*",
"sort", "id asc",
"fq", "subject:\"parentDocument\" ",
"fl", "id, cat, title, [child childFilter='cat:childDocument' parentFilter=\"subject:parentDocument\"]"),
tests);
// shows if parentFilter matches all docs, then there are effectively no child docs
assertJQ(req("q", "*:*",
"sort", "id asc",
"fq", "subject:\"parentDocument\" ",
"fl", "id,[child childFilter='cat:childDocument' parentFilter=\"*:*\"]"),
"/response==" +
"{'numFound':2,'start':0,'numFoundExact':true,'docs':[{'id':'1'},{'id':'4'}]}");
}
private void testSubQueryParentFilterJSON() throws Exception {
String[] tests = new String[] {
"/response/docs/[0]/id=='1'",
"/response/docs/[0]/children/docs/[0]/id=='2'",
"/response/docs/[0]/children/docs/[0]/cat=='childDocument'",
"/response/docs/[0]/children/docs/[0]/title=='" + titleVals[0] + "'",
"/response/docs/[1]/id=='4'",
"/response/docs/[1]/children/docs/[0]/id=='5'",
"/response/docs/[1]/children/docs/[0]/cat=='childDocument'",
"/response/docs/[1]/children/docs/[0]/title=='" + titleVals[1] + "'"
};
assertJQ(req(
"q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,children:[subquery]",
"sort", "id asc",
"children.q","{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
"children.fq","cat:childDocument",
"children.sort","_docid_ asc"),
tests);
assertJQ(req(
"q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "id,children:[subquery]",
"sort", "id asc",
"children.q","{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
"children.fq","cat:childDocument",
"children.sort","_docid_ asc"),
tests);
}
private void testParentFilterXML() {
String tests[] = new String[] {
"//*[@numFound='2']",
"/response/result/doc[1]/str[@name='id']='1'" ,
"/response/result/doc[1]/doc[1]/str[@name='id']='2'" ,
"/response/result/doc[1]/doc[1]/str[@name='cat']='childDocument'" ,
"/response/result/doc[1]/doc[1]/str[@name='title']='" + titleVals[0] + "'" ,
"/response/result/doc[2]/str[@name='id']='4'" ,
"/response/result/doc[2]/doc[1]/str[@name='id']='5'",
"/response/result/doc[2]/doc[1]/str[@name='cat']='childDocument'",
"/response/result/doc[2]/doc[1]/str[@name='title']='" + titleVals[1] + "'"};
assertQ(req("q", "*:*",
"sort", "id asc",
"fq", "subject:\"parentDocument\" ",
"fl", "*,[child childFilter='cat:childDocument']"),
tests);
assertQ(req("q", "*:*",
"sort", "id asc",
"fq", "subject:\"parentDocument\" ",
"fl", "id, cat, title, [child childFilter='cat:childDocument']"),
tests);
}
private void testSubQueryParentFilterXML() {
String tests[] = new String[] {
"//*[@numFound='2']",
"/response/result/doc[1]/str[@name='id']='1'" ,
"/response/result/doc[1]/result[@name='children'][@numFound=1]/doc[1]/str[@name='id']='2'" ,
"/response/result/doc[1]/result[@name='children'][@numFound=1]/doc[1]/str[@name='cat']='childDocument'" ,
"/response/result/doc[1]/result[@name='children'][@numFound=1]/doc[1]/str[@name='title']='" + titleVals[0] + "'" ,
"/response/result/doc[2]/str[@name='id']='4'" ,
"/response/result/doc[2]/result[@name='children'][@numFound=1]/doc[1]/str[@name='id']='5'",
"/response/result/doc[2]/result[@name='children'][@numFound=1]/doc[1]/str[@name='cat']='childDocument'",
"/response/result/doc[2]/result[@name='children'][@numFound=1]/doc[1]/str[@name='title']='" + titleVals[1] + "'"};
assertQ(req(
"q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "*,children:[subquery]",
"sort", "id asc",
"children.q","{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
"children.fq","cat:childDocument",
"children.sort","_docid_ asc"
),
tests);
assertQ(req("q", "*:*", "fq", "subject:\"parentDocument\" ",
"fl", "id,children:[subquery]",
"sort", "id asc",
"children.q","{!child of=subject:parentDocument}{!terms f=id v=$row.id}",
"children.fq","cat:childDocument",
"children.sort","_docid_ asc"),
tests);
}
}