Un motor de sugerencias en PHP

Otro desafío interesante que me tocó encarar junto a mi equipo en el desarrollo de una red social de viajes fue el Sugeridor de Opinables.

Una de las características que tenía el sitio en que estaba trabajando era la posibilidad de que los usuarios dejaran opiniones (o reseñas mejor dicho) sobre lugares que habían visitado (en general hoteles, restaurantes, atractivos, etc…).

En un determinado momento, para el negocio se volvió sumamente importante incrementar la cantidad de reseñas (obviamente debían ser auténticas, no inventadas por robots ni plagiadas de otros sitios).

Una idea que tuvimos junto con el equipo de producto fue la de intentar «adivinar» sobre qué otros lugares podría un usuario dejar su reseña, sabiendo qué lugares había visitado.

Lo que debíamos lograr era algo similar a esto:

Pero había una serie de restricciones que debíamos cumplir:

  • Siempre debía haber una próxima sugerencia para el usuario que acababa de opinar
  • Sólo debían sugerirse opinables de ciertos rubros (Por ejemplo, sólo hoteles y restaurantes y esta regla debía poder alterarse para acompañar las necesidades del negocio)
  • La cantidad de opinables por destino y rubro (en el conjunto inicial) debía poder ser modificable en forma sencilla (es decir configurable)
  • En el caso de que no se tuviera nada de historia (el sitio permitía las opiniones de usuarios no registrados), debía existir un conjunto de opinables general
  • No debían sugerirse empresas sobre las que el usuario ya hubiese opinado (Se buscaba ampliar la cantidad de empresas con al menos 1 opinión)

Todo esto dio lugar a una arquitectura bastante compleja pero sumamente eficaz, basada en estas clases:

SugeridorOpinables
  • CalculadorDestinosSugeridos
    CalculadorOpinablesParticulares
    CalculadorOpinablesGenerales

     

  • SaneadorOpinables
  • Con algunas subclases también para implementar diferentes estrategias de selección de destinos y empresas (Por ejemplo, si el usuario era anónimo o ya tenía algo de historia en el sitio).
  • Una vez que se habían instanciado las clases «auxiliares» según el caso particular, todas se inyectaban como dependencias de la clase principal:
  • $oSugeridor->setCalculadorDestinos( $oCalculadorDestinos );
    $oSugeridor->setCalculadorOpinablesParticulares( $oCalculadorOpinables );
    $oSugeridor->setRubrosParticipantes( $aRubrosSugeribles );
    $oSugeridor->setCalculadorOpinablesGenerales( $oCalculadorOpinablesGenerales );
    $oSugeridor->setSaneador( new SaneadorOpinablesDuplicados() );

    Y la parte más interesante era cómo se encadenaban las funciones de cada una de las clases en algo como esto:

  • /**
     * Obtiene el conjunto inicial del flow y genera los opinables para ser enviados posteriormente por @see self::getProximosNElementos
     * 
     * @return array<empresas> Conjunto inicial de sugerencias generado en base a las estrategias elegidas
     */
    public function getConjuntoInicial()
    {
       $aDestinosSugeridos = $this->getCalculadorDestinos()->getDestinos();
       $oCalculadorOpinablesParticulares = $this->getCalculadorOpinablesParticulares();
       $aRubrosParticipantes = $this->getRubrosParticipantes();
             
       /** En el caso de partir de una review la primera condición debería ser siempre true, la segunda depende de la configuración */
       if ( !empty($aDestinosSugeridos) && !empty($aRubrosParticipantes) && array_sum( $aDestinosSugeridos ) != array_sum( $aRubrosParticipantes ) ) {
          throw new ExcepcionDatoInvalido( 'Las expectativas de proporción de rubros no son compatibles con las de proporción de destinos' );
       }
       
       $oCalculadorOpinablesParticulares->setDestinosSugeridos( $aDestinosSugeridos );
       $oCalculadorOpinablesParticulares->setRubrosParticipantes( $aRubrosParticipantes );
       
       $preOpinablesParticulares = $oCalculadorOpinablesParticulares->getOpinables();
       $opinablesParticularesCalificacionAlta = array();
       $opinablesParticularesCalificacionBaja = array();
       
       foreach($preOpinablesParticulares as $opinablesParticulares)
       {
          $oCalificacionEditorial = $opinablesParticulares->getCalificacionEditorial();
          if( $oCalificacionEditorial && $oCalificacionEditorial['valor'] >= empresas::CALIFICACION_MEDIA ){
             $opinablesParticularesCalificacionAlta[$opinablesParticulares->getKeyValue()] = $opinablesParticulares;
          } else {
             $opinablesParticularesCalificacionBaja[$opinablesParticulares->getKeyValue()] = $opinablesParticulares;
          }
       }
       
       $aOpinablesParticulares = $opinablesParticularesCalificacionAlta + $this->shuffleArray($opinablesParticularesCalificacionBaja);
       
       // Se sanean los opinables según la estrategia enviada desde el llamador
       $oSaneador = $this->getSaneador();
       $oSaneador->setOpinablesGenerales($this->getCalculadorOpinablesGenerales()->getOpinables());
       $oSaneador->setOpinablesParticulares($aOpinablesParticulares);
       $oSaneador->sanear();
       
       $aOpinablesGenerales = $this->shuffleArray( $oSaneador->getOpinablesGenerales() );
       
       $aConjuntoInicial = $this->buildSetInicial(
             $this->getLongitudInicial(), 
             $aDestinosSugeridos, 
             $aRubrosParticipantes, 
             $aOpinablesParticulares, 
             $aOpinablesGenerales
       );
       
       // Se genera un conjunto con la union de los opinables particulares y generales
       $aConjuntoTotal = $aOpinablesParticulares + $aOpinablesGenerales;
    
       // Se eliminan del conjunto total los opinables que entraron en el conjunto inicial
       foreach ( $aConjuntoInicial as $oOpinableInicial ) {
          unset( $aConjuntoTotal[ $oOpinableInicial->getKeyValue() ] );
       }
    
       // Se guarda en el storage el conjunto total para ser accedido posteriormente
       // Solo se guardan los IDs de los opinables
       $this->getStorage()->store( array_map(function($a) { return $a->getKeyValue(); }, $aConjuntoTotal) );
    
       return $aConjuntoInicial;
    }

    (Es difícil entender todo el código sin tener una idea del framework que estábamos usando… desafortunadamente hecho en casa, pero lo importante son las líneas resaltadas).

  • Lo que se ve acá es cómo al tener todo tan desacoplado es muy simple alterar la lógica de una parte del motor sin afectar al resto (Por ejemplo, si cambiara la forma de seleccionar los destinos, la de los rubros o los opinables).

Este es otro ejemplo de aplicación del patrón strategy, en este caso, para una red social de viajes.

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a acceder mercados y clientes más sofisticados y exigentes

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.