Improve search performance.

This commit is contained in:
Greyson Parrelli
2021-06-10 15:47:12 -04:00
committed by GitHub
parent 53ffca964d
commit c274ed6a96
18 changed files with 413 additions and 219 deletions

View File

@@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.search
import org.thoughtcrime.securesms.recipients.Recipient
data class ContactSearchResult(val results: List<Recipient>, val query: String)

View File

@@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.search
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Represents a search result for a message.
*/
data class MessageResult(
val conversationRecipient: Recipient,
val messageRecipient: Recipient,
val body: String,
val bodySnippet: String,
val threadId: Long,
val messageId: Long,
val receivedTimestampMs: Long,
val isMms: Boolean
)

View File

@@ -0,0 +1,3 @@
package org.thoughtcrime.securesms.search
data class MessageSearchResult(val results: List<MessageResult>, val query: String)

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.search;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.MergeCursor;
import android.text.TextUtils;
@@ -13,12 +12,11 @@ import com.annimon.stream.Stream;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactRepository;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.database.CursorList;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MentionDatabase;
import org.thoughtcrime.securesms.database.MentionUtil;
import org.thoughtcrime.securesms.database.MessageDatabase;
@@ -35,18 +33,19 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.FtsUtil;
import org.thoughtcrime.securesms.util.Util;
import org.signal.core.util.concurrent.LatestPrioritizedSerialExecutor;
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import static org.thoughtcrime.securesms.database.SearchDatabase.SNIPPET_WRAP;
@@ -61,13 +60,13 @@ public class SearchRepository {
private final SearchDatabase searchDatabase;
private final ContactRepository contactRepository;
private final ThreadDatabase threadDatabase;
private final ContactAccessor contactAccessor;
private final Executor serialExecutor;
private final ExecutorService parallelExecutor;
private final RecipientDatabase recipientDatabase;
private final MentionDatabase mentionDatabase;
private final MessageDatabase mmsDatabase;
private final LatestPrioritizedSerialExecutor searchExecutor;
private final Executor serialExecutor;
public SearchRepository() {
this.context = ApplicationDependencies.getApplication().getApplicationContext();
this.searchDatabase = DatabaseFactory.getSearchDatabase(context);
@@ -76,36 +75,44 @@ public class SearchRepository {
this.mentionDatabase = DatabaseFactory.getMentionDatabase(context);
this.mmsDatabase = DatabaseFactory.getMmsDatabase(context);
this.contactRepository = new ContactRepository(context);
this.contactAccessor = ContactAccessor.getInstance();
this.serialExecutor = SignalExecutors.SERIAL;
this.parallelExecutor = SignalExecutors.BOUNDED;
this.searchExecutor = new LatestPrioritizedSerialExecutor(SignalExecutors.BOUNDED);
this.serialExecutor = new SerialExecutor(SignalExecutors.BOUNDED);
}
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
if (TextUtils.isEmpty(query)) {
callback.onResult(SearchResult.EMPTY);
return;
}
public void queryThreads(@NonNull String query, @NonNull Consumer<ThreadSearchResult> callback) {
searchExecutor.execute(2, () -> {
long start = System.currentTimeMillis();
List<ThreadRecord> result = queryConversations(query);
serialExecutor.execute(() -> {
Log.d(TAG, "[threads] Search took " + (System.currentTimeMillis() - start) + " ms");
callback.accept(new ThreadSearchResult(result, query));
});
}
public void queryContacts(@NonNull String query, @NonNull Consumer<ContactSearchResult> callback) {
searchExecutor.execute(1, () -> {
long start = System.currentTimeMillis();
List<Recipient> result = queryContacts(query);
Log.d(TAG, "[contacts] Search took " + (System.currentTimeMillis() - start) + " ms");
callback.accept(new ContactSearchResult(result, query));
});
}
public void queryMessages(@NonNull String query, @NonNull Consumer<MessageSearchResult> callback) {
searchExecutor.execute(0, () -> {
long start = System.currentTimeMillis();
String cleanQuery = FtsUtil.sanitize(query);
Future<List<Recipient>> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery));
Future<List<ThreadRecord>> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery));
Future<List<MessageResult>> messages = parallelExecutor.submit(() -> queryMessages(cleanQuery));
Future<List<MessageResult>> mentionMessages = parallelExecutor.submit(() -> queryMentions(sanitizeQueryAsTokens(query)));
List<MessageResult> messages = queryMessages(cleanQuery);
List<MessageResult> mentionMessages = queryMentions(sanitizeQueryAsTokens(query));
List<MessageResult> combined = mergeMessagesAndMentions(messages, mentionMessages);
try {
long startTime = System.currentTimeMillis();
SearchResult result = new SearchResult(cleanQuery, contacts.get(), conversations.get(), mergeMessagesAndMentions(messages.get(), mentionMessages.get()));
Log.d(TAG, "[messages] Search took " + (System.currentTimeMillis() - start) + " ms");
Log.d(TAG, "Total time: " + (System.currentTimeMillis() - startTime) + " ms");
callback.onResult(result);
} catch (ExecutionException | InterruptedException e) {
Log.w(TAG, e);
callback.onResult(SearchResult.EMPTY);
}
callback.accept(new MessageSearchResult(combined, query));
});
}
@@ -127,6 +134,10 @@ public class SearchRepository {
}
private List<Recipient> queryContacts(String query) {
if (Util.isEmpty(query)) {
return Collections.emptyList();
}
Cursor contacts = null;
try {
@@ -144,15 +155,39 @@ public class SearchRepository {
}
private @NonNull List<ThreadRecord> queryConversations(@NonNull String query) {
List<String> numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
List<RecipientId> recipientIds = Stream.of(numbers).map(number -> Recipient.external(context, number)).map(Recipient::getId).toList();
if (Util.isEmpty(query)) {
return Collections.emptyList();
}
try (Cursor cursor = threadDatabase.getFilteredConversationList(recipientIds)) {
Set<RecipientId> recipientIds = new LinkedHashSet<>();
try (Cursor cursor = DatabaseFactory.getRecipientDatabase(context).queryAllContacts(query)) {
while (cursor != null && cursor.moveToNext()) {
recipientIds.add(RecipientId.from(CursorUtil.requireString(cursor, RecipientDatabase.ID)));
}
}
GroupDatabase.GroupRecord record;
try (GroupDatabase.Reader reader = DatabaseFactory.getGroupDatabase(context).getGroupsFilteredByTitle(query, true, false)) {
while ((record = reader.getNext()) != null) {
recipientIds.add(record.getRecipientId());
}
}
if (context.getString(R.string.note_to_self).toLowerCase().contains(query.toLowerCase())) {
recipientIds.add(Recipient.self().getId());
}
try (Cursor cursor = threadDatabase.getFilteredConversationList(new ArrayList<>(recipientIds))) {
return readToList(cursor, new ThreadModelBuilder(threadDatabase));
}
}
private @NonNull List<MessageResult> queryMessages(@NonNull String query) {
if (Util.isEmpty(query)) {
return Collections.emptyList();
}
List<MessageResult> results;
try (Cursor cursor = searchDatabase.queryMessages(query)) {
results = readToList(cursor, new MessageModelBuilder());
@@ -160,8 +195,8 @@ public class SearchRepository {
List<Long> messageIds = new LinkedList<>();
for (MessageResult result : results) {
if (result.isMms) {
messageIds.add(result.messageId);
if (result.isMms()) {
messageIds.add(result.getMessageId());
}
}
@@ -176,15 +211,15 @@ public class SearchRepository {
List<MessageResult> updatedResults = new ArrayList<>(results.size());
for (MessageResult result : results) {
if (result.isMms && mentions.containsKey(result.messageId)) {
List<Mention> messageMentions = mentions.get(result.messageId);
if (result.isMms() && mentions.containsKey(result.getMessageId())) {
List<Mention> messageMentions = mentions.get(result.getMessageId());
//noinspection ConstantConditions
String updatedBody = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, result.body, messageMentions).getBody().toString();
String updatedSnippet = updateSnippetWithDisplayNames(result.body, result.bodySnippet, messageMentions);
String updatedBody = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, result.getBody(), messageMentions).getBody().toString();
String updatedSnippet = updateSnippetWithDisplayNames(result.getBody(), result.getBodySnippet(), messageMentions);
//noinspection ConstantConditions
updatedResults.add(new MessageResult(result.conversationRecipient, result.messageRecipient, updatedBody, updatedSnippet, result.threadId, result.messageId, result.receivedTimestampMs, result.isMms));
updatedResults.add(new MessageResult(result.getConversationRecipient(), result.getMessageRecipient(), updatedBody, updatedSnippet, result.getThreadId(), result.getMessageId(), result.getReceivedTimestampMs(), result.isMms()));
} else {
updatedResults.add(result);
}
@@ -345,18 +380,18 @@ public class SearchRepository {
List<MessageResult> combined = new ArrayList<>(messages.size() + mentionMessages.size());
for (MessageResult result : messages) {
combined.add(result);
if (result.isMms) {
includedMmsMessages.add(result.messageId);
if (result.isMms()) {
includedMmsMessages.add(result.getMessageId());
}
}
for (MessageResult result : mentionMessages) {
if (!includedMmsMessages.contains(result.messageId)) {
if (!includedMmsMessages.contains(result.getMessageId())) {
combined.add(result);
}
}
Collections.sort(combined, Collections.reverseOrder((left, right) -> Long.compare(left.receivedTimestampMs, right.receivedTimestampMs)));
Collections.sort(combined, Collections.reverseOrder((left, right) -> Long.compare(left.getReceivedTimestampMs(), right.getReceivedTimestampMs())));
return combined;
}

View File

@@ -0,0 +1,39 @@
package org.thoughtcrime.securesms.search
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Represents an all-encompassing search result that can contain various result for different
* subcategories.
*/
data class SearchResult(
val query: String,
val contacts: List<Recipient>,
val conversations: List<ThreadRecord>,
val messages: List<MessageResult>
) {
fun size(): Int {
return contacts.size + conversations.size + messages.size
}
val isEmpty: Boolean
get() = size() == 0
fun merge(result: ContactSearchResult): SearchResult {
return this.copy(contacts = result.results, query = result.query)
}
fun merge(result: ThreadSearchResult): SearchResult {
return this.copy(conversations = result.results, query = result.query)
}
fun merge(result: MessageSearchResult): SearchResult {
return this.copy(messages = result.results, query = result.query)
}
companion object {
@JvmField
val EMPTY = SearchResult("", emptyList(), emptyList(), emptyList())
}
}

View File

@@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.search
import org.thoughtcrime.securesms.database.model.ThreadRecord
data class ThreadSearchResult(val results: List<ThreadRecord>, val query: String)