iachat/app/services/search_service.rb
Shivam Mishra 4c26fe5d57
feat: use gin index for message search (#11107)
This PR updates the search implementation to better utilize the GIN
indexes. The option is toggled behind a feature flag for us to test it
internally before making it available publicly
2025-03-19 12:56:23 +05:30

107 lines
3.5 KiB
Ruby

class SearchService
pattr_initialize [:current_user!, :current_account!, :params!, :search_type!]
def perform
case search_type
when 'Message'
{ messages: filter_messages }
when 'Conversation'
{ conversations: filter_conversations }
when 'Contact'
{ contacts: filter_contacts }
else
{ contacts: filter_contacts, messages: filter_messages, conversations: filter_conversations }
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 = 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}%")
.order('conversations.created_at DESC')
.page(params[:page])
.per(15)
end
def filter_messages
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@messages = if use_gin_search
filter_messages_with_gin
else
filter_messages_with_like
end
log_search_performance(start_time)
@messages
end
def filter_messages_with_gin
base_query = message_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
message_base_query
.where('messages.content ILIKE :search', search: "%#{search_query}%")
.reorder('created_at DESC')
.page(params[:page])
.per(15)
end
def message_base_query
current_account.messages.where(inbox_id: accessable_inbox_ids)
.where('created_at >= ?', 3.months.ago)
end
def log_search_performance(start_time)
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
search_type = use_gin_search ? 'GIN' : 'ILIKE'
duration_ms = (end_time - start_time) * 1000
Rails.logger.info "[SearchService][#{current_account.id}] #{search_type} search query time: #{duration_ms}ms for #{search_query}"
end
def use_gin_search
current_account.feature_enabled?('search_with_gin')
end
def filter_contacts
@contacts = current_account.contacts.where(
"name ILIKE :search OR email ILIKE :search OR phone_number
ILIKE :search OR identifier ILIKE :search", search: "%#{search_query}%"
).resolved_contacts.order_on_last_activity_at('desc').page(params[:page]).per(15)
end
end