Skip to content

Commit 18efbc5

Browse files
Merge pull request #18 from filipecosta90/spellcheck
[add] added support for FT.SPELLCHECK
2 parents eeb2e42 + f5704d7 commit 18efbc5

File tree

3 files changed

+221
-1
lines changed

3 files changed

+221
-1
lines changed

redisearch/client.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,49 @@ func (i *Client) Search(q *Query) (docs []Document, total int, err error) {
346346
return
347347
}
348348

349+
// SpellCheck performs spelling correction on a query, returning suggestions for misspelled terms,
350+
// the total number of results, or an error if something went wrong
351+
func (i *Client) SpellCheck(q *Query, s *SpellCheckOptions) (suggs []MisspelledTerm, total int, err error) {
352+
conn := i.pool.Get()
353+
defer conn.Close()
354+
355+
args := redis.Args{i.name}
356+
args = append(args, q.serialize()...)
357+
args = append(args, s.serialize()...)
358+
359+
res, err := redis.Values(conn.Do("FT.SPELLCHECK", args...))
360+
if err != nil {
361+
return
362+
}
363+
total = 0
364+
suggs = make([]MisspelledTerm, 0 )
365+
366+
// Each misspelled term, in turn, is a 3-element array consisting of
367+
// - the constant string "TERM" ( 3-element position 0 -- we dont use it )
368+
// - the term itself ( 3-element position 1 )
369+
// - an array of suggestions for spelling corrections ( 3-element position 2 )
370+
termIdx := 1
371+
suggIdx := 2
372+
for i := 0; i < len(res); i ++ {
373+
var termArray []interface{} = nil
374+
termArray, err = redis.Values(res[i], nil)
375+
if err != nil {
376+
return
377+
}
378+
379+
if d, e := loadMisspelledTerm(termArray, termIdx, suggIdx); e == nil {
380+
suggs = append(suggs, d)
381+
if d.Len() > 0 {
382+
total++
383+
}
384+
} else {
385+
log.Print("Error parsing misspelled suggestion: ", e)
386+
}
387+
}
388+
389+
return
390+
}
391+
349392
// Aggregate
350393
func (i *Client) Aggregate( q *AggregateQuery ) ( aggregateReply [][]string, total int, err error) {
351394
conn := i.pool.Get()
@@ -389,7 +432,6 @@ func (i *Client) Aggregate( q *AggregateQuery ) ( aggregateReply [][]string, tot
389432
return
390433
}
391434

392-
393435
// Explain Return a textual string explaining the query
394436
func (i *Client) Explain(q *Query) (string, error) {
395437
conn := i.pool.Get()

redisearch/redisearch_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,47 @@ func TestDelete(t *testing.T) {
383383
assert.Equal(t, uint64(0), info.DocCount)
384384
}
385385

386+
func TestSpellCheck(t *testing.T) {
387+
c := createClient("testung")
388+
countries := []string{"Spain", "Israel", "Portugal", "France", "England", "Angola"}
389+
sc := redisearch.NewSchema(redisearch.DefaultOptions).
390+
AddField(redisearch.NewTextField("country"))
391+
c.Drop()
392+
assert.Nil(t, c.CreateIndex(sc))
393+
394+
docs := make([]redisearch.Document, len(countries))
395+
396+
for i := 0; i < len(countries); i++ {
397+
docs[i] = redisearch.NewDocument(fmt.Sprintf("doc%d", i), 1).Set("country", countries[i])
398+
}
399+
400+
assert.Nil(t, c.Index(docs...))
401+
query := redisearch.NewQuery("Anla Portuga" )
402+
opts := redisearch.NewSpellCheckOptions(2 )
403+
sugs, total, err := c.SpellCheck(query,opts )
404+
assert.Nil(t, err)
405+
assert.Equal(t, 2, len(sugs))
406+
assert.Equal(t, 2, total)
407+
408+
// query that return the MisspelledTerm but with an empty list of suggestions
409+
// 1) 1) "TERM"
410+
// 2) "an"
411+
// 3) (empty list or set)
412+
queryEmpty := redisearch.NewQuery("An" )
413+
sugs, total, err = c.SpellCheck(queryEmpty,opts )
414+
assert.Nil(t, err)
415+
assert.Equal(t, 1, len(sugs))
416+
assert.Equal(t, 0, total)
417+
418+
// same query but now with a distance of 4
419+
opts.SetDistance(4)
420+
sugs, total, err = c.SpellCheck(queryEmpty,opts )
421+
assert.Nil(t, err)
422+
assert.Equal(t, 1, len(sugs))
423+
assert.Equal(t, 1, total)
424+
425+
}
426+
386427
func ExampleClient() {
387428

388429
// Create a client. By default a client is schemaless

redisearch/spellcheck.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package redisearch
2+
3+
import (
4+
"fmt"
5+
"github.com/gomodule/redigo/redis"
6+
"sort"
7+
)
8+
9+
// SpellCheckOptions are options which are passed when performing spelling correction on a query
10+
type SpellCheckOptions struct {
11+
Distance int
12+
ExclusionDicts []string
13+
InclusionDicts []string
14+
}
15+
16+
func NewSpellCheckOptionsDefaults() *SpellCheckOptions {
17+
return &SpellCheckOptions{
18+
Distance: 1,
19+
ExclusionDicts: make([]string, 0),
20+
InclusionDicts: make([]string, 0),
21+
}
22+
}
23+
24+
func NewSpellCheckOptions(distance int) *SpellCheckOptions {
25+
return &SpellCheckOptions{
26+
Distance: distance,
27+
ExclusionDicts: make([]string, 0),
28+
InclusionDicts: make([]string, 0),
29+
}
30+
}
31+
32+
// SetDistance Sets the the maximal Levenshtein distance for spelling suggestions (default: 1, max: 4)
33+
func (s *SpellCheckOptions) SetDistance(distance int) (*SpellCheckOptions, error) {
34+
if distance < 1 || distance > 4 {
35+
return s, fmt.Errorf("The maximal Levenshtein distance for spelling suggestions should be between [1,4]. Got %d", distance)
36+
} else {
37+
s.Distance = distance
38+
}
39+
return s, nil
40+
}
41+
42+
// AddExclusionDict adds a custom dictionary named {dictname} to the exclusion list
43+
func (s *SpellCheckOptions) AddExclusionDict(dictname string) *SpellCheckOptions {
44+
s.ExclusionDicts = append(s.ExclusionDicts, dictname)
45+
return s
46+
}
47+
48+
// AddInclusionDict adds a custom dictionary named {dictname} to the inclusion list
49+
func (s *SpellCheckOptions) AddInclusionDict(dictname string) *SpellCheckOptions {
50+
s.InclusionDicts = append(s.InclusionDicts, dictname)
51+
return s
52+
}
53+
54+
func (s SpellCheckOptions) serialize() redis.Args {
55+
args := redis.Args{}
56+
if s.Distance > 1 {
57+
args = args.Add("DISTANCE").Add(s.Distance)
58+
}
59+
for _, exclusion := range s.ExclusionDicts {
60+
args = args.Add("TERMS").Add("EXCLUDE").Add(exclusion)
61+
}
62+
for _, inclusion := range s.InclusionDicts {
63+
args = args.Add("TERMS").Add("INCLUDE").Add(inclusion)
64+
}
65+
return args
66+
}
67+
68+
// MisspelledSuggestion is a single suggestion from the spelling corrections
69+
type MisspelledSuggestion struct {
70+
Suggestion string
71+
Score float32
72+
}
73+
74+
// NewMisspelledSuggestion creates a MisspelledSuggestion with the specific term and score
75+
func NewMisspelledSuggestion(term string, score float32) MisspelledSuggestion {
76+
return MisspelledSuggestion{
77+
Suggestion: term,
78+
Score: score,
79+
}
80+
}
81+
82+
// MisspelledTerm contains the misspelled term and a sortable list of suggestions returned from an engine
83+
type MisspelledTerm struct {
84+
Term string
85+
// MisspelledSuggestionList is a sortable list of suggestions returned from an engine
86+
MisspelledSuggestionList []MisspelledSuggestion
87+
}
88+
89+
func NewMisspelledTerm(term string) MisspelledTerm {
90+
return MisspelledTerm{
91+
Term: term,
92+
MisspelledSuggestionList: make([]MisspelledSuggestion, 0),
93+
}
94+
}
95+
96+
func (l MisspelledTerm) Len() int { return len(l.MisspelledSuggestionList) }
97+
func (l MisspelledTerm) Swap(i, j int) {
98+
l.MisspelledSuggestionList[i], l.MisspelledSuggestionList[j] = l.MisspelledSuggestionList[j], l.MisspelledSuggestionList[i]
99+
}
100+
func (l MisspelledTerm) Less(i, j int) bool {
101+
return l.MisspelledSuggestionList[i].Score > l.MisspelledSuggestionList[j].Score
102+
} //reverse sorting
103+
104+
// Sort the SuggestionList
105+
func (l MisspelledTerm) Sort() {
106+
sort.Sort(l)
107+
}
108+
109+
// convert the result from a redis spelling correction on a query to a proper MisspelledTerm object
110+
func loadMisspelledTerm(arr []interface{}, termIdx, suggIdx int) (missT MisspelledTerm, err error) {
111+
term, err := redis.String(arr[termIdx], err)
112+
if err != nil {
113+
return MisspelledTerm{}, fmt.Errorf("Could not parse term: %s", err)
114+
}
115+
missT = NewMisspelledTerm(term)
116+
lst, err := redis.Values(arr[suggIdx], err)
117+
if err != nil {
118+
return MisspelledTerm{}, fmt.Errorf("Could not get the array of suggestions for spelling corrections on term %s. Error: %s", term, err)
119+
}
120+
for i := 0; i < len(lst); i++ {
121+
innerLst, err := redis.Values(lst[i], err)
122+
if err != nil {
123+
return MisspelledTerm{}, fmt.Errorf("Could not get the inner array of suggestions for spelling corrections on term %s. Error: %s", term, err)
124+
}
125+
score, err := redis.Float64(innerLst[0], err)
126+
if err != nil {
127+
return MisspelledTerm{}, fmt.Errorf("Could not parse score: %s", err)
128+
}
129+
suggestion, err := redis.String(innerLst[1], err)
130+
if err != nil {
131+
return MisspelledTerm{}, fmt.Errorf("Could not parse suggestion: %s", err)
132+
}
133+
missT.MisspelledSuggestionList = append(missT.MisspelledSuggestionList, NewMisspelledSuggestion(suggestion, float32(score)))
134+
}
135+
136+
return missT, nil
137+
}

0 commit comments

Comments
 (0)