FTS non funziona come previsto con le e-mail con punti


9

Stiamo sviluppando una ricerca come parte di un sistema più grande.

Abbiamo Microsoft SQL Server 2014 - 12.0.2000.8 (X64) Standard Edition (64-bit)con questa configurazione:

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 è una stringa di cifre separate da virgola strutturata come "77777777777, 88888888888"
  2. Emailè una stringa di email strutturata con virgole simili "email1@gmail.com, email2@gmail.com"(o senza virgole affatto simili "email1@gmail.com")
  3. Contacts1, Contacts2, Contacts3, Contacts4sono campi di testo in cui gli utenti possono specificare i dettagli di contatto in forma libera. Come "John Smith +1 202 555 0156"o "Bob, +1-999-888-0156, bob@company.com". Questi campi possono contenere e-mail e telefoni che vogliamo cercare ulteriormente.

Qui creiamo materiale full-text

-- 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

Ecco un esempio di dati

INSERT INTO NewCompanies(Id, Name, Phone, Email, Contacts1, Contacts2, Contacts3, Contacts4) 
VALUES ('7BA05F18-1337-4AFB-80D9-00001A777E4F', 'PJSC Azimuth', '79001002030, 78005005044', 'regular@hotmail.com, s.m.s@gmail.com', 'John Smith', 'Call only at weekends +7-999-666-22-11', NULL, NULL)

In realtà abbiamo circa 100 mila di tali record.

Ci aspettiamo che gli utenti possano specificare una parte dell'email come "@ gmail.com" e questo dovrebbe restituire tutte le righe con indirizzi email Gmail in uno qualsiasi dei Email, Contacts1, Contacts2, Contacts3, Contacts4campi.

Lo stesso per i numeri di telefono. Gli utenti possono cercare un modello come "70283" e una query dovrebbe restituire i telefoni con queste cifre al loro interno. È anche per i Contacts1, Contacts2, Contacts3, Contacts4campi in formato libero in cui probabilmente dovremmo rimuovere tutti tranne caratteri e spazi prima di cercare.

Usavamo LIKEper la ricerca quando avevamo circa 1500 record e funzionava bene, ma ora abbiamo molti record e la LIKEricerca impiega infiniti per ottenere risultati.

Ecco come proviamo a ottenere dati da lì:

SELECT * FROM NewCompanies WHERE CONTAINS((Email, Contacts1, Contacts2, Contacts3, Contacts4), '"s.m.s@gmail.com*"') -- 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

5
Perché tutte le tue colonne sono nvarchar(MAX)qui? Non ho mai sentito parlare o incontrato qualcuno che si chiama ha una lunghezza di 1 miliardo di caratteri. E, secondo questa risposta , un indirizzo e-mail non può contenere più di 254 caratteri; quindi hai anche 1 miliardo di personaggi ~ sprecati lì.
Larnu,

2
Sembra che tu stia combattendo con i word breaker della ricerca full-text. È improbabile trovare qualcosa che usi @gmail.comcome termine di ricerca perché il @personaggio è un word breaker. In altre parole, a seconda della versione di SQL Server che hai, parole l'indice user@gmail.comsarà o (A) user, gmaile como (B) user, user@gmail.com, gmaile com. RIF: modifiche del comportamento alla ricerca full-text
Apprendimento sempre

1
"ma non voglio cercare altro che e-mail e telefoni in quei campi" , dovrebbero essere archiviati in una colonna appropriata, come ho detto prima. Hai colonne per quei dati, che dovrebbero essere normalizzati. I word breaker sono impostati a livello di istanza / database. quindi sarebbe una modifica significativa da rimuovere ..
Larnu,

1
O vorresti normalizzare le tabelle a 1-M per tutti i record di telefono, e-mail ecc. La seconda opzione è quella di dividere le colonne (usa string_split (e-mail, ','), in combinazione con Outer Apply. Dovresti specifica un limite teorico al numero di email che un utente può avere, quindi scrivi una ricerca come questa: 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+'%')Crea circa cinque singoli indici su ciascuno dei campi e includi la chiave primaria
Starbyone,

2
@TheDudeWithHat Non lo farò, non significa che non dovrebbe. Il motivo per cui l'OP sta avendo il problema è a causa della mancanza di normalizzazione.
Larnu,

