You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by mk...@apache.org on 2017/07/03 11:35:05 UTC

lucene-solr:master: LUCENE-7871: fix false positive match in BlockJoinSelector when children have no value.

Repository: lucene-solr
Updated Branches:
  refs/heads/master d070ca605 -> 706d20181


LUCENE-7871: fix false positive match in BlockJoinSelector when children have no value.

- introducing  BlockJoinSelector.wrap methods accepting children as DISI.
- extracting ToParentDocValues


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/706d2018
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/706d2018
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/706d2018

Branch: refs/heads/master
Commit: 706d2018153d907642c77ae3673b99142b0d1734
Parents: d070ca6
Author: Mikhail Khludnev <mk...@apache.org>
Authored: Thu Jun 15 10:50:46 2017 +0300
Committer: Mikhail Khludnev <mk...@apache.org>
Committed: Tue Jun 20 16:31:09 2017 +0300

----------------------------------------------------------------------
 .../lucene/search/join/BlockJoinSelector.java   | 328 +++----------------
 .../lucene/search/join/ToParentDocValues.java   | 296 +++++++++++++++++
 .../search/join/TestBlockJoinSelector.java      |  76 ++++-
 solr/CHANGES.txt                                |   3 +
 4 files changed, 401 insertions(+), 302 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/706d2018/lucene/join/src/java/org/apache/lucene/search/join/BlockJoinSelector.java
----------------------------------------------------------------------
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/BlockJoinSelector.java b/lucene/join/src/java/org/apache/lucene/search/join/BlockJoinSelector.java
index 359b3cb..a81230d 100644
--- a/lucene/join/src/java/org/apache/lucene/search/join/BlockJoinSelector.java
+++ b/lucene/join/src/java/org/apache/lucene/search/join/BlockJoinSelector.java
@@ -16,18 +16,17 @@
  */
 package org.apache.lucene.search.join;
 
-import java.io.IOException;
-
 import org.apache.lucene.index.NumericDocValues;
 import org.apache.lucene.index.SortedDocValues;
 import org.apache.lucene.index.SortedNumericDocValues;
 import org.apache.lucene.index.SortedSetDocValues;
+import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.search.SortField;
 import org.apache.lucene.search.SortedNumericSelector;
 import org.apache.lucene.search.SortedSetSelector;
 import org.apache.lucene.util.BitSet;
+import org.apache.lucene.util.BitSetIterator;
 import org.apache.lucene.util.Bits;
-import org.apache.lucene.util.BytesRef;
 
 /** Select a value from a block of documents.
  *  @lucene.internal */
