12.1. Introducción #

12.1.1. ¿Qué es un documento?
12.1.2. Coincidencia básica de texto
12.1.3. Configuraciones

La búsqueda en texto completo (o simplemente búsqueda de texto) proporciona la capacidad de identificar documentos en lenguaje natural que satisfacen una consulta, y opcionalmente ordenarlos por relevancia respecto a la consulta. El tipo de búsqueda más común es encontrar todos los documentos que contienen determinados términos de consulta y devolverlos en orden de su similitud con la consulta. Las nociones de consulta y similitud son muy flexibles y dependen de la aplicación específica. La búsqueda más simple considera una consulta como un conjunto de palabras y la similitud como la frecuencia de las palabras de la consulta en el documento.

Los operadores de búsqueda de texto han existido en las bases de datos desde hace años. PostgreSQL tiene los operadores ~, ~*, LIKE e ILIKE para tipos de datos de texto, pero carecen de muchas propiedades esenciales requeridas por los sistemas modernos de información:

La indexación de texto completo permite que los documentos sean preprocesados y que se guarde un índice para búsquedas rápidas posteriores. El preprocesamiento incluye:

Los diccionarios permiten un control detallado sobre cómo se normalizan los tokens. Con los diccionarios adecuados, puedes:

Se proporciona un tipo de datos tsvector para almacenar documentos preprocesados, junto con un tipo tsquery para representar consultas procesadas (Section 8.11). Hay muchas funciones y operadores disponibles para estos tipos de datos (Section 9.13), el más importante de los cuales es el operador de coincidencia @@, que presentamos en la Section 12.1.2. Las búsquedas de texto completo se pueden acelerar utilizando índices (Section 12.9).

12.1.1. ¿Qué es un documento? #

Un documento es la unidad de búsqueda en un sistema de búsqueda en texto completo; por ejemplo, el artículo de una revista o un mensaje de correo electrónico. El motor de búsqueda de texto debe ser capaz de analizar los documentos y almacenar asociaciones de lexemas (palabras clave) con su documento de origen. Más tarde, estas asociaciones se utilizan para buscar documentos que contengan palabras de consulta.

Para las búsquedas en PostgreSQL, un documento suele ser un campo de texto dentro de una fila de una tabla de base de datos, o posiblemente una combinación (concatenación) de tales campos, quizás almacenados en varias tablas u obtenidos dinámicamente. En otras palabras, un documento se puede construir a partir de diferentes partes para la indexación y podría no estar almacenado en ningún lugar como un todo. Por ejemplo:

SELECT title || ' ' ||  author || ' ' ||  abstract || ' ' || body AS document
FROM messages
WHERE mid = 12;

SELECT m.title || ' ' || m.author || ' ' || m.abstract || ' ' || d.body AS document
FROM messages m, docs d
WHERE m.mid = d.did AND m.mid = 12;

Note

En realidad, en estas consultas de ejemplo, se debería usar coalesce para evitar que un solo atributo NULL haga que el resultado de todo el documento sea NULL.

Otra posibilidad es almacenar los documentos como archivos de texto simple en el sistema de archivos. In este caso, la base de datos se puede usar para almacenar el índice de texto completo y ejecutar búsquedas, y se puede usar algún identificador único para recuperar el documento del sistema de archivos. Sin embargo, la recuperación de archivos desde fuera de la base de datos requiere permisos de superusuario o soporte de funciones especiales, por lo que esto suele ser menos conveniente que mantener todos los datos dentro de PostgreSQL. Además, mantener todo dentro de la base de datos permite un acceso sencillo a los metadatos del documento para ayudar en la indexación y visualización.

A efectos de búsqueda de texto, cada documento debe reducirse al formato preprocesado tsvector. La búsqueda y la clasificación se realizan en su totalidad en la representación tsvector de un documento — el texto original solo necesita ser recuperado cuando el documento ha sido seleccionado para mostrarse a un usuario. Por lo tanto, a menudo hablamos del tsvector como si fuera el documento, pero por supuesto es solo una representación compacta del documento completo.

12.1.2. Coincidencia básica de texto #

