|  | @@ -62,18 +62,23 @@ public class SequenceMatcher {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      /** list of completed sequences - separate to avoid polluting the other stages */
 | 
	
		
			
				|  |  |      private final List<Sequence> completed;
 | 
	
		
			
				|  |  | +    private int completedInsertPosition = 0;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      private final long maxSpanInMillis;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    private final boolean descending;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      private Limit limit;
 | 
	
		
			
				|  |  | -    private boolean limitReached = false;
 | 
	
		
			
				|  |  | +    private boolean headLimit = false;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      private final Stats stats = new Stats();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      @SuppressWarnings("rawtypes")
 | 
	
		
			
				|  |  | -    public SequenceMatcher(int stages, TimeValue maxSpan, Limit limit) {
 | 
	
		
			
				|  |  | +    public SequenceMatcher(int stages, boolean descending, TimeValue maxSpan, Limit limit) {
 | 
	
		
			
				|  |  |          this.numberOfStages = stages;
 | 
	
		
			
				|  |  |          this.completionStage = stages - 1;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +        this.descending = descending;
 | 
	
		
			
				|  |  |          this.stageToKeys = new StageToKeys(completionStage);
 | 
	
		
			
				|  |  |          this.keyToSequences = new KeyToSequences(completionStage);
 | 
	
		
			
				|  |  |          this.completed = new LinkedList<>();
 | 
	
	
		
			
				|  | @@ -84,7 +89,7 @@ public class SequenceMatcher {
 | 
	
		
			
				|  |  |          this.limit = limit;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    public void trackSequence(Sequence sequence) {
 | 
	
		
			
				|  |  | +    private void trackSequence(Sequence sequence) {
 | 
	
		
			
				|  |  |          SequenceKey key = sequence.key();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          stageToKeys.add(0, key);
 | 
	
	
		
			
				|  | @@ -97,29 +102,42 @@ public class SequenceMatcher {
 | 
	
		
			
				|  |  |       * Match hits for the given stage.
 | 
	
		
			
				|  |  |       * Returns false if the process needs to be stopped.
 | 
	
		
			
				|  |  |       */
 | 
	
		
			
				|  |  | -    public boolean match(int stage, Iterable<Tuple<KeyAndOrdinal, HitReference>> hits) {
 | 
	
		
			
				|  |  | +    boolean match(int stage, Iterable<Tuple<KeyAndOrdinal, HitReference>> hits) {
 | 
	
		
			
				|  |  |          for (Tuple<KeyAndOrdinal, HitReference> tuple : hits) {
 | 
	
		
			
				|  |  |              KeyAndOrdinal ko = tuple.v1();
 | 
	
		
			
				|  |  |              HitReference hit = tuple.v2();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              if (stage == 0) {
 | 
	
		
			
				|  |  |                  Sequence seq = new Sequence(ko.key, numberOfStages, ko.ordinal, hit);
 | 
	
		
			
				|  |  | +                // descending queries return descending blocks of ASC data
 | 
	
		
			
				|  |  | +                // to avoid sorting things during insertion,
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |                  trackSequence(seq);
 | 
	
		
			
				|  |  |              } else {
 | 
	
		
			
				|  |  |                  match(stage, ko.key, ko.ordinal, hit);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |                  // early skip in case of reaching the limit
 | 
	
		
			
				|  |  |                  // check the last stage to avoid calling the state machine in other stages
 | 
	
		
			
				|  |  | -                if (reachedLimit()) {
 | 
	
		
			
				|  |  | -                    log.trace("Limit reached {}", stats);
 | 
	
		
			
				|  |  | +                if (headLimit) {
 | 
	
		
			
				|  |  | +                    log.trace("(Head) Limit reached {}", stats);
 | 
	
		
			
				|  |  |                      return false;
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // check tail limit
 | 
	
		
			
				|  |  | +        if (tailLimitReached()) {
 | 
	
		
			
				|  |  | +            log.trace("(Tail) Limit reached {}", stats);
 | 
	
		
			
				|  |  | +            return false;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  |          log.trace("{}", stats);
 | 
	
		
			
				|  |  |          return true;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    private boolean tailLimitReached() {
 | 
	
		
			
				|  |  | +        return limit != null && limit.limit() < 0 && limit.absLimit() <= completed.size();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      /**
 | 
	
		
			
				|  |  |       * Match the given hit (based on key and timestamp and potential tiebreaker) with any potential sequence from the previous
 | 
	
		
			
				|  |  |       * given stage. If that's the case, update the sequence and the rest of the references.
 | 
	
	
		
			
				|  | @@ -175,21 +193,41 @@ public class SequenceMatcher {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          // bump the stages
 | 
	
		
			
				|  |  |          if (stage == completionStage) {
 | 
	
		
			
				|  |  | -            if (limitReached == false) {
 | 
	
		
			
				|  |  | -                completed.add(sequence);
 | 
	
		
			
				|  |  | -                // update the bool lazily
 | 
	
		
			
				|  |  | -                limitReached = limit != null && completed.size() == limit.totalLimit();
 | 
	
		
			
				|  |  | +            // when dealing with descending queries
 | 
	
		
			
				|  |  | +            // avoid duplicate matching (since the ASC query can return previously seen results)
 | 
	
		
			
				|  |  | +            if (descending) {
 | 
	
		
			
				|  |  | +                for (Sequence seen : completed) {
 | 
	
		
			
				|  |  | +                    if (seen.key().equals(key) && seen.ordinal().equals(ordinal)) {
 | 
	
		
			
				|  |  | +                        return;
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            completed.add(completedInsertPosition++, sequence);
 | 
	
		
			
				|  |  | +            // update the bool lazily
 | 
	
		
			
				|  |  | +            // only consider positive limits / negative ones imply tail which means having to go
 | 
	
		
			
				|  |  | +            // through the whole page of results before selecting the last ones
 | 
	
		
			
				|  |  | +            // doing a limit early returns the 'head' not 'tail'
 | 
	
		
			
				|  |  | +            headLimit = limit != null && limit.limit() > 0 && completed.size() == limit.totalLimit();
 | 
	
		
			
				|  |  |          } else {
 | 
	
		
			
				|  |  | +            if (descending) {
 | 
	
		
			
				|  |  | +                // when dealing with descending queries
 | 
	
		
			
				|  |  | +                // avoid duplicate matching (since the ASC query can return previously seen results)
 | 
	
		
			
				|  |  | +                group = keyToSequences.groupIfPresent(stage, key);
 | 
	
		
			
				|  |  | +                if (group != null) {
 | 
	
		
			
				|  |  | +                    for (Ordinal previous : group) {
 | 
	
		
			
				|  |  | +                        if (previous.equals(ordinal)) {
 | 
	
		
			
				|  |  | +                            return;
 | 
	
		
			
				|  |  | +                        }
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              stageToKeys.add(stage, key);
 | 
	
		
			
				|  |  |              keyToSequences.add(stage, sequence);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    public boolean reachedLimit() {
 | 
	
		
			
				|  |  | -        return limitReached;
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |      /**
 | 
	
		
			
				|  |  |       * Checks whether the rest of the stages have in-flight data.
 | 
	
		
			
				|  |  |       * This method is called when a query returns no data meaning
 | 
	
	
		
			
				|  | @@ -197,7 +235,7 @@ public class SequenceMatcher {
 | 
	
		
			
				|  |  |       * However sequences on higher stages can, hence this check to know whether
 | 
	
		
			
				|  |  |       * it's possible to advance the window early.
 | 
	
		
			
				|  |  |       */
 | 
	
		
			
				|  |  | -    public boolean hasCandidates(int stage) {
 | 
	
		
			
				|  |  | +    boolean hasCandidates(int stage) {
 | 
	
		
			
				|  |  |          for (int i = stage; i < completionStage; i++) {
 | 
	
		
			
				|  |  |              if (stageToKeys.isEmpty(i) == false) {
 | 
	
		
			
				|  |  |                  return true;
 | 
	
	
		
			
				|  | @@ -207,18 +245,31 @@ public class SequenceMatcher {
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    public List<Sequence> completed() {
 | 
	
		
			
				|  |  | +    List<Sequence> completed() {
 | 
	
		
			
				|  |  |          return limit != null ? limit.view(completed) : completed;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    public void dropUntil() {
 | 
	
		
			
				|  |  | +    void dropUntil() {
 | 
	
		
			
				|  |  |          keyToSequences.dropUntil();
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    public void until(Iterable<KeyAndOrdinal> markers) {
 | 
	
		
			
				|  |  | +    void until(Iterable<KeyAndOrdinal> markers) {
 | 
	
		
			
				|  |  |          keyToSequences.until(markers);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    void resetInsertPosition() {
 | 
	
		
			
				|  |  | +        // when dealing with descending calls
 | 
	
		
			
				|  |  | +        // update the insert point of all sequences
 | 
	
		
			
				|  |  | +        // for the next batch of hits which will be sorted ascending
 | 
	
		
			
				|  |  | +        // yet will occur _before_ the current batch
 | 
	
		
			
				|  |  | +        if (descending) {
 | 
	
		
			
				|  |  | +            keyToSequences.resetGroupInsertPosition();
 | 
	
		
			
				|  |  | +            keyToSequences.resetUntilInsertPosition();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            completedInsertPosition = 0;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      public Stats stats() {
 | 
	
		
			
				|  |  |          return stats;
 | 
	
		
			
				|  |  |      }
 |