blob: 37051df427d262743e6aeed33b121217ec7a0224 [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.lucene.facet.range;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* This implementation assumes the requested ranges <i>do not</i> overlap. With this assumption,
* we're able to take a simpler approach to accumulating range counts by just binary searching for
* the appropriate range and counting directly as each value comes in.
*/
class ExclusiveLongRangeCounter extends LongRangeCounter {
/** elementary interval boundaries used for efficient counting (bsearch to find interval) */
private final long[] boundaries;
/** original range number each elementary interval corresponds to (index into countBuffer) */
private final int[] rangeNums;
/** number of counted documents that haven't matched any requested ranges */
private int missingCount;
/** whether-or-not the multi-valued doc currently being counted has matched any ranges */
private boolean multiValuedDocMatchedRange;
ExclusiveLongRangeCounter(LongRange[] ranges, int[] countBuffer) {
super(countBuffer);
// Create a copy of the requested ranges, sorted by min, and keeping track of the original
// position:
LongRangeAndPos[] sortedRanges = new LongRangeAndPos[ranges.length];
for (int i = 0; i < ranges.length; i++) {
sortedRanges[i] = new LongRangeAndPos(ranges[i], i);
}
Arrays.sort(sortedRanges, Comparator.comparingLong(r -> r.range.min));
// Create elementary intervals, which include requested ranges and "gaps" in-between:
List<InclusiveRange> elementaryIntervals = buildElementaryIntervals(sortedRanges);
// Keep track of elementary interval boundary ends (for bsearching) along with the requested
// range they map back to (and -1 when they map to a "gap" range):
boundaries = new long[elementaryIntervals.size()];
rangeNums = new int[elementaryIntervals.size()];
Arrays.fill(rangeNums, -1);
int currRange = 0;
for (int i = 0; i < boundaries.length; i++) {
boundaries[i] = elementaryIntervals.get(i).end;
if (currRange < sortedRanges.length) {
LongRangeAndPos curr = sortedRanges[currRange];
if (boundaries[i] == curr.range.max) {
rangeNums[i] = curr.pos;
currRange++;
}
}
}
}
@Override
void startMultiValuedDoc() {
super.startMultiValuedDoc();
multiValuedDocMatchedRange = false;
}
@Override
boolean endMultiValuedDoc() {
return multiValuedDocMatchedRange;
}
@Override
void addSingleValued(long v) {
if (rangeCount() == 0) {
missingCount++;
return;
}
super.addSingleValued(v);
}
@Override
int finish() {
// Nothing much to do in this case since we're able to count directly into the requested
// ranges as we go in this implementation. Just report any missing count:
return missingCount;
}
@Override
protected long[] boundaries() {
return boundaries;
}
@Override
protected void processSingleValuedHit(int elementaryIntervalNum) {
int rangeNum = rangeNums[elementaryIntervalNum];
if (rangeNum != -1) {
// The elementary interval we matched against corresponds to a requested
// range, so increment it:
increment(rangeNum);
} else {
// The matched elementary interval is a "gap" range, so the doc isn't
// present in any requested ranges:
missingCount++;
}
}
@Override
protected void processMultiValuedHit(int elementaryIntervalNum) {
int rangeNum = rangeNums[elementaryIntervalNum];
if (rangeNum != -1) {
// The elementary interval we matched against corresponds to a requested
// range, so increment it. We can do this without fear of double-counting
// since we know the requested ranges don't overlap:
increment(rangeNum);
multiValuedDocMatchedRange = true;
}
}
/**
* Create elementary intervals, which include requested ranges and "gaps" in-between. This logic
* assumes no requested ranges overlap, and that the incoming ranges have already been sorted.
*/
private static List<InclusiveRange> buildElementaryIntervals(LongRangeAndPos[] sortedRanges) {
List<InclusiveRange> elementaryIntervals = new ArrayList<>();
long prev = Long.MIN_VALUE;
for (LongRangeAndPos range : sortedRanges) {
if (range.range.min > prev) {
// add a "gap" range preceding requested range if necessary:
elementaryIntervals.add(new InclusiveRange(prev, range.range.min - 1));
}
// add the requested range:
elementaryIntervals.add(new InclusiveRange(range.range.min, range.range.max));
prev = range.range.max + 1;
}
if (elementaryIntervals.isEmpty() == false) {
long lastEnd = elementaryIntervals.get(elementaryIntervals.size() - 1).end;
if (lastEnd < Long.MAX_VALUE) {
elementaryIntervals.add(new InclusiveRange(lastEnd + 1, Long.MAX_VALUE));
}
} else {
// If no ranges were requested, create a single entry from MIN_VALUE to MAX_VALUE:
elementaryIntervals.add(new InclusiveRange(Long.MIN_VALUE, Long.MAX_VALUE));
}
return elementaryIntervals;
}
/** Simple container for a requested range and its original position */
private static final class LongRangeAndPos {
final LongRange range;
final int pos;
LongRangeAndPos(LongRange range, int pos) {
this.range = range;
this.pos = pos;
}
}
}