55 "fmt"
66 "net/http"
77 "regexp"
8+ "strconv"
89 "strings"
910 "time"
1011
@@ -53,26 +54,59 @@ func DiscountSearch(c *gin.Context) {
5354 ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
5455 defer cancel ()
5556
56- query , err := buildDiscountSearchQuery (c )
57- if err != nil {
58- respond (c , http .StatusBadRequest , "Invalid query parameters" , err .Error ())
59- return
60- }
57+ var cursor * mongo.Cursor
58+ var err error
6159
62- optionLimit , err := configs .GetOptionLimit (& query , c )
63- if err != nil {
64- respond (c , http .StatusBadRequest , "offset is not type integer" , err .Error ())
65- return
66- }
60+ _ , hasQ := c .GetQuery ("q" )
61+ _ , hasBusiness := c .GetQuery ("business" )
62+ _ , hasAddress := c .GetQuery ("address" )
63+ _ , hasDiscount := c .GetQuery ("discount" )
64+ _ , hasCategory := c .GetQuery ("category" )
65+ if hasQ {
66+ if hasBusiness || hasAddress || hasDiscount || hasCategory {
67+ // q may only be used alone
68+ respond (c , http .StatusBadRequest , "Invalid query parameters" , "Parameter q may not be used with other parameters" )
69+ return
70+ }
6771
68- cursor , err := discountCollection .Find (ctx , query , optionLimit )
69- if err != nil {
70- respondWithInternalError (c , err )
71- return
72+ pipeline , err := buildFuzzySearchPipeline (c )
73+ if err != nil {
74+ respond (c , http .StatusBadRequest , "Invalid query parameters" , err .Error ())
75+ return
76+ }
77+ cursor , err = discountCollection .Aggregate (ctx , pipeline )
78+ if err != nil {
79+ respondWithInternalError (c , err )
80+ return
81+ }
82+
83+ } else {
84+ if ! hasBusiness && ! hasAddress && ! hasDiscount && ! hasCategory {
85+ respond (c , http .StatusBadRequest , "Invalid query parameters" , "Unknown query" )
86+ return
87+ }
88+
89+ query , err := buildDiscountSearchQuery (c )
90+ if err != nil {
91+ respond (c , http .StatusBadRequest , "Invalid query parameters" , err .Error ())
92+ return
93+ }
94+
95+ optionLimit , err := configs .GetOptionLimit (& query , c )
96+ if err != nil {
97+ respond (c , http .StatusBadRequest , "offset is not type integer" , err .Error ())
98+ return
99+ }
100+ cursor , err = discountCollection .Find (ctx , query , optionLimit )
101+ if err != nil {
102+ respondWithInternalError (c , err )
103+ return
104+ }
72105 }
106+
73107 defer cursor .Close (ctx )
74108
75- discounts := make ( []schema.DiscountProgram , 0 )
109+ var discounts []schema.DiscountProgram
76110 if err = cursor .All (ctx , & discounts ); err != nil {
77111 respondWithInternalError (c , err )
78112 return
@@ -82,24 +116,15 @@ func DiscountSearch(c *gin.Context) {
82116
83117}
84118
119+ // Build the query for field-based search
85120func buildDiscountSearchQuery (c * gin.Context ) (bson.M , error ) {
86121 business , hasBusiness := c .GetQuery ("business" )
87122 address , hasAddress := c .GetQuery ("address" )
88123 discount , hasDiscount := c .GetQuery ("discount" )
89124 category , hasCategory := c .GetQuery ("category" )
90- q , hasQ := c .GetQuery ("q" )
91125
92126 query := bson.M {}
93127
94- if hasQ {
95- // q may only be used alone
96- if hasBusiness || hasAddress || hasDiscount || hasCategory {
97- return nil , fmt .Errorf ("parameter q may not be used with other parameters" )
98- }
99- query ["$text" ] = bson.D {{Key : "$search" , Value : q }}
100- return query , nil
101- }
102-
103128 // We use regexp.QuoteMeta and option i to essentially do string.toLower().contains(key) on fields
104129 if hasBusiness {
105130 cleanedBusiness := strings .TrimSpace (regexp .QuoteMeta (business ))
@@ -125,9 +150,72 @@ func buildDiscountSearchQuery(c *gin.Context) (bson.M, error) {
125150 }
126151 }
127152 if ! found {
128- return nil , fmt .Errorf ("unknown category %s. Valid categories are %s" , category , strings .Join (discountCategories , ", " ))
153+ return nil , fmt .Errorf ("unknown category %s. Valid categories are %s" , category , strings .Join (discountCategories , " | " ))
129154 }
130155 }
131156
132157 return query , nil
133158}
159+
160+ // Build the pipeline to perform fuzzy search on q
161+ func buildFuzzySearchPipeline (c * gin.Context ) (mongo.Pipeline , error ) {
162+ q , _ := c .GetQuery ("q" )
163+ if strings .TrimSpace (q ) == "" {
164+ return mongo.Pipeline {}, fmt .Errorf ("empty q" )
165+ }
166+
167+ var offset int64
168+ var err error
169+ if c .Query ("offset" ) == "" {
170+ offset = 0
171+ } else {
172+ offset , err = strconv .ParseInt (c .Query ("offset" ), 10 , 64 )
173+ if err != nil {
174+ return mongo.Pipeline {}, err
175+ }
176+ }
177+
178+ var fuzzySearchArr bson.A
179+ fields := [4 ]string {"category" , "discount" , "business" , "address" }
180+ maxEditsList := [4 ]int {2 , 2 , 2 , 1 }
181+ boostScores := [4 ]int {5 , 3 , 2 , 1 }
182+ for i , field := range fields {
183+ fuzzySearchArr = append (fuzzySearchArr , bson.D {
184+ {Key : "text" , Value : bson.D {
185+ {Key : "query" , Value : q },
186+ {Key : "path" , Value : field },
187+ {Key : "fuzzy" , Value : bson.D {
188+ {Key : "maxEdits" , Value : maxEditsList [i ]},
189+ }},
190+ {Key : "score" , Value : bson.D {
191+ {Key : "boost" , Value : bson.D {
192+ {Key : "value" , Value : boostScores [i ]},
193+ }},
194+ }},
195+ }},
196+ })
197+ }
198+
199+ return mongo.Pipeline {
200+ // Fuzzy searches
201+ bson.D {
202+ {Key : "$search" , Value : bson.D {
203+ {Key : "index" , Value : "discount_searches" },
204+ {Key : "compound" , Value : bson.D {
205+ {Key : "should" , Value : fuzzySearchArr },
206+ }},
207+ }},
208+ },
209+
210+ // Sort and paginate
211+ bson.D {
212+ {Key : "$sort" , Value : bson.D {
213+ {Key : "score" , Value : bson.D {
214+ {Key : "$meta" , Value : "searchScore" },
215+ }},
216+ }},
217+ },
218+ bson.D {{Key : "$skip" , Value : offset }},
219+ bson.D {{Key : "$limit" , Value : configs .GetEnvLimit ()}},
220+ }, nil
221+ }
0 commit comments