Implementando un motor de búsqueda web con Typesense

Implementando un motor de búsqueda web con Typesense

A medida que un proyecto crece necesita un motor de búsqueda más rápido y potente de lo que es capaz de ofrecer una búsqueda fulltext en base de datos. Así lo hemos hecho con Typesense.

Hace unas cuantas semanas el Project Lead de FormulaTV me dijo que íbamos a sustituir nuestro código de búsqueda de contenidos por una herramienta externa. Y tengo que admitir que suspiré hondo.

Como desarrolladores, la idea de utilizar middleware en general siempre nos presenta un conflicto que resolver, que está hecho de tres dimensiones principalmente.

Optimización, mantenimiento y manpower

La primera es qué va a hacer el componente nuevo con nuestra optimización. FormulaTV, por ejemplo, es una plataforma en la que, en cualquier momento dado, hay varios miles de usuarios simultáneos realizando cientos de miles o incluso millones de peticiones a los diferentes sistemas que la componen. Un solo error o una sola línea de código mal diseñada o ejecutada pueden ralentizar el sistema y en el peor de los casos generar otros errores en cascada que, en último término, siempre se terminan traduciendo en una mala experiencia para el usuario.

La segunda es si el componente nuevo va a traer ventajas que justifiquen el esfuerzo de despliegue. Al fin y al cabo, nuestro código de búsqueda funcionaba, y era estable y rápido, y nuestro manpower es un recurso limitado. Pero nuestro equipo estaba determinado a encontrar una solución a ciertas carencias en cuanto a prioridad de resultados y búsqueda semántica.

La tercera es, por sorprendente que parezca, si el componente nuevo va a traer más problemas que soluciones. Desgraciadamente vivimos en una era en la que estamos acostumbrados a trabajar con software que requiere gran cantidad de mantenimiento únicamente para hacernos elegibles en el estándar que la empresa que lo publica decida establecer, y así acceder a su tráfico o lo que sea que nos ofrece. Todos nos hemos enfrentado a APIs faraónicas, mal documentadas y en cambio constante que descifrar era, sin embargo, imprescindible para nuestro trabajo.

De modo que inmediatamente tuve dudas. Desconfiaba de que existiese una solución externa que...

  • requiriese menos mantenimiento que nuestro código existente
  • teniendo en cuenta el esfuerzo de despliegue, incrementase de forma neta el valor de un sistema que ya era rápido y estable, y
  • que no generase problemas de optimización en una plataforma de tráfico alto

De modo que me sorprendió mucho saber que esa solución existía... y que me había pasado desapercibida durante mucho tiempo. Su nombre es Typesense, un motor de búsqueda open source similar a otras alternativas de pago como Algolia.

Abierto, escalable y sencillo

Typesense se define en su web oficial como un motor de búsqueda de código abierto que ofrece resultados rapidísimos, tolerancia a errores y facilidad de integración, y nuestra experiencia es que cumple en todos esos terrenos. La Fase 1 de despliegue de Typesense en FormulaTV se completó en cuestión de pocas horas, ha mejorado los resultados de búsqueda en nuestra base de datos de forma palpable y no ha tenido ningún efecto negativo en el rendimiento de la plataforma, más al contrario, en todo caso mejorando nuestro código anterior.

Su implementación básica tiene tres partes.

La primera es el propio servidor de búsqueda, que es el que hace el trabajo pesado. Nuestro administrador de sistemas se encontró con multitud de opciones para instalarlo y una documentación clara y actualizada. Existen paquetes DEB y RPM, imágenes Docker y binarios para casi cualquier sistema, y la configuración y arranque son muy sencillos.

La segunda es la gestión de colecciones de contenido, quizá la parte más interesante y la que da a Typesense el tremendo valor que tiene.

Como la mayoría de motores contemporáneos, Typesense no se conecta a nuestra base de datos, sino que ofrece un motor de gestión de contenidos que tendremos que mantener al día por nuestros propios medios. Preparar esta parte es quizá la más laboriosa, pero también la más interesante. Y, de nuevo, una documentación clara, directa al grano y al día nos pone la labor tremendamente fácil.

Un esquema de datos propio

La idea es convertir los datos que queramos que sean indexables en el motor de búsqueda en Colecciones de Documentos, que son más o menos equivalentes a Tablas de Entidades en un diseño de base de datos relacional convencional. Al principio del desarrollo, definimos las colecciones de datos que íbamos a usar y las creamos en el motor.

$client->collections->create($schema);
$schema = [
  'name'      => 'videos',
  'fields'    => [
    [
      'name'  => 'id',
      'type'  => 'int32'
    ],
    [
      'name'  => 'titulo',
      'type'  => 'string'
    ],
    [
      'name'  => 'titulos',
      'type'  => 'string[]'
    ],
    [
      'name'  => 'url',
      'type'  => 'string'
    ],
    [
      'name'  => 'descripcion',
      'type'  => 'string'
    ]
  ]
];

Luego se trata simplemente de mantener las colecciones actualizadas. Podemos usar cualquier método que nos venga bien (lo cual es una constante al trabajar con Typesense, que parece interesado en no interferir en procesos existentes en tu organización). Nosotros tenemos un sistema de robots que realiza gran cantidad de tareas automáticas de forma cíclica, y simplemente añadimos uno.

