12.4. Características adicionales #

12.4.1. Manipular documentos
12.4.2. Manipular consultas
12.4.3. Triggers para actualizaciones automáticas
12.4.4. Recopilación de estadísticas de documentos

Esta sección describe funciones y operadores adicionales que son útiles en relación con la búsqueda de texto.

12.4.1. Manipular documentos #

La Section 12.3.1 mostró cómo se pueden convertir los documentos de texto sin procesar en valores de tsvector. PostgreSQL también proporciona funciones y operadores que se pueden utilizar para manipular documentos que ya están en formato tsvector.

tsvector || tsvector

El operador de concatenación de tsvector devuelve un vector que combina los lexemas y la información posicional de los dos vectores proporcionados como argumentos. Las posiciones y las etiquetas de peso se conservan durante la concatenación. Las posiciones que aparecen en el vector de la derecha se desplazan según la posición más alta mencionada en el vector de la izquierda, de modo que el resultado es casi equivalente al resultado de aplicar to_tsvector en la concatenación de las dos cadenas de documentos originales. (La equivalencia no es exacta, porque las palabras vacías eliminadas del final del argumento de la izquierda no afectarán al resultado, mientras que habrían afectado a las posiciones de los lexemas en el argumento de la derecha si se utilizara la concatenación de texto).

Una ventaja de utilizar la concatenación en formato de vector, en lugar de concatenar el texto antes de aplicar to_tsvector, es que se pueden utilizar diferentes configuraciones para analizar diferentes secciones del documento. Además, dado que la función setweight etiqueta todos los lexemas del vector dado de la misma manera, es necesario analizar el texto y hacer setweight antes de concatenar si se desea etiquetar diferentes partes del documento con diferentes pesos.

setweight(vector tsvector, weight "char") returns tsvector

setweight devuelve una copia del vector de entrada en la que cada posición ha sido etiqualizada con el weight dado, ya sea A, B, C o D. (D es el valor por omisión para los nuevos vectores y, como tal, no se muestra en la salida). Estas etiquetas se conservan cuando se concatenan los vectores, lo que permite que las palabras de diferentes partes de un documento sean pesadas de manera diferente por las funciones de clasificación.

Ten en cuenta que las etiquetas de peso se aplican a las posiciones, no a los lexemas. Si al vector de entrada se le han despojado las posiciones, entonces setweight no hace nada.

length(vector tsvector) returns integer

Devuelve el número de lexemas almacenados en el vector.

strip(vector tsvector) returns tsvector

Devuelve un vector que enumera los mismos lexemas que el vector dado, pero carece de cualquier información de posición o peso. El resultado suele ser mucho más pequeño que un vector no despojado, pero también es menos útil. La clasificación de relevancia no funciona tan bien en vectores despojados como en los no despojados. Además, el operador de tsquery <-> (FOLLOWED BY) nunca coincidirá con una entrada despojada, ya que no puede determinar la distancia entre las apariciones de los lexemas.

Una lista completa de las funciones relacionadas con tsvector está disponible en la Table 9.43.

12.4.2. Manipular consultas #

La Section 12.3.2 mostró cómo se pueden convertir las consultas de texto sin procesar en valores de tsquery. La herramienta de búsqueda de texto de PostgreSQL también proporciona funciones y operadores que se pueden utilizar para manipular consultas que ya están en formato tsquery.

tsquery && tsquery

Devuelve la combinación AND de las dos consultas dadas.

tsquery || tsquery

Devuelve la combinación OR de las dos consultas dadas.

!! tsquery

Devuelve la negación (NOT) de la consulta dada.

tsquery <-> tsquery

Devuelve una consulta que busca una coincidencia para la primera consulta dada seguida inmediatamente por una coincidencia para la segunda consulta dada, utilizando el operador de tsquery <-> (FOLLOWED BY). Por ejemplo:

SELECT to_tsquery('fat') <-> to_tsquery('cat | rat');
          ?column?
----------------------------
 'fat' <-> ( 'cat' | 'rat' )

tsquery_phrase(query1 tsquery, query2 tsquery [, distance integer ]) returns tsquery

Devuelve una consulta que busca una coincidencia para la primera consulta dada seguida por una coincidencia para la segunda consulta dada a una distancia de exactamente distance lexemas, utilizando el operador de tsquery <N>. Por ejemplo:

SELECT tsquery_phrase(to_tsquery('fat'), to_tsquery('cat'), 10);
  tsquery_phrase
------------------
 'fat' <10> 'cat'

numnode(query tsquery) returns integer

Devuelve el número de nodos (lexemas más operadores) en un tsquery. Esta función es útil para determinar si la consulta (query) es significativa (devuelve > 0), o si contiene únicamente palabras vacías (stop words) (devuelve 0). Ejemplos:

