Optimize search

pull/139/head
Thibaut 10 years ago
parent 50c18b2f89
commit a3aa03bc3d

@ -1,8 +1,99 @@
#
# Match functions
#
SEPARATOR = '.'
query =
queryLength =
value =
valueLength =
matcher = # current match function
fuzzyRegexp = # query fuzzy regexp
index = # position of the query in the string being matched
lastIndex = # last position of the query in the string being matched
match = # regexp match data
score = # score for the current match
separators = # counter
i = null # cursor
`function exactMatch() {`
index = value.indexOf(query)
return unless index >= 0
lastIndex = value.lastIndexOf(query)
if index isnt lastIndex
return Math.max(scoreExactMatch(), (index = lastIndex) and scoreExactMatch())
else
return scoreExactMatch()
`}`
`function scoreExactMatch() {`
# Remove one point for each unmatched character.
score = 100 - (valueLength - queryLength)
if index > 0
# If the character preceding the query is a dot, assign the same score
# as if the query was found at the beginning of the string, minus one.
if value.charAt(index - 1) is SEPARATOR
score += index - 1
# Don't match a single-character query unless it's found at the beginning
# of the string or is preceded by a dot.
else if queryLength is 1
return
# (1) Remove one point for each unmatched character up to the nearest
# preceding dot or the beginning of the string.
# (2) Remove one point for each unmatched character following the query.
else
i = index - 2
i-- while i >= 0 and value.charAt(i) isnt SEPARATOR
score -= (index - i) + # (1)
(valueLength - queryLength - index) # (2)
# Remove one point for each dot preceding the query, except for the one
# immediately before the query.
separators = 0
i = index - 2
while i >= 0
separators++ if value.charAt(i) is SEPARATOR
i--
score -= separators
# Remove five points for each dot following the query.
separators = 0
i = valueLength - queryLength - index - 1
while i >= 0
separators++ if value.charAt(index + queryLength + i) is SEPARATOR
i--
score -= separators * 5
return Math.max 1, score
`}`
`function fuzzyMatch() {`
return if valueLength <= queryLength or value.indexOf(query) >= 0
return unless match = fuzzyRegexp.exec(value)
# When the match is at the beginning of the string or preceded by a dot.
if match.index is 0 or value.charAt(match.index - 1) is SEPARATOR
return Math.max 66, 100 - match[0].length
# When the match is at the end of the string.
else if match.index + match[0].length is valueLength
return Math.max 33, 67 - match[0].length
# When the match is in the middle of the string.
else
return Math.max 1, 34 - match[0].length
`}`
#
# Searchers
#
class app.Searcher class app.Searcher
$.extend @prototype, Events $.extend @prototype, Events
CHUNK_SIZE = 20000 CHUNK_SIZE = 20000
SEPARATOR = '.'
DEFAULTS = DEFAULTS =
max_results: app.config.max_results max_results: app.config.max_results
@ -11,34 +102,36 @@ class app.Searcher
constructor: (options = {}) -> constructor: (options = {}) ->
@options = $.extend {}, DEFAULTS, options @options = $.extend {}, DEFAULTS, options
find: (data, attr, query) -> find: (data, attr, q) ->
@kill() @kill()
@data = data @data = data
@attr = attr @attr = attr
@query = query @query = q
@setup() @setup()
if @isValid() then @match() else @end() if @isValid() then @match() else @end()
return return
setup: -> setup: ->
@query = @normalizeQuery @query query = @query = @normalizeQuery(@query)
@queryLength = @query.length queryLength = query.length
@dataLength = @data.length @dataLength = @data.length
@matchers = ['exactMatch'] @matchers = [exactMatch]
@totalResults = 0 @totalResults = 0
@setupFuzzy() @setupFuzzy()
return return
setupFuzzy: -> setupFuzzy: ->
if @queryLength >= @options.fuzzy_min_length if queryLength >= @options.fuzzy_min_length
@fuzzyRegexp = @queryToFuzzyRegexp @query fuzzyRegexp = @queryToFuzzyRegexp(query)
@matchers.push 'fuzzyMatch' @matchers.push(fuzzyMatch)
else
fuzzyRegexp = null
return return
isValid: -> isValid: ->
@queryLength > 0 queryLength > 0
end: -> end: ->
@triggerResults [] unless @totalResults @triggerResults [] unless @totalResults
@ -53,9 +146,8 @@ class app.Searcher
return return
free: -> free: ->
@data = @attr = @query = @queryLength = @dataLength = @data = @attr = @dataLength = @matchers = @matcher = @query =
@fuzzyRegexp = @matchers = @totalResults = @scoreMap = @totalResults = @scoreMap = @cursor = @timeout = null
@cursor = @matcher = @timeout = null
return return
match: => match: =>
@ -82,8 +174,11 @@ class app.Searcher
return return
matchChunk: -> matchChunk: ->
matcher = @matcher
for [0...@chunkSize()] for [0...@chunkSize()]
if score = @[@matcher](@data[@cursor][@attr]) value = @data[@cursor][@attr]
valueLength = value.length
if score = matcher()
@addResult @data[@cursor], score @addResult @data[@cursor], score
@cursor++ @cursor++
return return
@ -131,83 +226,6 @@ class app.Searcher
chars[i] = $.escapeRegexp(char) for char, i in chars chars[i] = $.escapeRegexp(char) for char, i in chars
new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/ new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/
#
# Match functions
#
index = # position of the query in the string being matched
lastIndex = # last position of the query in the string being matched
match = # regexp match data
score = # score for the current match
separators = # counter
i = null # cursor
exactMatch: (value) ->
index = value.indexOf @query
return unless index >= 0
lastIndex = value.lastIndexOf @query
if index isnt lastIndex
Math.max(@scoreExactMatch(value, index), @scoreExactMatch(value, lastIndex))
else
@scoreExactMatch(value, index)
scoreExactMatch: (value, index) ->
# Remove one point for each unmatched character.
score = 100 - (value.length - @queryLength)
if index > 0
# If the character preceding the query is a dot, assign the same score
# as if the query was found at the beginning of the string, minus one.
if value[index - 1] is SEPARATOR
score += index - 1
# Don't match a single-character query unless it's found at the beginning
# of the string or is preceded by a dot.
else if @queryLength is 1
return
# (1) Remove one point for each unmatched character up to the nearest
# preceding dot or the beginning of the string.
# (2) Remove one point for each unmatched character following the query.
else
i = index - 2
i-- while i >= 0 and value[i] isnt SEPARATOR
score -= (index - i) + # (1)
(value.length - @queryLength - index) # (2)
# Remove one point for each dot preceding the query, except for the one
# immediately before the query.
separators = 0
i = index - 2
while i >= 0
separators++ if value[i] is SEPARATOR
i--
score -= separators
# Remove five points for each dot following the query.
separators = 0
i = value.length - @queryLength - index - 1
while i >= 0
separators++ if value[index + @queryLength + i] is SEPARATOR
i--
score -= separators * 5
Math.max 1, score
fuzzyMatch: (value) ->
return if value.length <= @queryLength or value.indexOf(@query) >= 0
return unless match = @fuzzyRegexp.exec(value)
# When the match is at the beginning of the string or preceded by a dot.
if match.index is 0 or value[match.index - 1] is SEPARATOR
Math.max 66, 100 - match[0].length
# When the match is at the end of the string.
else if match.index + match[0].length is value.length
Math.max 33, 67 - match[0].length
# When the match is in the middle of the string.
else
Math.max 1, 34 - match[0].length
class app.SynchronousSearcher extends app.Searcher class app.SynchronousSearcher extends app.Searcher
match: => match: =>
if @matcher if @matcher

