Cuando realizamos búsquedas sobre un tema, muchas veces el contenido de nuestros resultados no tan relevante como deseamos, por lo que debemos repetirla con otras palabras claves para tratar de encontrar la respuesta que esperamos. Una estrategia que existe para enfrentar este problema es la de retroalimentación por relevancia, la cual nos permite incorporar nuevos documentos relacionados con la frase original en una «segunda fase de búsqueda».

En Laravel, podemos utilizar el modo de expansión de consulta que ofrece MySQL para lograr que este feedback sea automático y nos proporcione nueva información importante. Está basado en una idea muy sencilla: extraer de los pocos documentos que coinciden con nuestra búsqueda aquellas palabras con score más altos y después agregarlas a la frase (sin intervención del usuario) para realizar una segunda consulta que abarque más resultados.

El modo de expansión de consulta se especifica en la función AGAINST(), colocando  el modificador WITH QUERY EXPANSION o IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION.

Este modo se considera una ampliación de las búsquedas en lenguaje natural. Así que para ver la diferencia, vamos a realizar una consulta utilizando lenguaje natural y luego la consulta expandida (query expansion).

Lo primero que debemos hacer es cambiar nuestra búsqueda a modo de lenguaje natural:

// routes/web.php

Route::get('/', function () {
    return Chapter::query()
        ->when(request('search'), function ($query, $search) {
            $query->select('id', 'title', 'content')
                ->selectRaw(
                    'match(title,content) against(? in natural language mode) as score',
                    [$search]
                )
                ->whereRaw(
                    'match(title,content) against(? in natural language mode) > 0.0000001',
                    [$search]
                );
        })
        ->get();
});

Ahora, vamos a modificar ChapterSeeder.php, de manera que podamos usarlo para hacer las búsquedas de texto completo. Puedes ver este cambio en el repositorio.

Recuerda ejecutar las migraciones y seeders nuevamente.

Imagina que una persona que está estudiando Laravel por primera vez desea aprender sobre el manejo de bases de datos. Es muy probable que la «frase de búsqueda» que utilice sea base de datos, por lo que realizaría una consulta como esta: http://127.0.0.1:8000/?search=base+de+datos.

La cual devolvería un resultado como el siguiente:

[
  {
    "id": 1,
    "title": "Primeros pasos con bases de datos",
    "content": "El manejo de bases de datos se hace utilizando Eloquent, el constructor de consultas, o SQL puro",
    "score": 1.2110387086868286
  }
]

Observa que solamente obtenemos el primer registro o documento. Sin embargo, si nuestro usuario tiene conocimientos del framework quizás espere registros que traten sobre Eloquent, y al realizar la consulta usando lenguaje natural no podemos ofrecer esa respuesta.

El modo de expansión de consulta nos permite solucionar este detalle, solo debemos modificar la función AGAINST() para agregar el modificador WITH QUERY EXPANSION:

// routes/web.php

Route::get('/', function () {
    return Chapter::query()
        ->when(request('search'), function ($query, $search) {
            $query->select('id', 'title', 'content')
                ->selectRaw(
                    'match(title,content) against(? with query expansion) as score',
                    [$search]
                )
                ->whereRaw(
                    'match(title,content) against(? with query expansion) > 0.0000001',
                    [$search]
                );
        })
        ->get();
});

Ahora, si ejecutamos la consulta anterior nuevamente, obtenemos un resultado más amplio con los registros que son más relevantes:

[
  {
    "id": 1,
    "title": "Primeros pasos con bases de datos",
    "content": "El manejo de bases de datos se hace utilizando Eloquent, el constructor de consultas, o SQL puro",
    "score": 7.05639123916626
  },
  {
    "id": 6,
    "title": "El constructor de consultas",
    "content": "Interface para crear y ejecutar consultas por medio de métodos poderosos que sustituyen los comandos SQL",
    "score": 0.9105787873268127
  },
  {
    "id": 3,
    "title": "Eloquent",
    "content": "Es una interaz active record incluida con Laravel donde un modelo es definido para acceder a una tabla.",
    "score": 0.3182637691497803
  },
  {
    "id": 4,
    "title": "Relationships",
    "content": "Eloquent permite que podamos accesar la información de los vínculos entre las tablas.",
    "score": 0.0906190574169159
  }
]

Las búsquedas en este modo se realizan en dos fases:

La primera consulta se hace en lenguaje natural y trae un solo registro de vuelta, que en nuestro ejemplo es el documento: «Primeros pasos con bases de datos».

Para la segunda iteración, se toma cada palabra que conforma al documento (tanto el título como el contenido). En nuestro caso serían las palabras: «primeros», «pasos», «bases», «datos», «manejo», «hace», «utilizando», «Eloquent», «constructor», «consultas», «SQL» y «puro».

Recuerda que cada palabra tiene un aporte de relevancia para toda la colección.

A continuación, el sistema amplía la frase agregando todas estas palabras y luego realiza una nueva búsqueda de texto completo utilizando el modo de lenguaje natural, para finalmente devolvernos los resultados ordenados por score de relevancia.

Las consultas con expansión del lenguaje natural arrojan un feedback que es automático pues no dependen de la intervención del usuario.

Debes tomar en cuenta que la relevancia de un documento es más que un conteo de palabras. Realmente es mucho más que eso, tiene que ver con una expresión logarítmica basada en la relación que existe entre el total de registros de la tabla y el número de registros donde aparece dicha palabra. De esta manera, la relevancia de una palabra con respecto al registro que la contiene, es el factor resultante anterior multiplicado por el número de veces que aparece la palabra dada.

La sumatoria de las relevancias de todas las palabras de la frase que aparecen dentro de cada registro, es lo que compone la relevancia total de la frase con respecto a ese registro o documento. Este esquema de cálculo ofrece los mejores resultados, aunque la base de datos se haga más grande, lo que es la situación más idónea.

Material relacionado

Regístrate hoy en Styde y obtén acceso a todo nuestro contenido.

Lección anterior Búsquedas de texto completo en modo boolean con Laravel Lección siguiente La sensibilidad a las mayúsculas en las búsquedas de texto completo