Merge pull request #3715 from ByteHamster/speed-up-search

Increased search performance
This commit is contained in:
H. Lehmann 2020-01-09 17:27:48 +01:00 committed by GitHub
commit cd0a69ef63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 105 additions and 311 deletions

View File

@ -93,10 +93,7 @@ public class SearchlistAdapter extends BaseAdapter {
} else if (component.getClass() == FeedItem.class) {
final FeedItem item = (FeedItem) component;
holder.title.setText(item.getTitle());
if (result.getSubtitle() != null) {
holder.subtitle.setVisibility(View.VISIBLE);
holder.subtitle.setText(result.getSubtitle());
}
holder.subtitle.setText(result.getLocation().getDescription());
convertView.setAlpha(item.isPlayed() ? 0.5f : 1.0f);

View File

@ -1,34 +1,22 @@
package de.danoeh.antennapod.core.feed;
import de.danoeh.antennapod.core.storage.SearchLocation;
public class SearchResult {
private final FeedComponent component;
/** Additional information (e.g. where it was found) */
private String subtitle;
/** Higher value means more importance */
private final int value;
private final FeedComponent component;
private SearchLocation location;
public SearchResult(FeedComponent component, int value, String subtitle) {
super();
this.component = component;
this.value = value;
this.subtitle = subtitle;
}
public SearchResult(FeedComponent component, SearchLocation location) {
super();
this.component = component;
this.location = location;
}
public FeedComponent getComponent() {
return component;
}
public String getSubtitle() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle = subtitle;
}
public int getValue() {
return value;
}
public FeedComponent getComponent() {
return component;
}
public SearchLocation getLocation() {
return location;
}
}

View File