SELECT numnode(plainto_tsquery('the any'));
NOTICE:  query contains only stopword(s) or doesn't contain lexeme(s), ignored
 numnode
---------
       0

SELECT numnode('foo & bar'::tsquery);
 numnode
---------
       3

querytree(query tsquery) returns text

Devuelve la parte de un tsquery que se puede utilizar para buscar en un índice. Esta función es útil para detectar consultas no indexables, por ejemplo aquellas que contienen solo palabras vacías o solo términos negados. Por ejemplo:

SELECT querytree(to_tsquery('defined'));
 querytree
-----------
 'defin'

SELECT querytree(to_tsquery('!defined'));
 querytree
-----------
 T

12.4.2.1. Reescritura de consultas #

La familia de funciones ts_rewrite busca en un tsquery dado las apariciones de una subconsulta objetivo y reemplaza cada aparición con una subconsulta sustituta. En esencia, esta operación es una versión específica para tsquery del reemplazo de subcadenas. Una combinación de objetivo y sustituto se puede considerar como una regla de reescritura de consulta. Una colección de tales reglas de reescritura puede ser una poderosa ayuda en la búsqueda. Por ejemplo, puedes expandir la búsqueda utilizando sinónimos (por ejemplo, new york, big apple, nyc, gotham) o reducir la búsqueda para dirigir al usuario a algún tema candente. Existe cierto solapamiento en la funcionalidad entre esta característica y los diccionarios de sinónimos (thesaurus) (Section 12.6.4). Sin embargo, puedes modificar un conjunto de reglas de reescritura sobre la marcha sin volver a indexar, mientras que actualizar un diccionario de sinónimos requiere volver a indexar para que sea efectivo.

ts_rewrite (query tsquery, target tsquery, substitute tsquery) returns tsquery

Esta forma de ts_rewrite simplemente aplica una sola regla de reescritura: target se reemplaza por substitute dondequiera que aparezca en query. Por ejemplo:

SELECT ts_rewrite('a & b'::tsquery, 'a'::tsquery, 'c'::tsquery);
 ts_rewrite
------------
 'b' & 'c'

ts_rewrite (query tsquery, select text) returns tsquery

Esta forma de ts_rewrite acepta una query inicial y un comando SQL select, el cual se proporciona como una cadena de texto. El select debe producir dos columnas de tipo tsquery. Para cada fila del resultado del select, las apariciones del valor de la primera columna (el objetivo) se reemplazan por el valor de la segunda columna (el sustituto) dentro del valor de query actual. Por ejemplo:

CREATE TABLE aliases (t tsquery PRIMARY KEY, s tsquery);
INSERT INTO aliases VALUES('a', 'c');

SELECT ts_rewrite('a & b'::tsquery, 'SELECT t,s FROM aliases');
 ts_rewrite
------------
 'b' & 'c'

Ten en cuenta que cuando se aplican múltiples reglas de reescritura de esta manera, el orden de aplicación puede ser importante; por lo que en la práctica querrás que la consulta de origen use ORDER BY con alguna clave de ordenamiento.

Consideremos un ejemplo astronómico de la vida real. Expandiremos la consulta supernovae utilizando reglas de reescritura basadas en tablas:

CREATE TABLE aliases (t tsquery primary key, s tsquery);
INSERT INTO aliases VALUES(to_tsquery('supernovae'), to_tsquery('supernovae|sn'));

SELECT ts_rewrite(to_tsquery('supernovae & crab'), 'SELECT * FROM aliases');
           ts_rewrite
---------------------------------
 'crab' & ( 'supernova' | 'sn' )

Podemos cambiar las reglas de reescritura simplemente actualizando la tabla:

UPDATE aliases
SET s = to_tsquery('supernovae|sn & !nebulae')
WHERE t = to_tsquery('supernovae');

SELECT ts_rewrite(to_tsquery('supernovae & crab'), 'SELECT * FROM aliases');
                 ts_rewrite
---------------------------------------------
 'crab' & ( 'supernova' | 'sn' & !'nebula' )

La reescritura puede ser lenta cuando hay muchas reglas de reescritura, ya que comprueba cada regla para buscar una posible coincidencia. Para filtrar las reglas que obviamente no son candidatas, podemos utilizar los operadores de contención para el tipo tsquery. En el siguiente ejemplo, seleccionamos únicamente aquellas reglas que podrían coincidir con la consulta original:

SELECT ts_rewrite('a & b'::tsquery,
                  'SELECT t,s FROM aliases WHERE ''a & b''::tsquery @> t');
 ts_rewrite
------------
 'b' & 'c'

12.4.3. Triggers para actualizaciones automáticas #

Note

