feat(search): Fix slow queries with FTS5 on Contacts

This commit is contained in:
Ben Gotow 2016-11-02 13:03:20 -07:00
parent ec7133dff1
commit 432012a6bc
6 changed files with 109 additions and 10 deletions

View file

@ -0,0 +1,86 @@
import {
Contact,
DatabaseStore,
} from 'nylas-exports';
const INDEX_VERSION = 1;
class ContactSearchIndexStore {
constructor() {
this.unsubscribers = []
}
activate() {
this.initializeIndex();
this.unsubscribers = [
DatabaseStore.listen(this.onDataChanged),
];
}
deactivate() {
this.unsubscribers.forEach(unsub => unsub())
}
initializeIndex() {
if (NylasEnv.config.get('contactSearchIndexVersion') !== INDEX_VERSION) {
DatabaseStore.dropSearchIndex(Contact)
.then(() => DatabaseStore.createSearchIndex(Contact))
.then(() => this.buildIndex())
}
}
buildIndex(offset = 0) {
const indexingPageSize = 1000;
const indexingPageDelay = 1000;
DatabaseStore.findAll(Contact).limit(indexingPageSize).offset(offset).then((contacts) => {
if (contacts.length === 0) {
NylasEnv.config.set('contactSearchIndexVersion', INDEX_VERSION)
return;
}
Promise.each(contacts, (contact) => {
return DatabaseStore.indexModel(contact, this.getIndexDataForContact(contact))
}).then(() => {
setTimeout(() => {
this.buildIndex(offset + contacts.length);
}, indexingPageDelay);
});
});
}
/**
* When a contact gets updated we will update the search index with the data
* from that contact if the account it belongs to is not being currently
* synced.
*
* When the account is successfully synced, its contacts will be added to the
* index either via `onAccountsChanged` or via `initializeIndex` when the app
* starts
*/
onDataChanged = (change) => {
if (change.objectClass !== Contact.name) {
return;
}
change.objects.forEach((contact) => {
if (change.type === 'persist') {
DatabaseStore.indexModel(contact, this.getIndexDataForContact(contact))
} else {
DatabaseStore.unindexModel(contact)
}
});
}
getIndexDataForContact(contact) {
return {
content: [
contact.name ? contact.name : '',
contact.email ? contact.email : '',
contact.email ? contact.email.replace('@', ' ') : '',
].join(' '),
};
}
}
export default new ContactSearchIndexStore()

View file

@ -1,10 +1,13 @@
import SearchIndexStore from './search-index-store'
import ThreadSearchIndexStore from './thread-search-index-store'
import ContactSearchIndexStore from './contact-search-index-store'
export function activate() {
SearchIndexStore.activate()
ThreadSearchIndexStore.activate()
ContactSearchIndexStore.activate()
}
export function deactivate() {
SearchIndexStore.deactivate()
ThreadSearchIndexStore.deactivate()
ContactSearchIndexStore.deactivate()
}

View file

@ -230,7 +230,11 @@ class SearchIndexStore {
participants,
body: messageBodies,
subject: thread.subject,
})
}).then((results) => {
return {
content: [results.participants, results.body, results.subject].concat(' '),
};
});
}
deactivate() {

View file

@ -241,6 +241,9 @@ class NotCompositeMatcher extends AndCompositeMatcher {
class SearchMatcher extends Matcher {
constructor(searchQuery) {
if ((typeof searchQuery !== 'string') || (searchQuery.length === 0)) {
throw new Error("You must pass a string with non-zero length to search.")
}
super(null, null, null);
this.searchQuery = (
searchQuery.trim()

View file

@ -86,6 +86,10 @@ export default class Contact extends Model {
},
};
static searchable = true;
static searchFields = ['content'];
static fromString(string, {accountId} = {}) {
const emailRegex = RegExpUtils.emailRegex();
const match = emailRegex.exec(string);

View file

@ -47,7 +47,8 @@ class ContactStore extends NylasStore
search = search.toLowerCase()
accountCount = AccountStore.accounts().length
return Promise.resolve([]) if not search or search.length is 0
if not search or search.length is 0
return Promise.resolve([])
# Search ranked contacts which are stored in order in memory
results = []
@ -63,11 +64,9 @@ class ContactStore extends NylasStore
# return contacts with distinct email addresses, and the same contact
# could exist in every account. Rather than make SQLite do a SELECT DISTINCT
# (which is very slow), we just ask for more items.
query = DatabaseStore.findAll(Contact).whereAny([
Contact.attributes.name.like(search),
Contact.attributes.email.like(search)
])
query.limit(limit * accountCount)
query = DatabaseStore.findAll(Contact)
.search(search)
.limit(limit * accountCount)
query.then (queryResults) =>
existingEmails = _.pluck(results, 'email')