discourse/lib/search.rb

234 lines
6.4 KiB
Ruby
Raw Normal View History

2013-02-05 14:16:51 -05:00
module Search
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,
NULL AS text_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)
2013-04-01 20:57:50 -04:00
ORDER BY CASE WHEN u.username_lower = lower(:orig) then 0 else 1 end, last_posted_at desc
2013-02-28 14:03:52 -05:00
LIMIT :limit
2013-02-05 14:16:51 -05:00
"
end
def self.post_query(guardian, type, args)
2013-05-12 20:48:32 -04:00
builder = SqlBuilder.new <<SQL
/*select*/
2013-02-05 14:16:51 -05:00
FROM topics AS ft
2013-05-12 20:48:32 -04:00
/*join*/
2013-02-05 14:16:51 -05:00
JOIN posts_search s on s.id = p.id
2013-05-12 20:48:32 -04:00
LEFT JOIN categories c ON c.id = ft.category_id
/*where*/
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-05-12 20:48:32 -04:00
SQL
2013-02-25 11:42:20 -05:00
2013-05-12 20:48:32 -04:00
builder.select "'topic' AS type"
builder.select("CAST(ft.id AS VARCHAR)")
2013-02-05 14:16:51 -05:00
2013-05-12 20:48:32 -04:00
if type == :topic
builder.select "'/t/slug/' || ft.id AS url"
else
builder.select "'/t/slug/' || ft.id || '/' || p.post_number AS url"
end
builder.select "ft.title, NULL AS email, NULL AS color, NULL AS text_color"
if type == :topic
builder.join "posts AS p ON p.topic_id = ft.id AND p.post_number = 1"
else
builder.join "posts AS p ON p.topic_id = ft.id AND p.post_number > 1"
end
builder.where <<SQL
s.search_data @@ TO_TSQUERY(:locale, :query)
AND ft.deleted_at IS NULL
AND p.deleted_at IS NULL
2013-02-05 14:16:51 -05:00
AND ft.visible
AND ft.archetype <> '#{Archetype.private_message}'
2013-05-12 20:48:32 -04:00
SQL
add_allowed_categories(builder, guardian)
builder.exec(args)
end
def self.add_allowed_categories(builder, guardian)
2013-05-12 20:48:32 -04:00
allowed_categories = nil
allowed_categories = guardian.secure_category_ids
2013-05-12 20:48:32 -04:00
if allowed_categories.present?
builder.where("(c.id IS NULL OR c.secure = 'f' OR c.id in (:category_ids))", category_ids: allowed_categories)
else
builder.where("(c.id IS NULL OR c.secure = 'f')")
end
2013-02-25 11:42:20 -05:00
end
2013-02-05 14:16:51 -05:00
2013-05-12 20:48:32 -04:00
def self.category_query(guardian, args)
builder = SqlBuilder.new <<SQL
SELECT 'category' AS type,
2013-02-05 14:16:51 -05:00
c.name AS id,
'/category/' || c.slug AS url,
c.name AS title,
NULL AS email,
c.color,
c.text_color
2013-02-05 14:16:51 -05:00
FROM categories AS c
JOIN categories_search s on s.id = c.id
/*where*/
2013-02-05 14:16:51 -05:00
ORDER BY topics_month desc
2013-02-28 14:03:52 -05:00
LIMIT :limit
SQL
builder.where "s.search_data @@ TO_TSQUERY(:locale, :query)"
add_allowed_categories(builder,guardian)
builder.exec(args)
2013-02-05 14:16:51 -05:00
end
def self.current_locale_long
case I18n.locale # Currently-present in /conf/locales/* only, sorry :-( Add as needed
when :da then 'danish'
when :de then 'german'
when :en then 'english'
when :es then 'spanish'
when :fr then 'french'
when :it then 'italian'
when :nl then 'dutch'
when :pt then 'portuguese'
when :sv then 'swedish'
else 'simple' # use the 'simple' stemmer for other languages
end
end
2013-05-12 20:48:32 -04:00
# needs current user for secure categories
def self.query(term, guardian, type_filter=nil, min_search_term_length=3)
guardian ||= Guardian.new(nil)
2013-02-05 14:16:51 -05:00
return nil if term.blank?
# We are stripping only symbols taking place in FTS and simply sanitizing the rest.
sanitized_term = PG::Connection.escape_string(term.gsub(/[:()&!]/,''))
2013-02-05 14:16:51 -05:00
# really short terms are totally pointless
return nil if sanitized_term.blank? || sanitized_term.length < min_search_term_length
2013-02-05 14:16:51 -05:00
terms = sanitized_term.split
terms.map! {|t| "#{t}:*"}
2013-05-12 20:48:32 -04:00
args = {orig: sanitized_term, query: terms.join(" & "), locale: current_locale_long}
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-05-12 20:48:32 -04:00
a = args.merge(limit: Search.per_facet * Search.facets.size)
if type_filter.to_s == "post"
db_result = post_query(guardian, :post, a)
2013-05-12 20:48:32 -04:00
elsif type_filter.to_s == "topic"
db_result = post_query(guardian, :topic, a)
elsif type_filter.to_s == "category"
db_result = category_query(guardian, a)
2013-05-12 20:48:32 -04:00
else
sql = Search.send("#{type_filter}_query_sql")
2013-05-12 20:48:32 -04:00
db_result = ActiveRecord::Base.exec_sql(sql , a)
end
2013-02-05 14:16:51 -05:00
else
db_result = []
2013-05-12 20:48:32 -04:00
a = args.merge(limit: (Search.per_facet + 1))
db_result += ActiveRecord::Base.exec_sql(user_query_sql, a).to_a
db_result += category_query(guardian, a).to_a
db_result += post_query(guardian, :topic, a).to_a
2013-02-05 14:16:51 -05:00
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
tmp = post_query(guardian, :post, args.merge(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
if type == 'topic'
new_slug = Slug.for(row['title'])
new_slug = "topic" if new_slug.blank?
row['url'].gsub!('slug', new_slug)
end
2013-02-05 14:16:51 -05:00
# Remove attributes when we know they don't matter
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'
row.delete('text_color') unless type == 'category'
2013-02-05 14:16:51 -05:00
grouped[type] ||= []
grouped[type] << row
end
result = grouped.map do |type, results|
more = type_filter.blank? && (results.size > Search.per_facet)
results = results[0..([results.length, Search.per_facet].min - 1)] if type_filter.blank?
{
type: type,
name: I18n.t("search.types.#{type}"),
more: more,
results: results
}
2013-02-05 14:16:51 -05:00
end
2013-02-05 14:16:51 -05:00
result
end
end