2013-02-05 14:16:51 -05:00
|
|
|
module Search
|
|
|
|
|
|
|
|
def self.min_search_term_length
|
|
|
|
3
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.per_facet
|
|
|
|
5
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.facets
|
|
|
|
%w(topic category user)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.user_query_sql
|
|
|
|
"SELECT 'user' AS type,
|
|
|
|
u.username_lower AS id,
|
|
|
|
'/users/' || u.username_lower AS url,
|
|
|
|
u.username AS title,
|
|
|
|
u.email,
|
|
|
|
NULL AS color
|
2013-02-28 14:03:52 -05:00
|
|
|
FROM users AS u
|
|
|
|
JOIN users_search s on s.id = u.id
|
|
|
|
WHERE s.search_data @@ TO_TSQUERY(:locale, :query)
|
|
|
|
ORDER BY last_posted_at desc
|
|
|
|
LIMIT :limit
|
2013-02-05 14:16:51 -05:00
|
|
|
"
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.topic_query_sql
|
|
|
|
"SELECT 'topic' AS type,
|
|
|
|
CAST(ft.id AS VARCHAR),
|
|
|
|
'/t/slug/' || ft.id AS url,
|
|
|
|
ft.title,
|
|
|
|
NULL AS email,
|
|
|
|
NULL AS color
|
|
|
|
FROM topics AS ft
|
|
|
|
JOIN posts AS p ON p.topic_id = ft.id AND p.post_number = 1
|
|
|
|
JOIN posts_search s on s.id = p.id
|
2013-02-28 14:03:52 -05:00
|
|
|
WHERE s.search_data @@ TO_TSQUERY(:locale, :query)
|
2013-02-05 14:16:51 -05:00
|
|
|
AND ft.deleted_at IS NULL
|
|
|
|
AND ft.visible
|
|
|
|
AND ft.archetype <> '#{Archetype.private_message}'
|
2013-02-25 11:42:20 -05:00
|
|
|
ORDER BY
|
2013-02-28 14:03:52 -05:00
|
|
|
TS_RANK_CD(TO_TSVECTOR(:locale, ft.title), TO_TSQUERY(:locale, :query)) desc,
|
|
|
|
TS_RANK_CD(search_data, TO_TSQUERY(:locale, :query)) desc,
|
|
|
|
bumped_at desc
|
|
|
|
LIMIT :limit
|
|
|
|
"
|
2013-02-25 11:42:20 -05:00
|
|
|
end
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
def self.post_query_sql
|
|
|
|
"SELECT cast('topic' as varchar) AS type,
|
|
|
|
CAST(ft.id AS VARCHAR),
|
|
|
|
'/t/slug/' || ft.id || '/' || p.post_number AS url,
|
|
|
|
ft.title,
|
|
|
|
NULL AS email,
|
|
|
|
NULL AS color
|
|
|
|
FROM topics AS ft
|
|
|
|
JOIN posts AS p ON p.topic_id = ft.id AND p.post_number <> 1
|
|
|
|
JOIN posts_search s on s.id = p.id
|
2013-02-28 14:03:52 -05:00
|
|
|
WHERE s.search_data @@ TO_TSQUERY(:locale, :query)
|
2013-02-05 14:16:51 -05:00
|
|
|
AND ft.deleted_at IS NULL and p.deleted_at IS NULL
|
|
|
|
AND ft.visible
|
|
|
|
AND ft.archetype <> '#{Archetype.private_message}'
|
2013-02-25 11:42:20 -05:00
|
|
|
ORDER BY
|
2013-02-28 14:03:52 -05:00
|
|
|
TS_RANK_CD(TO_TSVECTOR(:locale, ft.title), TO_TSQUERY(:locale, :query)) desc,
|
|
|
|
TS_RANK_CD(search_data, TO_TSQUERY(:locale, :query)) desc,
|
|
|
|
bumped_at desc
|
|
|
|
LIMIT :limit
|
|
|
|
"
|
2013-02-25 11:42:20 -05:00
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
def self.category_query_sql
|
|
|
|
"SELECT 'category' AS type,
|
|
|
|
c.name AS id,
|
|
|
|
'/category/' || c.slug AS url,
|
|
|
|
c.name AS title,
|
|
|
|
NULL AS email,
|
|
|
|
c.color
|
|
|
|
FROM categories AS c
|
|
|
|
JOIN categories_search s on s.id = c.id
|
2013-02-28 14:03:52 -05:00
|
|
|
WHERE s.search_data @@ TO_TSQUERY(:locale, :query)
|
2013-02-05 14:16:51 -05:00
|
|
|
ORDER BY topics_month desc
|
2013-02-28 14:03:52 -05:00
|
|
|
LIMIT :limit
|
2013-02-05 14:16:51 -05:00
|
|
|
"
|
|
|
|
end
|
|
|
|
|
2013-02-25 11:42:20 -05:00
|
|
|
def self.query(term, type_filter=nil)
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
return nil if term.blank?
|
2013-02-28 14:03:52 -05:00
|
|
|
sanitized_term = PG::Connection.escape_string(term) #term.gsub(/[^0-9a-zA-Z_ ]/, '')
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
# really short terms are totally pointless
|
|
|
|
return nil if sanitized_term.blank? || sanitized_term.length < self.min_search_term_length
|
|
|
|
|
|
|
|
terms = sanitized_term.split
|
|
|
|
terms.map! {|t| "#{t}:*"}
|
|
|
|
|
2013-02-25 11:42:20 -05:00
|
|
|
if type_filter.present?
|
2013-02-05 14:16:51 -05:00
|
|
|
raise Discourse::InvalidAccess.new("invalid type filter") unless Search.facets.include?(type_filter)
|
2013-02-28 14:03:52 -05:00
|
|
|
sql = Search.send("#{type_filter}_query_sql")
|
|
|
|
db_result = ActiveRecord::Base.exec_sql(sql , query: terms.join(" & "), locale: 'english', limit: Search.per_facet * Search.facets.size)
|
2013-02-05 14:16:51 -05:00
|
|
|
else
|
|
|
|
|
|
|
|
db_result = []
|
|
|
|
[user_query_sql, category_query_sql, topic_query_sql].each do |sql|
|
2013-02-28 14:03:52 -05:00
|
|
|
db_result += ActiveRecord::Base.exec_sql(sql , query: terms.join(" & "),locale: 'english', limit: (Search.per_facet + 1)).to_a
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
db_result = db_result.to_a
|
2013-02-25 11:42:20 -05:00
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
expected_topics = 0
|
|
|
|
expected_topics = Search.facets.size unless type_filter.present?
|
2013-02-11 01:03:21 -05:00
|
|
|
expected_topics = Search.per_facet * Search.facets.size if type_filter == 'topic'
|
2013-02-25 11:42:20 -05:00
|
|
|
|
|
|
|
if expected_topics > 0
|
2013-02-05 14:16:51 -05:00
|
|
|
db_result.each do |row|
|
|
|
|
expected_topics -= 1 if row['type'] == 'topic'
|
|
|
|
end
|
|
|
|
end
|
2013-02-25 11:42:20 -05:00
|
|
|
|
|
|
|
if expected_topics > 0
|
2013-02-28 14:03:52 -05:00
|
|
|
tmp = ActiveRecord::Base.exec_sql post_query_sql,
|
|
|
|
query: terms.join(" & "), locale: 'english', limit: expected_topics * 3
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
topic_ids = Set.new db_result.map{|r| r["id"]}
|
|
|
|
|
|
|
|
tmp = tmp.to_a
|
|
|
|
tmp = tmp.reject{ |i|
|
2013-02-25 11:42:20 -05:00
|
|
|
if topic_ids.include? i["id"]
|
2013-02-05 14:16:51 -05:00
|
|
|
true
|
|
|
|
else
|
|
|
|
topic_ids << i["id"]
|
|
|
|
false
|
|
|
|
end
|
|
|
|
}
|
|
|
|
|
|
|
|
db_result += tmp[0..expected_topics-1]
|
|
|
|
end
|
|
|
|
|
|
|
|
# Group the results by type
|
|
|
|
grouped = {}
|
|
|
|
db_result.each do |row|
|
|
|
|
type = row.delete('type')
|
|
|
|
|
|
|
|
# Add the slug for topics
|
|
|
|
row['url'].gsub!('slug', Slug.for(row['title'])) if type == 'topic'
|
|
|
|
|
|
|
|
# Remove attributes when we know they don't matter
|
|
|
|
row.delete('id')
|
|
|
|
if type == 'user'
|
|
|
|
row['avatar_template'] = User.avatar_template(row['email'])
|
|
|
|
end
|
2013-02-25 11:42:20 -05:00
|
|
|
row.delete('email')
|
2013-02-05 14:16:51 -05:00
|
|
|
row.delete('color') unless type == 'category'
|
|
|
|
|
|
|
|
grouped[type] ||= []
|
|
|
|
grouped[type] << row
|
|
|
|
end
|
|
|
|
|
|
|
|
result = grouped.map do |type, results|
|
2013-02-17 02:52:21 -05:00
|
|
|
more = type_filter.blank? && (results.size > Search.per_facet)
|
|
|
|
results = results[0..([results.length, Search.per_facet].min - 1)] if type_filter.blank?
|
2013-02-25 11:42:20 -05:00
|
|
|
|
|
|
|
{type: type,
|
2013-02-05 14:16:51 -05:00
|
|
|
name: I18n.t("search.types.#{type}"),
|
2013-02-17 02:52:21 -05:00
|
|
|
more: more,
|
2013-02-05 14:16:51 -05:00
|
|
|
results: results}
|
|
|
|
end
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|