Skip to content

Commit

Permalink
Search completion; refactor to separate file.
Browse files Browse the repository at this point in the history
  • Loading branch information
smblott-github committed May 2, 2015
1 parent 3616e0d commit d848f50
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 48 deletions.
59 changes: 11 additions & 48 deletions background_scripts/completion.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -321,63 +321,26 @@ class TabCompleter
else
tabRecency.recencyScore(suggestion.tabId)

# searchUrl is the URL that will be used for the search, either the default search URL, or a custom
# search-engine URL. The other arguments area obvious.
# If we know the search-suggestion URL for searchUrl, then use it to pass a list of suggestions to callback.
# Otherwise, just call callback.
#
# Note: That's all TBD. For now, we just assume Google and use it.
#
getOnlineSuggestions = do ->
xhrs = {} # Maps searchUrl to outstanding HTTP request.
(searchUrl, queryTerms, callback) ->
# Cancel any outstanding requests.
xhrs?[searchUrl]?.abort()
xhrs[searchUrl] = null

sendNoSuggestions = -> xhrs[searchUrl] = null; callback []
return sendNoSuggestions() if queryTerms.length == 0

url = "http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"
xhrs[searchUrl] = xhr = new XMLHttpRequest()
xhr.open "GET", url, true
xhr.timeout = 500
xhr.ontimeout = sendNoSuggestions
xhr.onerror = sendNoSuggestions
xhr.send()

xhr.onreadystatechange = (response) =>
if xhr.readyState == 4
suggestions = xhr.responseXML?.getElementsByTagName "suggestion"
return sendNoSuggestions() unless xhr.status == 200 and suggestions
xhr[searchUrl] = null
suggestions =
for suggestion in suggestions
continue unless suggestion = suggestion.getAttribute "data"
suggestion
callback suggestions

class SearchEngineCompleter
refresh: ->
filter: (queryTerms, onComplete) ->
return onComplete([]) if queryTerms.length == 0

getOnlineSuggestions Settings.get("searchUrl"), queryTerms, (suggestions) =>
completions =
for suggestion in suggestions
url = Utils.createSearchUrl suggestion.split /\s+/
new Suggestion queryTerms, "suggestion", url, suggestion, @computeRelevancy
characterCount = queryTerms.join(" ").length
completion.characterCount = characterCount for completion in completions
onComplete completions
filter: (queryTerms, onComplete) ->
SearchEngines.complete Settings.get("searchUrl"), queryTerms, (suggestions = []) =>
console.log suggestions.length
characterCount = queryTerms.join("").length
completions =
for suggestion in suggestions
url = Utils.createSearchUrl suggestion.split /\s+/
new Suggestion queryTerms, "search", url, suggestion, @computeRelevancy, characterCount
onComplete completions

computeRelevancy: (suggestion) ->
# We score search-engine completions by word relevancy, but weight increasingly as the number of
# characters in the query terms increases. The idea is that, the more the user has had to type, the less
# likely it is that one of the other suggestion types has found what they're looking for, so the more
# likely it is that a search suggestion will be useful.
# likely it is that this suggestion will be useful.
# (1.0 - (1.0 / suggestion.characterCount)) *
(Math.min(suggestion.characterCount, 12)/12) *
(Math.min(suggestion.extraRelevancyData, 12)/12) *
RankingUtils.wordRelevancy suggestion.queryTerms, suggestion.title, suggestion.title

# A completer which will return your search engines
Expand Down
98 changes: 98 additions & 0 deletions background_scripts/search_engines.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@

# Each completer implements three functions:
#
# match: can this completer be used for this search URL?
# getUrl: map these query terms to a completion URL.
# parse: extract suggestions from the resulting (successful) XMLHttpRequest.
#
Google =
name: "Google"
match: (searchUrl) ->
true # TBD.

getUrl: (queryTerms) ->
"http://suggestqueries.google.com/complete/search?ss_protocol=legace&client=toolbar&q=#{Utils.createSearchQuery queryTerms}"

parse: (xhr, callback) ->
if suggestions = xhr?.responseXML?.getElementsByTagName "suggestion"
suggestions =
for suggestion in suggestions
continue unless suggestion = suggestion.getAttribute "data"
suggestion
callback suggestions
else
callback []

