Para implementar la búsqueda en texto completo, debe haber una función para crear un
tsvector a partir de un documento y un tsquery a partir de una
consulta del usuario. Además, necesitamos devolver los resultados en un orden útil, por lo que necesitamos
una función que compare los documentos con respecto a su relevancia con
la consulta. También es importante poder mostrar los resultados de forma agradable.
PostgreSQL proporciona soporte para todas estas
funciones.
PostgreSQL proporciona la
función to_tsvector para convertir un documento al
tipo de datos tsvector.
to_tsvector([configregconfig, ]documenttext) returnstsvector
to_tsvector analiza un documento textual en tokens,
reduce los tokens a lexemas y devuelve un tsvector que
enumera los lexemas junto con sus posiciones en el documento.
El documento se procesa de acuerdo con la configuración de búsqueda de texto
especificada o por omisión.
Aquí hay un ejemplo simple:
SELECT to_tsvector('english', 'a fat cat sat on a mat - it ate a fat rats');
to_tsvector
-----------------------------------------------------
'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4
En el ejemplo anterior vemos que el tsvector resultante no
contiene las palabras a, on o
it, la palabra rats se convirtió en
rat y el signo de puntuación - fue
ignorado.
La función to_tsvector llama internamente a un analizador
que divide el texto del documento en tokens y asigna un tipo a
cada token. Para cada token, se consulta una lista de
diccionarios (Section 12.6),
donde la lista puede variar según el tipo de token. El primer diccionario
que reconoce el token emite uno o más
lexemas normalizados para representar el token. Por ejemplo,
rats se convirtió en rat porque uno de los
diccionarios reconoció que la palabra rats es una forma plural
de rat. Algunas palabras se reconocen como
palabras vacías (Section 12.6.1), lo que
hace que se ignoren, ya que ocurren con demasiada frecuencia para ser útiles en
las búsquedas. En nuestro ejemplo estas son
a, on e it.
Si ningún diccionario de la lista reconoce el token, este también se ignora.
En este ejemplo, eso le sucedió al signo de puntuación -
porque en realidad no hay diccionarios asignados para su tipo de token
(Space symbols), lo que significa que los tokens de espacio nunca se
indexarán. Las elecciones del analizador, los diccionarios y qué tipos de tokens
indexar se determinan mediante la configuración de búsqueda de texto seleccionada (Section 12.7). Es posible tener
muchas configuraciones diferentes en la misma base de datos, y hay
configuraciones predefinidas disponibles para varios idiomas. En nuestro ejemplo
utilizamos la configuración por omisión english para el
idioma inglés.
La función setweight se puede utilizar para etiquetar las
entradas de un tsvector con un peso determinado,
donde un peso es una de las letras A, B,
C o D.
Esto se suele utilizar para marcar las entradas que provienen de
diferentes partes de un documento, como el título frente al cuerpo. Más tarde, esta
información se puede utilizar para clasificar los resultados de la búsqueda.
Debido a que to_tsvector(NULL) devolverá
NULL, se recomienda usar
coalesce siempre que un campo pueda ser nulo.
Aquí está el método recomendado para crear
un tsvector a partir de un documento estructurado:
UPDATE tt SET ti =
setweight(to_tsvector(coalesce(title,'')), 'A') ||
setweight(to_tsvector(coalesce(keyword,'')), 'B') ||
setweight(to_tsvector(coalesce(abstract,'')), 'C') ||
setweight(to_tsvector(coalesce(body,'')), 'D');
Aquí hemos utilizado setweight para etiquetar el origen
de cada lexema en el tsvector final, y luego hemos fusionado
los valores de tsvector etiquetados utilizando el operador de
concatenación de tsvector ||. (La Section 12.4.1 proporciona detalles sobre estas
operaciones).
PostgreSQL proporciona las
funciones to_tsquery,
plainto_tsquery,
phraseto_tsquery y
websearch_to_tsquery para convertir una consulta
al tipo de datos tsquery.
websearch_to_tsquery es una versión simplificada
de to_tsquery con una sintaxis alternativa, similar
a la utilizada por los motores de búsqueda web.
to_tsquery([configregconfig, ]querytexttext) returnstsquery
to_tsquery crea un valor tsquery a partir de
querytext, el cual debe consistir en tokens simples
separados por los operadores de tsquery & (AND),
| (OR), ! (NOT) y
<-> (FOLLOWED BY), opcionalmente agrupados
mediante paréntesis. En otras palabras, la entrada para
to_tsquery ya debe seguir las reglas generales para
la entrada de tsquery, tal como se describe en la Section 8.11.2. La diferencia es que, mientras que la entrada básica de
tsquery toma los tokens de manera literal,
to_tsquery normaliza cada token en un lexema utilizando
la configuración especificada o por omisión, y descarta cualquier token que sea
una palabra vacía según la configuración. Por ejemplo:
SELECT to_tsquery('english', 'The & Fat & Rats');
to_tsquery
---------------
'fat' & 'rat'
Al igual que en la entrada básica de tsquery, se pueden adjuntar pesos a cada
lexema para restringirlo a que solo coincida con lexemas de tsvector de esos
pesos. Por ejemplo:
SELECT to_tsquery('english', 'Fat | Rats:AB');
to_tsquery
------------------
'fat' | 'rat':AB
También se puede adjuntar * a un lexema para especificar una coincidencia de prefijo:
SELECT to_tsquery('supern:*A & star:A*B');
to_tsquery
--------------------------
'supern':*A & 'star':*AB
Tal lexema coincidirá con cualquier palabra en un tsvector que comience
con la cadena de texto dada.
to_tsquery también puede aceptar frases entre comillas
simples. Esto es útil principalmente cuando la configuración incluye un
diccionario de tesauro que puede activarse con tales frases.
En el ejemplo siguiente, un tesauro contiene la regla supernovae
stars : sn:
SELECT to_tsquery('''supernovae stars'' & !crab');
to_tsquery
---------------
'sn' & !'crab'
Sin comillas, to_tsquery generará un error de sintaxis
para los tokens que no estén separados por un operador AND, OR o FOLLOWED BY.
plainto_tsquery([configregconfig, ]querytexttext) returnstsquery
plainto_tsquery transforma el texto sin formato
querytext en un valor tsquery.
El texto se analiza y normaliza de forma similar a como se hace para to_tsvector,
luego se inserta el operador de tsquery & (AND)
entre las palabras que sobreviven.
Ejemplo:
SELECT plainto_tsquery('english', 'The Fat Rats');
plainto_tsquery
-----------------
'fat' & 'rat'
Ten en cuenta que plainto_tsquery no
reconocerá operadores de tsquery, etiquetas de peso
ni etiquetas de coincidencia de prefijo en su entrada:
SELECT plainto_tsquery('english', 'The Fat & Rats:C');
plainto_tsquery
---------------------
'fat' & 'rat' & 'c'
Aquí, se descartó toda la puntuación de entrada.
phraseto_tsquery([configregconfig, ]querytexttext) returnstsquery
phraseto_tsquery se comporta de manera muy similar a
plainto_tsquery, excepto que inserta
el operador <-> (FOLLOWED BY) entre
las palabras supervivientes en lugar del operador & (AND).
Además, las palabras vacías no se descartan simplemente, sino que se tienen en cuenta
insertando operadores < en lugar de
operadores N><->. Esta función es útil
cuando se buscan secuencias exactas de lexemas, ya que los operadores FOLLOWED BY
comprueban el orden de los lexemas y no solo la presencia de todos ellos.
Ejemplo:
SELECT phraseto_tsquery('english', 'The Fat Rats');
phraseto_tsquery
------------------
'fat' <-> 'rat'
Al igual que plainto_tsquery, la función
phraseto_tsquery no
reconocerá operadores de tsquery, etiquetas de peso
ni etiquetas de coincidencia de prefijo en su entrada:
SELECT phraseto_tsquery('english', 'The Fat & Rats:C');
phraseto_tsquery
-----------------------------
'fat' <-> 'rat' <-> 'c'
websearch_to_tsquery([configregconfig, ]querytexttext) returnstsquery
websearch_to_tsquery crea un valor tsquery
a partir de querytext utilizando una sintaxis
alternativa en la que el texto simple sin formato es una consulta válida.
A diferencia de plainto_tsquery
y phraseto_tsquery, también reconoce ciertos
operadores. Además, esta función nunca producirá errores de sintaxis,
lo que permite utilizar la entrada sin procesar suministrada por el usuario para la búsqueda.
Se admite la siguiente sintaxis:
texto sin comillas: el texto que no esté dentro de comillas se
convertirá en términos separados por operadores &, como
si fuera procesado por plainto_tsquery.
"texto entre comillas": el texto dentro de comillas se
convertirá en términos separados por operadores <->,
como si fuera procesado por phraseto_tsquery.
OR: la palabra “or” se convertirá en
el operador |.
-: un guión se convertirá en
el operador !.
Se ignora cualquier otra puntuación. Así que
al igual que plainto_tsquery
y phraseto_tsquery,
la función websearch_to_tsquery no
reconocerá operadores de tsquery, etiquetas de peso ni etiquetas de
coincidencia de prefijo en su entrada.
Ejemplos:
SELECT websearch_to_tsquery('english', 'The fat rats');
websearch_to_tsquery
----------------------
'fat' & 'rat'
(1 row)
SELECT websearch_to_tsquery('english', '"supernovae stars" -crab');
websearch_to_tsquery
----------------------------------
'supernova' <-> 'star' & !'crab'
(1 row)
SELECT websearch_to_tsquery('english', '"sad cat" or "fat rat"');
websearch_to_tsquery
-----------------------------------
'sad' <-> 'cat' | 'fat' <-> 'rat'
(1 row)
SELECT websearch_to_tsquery('english', 'signal -"segmentation fault"');
websearch_to_tsquery
---------------------------------------
'signal' & !( 'segment' <-> 'fault' )
(1 row)
SELECT websearch_to_tsquery('english', '""" )( dummy \\ query <->');
websearch_to_tsquery
----------------------
'dummi' & 'queri'
(1 row)
La clasificación (ranking) intenta medir qué tan relevantes son los documentos para una consulta en particular, de modo que cuando haya muchas coincidencias se puedan mostrar primero las más relevantes. PostgreSQL proporciona dos funciones de clasificación predefinidas, que tienen en cuenta información léxica, de proximidad y estructural; es decir, consideran con qué frecuencia aparecen los términos de la consulta en el documento, qué tan cercanos están los términos entre sí en el documento y qué tan importante es la parte del documento donde ocurren. Sin embargo, el concepto de relevancia es difuso y muy específico de cada aplicación. Las diferentes aplicaciones pueden requerir información adicional para la clasificación, por ejemplo, el tiempo de modificación del documento. Las funciones de clasificación integradas son solo ejemplos. Puedes escribir tus propias funciones de clasificación y/o combinar sus resultados con factores adicionales para adaptarlos a tus necesidades específicas.
Las dos funciones de clasificación disponibles actualmente son:
ts_rank([ weights float4[], ] vector tsvector, query tsquery [, normalization integer ]) returns float4
Clasifica los vectores basándose en la frecuencia de sus lexemas coincidentes.
ts_rank_cd([ weights float4[], ] vector tsvector, query tsquery [, normalization integer ]) returns float4
Esta función calcula la clasificación de densidad de cobertura (cover density)
para el vector de documento y la consulta dados, tal como se describe en
"Relevance Ranking for One to Three Term Queries" de Clarke, Cormack y Tudhope en la revista
"Information Processing and Management", 1999. La densidad de cobertura es similar a la clasificación
ts_rank, excepto que se toma en consideración la proximidad
de los lexemas coincidentes entre sí.
Esta función requiere información posicional de los lexemas para realizar
su cálculo. Por lo tanto, ignora cualquier lexema “despojado” (stripped)
en el tsvector. Si no hay lexemas con información de posición
en la entrada, el resultado será cero. (Consulta la Section 12.4.1 para obtener más información
sobre la función strip y la información posicional
en los tsvector).
Para ambas funciones,
el argumento opcional weights
ofrece la posibilidad de pesar las instancias de palabras con mayor o menor
fuerza según cómo estén etiquetadas. Los arrays de pesos especifican
con qué fuerza pesar cada categoría de palabra, en el orden:
{D-weight, C-weight, B-weight, A-weight}
Si no se proporcionan weights,
entonces se utilizan estos valores por omisión:
{0.1, 0.2, 0.4, 1.0}
Normalmente, los pesos se utilizan para marcar palabras de áreas especiales del documento, como el título o un resumen inicial, de modo que puedan tratarse con mayor o menor importancia que las palabras en el cuerpo del documento.
Dado que un documento más largo tiene una mayor probabilidad de contener un término de consulta,
es razonable tener en cuenta el tamaño del documento, por ejemplo, un documento de cien palabras
con cinco instancias de una palabra de búsqueda es probablemente más relevante
que un documento de mil palabras con cinco instancias. Ambas funciones de clasificación
toman una opción entera de normalization que
especifica si la longitud de un documento debe afectar su clasificación y cómo.
La opción entera controla varios comportamientos, por lo que es una máscara de bits:
puedes especificar uno o más comportamientos utilizando
| (por ejemplo, 2|4).
0 (por omisión) ignora la longitud del documento
1 divide la clasificación por (1 + el logaritmo de la longitud del documento)
2 divide la clasificación por la longitud del documento
4 divide la clasificación por la distancia armónica media entre extensiones
(esto solo lo implementa ts_rank_cd)
8 divide la clasificación por el número de palabras únicas en el documento
16 divide la clasificación por (1 + el logaritmo del número de palabras únicas en el documento)
32 divide la clasificación por sí misma + 1
Si se especifica más de un bit de bandera, las transformaciones se aplican en el orden indicado.
Es importante notar que las funciones de clasificación no utilizan ninguna información
global, por lo que es imposible producir una normalización justa al 1% o
100% como a veces se desea. La opción de normalización 32
(rank/(rank+1)) se puede aplicar para escalar todas las clasificaciones
en el rango de cero a uno, pero por supuesto esto es solo un cambio estético;
no afectará el orden de los resultados de la búsqueda.
Aquí hay un ejemplo que selecciona solo las diez coincidencias con mayor clasificación:
SELECT title, ts_rank_cd(textsearch, query) AS rank
FROM apod, to_tsquery('neutrino|(dark & matter)') query
WHERE query @@ textsearch
ORDER BY rank DESC
LIMIT 10;
title | rank
-----------------------------------------------+----------
Neutrinos in the Sun | 3.1
The Sudbury Neutrino Detector | 2.4
A MACHO View of Galactic Dark Matter | 2.01317
Hot Gas and Dark Matter | 1.91171
The Virgo Cluster: Hot Plasma and Dark Matter | 1.90953
Rafting for Solar Neutrinos | 1.9
NGC 4650A: Strange Galaxy and Dark Matter | 1.85774
Hot Gas and Dark Matter | 1.6123
Ice Fishing for Cosmic Neutrinos | 1.6
Weak Lensing Distorts the Universe | 0.818218
Este es el mismo ejemplo utilizando la clasificación normalizada:
SELECT title, ts_rank_cd(textsearch, query, 32 /* rank/(rank+1) */ ) AS rank
FROM apod, to_tsquery('neutrino|(dark & matter)') query
WHERE query @@ textsearch
ORDER BY rank DESC
LIMIT 10;
title | rank
-----------------------------------------------+-------------------
Neutrinos in the Sun | 0.756097569485493
The Sudbury Neutrino Detector | 0.705882361190954
A MACHO View of Galactic Dark Matter | 0.668123210574724
Hot Gas and Dark Matter | 0.65655958650282
The Virgo Cluster: Hot Plasma and Dark Matter | 0.656301290640973
Rafting for Solar Neutrinos | 0.655172410958162
NGC 4650A: Strange Galaxy and Dark Matter | 0.650072921219637
Hot Gas and Dark Matter | 0.617195790024749
Ice Fishing for Cosmic Neutrinos | 0.615384618911517
Weak Lensing Distorts the Universe | 0.450010798361481
La clasificación puede ser costosa ya que requiere consultar el
tsvector de cada documento coincidente, lo que puede estar limitado por E/S y
por lo tanto ser lento. Desafortunadamente, es casi imposible de evitar ya que las
consultas prácticas a menudo resultan en grandes números de coincidencias.
Para presentar los resultados de la búsqueda, lo ideal es mostrar una parte de cada documento y
cómo se relaciona con la consulta. Por lo general, los motores de búsqueda muestran fragmentos del
documento con los términos de búsqueda marcados. PostgreSQL
proporciona una función ts_headline que
implementa esta funcionalidad.
ts_headline([configregconfig, ]documenttext,querytsquery[,optionstext]) returnstext
ts_headline acepta un documento junto
con una consulta y devuelve un extracto del
documento en el que se resaltan los términos de la consulta.
Específicamente, la función utilizará la consulta para seleccionar fragmentos de
texto relevantes y luego resaltará todas las palabras que aparezcan en la consulta,
incluso si esas posiciones de palabras no coinciden con las restricciones de la consulta. La
configuración que se utilizará para analizar el documento se puede especificar mediante
config; si se omite config,
se utiliza la configuración default_text_search_config.
Si se especifica una cadena de options, debe
consistir en una lista separada por comas de uno o más pares
option=value.
Las opciones disponibles son:
MaxWords, MinWords (enteros):
estos números determinan los títulos más largos y más cortos a generar.
Los valores por omisión son 35 y 15.
ShortWord (entero): las palabras de esta longitud o menor
se descartarán al principio y al final de un título, a menos que sean
términos de consulta. El valor por omisión de tres elimina los artículos comunes
en inglés o palabras cortas sin significado.
HighlightAll (booleano): si es
true, se utilizará todo el documento como título,
ignorando los tres parámetros anteriores. El valor por omisión
es false.
MaxFragments (entero): número máximo de fragmentos de texto
a mostrar. El valor por omisión de cero selecciona un método de generación de
títulos no basado en fragmentos. Un valor mayor que cero selecciona la
generación de títulos basada en fragmentos (ver más abajo).
StartSel, StopSel (cadenas):
las cadenas con las que delimitar las palabras de consulta que aparecen en el
documento, para distinguirlas de otras palabras extraídas. Los
valores por omisión son “<b>” y
“</b>”, que pueden ser adecuados
para la salida HTML (pero ver la advertencia más abajo).
FragmentDelimiter (cadena): cuando se muestra más de un
fragmento, los fragmentos se separarán con esta cadena.
El valor por omisión es “ ... ”.
No se garantiza que la salida de ts_headline
sea segura para su inclusión directa en páginas web. Cuando
HighlightAll es false (el valor por
omisión), se eliminan algunas etiquetas XML simples del documento, pero esto
no garantiza la eliminación de todo el marcado HTML. Por lo tanto, esto no
proporciona una defensa eficaz contra ataques como los de secuencias de comandos en
sitios cruzados (XSS) cuando se trabaja con entradas no confiables. Para protegerse
contra tales ataques, se debe eliminar todo el marcado HTML del documento de entrada
o utilizar un desinfectador de HTML en la salida.
Estos nombres de opciones se reconocen sin distinción de mayúsculas y minúsculas. Debes poner entre comillas dobles los valores de las cadenas si contienen espacios o comas.
En la generación de títulos no basada en fragmentos, ts_headline
localiza las coincidencias para la query dada
y elige una sola para mostrar, prefiriendo las coincidencias que tienen más palabras de consulta
dentro de la longitud permitida del título. En la generación de títulos basada
en fragmentos, ts_headline localiza las coincidencias de la consulta
y divide cada coincidencia en “fragmentos” de no más de MaxWords
palabras cada uno, prefiriendo los fragmentos con más palabras de consulta y, cuando
sea posible, “estirando” los fragmentos para incluir las palabras circundantes.
El modo basado en fragmentos es, por lo tanto, más útil cuando las coincidencias de la consulta
abarcan grandes secciones del documento o cuando se desea mostrar múltiples coincidencias.
En cualquiera de los dos modos, si no se pueden identificar coincidencias de la consulta,
se mostrará un único fragmento de las primeras MinWords palabras
del documento.
Por ejemplo:
SELECT ts_headline('english',
'The most common type of search
is to find all documents containing given query terms
and return them in order of their similarity to the
query.',
to_tsquery('english', 'query & similarity'));
ts_headline
------------------------------------------------------------
containing given <b>query</b> terms +
and return them in order of their <b>similarity</b> to the+
<b>query</b>.
SELECT ts_headline('english',
'Search terms may occur
many times in a document,
requiring ranking of the search matches to decide which
occurrences to display in the result.',
to_tsquery('english', 'search & term'),
'MaxFragments=10, MaxWords=7, MinWords=3, StartSel=<<, StopSel=>>');
ts_headline
------------------------------------------------------------
<<Search>> <<terms>> may occur +
many times ... ranking of the <<search>> matches to decide
ts_headline utiliza el documento original, no un resumen de
tsvector, por lo que puede ser lento y debe utilizarse con cuidado.