docs: def start_greedy_ensemble_search(automated_run, session, path):
      """Starts an automated ensemble search using greedy forward model selection.
  
      The steps for this search are adapted from "Ensemble Selection from Libraries of Models" by
      Caruana.
  
      1. Start with the empty ensemble
  
      2. Add to the ensemble the model in the library that maximizes the ensemmble's
      performance on the error metric.
  
      3. Repeat step 2 for a fixed number of iterations or until all models have been used.
  
      Args:
          automated_run (xcessiv.models.AutomatedRun): Automated run object
  
          session: Valid SQLAlchemy session
  
          path (str, unicode): Path to project folder
      """
      module = functions.import_string_code_as_module(automated_run.source)
      assert module.metric_to_optimize in automated_run.base_learner_origin.metric_generators
  
      best_ensemble = []  # List containing IDs of best performing ensemble for the last round
  
      secondary_learner = automated_run.base_learner_origin.return_estimator()
      secondary_learner.set_params(**module.secondary_learner_hyperparameters)
  
      for i in range(module.max_num_base_learners):
          best_score = -float('inf')  # Best metric for this round (not in total!)
          current_ensemble = best_ensemble[:]  # Shallow copy of best ensemble
          for base_learner in session.query(models.BaseLearner).filter_by(job_status='finished').all():
              if base_learner in current_ensemble:  # Don't append when learner is already in
                  continue
              current_ensemble.append(base_learner)
  
              # Check if our "best ensemble" already exists
              existing_ensemble = session.query(models.StackedEnsemble).\
                  filter_by(base_learner_origin_id=automated_run.base_learner_origin.id,
                            secondary_learner_hyperparameters=secondary_learner.get_params(),
                            base_learner_ids=sorted([bl.id for bl in current_ensemble])).first()
  
              if existing_ensemble and existing_ensemble.job_status == 'finished':
                  score = existing_ensemble.individual_score[module.metric_to_optimize]
  
              elif existing_ensemble and existing_ensemble.job_status != 'finished':
                  eval_stacked_ensemble(existing_ensemble, session, path)
                  score = existing_ensemble.individual_score[module.metric_to_optimize]
  
              else:
                  stacked_ensemble = models.StackedEnsemble(
                      secondary_learner_hyperparameters=secondary_learner.get_params(),
                      base_learners=current_ensemble,
                      base_learner_origin=automated_run.base_learner_origin,
                      job_status='started'
                  )
                  session.add(stacked_ensemble)
                  session.commit()
                  eval_stacked_ensemble(stacked_ensemble, session, path)
                  score = stacked_ensemble.individual_score[module.metric_to_optimize]
  
              score = -score if module.invert_metric else score
  
              if best_score < score:
                  best_score = score
                  best_ensemble = current_ensemble[:]
  
              current_ensemble.pop()