# A dummy search engine which is guaranteed to match any search URL, but produces no completions. This allows
# the rest of the logic to be written knowing that there will be a search engine match.
DummySearchEngine =
name: "Dummy"
match: -> true
# We return a useless URL which we know will succeed, but which won't generate any network traffic.
getUrl: -> chrome.runtime.getURL "content_scripts/vimium.css"
parse: (_, callback) -> callback []

CompletionEngines = [ Google, DummySearchEngine ]

SearchEngines =
cancel: (searchUrl, callback = null) ->
@requests[searchUrl]?.abort()
delete @requests[searchUrl]
callback? null

# Perform and HTTP GET.
# searchUrl is the search engine's URL, e.g. Settings.get("searchUrl")
# url is the URL to fetch
# callback will be called a successful XMLHttpRequest object, or null.
get: (searchUrl, url, callback) ->
@requests ?= {} # Maps searchUrls to any outstanding HTTP request for that search engine.
@cancel searchUrl

# We cache the results of recent requests (with a two-hour expiry).
@requestCache ?= new SimpleCache 2 * 60 * 60 * 1000

if @requestCache.has url
callback @requestCache.get url
return

@requests[searchUrl] = xhr = new XMLHttpRequest()
xhr.open "GET", url, true
xhr.timeout = 500
xhr.ontimeout = => @cancel searchUrl, callback
xhr.onerror = => @cancel searchUrl, callback
xhr.send()

xhr.onreadystatechange = =>
if xhr.readyState == 4
if xhr.status == 200
@requests[searchUrl] = null
callback @requestCache.set url, xhr
else
callback null

# Look up the search engine for this search URL. Because of DummySearchEngine, above, we know there will
# always be a match. Imagining that there may be many search engines, and knowing that this is called for
# every character entered, we cache the result.
lookupEngine: (searchUrl) ->
@engineCache ?= new SimpleCache 24 * 60 * 60 * 1000
if @engineCache.has searchUrl
@engineCache.get searchUrl
else
for engine in CompletionEngines
return @engineCache.set searchUrl, engine if engine.match searchUrl

# This is the main (actually, the only) entry point.
# searchUrl is the search engine's URL, e.g. Settings.get("searchUrl")
# queryTerms are the queryTerms
# callback will be applied to a list of suggestion strings (which will be an empty list, if anything goes
# wrong).
complete: (searchUrl, queryTerms, callback) ->
return callback [] unless 0 < queryTerms.length

engine = @lookupEngine searchUrl
url = engine.getUrl queryTerms
@get searchUrl, url, (xhr = null) ->
if xhr? then engine.parse xhr, callback else callback []

root = exports ? window
root.SearchEngines = SearchEngines
38 changes: 38 additions & 0 deletions lib/utils.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -194,5 +194,43 @@ globalRoot.extend = (hash1, hash2) ->
hash1[key] = hash2[key]
hash1

# A simple cache. Entries used within an expiry period are retained (for one more expiry period), otherwise
# they are discarded.
class SimpleCache
# expiry: expiry time in milliseconds (default, one hour)
# entries: maximum number of entries
constructor: (@expiry = 60 * 60 * 1000, @entries = 1000) ->
@cache = {}
@previous = {}
setInterval (=> @rotate()), @expiry

rotate: ->
@previous = @cache
@cache = {}

has: (key) ->
(key of @cache) or key of @previous

get: (key) ->
console.log "get", key
if key of @cache
@cache[key]
else if key of @previous
@cache[key] = @previous[key]
else
null

# Set value, and return that value. If value is null, then delete key.
set: (key, value = null) ->
if value?
@cache[key] = value
delete @previous[key]
@rotate() if @entries < Object.keys(@cache).length
else
delete @cache[key]
delete @previous[key]
value

root = exports ? window
root.Utils = Utils
root.SimpleCache = SimpleCache
1 change: 1 addition & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"background_scripts/sync.js",
"background_scripts/settings.js",
"background_scripts/exclusions.js",
"background_scripts/search_engines.js",
"background_scripts/completion.js",
"background_scripts/marks.js",
"background_scripts/main.js"
Expand Down

0 comments on commit d848f50

Please sign in to comment.