2323import org .apache .lucene .facet .MultiLongValues ;
2424import org .apache .lucene .facet .MultiLongValuesSource ;
2525import org .apache .lucene .facet .range .LongRange ;
26+ import org .apache .lucene .index .DocValues ;
27+ import org .apache .lucene .index .DocValuesSkipper ;
28+ import org .apache .lucene .index .LeafReaderContext ;
29+ import org .apache .lucene .index .NumericDocValues ;
30+ import org .apache .lucene .index .SortedNumericDocValues ;
2631import org .apache .lucene .sandbox .facet .cutters .FacetCutter ;
2732import org .apache .lucene .sandbox .facet .cutters .LeafFacetCutter ;
2833import org .apache .lucene .search .LongValues ;
@@ -42,6 +47,10 @@ public abstract class LongRangeFacetCutter implements FacetCutter {
4247
4348 // TODO: refactor - weird that we have both multi and single here.
4449 final LongValuesSource singleValues ;
50+
51+ // Field to read a DocValuesSkipper from on the single-valued path, or null when disabled.
52+ final String skipField ;
53+
4554 final LongRangeAndPos [] sortedRanges ;
4655
4756 final int requestedRangeCount ;
@@ -62,32 +71,51 @@ static LongRangeFacetCutter createSingleOrMultiValued(
6271 MultiLongValuesSource longValuesSource ,
6372 LongValuesSource singleLongValuesSource ,
6473 LongRange [] longRanges ) {
74+ return createSingleOrMultiValued (longValuesSource , singleLongValuesSource , longRanges , null );
75+ }
76+
77+ /** Same as above, but uses the {@code skipField} skip index on the single-valued path. */
78+ static LongRangeFacetCutter createSingleOrMultiValued (
79+ MultiLongValuesSource longValuesSource ,
80+ LongValuesSource singleLongValuesSource ,
81+ LongRange [] longRanges ,
82+ String skipField ) {
6583 if (areOverlappingRanges (longRanges )) {
6684 return new OverlappingLongRangeFacetCutter (
67- longValuesSource , singleLongValuesSource , longRanges );
85+ longValuesSource , singleLongValuesSource , longRanges , skipField );
6886 }
6987 return new NonOverlappingLongRangeFacetCutter (
70- longValuesSource , singleLongValuesSource , longRanges );
88+ longValuesSource , singleLongValuesSource , longRanges , skipField );
7189 }
7290
7391 public static LongRangeFacetCutter create (
7492 MultiLongValuesSource longValuesSource , LongRange [] longRanges ) {
75- return createSingleOrMultiValued (longValuesSource , null , longRanges );
93+ return createSingleOrMultiValued (longValuesSource , null , longRanges , null );
94+ }
95+
96+ /** Create {@link FacetCutter} for a long field by name, using its skip index when present. */
97+ public static LongRangeFacetCutter create (String field , LongRange [] longRanges ) {
98+ // Leave the single-valued source null. The skip path reads the field directly, and a
99+ // multi-valued segment must fall back to the multi-valued leaf cutter.
100+ return createSingleOrMultiValued (
101+ MultiLongValuesSource .fromLongField (field ), null , longRanges , field );
76102 }
77103
78104 // caller handles conversion of Doubles and DoubleRange to Long and LongRange
79105 // ranges need not be sorted
80106 LongRangeFacetCutter (
81107 MultiLongValuesSource longValuesSource ,
82108 LongValuesSource singleLongValuesSource ,
83- LongRange [] longRanges ) {
109+ LongRange [] longRanges ,
110+ String skipField ) {
84111 super ();
85112 valuesSource = longValuesSource ;
86113 if (singleLongValuesSource != null ) {
87114 singleValues = singleLongValuesSource ;
88115 } else {
89116 singleValues = MultiLongValuesSource .unwrapSingleton (valuesSource );
90117 }
118+ this .skipField = skipField ;
91119
92120 sortedRanges = new LongRangeAndPos [longRanges .length ];
93121 requestedRangeCount = longRanges .length ;
@@ -124,6 +152,39 @@ public static LongRangeFacetCutter create(
124152 */
125153 abstract List <InclusiveRange > buildElementaryIntervals ();
126154
155+ /**
156+ * Returns the {@link DocValuesSkipper} for {@link #skipField} in this segment. Null when: no skip
157+ * field is configured, the field has no skip index, or some doc in this segment has more than one
158+ * value.
159+ */
160+ final DocValuesSkipper maybeSkipper (LeafReaderContext context ) throws IOException {
161+ if (skipField == null ) {
162+ return null ;
163+ }
164+ SortedNumericDocValues sortedNumeric = DocValues .getSortedNumeric (context .reader (), skipField );
165+ if (DocValues .unwrapSingleton (sortedNumeric ) == null ) {
166+ return null ;
167+ }
168+ return context .reader ().getDocValuesSkipper (skipField );
169+ }
170+
171+ /** Single-valued {@link LongValues} for {@link #skipField} in this segment. */
172+ final LongValues skipFieldValues (LeafReaderContext context ) throws IOException {
173+ NumericDocValues values =
174+ DocValues .unwrapSingleton (DocValues .getSortedNumeric (context .reader (), skipField ));
175+ return new LongValues () {
176+ @ Override
177+ public long longValue () throws IOException {
178+ return values .longValue ();
179+ }
180+
181+ @ Override
182+ public boolean advanceExact (int doc ) throws IOException {
183+ return values .advanceExact (doc );
184+ }
185+ };
186+ }
187+
127188 private static boolean areOverlappingRanges (LongRange [] ranges ) {
128189 if (ranges .length == 0 ) {
129190 return false ;
@@ -252,21 +313,52 @@ abstract static class LongRangeSingleValuedLeafFacetCutter implements LeafFacetC
252313
253314 IntervalTracker requestedIntervalTracker ;
254315
316+ // Skip index for the faceted field, or null when disabled.
317+ private final DocValuesSkipper skipper ;
318+
319+ // Cached decision from advanceSkipper, valid for every doc up to (and including) upToInclusive:
320+ // when upToSameInterval is true, all those docs map to elementary interval upToIntervalOrd.
321+ private int upToInclusive = -1 ;
322+ private boolean upToSameInterval ;
323+ private int upToIntervalOrd ;
324+
255325 LongRangeSingleValuedLeafFacetCutter (LongValues longValues , long [] boundaries , int [] pos ) {
326+ this (longValues , boundaries , pos , null );
327+ }
328+
329+ LongRangeSingleValuedLeafFacetCutter (
330+ LongValues longValues , long [] boundaries , int [] pos , DocValuesSkipper skipper ) {
256331 this .longValues = longValues ;
257332 this .boundaries = boundaries ;
258333 this .pos = pos ;
334+ this .skipper = skipper ;
335+ // The skip path counts a dense block as one value per doc, so it's single-valued only.
336+ assert skipper == null || skipper .maxValueCount () <= 1
337+ : "skip-index fast path requires a single-valued field, got maxValueCount="
338+ + skipper .maxValueCount ();
259339 }
260340
261341 @ Override
262342 public boolean advanceExact (int doc ) throws IOException {
263- if (longValues .advanceExact (doc ) == false ) {
343+ if (skipper != null && doc > upToInclusive ) {
344+ advanceSkipper (doc );
345+ }
346+
347+ int intervalOrd ;
348+ if (upToSameInterval ) {
349+ // We are inside a dense skip block that maps entirely to one elementary interval, so reuse
350+ // the cached ordinal and skip the per-doc value lookup and binary search.
351+ intervalOrd = upToIntervalOrd ;
352+ } else if (longValues .advanceExact (doc )) {
353+ intervalOrd = processValue (longValues .longValue ());
354+ } else {
264355 return false ;
265356 }
357+
266358 if (requestedIntervalTracker != null ) {
267359 requestedIntervalTracker .clear ();
268360 }
269- elementaryIntervalOrd = processValue ( longValues . longValue ()) ;
361+ elementaryIntervalOrd = intervalOrd ;
270362 maybeRollUp (requestedIntervalTracker );
271363 if (requestedIntervalTracker != null ) {
272364 requestedIntervalTracker .freeze ();
@@ -275,6 +367,40 @@ public boolean advanceExact(int doc) throws IOException {
275367 return true ;
276368 }
277369
370+ /** Mirrors {@code HistogramCollector#advanceSkipper}. */
371+ private void advanceSkipper (int doc ) throws IOException {
372+ if (doc > skipper .maxDocID (0 )) {
373+ skipper .advance (doc );
374+ }
375+ upToSameInterval = false ;
376+
377+ if (skipper .minDocID (0 ) > doc ) {
378+ // Corner case which happens if doc doesn't have a value and is between two intervals of the
379+ // skip index. Fall back to per-doc lookups until the next block.
380+ upToInclusive = skipper .minDocID (0 ) - 1 ;
381+ return ;
382+ }
383+
384+ upToInclusive = skipper .maxDocID (0 );
385+ // Now find the highest level where all docs have a value and map to the same interval.
386+ for (int level = 0 ; level < skipper .numLevels (); ++level ) {
387+ int totalDocsAtLevel = skipper .maxDocID (level ) - skipper .minDocID (level ) + 1 ;
388+ if (skipper .docCount (level ) != totalDocsAtLevel ) {
389+ // Some docs at this level have no value, so we can't resolve the whole block at once.
390+ break ;
391+ }
392+ // Long fields store raw values, the skipper's min/max map straight into the boundary space.
393+ int minInterval = processValue (skipper .minValue (level ));
394+ int maxInterval = processValue (skipper .maxValue (level ));
395+ if (minInterval != maxInterval ) {
396+ break ;
397+ }
398+ upToInclusive = skipper .maxDocID (level );
399+ upToSameInterval = true ;
400+ upToIntervalOrd = minInterval ;
401+ }
402+ }
403+
278404 // Returns the value of the interval v belongs or lastIntervalSeen
279405 // if no processing is done, it returns the lastIntervalSeen
280406 private int processValue (long v ) {
0 commit comments