2013-05-23 14:26:51 -04:00
require_dependency 'search/search_result'
require_dependency 'search/search_result_type'
require_dependency 'search/grouped_search_results'
2013-05-22 14:36:14 -04:00
class Search
2013-02-05 14:16:51 -05:00
def self . per_facet
5
end
2013-05-27 15:18:55 -04:00
# Sometimes we want more topics than are returned due to exclusion of dupes. This is the
# factor of extra results we'll ask for.
def self . burst_factor
3
end
2013-02-05 14:16:51 -05:00
def self . facets
%w( topic category user )
end
2013-05-23 14:26:51 -04:00
def self . long_locale
2013-02-28 23:14:22 +04:00
case I18n . locale # Currently-present in /conf/locales/* only, sorry :-( Add as needed
2013-04-10 19:45:29 +02:00
when :da then 'danish'
when :de then 'german'
when :en then 'english'
when :es then 'spanish'
2013-02-28 23:14:22 +04:00
when :fr then 'french'
2013-04-10 19:45:29 +02:00
when :it then 'italian'
2013-11-19 22:20:19 +01:00
when :ja then 'japanese'
2013-02-28 23:14:22 +04:00
when :nl then 'dutch'
2013-04-10 19:45:29 +02:00
when :pt then 'portuguese'
2013-02-28 23:14:22 +04:00
when :sv then 'swedish'
2013-07-23 02:05:18 +04:00
when :ru then 'russian'
2013-04-10 19:45:29 +02:00
else 'simple' # use the 'simple' stemmer for other languages
2013-02-28 23:14:22 +04:00
end
end
2013-05-22 14:36:14 -04:00
def initialize ( term , opts = nil )
2013-05-23 14:26:51 -04:00
if term . present?
@term = term . to_s
@original_term = PG :: Connection . escape_string ( @term )
end
2013-05-22 14:36:14 -04:00
@opts = opts || { }
@guardian = @opts [ :guardian ] || Guardian . new
2013-05-24 14:03:45 -04:00
@search_context = @opts [ :search_context ]
2014-04-11 15:07:39 -05:00
@include_blurbs = @opts [ :include_blurbs ] || false
2013-05-23 14:26:51 -04:00
@limit = Search . per_facet * Search . facets . size
@results = GroupedSearchResults . new ( @opts [ :type_filter ] )
2014-02-19 08:59:18 +11:00
2014-03-26 12:20:41 -07:00
if @search_context . is_a? ( Topic ) && @search_context . posts_count < SiteSetting . min_posts_for_search_in_topic
2014-02-19 08:59:18 +11:00
@search_context = nil
end
2013-05-13 17:04:41 -04:00
end
# Query a term
2013-05-22 14:36:14 -04:00
def execute
2013-05-23 14:26:51 -04:00
return nil if @term . blank? || @term . length < ( @opts [ :min_search_term_length ] || SiteSetting . min_search_term_length )
2013-05-13 17:04:41 -04:00
# If the term is a number or url to a topic, just include that topic
2013-05-23 14:26:51 -04:00
if @results . type_filter == 'topic'
2013-05-13 17:04:41 -04:00
begin
2013-05-22 14:36:14 -04:00
route = Rails . application . routes . recognize_path ( @term )
2013-05-23 14:26:51 -04:00
return single_topic ( route [ :topic_id ] ) . as_json if route [ :topic_id ] . present?
2013-05-13 17:04:41 -04:00
rescue ActionController :: RoutingError
end
2013-05-23 14:26:51 -04:00
return single_topic ( @term . to_i ) . as_json if @term =~ / ^ \ d+$ /
2013-05-13 17:04:41 -04:00
end
2013-03-01 12:45:25 -05:00
2013-05-23 14:26:51 -04:00
find_grouped_results . as_json
2013-05-13 17:04:41 -04:00
end
2013-02-05 14:16:51 -05:00
2013-05-22 14:36:14 -04:00
private
2013-02-05 14:16:51 -05:00
2013-05-23 14:26:51 -04:00
def find_grouped_results
2013-05-22 14:36:14 -04:00
2013-05-23 14:26:51 -04:00
if @results . type_filter . present?
raise Discourse :: InvalidAccess . new ( " invalid type filter " ) unless Search . facets . include? ( @results . type_filter )
send ( " #{ @results . type_filter } _search " )
2013-05-22 14:36:14 -04:00
else
2013-05-23 14:26:51 -04:00
@limit = Search . per_facet + 1
user_search
category_search
topic_search
2013-05-22 14:36:14 -04:00
end
2013-02-05 14:16:51 -05:00
2013-05-23 14:26:51 -04:00
add_more_topics_if_expected
@results
2014-03-07 14:59:29 -05:00
rescue ActiveRecord :: StatementInvalid
# In the event of a PG:Error return nothing, it is likely they used a foreign language whose
# locale is not supported by postgres
2013-05-23 14:26:51 -04:00
end
2013-02-05 14:16:51 -05:00
2013-05-23 14:26:51 -04:00
# Add more topics if we expected them
def add_more_topics_if_expected
expected_topics = 0
expected_topics = Search . facets . size unless @results . type_filter . present?
expected_topics = Search . per_facet * Search . facets . size if @results . type_filter == 'topic'
expected_topics -= @results . topic_count
if expected_topics > 0
2013-05-29 17:52:09 -04:00
extra_posts = posts_query ( expected_topics * Search . burst_factor )
extra_posts = extra_posts . where ( " posts.topic_id NOT in (?) " , @results . topic_ids ) if @results . topic_ids . present?
2013-05-27 15:18:55 -04:00
extra_posts . each do | p |
2014-04-11 15:07:39 -05:00
@results . add_result ( SearchResult . from_post ( p , @search_context , @term , @include_blurbs ) )
2013-02-05 14:16:51 -05:00
end
2013-05-13 17:04:41 -04:00
end
2013-02-05 14:16:51 -05:00
end
2013-05-22 14:36:14 -04:00
# If we're searching for a single topic
def single_topic ( id )
2014-05-06 14:41:59 +01:00
topic = Topic . find_by ( id : id )
2013-05-22 14:36:14 -04:00
return nil unless @guardian . can_see? ( topic )
2013-05-13 17:04:41 -04:00
2013-05-23 14:26:51 -04:00
@results . add_result ( SearchResult . from_topic ( topic ) )
@results
2013-05-22 14:36:14 -04:00
end
2013-05-23 14:26:51 -04:00
def secure_category_ids
return @secure_category_ids unless @secure_category_ids . nil?
@secure_category_ids = @guardian . secure_category_ids
2013-05-22 14:36:14 -04:00
end
2013-02-05 14:16:51 -05:00
2013-05-23 14:26:51 -04:00
def category_search
2013-12-13 19:00:48 +11:00
# scope is leaking onto Category, this is not good and probably a bug in Rails
# the secure_category_ids will invoke the same method on User, it calls Category.where
# however the scope from the query below is leaking in to Category, this works around
# the issue while we figure out what is up in Rails
secure_category_ids
2013-05-23 14:26:51 -04:00
categories = Category . includes ( :category_search_data )
. where ( " category_search_data.search_data @@ #{ ts_query } " )
2013-08-25 23:18:11 +02:00
. references ( :category_search_data )
2013-05-23 14:26:51 -04:00
. order ( " topics_month DESC " )
. secured ( @guardian )
. limit ( @limit )
2013-05-13 17:04:41 -04:00
2013-05-23 14:26:51 -04:00
categories . each do | c |
@results . add_result ( SearchResult . from_category ( c ) )
end
2013-05-22 14:36:14 -04:00
end
2013-05-23 14:26:51 -04:00
def user_search
users = User . includes ( :user_search_data )
. where ( " user_search_data.search_data @@ #{ ts_query } " )
. order ( " CASE WHEN username_lower = ' #{ @original_term . downcase } ' THEN 0 ELSE 1 END " )
. order ( " last_posted_at DESC " )
. limit ( @limit )
2013-08-16 14:53:40 +02:00
. references ( :user_search_data )
2013-05-22 14:36:14 -04:00
2013-05-23 14:26:51 -04:00
users . each do | u |
@results . add_result ( SearchResult . from_user ( u ) )
2013-05-22 14:36:14 -04:00
end
2013-05-23 14:26:51 -04:00
end
2013-05-22 14:36:14 -04:00
2013-05-23 14:26:51 -04:00
def posts_query ( limit )
posts = Post . includes ( :post_search_data , { :topic = > :category } )
. where ( " post_search_data.search_data @@ #{ ts_query } " )
. where ( " topics.deleted_at " = > nil )
. where ( " topics.visible " )
. where ( " topics.archetype <> ? " , Archetype . private_message )
2013-08-16 14:53:40 +02:00
. references ( :post_search_data , { :topic = > :category } )
2013-08-02 10:31:36 +10:00
2013-05-24 16:17:09 -04:00
# If we have a search context, prioritize those posts first
2013-05-24 14:03:45 -04:00
if @search_context . present?
if @search_context . is_a? ( User )
2013-05-24 16:17:09 -04:00
# If the context is a user, prioritize that user's posts
posts = posts . order ( " CASE WHEN posts.user_id = #{ @search_context . id } THEN 0 ELSE 1 END " )
2013-05-24 14:03:45 -04:00
elsif @search_context . is_a? ( Category )
# If the context is a category, restrict posts to that category
2013-05-24 16:17:09 -04:00
posts = posts . order ( " CASE WHEN topics.category_id = #{ @search_context . id } THEN 0 ELSE 1 END " )
2014-02-17 13:54:51 +11:00
elsif @search_context . is_a? ( Topic )
posts = posts . order ( " CASE WHEN topics.id = #{ @search_context . id } THEN 0 ELSE 1 END,
CASE WHEN topics . id = #{@search_context.id} THEN posts.post_number ELSE 999999 END")
2013-05-24 14:03:45 -04:00
end
end
2013-05-24 16:17:09 -04:00
posts = posts . order ( " TS_RANK_CD(TO_TSVECTOR( #{ query_locale } , topics.title), #{ ts_query } ) DESC " )
. order ( " TS_RANK_CD(post_search_data.search_data, #{ ts_query } ) DESC " )
. order ( " topics.bumped_at DESC " )
2013-05-23 14:26:51 -04:00
if secure_category_ids . present?
2013-08-25 23:18:11 +02:00
posts = posts . where ( " (categories.id IS NULL) OR (NOT categories.read_restricted) OR (categories.id IN (?)) " , secure_category_ids ) . references ( :categories )
2013-05-22 14:36:14 -04:00
else
2013-08-25 23:18:11 +02:00
posts = posts . where ( " (categories.id IS NULL) OR (NOT categories.read_restricted) " ) . references ( :categories )
2013-05-22 14:36:14 -04:00
end
2013-05-24 16:17:09 -04:00
posts . limit ( limit )
2013-05-22 14:36:14 -04:00
end
2013-05-23 14:26:51 -04:00
def query_locale
@query_locale || = Post . sanitize ( Search . long_locale )
2013-05-23 11:13:23 -04:00
end
2013-05-23 14:26:51 -04:00
def ts_query
@ts_query || = begin
2013-08-26 16:25:02 -04:00
all_terms = @term . gsub ( / [:()&!'"] / , '' ) . split
query = Post . sanitize ( all_terms . map { | t | " #{ PG :: Connection . escape_string ( t ) } :* " } . join ( " & " ) )
2013-05-23 14:26:51 -04:00
" TO_TSQUERY( #{ query_locale } , #{ query } ) "
2013-05-23 11:13:23 -04:00
end
end
2013-05-23 14:26:51 -04:00
def topic_search
2013-05-24 17:44:35 -04:00
2014-02-17 13:54:51 +11:00
posts = if @search_context . is_a? ( User )
# If we have a user filter, search all posts by default with a higher limit
posts_query ( @limit * Search . burst_factor )
elsif @search_context . is_a? ( Topic )
posts_query ( @limit ) . where ( 'posts.post_number = 1 OR posts.topic_id = ?' , @search_context . id )
else
posts_query ( @limit ) . where ( post_number : 1 )
end
2013-05-24 17:44:35 -04:00
posts . each do | p |
2014-04-11 15:07:39 -05:00
@results . add_result ( SearchResult . from_post ( p , @search_context , @term , @include_blurbs ) )
2013-05-23 11:13:23 -04:00
end
2014-02-17 13:54:51 +11:00
2013-02-05 14:16:51 -05:00
end
end