El tutorial Jobeet

17.2. Indices

El buscador de Jobeet debe encontrar todas las ofertas de trabajo que coincidan de alguna manera con las palabras clave introducidas por los usuarios. Por ello, antes de poder realizar cualquier búsqueda, es necesario crear los índices con la información de las ofertas de trabajo. En el caso de Jobeet, los índices generados los vamos a guardar en el directorio data/

Zend Lucene incluye dos métodos para obtener un índice dependiendo de si ese índice ya existe o no. Vamos a crear un helper en la clase JobeetJobPeer que devuelve o crea un índice en función de si ya existía o no:

// lib/model/JobeetJobPeer.php
static public function getLuceneIndex()
{
  ProjectConfiguration::registerZend();

  if (file_exists($index = self::getLuceneIndexFile()))
  {
    return Zend_Search_Lucene::open($index);
  }
  else
  {
    return Zend_Search_Lucene::create($index);
  }
}

static public function getLuceneIndexFile()
{
  return sfConfig::get('sf_data_dir').'/job.'.sfConfig::get('sf_environment').'.index';
}

17.2.1. El método save()

Cada vez que creamos, modificamos o borramos una oferta de trabajo, debemos actualizar el índice. Modifica la clase JobeetJob para que se actualice el índice cada vez que guardamos una oferta de trabajo en la base de datos:

// lib/model/JobeetJob.php
public function save(PropelPDO $con = null)
{
  // ...

  $ret = parent::save($con);

  $this->updateLuceneIndex();

  return $ret;
}

A continuación, crea el método updateLuceneIndex() que es realmente el que actualiza el índice:

// lib/model/JobeetJob.php
public function updateLuceneIndex()
{
  $index = JobeetJobPeer::getLuceneIndex();

  // remove existing entries
  foreach ($index->find('pk:'.$this->getId()) as $hit)
  {
    $index->delete($hit->id);
  }

  // don't index expired and non-activated jobs
  if ($this->isExpired() || !$this->getIsActivated())
  {
    return;
  }

  $doc = new Zend_Search_Lucene_Document();

  // store job primary key to identify it in the search results
  $doc->addField(Zend_Search_Lucene_Field::Keyword('pk', $this->getId()));

  // index job fields
  $doc->addField(Zend_Search_Lucene_Field::UnStored('position', $this->getPosition(), 'utf-8'));
  $doc->addField(Zend_Search_Lucene_Field::UnStored('company', $this->getCompany(), 'utf-8'));
  $doc->addField(Zend_Search_Lucene_Field::UnStored('location', $this->getLocation(), 'utf-8'));
  $doc->addField(Zend_Search_Lucene_Field::UnStored('description', $this->getDescription(), 'utf-8'));

  // add job to the index
  $index->addDocument($doc);
  $index->commit();
}

Como Zend Lucene no es capaz de actualizar un registro existente en el índice, primero comprobamos si ya existía esa oferta de trabajo en el índice y en caso afirmativo, la eliminamos antes de volver a añadirla.

Indexar la información de una oferta de trabajo es muy sencillo: guardamos la clave primaria para utilizarla posteriormente en las búsquedas e indexamos el contenido de las columnas de datos principales (position, company, location y description). El contenido de estas columnas se indexa pero no se guarda porque al mostrar los resultados de búsqueda utilizaremos los objetos reales.

17.2.2. Transacciones Propel

¿Qué sucede si surge un problema al indexar una oferta de trabajo o si la oferta no se guarda correctamente en la base de datos? En este caso, tanto Propel como Zend Lucene lanzan una excepción. No obstante, puede suceder que hayamos guardado una oferta de trabajo en la base de datos pero su información no se encuentre en el índice. Para evitar que esto ocurra, vamos a encerrar las dos actualizaciones de datos en una transacción que podremos anular en caso de error:

// lib/model/JobeetJob.php
public function save(PropelPDO $con = null)
{
  // ...

  if (is_null($con))
  {
    $con = Propel::getConnection(JobeetJobPeer::DATABASE_NAME, Propel::CONNECTION_WRITE);
  }

  $con->beginTransaction();
  try
  {
    $ret = parent::save($con);

    $this->updateLuceneIndex();

    $con->commit();

    return $ret;
  }
  catch (Exception $e)
  {
    $con->rollBack();
    throw $e;
  }
}

17.2.3. El método delete()

Además de modificar el método save(), también tenemos que redefinir el método delete() para eliminar del índice el registro de la oferta de trabajo borrada:

// lib/model/JobeetJob.php
public function delete(PropelPDO $con = null)
{
  $index = JobeetJobPeer::getLuceneIndex();

  foreach ($index->find('pk:'.$this->getId()) as $hit)
  {
    $index->delete($hit->id);
  }

  return parent::delete($con);
}

17.2.4. Borrados masivos

Cada vez que utilizas la tarea propel:data-load para cargar la información de los archivos de datos, Symfony borra todos los registros de las ofertas de trabajo en la base de datos con el método JobeetJobPeer::doDeleteAll(). A continuación, redefinimos este comportamiento por defecto para que también borre todo el índice de ofertas de trabajo:

// lib/model/JobeetJobPeer.php
public static function doDeleteAll($con = null)
{
  if (file_exists($index = self::getLuceneIndexFile()))
  {
    sfToolkit::clearDirectory($index);
    rmdir($index);
  }

  return parent::doDeleteAll($con);
}