La búsqueda en texto completo en PostgreSQL se basa en el operador de coincidencia @@, que devuelve true si un tsvector (documento) coincide con un tsquery (consulta). No importa qué tipo de datos se escriba primero:

SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector @@ 'cat & rat'::tsquery;
 ?column?
----------
 t

SELECT 'fat & cow'::tsquery @@ 'a fat cat sat on a mat and ate a fat rat'::tsvector;
 ?column?
----------
 f

Como sugiere el ejemplo anterior, un tsquery no es solo texto sin procesar, como tampoco lo es un tsvector. Un tsquery contiene términos de búsqueda, que deben ser lexemas ya normalizados, y puede combinar múltiples términos usando los operadores AND, OR, NOT y FOLLOWED BY. (Para detalles de sintaxis, consulta la Section 8.11.2). Existen las funciones to_tsquery, plainto_tsquery y phraseto_tsquery que son de gran ayuda para convertir texto escrito por el usuario en un tsquery adecuado, principalmente normalizando las palabras que aparecen en el texto. De manera similar, to_tsvector se utiliza para analizar y normalizar una cadena de documento. Así que en la práctica, una coincidencia de búsqueda de texto se vería más como esto:

SELECT to_tsvector('fat cats ate fat rats') @@ to_tsquery('fat & rat');
 ?column?
----------
 t

Observa que esta coincidencia no tendría éxito si se escribiera como:

SELECT 'fat cats ate fat rats'::tsvector @@ to_tsquery('fat & rat');
 ?column?
----------
 f

ya que aquí no ocurrirá ninguna normalización de la palabra rats. Los elementos de un tsvector son lexemas, que se asumen ya normalizados, por lo que rats no coincide con rat.

El operador @@ también admite la entrada de tipo text, lo que permite omitir la conversión explícita de una cadena de texto a tsvector o tsquery en casos simples. Las variantes disponibles son:

tsvector @@ tsquery
tsquery  @@ tsvector
text @@ tsquery
text @@ text

Las dos primeras de estas ya las vimos. La forma text @@ tsquery es equivalente a to_tsvector(x) @@ y. La forma text @@ text es equivalente a to_tsvector(x) @@ plainto_tsquery(y).

Dentro de un tsquery, el operador & (AND) especifica que ambos argumentos deben aparecer en el documento para tener una coincidencia. De manera similar, el operador | (OR) especifica que debe aparecer al menos uno de sus argumentos, mientras que el operador ! (NOT) especifica que su argumento no debe aparecer para tener una coincidencia. Por ejemplo, la consulta fat & ! rat coincide con documentos que contienen fat pero no rat.

La búsqueda de frases es posible con la ayuda de del operador de tsquery <-> (FOLLOWED BY), que coincide solo si sus argumentos tienen coincidencias que son adyacentes y en el orden dado. Por ejemplo:

SELECT to_tsvector('fatal error') @@ to_tsquery('fatal <-> error');
 ?column?
----------
 t

SELECT to_tsvector('error is not fatal') @@ to_tsquery('fatal <-> error');
 ?column?
----------
 f

Existe una versión más general del operador FOLLOWED BY que tiene la forma <N>, donde N es un entero que representa la diferencia entre las posiciones de los lexemas coincidentes. <1> es lo mismo que <->, mientras que <2> permite que aparezca exactamente otro lexeme entre las coincidencias, y así sucesivamente. La función phraseto_tsquery hace uso de este operador para construir un tsquery que puede coincidir con una frase de varias palabras cuando algunas de las palabras son palabras vacías. Por ejemplo:

SELECT phraseto_tsquery('cats ate rats');
       phraseto_tsquery
-------------------------------
 'cat' <-> 'ate' <-> 'rat'

SELECT phraseto_tsquery('the cats ate the rats');
       phraseto_tsquery
-------------------------------
 'cat' <-> 'ate' <2> 'rat'

Un caso especial que a veces es útil es que se puede usar <0> para requerir que dos patrones coincidan con la misma palabra.

