/** {@inheritDoc} */
  @Override
  public Recommendations<OUT> doRecommend(IN input, Context<OUT, IN> context) {
    Recommendations<OUT> recommendations = new Recommendations<>();

    for (RecommendationEngine<OUT, IN> engine : engines) {
      if (engine.participationPolicy(context).participate(input, context, recommendations)) {
        recommendations.merge(engine.recommend(input, context));
      }
    }

    removeIrrelevant(input, context, recommendations);

    for (PostProcessor<OUT, IN> postProcessor : postProcessors) {
      postProcessor.postProcess(recommendations, input, context);
    }

    return recommendations;
  }
  /**
   * Remove recommendations that have no chance of making it to the final selection, because their
   * score will always be lower than the score of the last returned recommendation, even after post
   * processing. This is a performance optimisation, so that irrelevant recommendations don't have
   * to be post-processed.
   *
   * @param input input to the recommendation engine. Typically the person or item recommendations
   *     are being computed for.
   * @param context additional information about the recommendation process.
   * @param recommendations computed so far.
   */
  private void removeIrrelevant(
      IN input, Context<OUT, IN> context, Recommendations<OUT> recommendations) {
    float maxRelativeChange = maxRelativeChange(input, context);

    if (Float.POSITIVE_INFINITY == maxRelativeChange) {
      return;
    }

    int i = 0;
    float minScoreInLimit = 0;
    for (Recommendation<OUT> recommendation : recommendations.get(Integer.MAX_VALUE)) {
      if (++i == context.config().limit()) {
        minScoreInLimit = recommendation.getScore().getTotalScore() - maxRelativeChange;
      } else if (i > context.config().limit()
          && recommendation.getScore().getTotalScore() < minScoreInLimit) {
        recommendations.remove(recommendation.getItem());
      }
    }
  }