feat: Use resolved contacts as base relation for filtering (#12520)
This PR has two changes to speed up contact filtering ### Updated Base Relation Update the `base_relation` to use resolved contacts scope to improve perf when filtering conversations. This narrows the search space drastically, and what is usually a sequential scan becomes a index scan for that `account_id` ref: https://github.com/chatwoot/chatwoot/pull/9347 ref: https://github.com/chatwoot/chatwoot/pull/7175/ Result: https://explain.dalibo.com/plan/c8a8gb17f0275fgf#plan ## Selective filtering in Compose New Conversation We also cost of filtering in compose new conversation dialog by reducing the search space based on the search candidate. For instance, a search term that obviously can’t be a phone, we exclude that from the filter. Similarly we skip name lookups for email-shaped queries. Removing the phone number took the query times from 50 seconds to under 1 seconds ### Comparison 1. Only Email: https://explain.dalibo.com/plan/h91a6844a4438a6a 2. Email + Name: https://explain.dalibo.com/plan/beg3aah05ch9ade0 3. Email + Name + Phone: https://explain.dalibo.com/plan/c8a8gb17f0275fgf
This commit is contained in:
@@ -148,10 +148,21 @@ const isAnyDropdownActive = computed(() => {
|
|||||||
|
|
||||||
const handleContactSearch = value => {
|
const handleContactSearch = value => {
|
||||||
showContactsDropdown.value = true;
|
showContactsDropdown.value = true;
|
||||||
emit('searchContacts', {
|
const query = typeof value === 'string' ? value.trim() : '';
|
||||||
keys: ['email', 'phone_number', 'name'],
|
const hasAlphabet = Array.from(query).some(char => {
|
||||||
query: value,
|
const lower = char.toLowerCase();
|
||||||
|
const upper = char.toUpperCase();
|
||||||
|
return lower !== upper;
|
||||||
});
|
});
|
||||||
|
const isEmailLike = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(query);
|
||||||
|
|
||||||
|
const keys = ['email', 'phone_number', 'name'].filter(key => {
|
||||||
|
if (key === 'phone_number' && hasAlphabet) return false;
|
||||||
|
if (key === 'name' && isEmailLike) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('searchContacts', { keys, query: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDropdownUpdate = (type, value) => {
|
const handleDropdownUpdate = (type, value) => {
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ class Contacts::FilterService < FilterService
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: @account.contacts.resolved_contacts ? to stay consistant with the behavior in ui
|
|
||||||
def base_relation
|
def base_relation
|
||||||
@account.contacts
|
@account.contacts.resolved_contacts(use_crm_v2: @account.feature_enabled?('crm_v2'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_config
|
def filter_config
|
||||||
|
|||||||
@@ -103,13 +103,11 @@ RSpec.describe Account::ContactsExportJob do
|
|||||||
expect(csv_data.length).to eq(1)
|
expect(csv_data.length).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: This returns unresolved contacts as well since filter service returns the same
|
it 'returns filtered data limited to resolved contacts when filter is provided' do
|
||||||
# Change this when we make changes to filter service and ensure only resolved contacts are returned
|
|
||||||
it 'returns filtered data which inclues unresolved contacts when filter is provided' do
|
|
||||||
create(:contact, account: account, email: nil, phone_number: nil, additional_attributes: { :country_code => 'India' })
|
create(:contact, account: account, email: nil, phone_number: nil, additional_attributes: { :country_code => 'India' })
|
||||||
described_class.perform_now(account.id, user.id, [], { :payload => [city_filter.merge(:query_operator => nil)] }.with_indifferent_access)
|
described_class.perform_now(account.id, user.id, [], { :payload => [city_filter.merge(:query_operator => nil)] }.with_indifferent_access)
|
||||||
csv_data = CSV.parse(account.contacts_export.download, headers: true)
|
csv_data = CSV.parse(account.contacts_export.download, headers: true)
|
||||||
expect(csv_data.length).to eq(5)
|
expect(csv_data.length).to eq(4)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns filtered data when multiple filters are provided' do
|
it 'returns filtered data when multiple filters are provided' do
|
||||||
|
|||||||
@@ -7,9 +7,25 @@ describe Contacts::FilterService do
|
|||||||
let!(:first_user) { create(:user, account: account) }
|
let!(:first_user) { create(:user, account: account) }
|
||||||
let!(:second_user) { create(:user, account: account) }
|
let!(:second_user) { create(:user, account: account) }
|
||||||
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
|
let!(:inbox) { create(:inbox, account: account, enable_auto_assignment: false) }
|
||||||
let!(:en_contact) { create(:contact, account: account, additional_attributes: { 'country_code': 'uk' }) }
|
let!(:en_contact) do
|
||||||
let!(:el_contact) { create(:contact, account: account, additional_attributes: { 'country_code': 'gr' }) }
|
create(:contact,
|
||||||
let!(:cs_contact) { create(:contact, :with_phone_number, account: account, additional_attributes: { 'country_code': 'cz' }) }
|
account: account,
|
||||||
|
email: Faker::Internet.unique.email,
|
||||||
|
additional_attributes: { 'country_code': 'uk' })
|
||||||
|
end
|
||||||
|
let!(:el_contact) do
|
||||||
|
create(:contact,
|
||||||
|
account: account,
|
||||||
|
email: Faker::Internet.unique.email,
|
||||||
|
additional_attributes: { 'country_code': 'gr' })
|
||||||
|
end
|
||||||
|
let!(:cs_contact) do
|
||||||
|
create(:contact,
|
||||||
|
:with_phone_number,
|
||||||
|
account: account,
|
||||||
|
email: Faker::Internet.unique.email,
|
||||||
|
additional_attributes: { 'country_code': 'cz' })
|
||||||
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
create(:inbox_member, user: first_user, inbox: inbox)
|
create(:inbox_member, user: first_user, inbox: inbox)
|
||||||
@@ -103,7 +119,12 @@ describe Contacts::FilterService do
|
|||||||
|
|
||||||
context 'with standard attributes - blocked' do
|
context 'with standard attributes - blocked' do
|
||||||
it 'filter contacts by blocked' do
|
it 'filter contacts by blocked' do
|
||||||
blocked_contact = create(:contact, account: account, blocked: true)
|
blocked_contact = create(
|
||||||
|
:contact,
|
||||||
|
account: account,
|
||||||
|
blocked: true,
|
||||||
|
email: Faker::Internet.unique.email
|
||||||
|
)
|
||||||
params = { payload: [{ attribute_key: 'blocked', filter_operator: 'equal_to', values: ['true'],
|
params = { payload: [{ attribute_key: 'blocked', filter_operator: 'equal_to', values: ['true'],
|
||||||
query_operator: nil }.with_indifferent_access] }
|
query_operator: nil }.with_indifferent_access] }
|
||||||
result = filter_service.new(account, first_user, params).perform
|
result = filter_service.new(account, first_user, params).perform
|
||||||
|
|||||||
Reference in New Issue
Block a user