@@ -78,7 +77,15 @@ public class BlockJoinSelector {
   /** Wraps the provided {@link SortedSetDocValues} in order to only select
    *  one value per parent among its {@code children} using the configured
    *  {@code selection} type. */
+  @Deprecated
   public static SortedDocValues wrap(SortedSetDocValues sortedSet, Type selection, BitSet parents, BitSet children) {
+    return wrap(sortedSet, selection, parents, toIter(children));
+  }
+
+  /** Wraps the provided {@link SortedSetDocValues} in order to only select
+   *  one value per parent among its {@code children} using the configured
+   *  {@code selection} type. */
+  public static SortedDocValues wrap(SortedSetDocValues sortedSet, Type selection, BitSet parents, DocIdSetIterator children) {
     SortedDocValues values;
     switch (selection) {
       case MIN:
@@ -92,162 +99,42 @@ public class BlockJoinSelector {
     }
     return wrap(values, selection, parents, children);
   }
-
+  
   /** Wraps the provided {@link SortedDocValues} in order to only select
    *  one value per parent among its {@code children} using the configured
    *  {@code selection} type. */
+  @Deprecated
   public static SortedDocValues wrap(final SortedDocValues values, Type selection, BitSet parents, BitSet children) {
+    return wrap(values, selection, parents, toIter(children));
+  }
+
+  /** Wraps the provided {@link SortedDocValues} in order to only select
+   *  one value per parent among its {@code children} using the configured
+   *  {@code selection} type. */
+  public static SortedDocValues wrap(final SortedDocValues values, Type selection, BitSet parents, DocIdSetIterator children) {
     if (values.docID() != -1) {
       throw new IllegalArgumentException("values iterator was already consumed: values.docID=" + values.docID());
     }
-    return new SortedDocValues() {
-
-      private int ord = -1;
-      private int docID = -1;
-
-      @Override
-      public int docID() {
-        return docID;
-      }
-
-      @Override
-      public int nextDoc() throws IOException {
-        assert docID != NO_MORE_DOCS;
-        
-        if (values.docID() == -1) {
-          if (values.nextDoc() == NO_MORE_DOCS) {
-            docID = NO_MORE_DOCS;
-            return docID;
-          }
-        }
-
-        if (values.docID() == NO_MORE_DOCS) {
-          docID = NO_MORE_DOCS;
-          return docID;
-        }
-        
-        int nextParentDocID = parents.nextSetBit(values.docID());
-        ord = values.ordValue();
-
-        while (true) {
-          int childDocID = values.nextDoc();
-          assert childDocID != nextParentDocID;
-          if (childDocID > nextParentDocID) {
-            break;
-          }
-          if (children.get(childDocID) == false) {
-            continue;
-          }
-          if (selection == Type.MIN) {
-            ord = Math.min(ord, values.ordValue());
-          } else if (selection == Type.MAX) {
-            ord = Math.max(ord, values.ordValue());
-          } else {
-            throw new AssertionError();
-          }
-        }
-
-        docID = nextParentDocID;
-        return docID;
-      }
-
-      @Override
-      public int advance(int target) throws IOException {
-        if (target >= parents.length()) {
-          docID = NO_MORE_DOCS;
-          return docID;
-        }
-        if (target == 0) {
-          assert docID() == -1;
-          return nextDoc();
-        }
-        int prevParentDocID = parents.prevSetBit(target-1);
-        if (values.docID() <= prevParentDocID) {
-          values.advance(prevParentDocID+1);
-        }
-        return nextDoc();
-      }
-
-      @Override
-      public boolean advanceExact(int targetParentDocID) throws IOException {
-        if (targetParentDocID < docID) {
-          throw new IllegalArgumentException("target must be after the current document: current=" + docID + " target=" + targetParentDocID);
-        }
-        int previousDocId = docID;
-        docID = targetParentDocID;
-        if (targetParentDocID == previousDocId) {
-          return ord != -1;
-        }
-        docID = targetParentDocID;
-        ord = -1;
-        if (parents.get(targetParentDocID) == false) {
-          return false;
-        }
-        int prevParentDocId = docID == 0 ? -1 : parents.prevSetBit(docID - 1);
-        int childDoc = values.docID();
-        if (childDoc <= prevParentDocId) {
-          childDoc = values.advance(prevParentDocId + 1);
-        }
-        if (childDoc >= docID) {
-          return false;
-        }
-        
-        boolean hasValue = false;
-        for (int doc = values.docID(); doc < docID; doc = values.nextDoc()) {
-          if (children.get(doc)) {
-            ord = values.ordValue();
-            hasValue = true;
-            values.nextDoc();
-            break;
-          }
-        }
-        if (hasValue == false) {
-          return false;
-        }
-
-        for (int doc = values.docID(); doc < docID; doc = values.nextDoc()) {
-          if (children.get(doc)) {
-            switch (selection) {
-              case MIN:
-                ord = Math.min(ord, values.ordValue());
-                break;
-              case MAX:
-                ord = Math.max(ord, values.ordValue());
-                break;
-              default:
-                throw new AssertionError();
-            }
-          }
-        }
-        return true;
-      }
-
-      @Override
-      public int ordValue() {
-        return ord;
-      }
-
-      @Override
-      public BytesRef lookupOrd(int ord) throws IOException {
-        return values.lookupOrd(ord);
-      }
-
-      @Override
-      public int getValueCount() {
-        return values.getValueCount();
-      }
-
-      @Override
-      public long cost() {
-        return values.cost();
-      }
-    };
+    return ToParentDocValues.wrap(values, selection, parents, children);
   }
 
   /** Wraps the provided {@link SortedNumericDocValues} in order to only select
    *  one value per parent among its {@code children} using the configured
    *  {@code selection} type. */
+  @Deprecated
   public static NumericDocValues wrap(SortedNumericDocValues sortedNumerics, Type selection, BitSet parents, BitSet children) {
+    return wrap(sortedNumerics, selection, parents, toIter(children));
+  }
+
+  /** creates an interator for the given bitset */
+  protected static BitSetIterator toIter(BitSet children) {
+    return new BitSetIterator(children, 0);
+  }
+  
+  /** Wraps the provided {@link SortedNumericDocValues} in order to only select
+   *  one value per parent among its {@code children} using the configured
+   *  {@code selection} type. */
+  public static NumericDocValues wrap(SortedNumericDocValues sortedNumerics, Type selection, BitSet parents, DocIdSetIterator children) {
     NumericDocValues values;
     switch (selection) {
       case MIN:
@@ -265,145 +152,18 @@ public class BlockJoinSelector {
   /** Wraps the provided {@link NumericDocValues}, iterating over only
    *  child documents, in order to only select one value per parent among
    *  its {@code children} using the configured {@code selection} type. */
+  @Deprecated
   public static NumericDocValues wrap(final NumericDocValues values, Type selection, BitSet parents, BitSet children) {
-    return new NumericDocValues() {
-
-      private int parentDocID = -1;
-      private long value;
-
-      @Override
-      public int nextDoc() throws IOException {
-
-        if (parentDocID == -1) {
-          values.nextDoc();
-        }
-
-        while (true) {
-
-          // TODO: make this crazy loop more efficient
-
-          int childDocID = values.docID();
-          if (childDocID == NO_MORE_DOCS) {
-            parentDocID = NO_MORE_DOCS;
-            return parentDocID;
-          }
-          if (children.get(childDocID) == false) {
-            values.nextDoc();
-            continue;
-          }
-
-          assert parents.get(childDocID) == false;
-        
-          parentDocID = parents.nextSetBit(childDocID);
-          value = values.longValue();
-
-          while (true) {
-            childDocID = values.nextDoc();
-            assert childDocID != parentDocID;
-            if (childDocID > parentDocID) {
-              break;
-            }
-
-            switch (selection) {
-            case MIN:
-              value = Math.min(value, values.longValue());
-              break;
-            case MAX:
-              value = Math.max(value, values.longValue());
-              break;
-            default:
-              throw new AssertionError();
-            }
-          }
-
-          break;
-        }
-
-        return parentDocID;
-      }
-
-      @Override
-      public int advance(int targetParentDocID) throws IOException {
-        if (targetParentDocID <= parentDocID) {
-          throw new IllegalArgumentException("target must be after the current document: current=" + parentDocID + " target=" + targetParentDocID);
-        }
-
-        if (targetParentDocID == 0) {
-          return nextDoc();
-        }
-        
-        int firstChild = parents.prevSetBit(targetParentDocID - 1) + 1;
-        if (values.advance(firstChild) == NO_MORE_DOCS) {
-          parentDocID = NO_MORE_DOCS;
-          return parentDocID;
-        } else {
-          return nextDoc();
-        }
-      }
-
-      @Override
-      public boolean advanceExact(int targetParentDocID) throws IOException {
-        if (targetParentDocID <= parentDocID) {
-          throw new IllegalArgumentException("target must be after the current document: current=" + parentDocID + " target=" + targetParentDocID);
-        }
-        parentDocID = targetParentDocID;
-        if (parents.get(targetParentDocID) == false) {
-          return false;
-        }
-        int prevParentDocId = parentDocID == 0 ? -1 : parents.prevSetBit(parentDocID - 1);
-        int childDoc = values.docID();
-        if (childDoc <= prevParentDocId) {
-          childDoc = values.advance(prevParentDocId + 1);
-        }
-        if (childDoc >= parentDocID) {
-          return false;
-        }
-        
-        boolean hasValue = false;
-        for (int doc = values.docID(); doc < parentDocID; doc = values.nextDoc()) {
-          if (children.get(doc)) {
-            value = values.longValue();
-            hasValue = true;
-            values.nextDoc();
-            break;
-          }
-        }
-        if (hasValue == false) {
-          return false;
-        }
-
-        for (int doc = values.docID(); doc < parentDocID; doc = values.nextDoc()) {
-          if (children.get(doc)) {
-            switch (selection) {
-              case MIN:
-                value = Math.min(value, values.longValue());
-                break;
-              case MAX:
-                value = Math.max(value, values.longValue());
-                break;
-              default:
-                throw new AssertionError();
-            }
-          }
-        }
-        return true;
-      }
-
-      @Override
-      public long longValue() {
-        return value;
-      }
-      
-      @Override
-      public int docID() {
-        return parentDocID;
-      }      
-
-      @Override
-      public long cost() {
-        return values.cost();
-      }      
-    };
+    return wrap(values,selection, parents, toIter(children));
   }
 
+  /** Wraps the provided {@link NumericDocValues}, iterating over only
+   *  child documents, in order to only select one value per parent among
+   *  its {@code children} using the configured {@code selection} type. */
+  public static NumericDocValues wrap(final NumericDocValues values, Type selection, BitSet parents, DocIdSetIterator children) {
+    if (values.docID() != -1) {
+      throw new IllegalArgumentException("values iterator was already consumed: values.docID=" + values.docID());
+    }
+    return ToParentDocValues.wrap(values,selection, parents, children);
+  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/706d2018/lucene/join/src/java/org/apache/lucene/search/join/ToParentDocValues.java
----------------------------------------------------------------------
diff --git a/lucene/join/src/java/org/apache/lucene/search/join/ToParentDocValues.java b/lucene/join/src/java/org/apache/lucene/search/join/ToParentDocValues.java
new file mode 100644
index 0000000..4e227ed
--- /dev/null
+++ b/lucene/join/src/java/org/apache/lucene/search/join/ToParentDocValues.java
@@ -0,0 +1,296 @@
+/*
+ * 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.lucene.search.join;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.apache.lucene.index.NumericDocValues;
+import org.apache.lucene.index.SortedDocValues;
+import org.apache.lucene.search.ConjunctionDISI;
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.search.join.BlockJoinSelector.Type;
+import org.apache.lucene.util.BitSet;
+import org.apache.lucene.util.BytesRef;
+
+class ToParentDocValues extends DocIdSetIterator {
+
+  interface Accumulator{
+    void reset() throws IOException;
+    void increment() throws IOException;
+  }
+
+  private static final class SortedDVs extends SortedDocValues implements Accumulator{
+    private final SortedDocValues values;
+    private final BlockJoinSelector.Type selection;
+    private int ord = -1;
+    private final ToParentDocValues iter;
+  
+    private SortedDVs(SortedDocValues values, BlockJoinSelector.Type selection, BitSet parents, DocIdSetIterator children) {
+      this.values = values;
+      this.selection = selection;
+      this.iter = new ToParentDocValues(values,parents, children, this);
+    }
+  
+    @Override
+    public int docID() {
+      return iter.docID();
+    }
+  
+    @Override
+    public
+    void reset() throws IOException {
+      ord = values.ordValue();
+    }
+  
+    @Override
+    public
+    void increment() throws IOException {
+      if (selection == BlockJoinSelector.Type.MIN) {
+        ord = Math.min(ord, values.ordValue());
+      } else if (selection == BlockJoinSelector.Type.MAX) {
+        ord = Math.max(ord, values.ordValue());
+      } else {
+        throw new AssertionError();
+      }
+    }
+  
+    @Override
+    public int nextDoc() throws IOException {
+      return iter.nextDoc();
+    }
+  
+    @Override
+    public int advance(int target) throws IOException {
+      return iter.advance(target);
+    }
+  
+    @Override
+    public boolean advanceExact(int targetParentDocID) throws IOException {
+      return iter.advanceExact(targetParentDocID);
+    }
+  
+    @Override
+    public int ordValue() {
+      return ord;
+    }
+  
+    @Override
+    public BytesRef lookupOrd(int ord) throws IOException {
+      return values.lookupOrd(ord);
+    }
+  
+    @Override
+    public int getValueCount() {
+      return values.getValueCount();
+    }
+  
+    @Override
+    public long cost() {
+      return values.cost();
+    }
+  }
+
+  static private final class NumDV extends NumericDocValues implements Accumulator{
+    private final NumericDocValues values;
+    private long value;
+    private final BlockJoinSelector.Type selection;
+  
+    private final ToParentDocValues iter;
+  
+    private NumDV(NumericDocValues values, BlockJoinSelector.Type selection, BitSet parents, DocIdSetIterator children) {
+      this.values = values;
+      this.selection = selection;
+      iter = new ToParentDocValues(values, parents, children, this);
+    }
+  
+    @Override
+    public void reset() throws IOException {
+      value = values.longValue();
+    }
+    
+    @Override
+    public void increment() throws IOException {
+      switch (selection) {
+        case MIN:
+          value = Math.min(value, values.longValue());
+          break;
+        case MAX:
+          value = Math.max(value, values.longValue());
+          break;
+        default:
+          throw new AssertionError();
+        }
+    }
+    
+    @Override
+    public int nextDoc() throws IOException {
+      return iter.nextDoc();
+    }
+  
+    @Override
+    public int advance(int targetParentDocID) throws IOException {
+      return iter.advance(targetParentDocID);
+    }
+  
+    @Override
+    public boolean advanceExact(int targetParentDocID) throws IOException {
+      return iter.advanceExact(targetParentDocID);
+    }
+  
+    @Override
+    public long longValue() {
+      return value;
+    }
+  
+    @Override
+    public int docID() {
+      return iter.docID();
+    }
+  
+    @Override
+    public long cost() {
+      return values.cost();
+    }
+  }
+
+  private ToParentDocValues(DocIdSetIterator values, BitSet parents, DocIdSetIterator children, Accumulator collect) {
+    this.parents = parents;
+    childWithValues = ConjunctionDISI.intersectIterators(Arrays.asList(children, values));
+    this.collector = collect;
+  }
+
+  
+  final private BitSet parents;
+  private int docID = -1;
+  final private Accumulator collector;
+  boolean seen=false;
+  private DocIdSetIterator childWithValues;
+
+  @Override
+  public int docID() {
+    return docID;
+  }
+
+  @Override
+  public int nextDoc() throws IOException {
+    assert docID != NO_MORE_DOCS;
+    
+    assert childWithValues.docID()!=docID || docID==-1;
+    if (childWithValues.docID()<docID || docID==-1) {
+      childWithValues.nextDoc();
+    }
+    if (childWithValues.docID() == NO_MORE_DOCS) {
+      docID = NO_MORE_DOCS;
+      return docID;
+    }
+    
+
+    assert parents.get(childWithValues.docID()) == false;
+    
+    int nextParentDocID = parents.nextSetBit(childWithValues.docID());
+    collector.reset();
+    seen=true;
+    
+    while (true) {
+      int childDocID = childWithValues.nextDoc();
+      assert childDocID != nextParentDocID;
+      if (childDocID > nextParentDocID) {
+        break;
+      }
+      collector.increment();
+    }
+
+    docID = nextParentDocID;
+
+    return docID;
+  }
+
+  @Override
+  public int advance(int target) throws IOException {
+    if (target >= parents.length()) {
+      docID = NO_MORE_DOCS;
+      return docID;
+    }
+    if (target == 0) {
+      assert docID() == -1;
+      return nextDoc();
+    }
+    int prevParentDocID = parents.prevSetBit(target-1);
+    if (childWithValues.docID() <= prevParentDocID) {
+      childWithValues.advance(prevParentDocID+1);
+    }
+    return nextDoc();
+  }
+
+  //@Override
+  public boolean advanceExact(int targetParentDocID) throws IOException {
+    if (targetParentDocID < docID) {
+      throw new IllegalArgumentException("target must be after the current document: current=" + docID + " target=" + targetParentDocID);
+    }
+    int previousDocId = docID;
+    docID = targetParentDocID;
+    if (targetParentDocID == previousDocId) {
+      return seen;//ord != -1; rlly???
+    }
+    docID = targetParentDocID;
+    seen =false;
+    //ord = -1;
+    if (parents.get(targetParentDocID) == false) {
+      return false;
+    }
+    int prevParentDocId = docID == 0 ? -1 : parents.prevSetBit(docID - 1);
+    int childDoc = childWithValues.docID();
+    if (childDoc <= prevParentDocId) {
+      childDoc = childWithValues.advance(prevParentDocId + 1);
+    }
+    if (childDoc >= docID) {
+      return false;
+    }
+    
+    if (childWithValues.docID() < docID) {
+      collector.reset();
+      seen=true;
+      childWithValues.nextDoc();
+    }
+    
+    if (seen == false) {
+      return false;
+    }
+
+    for (int doc = childWithValues.docID(); doc < docID; doc = childWithValues.nextDoc()) {
+        collector.increment();
+    }
+    return true;
+  }
+
+  @Override
+  public long cost() {
+    return 0;
+  }
+
+  static NumericDocValues wrap(NumericDocValues values, Type selection, BitSet parents2,
+      DocIdSetIterator children) {
+    return new ToParentDocValues.NumDV(values,selection, parents2, children);
+  }
+
+  static SortedDocValues wrap(SortedDocValues values, Type selection, BitSet parents2,
+      DocIdSetIterator children) {
+    return new ToParentDocValues.SortedDVs(values, selection, parents2, children);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/706d2018/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoinSelector.java
----------------------------------------------------------------------
diff --git a/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoinSelector.java b/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoinSelector.java
index 04cb771..838826f 100644
--- a/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoinSelector.java
+++ b/lucene/join/src/test/org/apache/lucene/search/join/TestBlockJoinSelector.java
@@ -16,20 +16,22 @@
  */
 package org.apache.lucene.search.join;
 
+import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS;
+
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Random;
 
 import org.apache.lucene.index.DocValues;
 import org.apache.lucene.index.NumericDocValues;
 import org.apache.lucene.index.SortedDocValues;
+import org.apache.lucene.search.DocIdSetIterator;
 import org.apache.lucene.util.BitSet;
 import org.apache.lucene.util.Bits;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.FixedBitSet;
 import org.apache.lucene.util.LuceneTestCase;
 
-import static org.apache.lucene.search.DocIdSetIterator.NO_MORE_DOCS;
-
 public class TestBlockJoinSelector extends LuceneTestCase {
 
   public void testDocsWithValue() {
@@ -64,6 +66,48 @@ public class TestBlockJoinSelector extends LuceneTestCase {
     assertFalse(docsWithValue.get(19));
   }
 
+  static void assertNoMoreDoc(DocIdSetIterator sdv, int maxDoc) throws IOException{
+    Random r = random();
+    if(r.nextBoolean()){
+      assertEquals(NO_MORE_DOCS, sdv.nextDoc());
+    } else {
+      if (r.nextBoolean()) {
+        assertEquals(NO_MORE_DOCS,  sdv.advance(sdv.docID()+random().nextInt(maxDoc-sdv.docID())));
+      } else {
+        final int noMatchDoc = sdv.docID()+random().nextInt(maxDoc-sdv.docID()-1)+1;
+        assertFalse(advanceExact(sdv,noMatchDoc));
+        assertEquals(noMatchDoc, sdv.docID());
+        if (r.nextBoolean()){
+          assertEquals(NO_MORE_DOCS, sdv.nextDoc());
+        }
+      }
+    }
+  }
+  
+  static int nextDoc(DocIdSetIterator sdv, int docId) throws IOException{
+    Random r = random();
+    if(r.nextBoolean()){
+      return sdv.nextDoc();
+    } else {
+      if (r.nextBoolean()) {
+        return sdv.advance(sdv.docID()+random().nextInt(docId-sdv.docID()-1)+1);
+      } else {
+        if (r.nextBoolean()){
+          final int noMatchDoc = sdv.docID()+random().nextInt(docId-sdv.docID()-1)+1;
+          assertFalse(advanceExact(sdv,noMatchDoc));
+          assertEquals(noMatchDoc, sdv.docID());
+        }
+        assertTrue(advanceExact(sdv,docId));
+        return sdv.docID();
+      }
+    }
+  }
+  
+  private static boolean advanceExact(DocIdSetIterator sdv, int target) throws IOException {
+    return sdv instanceof SortedDocValues ? ((SortedDocValues) sdv).advanceExact(target)
+        : ((NumericDocValues) sdv).advanceExact(target);
+  }
+
   public void testSortedSelector() throws IOException {
     final BitSet parents = new FixedBitSet(20);
     parents.set(0);
@@ -89,22 +133,18 @@ public class TestBlockJoinSelector extends LuceneTestCase {
     ords[18] = 10;
 
     final SortedDocValues mins = BlockJoinSelector.wrap(DocValues.singleton(new CannedSortedDocValues(ords)), BlockJoinSelector.Type.MIN, parents, children);
-    assertEquals(5, mins.nextDoc());
+    assertEquals(5, nextDoc(mins,5));
     assertEquals(3, mins.ordValue());
-    assertEquals(15, mins.nextDoc());
+    assertEquals(15, nextDoc(mins,15));
     assertEquals(10, mins.ordValue());
-    assertEquals(19, mins.nextDoc());
-    assertEquals(10, mins.ordValue());
-    assertEquals(NO_MORE_DOCS, mins.nextDoc());
+    assertNoMoreDoc(mins, 20);
 
     final SortedDocValues maxs = BlockJoinSelector.wrap(DocValues.singleton(new CannedSortedDocValues(ords)), BlockJoinSelector.Type.MAX, parents, children);
-    assertEquals(5, maxs.nextDoc());
+    assertEquals(5, nextDoc(maxs,5));
     assertEquals(7, maxs.ordValue());
-    assertEquals(15, maxs.nextDoc());
-    assertEquals(10, maxs.ordValue());
-    assertEquals(19, maxs.nextDoc());
+    assertEquals(15, nextDoc(maxs,15));
     assertEquals(10, maxs.ordValue());
-    assertEquals(NO_MORE_DOCS, maxs.nextDoc());
+    assertNoMoreDoc( maxs,20);
   }
 
   private static class CannedSortedDocValues extends SortedDocValues {
@@ -207,18 +247,18 @@ public class TestBlockJoinSelector extends LuceneTestCase {
     longs[18] = 10;
 
     final NumericDocValues mins = BlockJoinSelector.wrap(DocValues.singleton(new CannedNumericDocValues(longs, docsWithValue)), BlockJoinSelector.Type.MIN, parents, children);
-    assertEquals(5, mins.nextDoc());
+    assertEquals(5, nextDoc(mins,5));
     assertEquals(3, mins.longValue());
-    assertEquals(15, mins.nextDoc());
+    assertEquals(15, nextDoc(mins,15));
     assertEquals(10, mins.longValue());
-    assertEquals(NO_MORE_DOCS, mins.nextDoc());
+    assertNoMoreDoc(mins, 20);
 
     final NumericDocValues maxs = BlockJoinSelector.wrap(DocValues.singleton(new CannedNumericDocValues(longs, docsWithValue)), BlockJoinSelector.Type.MAX, parents, children);
-    assertEquals(5, maxs.nextDoc());
+    assertEquals(5, nextDoc(maxs, 5));
     assertEquals(7, maxs.longValue());
-    assertEquals(15, maxs.nextDoc());
+    assertEquals(15, nextDoc(maxs, 15));
     assertEquals(10, maxs.longValue());
-    assertEquals(NO_MORE_DOCS, maxs.nextDoc());
+    assertNoMoreDoc(maxs, 20);
   }
 
   private static class CannedNumericDocValues extends NumericDocValues {

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/706d2018/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index aadc6db..8c06107 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -501,6 +501,9 @@ when using one of Exact*StatsCache (Mikhail Khludnev)
 * SOLR-10910: Clean up a few details left over from pluggable transient core and untangling
   CoreDescriptor/CoreContainer references (Erick Erickson)
 
+* LUCENE-7871: fix false positive match in BlockJoinSelector when children have no value, introducing 
+  wrap methods accepting children as DISI. Extracting ToParentDocValues (Mikhail Khludnev)
+
 Optimizations
 ----------------------
 * SOLR-10634: JSON Facet API: When a field/terms facet will retrieve all buckets (i.e. limit:-1)