Dopasowanie rozmyte pozwala na identyfikację niedokładnych dopasowań elementu docelowego. Jest to kamień węgielny wielu frameworków wyszukiwarek i jeden z głównych powodów, dla których można uzyskać odpowiednie wyniki wyszukiwania, nawet jeśli masz literówkę w zapytaniu lub inny czasownik.

Jak można się spodziewać, istnieje wiele algorytmów, które mogą być użyte do wyszukiwania rozmytego w tekście, ale praktycznie wszystkie wyszukiwarki (w tym bleve) używają głównie Dystansu Levenshteina do rozmytego dopasowywania łańcuchów znaków:

Odległość Levenshteina

Znana również jako Odległość Edycji, jest to liczba transformacji (usunięć, wstawek lub substytucji) wymaganych do przekształcenia łańcucha źródłowego w docelowy. Na przykład, jeśli terminem docelowym jest „książka”, a źródłem jest „z powrotem”, będziesz musiał zmienić pierwsze „o” na „a” i drugie „o” na „c”, co da nam odległość Levenshtein Distance równą 2.Edit Distance jest bardzo łatwy do wdrożenia i jest popularnym wyzwaniem podczas wywiadów z kodem (Możesz znaleźć implementacje Levenshtein w JavaScript, Kotlin, Java i wiele innych tutaj).

Dodatkowo, niektóre frameworki obsługują również odległość Damerau-Levenshtein:

Damerau-Levenshtein distance

Jest to rozszerzenie do Levenshtein Distance, pozwalające na jedną dodatkową operację: Transpozycja dwóch sąsiednich znaków:

Ex: TSAR to STAR

Damerau-Levenshtein distance = 1 (Zamiana pozycji S i T kosztuje tylko jedną operację)

Levenshtein distance = 2 (Zamień S na T i T na S)

Fuzzy matching and relevance

Fuzzy matching has one big side effect; it messes up with relevance. Chociaż Damerau-Levenshtein jest algorytm, który bierze pod uwagę większość wspólnych użytkowników błędnej pisowni, może również zawierać znaczną liczbę fałszywych pozytywów, zwłaszcza gdy używamy języka ze średnią tylko 5 liter na słowo, takich jak angielski. Dlatego też większość frameworków wyszukiwarek woli trzymać się odległości Levenshteina. Zobaczmy tego prawdziwy przykład:

Po pierwsze, użyjemy tego zestawu danych katalogu filmowego. Gorąco polecam, jeśli chcesz się pobawić wyszukiwaniem pełnotekstowym. Następnie, wyszukajmy filmy z „książką” w tytule. Prosty kod wyglądałby następująco:

Java

1
2

..

3
4
5
6

String indexName = „movies_index”;
MatchQuery query = SearchQuery.match(„book”).field(„title”);
SearchQueryResult result = movieRepository.getCouchbaseOperations().getCouchbaseBucket().query(
new SearchQuery(indexName, query).highlight().limit(6));
printResults(movies);

Powyższy kod przyniesie następujące wyniki:

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) The Book Thief
score: 4.826942606027416
Lokalizacje trafień:
pole:tytuł
termin:książka
fragment:
—————————–
2) The Book of Eli
score: 4.826942606027416
Hit Locations:
pole:tytuł
termin:książka
fragment:
—————————–
3) The Book of Life
score: 4.826942606027416
Hit Locations:
pole:tytuł
termin:książka
fragment:
—————————–
4) Black Book
score: 4.826942606027416
Hit Locations:
pole:tytuł
termin:książka
fragment:
—————————–
5) The Jungle Book
score: 4.826942606027416
Hit Locations:
pole:tytuł
termin:książka
fragment:
—————————–
6) The Jungle Book 2
score: 3.9411821308689627
Hit Locations:
pole:tytuł
termin:książka
fragment:
—————————–

Domyślnie wyniki nie uwzględniają wielkości liter, ale można łatwo zmienić to zachowanie, tworząc nowe indeksy z różnymi analizatorami.

Teraz dodajmy do naszego zapytania możliwość dopasowania rozmytego, ustawiając rozmycie jako 1 (odległość Levenshteina 1), co oznacza, że „książka” i „wygląd” będą miały taką samą trafność.

Java

1
2
3

.

4
5
6

String indexName = „movies_index”;
MatchQuery query = SearchQuery.match(„book”).field(„title”).fuzziness(1);
SearchQueryResult result = movieRepository.getCouchbaseOperations().getCouchbaseBucket().query(
new SearchQuery(indexName, query).highlight().limit(6));
printResults(movies);

