Saltar al contenido

PostgreSQL, trigramas y similitud

Te doy la bienvenida a nuestra página web, en este sitio encontrarás la solucíon que buscas.

Solución:

El concepto de similitud de trigramas se basa en dividir cualquier oración en “trigramas” (secuencias de tres letras consecutivas) y tratar el resultado como un CONJUNTO (es decir, el orden no importa y usted no tiene valores repetidos). Antes de que se considere la oración, se agregan dos espacios en blanco al principio y uno al final, y los espacios simples se reemplazan por dobles.

Trigramas son un caso especial de N-gramos.

El conjunto de trigramas correspondiente a “Chateau blanc” se encuentra al encontrar todas las secuencias de tres letras que aparecen en él:

  chateau  blanc
---                 => '  c'
 ---                => ' ch'
  ---               => 'cha'
   ---              => 'hat'
    ---             => 'ate'
     ---            => 'tea'
      ---           => 'eau'
       ---          => 'au '
        ---         => 'u  '
         ---        => '  b'
          ---       => ' bl'
           ---      => 'bla'
            ---     => 'lan'
             ---    => 'anc'
              ---   => 'nc '

Ordenarlos y eliminar repeticiones te permite:

'  b'
'  c'
' bl'
' ch'
'anc'
'ate'
'au '
'bla'
'cha'
'eau'
'hat'
'lan'
'nc '
'tea'

Esto puede ser calculado por PostgreSQL mediante la función show_trgm:

SELECT show_trgm('Chateau blanc') AS A

A = [  b,  c, bl, ch,anc,ate,au ,bla,cha,eau,hat,lan,nc ,tea]

… que tiene 14 trigramas. (Marque pg_trgm).

Y el conjunto de trigramas correspondiente a “Chateau Cheval Blanc” es:

SELECT show_trgm('Chateau Cheval Blanc') AS B 

B = [  b,  c, bl, ch,anc,ate,au ,bla,cha,che,eau,evl,hat,hev,la ,lan,nc ,tea,vla]

… que tiene 19 trigrams

Si cuenta cuántos trigramas tienen ambos conjuntos en común, encontrará que tienen los siguientes:

A intersect B = 
    [  b,  c, bl, ch,anc,ate,au ,bla,cha,eau,hat,lan,nc ,tea]

y los que tienen en total son:

A union B = 
    [  b,  c, bl, ch,anc,ate,au ,bla,cha,che,eau,evl,hat,hev,la ,lan,nc ,tea,vla]

Es decir, ambas frases tienen 14 trigramas en común y 19 en total.
La similitud se calcula como:

 similarity = 14 / 19

Puedes comprobarlo con:

SELECT 
    cast(14.0/19.0 as real) AS computed_result, 
    similarity('Chateau blanc', 'chateau cheval blanc') AS function_in_pg

y verás que obtienes: 0.736842

… lo cual explica cómo se calcula la similitud, y por qué obtienes los valores que obtienes.


NOTA: Puede calcular la intersección y la unión mediante:

SELECT 
   array_agg(t) AS in_common
FROM
(
    SELECT unnest(show_trgm('Chateau blanc')) AS t 
    INTERSECT 
    SELECT unnest(show_trgm('chateau chevla blanc')) AS t
    ORDER BY t
) AS trigrams_in_common ;

SELECT 
   array_agg(t) AS in_total
FROM
(
    SELECT unnest(show_trgm('Chateau blanc')) AS t 
    UNION 
    SELECT unnest(show_trgm('chateau chevla blanc')) AS t
) AS trigrams_in_total ;

Y esta es una forma de explorar la similitud de diferentes pares de oraciones:

WITH p AS
(
    SELECT 
      'This is just a sentence I''ve invented'::text AS f1,
      'This is just a sentence I''ve also invented'::text AS f2
),
t1 AS
(
    SELECT unnest(show_trgm(f1)) FROM p
),
t2 AS
(
    SELECT unnest(show_trgm(f2)) FROM p
),
x AS
(
    SELECT
        (SELECT count(*) FROM 
            (SELECT * FROM t1 INTERSECT SELECT * FROM t2) AS s0)::integer AS same,
        (SELECT count(*) FROM 
            (SELECT * FROM t1 UNION     SELECT * FROM t2) AS s0)::integer AS total,
        similarity(f1, f2) AS sim_2
FROM
    p 
)
SELECT
    same, total, same::real/total::real AS sim_1, sim_2
FROM
    x ;

Puedes comprobarlo en Rextester

El algoritmo del trigrama debe ser más preciso cuanto menor sea la diferencia en la longitud de las cadenas comparadas. Puede modificar el algoritmo para compensar el efecto de la diferencia de longitud.

La siguiente función ejemplar reduce la similitud en un 1% para la diferencia de 1 carácter en string longitudes. Esto significa que favorece cadenas de la misma longitud (similar).