El método descrito en esta sección ha quedado obsoleto por el uso de columnas generadas almacenadas, como se describe en Section 12.2.2.

Cuando utilizas una columna separada para almacenar la representación tsvector de tus documentos, es necesario crear un trigger para actualizar la columna tsvector cuando cambian las columnas de contenido del documento. Hay dos funciones de trigger integradas disponibles para esto, o puedes escribir la tuya propia.

tsvector_update_trigger(tsvector_column_name,​ config_name, text_column_name [, ... ])
tsvector_update_trigger_column(tsvector_column_name,​ config_column_name, text_column_name [, ... ])

Estas funciones de trigger calculan automáticamente una columna tsvector a partir de una o más columnas de texto, bajo el control de los parámetros especificados en el comando CREATE TRIGGER. Un ejemplo de su uso es:

CREATE TABLE messages (
    title       text,
    body        text,
    tsv         tsvector
);

CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
ON messages FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(tsv, 'pg_catalog.english', title, body);

INSERT INTO messages VALUES('title here', 'the body text is here');

SELECT * FROM messages;
   title    |         body          |            tsv
------------+-----------------------+----------------------------
 title here | the body text is here | 'bodi':4 'text':5 'titl':1

SELECT title, body FROM messages WHERE tsv @@ to_tsquery('title & body');
   title    |         body
------------+-----------------------
 title here | the body text is here

Una vez creado este trigger, cualquier cambio en title o body se reflejará automáticamente en tsv, sin que la aplicación tenga que preocuparse por ello.

El primer argumento del trigger debe ser el nombre de la columna tsvector que se va a actualizar. El segundo argumento especifica la configuración de búsqueda de texto que se utilizará para realizar la conversión. Para tsvector_update_trigger, el nombre de la configuración simplemente se proporciona como el segundo argumento del trigger. Debe estar calificado con el esquema, como se muestra arriba, para que el comportamiento del trigger no cambie con los cambios en search_path. Para tsvector_update_trigger_column, el segundo argumento del trigger es el nombre de otra columna de la tabla, que debe ser de tipo regconfig. Esto permite realizar una selección de configuración por fila. Los argumentos restantes son los nombres de las columnas de texto (de tipo text, varchar o char). Estas se incluirán en el documento en el orden proporcionado. Los valores NULL se omitirán (pero las otras columnas se seguirán indexando).

Una limitación de estos triggers integrados es que tratan todas las columnas de entrada por igual. Para procesar las columnas de manera diferente — por ejemplo, para dar un peso diferente al título que al cuerpo — es necesario escribir un trigger personalizado. Aquí tienes un ejemplo utilizando PL/pgSQL como lenguaje del trigger:

CREATE FUNCTION messages_trigger() RETURNS trigger AS $$
begin
  new.tsv :=
     setweight(to_tsvector('pg_catalog.english', coalesce(new.title,'')), 'A') ||
     setweight(to_tsvector('pg_catalog.english', coalesce(new.body,'')), 'D');
  return new;
end
$$ LANGUAGE plpgsql;

CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
    ON messages FOR EACH ROW EXECUTE FUNCTION messages_trigger();

Ten en cuenta que es importante especificar el nombre de la configuración explícitamente cuando creas valores de tsvector dentro de los triggers, de modo que el contenido de la columna no se vea afectado por los cambios en default_text_search_config. No hacer esto probablemente provocará problemas como que los resultados de búsqueda cambien después de un volcado y restauración.

12.4.4. Recopilación de estadísticas de documentos #

La función ts_stat es útil para verificar tu configuración y para encontrar palabras candidatas a palabras vacías (stop-words).

ts_stat(sqlquery text, [ weights text, ]
        OUT word text, OUT ndoc integer,
        OUT nentry integer) returns setof record

sqlquery es un valor de texto que contiene una consulta SQL que debe devolver una sola columna de tipo tsvector. ts_stat ejecuta la consulta y devuelve estadísticas sobre cada lexema (palabra) distinto contenido en los datos tsvector. Las columnas devueltas son:

  • word text — el valor de un lexema

  • ndoc integer — número de documentos (tsvectors) en los que ocurrió la palabra

  • nentry integer — número total de apariciones de la palabra

Si se proporciona weights, solo se cuentan las apariciones que tienen uno de esos pesos.

Por ejemplo, para encontrar las diez palabras más frecuentes en una colección de documentos:

SELECT * FROM ts_stat('SELECT vector FROM apod')
ORDER BY nentry DESC, ndoc DESC, word
LIMIT 10;

Lo mismo, pero contando solo las apariciones de palabras con peso A o B:

SELECT * FROM ts_stat('SELECT vector FROM apod', 'ab')
ORDER BY nentry DESC, ndoc DESC, word
LIMIT 10;