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:
No hay soporte lingüístico, incluso para el inglés. Las expresiones regulares
no son suficientes porque no pueden manejar fácilmente palabras derivadas, por ejemplo,
satisfies y satisfy (en inglés) o «satisface» y «satisfacer» (en español). Podrías
perder documentos que contienen satisfies, aunque
probablemente te gustaría encontrarlos al buscar
satisfy. Es posible usar OR
para buscar múltiples formas derivadas, pero esto es tedioso y propenso a errores
(algunas palabras pueden tener varios miles de derivados).
No proporcionan una ordenación (clasificación o ranking) de los resultados de búsqueda, lo que los hace ineficaces cuando se encuentran miles de documentos coincidentes.
Suelen ser lentos porque no hay soporte de índices, por lo que deben procesar todos los documentos para cada búsqueda.
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:
Dividir los documentos en tokens. Es útil identificar varias clases de tokens, por ejemplo, números, palabras, palabras complejas, direcciones de correo electrónico, para que puedan ser procesados de manera diferente. En principio, las clases de tokens dependen de la aplicación específica, pero para la mayoría de los propósitos es adecuado usar un conjunto predefinido de clases. PostgreSQL utiliza un analizador (parser) para realizar este paso. Se proporciona un analizador estándar y se pueden crear analizadores personalizados para necesidades específicas.
Convertir tokens en lexemas.
Un lexema es una cadena de texto, al igual que un token, pero ha sido
normalizado para que las diferentes formas de la misma palabra
se hagan iguales. Por ejemplo, la normalización casi siempre incluye la
conversión de letras mayúsculas a minúsculas, y a menudo implica la eliminación
de sufijos (como la s o es en inglés o plurales en español).
Esto permite que las búsquedas encuentren formas variantes de la
misma palabra, sin tener que introducir tediosamente todas las variantes posibles.
Además, este paso suele eliminar las palabras vacías (stop words), que
son palabras tan comunes que resultan inútiles para la búsqueda.
(En resumen, entonces, los tokens son fragmentos crudos del texto del documento, mientras que
los lexemas son palabras que se consideran útiles para la indexación y la búsqueda).
PostgreSQL utiliza diccionarios para
realizar este paso. Se proporcionan varios diccionarios estándar y se pueden
crear otros personalizados para necesidades específicas.
Almacenar documentos preprocesados optimizados para la búsqueda. Por ejemplo, cada documento puede representarse como un array ordenado de lexemas normalizados. Junto con los lexemas, a menudo es conveniente almacenar información posicional para usar en la clasificación por proximidad (proximity ranking), de modo que a un documento que contenga una región más “densa” de palabras de consulta se le asigne una clasificación más alta que a uno con palabras de consulta dispersas.
Los diccionarios permiten un control detallado sobre cómo se normalizan los tokens. Con los diccionarios adecuados, puedes:
Definir palabras vacías (stop words) que no deben ser indexadas.
Asociar sinónimos a una sola palabra usando Ispell.
Asociar frases a una sola palabra usando un tesauro.
Asociar diferentes variaciones de una palabra a una forma canónica usando un diccionario Ispell.
Asociar diferentes variaciones de una palabra a una forma canónica usando las reglas del lematizador Snowball.
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).
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;
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.
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 <,
donde N>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).
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.