Se pueden usar paréntesis para controlar el anidamiento de los operadores de tsquery. Sin paréntesis, | es el que menos fuerza de unión tiene, luego &, después <->, y ! es el que tiene mayor fuerza de unión.

Vale la pena notar que los operadores AND/OR/NOT significan algo sutilmente diferente cuando están dentro de los argumentos de un operador FOLLOWED BY que cuando no lo están, porque dentro de FOLLOWED BY la posición exacta de la coincidencia es significativa. Por ejemplo, normalmente !x coincide solo con documentos que no contienen x en ninguna parte. Pero !x <-> y coincide con y si no está inmediatamente después de una x; una ocurrencia de x en cualquier otra parte del documento no impide una coincidencia. Otro ejemplo es que x & y normalmente solo requiere que tanto x como y aparezcan en algún lugar del documento, pero (x & y) <-> z requiere que x e y coincidan en el mismo lugar, inmediatamente antes de un z. Por lo tanto, esta consulta se comporta de manera diferente a x <-> z & y <-> z, la cual coincidirá con un documento que contenga dos secuencias separadas x z e y z. (Esta consulta específica es inútil tal como está escrita, ya que x e y no podrían coincidir en el mismo lugar; pero con situaciones más complejas, como patrones de coincidencia de prefijos, una consulta de esta forma podría ser útil).

12.1.3. Configuraciones #

Los anteriores son todos ejemplos simples de búsqueda de texto. Como se mencionó antes, la funcionalidad de búsqueda en texto completo incluye la capacidad de hacer muchas cosas más: omitir la indexación de ciertas palabras (palabras vacías), procesar sinónimos y usar un análisis sofisticado, por ejemplo, analizar basándose en algo más que el espacio en blanco. Esta funcionalidad está controlada por las configuraciones de búsqueda de texto. PostgreSQL viene con configuraciones predefinidas para muchos idiomas, y puedes crear fácilmente tus propias configuraciones. (El comando \dF de psql muestra todas las configuraciones disponibles).

Durante la instalación se selecciona una configuración adecuada y se establece default_text_search_config en consecuencia en postgresql.conf. Si estás utilizando la misma configuración de búsqueda de texto para todo el clúster, puedes usar el valor en postgresql.conf. Para usar diferentes configuraciones en todo el clúster pero la misma configuración dentro de cualquier base de datos, usa ALTER DATABASE ... SET. De lo contrario, puedes establecer default_text_search_config en cada sesión.

Cada función de búsqueda de texto que depende de una configuración tiene un argumento opcional regconfig, de modo que la configuración a usar se puede especificar explícitamente. default_text_search_config se usa solo cuando se omite este argumento.

Para facilitar la creación de configuraciones de búsqueda de texto personalizadas, una configuración se construye a partir de objetos de base de datos más simples. La herramienta de búsqueda de texto de PostgreSQL proporciona cuatro tipos de objetos de base de datos relacionados con la configuración:

  • Los analizadores de búsqueda de texto (parsers) dividen los documentos en tokens y clasifican cada token (por ejemplo, como palabras o números).

  • Los diccionarios de búsqueda de texto convierten los tokens a una forma normalizada y descartan las palabras vacías.

  • Las plantillas de búsqueda de texto (templates) proporcionan las funciones subyacentes de los diccionarios. (Un diccionario simplemente especifica una plantilla y un conjunto de parámetros para la plantilla).

  • Las configuraciones de búsqueda de texto seleccionan un analizador y un conjunto de diccionarios a usar para normalizar los tokens producidos por el analizador.

Los analizadores y las plantillas de búsqueda de texto se construyen a partir de funciones C de bajo nivel; por lo tanto, se requiere capacidad de programación en C para desarrollar nuevos, y privilegios de superusuario para instalar uno en una base de datos. (Hay ejemplos de analizadores y plantillas adicionales en el área contrib/ de la distribución de PostgreSQL). Dado que los diccionarios y las configuraciones solo parametrizan y conectan algunos analizadores y plantillas subyacentes, no se necesita ningún privilegio especial para crear un nuevo diccionario o configuración. Los ejemplos de creación de diccionarios y configuraciones personalizadas aparecen más adelante en este capítulo.