Risposte:


2

Richieste in realtà

SELECT [...] CONTAINS ([...], '"6662211 *"') - non ottiene nulla

contro 'Call only at weekends +7-999-666-22-11' e

SELECT [...] CONTAINS (Nome, '"zimuth *"') - non ottiene nulla

contro 'PJSC Azimuth'

fare il lavoro come previsto .
Vedi termine prefisso . Perché 6662211*non è un prefisso di +7-999-666-22-11così come zimuth*non è un prefisso diAzimuth

Quanto a

SELEZIONA [...] CONTAINS ([...], '"sms@gmail.com*"') - questo non ottiene la riga

Ciò è probabilmente dovuto ai word breaker, come sottolinea sempre l' apprendimento nei commenti. Vedi i word breaker

Non credo che la ricerca full-text sia applicabile per il tuo compito.

Perché utilizzare per FTS nelle stesse identiche attività per cui l'operatore LIKE è utilizzato? Se ci fosse un tipo di indice migliore per le query LIKE ... allora ci sarebbe il tipo di indice migliore , non la tecnologia e la sintassi totalmente diverse.
E in nessun modo ti aiuterà a trovare una corrispondenza "6662211*"con "666 caratteri arbitrari 22 caratteri arbitrari 11".
La ricerca full-text non riguarda regex-es (e "6662211*"non è nemmeno un'espressione corretta per il lavoro - non c'è nulla sulla parte "qualche carattere arbitrario") riguarda sinonimi, forme di parole, ecc.

Ma è possibile cercare efficacemente le sottostringhe?

Sì. Lasciando da parte prospettive come la scrittura del proprio motore di ricerca, cosa possiamo fare SQL?

Prima di tutto, è indispensabile ripulire i dati! Se si desidera restituire agli utenti le stringhe esatte che hanno inserito

gli utenti possono specificare i dettagli di contatto in forma libera

