Il Fuzzy Matching ti permette di identificare le corrispondenze non esatte del tuo oggetto. È la pietra angolare di molte strutture dei motori di ricerca e uno dei motivi principali per cui è possibile ottenere risultati di ricerca pertinenti anche se si ha un errore di battitura nella query o un diverso tempo verbale.
Come ci si potrebbe aspettare, ci sono molti algoritmi che possono essere usati per la ricerca fuzzy sul testo, ma praticamente tutti i framework dei motori di ricerca (incluso bleve) usano principalmente la Levenshtein Distance per l’abbinamento fuzzy delle stringhe:
Levenshtein Distance
Conosciuta anche come Edit Distance, è il numero di trasformazioni (cancellazioni, inserzioni o sostituzioni) necessarie per trasformare una stringa sorgente in quella di destinazione. Per esempio, se il termine di destinazione è “book” e la fonte è “back”, sarà necessario cambiare la prima “o” in “a” e la seconda “o” in “c”, che ci darà una distanza di Levenshtein di 2.Edit Distance è molto facile da implementare, ed è una sfida popolare durante le interviste sul codice (È possibile trovare implementazioni di Levenshtein in JavaScript, Kotlin, Java, e molti altri qui).
Inoltre, alcuni framework supportano anche la distanza Damerau-Levenshtein:
Damerau-Levenshtein distance
È un’estensione della Levenshtein Distance, che permette un’operazione extra: Trasposizione di due caratteri adiacenti:
Ex: TSAR a STAR
Damerau-Levenshtein distance = 1 (Cambiare le posizioni S e T costa solo un’operazione)
Levenshtein distance = 2 (Sostituire S con T e T con S)
Fuzzy matching e rilevanza
Fuzzy matching ha un grande effetto collaterale; incasina la rilevanza. Anche se Damerau-Levenshtein è un algoritmo che considera la maggior parte degli errori di ortografia dell’utente comune, può anche includere un numero significativo di falsi positivi, soprattutto quando stiamo usando una lingua con una media di sole 5 lettere per parola, come l’inglese. Questo è il motivo per cui la maggior parte dei framework dei motori di ricerca preferisce attenersi alla distanza di Levenshtein. Vediamo un esempio reale:
Primo, useremo questo set di dati del catalogo dei film. Lo consiglio vivamente se volete giocare con la ricerca full-text. Quindi, cerchiamo i film con “libro” nel titolo. Un semplice codice sarebbe come il seguente:
1
2
3
4
5
6
|
String indexName = “movies_index”;
MatchQuery query = SearchQuery.match(“libro”).field(“titolo”);
SearchQueryResult result = movieRepository.getCouchbaseOperations().getCouchbaseBucket().query(
new SearchQuery(indexName, query).highlight().limit(6));
printResults(movies);
|
Il codice sopra riportato porterà i seguenti risultati:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
1) Il ladro di libri
punteggio: 4.826942606027416
Punti colpiti:
campo:titolo
termine:libro
frammento:
—————————–
2) The Book of Eli
score: 4.826942606027416
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
3) The Book of Life
score: 4.826942606027416
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
4) Black Book
punteggio: 4.826942606027416
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
5) The Jungle Book
score: 4.826942606027416
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
6) The Jungle Book 2
score: 3.9411821308689627
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
|
Di default, i risultati sono case-insensitive, ma si può facilmente cambiare questo comportamento creando nuovi indici con diversi analizzatori.
Ora, aggiungiamo una capacità di fuzzy matching alla nostra query impostando fuzziness a 1 (Levenshtein distance 1), il che significa che “libro” e “look” avranno la stessa rilevanza.
1
2
3
4
5
6
|
String indexName = “movies_index”;
MatchQuery query = SearchQuery.match(“libro”).field(“titolo”).fuzziness(1);
SearchQueryResult result = movieRepository.getCouchbaseOperations().getCouchbaseBucket().query(
new SearchQuery(indexName, query).highlight().limit(6));
printResults(movies);
|
Ed ecco il risultato della ricerca fuzzy:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
1) Gancio
punteggio: 1.1012248063242538
Punti colpiti:
campo:titolo
termine:gancio
frammento:
—————————–
2) Here Comes the Boom
punteggio: 0.7786835148361213
Hit Locations:
campo:titolo
termine:boom
frammento:
—————————–
3) Guarda chi parla troppo
punteggio: 0.7047266634351538
Località colpite:
campo:titolo
termine:guarda
frammento:
—————————–
4) Senti chi parla
punteggio: 0,7047266634351538
Punti colpiti:
campo:titolo
termine:look
frammento:
—————————–
5) Il ladro di libri
punteggio: 0,52288117537184
Punti colpiti:
campo:titolo
termine:libro
frammento:
—————————–
6) Il Libro di Eli
punteggio: 0,52288117537184
Punti colpiti:
campo:titolo
termine:libro
frammento:
—————————–
|
Ora, il film chiamato “Hook” è il primissimo risultato della ricerca, che potrebbe non essere esattamente quello che l’utente si aspetta in una ricerca per “Book”.
Come minimizzare i falsi positivi durante le ricerche fuzzy
In un mondo ideale, gli utenti non farebbero mai errori di battitura mentre cercano qualcosa. Tuttavia, questo non è il mondo in cui viviamo, e se volete che i vostri utenti abbiano un’esperienza piacevole, dovete gestire almeno una distanza di modifica di 1. Pertanto, la vera domanda è: come possiamo fare un fuzzy string matching minimizzando la perdita di rilevanza? Una corrispondenza con una distanza di modifica più bassa di solito ottiene un punteggio più alto. Questa caratteristica ci permette di combinare le due query con diversi livelli di fuzziness in una sola:
1
2
3
4
5
6
7
8
|
String indexName = “movies_index”;
String word = “Book”;
MatchQuery titleFuzzy = SearchQuery.match(parola).fuzziness(1).field(“titolo”);
MatchQuery titleSimple = SearchQuery.match(parola).field(“titolo”);
DisjunctionQuery ftsQuery = SearchQuery.disjuncts(titleSimple, titleFuzzy);
SearchQueryResult result = movieRepository.getCouchbaseOperations().getCouchbaseBucket().query(
new SearchQuery(indexName, ftsQuery).highlight().limit(20)); printResults(result);
|
Ecco il risultato della query fuzzy di cui sopra:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
1) Il ladro di libri
punteggio: 2.398890092610849
Punti colpiti:
campo:titolo
termine:libro
frammento:
—————————–
campo:titolo
termine:libro
frammento:
—————————–
2) The Book of Eli
score: 2.398890092610849
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
campo:titolo
termine:libro
frammento:
—————————–
3) The Book of Life
score: 2.398890092610849
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
campo:titolo
termine:libro
frammento:
—————————–
4) Black Book
punteggio: 2.398890092610849
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
campo:titolo
termine:libro
frammento:
—————————–
5) The Jungle Book
score: 2.398890092610849
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
campo:titolo
termine:libro
frammento:
—————————–
6) The Jungle Book 2
score: 1.958685557004688
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
campo:titolo
termine:libro
frammento:
—————————–
7) National Treasure: Book of Secrets
score: 1.6962714808368062
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
campo:titolo
termine:libro
frammento:
—————————–
8) American Pie Presents: The Book of Love
score: 1.517191317611584
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
campo:titolo
termine:libro
frammento:
—————————–
9) Hook
score: 0.5052232518679681
Hit Locations:
campo:titolo
termine:gancio
frammento:
—————————–
10) Here Comes the Boom
punteggio: 0,357246781294941
Punti di impatto:
campo:titolo
termine:albero
frammento:
—————————–
11) Guarda anche chi parla
punteggio: 0,32331663301992025
Punti colpiti:
campo:titolo
termine:look
frammento:
—————————–
12) Senti chi parla
punteggio: 0,32331663301992025
Punti colpiti:
campo:titolo
termine:look
frammento:
—————————–
|
Come potete vedere, questo risultato è molto più vicino a quello che l’utente potrebbe aspettarsi. Notate che ora stiamo usando una classe chiamata DisjunctionQuery, le disgiunzioni sono un equivalente dell’operatore “OR” in SQL.
Che altro potremmo migliorare per ridurre l’effetto collaterale negativo di un algoritmo di corrispondenza fuzzy? Rianalizziamo il nostro problema per capire se ha bisogno di ulteriori miglioramenti:
Sappiamo già che le ricerche fuzzy possono produrre alcuni risultati inaspettati (ad esempio Book -> Look, Hook). Tuttavia, una ricerca per singolo termine è di solito una query terribile, poiché ci dà a malapena un indizio di ciò che esattamente l’utente sta cercando di realizzare.
Anche Google, che ha probabilmente uno degli algoritmi di ricerca fuzzy più sviluppati in uso, non sa esattamente cosa sto cercando quando cerco “tavolo”:
Quindi, qual è la lunghezza media delle parole chiave in una query di ricerca? Per rispondere a questa domanda, mostrerò un grafico tratto dalla presentazione di Rand Fishkin del 2016. (È uno dei guru più famosi del mondo SEO)
Secondo il grafico sopra, ~80% delle query di ricerca hanno 2 o più parole chiave, quindi proviamo a cercare il film “Black Book” usando la sfocatura 1:
1
2
3
4
5
6
|
String indexName = “movies_index”;
MatchQuery query = SearchQuery.match(“Black Book”).field(“title”).fuzziness(1);
SearchQueryResult result = movieRepository.getCouchbaseOperations().getCouchbaseBucket().query(
new SearchQuery(indexName, query).highlight().limit(12));
printResults(movies);
|
Risultato:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
1) Libro Nero
punteggio: 0.6946442424065404
Punti colpiti:
campo:titolo
termine:libro
frammento:
—————————–
campo:titolo
termine:nero
frammento:
—————————–
2) Hook
score: 0.40139742528039857
Hit Locations:
campo:titolo
termine:gancio
frammento:
—————————–
3) Attacca il blocco
punteggio: 0.2838308365090324
Luoghi colpiti:
campo:titolo
termine:blocco
frammento:
—————————–
4) Here Comes the Boom
punteggio: 0.2838308365090324
Hit Locations:
campo:titolo
termine:boom
frammento:
—————————–
5) Guarda chi parla troppo
punteggio: 0.25687349813115684
Luoghi colpiti:
campo:titolo
termine:guarda
frammento:
—————————–
6) Senti chi parla
punteggio: 0.25687349813115684
Località colpite:
campo:titolo
termine:guarda
frammento:
—————————–
7) Grosse Pointe Blank
score: 0.2317469073782136
Hit Locations:
campo:titolo
termine:vuoto
frammento:
—————————–
8) The Book Thief
score: 0.19059065534780495
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
9) The Book of Eli
score: 0.19059065534780495
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
10) The Book of Life
score: 0.19059065534780495
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
11) The Jungle Book
score: 0.19059065534780495
Hit Locations:
campo:titolo
termine:libro
frammento:
—————————–
12) Back to the Future
score: 0.17061000968368
Hit Locations:
campo:titolo
termine:indietro
frammento:
—————————–
|
Non male. Abbiamo ottenuto il film che stavamo cercando come primo risultato. Tuttavia, una query di disgiunzione porterebbe ancora un migliore insieme di risultati.
Ma ancora, sembra che abbiamo una nuova bella proprietà qui; l’effetto collaterale del fuzziness matching diminuisce leggermente all’aumentare del numero di parole chiave. Una ricerca per “Black Book” con fuzziness 1 può ancora portare risultati come back look o lack cook (alcune combinazioni con edit distance 1), ma è improbabile che questi siano veri titoli di film.
Una ricerca di “book eli” con fuzziness 2 lo porterebbe ancora come terzo risultato:
1
2
3
4
5
6
|
String indexName = “movies_index”;
MatchQuery query = SearchQuery.match(“libro eli”).field(“titolo”).fuzziness(2);
SearchQueryResult result = movieRepository.getCouchbaseOperations().getCouchbaseBucket().query(
new SearchQuery(indexName, query).highlight().limit(12));
printResults(movies);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
1) Ed Wood
punteggio: 0.030723793900031805
Punti colpiti:
campo:titolo
termine:legno
frammento:
—————————–
campo:titolo
termine:ed
frammento:
—————————–
2) Uomo di Tai Chi
punteggio: 0.0271042761982626
Punti colpiti:
campo:titolo
termine:chi
frammento:
—————————–
campo:titolo
termine:tai
frammento:
—————————–
3) The Book of Eli
score: 0.02608335441670756
Hit Locations:
campo:titolo
termine:eli
frammento:
—————————–
campo:titolo
termine:libro
frammento:
—————————–
4) The Good Lie
score: 0.02439822770591834
Hit Locations:
campo:titolo
termine:bugia
frammento:
—————————–
campo:titolo
termine:buono
frammento:
—————————–
|
Tuttavia, poiché la parola inglese media è lunga 5 lettere, NON raccomanderei di usare una distanza di modifica maggiore di 2 a meno che l’utente non stia cercando parole lunghe che sono facili da scrivere male, come “Schwarzenegger” per esempio (almeno per i non tedeschi o non austriaci).
Conclusione
In questo articolo, abbiamo discusso il fuzziness matching e come superare il suo principale effetto collaterale senza rovinare la sua rilevanza. Attenzione, l’abbinamento fuzzy è solo una delle tante caratteristiche di cui dovreste approfittare per implementare una ricerca pertinente e user-friendly. Ne discuteremo alcune durante questa serie: N-Grams, Stopwords, Steeming, Shingle, Elision. Etc.
Guardate anche la Parte 1 e la Parte 2 di questa serie.
Nel frattempo, se avete domande, twittatemi a @deniswsrosa.
Autore
- Denis Rosa, Developer Advocate, Couchbase –