8 // Exact string search, returns first N matches (nondeterministic)
9 func (db *TitlesDatabase) ExactSearchN(s string, n int) (matches SearchMatches) {
10 return db.doSearchN(func(k string) bool { return s == k }, n)
13 // Exact string search (nondeterministic order)
14 func (db *TitlesDatabase) ExactSearchAll(s string) (matches SearchMatches) {
15 return db.ExactSearchN(s, -1)
18 // Exact string search, returns first match (nondeterministic)
19 func (db *TitlesDatabase) ExactSearch(s string) (m SearchMatch) {
20 return firstMatch(db.ExactSearchN(s, 1))
23 // String search with case folding, returns first N matches (nondeterministic)
24 func (db *TitlesDatabase) ExactSearchFoldN(s string, n int) (matches SearchMatches) {
25 return db.doSearchN(func(k string) bool { return strings.EqualFold(k, s) }, n)
28 // String search with case folding (nondeterministic order)
29 func (db *TitlesDatabase) ExactSearchFoldAll(s string) (matches SearchMatches) {
30 return db.ExactSearchFoldN(s, -1)
33 // String search with case folding, returns first match (nondeterministic)
34 func (db *TitlesDatabase) ExactSearchFold(s string) (m SearchMatch) {
35 return firstMatch(db.ExactSearchFoldN(s, 1))
38 // Regular expression search, returns first N matches (nondeterministic)
39 func (db *TitlesDatabase) RegexpSearchN(re *regexp.Regexp, n int) (matches SearchMatches) {
40 return db.doSearchN(func(k string) bool { return re.MatchString(k) }, n)
43 // Regular expression search (nondeterministic order)
44 func (db *TitlesDatabase) RegexpSearchAll(re *regexp.Regexp) (matches SearchMatches) {
45 return db.RegexpSearchN(re, -1)
48 // Regular expression search, returns first match (nondeterministic)
49 func (db *TitlesDatabase) RegexpSearch(re *regexp.Regexp) (m SearchMatch) {
50 return firstMatch(db.RegexpSearchN(re, 1))
53 // Prefix exact string search, returns first N matches (nondeterministic)
54 func (db *TitlesDatabase) PrefixSearchN(s string, n int) (matches SearchMatches) {
55 return db.doSearchN(func(k string) bool { return strings.HasPrefix(k, s) }, n)
58 // Prefix exact string search (nondeterministic order)
59 func (db *TitlesDatabase) PrefixSearchAll(s string) (matches SearchMatches) {
60 return db.PrefixSearchN(s, -1)
63 // Prefix exact string search, returns first match (nondeterministic)
64 func (db *TitlesDatabase) PrefixSearch(s string) (m SearchMatch) {
65 return firstMatch(db.PrefixSearchN(s, 1))
68 // Suffix exact string search, returns first N matches (nondeterministic)
69 func (db *TitlesDatabase) SuffixSearchN(s string, n int) (matches SearchMatches) {
70 return db.doSearchN(func(k string) bool { return strings.HasSuffix(k, s) }, n)
73 // Suffix exact string search (nondeterministic order)
74 func (db *TitlesDatabase) SuffixSearchAll(s string) (matches SearchMatches) {
75 return db.SuffixSearchN(s, -1)
78 // Suffix exact string search, returns first match (nondeterministic)
79 func (db *TitlesDatabase) SuffixSearch(s string) (m SearchMatch) {
80 return firstMatch(db.SuffixSearchN(s, 1))
83 // Prefix string search with case folding, returns first N matches (nondeterministic)
84 func (db *TitlesDatabase) PrefixSearchFoldN(s string, n int) (matches SearchMatches) {
85 s = strings.ToLower(s)
86 return db.doSearchN(func(k string) bool { return strings.HasPrefix(strings.ToLower(k), s) }, n)
89 // Prefix string search with case folding (nondeterministic order)
90 func (db *TitlesDatabase) PrefixSearchFoldAll(s string) (matches SearchMatches) {
91 return db.PrefixSearchFoldN(s, -1)
94 // Prefix string search with case folding, returns first match (nondeterministic)
95 func (db *TitlesDatabase) PrefixSearchFold(s string) (m SearchMatch) {
96 return firstMatch(db.PrefixSearchFoldN(s, 1))
99 // Suffix string search with case folding, returns first N matches (nondeterministic)
100 func (db *TitlesDatabase) SuffixSearchFoldN(s string, n int) (matches SearchMatches) {
101 s = strings.ToLower(s)
102 return db.doSearchN(func(k string) bool { return strings.HasSuffix(strings.ToLower(k), s) }, n)
105 // Suffix string search with case folding (nondeterministic order)
106 func (db *TitlesDatabase) SuffixSearchFoldAll(s string) (matches SearchMatches) {
107 return db.SuffixSearchFoldN(s, -1)
110 // Suffix string search with case folding, returns first match (nondeterministic)
111 func (db *TitlesDatabase) SuffixSearchFold(s string) (m SearchMatch) {
112 return firstMatch(db.SuffixSearchFoldN(s, 1))
115 // \b doesn't consider the boundary between e.g. '.' and ' ' in ". "
116 // to be a word boundary, but . may be significant in a title
117 const wordBound = ` `
119 // Fuzzy string search with algorithm similar to the official Chii[AR] IRC bot.
121 // First attempts an exact search. Otherwise, uses strings.Fields to split the string
122 // into words and tries, in order, the following alternate matches:
124 // * Initial words (prefix, but ending at word boundary)
126 // * Final words (suffix, but starting at word boundary)
134 // * Initial words in the given order, but with possible words between them
136 // * Final words in the given order
138 // * Infix words in the given order
140 // * Initial strings in the given order, but with other possible strings between them
142 // * Final strings in the given order
144 // * Any match with strings in the given order
146 // Failing all those cases, the search returns a nil ResultSet.
147 func (db *TitlesDatabase) FuzzySearch(s string) (rs ResultSet) {
149 if matches := db.ExactSearchAll(s); len(matches) > 0 {
150 // log.Printf("best case: %q", s)
151 return matches.ToResultSet(db)
154 // all regexes are guaranteed to compile:
155 // the user-supplied token already went through regexp.QuoteMeta
156 // all other tokens are hardcoded, so a compilation failure is reason for panic
158 words := strings.Fields(regexp.QuoteMeta(s))
159 q := strings.Join(words, `.*`)
161 candidates := db.RegexpSearchAll(regexp.MustCompile(q)).ToResultSet(db)
162 if len(candidates) == 0 {
163 // log.Printf("no results: %q", s)
166 q = strings.Join(words, ` `)
168 // initial words (prefix, but ending at word boundary)
169 re := regexp.MustCompile(`\A` + q + wordBound)
170 reCmp := func(k string) bool { return re.MatchString(k) }
171 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
172 // log.Printf("1st case: %q", s)
176 // final words (suffix, but starting at a word boundary)
177 re = regexp.MustCompile(wordBound + q + `\z`)
178 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
179 // log.Printf("2nd case: %q", s)
184 re = regexp.MustCompile(wordBound + q + wordBound)
185 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
186 // log.Printf("3rd case: %q", s)
191 if rs = candidates.FilterByTitles(
192 func(k string) bool {
193 return strings.HasPrefix(k, s)
195 // log.Printf("4th case: %q", s)
199 // terminal substring
200 if rs = candidates.FilterByTitles(
201 func(k string) bool {
202 return strings.HasSuffix(k, s)
204 // log.Printf("5th case: %q", s)
208 // words in that order, but with possible words between them...
209 q = strings.Join(words, ` +(?:[^ ]+ +)*`)
212 re = regexp.MustCompile(`\A` + q + wordBound)
213 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
214 // log.Printf("6th case: %q", s)
218 // ... then final ...
219 re = regexp.MustCompile(wordBound + q + `\z`)
220 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
221 // log.Printf("7th case: %q", s)
226 re = regexp.MustCompile(wordBound + q + wordBound)
227 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
228 // log.Printf("8th case: %q", s)
232 // then it's that, but with any or no characters between the input words...
233 q = strings.Join(words, `.*`)
235 // and the same priority order as for the substring case
237 re = regexp.MustCompile(`\A` + q)
238 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
239 // log.Printf("9th case: %q", s)
244 re = regexp.MustCompile(q + `\z`)
245 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
246 // log.Printf("10th case: %q", s)
250 // no result better than the inital candidates
251 // log.Printf("worst case: %q", s)
255 // Version with case folding of FuzzySearch.
257 // See the FuzzySearch documentation for details.
258 func (db *TitlesDatabase) FuzzySearchFold(s string) (rs ResultSet) {
260 if matches := db.ExactSearchFoldAll(s); len(matches) > 0 {
261 return matches.ToResultSet(db)
264 words := strings.Fields(`(?i:` + regexp.QuoteMeta(s) + `)`)
265 q := strings.Join(words, `.*`)
267 candidates := db.RegexpSearchAll(regexp.MustCompile(q)).ToResultSet(db)
268 if len(candidates) == 0 {
269 // log.Printf("no results: %q", s)
272 q = strings.Join(words, `\s+`)
274 // initial words (prefix, but ending at word boundary)
275 re := regexp.MustCompile(`\A` + q + wordBound)
276 reCmp := func(k string) bool { return re.MatchString(k) }
277 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
278 // log.Printf("1st case: %q", s)
282 // final words (suffix, but starting at a word boundary)
283 re = regexp.MustCompile(wordBound + q + `\z`)
284 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
285 // log.Printf("2nd case: %q", s)
290 re = regexp.MustCompile(wordBound + q + wordBound)
291 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
292 // log.Printf("3rd case: %q", s)
297 ls := strings.ToLower(s)
298 if rs = candidates.FilterByTitles(
299 func(k string) bool {
300 return strings.HasPrefix(strings.ToLower(k), ls)
302 // log.Printf("4th case: %q", s)
306 // terminal substring
307 if rs = candidates.FilterByTitles(
308 func(k string) bool {
309 return strings.HasSuffix(strings.ToLower(k), ls)
311 // log.Printf("5th case: %q", s)
315 // words in that order, but with possible words between them...
316 q = strings.Join(words, `\s+(?:\S+\s+)*`)
319 re = regexp.MustCompile(`\A` + q + wordBound)
320 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
321 // log.Printf("6th case: %q", s)
325 // ... then final ...
326 re = regexp.MustCompile(wordBound + q + `\z`)
327 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
328 // log.Printf("7th case: %q", s)
333 re = regexp.MustCompile(wordBound + q + wordBound)
334 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
335 // log.Printf("8th case: %q", s)
339 // then it's that, but with any or no characters between the input words...
340 q = strings.Join(words, `.*`)
342 // and the same priority order as for the substring case
344 re = regexp.MustCompile(`\A` + q)
345 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
346 // log.Printf("9th case: %q", s)
351 re = regexp.MustCompile(q + `\z`)
352 if rs = candidates.FilterByTitles(reCmp); len(rs) > 0 {
353 // log.Printf("10th case: %q", s)
357 // no result better than the inital candidates
358 // log.Printf("worst case: %q", s)