@ -543,140 +543,20 @@ public final class DBTasks {
}
/**
* Searches the titles of FeedItems of a specific Feed for a given
* string.
* Searches the FeedItems of a specific Feed for a given string.
*
* @param context Used for accessing the DB.
* @param feedID The id of the feed whose items should be searched.
* @param query The search string.
* @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
* @return A FutureTask object that executes the search request
* and returns the search result as a List of FeedItems.
*/
public static FutureTask<List<FeedItem>> searchFeedItemTitle(final Context context,
public static FutureTask<List<FeedItem>> searchFeedItems(final Context context,
final long feedID, final String query) {
return new FutureTask<>(new QueryTask<List<FeedItem>>(context) {
@Override
public void execute(PodDBAdapter adapter) {
Cursor searchResult = adapter.searchItemTitles(feedID,
query);
List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult);
DBReader.loadAdditionalFeedItemListData(items);
setResult(items);
searchResult.close();
}
});
}
/**
* Searches the authors of FeedItems of a specific Feed for a given
* string.
*
* @param context Used for accessing the DB.
* @param feedID The id of the feed whose items should be searched.
* @param query The search string.
* @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
*/
public static FutureTask<List<FeedItem>> searchFeedItemAuthor(final Context context,
final long feedID, final String query) {
return new FutureTask<>(new QueryTask<List<FeedItem>>(context) {
@Override
public void execute(PodDBAdapter adapter) {
Cursor searchResult = adapter.searchItemAuthors(feedID,
query);
List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult);
DBReader.loadAdditionalFeedItemListData(items);
setResult(items);
searchResult.close();
}
});
}
/**
* Searches the feed identifiers of FeedItems of a specific Feed for a given
* string.
*
* @param context Used for accessing the DB.
* @param feedID The id of the feed whose items should be searched.
* @param query The search string.
* @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
*/
public static FutureTask<List<FeedItem>> searchFeedItemFeedIdentifier(final Context context,
final long feedID, final String query) {
return new FutureTask<>(new QueryTask<List<FeedItem>>(context) {
@Override
public void execute(PodDBAdapter adapter) {
Cursor searchResult = adapter.searchItemFeedIdentifiers(feedID,
query);
List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult);
DBReader.loadAdditionalFeedItemListData(items);
setResult(items);
searchResult.close();
}
});
}
/**
* Searches the descriptions of FeedItems of a specific Feed for a given
* string.
*
* @param context Used for accessing the DB.
* @param feedID The id of the feed whose items should be searched.
* @param query The search string
* @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
*/
public static FutureTask<List<FeedItem>> searchFeedItemDescription(final Context context,
final long feedID, final String query) {
return new FutureTask<>(new QueryTask<List<FeedItem>>(context) {
@Override
public void execute(PodDBAdapter adapter) {
Cursor searchResult = adapter.searchItemDescriptions(feedID,
query);
List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult);
DBReader.loadAdditionalFeedItemListData(items);
setResult(items);
searchResult.close();
}
});
}
/**
* Searches the contentEncoded-value of FeedItems of a specific Feed for a given
* string.
*
* @param context Used for accessing the DB.
* @param feedID The id of the feed whose items should be searched.
* @param query The search string
* @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
*/
public static FutureTask<List<FeedItem>> searchFeedItemContentEncoded(final Context context,
final long feedID, final String query) {
return new FutureTask<>(new QueryTask<List<FeedItem>>(context) {
@Override
public void execute(PodDBAdapter adapter) {
Cursor searchResult = adapter.searchItemContentEncoded(feedID,
query);
List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult);
DBReader.loadAdditionalFeedItemListData(items);
setResult(items);
searchResult.close();
}
});
}
/**
* Searches chapters of the FeedItems of a specific Feed for a given string.
*
* @param context Used for accessing the DB.
* @param feedID The id of the feed whose items should be searched.
* @param query The search string
* @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems.
*/
public static FutureTask<List<FeedItem>> searchFeedItemChapters(final Context context,
final long feedID, final String query) {
return new FutureTask<>(new QueryTask<List<FeedItem>>(context) {
@Override
public void execute(PodDBAdapter adapter) {
Cursor searchResult = adapter.searchItemChapters(feedID,
query);
Cursor searchResult = adapter.searchItems(feedID, query);
List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult);
DBReader.loadAdditionalFeedItemListData(items);
setResult(items);

View File

@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.storage;
import android.content.Context;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.core.feed.Chapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@ -17,13 +18,12 @@ import de.danoeh.antennapod.core.feed.SearchResult;
import de.danoeh.antennapod.core.util.comparator.InReverseChronologicalOrder;
/**
* Performs search on Feeds and FeedItems
* Performs search on Feeds and FeedItems.
*/
public class FeedSearcher {
private FeedSearcher(){}
private static final String TAG = "FeedSearcher";
private FeedSearcher() {
}
/**
* Search through a feed, or all feeds, for episodes that match the query in either the title,
@ -31,52 +31,54 @@ public class FeedSearcher {
* show notes. The list of resulting episodes also describes where the first match occurred
* (title, chapters, or show notes).
*
* @param context
* @param context Used for database access
* @param query search query
* @param selectedFeed feed to search, 0 to search through all feeds
* @return list of episodes containing the query
*/
@NonNull
public static List<SearchResult> performSearch(final Context context,
final String query, final long selectedFeed) {
final int values[] = {2, 1, 0, 0, 0, 0};
final String[] subtitles = {context.getString(R.string.found_in_title_label),
context.getString(R.string.found_in_chapters_label),
context.getString(R.string.found_in_shownotes_label),
context.getString(R.string.found_in_shownotes_label),
context.getString(R.string.found_in_authors_label),
context.getString(R.string.found_in_feeds_label)};
public static List<SearchResult> performSearch(final Context context, final String query, final long selectedFeed) {
final List<SearchResult> result = new ArrayList<>();
List<FutureTask<List<FeedItem>>> tasks = new ArrayList<>();
tasks.add(DBTasks.searchFeedItemTitle(context, selectedFeed, query));
tasks.add(DBTasks.searchFeedItemChapters(context, selectedFeed, query));
tasks.add(DBTasks.searchFeedItemDescription(context, selectedFeed, query));
tasks.add(DBTasks.searchFeedItemContentEncoded(context, selectedFeed, query));
tasks.add(DBTasks.searchFeedItemAuthor(context, selectedFeed, query));
tasks.add(DBTasks.searchFeedItemFeedIdentifier(context, selectedFeed, query));
for (FutureTask<List<FeedItem>> task : tasks) {
task.run();
}
try {
Set<Long> set = new HashSet<>();
for (int i = 0; i < tasks.size(); i++) {
FutureTask<List<FeedItem>> task = tasks.get(i);
List<FeedItem> items = task.get();
for (FeedItem item : items) {
if (!set.contains(item.getId())) { // to prevent duplicate results
result.add(new SearchResult(item, values[i], subtitles[i]));
set.add(item.getId());
}
FutureTask<List<FeedItem>> searchTask = DBTasks.searchFeedItems(context, selectedFeed, query);
searchTask.run();
final List<FeedItem> items = searchTask.get();
for (FeedItem item : items) {
SearchLocation location;
if (safeContains(item.getTitle(), query)) {
location = SearchLocation.TITLE;
} else if (safeContains(item.getContentEncoded(), query)) {
location = SearchLocation.SHOWNOTES;
} else if (safeContains(item.getDescription(), query)) {
location = SearchLocation.SHOWNOTES;
} else if (safeContains(item.getChapters(), query)) {
location = SearchLocation.CHAPTERS;
} else if (safeContains(item.getFeed().getAuthor(), query)) {
location = SearchLocation.AUTHORS;
} else {
location = SearchLocation.FEED;
}
result.add(new SearchResult(item, location));
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
Collections.sort(result, new InReverseChronologicalOrder());
return result;
}
private static boolean safeContains(String haystack, String needle) {
return haystack != null && haystack.contains(needle);
}
private static boolean safeContains(List<Chapter> haystack, String needle) {
if (haystack == null) {
return false;
}
for (Chapter chapter : haystack) {
if (safeContains(chapter.getTitle(), needle)) {
return true;
}
}
return false;
}
}

View File

@ -1290,133 +1290,39 @@ public class PodDBAdapter {
}
/**
* Searches for the given query in the description of all items or the items
* Searches for the given query in various values of all items or the items
* of a specified feed.
*
* @return A cursor with all search results in SEL_FI_EXTRA selection.
*/
public Cursor searchItemDescriptions(long feedID, String query) {
public Cursor searchItems(long feedID, String searchQuery) {
String preparedQuery = prepareSearchQuery(searchQuery);
String queryFeedId = "";
if (feedID != 0) {
// search items in specific feed
return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED
+ "=? AND " + KEY_DESCRIPTION + " LIKE '%"
+ prepareSearchQuery(query) + "%'",
new String[]{String.valueOf(feedID)}, null, null,
null
);
queryFeedId = KEY_FEED + " = " + feedID;
} else {
// search through all items
return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
KEY_DESCRIPTION + " LIKE '%" + prepareSearchQuery(query)
+ "%'", null, null, null, null
);
queryFeedId = "1 = 1";
}
}
/**
* Searches for the given query in the content-encoded field of all items or
* the items of a specified feed.
*
* @return A cursor with all search results in SEL_FI_EXTRA selection.
*/
public Cursor searchItemContentEncoded(long feedID, String query) {
if (feedID != 0) {
// search items in specific feed
return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED
+ "=? AND " + KEY_CONTENT_ENCODED + " LIKE '%"
+ prepareSearchQuery(query) + "%'",
new String[]{String.valueOf(feedID)}, null, null,
null
);
} else {
// search through all items
return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
KEY_CONTENT_ENCODED + " LIKE '%"
+ prepareSearchQuery(query) + "%'", null, null,
null, null
);
}
}
public Cursor searchItemTitles(long feedID, String query) {
if (feedID != 0) {
// search items in specific feed
return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED
+ "=? AND " + KEY_TITLE + " LIKE '%"
+ prepareSearchQuery(query) + "%'",
new String[]{String.valueOf(feedID)}, null, null,
null
);
} else {
// search through all items
return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL,
KEY_TITLE + " LIKE '%"
+ prepareSearchQuery(query) + "%'", null, null,
null, null
);
}
}
public Cursor searchItemAuthors(long feedID, String query) {
if (feedID != 0) {
// search items in specific feed
return db.rawQuery("SELECT " + TextUtils.join(", ", FEEDITEM_SEL_FI_SMALL) + " FROM " + TABLE_NAME_FEED_ITEMS
+ " JOIN " + TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
+ " WHERE " + KEY_FEED
+ "=? AND " + KEY_AUTHOR + " LIKE '%"
+ prepareSearchQuery(query) + "%' ORDER BY "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + " DESC",
new String[]{String.valueOf(feedID)}
);
} else {
// search through all items
return db.rawQuery("SELECT " + TextUtils.join(", ", FEEDITEM_SEL_FI_SMALL) + " FROM " + TABLE_NAME_FEED_ITEMS
+ " JOIN " + TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
+ " WHERE " + KEY_AUTHOR + " LIKE '%"
+ prepareSearchQuery(query) + "%' ORDER BY "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + " DESC",
null
);
}
}
public Cursor searchItemFeedIdentifiers(long feedID, String query) {
if (feedID != 0) {
// search items in specific feed
return db.rawQuery("SELECT " + TextUtils.join(", ", FEEDITEM_SEL_FI_SMALL) + " FROM " + TABLE_NAME_FEED_ITEMS
+ " JOIN " + TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
+ " WHERE " + KEY_FEED
+ "=? AND " + KEY_FEED_IDENTIFIER + " LIKE '%"
+ prepareSearchQuery(query) + "%' ORDER BY "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + " DESC",
new String[]{String.valueOf(feedID)}
);
} else {
// search through all items
return db.rawQuery("SELECT " + TextUtils.join(", ", FEEDITEM_SEL_FI_SMALL) + " FROM " + TABLE_NAME_FEED_ITEMS
+ " JOIN " + TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
+ " WHERE " + KEY_FEED_IDENTIFIER + " LIKE '%"
+ prepareSearchQuery(query) + "%' ORDER BY "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + " DESC",
null
);
}
}
public Cursor searchItemChapters(long feedID, String searchQuery) {
final String query;
if (feedID != 0) {
query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " +
TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" +
TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" +
feedID + " AND " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%"
+ prepareSearchQuery(searchQuery) + "%'";
} else {
query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " +
TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" +
TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%"
+ prepareSearchQuery(searchQuery) + "%'";
}
String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS
+ " LEFT JOIN " + TABLE_NAME_SIMPLECHAPTERS
+ " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM
+ "=" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID
+ " LEFT JOIN " + TABLE_NAME_FEEDS
+ " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED
+ "=" + TABLE_NAME_FEEDS + "." + KEY_ID
+ " WHERE " + queryFeedId + " AND ("
+ TABLE_NAME_FEED_ITEMS + "." + KEY_DESCRIPTION + " LIKE '%" + preparedQuery + "%' OR "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_CONTENT_ENCODED + " LIKE '%" + preparedQuery + "%' OR "
+ TABLE_NAME_FEED_ITEMS + "." + KEY_TITLE + " LIKE '%" + preparedQuery + "%' OR "
+ TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%" + preparedQuery + "%' OR "
+ TABLE_NAME_FEEDS + "." + KEY_AUTHOR + " LIKE '%" + preparedQuery + "%' OR "
+ TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER + " LIKE '%" + preparedQuery + "%'"
+ ") ORDER BY " + KEY_PUBDATE + " DESC "
+ "LIMIT 500";
return db.rawQuery(query, null);
}

View File

@ -0,0 +1,21 @@
package de.danoeh.antennapod.core.storage;
import androidx.annotation.StringRes;
import de.danoeh.antennapod.core.R;
public enum SearchLocation {
TITLE(R.string.found_in_title_label),
CHAPTERS(R.string.found_in_chapters_label),
SHOWNOTES(R.string.found_in_shownotes_label),
AUTHORS(R.string.found_in_authors_label),
FEED(R.string.found_in_feeds_label);
private int description;
SearchLocation(@StringRes int description) {
this.description = description;
}
public @StringRes int getDescription() {
return description;
}
}