create or replace function corrected_similarity(str1 text, str2 text)
returns float4 language sql as $$
    select similarity(str1, str2)* (1- abs(length(str1)-length(str2))/100.0)::float4
$$;

select 
    winery, 
    similarity(winery, 'chateau chevla blanc') as similarity,
    corrected_similarity(winery, 'chateau chevla blanc') as corrected_similarity
from usr_wines 
where winery % 'chateau chevla blanc'  
order by corrected_similarity desc;

          winery          | similarity | corrected_similarity 
--------------------------+------------+----------------------
 Chateau ChevL Blanc      |       0.85 |               0.8415
 Chateau Cheval Blanc     |   0.727273 |             0.727273
 Chateau Cheval Blanc     |   0.727273 |             0.727273
 Chateau Cheval Blanc     |   0.727273 |             0.727273
 Chateau Blanc,           |   0.736842 |             0.692632
 Chateau Blanc            |   0.736842 |             0.685263
 Chateau Blanc            |   0.736842 |             0.685263
 Chateau Blanc            |   0.736842 |             0.685263
 Chateau Blanc            |   0.736842 |             0.685263
 Chateau Blanc            |   0.736842 |             0.685263
 Chateau Cheval Blanc (7) |   0.666667 |                 0.64
 Chateau Du Cheval Blanc  |       0.64 |               0.6208
 Chateau Du Cheval Blanc  |       0.64 |               0.6208
 Chateau Cheval Blanc Cbo |       0.64 |               0.6144
(14 rows)

De manera similar, puede corregir la similitud estándar, por ejemplo, cuántos caracteres iniciales son idénticos (aunque la función será un poco más complicada).

A veces quieres lo opuesto a la respuesta de klin. Hay aplicaciones donde una gran diferencia en string las longitudes no deben dar lugar a una penalización de puntuación tan significativa.

Por ejemplo, imagine un formulario de resultados de autocompletar con sugerencias de concordancia de trigramas que mejoran a medida que escribe.

Aquí hay una forma alternativa de igualar la puntuación que todavía usa trigramas pero favorece las coincidencias de subcadenas un poco más de lo habitual.

En cambio, la fórmula de la similitud comienza con

the number of common trigrams
-------------------------------------------
the number of trigrams in the shortest word    <-- key difference

y puede aumentar desde allí en función de una buena puntuación de similitud estándar.

CREATE OR REPLACE FUNCTION substring_similarity(string_a TEXT, string_b TEXT) RETURNS FLOAT4 AS $$
DECLARE
  a_trigrams TEXT[];
  b_trigrams TEXT[];
  a_tri_len INTEGER;
  b_tri_len INTEGER;
  common_trigrams TEXT[];
  max_common INTEGER;
BEGIN
  a_trigrams = SHOW_TRGM(string_a);
  b_trigrams = SHOW_TRGM(string_b);
  a_tri_len = ARRAY_LENGTH(a_trigrams, 1);
  b_tri_len = ARRAY_LENGTH(b_trigrams, 1);
  IF (NOT (a_tri_len > 0) OR NOT (b_tri_len > 0)) THEN
    IF (string_a = string_b) THEN
      RETURN 1;
    ELSE
      RETURN 0;
    END IF;
  END IF;
  common_trigrams := ARRAY(SELECT UNNEST(a_trigrams) INTERSECT SELECT UNNEST(b_trigrams));
  max_common = LEAST(a_tri_len, b_tri_len);
  RETURN COALESCE(ARRAY_LENGTH(common_trigrams, 1), 0)::FLOAT4 / max_common::FLOAT4;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION corrected_similarity(string_a TEXT, string_b TEXT) 
RETURNS FLOAT4 AS $$
DECLARE
  base_score FLOAT4;
BEGIN
  base_score := substring_similarity(string_a, string_b);
  -- a good standard similarity score can raise the base_score
  RETURN base_score + ((1.0 - base_score) * SIMILARITY(string_a, string_b));
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION is_minimally_substring_similar(string_a TEXT, string_b TEXT) RETURNS BOOLEAN AS $$
BEGIN
  RETURN corrected_similarity(string_a, string_b) >= 0.5;
END;
$$ LANGUAGE plpgsql;

CREATE OPERATOR %%% (
  leftarg = TEXT,
  rightarg = TEXT,
  procedure = is_minimally_substring_similar,
  commutator = %%%
);

Ahora puede usar esto de la misma manera que la consulta de similitud estándar:

SELECT * FROM table WHERE name %%% 'chateau'
ORDER BY corrected_similarity(name, 'chateau') DESC;

Rendimiento

El rendimiento es aceptable para un espacio de búsqueda de 100K registros, pero probablemente no sería bueno para un espacio de búsqueda de millones. Para eso, es posible que desee utilizar una compilación modificada del módulo pg_trgm, código en github.

Reseñas y calificaciones

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *