FTS no funciona como se esperaba con correos electrónicos con puntos

9

Estamos desarrollando una búsqueda como parte de un sistema más grande.

Tenemos Microsoft SQL Server 2014 - 12.0.2000.8 (X64) Standard Edition (64-bit)con esta configuración:

CREATE TABLE NewCompanies(
    [Id] [uniqueidentifier] NOT NULL,
    [Name] [nvarchar](400) NOT NULL,
    [Phone] [nvarchar](max) NULL,
    [Email] [nvarchar](max) NULL,
    [Contacts1] [nvarchar](max) NULL,
    [Contacts2] [nvarchar](max) NULL,
    [Contacts3] [nvarchar](max) NULL,
    [Contacts4] [nvarchar](max) NULL,
    [Address] [nvarchar](max) NULL,
    CONSTRAINT PK_Id PRIMARY KEY (Id)
);
  1. Phone es una cadena de dígitos estructurados separados por comas como "77777777777, 88888888888"
  2. Emailes una cadena de correos electrónicos estructurados con comas similares "[email protected], [email protected]"(o sin comas "[email protected]")
  3. Contacts1, Contacts2, Contacts3, Contacts4son campos de texto donde los usuarios pueden especificar detalles de contacto en forma libre. Me gusta "John Smith +1 202 555 0156"o "Bob, +1-999-888-0156, [email protected]". Estos campos pueden contener correos electrónicos y teléfonos que queremos buscar más.

Aquí creamos texto completo

-- FULL TEXT SEARCH
CREATE FULLTEXT CATALOG NewCompanySearch AS DEFAULT;  
CREATE FULLTEXT INDEX ON NewCompanies(Name, Phone, Email, Contacts1, Contacts2, Contacts3, Contacts4, Address)
KEY INDEX PK_Id

Aquí hay una muestra de datos

INSERT INTO NewCompanies(Id, Name, Phone, Email, Contacts1, Contacts2, Contacts3, Contacts4) 
VALUES ('7BA05F18-1337-4AFB-80D9-00001A777E4F', 'PJSC Azimuth', '79001002030, 78005005044', '[email protected], [email protected]', 'John Smith', 'Call only at weekends +7-999-666-22-11', NULL, NULL)

En realidad, tenemos alrededor de 100 mil de esos registros.

Esperamos que los usuarios puedan especificar una parte del correo electrónico como "@ gmail.com" y esto debería devolver todas las filas con direcciones de correo electrónico de Gmail en cualquiera de los Email, Contacts1, Contacts2, Contacts3, Contacts4campos.

Lo mismo para los números de teléfono. Los usuarios pueden buscar un patrón como "70283" y una consulta debe devolver los teléfonos con estos dígitos en ellos. Incluso es para Contacts1, Contacts2, Contacts3, Contacts4campos de formulario libre donde probablemente deberíamos eliminar todos menos los dígitos y los caracteres de espacio antes de buscar.

Solíamos usar LIKEpara la búsqueda cuando teníamos alrededor de 1500 registros y funcionó bien, pero ahora tenemos muchos registros y la LIKEbúsqueda lleva infinitos para obtener resultados.

Así es como intentamos obtener datos desde allí:

