History

Optimize search

Thibaut committed 12 years ago

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

Showing the changes to assets/javascripts (2 of 2 files). Show all

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
5SEPARATOR = '.'
6
7query =
8queryLength =
9value =
10valueLength =
11matcher = # current match function
12fuzzyRegexp = # query fuzzy regexp
13index = # position of the query in the string being matched
14lastIndex = # last position of the query in the string being matched
15match = # regexp match data
16score = # score for the current match
17separators = # counter
18i = null # cursor
19
20`function exactMatch() {`
21index = value.indexOf(query)
22return unless index >= 0
23
24lastIndex = value.lastIndexOf(query)
25
26if index isnt lastIndex
27 return Math.max(scoreExactMatch(), (index = lastIndex) and scoreExactMatch())
28else
29 return scoreExactMatch()
30`}`
31
32`function scoreExactMatch() {`
33# Remove one point for each unmatched character.
34score = 100 - (valueLength - queryLength)
35
36if 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.
64separators = 0
65i = valueLength - queryLength - index - 1
66while i >= 0
67 separators++ if value.charAt(index + queryLength + i) is SEPARATOR
68 i--
69score -= separators * 5
70
71return Math.max 1, score
72`}`
73
74`function fuzzyMatch() {`
75return if valueLength <= queryLength or value.indexOf(query) >= 0
76return unless match = fuzzyRegexp.exec(value)
77
78# When the match is at the beginning of the string or preceded by a dot.
79if 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.
82else 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.
85else
86 return Math.max 1, 34 - match[0].length
87`}`
88
89#
90# Searchers
91#
92
1class app.Searcher93class app.Searcher
2 $.extend @prototype, Events94 $.extend @prototype, Events
3 95
4 CHUNK_SIZE = 2000096 CHUNK_SIZE = 20000
5 SEPARATOR = '.'
6 97
7 DEFAULTS =98 DEFAULTS =
8 max_results: app.config.max_results99 max_results: app.config.max_results
@@ -11,34 +102,36 @@ class app.Searcher · 2 unmodified lines
11 constructor: (options = {}) ->102 constructor: (options = {}) ->
12 @options = $.extend {}, DEFAULTS, options103 @options = $.extend {}, DEFAULTS, options
13 104
14 find: (data, attr, query) ->105 find: (data, attr, q) ->
15 @kill()106 @kill()
16 107
17 @data = data108 @data = data
18 @attr = attr109 @attr = attr
19 @query = query110 @query = q
20 @setup()111 @setup()
21 112
22 if @isValid() then @match() else @end()113 if @isValid() then @match() else @end()
23 return114 return
24 115
25 setup: ->116 setup: ->
26 @query = @normalizeQuery @query117 query = @query = @normalizeQuery(@query)
27 @queryLength = @query.length118 queryLength = query.length
28 @dataLength = @data.length119 @dataLength = @data.length
29 @matchers = ['exactMatch']120 @matchers = [exactMatch]
30 @totalResults = 0121 @totalResults = 0
31 @setupFuzzy()122 @setupFuzzy()
32 return123 return
33 124
34 setupFuzzy: ->125 setupFuzzy: ->
35 if @queryLength >= @options.fuzzy_min_length126 if queryLength >= @options.fuzzy_min_length
36 @fuzzyRegexp = @queryToFuzzyRegexp @query127 fuzzyRegexp = @queryToFuzzyRegexp(query)
37 @matchers.push 'fuzzyMatch'128 @matchers.push(fuzzyMatch)
129 else
130 fuzzyRegexp = null
38 return131 return
39 132
40 isValid: ->133 isValid: ->
41 @queryLength > 0134 queryLength > 0
42 135
43 end: ->136 end: ->
44 @triggerResults [] unless @totalResults137 @triggerResults [] unless @totalResults
@@ -53,9 +146,8 @@ class app.Searcher · 8 unmodified lines
53 return146 return
54 147
55 free: ->148 free: ->
56 @data = @attr = @query = @queryLength = @dataLength =149 @data = @attr = @dataLength = @matchers = @matcher = @query =
57 @fuzzyRegexp = @matchers = @totalResults = @scoreMap =150 @totalResults = @scoreMap = @cursor = @timeout = null
58 @cursor = @matcher = @timeout = null
59 return151 return
60 152
61 match: =>153 match: =>
@@ -82,8 +174,11 @@ class app.Searcher · 20 unmodified lines
82 return174 return
83 175
84 matchChunk: ->176 matchChunk: ->
177 matcher = @matcher
85 for [0...@chunkSize()]178 for [0...@chunkSize()]
86 if score = @[@matcher](@data[@cursor][@attr])179 value = @data[@cursor][@attr]
180 valueLength = value.length
181 if score = matcher()
87 @addResult @data[@cursor], score182 @addResult @data[@cursor], score
88 @cursor++183 @cursor++
89 return184 return
@@ -131,83 +226,6 @@ class app.Searcher · 41 unmodified lines
131 chars[i] = $.escapeRegexp(char) for char, i in chars226 chars[i] = $.escapeRegexp(char) for char, i in chars
132 new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/227 new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/
133 228
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
211class app.SynchronousSearcher extends app.Searcher229class app.SynchronousSearcher extends app.Searcher
212 match: =>230 match: =>
213 if @matcher231 if @matcher
assets/javascripts/debug.js.coffee+6−6
@@ -35,15 +35,15 @@ app.Searcher = -> · 34 unmodified lines
35 _match = @match.bind(@)35 _match = @match.bind(@)
36 @match = =>36 @match = =>
37 if @matcher37 if @matcher
38 console.timeEnd @matcher38 console.timeEnd @matcher.name
39 if @matcher is 'exactMatch'39 if @matcher.name is 'exactMatch'
40 for entries, score in @scoreMap by -1 when entries40 for entries, score in @scoreMap by -1 when entries
41 console.log '' + score + ': ' + entries.map((entry) -> entry.text).join("\n ")41 console.log '' + score + ': ' + entries.map((entry) -> entry.text).join("\n ")
42 _match()42 _match()
43 43
44 _setupMatcher = @setupMatcher.bind(@)44 _setupMatcher = @setupMatcher.bind(@)
45 @setupMatcher = ->45 @setupMatcher = ->
46 console.time @matcher46 console.time @matcher.name
47 _setupMatcher()47 _setupMatcher()
48 48
49 _end = @end.bind(@)49 _end = @end.bind(@)
@@ -56,7 +56,7 @@ app.Searcher = -> · 6 unmodified lines
56 _kill = @kill.bind(@)56 _kill = @kill.bind(@)
57 @kill = ->57 @kill = ->
58 if @timeout58 if @timeout
59 console.timeEnd @matcher if @matcher59 console.timeEnd @matcher.name if @matcher
60 console.groupEnd()60 console.groupEnd()
61 console.timeEnd 'Total'61 console.timeEnd 'Total'
62 console.warn 'Killed'62 console.warn 'Killed'