@ -35,15 +35,15 @@ app.Searcher = ->
_match = @match.bind(@) _match = @match.bind(@)
@match = => @match = =>
if @matcher if @matcher
console.timeEnd @matcher console.timeEnd @matcher.name
if @matcher is 'exactMatch' if @matcher.name is 'exactMatch'
for entries, score in @scoreMap by -1 when entries for entries, score in @scoreMap by -1 when entries
console.log '' + score + ': ' + entries.map((entry) -> entry.text).join("\n ") console.log '' + score + ': ' + entries.map((entry) -> entry.text).join("\n ")
_match() _match()
_setupMatcher = @setupMatcher.bind(@) _setupMatcher = @setupMatcher.bind(@)
@setupMatcher = -> @setupMatcher = ->
console.time @matcher console.time @matcher.name
_setupMatcher() _setupMatcher()
_end = @end.bind(@) _end = @end.bind(@)
@ -56,7 +56,7 @@ app.Searcher = ->
_kill = @kill.bind(@) _kill = @kill.bind(@)
@kill = -> @kill = ->
if @timeout if @timeout
console.timeEnd @matcher if @matcher console.timeEnd @matcher.name if @matcher
console.groupEnd() console.groupEnd()
console.timeEnd 'Total' console.timeEnd 'Total'
console.warn 'Killed' console.warn 'Killed'

Loading…
Cancel
Save