A oto wynik wyszukiwania rozmytego:

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) Hak
punktacja: 1.1012248063242538
Lokalizacje trafień:
pole:tytuł
termin:hak
fragment:
—————————–
2) Here Comes the Boom
score: 0.7786835148361213
Hit Locations:
field:title
term:boom
fragment:
—————————–
3) Look Who’s Talking Too
score: 0.7047266634351538
Hit Locations:
field:title
term:look
fragment:
—————————–
4) Look Who’s Talking
score: 0.7047266634351538
Hit Locations:
field:title
term:look
fragment:
—————————–
5) The Book Thief
score: 0.52288117537184
Hit Locations:
pole:tytuł
termin:książka
fragment:
—————————–
6) The Book of Eli
score: 0.52288117537184
Hit Locations:
pole:tytuł
termin:książka
fragment:
—————————–

Teraz, film o nazwie „Hook” jest pierwszym wynikiem wyszukiwania, który może nie być dokładnie tym, czego oczekuje użytkownik szukający „książki”.

Jak zminimalizować fałszywe wyniki podczas wyszukiwania rozmytego

W idealnym świecie użytkownicy nigdy nie popełnialiby literówek podczas wyszukiwania. Jednak nie jest to świat, w którym żyjemy i jeśli chcesz, aby użytkownicy mieli przyjemne doświadczenie, musisz obsługiwać przynajmniej odległość edycyjną równą 1. Dlatego prawdziwe pytanie brzmi: Jak możemy dokonać rozmytego dopasowania ciągów znaków przy jednoczesnym zminimalizowaniu utraty trafności? Dopasowanie z mniejszą odległością edycyjną będzie zazwyczaj wyżej punktowane. Ta cecha pozwala nam połączyć te dwa zapytania o różnych poziomach rozmytości w jedno:

Java

1
2
3
4
5
6
7
8

String indexName = „movies_index”;
String word = „Książka”;
MatchQuery titleFuzzy = SearchQuery.match(word).fuzziness(1).field(„title”);
MatchQuery titleSimple = SearchQuery.match(word).field(„title”);
DisjunctionQuery ftsQuery = SearchQuery.disjuncts(titleSimple, titleFuzzy);
SearchQueryResult result = movieRepository.getCouchbaseOperations().getCouchbaseBucket().query(
new SearchQuery(indexName, ftsQuery).highlight().limit(20)); printResults(result);

Tutaj znajduje się wynik powyższego zapytania rozmytego:

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) The Book Thief
score: 2.398890092610849
Lokalizacje trafień:
pole:tytuł
termin:książka
fragment:
—————————–
field:title
term:book
fragment:
—————————–
2) The Book of Eli
score: 2.398890092610849
Hit Locations:
field:title
term:book
fragment:
—————————–
field:title
term:book
fragment:
—————————–
3) The Book of Life
score: 2.398890092610849
Hit Locations:
field:title
term:book
fragment:
—————————–
field:title
term:book
fragment:
—————————–
4) Black Book
score: 2.398890092610849
Hit Locations:
field:title
term:book
fragment:
—————————–
field:title
term:book
fragment:
—————————–
5) The Jungle Book
score: 2.398890092610849
Hit Locations:
field:title
term:book
fragment:
—————————–
field:title
term:book
fragment:
—————————–
6) The Jungle Book 2
score: 1.958685557004688
Hit Locations:
field:title
term:book
fragment:
—————————–
field:title
term:book
fragment:
—————————–
7) National Treasure: Book of Secrets
score: 1.6962714808368062
Hit Locations:
field:title
term:book
fragment:
—————————–
field:title
term:book
fragment:
—————————–
8) American Pie Presents: The Book of Love
score: 1.517191317611584
Hit Locations:
field:title
term:book
fragment:
—————————–
field:title
term:book
fragment:
—————————–
9) Hook
score: 0.5052232518679681
Hit Locations:
pole:tytuł
termin:hak
fragment:
—————————–
10) Here Comes the Boom
wynik: 0.357246781294941
Hit Locations:
field:title
term:tree
fragment:
—————————–
11) Look Who’s Talking Too
score: 0.32331663301992025
Hit Locations:
field:title
term:look
fragment:
—————————–
12) Look Who’s Talking
score: 0.32331663301992025
Hit Locations:
field:title
term:look
fragment:
—————————–

Jak widać, wynik ten jest znacznie bliższy temu, czego może oczekiwać użytkownik. Zauważ, że używamy teraz klasy zwanej DisjunctionQuery, disjunctions są odpowiednikiem operatora „OR” w SQL.

Co jeszcze moglibyśmy poprawić, aby zmniejszyć negatywny efekt uboczny algorytmu dopasowania rozmytego? Przeanalizujmy ponownie nasz problem, aby zrozumieć, czy wymaga on dalszych ulepszeń:

Wiemy już, że wyszukiwania rozmyte mogą dawać pewne nieoczekiwane wyniki (np. Książka -> Szukaj, Hook). Jednak wyszukiwanie pojedynczego terminu jest zazwyczaj okropnym zapytaniem, ponieważ ledwie daje nam wskazówkę, co dokładnie użytkownik próbuje osiągnąć.