... puoi salvarli così come sono ... e lasciarli insieme.
Quindi è necessario estrarre i dati dal testo del modulo libero (non è così difficile per e-mail e numeri di telefono) e salvare i dati in una forma canonica. Per l'e-mail, l'unica cosa che devi veramente fare: rendili tutti minuscoli o maiuscoli (non importa), e forse dividi poi sul @cantare. Ma nei numeri di telefono devi lasciare solo cifre
(... E poi puoi persino memorizzarli come numeri . Ciò può farti risparmiare un po 'di spazio e tempo. Ma la ricerca sarà diversa ... Per ora tuffiamoci in un più semplice e soluzione universale usando stringhe.)

Come menzionato da MatthewBaker , puoi creare una tabella di suffissi. Quindi puoi cercare in questo modo

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

Dovresti posizionare il carattere jolly %solo alla fine . O non ci sarebbero benefici dalla tabella Suffissi.

Prendiamo ad esempio un numero di telefono

+ 7-999-666-22-11

Dopo che ci sbarazzeremo dei caratteri di scarto, avrà 11 cifre. Ciò significa che avremo bisogno di 11 suffissi per un numero di telefono

           1
          11
         211
        2211
       62211
      662211
     6662211
    96662211
   996662211
  9996662211
 79996662211

Quindi la complessità dello spazio per questa soluzione è lineare ... non così male, direi ... Ma aspettiamo che sia complessità nel numero di record. Ma nei simboli ... abbiamo bisogno di N(N+1)/2simboli per memorizzare tutti i suffissi - questa è la complessità quadratica ... non va bene ... ma se ora hai 100 000registrazioni e non hai piani per milioni nel prossimo futuro - puoi andare con questo soluzione.

Possiamo ridurre la complessità dello spazio?

Descriverò solo l'idea, la sua attuazione richiederà uno sforzo. E probabilmente dovremo oltrepassare i confini diSQL

Supponiamo che tu abbia 2 righe NewCompaniese 2 stringhe di testo in formato libero:

    aaaaa
    11111

Quanto dovrebbe essere grande la tabella dei suffissi? Ovviamente, abbiamo bisogno solo di 2 record.

Facciamo un altro esempio. Anche 2 righe, 2 stringhe di testo gratuite da cercare. Ma ora è:

    aa11aa
    cc11cc

Vediamo quanti suffissi abbiamo bisogno ora:

         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

Non così male, ma neanche così buono.

Cos'altro possiamo fare?

Diciamo, l'utente entra "c11"nel campo di ricerca. Quindi ha LIKE 'c11%'bisogno del suffisso ' c11 cc' per avere successo. Ma se invece di cercare "c11"prima cerchiamo "c%", quindi per "c1%"e così via? La prima ricerca darà come solo una riga da NewCompanies. E non sarebbero necessarie ricerche successive. E possiamo

       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

e finiamo con solo 4 suffissi

      11aa
    aa11aa
      11cc
    cc11cc

Non posso dire quale sarebbe la complessità dello spazio in questo caso, ma sembra che sarebbe accettabile.


1

In casi come questo, la ricerca a testo integrale non è l'ideale. Ero nella stessa barca come te. Come le ricerche sono troppo lente, e le ricerche full-text cercano parole che iniziano con un termine anziché con un termine.

Abbiamo provato diverse soluzioni, un'opzione SQL pura è quella di creare la tua versione della ricerca full-text, in particolare una ricerca indice invertita. Ci abbiamo provato ed è stato un successo, ma ci è voluto molto spazio. Abbiamo creato una tabella di supporto secondaria per termini di ricerca parziale e su questo abbiamo utilizzato l'indicizzazione di testo completo. Tuttavia, ciò significa che abbiamo archiviato ripetutamente più copie della stessa cosa. Ad esempio abbiamo memorizzato "longword" come Longword, ongword, ngword, gword .... ecc. Quindi ogni frase contenuta sarebbe sempre all'inizio del termine indicizzato. Una soluzione orrenda, piena di difetti, ma ha funzionato.

Abbiamo quindi esaminato l'hosting di un server separato per le ricerche. Googling Lucene ed elastisearch ti forniranno buone informazioni su questi pacchetti standard.

Alla fine, abbiamo sviluppato il nostro motore di ricerca interno, che funziona insieme a SQL laterale. Questo ci ha permesso di implementare ricerche fonetiche (doppio metafono) e quindi di utilizzare calcoli di levenshtein lungo il suono laterale per stabilire la pertinenza. Overkill per molte soluzioni, ma ne vale la pena nel nostro caso d'uso. Ora abbiamo anche un'opzione per sfruttare le GPU Nvidia per le ricerche di cuda, ma questo rappresentava un nuovo set di mal di testa e notti insonni. La rilevanza di tutto ciò dipenderà dalla frequenza con cui le tue ricerche vengono eseguite e dalla reattività di cui hanno bisogno.


1

Gli indici full-text presentano una serie di limitazioni. È possibile utilizzare i caratteri jolly sulle parole che l'indice trova come "parti" intere, ma anche in questo caso si è vincolati alla parte finale della parola. Ecco perché puoi usare CONTAINS(Name, '"Azimut*"')ma nonCONTAINS(Name, '"zimuth*"')

Dalla documentazione Microsoft :

Quando il termine prefisso è una frase, ogni token che compone la frase viene considerato un termine prefisso separato. Verranno restituite tutte le righe che hanno parole che iniziano con i termini del prefisso . Ad esempio, il termine prefisso "pane leggero *" troverà righe con il testo di "pane leggero", "pane leggero" o "pane leggero", ma non restituirà "pane leggermente tostato".

I punti nell'e-mail, come indicato dal titolo, non sono il problema principale. Questo, ad esempio, funziona:

SELECT * FROM NewCompanies 
WHERE CONTAINS((Email, Contacts1, Contacts2, Contacts3, Contacts4), 's.m.s@gmail.com') 

In questo caso, l'indice identifica l'intera stringa di posta elettronica come valida, nonché "gmail" e "gmail.com". Solo "sms" non è valido.

L'ultimo esempio è simile. Le parti del numero di telefono sono indicizzate (666-22-11 e 999-666-22-11 per esempio), ma rimuovere i trattini non è una stringa che l'indice sta per conoscere. Altrimenti, questo funziona:

SELECT * FROM NewCompanies 
WHERE CONTAINS((Phone, Contacts1, Contacts2, Contacts3, Contacts4), '"666-22-11*"')
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.