From 9f91ba996451c715fc2c7f5f515f96a764670848 Mon Sep 17 00:00:00 2001 From: Yohan Fernando Date: Fri, 2 Feb 2024 21:30:49 +0000 Subject: [PATCH] CAPI-571 Fix SolrJ Bind Nested Child DTO Fields The current solrj Document DTO to SolrInputDocument binder treat all child docs as anonymous rather than prefix them with the given path. This will result in the document being indexed without the `_nest_path_` field populated which is required for the ChildDocumentTransformer (`fl=[child]`) to work. Add a new property to `@Field` interface to determine if a `Field` annoted with `child=true` should be treated as anonymous field or if it should be treated as a nested child document. Where they should be nested child docs, each nested object would be converted to its own `SolrInputDocument` and added as a regular field to the parent `SolrInputDocument`. Having it as a regular field with nested objects will result in Solr indexing these child docs correctly, and it will populate appropriate nested child docs fields during index time. We can now have more that one child documents fields listed in the DTO until they are set and `anonymizeChild=false`. --- .../solrj/beans/DocumentObjectBinder.java | 50 +++++- .../apache/solr/client/solrj/beans/Field.java | 11 ++ .../solrj/beans/TestDocumentObjectBinder.java | 159 +++++++++++++++++- 3 files changed, 214 insertions(+), 6 deletions(-) diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/beans/DocumentObjectBinder.java b/solr/solrj/src/java/org/apache/solr/client/solrj/beans/DocumentObjectBinder.java index ef7f736eff15..c68d78fbf759 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/beans/DocumentObjectBinder.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/beans/DocumentObjectBinder.java @@ -91,8 +91,10 @@ public SolrInputDocument toSolrInputDocument(Object obj) { doc.setField(e.getKey(), e.getValue()); } } else { - if (field.child != null) { - addChild(obj, field, doc); + if (field.child != null && field.annotation.anonymizeChild()) { + addAnonymousChild(obj, field, doc); + } else if (field.child != null) { + addNestedChild(obj, field, doc); } else { doc.setField(field.name, field.get(obj)); } @@ -101,7 +103,7 @@ public SolrInputDocument toSolrInputDocument(Object obj) { return doc; } - private void addChild(Object obj, DocField field, SolrInputDocument doc) { + private void addAnonymousChild(Object obj, DocField field, SolrInputDocument doc) { Object val = field.get(obj); if (val == null) return; if (val instanceof Collection) { @@ -119,6 +121,31 @@ private void addChild(Object obj, DocField field, SolrInputDocument doc) { } } + private void addNestedChild(Object obj, DocField field, SolrInputDocument doc) { + Object val = field.get(obj); + if (val == null) return; + if (val instanceof Collection) { + @SuppressWarnings({"rawtypes"}) + Collection collection = (Collection) val; + List docs = new ArrayList<>(collection.size()); + for (final Object o : collection) { + final SolrInputDocument inpDoc = toSolrInputDocument(o); + docs.add(inpDoc); + } + val = docs; + } else if (val.getClass().isArray()) { + Object[] objs = (Object[]) val; + SolrInputDocument[] docs = (SolrInputDocument[]) Array.newInstance(SolrInputDocument.class, objs.length); + for (int i = 0; i < objs.length; i++) { + docs[i] = toSolrInputDocument(objs[i]); + } + val = docs; + } else { + val = toSolrInputDocument(val); + } + doc.addField(field.name, val); + } + private List getDocFields(@SuppressWarnings({"rawtypes"})Class clazz) { List fields = infocache.get(clazz); if (fields == null) { @@ -146,7 +173,7 @@ private List collectInfo(@SuppressWarnings({"rawtypes"})Class clazz) { if (member.isAnnotationPresent(Field.class)) { AccessController.doPrivileged((PrivilegedAction) () -> { member.setAccessible(true); return null; }); DocField df = new DocField(member); - if (df.child != null) { + if (df.child != null && df.annotation.anonymizeChild()) { if (childFieldFound) throw new BindingException(clazz.getName() + " cannot have more than one Field with child=true"); childFieldFound = true; @@ -334,7 +361,20 @@ private void populateChild(Type typ) { @SuppressWarnings({"unchecked", "rawtypes"}) private Object getFieldValue(SolrDocument solrDocument) { if (child != null) { - List children = solrDocument.getChildDocuments(); + List children = null; + if(solrDocument.hasChildDocuments()){ + children = solrDocument.getChildDocuments(); + } else if (!annotation.anonymizeChild()){ + final Object val = solrDocument.getFieldValue(name); + if(val == null){ + return null; + } else if (isList || isArray) { + children = (List) val; + } else { + children = new ArrayList<>(); + children.add((SolrDocument) val); + } + } if (children == null || children.isEmpty()) return null; if (isList) { ArrayList list = new ArrayList(children.size()); diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/beans/Field.java b/solr/solrj/src/java/org/apache/solr/client/solrj/beans/Field.java index 39f675246058..ddcb0004897a 100644 --- a/solr/solrj/src/java/org/apache/solr/client/solrj/beans/Field.java +++ b/solr/solrj/src/java/org/apache/solr/client/solrj/beans/Field.java @@ -34,5 +34,16 @@ @Retention(RUNTIME) public @interface Field { boolean child() default false; + + /** + * When converting fields with the `@Field(child=true)` annotation, this will + * determine if they should be converted as an anonymized nested child document + * or as a regular (mapped) nested child document. + * + * If intending to use `AtomicUpdates` or `ChildDocumentTransformer`, this + * should be set to `false`. + */ + boolean anonymizeChild() default true; + String value() default DEFAULT; } diff --git a/solr/solrj/src/test/org/apache/solr/client/solrj/beans/TestDocumentObjectBinder.java b/solr/solrj/src/test/org/apache/solr/client/solrj/beans/TestDocumentObjectBinder.java index 4894c7476b9f..dae989f4d8e6 100644 --- a/solr/solrj/src/test/org/apache/solr/client/solrj/beans/TestDocumentObjectBinder.java +++ b/solr/solrj/src/test/org/apache/solr/client/solrj/beans/TestDocumentObjectBinder.java @@ -27,7 +27,9 @@ import org.junit.Test; import java.io.StringReader; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; @@ -151,10 +153,124 @@ public void testChild() throws Exception { assertEquals(arrIn.child[1].name, arrOut.child[1].name); } + public void testMappedChild() throws Exception { + DocumentObjectBinder binder = new DocumentObjectBinder(); + + Child childOne = new Child(); + childOne.id = "1.1"; + childOne.name = "Name One"; + Child childTwo = new Child(); + childTwo.id = "1.2"; + childTwo.name = "Name Two"; + Child childThree = new Child(); + childThree.id = "1.3"; + childThree.name = "Name Three"; + Child childFour = new Child(); + childFour.id = "1.4"; + childFour.name = "Name Four"; + + SingleValueNestedChild nestedNullChildIn = new SingleValueNestedChild(); + nestedNullChildIn.id = "1-null-child"; + nestedNullChildIn.child = null; + SolrInputDocument nestedNullSolrInputDoc = binder.toSolrInputDocument(nestedNullChildIn); + SolrDocument nestedNullSolrDoc = toSolrDocument(nestedNullSolrInputDoc); + assertNull(nestedNullSolrInputDoc.getChildDocuments()); + assertNull(nestedNullSolrDoc.getChildDocuments()); + SingleValueNestedChild nestedNullChildOut = binder.getBean(SingleValueNestedChild.class, nestedNullSolrDoc); + assertEquals(nestedNullChildIn.id, nestedNullChildOut.id); + assertNull(nestedNullChildIn.child); + assertNull(nestedNullChildOut.child); + + SingleValueNestedChild singleNestedIn = new SingleValueNestedChild(); + singleNestedIn.id = "1"; + singleNestedIn.child = childOne; + SolrInputDocument singleNestedSolrInputDoc = binder.toSolrInputDocument(singleNestedIn); + SolrDocument singleNestedSolrDoc = toSolrDocument(singleNestedSolrInputDoc); + assertNull(singleNestedSolrInputDoc.getChildDocuments()); + assertNull(singleNestedSolrDoc.getChildDocuments()); + SingleValueNestedChild singleNestedOut = binder.getBean(SingleValueNestedChild.class, singleNestedSolrDoc); + assertEquals(singleNestedIn.id, singleNestedOut.id); + assertEquals(singleNestedIn.child.id, singleNestedOut.child.id); + assertEquals(singleNestedIn.child.name, singleNestedOut.child.name); + + ListNestedChild listNestedIn = new ListNestedChild(); + listNestedIn.id = "2"; + listNestedIn.child = Arrays.asList(childOne, childTwo); + SolrInputDocument listNestedSolrInputDoc = binder.toSolrInputDocument(listNestedIn); + SolrDocument listNestedSolrDoc = toSolrDocument(listNestedSolrInputDoc); + assertNull(listNestedSolrInputDoc.getChildDocuments()); + assertNull(listNestedSolrDoc.getChildDocuments()); + ListNestedChild listNestedOut = binder.getBean(ListNestedChild.class, listNestedSolrDoc); + assertEquals(listNestedIn.id, listNestedOut.id); + assertEquals(listNestedIn.child.get(0).id, listNestedOut.child.get(0).id); + assertEquals(listNestedIn.child.get(0).name, listNestedOut.child.get(0).name); + assertEquals(listNestedIn.child.get(1).id, listNestedOut.child.get(1).id); + assertEquals(listNestedIn.child.get(1).name, listNestedOut.child.get(1).name); + + ArrayNestedChild arrayNestedIn = new ArrayNestedChild(); + arrayNestedIn.id = "3"; + arrayNestedIn.child = new Child[]{childOne, childTwo}; + SolrInputDocument arrayNestedSolrInputDoc = binder.toSolrInputDocument(arrayNestedIn); + SolrDocument arrayNestedSolrDoc = toSolrDocument(arrayNestedSolrInputDoc); + assertNull(arrayNestedSolrInputDoc.getChildDocuments()); + assertNull(arrayNestedSolrDoc.getChildDocuments()); + ArrayNestedChild arrayNestedOut = binder.getBean(ArrayNestedChild.class, arrayNestedSolrDoc); + assertEquals(arrayNestedIn.id, arrayNestedOut.id); + assertEquals(arrayNestedIn.child[0].id, arrayNestedOut.child[0].id); + assertEquals(arrayNestedIn.child[0].name, arrayNestedOut.child[0].name); + assertEquals(arrayNestedIn.child[1].id, arrayNestedOut.child[1].id); + assertEquals(arrayNestedIn.child[1].name, arrayNestedOut.child[1].name); + + MultipleNestedArrayChild multipleNestedIn = new MultipleNestedArrayChild(); + multipleNestedIn.id = "4"; + multipleNestedIn.favorite = childOne; + multipleNestedIn.bestChildren = new Child[]{childTwo, childThree}; + multipleNestedIn.worstChildren = new Child[]{childFour}; + SolrInputDocument multipleNestedSolrInputDoc = binder.toSolrInputDocument(multipleNestedIn); + SolrDocument multipleNestedSolrDoc = toSolrDocument(multipleNestedSolrInputDoc); + assertNull(multipleNestedSolrInputDoc.getChildDocuments()); + assertNull(multipleNestedSolrDoc.getChildDocuments()); + MultipleNestedArrayChild multipleNestedOut = binder.getBean(MultipleNestedArrayChild.class, multipleNestedSolrDoc); + assertEquals(multipleNestedIn.id, multipleNestedOut.id); + assertEquals(multipleNestedIn.favorite.id, multipleNestedOut.favorite.id); + assertEquals(multipleNestedIn.favorite.name, multipleNestedOut.favorite.name); + assertEquals(multipleNestedIn.bestChildren[0].id, multipleNestedOut.bestChildren[0].id); + assertEquals(multipleNestedIn.bestChildren[0].name, multipleNestedOut.bestChildren[0].name); + assertEquals(multipleNestedIn.bestChildren[1].id, multipleNestedOut.bestChildren[1].id); + assertEquals(multipleNestedIn.bestChildren[1].name, multipleNestedOut.bestChildren[1].name); + assertEquals(multipleNestedIn.worstChildren[0].id, multipleNestedOut.worstChildren[0].id); + assertEquals(multipleNestedIn.worstChildren[0].name, multipleNestedOut.worstChildren[0].name); + } private static SolrDocument toSolrDocument(SolrInputDocument d) { SolrDocument doc = new SolrDocument(); for (SolrInputField field : d) { + if (field.getValue() != null) { + if (field.getValue() instanceof Collection) { + Collection values = (Collection) field.getValue(); + if (!values.isEmpty() && values.iterator().next() instanceof SolrInputDocument) { + List docs = new ArrayList<>(values.size()); + for (Object value : values) { + docs.add(toSolrDocument((SolrInputDocument) value)); + } + doc.setField(field.getName(), docs); + continue; + } + } else if (field.getValue().getClass().isArray()) { + Object[] values = (Object[]) field.getValue(); + if (values.length > 0 && values[0] instanceof SolrInputDocument) { + SolrDocument[] docs = new SolrDocument[values.length]; + for (int i = 0; i < values.length; i++) { + docs[i] = toSolrDocument((SolrInputDocument) values[i]); + } + doc.setField(field.getName(), docs); + continue; + } + } else if (field.getValue() instanceof SolrInputDocument) { + doc.setField(field.getName(), toSolrDocument((SolrInputDocument) field.getValue())); + continue; + } + } doc.setField(field.getName(), field.getValue()); } if (d.getChildDocuments() != null) { @@ -245,7 +361,48 @@ public static class ArrayChild { @Field(child = true) Child[] child; } - + + public static class SingleValueNestedChild { + @Field + String id; + + @Field(child = true, anonymizeChild = false) + Child child; + } + + public static class ListNestedChild { + + @Field + String id; + + @Field(child = true, anonymizeChild = false) + List child; + } + + public static class ArrayNestedChild { + + @Field + String id; + + @Field(child = true, anonymizeChild = false) + Child[] child; + } + + public static class MultipleNestedArrayChild { + + @Field + String id; + + @Field(child = true, anonymizeChild = false) + Child favorite; + + @Field(child = true, anonymizeChild = false) + Child[] bestChildren; + + @Field(child = true, anonymizeChild = false) + Child[] worstChildren; + + } public static class NotGettableItem { @Field