Nawet Google, który ma prawdopodobnie jeden z najbardziej rozwiniętych algorytmów wyszukiwania rozmytego w użyciu, nie wie dokładnie, czego szukam, gdy wyszukuję „stół”:

Więc, jaka jest średnia długość słów kluczowych w zapytaniu wyszukiwania? Aby odpowiedzieć na to pytanie, pokażę wykres z prezentacji Randa Fishkina z 2016 roku. (Jest on jednym z najbardziej znanych guru w świecie SEO)

Według powyższego wykresu, ~80% zapytań wyszukiwania ma 2 lub więcej słów kluczowych, więc spróbujmy wyszukać film „Black Book” używając rozmycia 1:

Java

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

Result:

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) Czarna księga
punktacja: 0.6946442424065404
Lokalizacje trafień:
pole:tytuł
termin:książka
fragment:
—————————–
field:title
term:black
fragment:
—————————–
2) Hook
score: 0.40139742528039857
Hit Locations:
pole:tytuł
termin:hak
fragment:
—————————–
3) Attack the Block
score: 0.2838308365090324
Hit Locations:
pole:tytuł
termin:blok
fragment:
—————————–
4) Here Comes the Boom
score: 0.2838308365090324
Hit Locations:
field:title
term:boom
fragment:
—————————–
5) Look Who’s Talking Too
score: 0.25687349813115684
Hit Locations:
field:title
term:look
fragment:
—————————–
6) Look Who’s Talking
score: 0.25687349813115684
Hit Locations:
field:title
term:look
fragment:
—————————–
7) Grosse Pointe Blank
score: 0.2317469073782136
Hit Locations:
field:title
term:blank
fragment:
—————————–
8) The Book Thief
score: 0.19059065534780495
Hit Locations:
pole:tytuł
termin:książka
fragment:
—————————–
9) The Book of Eli
score: 0.19059065534780495
Hit Locations:
pole:tytuł
termin:książka
fragment:
—————————–
10) The Book of Life
score: 0.19059065534780495
Hit Locations:
field:title
term:book
fragment:
—————————–
11) The Jungle Book
score: 0.19059065534780495
Hit Locations:
field:title
term:book
fragment:
—————————–
12) Back to the Future
score: 0.17061000968368
Hit Locations:
pole:title
termin:back
fragment:
—————————–

Nieźle. Otrzymaliśmy film, którego szukaliśmy jako pierwszy wynik. Jednak zapytanie typu disjunction nadal przyniosłoby lepszy zestaw wyników.

Ale nadal, wygląda na to, że mamy tutaj nową fajną właściwość; efekt uboczny dopasowania rozmytości nieznacznie maleje wraz ze wzrostem liczby słów kluczowych. Wyszukiwanie „Black Book” z fuzziness 1 może nadal przynieść wyniki takie jak back look lub lack cook (niektóre kombinacje z edit distance 1), ale są one mało prawdopodobne, aby być prawdziwe tytuły filmów.

A search for „book eli” with fuzziness 2 would still bring it as the third result:

Java

1
2
3
4
5
6

String indexName = „movies_index”;
MatchQuery query = SearchQuery.match(„book eli”).field(„title”).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
score: 0.030723793900031805
Lokalizacje trafień:
field:title
term:wood
fragment:
—————————–
field:title
term:ed
fragment:
—————————–
2) Man of Tai Chi
score: 0.0271042761982626
Hit Locations:
pole:tytuł
termin:chi
fragment:
—————————–
field:title
term:tai
fragm:
—————————–
3) The Book of Eli
score: 0.02608335441670756
Hit Locations:
field:title
term:eli
fragment:
—————————–
field:title
term:book
fragment:
—————————–
4) The Good Lie
score: 0.02439822770591834
Hit Locations:
pole:tytuł
termin:kłamstwo
fragment:
—————————–
field:title
term:good
fragment:
—————————–

Jednakże, ponieważ przeciętne angielskie słowo ma długość 5 liter, NIE zalecałbym używania odległości edycyjnej większej niż 2, chyba że użytkownik szuka długich słów, które łatwo jest błędnie przeliterować, jak na przykład „Schwarzenegger” (przynajmniej dla nie-Niemców lub nie-Austriaków).

Podsumowanie

W tym artykule, omówiliśmy dopasowanie rozmyte i jak przezwyciężyć jego główny efekt uboczny, nie psując jego trafności. Należy pamiętać, że dopasowanie rozmyte jest tylko jedną z wielu funkcji, które należy wykorzystać podczas wdrażania trafnego i przyjaznego dla użytkownika wyszukiwania. W tej serii omówimy kilka z nich: N-Gramy, Stopwords, Steeming, Shingle, Elision. Etc.

Sprawdź również Część 1 i Część 2 tej serii.

W międzyczasie, jeśli masz jakieś pytania, tweetnij do mnie na @deniswsrosa.

Autor

  • Denis Rosa, Developer Advocate, Couchbase –

.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.