Typesense expone un método de importación que, dados una colección existente y un archivo .jsonl que contenga una serie de Documentos, actualiza la Colección (insertando y/o actualizando como sea necesario si utilizamos la acción “upsert”) y la pone instantáneamente al día.

{"id": "1", "company_name": "Stark Industries", "num_employees": 5215, "country": "USA"}
{"id": "2", "company_name": "Orbit Inc.", "num_employees": 256, "country": "UK"}
const documentsInJsonl = await fs.readFile("documents.jsonl");
client.collections('companies').documents().import(documentsInJsonl, {action: 'create'});

De modo que nuestro robot simplemente genera archivos .jsonl de nuestros tipos de dato, tomando los registros más recientes, y se los pasa a las colecciones directamente.

while($f = $res->FetchRow()){
        $o = new stdClass();
        $o->id = $f['ID'];
        $o->titulo = $f['Titulo'];
       
        $tmp = array_filter(array(
            $f['TituloPortada'],
            $f['Twitular']
        ));
        $tmp = array_values($tmp);
       
        $o->titulos = $tmp;
        $o->url = $f['URL'];
        $o->descripcion = $f['Descripcion'];
   
        echo "Procesando ".$f['Titulo']."
";
        $cad .= "
    ".json_encode($o);
    }
   
    file_put_contents($ruta, $cad);
    $client->collections['videos']->documents->import(file_get_contents($ruta), ['action' => 'upsert']);

Como se puede observar, aparte de proporcionar un identificador único, Typesense nos permite seleccionar exactamente qué campos y qué estructura proporcionarle de cara al motor de búsqueda semántica. Hay ciertas consideraciones de uso de memoria que tener en cuenta al utilizar campos de texto extensos, pero se salen del alcance de este artículo y de la primera fase de implementación que nosotros queríamos llevar a cabo.

También establecimos un sistema de refresco variable, dado que diferentes tipos de contenidos tienen diferentes frecuencias, y queremos que los resultados estén siempre tan al día como sea posible, sin interferir en los procesos de publicación existentes.

$contenidos = array (
	array("noticias", 60 * 5 + rand(0, 100)),
	array("videos", 60 * 15 + rand(0, 100)),            
	array("series", 60 * 60 + rand(0, 1000)),
	array("telenovelas", 60 * 60 * 2 + rand(0, 1000)),
	array("tvmovies", 60 * 60 * 6 + rand(0, 3600)),
	array("programas", 60 * 60 + rand(0, 3600)),
	array("personajes", 60 * 60 + rand(0, 3600))
);
foreach($contenidos as $c){
    if(time() - filemtime($dir.$c[0].".jsonl") > $c[1]){
        refrescar($c[0]);
    }
}

(Para la Fase 2, que ya ha pasado la etapa de diseño, uno de los pasos fundamentales será implementar una política más granular, que modifique las colecciones Typesense en tiempo real de forma síncrona con el trabajo de nuestros redactores de contenidos en la propia base de datos)

Con las colecciones al día, solo nos falta el tercer componente, el cliente, que es muy sencillo de ejecutar una vez instalado el paquete via composer y tras sanear el input del usuario.

use TypesenseClient;
$client = new Client(
  [
    'api_key' => '############################################',
    'nodes'   => [['host' => '#########', 'port' => '####', 'protocol' => 'http']],
  ]
);
$searchParameters = [
  'q'         => $termino,
  'query_by'  => 'titulo, titulos, url'
];
$resultados = array();
if(!isset($ajax) || $ajax == false){
  $resultados['NEWS'] = $client->collections['noticias']->documents->search($searchParameters);
  $resultados['VID'] = $client->collections['videos']->documents->search($searchParameters);  
}

Lo interesante es que, entre otras posibles configuraciones y datos a nuestra disposición, Typesense asigna a cada resultado un valor numérico en función de su nivel de coincidencia. Ordenar todos los resultados, correspondan a la Colección que correspondan, es trivial.

[0] => Array (
   [document] => Array (
      [id] => 168
      [titulo] => Desaparecida
      [titulos] => Array
         ( [0] => Patricia Marcos: Desaparecida )
      [url] => desaparecida
   )
   [highlights] => Array (
      [0] => Array (
         [field] => titulos
         [indices] => Array
            ( 0] => 0 )
         [matched_tokens] => Array
            ( [0] => Array
               ( [0] => Patricia )
            )
         [snippets] => Array
            ( [0] => <mark>Patricia</mark> Marcos: Desaparecida )
      )
   )
   [text_match] => 571754619796834
)

Resultado de la página del nuevo buscador

Resultado de la página del nuevo buscador

Conclusiones

Typesense es sencillo de instalar, adoptar, mantener, expandir (desde el despliegue inicial hemos adaptado el código para servir búsquedas incluso desde plataformas distintas de nuestra arquitectura) y utilizar, probablemente por su filosofía de código abierto y sus prioridades de diseño.

Aunque nos queda mucho camino por recorrer con este software, trabajar para integrarlo en FormulaTV ha sido un placer inesperado que sin duda podré repetir pronto en el resto de nuestras propiedades.

Samuel Samuel

Artículo escrito por

Samuel Aneiros

Desarrollador Senior

Experto en backend y optimización, Samuel lleva 8 años trabajando en nuevas funcionalidades y mantenimiento de las existentes en todas nuestras plataformas desde verano de 2014.