app/services/search_service.rb
class SearchService
def initialize; end
# Run a search in any of the associated systems for references that contain the search string
def search_all(search_criteria)
notes = search_notes(search_criteria.query)
wikis = search_wikis(search_criteria.query, search_criteria.limit)
search_criteria.sort_by = "recent"
profiles = search_profiles(search_criteria)
tags = search_tags(search_criteria.query, search_criteria.limit)
maps = search_maps(search_criteria.query, search_criteria.limit)
questions = search_questions(search_criteria.query, search_criteria.limit)
{ notes: notes,
wikis: wikis,
profiles: profiles,
tags: tags,
maps: maps,
questions: questions }
end
# Run a search in any of the associated systems for references that contain the search string
def search_content(query, limit)
nodes = search_nodes(query)
tags = search_tags(query, limit)
{ notes: nodes,
tags: tags }
end
# Search profiles for matching text with optional order_by=recent param and
# sorted direction DESC by default
# If no sort_by value present, then it returns a list of profiles ordered by id DESC
# a recent activity may be a node creation or a node revision
def search_profiles(search_criteria)
user_scope = find_users(search_criteria.query, search_criteria.limit, search_criteria.field)
user_scope =
if search_criteria.sort_by == "recent"
user_scope.joins(:revisions)
.where("node_revisions.status = 1")
.order("node_revisions.timestamp #{search_criteria.order_direction}")
.distinct
else
user_scope.order(id: :desc)
end
user_scope.limit(search_criteria.limit)
end
def search_nodes(input, limit = 25, order = :natural, type = :boolean)
Node.search(query: input, order: order, type: type, limit: limit)
end
def search_notes(input, limit = 25, order = :natural, type = :boolean)
Node.search(query: input, order: order, type: type, limit: limit)
.where("`node`.`type` = 'note'")
end
def search_wikis(input, limit = 25, order = :natural, type = :boolean)
Node.search(query: input, order: order, type: type, limit: limit)
.where("`node`.`type` = 'page'")
end
def search_maps(input, limit = 25, order = :natural, type = :boolean)
Node.search(query: input, order: order, type: type, limit: limit)
.where("`node`.`type` = 'map'")
end
# The search string that is passed in is split into tokens, and the tag names are compared and
# chained to the notes that are tagged with those values
def search_tags(query, limit = 10)
suggestions = []
# order tag autosuggestions by placing exact match of the tag on the top, then tags with query and some other text as suffix,
# then tags with some text as prefix or suffix and lastly the tags with some text as prefix
tags_order = 'CASE WHEN name LIKE ? OR name LIKE ? THEN 1 WHEN name LIKE ? OR name LIKE ? THEN 2 WHEN name LIKE ? OR name LIKE ? THEN 4 ELSE 3 END', "#{query}", "#{query.to_s.gsub(' ', '-')}", "#{query}%", "#{query.to_s.gsub(' ', '-')}%", "%#{query}", "%#{query.to_s.gsub(' ', '-')}"
# filtering out tag spam by requiring tags attached to a published node
# also, we search for both "balloon mapping" and "balloon-mapping":
Tag.where('name LIKE ? OR name LIKE ?', "%#{query}%", "%#{query.to_s.gsub(' ', '-')}%")
.includes(:node)
.references(:node)
.where('node.status = 1')
.order(tags_order)
.limit(limit).each do |tag|
suggestions << tag
end
suggestions
end
# Search question entries for matching text
def search_questions(input, limit = 25, order = :natural, type = :boolean)
Node.search(query: input, order: order, type: type, limit: limit)
.where("`node`.`type` = 'note'")
.joins(:tag)
.where('term_data.name LIKE ?', 'question:%')
.distinct
end
# Search nearby nodes with respect to given latitude, longitute and tags
def tagNearbyNodes(coordinates, tag, period = { "from" => nil, "to" => nil }, sort_by = nil, order_direction = nil, limit = 10)
raise("Must contain all four coordinates") if coordinates["nwlat"].nil?
raise("Must contain all four coordinates") if coordinates["nwlng"].nil?
raise("Must contain all four coordinates") if coordinates["selat"].nil?
raise("Must contain all four coordinates") if coordinates["selng"].nil?
raise("Must be a float") unless coordinates["nwlat"].is_a? Float
raise("Must be a float") unless coordinates["nwlng"].is_a? Float
raise("Must be a float") unless coordinates["selat"].is_a? Float
raise("Must be a float") unless coordinates["selng"].is_a? Float
raise("If 'from' is not null, must contain date") if period["from"] && !(period["from"].is_a? Date)
raise("If 'to' is not null, must contain date") if period["to"] && !(period["to"].is_a? Date)
nodes_scope = Node.select(:nid)
.where('`latitude` >= ? AND `latitude` <= ?', coordinates["selat"], coordinates["nwlat"])
.where(status: 1)
if tag.present?
nodes_scope = NodeTag.joins(:tag)
.where('name LIKE ?', tag)
.where(nid: nodes_scope.select(:nid))
end
nids = nodes_scope.collect(&:nid).uniq || []
# If the period["from"] was not specified, we use (1990,01,01)
# If the period["to"] was not specified, we use 'now'
period["from"] = period["from"].nil? ? Date.new(1990, 01, 01).to_time.to_i : period["from"].to_time.to_i
period["to"] = period["to"].nil? ? Time.now.to_i : period["to"].to_time.to_i
if period["from"] > period["to"]
period["from"], period["to"] = period["to"], period["from"]
end
items = Node.includes(:tag)
.references(:node, :term_data)
.where('node.nid IN (?)', nids)
.where('`longitude` >= ? AND `longitude` <= ?', coordinates["nwlng"], coordinates["selng"])
.where('created BETWEEN ' + period["from"].to_s + ' AND ' + period["to"].to_s)
# selects the items whose node_tags don't have the location:blurred tag
items.joins(:term_data).where('term_data.name <> "location:blurred"')
# sort nodes by recent activities if the sort_by==recent
if sort_by == "recent"
items.order("changed #{order_direction}")
.limit(limit)
else
items.order(Arel.sql("created #{order_direction}"))
.limit(limit)
end
end
# Search nearby people with respect to given latitude, longitute and tags
# and package up as a DocResult
def tagNearbyPeople(coordinates, tag, field, period = nil, sort_by = nil, order_direction = nil, limit = 10)
raise("Must contain all four coordinates") if coordinates["nwlat"].nil?
raise("Must contain all four coordinates") if coordinates["nwlng"].nil?
raise("Must contain all four coordinates") if coordinates["selat"].nil?
raise("Must contain all four coordinates") if coordinates["selng"].nil?
raise("Must be a float") unless coordinates["nwlat"].is_a? Float
raise("Must be a float") unless coordinates["nwlng"].is_a? Float
raise("Must be a float") unless coordinates["selat"].is_a? Float
raise("Must be a float") unless coordinates["selng"].is_a? Float
raise("If 'from' is not null, must contain date") if period["from"] && !(period["from"].is_a? Date)
raise("If 'to' is not null, must contain date") if period["to"] && !(period["to"].is_a? Date)
user_locations = User.where('rusers.status <> 0')
.joins(:user_tags)
.where('user_tags.value LIKE ?', 'lat%')
.where('REPLACE(user_tags.value, "lat:", "") BETWEEN ' + coordinates["selat"].to_s + ' AND ' + coordinates["nwlat"].to_s)
.joins('INNER JOIN user_tags AS lontags ON lontags.uid = rusers.id')
.where('lontags.value LIKE ?', 'lon%')
.where('REPLACE(lontags.value, "lon:", "") BETWEEN ' + coordinates["nwlng"].to_s + ' AND ' + coordinates["selng"].to_s)
.distinct
if tag.present?
if field.present? && field == 'node_tag'
tids = Tag.where("term_data.name = ?", tag).collect(&:tid).uniq || []
uids = TagSelection.where('tag_selections.tid IN (?)', tids).collect(&:user_id).uniq || []
else
uids = User.where('rusers.status <> 0')
.joins(:user_tags)
.where('user_tags.value = ?', tag)
.where(id: user_locations.select("rusers.id"))
.limit(limit)
.collect(&:id).uniq || []
end
user_locations = user_locations.where('rusers.id IN (?)', uids).distinct
end
items = user_locations
# selects the items whose node_tags don't have the location:blurred tag
items = items.where('user_tags.value <> "location:blurred"')
# Here we use period["from"] and period["to"] in the query only if they have been specified,
# so we avoid to join revision table
if !period["from"].nil? || !period["to"].nil?
items = items.joins(:revisions).where("node_revisions.status = 1")\
.distinct
items = items.where('node_revisions.timestamp > ' + period["from"].to_time.to_i.to_s) unless period["from"].nil?
items = items.where('node_revisions.timestamp < ' + period["to"].to_time.to_i.to_s) unless period["to"].nil?
end
# sort users by their recent activities if the sort_by==recent
if sort_by == "recent"
items.joins(:revisions).where("node_revisions.status = 1")\
.order("node_revisions.timestamp #{order_direction}")
.distinct
elsif sort_by == "content"
ids = items.collect(&:id).uniq || []
User.select('`rusers`.*, count(`node`.uid) AS ord')
.joins(:node)
.where('rusers.id IN (?)', ids)
.group('`node`.`uid`')
.order("ord #{order_direction}")
else
items.order("created_at #{order_direction}")
.limit(limit)
end
end
def find_users(query, limit, type = nil)
users = if type == 'tag'
User.where('rusers.status = 1')
.joins(:user_tags)\
.where('user_tags.value LIKE ?', "%#{query}%")\
elsif ActiveRecord::Base.connection.adapter_name == 'Mysql2'
type == 'username' ? User.search_by_username(query).where('rusers.status = ?', 1) : User.search(query).where('rusers.status = ?', 1)
else
User.where('username LIKE ? OR username = ? AND rusers.status = 1', "%#{query}%", query)
end
users.limit(limit)
end
end