History

Optimize search

Thibaut committed 12 years ago

Commit a3aa03b1 parent 50c18b22 files · +115 −97Browse files

Unified · Split

2 files
  1. assets/javascripts/app/searcher.coffee +109 −91
  2. assets/javascripts/debug.js.coffee +6 −6
assets/javascripts/app/searcher.coffee+109−91
@@ -1,8 +1,99 @@
1+#
2+# Match functions
3+#
4+
5+SEPARATOR = '.'
6+
7+query =
8+queryLength =
9+value =
10+valueLength =
11+matcher = # current match function
12+fuzzyRegexp = # query fuzzy regexp
13+index = # position of the query in the string being matched
14+lastIndex = # last position of the query in the string being matched
15+match = # regexp match data
16+score = # score for the current match
17+separators = # counter
18+i = null # cursor
19+
20+`function exactMatch() {`
21+index = value.indexOf(query)
22+return unless index >= 0
23+
24+lastIndex = value.lastIndexOf(query)
25+
26+if index isnt lastIndex
27+ return Math.max(scoreExactMatch(), (index = lastIndex) and scoreExactMatch())
28+else
29+ return scoreExactMatch()
30+`}`
31+
32+`function scoreExactMatch() {`
33+# Remove one point for each unmatched character.
34+score = 100 - (valueLength - queryLength)
35+
36+if index > 0
37+ # If the character preceding the query is a dot, assign the same score
38+ # as if the query was found at the beginning of the string, minus one.
39+ if value.charAt(index - 1) is SEPARATOR
40+ score += index - 1
41+ # Don't match a single-character query unless it's found at the beginning
42+ # of the string or is preceded by a dot.
43+ else if queryLength is 1
44+ return
45+ # (1) Remove one point for each unmatched character up to the nearest
46+ # preceding dot or the beginning of the string.
47+ # (2) Remove one point for each unmatched character following the query.
48+ else
49+ i = index - 2
50+ i-- while i >= 0 and value.charAt(i) isnt SEPARATOR
51+ score -= (index - i) + # (1)
52+ (valueLength - queryLength - index) # (2)
53+
54+ # Remove one point for each dot preceding the query, except for the one
55+ # immediately before the query.
56+ separators = 0
57+ i = index - 2
58+ while i >= 0
59+ separators++ if value.charAt(i) is SEPARATOR
60+ i--
61+ score -= separators
62+
63+# Remove five points for each dot following the query.
64+separators = 0
65+i = valueLength - queryLength - index - 1
66+while i >= 0
67+ separators++ if value.charAt(index + queryLength + i) is SEPARATOR
68+ i--
69+score -= separators * 5
70+
71+return Math.max 1, score
72+`}`
73+
74+`function fuzzyMatch() {`
75+return if valueLength <= queryLength or value.indexOf(query) >= 0
76+return unless match = fuzzyRegexp.exec(value)
77+
78+# When the match is at the beginning of the string or preceded by a dot.
79+if match.index is 0 or value.charAt(match.index - 1) is SEPARATOR
80+ return Math.max 66, 100 - match[0].length
81+# When the match is at the end of the string.
82+else if match.index + match[0].length is valueLength
83+ return Math.max 33, 67 - match[0].length
84+# When the match is in the middle of the string.
85+else
86+ return Math.max 1, 34 - match[0].length
87+`}`
88+
89+#
90+# Searchers
91+#
92+
193 class app.Searcher
294 $.extend @prototype, Events
395
496 CHUNK_SIZE = 20000
5− SEPARATOR = '.'
697
798 DEFAULTS =
899 max_results: app.config.max_results
@@ -11,34 +102,36 @@ class app.Searcher · 2 unmodified lines
11102 constructor: (options = {}) ->
12103 @options = $.extend {}, DEFAULTS, options
13104
14− find: (data, attr, query) ->
105+ find: (data, attr, q) ->
15106 @kill()
16107
17108 @data = data
18109 @attr = attr
19− @query = query
110+ @query = q
20111 @setup()
21112
22113 if @isValid() then @match() else @end()
23114 return
24115
25116 setup: ->
26− @query = @normalizeQuery @query
27− @queryLength = @query.length
117+ query = @query = @normalizeQuery(@query)
118+ queryLength = query.length
28119 @dataLength = @data.length
29− @matchers = ['exactMatch']
120+ @matchers = [exactMatch]
30121 @totalResults = 0
31122 @setupFuzzy()
32123 return
33124
34125 setupFuzzy: ->
35− if @queryLength >= @options.fuzzy_min_length
36− @fuzzyRegexp = @queryToFuzzyRegexp @query
37− @matchers.push 'fuzzyMatch'
126+ if queryLength >= @options.fuzzy_min_length
127+ fuzzyRegexp = @queryToFuzzyRegexp(query)
128+ @matchers.push(fuzzyMatch)
129+ else
130+ fuzzyRegexp = null
38131 return
39132
40133 isValid: ->
41− @queryLength > 0
134+ queryLength > 0
42135
43136 end: ->
44137 @triggerResults [] unless @totalResults
@@ -53,9 +146,8 @@ class app.Searcher · 8 unmodified lines
53146 return
54147
55148 free: ->
56− @data = @attr = @query = @queryLength = @dataLength =
57− @fuzzyRegexp = @matchers = @totalResults = @scoreMap =
58− @cursor = @matcher = @timeout = null
149+ @data = @attr = @dataLength = @matchers = @matcher = @query =
150+ @totalResults = @scoreMap = @cursor = @timeout = null
59151 return
60152
61153 match: =>
@@ -82,8 +174,11 @@ class app.Searcher · 20 unmodified lines
82174 return
83175
84176 matchChunk: ->
177+ matcher = @matcher
85178 for [0...@chunkSize()]
86− if score = @[@matcher](@data[@cursor][@attr])
179+ value = @data[@cursor][@attr]
180+ valueLength = value.length
181+ if score = matcher()
87182 @addResult @data[@cursor], score
88183 @cursor++
89184 return
@@ -131,83 +226,6 @@ class app.Searcher · 41 unmodified lines
131226 chars[i] = $.escapeRegexp(char) for char, i in chars
132227 new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/
133228
134− #
135− # Match functions
136− #
137−
138− index = # position of the query in the string being matched
139− lastIndex = # last position of the query in the string being matched
140− match = # regexp match data
141− score = # score for the current match
142− separators = # counter
143− i = null # cursor
144−
145− exactMatch: (value) ->
146− index = value.indexOf @query
147− return unless index >= 0
148−
149− lastIndex = value.lastIndexOf @query
150−
151− if index isnt lastIndex
152− Math.max(@scoreExactMatch(value, index), @scoreExactMatch(value, lastIndex))
153− else
154− @scoreExactMatch(value, index)
155−
156− scoreExactMatch: (value, index) ->
157− # Remove one point for each unmatched character.
158− score = 100 - (value.length - @queryLength)
159−
160− if index > 0
161− # If the character preceding the query is a dot, assign the same score
162− # as if the query was found at the beginning of the string, minus one.
163− if value[index - 1] is SEPARATOR
164− score += index - 1
165− # Don't match a single-character query unless it's found at the beginning
166− # of the string or is preceded by a dot.
167− else if @queryLength is 1
168− return
169− # (1) Remove one point for each unmatched character up to the nearest
170− # preceding dot or the beginning of the string.
171− # (2) Remove one point for each unmatched character following the query.
172− else
173− i = index - 2
174− i-- while i >= 0 and value[i] isnt SEPARATOR
175− score -= (index - i) + # (1)
176− (value.length - @queryLength - index) # (2)
177−
178− # Remove one point for each dot preceding the query, except for the one
179− # immediately before the query.
180− separators = 0
181− i = index - 2
182− while i >= 0
183− separators++ if value[i] is SEPARATOR
184− i--
185− score -= separators
186−
187− # Remove five points for each dot following the query.
188− separators = 0
189− i = value.length - @queryLength - index - 1
190− while i >= 0
191− separators++ if value[index + @queryLength + i] is SEPARATOR
192− i--
193− score -= separators * 5
194−
195− Math.max 1, score
196−
197− fuzzyMatch: (value) ->
198− return if value.length <= @queryLength or value.indexOf(@query) >= 0
199− return unless match = @fuzzyRegexp.exec(value)
200−
201− # When the match is at the beginning of the string or preceded by a dot.
202− if match.index is 0 or value[match.index - 1] is SEPARATOR
203− Math.max 66, 100 - match[0].length
204− # When the match is at the end of the string.
205− else if match.index + match[0].length is value.length
206− Math.max 33, 67 - match[0].length
207− # When the match is in the middle of the string.
208− else
209− Math.max 1, 34 - match[0].length
210−
211229 class app.SynchronousSearcher extends app.Searcher
212230 match: =>
213231 if @matcher
assets/javascripts/debug.js.coffee+6−6
@@ -35,15 +35,15 @@ app.Searcher = -> · 34 unmodified lines
3535 _match = @match.bind(@)
3636 @match = =>
3737 if @matcher
38− console.timeEnd @matcher
39− if @matcher is 'exactMatch'
40− for entries, score in @scoreMap by -1 when entries
41− console.log '' + score + ': ' + entries.map((entry) -> entry.text).join("\n ")
38+ console.timeEnd @matcher.name
39+ if @matcher.name is 'exactMatch'
40+ for entries, score in @scoreMap by -1 when entries
41+ console.log '' + score + ': ' + entries.map((entry) -> entry.text).join("\n ")
4242 _match()
4343
4444 _setupMatcher = @setupMatcher.bind(@)
4545 @setupMatcher = ->
46− console.time @matcher
46+ console.time @matcher.name
4747 _setupMatcher()
4848
4949 _end = @end.bind(@)
@@ -56,7 +56,7 @@ app.Searcher = -> · 6 unmodified lines
5656 _kill = @kill.bind(@)
5757 @kill = ->
5858 if @timeout
59− console.timeEnd @matcher if @matcher
59+ console.timeEnd @matcher.name if @matcher
6060 console.groupEnd()
6161 console.timeEnd 'Total'
6262 console.warn 'Killed'