Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
208
research/chatwoot/app/services/search_service.rb
Normal file
208
research/chatwoot/app/services/search_service.rb
Normal file
@@ -0,0 +1,208 @@
|
||||
class SearchService
|
||||
pattr_initialize [:current_user!, :current_account!, :params!, :search_type!]
|
||||
|
||||
def account_user
|
||||
@account_user ||= current_account.account_users.find_by(user: current_user)
|
||||
end
|
||||
|
||||
def perform
|
||||
case search_type
|
||||
when 'Message'
|
||||
{ messages: filter_messages }
|
||||
when 'Conversation'
|
||||
{ conversations: filter_conversations }
|
||||
when 'Contact'
|
||||
{ contacts: filter_contacts }
|
||||
when 'Article'
|
||||
{ articles: filter_articles }
|
||||
else
|
||||
{ contacts: filter_contacts, messages: filter_messages, conversations: filter_conversations, articles: filter_articles }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def accessable_inbox_ids
|
||||
@accessable_inbox_ids ||= @current_user.assigned_inboxes.pluck(:id)
|
||||
end
|
||||
|
||||
def search_query
|
||||
@search_query ||= params[:q].to_s.strip
|
||||
end
|
||||
|
||||
def filter_conversations
|
||||
conversations_query = current_account.conversations.where(inbox_id: accessable_inbox_ids)
|
||||
.joins('INNER JOIN contacts ON conversations.contact_id = contacts.id')
|
||||
.where("cast(conversations.display_id as text) ILIKE :search OR contacts.name ILIKE :search OR contacts.email
|
||||
ILIKE :search OR contacts.phone_number ILIKE :search OR contacts.identifier ILIKE :search", search: "%#{search_query}%")
|
||||
|
||||
if current_account.feature_enabled?('advanced_search')
|
||||
conversations_query = apply_time_filter(conversations_query,
|
||||
'conversations.last_activity_at')
|
||||
end
|
||||
|
||||
@conversations = conversations_query.order('conversations.created_at DESC')
|
||||
.page(params[:page])
|
||||
.per(15)
|
||||
end
|
||||
|
||||
def filter_messages
|
||||
@messages = if use_gin_search
|
||||
filter_messages_with_gin
|
||||
elsif should_run_advanced_search?
|
||||
advanced_search_with_fallback
|
||||
else
|
||||
filter_messages_with_like
|
||||
end
|
||||
end
|
||||
|
||||
def advanced_search_with_fallback
|
||||
advanced_search
|
||||
rescue Faraday::ConnectionFailed, Searchkick::Error, Elasticsearch::Transport::Transport::Error => e
|
||||
Rails.logger.warn("Elasticsearch unavailable, falling back to SQL search: #{e.message}")
|
||||
use_gin_search ? filter_messages_with_gin : filter_messages_with_like
|
||||
end
|
||||
|
||||
def should_run_advanced_search?
|
||||
ChatwootApp.advanced_search_allowed? && current_account.feature_enabled?('advanced_search')
|
||||
end
|
||||
|
||||
def advanced_search; end
|
||||
|
||||
def filter_messages_with_gin
|
||||
base_query = message_base_query
|
||||
base_query = apply_message_filters(base_query)
|
||||
|
||||
if search_query.present?
|
||||
# Use the @@ operator with to_tsquery for better GIN index utilization
|
||||
# Convert search query to tsquery format with prefix matching
|
||||
|
||||
# Use this if we wanna match splitting the words
|
||||
# split_query = search_query.split.map { |term| "#{term} | #{term}:*" }.join(' & ')
|
||||
|
||||
# This will do entire sentence matching using phrase distance operator
|
||||
tsquery = search_query.split.join(' <-> ')
|
||||
|
||||
# Apply the text search using the GIN index
|
||||
base_query.where('content @@ to_tsquery(?)', tsquery)
|
||||
.reorder('created_at DESC')
|
||||
.page(params[:page])
|
||||
.per(15)
|
||||
else
|
||||
base_query.reorder('created_at DESC')
|
||||
.page(params[:page])
|
||||
.per(15)
|
||||
end
|
||||
end
|
||||
|
||||
def filter_messages_with_like
|
||||
base_query = message_base_query
|
||||
base_query = apply_message_filters(base_query)
|
||||
base_query.where('messages.content ILIKE :search', search: "%#{search_query}%")
|
||||
.reorder('created_at DESC')
|
||||
.page(params[:page])
|
||||
.per(15)
|
||||
end
|
||||
|
||||
def message_base_query
|
||||
query = current_account.messages.where('created_at >= ?', 3.months.ago)
|
||||
query = query.where(inbox_id: accessable_inbox_ids) unless should_skip_inbox_filtering?
|
||||
query
|
||||
end
|
||||
|
||||
def apply_message_filters(query)
|
||||
return query unless current_account.feature_enabled?('advanced_search')
|
||||
|
||||
query = apply_time_filter(query, 'messages.created_at')
|
||||
query = apply_sender_filter(query)
|
||||
apply_inbox_id_filter(query)
|
||||
end
|
||||
|
||||
def apply_sender_filter(query)
|
||||
sender_type, sender_id = parse_from_param(params[:from])
|
||||
return query unless sender_type && sender_id
|
||||
|
||||
query.where(sender_type: sender_type, sender_id: sender_id)
|
||||
end
|
||||
|
||||
def parse_from_param(from_param)
|
||||
return [nil, nil] unless from_param&.match?(/\A(contact|agent):\d+\z/)
|
||||
|
||||
type, id = from_param.split(':')
|
||||
sender_type = type == 'agent' ? 'User' : 'Contact'
|
||||
[sender_type, id.to_i]
|
||||
end
|
||||
|
||||
def apply_inbox_id_filter(query)
|
||||
return query if params[:inbox_id].blank?
|
||||
|
||||
inbox_id = params[:inbox_id].to_i
|
||||
return query if inbox_id.zero?
|
||||
return query unless validate_inbox_access(inbox_id)
|
||||
|
||||
query.where(inbox_id: inbox_id)
|
||||
end
|
||||
|
||||
def validate_inbox_access(inbox_id)
|
||||
return true if should_skip_inbox_filtering?
|
||||
|
||||
accessable_inbox_ids.include?(inbox_id)
|
||||
end
|
||||
|
||||
def should_skip_inbox_filtering?
|
||||
account_user.administrator? || user_has_access_to_all_inboxes?
|
||||
end
|
||||
|
||||
def user_has_access_to_all_inboxes?
|
||||
accessable_inbox_ids.sort == current_account.inboxes.pluck(:id).sort
|
||||
end
|
||||
|
||||
def use_gin_search
|
||||
current_account.feature_enabled?('search_with_gin')
|
||||
end
|
||||
|
||||
def filter_contacts
|
||||
contacts_query = current_account.contacts.where(
|
||||
"name ILIKE :search OR email ILIKE :search OR phone_number
|
||||
ILIKE :search OR identifier ILIKE :search", search: "%#{search_query}%"
|
||||
)
|
||||
|
||||
contacts_query = apply_time_filter(contacts_query, 'last_activity_at') if current_account.feature_enabled?('advanced_search')
|
||||
|
||||
@contacts = contacts_query.resolved_contacts(
|
||||
use_crm_v2: current_account.feature_enabled?('crm_v2')
|
||||
).order_on_last_activity_at('desc').page(params[:page]).per(15)
|
||||
end
|
||||
|
||||
def filter_articles
|
||||
articles_query = current_account.articles.text_search(search_query)
|
||||
articles_query = apply_time_filter(articles_query, 'updated_at') if current_account.feature_enabled?('advanced_search')
|
||||
|
||||
@articles = articles_query.page(params[:page]).per(15)
|
||||
end
|
||||
|
||||
def apply_time_filter(query, column_name)
|
||||
return query if params[:since].blank? && params[:until].blank?
|
||||
|
||||
query = query.where("#{column_name} >= ?", cap_since_time(params[:since])) if params[:since].present?
|
||||
query = query.where("#{column_name} <= ?", cap_until_time(params[:until])) if params[:until].present?
|
||||
query
|
||||
end
|
||||
|
||||
def cap_since_time(since_param)
|
||||
max_lookback = 90.days.ago
|
||||
requested_time = Time.zone.at(since_param.to_i)
|
||||
|
||||
# Silently cap to max_lookback if requested time is too far back
|
||||
[requested_time, max_lookback].max
|
||||
end
|
||||
|
||||
def cap_until_time(until_param)
|
||||
max_future = 90.days.from_now
|
||||
requested_time = Time.zone.at(until_param.to_i)
|
||||
|
||||
[requested_time, max_future].min
|
||||
end
|
||||
end
|
||||
|
||||
SearchService.prepend_mod_with('SearchService')
|
||||
Reference in New Issue
Block a user