SELECT * FROM NewCompanies WHERE CONTAINS((Email, Contacts1, Contacts2, Contacts3, Contacts4), '"[email protected]*"') -- this doesn't get the row
SELECT * FROM NewCompanies WHERE CONTAINS((Phone, Contacts1, Contacts2, Contacts3, Contacts4), '"6662211*"') -- doesn't get anything
SELECT * FROM NewCompanies WHERE CONTAINS(Name, '"zimuth*"') -- doesn't get anything
kseen
fuente
55
¿Por qué todas tus columnas están nvarchar(MAX)aquí? Nunca he oído hablar, ni he conocido a nadie cuyo nombre tenga 1 mil millones de caracteres de largo. Y, según esta respuesta , una dirección de correo electrónico no puede tener más de 254 caracteres; así que también tienes 1 billón de personajes desperdiciados allí.
Larnu
2
Parece que estás luchando con los separadores de palabras de búsqueda de texto completo. Es poco probable que encuentre algo usando @gmail.comcomo término de búsqueda porque el @carácter es un separador de palabras. En otras palabras, según la versión de SQL Server que tiene, palabras en el índice para [email protected]serán o bien (A) user, gmaily como (B) user, [email protected], gmaily com. REF: Cambios de comportamiento en la búsqueda de texto completo
Siempre aprendiendo
1
"pero no quiero buscar nada más que correos electrónicos y teléfonos en esos campos", entonces deberían almacenarse en una columna apropiada, como dije antes. Tiene columnas para esos datos, que deben normalizarse. Los separadores de palabras se establecen a nivel de instancia / base de datos. Por lo tanto, sería un cambio importante para eliminar ..
Larnu
1
Desea normalizar las tablas a 1-M para todos los registros de teléfono, correo electrónico, etc. La segunda opción es dividir las columnas (use string_split (email, ','), en combinación con Outer Apply. Debería especifique un límite teórico sobre la cantidad de correos electrónicos que puede tener un usuario. Luego escriba una búsqueda como esta:. SELECT * FROM NewCompanies WHERE Id IN (SELECT ID from .... where MyOuterApply.EmailCol1 LIKE '%'+@SearchString+'%') OR Id IN (SELECT ID from .... where MyOuterApply.EmailCol2 LIKE '%'+@SearchString+'%')Cree alrededor de cinco índices individuales en cada uno de los campos e incluya la clave principal.
starbyone
2
@TheDudeWithHat No va a, no significa que no debería. La razón por la que el OP está teniendo el problema es por la falta de normalización.
Larnu

Respuestas:

2

Actualmente solicita

SELECT [...] CONTAINS ([...], '"6662211 *"') - no obtiene nada

en contra 'Call only at weekends +7-999-666-22-11' y

SELECCIONE [...] CONTIENE (Nombre, '"zimuth *"') - no obtiene nada

en contra 'PJSC Azimuth'

hacer el trabajo como se esperaba .
Ver término del prefijo . Porque 6662211*no es un prefijo de +7-999-666-22-11y tampoco zimuth*es un prefijo deAzimuth

Como para

SELECT [...] CONTAINS ([...], '"[email protected]*"') - esto no obtiene la fila

Esto probablemente se deba a los separadores de palabras, como siempre se aprende en los comentarios. Ver separadores de palabras

No creo que la búsqueda de texto completo sea aplicable para su tarea.

¿Por qué usar para FTS en exactamente las mismas tareas para las que se usa el operador LIKE? Si hubiera un mejor tipo de índice para las consultas LIKE ... entonces habría un mejor tipo de índice , no la tecnología y la sintaxis totalmente diferentes.
Y de ninguna manera te ayudará a enfrentarte "6662211*"contra "666 some arbitraria char 22 some arbitraria char 11".
La búsqueda de texto completo no se trata de "6662211*"expresiones regulares (y ni siquiera es una expresión correcta para el trabajo; no hay nada sobre la parte de "algún carácter arbitrario") se trata de sinónimos, formas de palabras, etc.

¿Pero es posible buscar subcadenas de manera efectiva?

Sí lo es. Dejando de lado perspectivas como escribir su propio motor de búsqueda, ¿qué podemos hacer dentro SQL?

En primer lugar, ¡es imperativo limpiar sus datos! Si desea devolver a los usuarios las cadenas exactas que han ingresado

los usuarios pueden especificar detalles de contacto en forma libre

... puedes guardarlos como están ... y dejarlos junto.
Luego, debe extraer datos del texto de forma libre (no es tan difícil para correos electrónicos y números de teléfono) y guardar los datos en alguna forma canónica. Para el correo electrónico, lo único que realmente necesita hacer: hacerlos todos en minúsculas o mayúsculas (no importa), y tal vez dividirlos luego en el @canto. Pero en los números de teléfono debe dejar solo dígitos
(... Y luego incluso puede almacenarlos como números . Eso puede ahorrarle algo de espacio y tiempo. Pero la búsqueda será diferente ... Por ahora vamos a sumergirnos en algo más simple y solución universal usando cadenas.)

Como MatthewBaker mencionó , puede crear una tabla de sufijos. Entonces puedes buscar así

SELECT DISTINCT * FROM NewCompanies JOIN Sufficies ON NewCompanies.Id = Sufficies.Id WHERE Sufficies.sufficies LIKE 'some text%'

Debe colocar el comodín %solo al final . O no habría beneficios de la tabla de sufijos.

Tomemos por ejemplo un número de teléfono

+ 7-999-666-22-11

Después de deshacernos de los caracteres de desecho, tendrá 11 dígitos. Eso significa que necesitaremos 11 sufijos para un número de teléfono

           1
          11
         211
        2211
       62211
      662211
     6662211
    96662211
   996662211
  9996662211
 79996662211

Entonces, la complejidad del espacio para esta solución es lineal ... no está tan mal, diría ... Pero espere que sea ​​la complejidad en el número de registros. Pero en los símbolos ... necesitamos N(N+1)/2símbolos para almacenar todos los sufijos, eso es complejidad cuadrática ... no es bueno ... pero si ahora tiene 100 000registros y no tiene planes para millones en el futuro cercano, puede continuar con esto solución.

¿Podemos reducir la complejidad del espacio?

Solo describiré la idea, implementarla requerirá un poco de esfuerzo. Y probablemente tendremos que cruzar los límites deSQL

Digamos que tiene 2 filas NewCompaniesy 2 cadenas de texto de forma libre:

    aaaaa
    11111

¿Qué tan grande debe ser la tabla de sufijos? Obviamente, solo necesitamos 2 registros.

Tomemos otro ejemplo. También 2 filas, 2 cadenas de texto libre para buscar. Pero ahora es:

    aa11aa
    cc11cc

Veamos cuántos sufijos necesitamos ahora:

         a // no need, LIKE `a%`  will match against 'aa' and 'a11aa' and 'aa11aa'
        aa // no need, LIKE `aa%` will match against 'aa11aa'
       1aa
      11aa
     a11aa
    aa11aa
         c // no need, LIKE `c%`  will match against 'cc' and 'c11cc' and 'cc11cc'
        cc // no need, LIKE `cc%` will match against 'cc11cc'
       1cc
      11cc
     c11cc
    cc11cc

No tan mal, pero tampoco tan bueno.

qué más podemos hacer?

Digamos que el usuario ingresa "c11"en el campo de búsqueda. Luego LIKE 'c11%'necesita el sufijo ' c11 cc' para tener éxito. Pero si en lugar de buscar "c11", primero buscamos "c%", ¿luego, "c1%"etc.? La primera búsqueda dará como solo una fila desde NewCompanies. Y no habría necesidad de búsquedas posteriores. Y podemos

       1aa // drop this as well, because LIKE '1%' matches '11aa'
      11aa
     a11aa // drop this as well, because LIKE 'a%' matches 'aa11aa'
    aa11aa
       1cc // same here
      11cc
     c11cc // same here
    cc11cc

y terminamos con solo 4 sufijos

      11aa
    aa11aa
      11cc
    cc11cc

No puedo decir cuál sería la complejidad del espacio en este caso, pero parece que sería aceptable.

x00
fuente
1

En casos como este, la búsqueda de texto completo es menos que ideal. Estaba en el mismo bote que tú. Las búsquedas similares son demasiado lentas, y las búsquedas de texto completo buscan palabras que comienzan con un término en lugar de contener un término.

Probamos varias soluciones, una opción de SQL puro es crear su propia versión de búsqueda de texto completo, en particular una búsqueda de índice invertido. Intentamos esto, y fue exitoso, pero tomó mucho espacio. Creamos una tabla de retención secundaria para términos de búsqueda parcial, y utilizamos indexación de texto completo en eso. Sin embargo, esto significa que almacenamos repetidamente varias copias de la misma cosa. Por ejemplo, almacenamos "longword" como Longword, ongword, ngword, gword ... etc. Por lo tanto, cualquier frase contenida siempre estará al comienzo del término indexado. Una solución horrible, llena de defectos, pero funcionó.

Luego buscamos alojar un servidor separado para las búsquedas. Buscar en Google Lucene y elastisearch le dará buena información sobre estos paquetes disponibles.

Finalmente, desarrollamos nuestro propio motor de búsqueda interno, que se ejecuta junto con SQL. Esto nos ha permitido implementar búsquedas fonéticas (doble metafonía) y luego usar cálculos de levenshtein junto con el índice de sonido para establecer relevancia. Exagerar para muchas soluciones, pero vale la pena el esfuerzo en nuestro caso de uso. Incluso ahora tenemos la opción de aprovechar las GPU Nvidia para las búsquedas de cuda, pero esto representó un nuevo conjunto de dolores de cabeza y noches de insomnio. La relevancia de todo esto dependerá de la frecuencia con la que veas que se realizan tus búsquedas y de cuán reactivo necesitas que sean.

Matthew Baker
fuente
1

Los índices de texto completo tienen varias limitaciones. Puede usar comodines en palabras que el índice encuentra como "partes" enteras, pero aun así está limitado a la parte final de la palabra. Es por eso que puedes usar CONTAINS(Name, '"Azimut*"')pero noCONTAINS(Name, '"zimuth*"')

De la documentación de Microsoft :

Cuando el término del prefijo es una frase, cada ficha que constituye la frase se considera un término de prefijo separado. Se devolverán todas las filas que tienen palabras que comienzan con los términos del prefijo . Por ejemplo, el término del prefijo "pan ligero *" encontrará filas con el texto de "pan ligero", "pan ligero" o "pan ligero", pero no devolverá "pan ligeramente tostado".

Los puntos en el correo electrónico, como lo indica el título, no son el problema principal. Esto, por ejemplo, funciona:

SELECT * FROM NewCompanies 
WHERE CONTAINS((Email, Contacts1, Contacts2, Contacts3, Contacts4), '[email protected]') 

En este caso, el índice identifica toda la cadena de correo electrónico como válida, así como "gmail" y "gmail.com". Solo "sms" aunque no es válido.

El último ejemplo es similar. Las partes del número de teléfono están indexadas (666-22-11 y 999-666-22-11, por ejemplo), pero eliminar los guiones no es una cadena que el índice va a conocer. De lo contrario, esto funciona:

SELECT * FROM NewCompanies 
WHERE CONTAINS((Phone, Contacts1, Contacts2, Contacts3, Contacts4), '"666-22-11*"')
smoore4
fuente