mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 11:37:58 +00:00
We now support searching within the actual message content, email subject lines, and audio transcriptions. This enables a faster, more accurate search experience going forward. Unlike the standard message search, which is limited to the last 3 months, this search has no time restrictions. The search engine also accounts for small variations in queries. Minor spelling mistakes, such as searching for slck instead of Slack, will still return the correct results. It also ignores differences in accents and diacritics, so searching for Deja vu will match content containing Déjà vu. We can also refine searches in the future by criteria such as: - Searching within a specific inbox - Filtering by sender or recipient - Limiting to messages sent by an agent Fixes https://github.com/chatwoot/chatwoot/issues/11656 Fixes https://github.com/chatwoot/chatwoot/issues/10669 Fixes https://github.com/chatwoot/chatwoot/issues/5910 --- Rake tasks to reindex all the messages. ```sh bundle exec rake search:all ``` Rake task to reindex messages from one account only ```sh bundle exec rake search:account ACCOUNT_ID=1 ```
128 lines
4.1 KiB
Ruby
128 lines
4.1 KiB
Ruby
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 = 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
|
|
@messages = if use_gin_search
|
|
filter_messages_with_gin
|
|
elsif should_run_advanced_search?
|
|
advanced_search
|
|
else
|
|
filter_messages_with_like
|
|
end
|
|
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
|
|
|
|
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
|
|
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 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 = 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(
|
|
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 = current_account.articles
|
|
.text_search(search_query)
|
|
.page(params[:page])
|
|
.per(15)
|
|
end
|
|
end
|
|
|
|
SearchService.prepend_mod_with('SearchService')
|