From b7b286cb893268bec38584acdf5696f68675eced Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Mon, 13 Feb 2017 17:45:46 -0800 Subject: [PATCH] Rank results that match the prefix of the first word higher Differentiates the results that match the first word in the title from matches farther into the title. Change-Id: I69f9804a51d1c2b0e476b0f082d634b3a598997c Fixes: 34975472 Test: make RunSettingsRoboTests --- .../CursorToSearchResultConverter.java | 3 +- .../search2/DatabaseResultLoader.java | 148 +++++++++++++++--- .../search2/InstalledAppResultLoader.java | 4 +- .../settings/search2/SearchResult.java | 2 +- .../DatabaseResultLoaderTest.java | 25 ++- 5 files changed, 147 insertions(+), 35 deletions(-) rename tests/robotests/src/com/android/settings/{search => search2}/DatabaseResultLoaderTest.java (95%) diff --git a/src/com/android/settings/search2/CursorToSearchResultConverter.java b/src/com/android/settings/search2/CursorToSearchResultConverter.java index bcb2e9da261..880a65b81de 100644 --- a/src/com/android/settings/search2/CursorToSearchResultConverter.java +++ b/src/com/android/settings/search2/CursorToSearchResultConverter.java @@ -220,9 +220,8 @@ class CursorToSearchResultConverter { private List getBreadcrumbs(SiteMapManager siteMapManager, Cursor cursor) { final String screenTitle = cursor.getString(COLUMN_INDEX_SCREEN_TITLE); final String screenClass = cursor.getString(COLUMN_INDEX_CLASS_NAME); - final List breadcrumbs = siteMapManager.buildBreadCrumb(mContext, screenClass, + return siteMapManager == null ? null : siteMapManager.buildBreadCrumb(mContext, screenClass, screenTitle); - return breadcrumbs; } /** Uses the breadcrumbs to determine the offset to the base rank. diff --git a/src/com/android/settings/search2/DatabaseResultLoader.java b/src/com/android/settings/search2/DatabaseResultLoader.java index 854b8dd4619..b7f5dd70836 100644 --- a/src/com/android/settings/search2/DatabaseResultLoader.java +++ b/src/com/android/settings/search2/DatabaseResultLoader.java @@ -20,6 +20,7 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import com.android.settings.dashboard.SiteMapManager; import com.android.settings.overlay.FeatureFactory; @@ -27,7 +28,6 @@ import com.android.settings.search.IndexDatabaseHelper; import com.android.settings.utils.AsyncLoader; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import static com.android.settings.search.IndexDatabaseHelper.IndexColumns; @@ -91,11 +91,13 @@ public class DatabaseResultLoader extends AsyncLoader primaryResults; + final List primaryFirstWordResults; + final List primaryMidWordResults; final List secondaryResults; final List tertiaryResults; - primaryResults = query(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]); - secondaryResults = query(MATCH_COLUMNS_SECONDARY, BASE_RANKS[1]); - tertiaryResults = query(MATCH_COLUMNS_TERTIARY, BASE_RANKS[2]); + primaryFirstWordResults = firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]); + primaryMidWordResults = secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]); + secondaryResults = anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]); + tertiaryResults = anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]); - final List results = new ArrayList<>(primaryResults.size() + final List results = new ArrayList<>( + primaryFirstWordResults.size() + + primaryMidWordResults.size() + secondaryResults.size() + tertiaryResults.size()); - results.addAll(primaryResults); + results.addAll(primaryFirstWordResults); + results.addAll(primaryMidWordResults); results.addAll(secondaryResults); results.addAll(tertiaryResults); + return results; } - private List query(String[] matchColumns, int baseRank) { - final String whereClause = buildWhereClause(matchColumns); - final String[] selection = buildQuerySelection(matchColumns.length * 2); - - final SQLiteDatabase database = IndexDatabaseHelper.getInstance(mContext) - .getReadableDatabase(); - final Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, whereClause, - selection, null, null, null); - return mConverter.convertCursor(mSiteMapManager, resultCursor, baseRank); - } - @Override protected boolean onCancelLoad() { // TODO @@ -168,7 +165,94 @@ public class DatabaseResultLoader extends AsyncLoader firstWordQuery(String[] matchColumns, int baseRank) { + final String whereClause = buildSingleWordWhereClause(matchColumns); + final String query = mQueryText + "%"; + final String[] selection = buildSingleWordSelection(query, matchColumns.length); + + return query(whereClause, selection, baseRank); + } + + /** + * Creates and executes the query which matches prefixes of the non-first words of the + * given columns. + * + * @param matchColumns The columns to match on + * @param baseRank The highest rank achievable by these results + * @return A list of the matching results. + */ + private List secondaryWordQuery(String[] matchColumns, int baseRank) { + final String whereClause = buildSingleWordWhereClause(matchColumns); + final String query = "% " + mQueryText + "%"; + final String[] selection = buildSingleWordSelection(query, matchColumns.length); + + return query(whereClause, selection, baseRank); + } + + /** + * Creates and executes the query which matches prefixes of the any word of the given columns. + * + * @param matchColumns The columns to match on + * @param baseRank The highest rank achievable by these results + * @return A list of the matching results. + */ + private List anyWordQuery(String[] matchColumns, int baseRank) { + final String whereClause = buildTwoWordWhereClause(matchColumns); + final String[] selection = buildAnyWordSelection(matchColumns.length * 2); + + return query(whereClause, selection, baseRank); + } + + /** + * Generic method used by all of the query methods above to execute a query. + * + * @param whereClause Where clause for the SQL query which uses bindings. + * @param selection List of the transformed query to match each bind in the whereClause + * @param baseRank The highest rank achievable by these results. + * @return A list of the matching results. + */ + private List query(String whereClause, String[] selection, int baseRank) { + final SQLiteDatabase database = IndexDatabaseHelper.getInstance(mContext) + .getReadableDatabase(); + final Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, whereClause, + selection, null, null, null); + return mConverter.convertCursor(mSiteMapManager, resultCursor, baseRank); + } + + /** + * Builds the SQLite WHERE clause that matches all matchColumns for a single query. + * + * @param matchColumns List of columns that will be used for matching. + * @return The constructed WHERE clause. + */ + private static String buildSingleWordWhereClause(String[] matchColumns) { + StringBuilder sb = new StringBuilder(" ("); + final int count = matchColumns.length; + for (int n = 0; n < count; n++) { + sb.append(matchColumns[n]); + sb.append(" like ? "); + if (n < count - 1) { + sb.append(" OR "); + } + } + sb.append(") AND enabled = 1"); + return sb.toString(); + } + + /** + * Builds the SQLite WHERE clause that matches all matchColumns to two different queries. + * + * @param matchColumns List of columns that will be used for matching. + * @return The constructed WHERE clause. + */ + private static String buildTwoWordWhereClause(String[] matchColumns) { StringBuilder sb = new StringBuilder(" ("); final int count = matchColumns.length; for (int n = 0; n < count; n++) { @@ -184,6 +268,20 @@ public class DatabaseResultLoader extends AsyncLoader { * Defines the max rank for a search result to be considered as ranked. Results with ranks * higher than this have no guarantee for sorting order. */ - public static final int MAX_RANK = 9; + public static final int MAX_RANK = 10; /** * The title of the result and main text displayed. diff --git a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java b/tests/robotests/src/com/android/settings/search2/DatabaseResultLoaderTest.java similarity index 95% rename from tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java rename to tests/robotests/src/com/android/settings/search2/DatabaseResultLoaderTest.java index 26b231e0286..3a21bb2508b 100644 --- a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java +++ b/tests/robotests/src/com/android/settings/search2/DatabaseResultLoaderTest.java @@ -15,7 +15,7 @@ * */ -package com.android.settings.search; +package com.android.settings.search2; import android.content.ContentValues; import android.content.Context; @@ -24,8 +24,7 @@ import android.database.sqlite.SQLiteDatabase; import com.android.settings.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; import com.android.settings.dashboard.SiteMapManager; -import com.android.settings.search2.DatabaseIndexingUtils; -import com.android.settings.search2.DatabaseResultLoader; +import com.android.settings.search.IndexDatabaseHelper; import com.android.settings.testutils.DatabaseTestUtils; import com.android.settings.testutils.FakeFeatureFactory; @@ -39,6 +38,8 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import java.util.List; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; @@ -200,12 +201,26 @@ public class DatabaseResultLoaderTest { assertThat(loader.loadInBackground().size()).isEqualTo(1); } + @Test + public void testSpecialCaseTwoWords_FirstWordMatches_RanksHigher() { + final String caseOne = "Apple pear"; + final String caseTwo = "Banana apple"; + insertSpecialCase(caseOne); + insertSpecialCase(caseTwo); + loader = new DatabaseResultLoader(mContext, "App", null); + List results = loader.loadInBackground(); + + assertThat(results.get(0).title).isEqualTo(caseOne); + assertThat(results.get(1).title).isEqualTo(caseTwo); + assertThat(results.get(0).rank).isLessThan(results.get(1).rank); + } + private void insertSpecialCase(String specialCase) { String normalized = DatabaseIndexingUtils.normalizeHyphen(specialCase); normalized = DatabaseIndexingUtils.normalizeString(normalized); ContentValues values = new ContentValues(); - values.put(IndexDatabaseHelper.IndexColumns.DOCID, 0); + values.put(IndexDatabaseHelper.IndexColumns.DOCID, normalized.hashCode()); values.put(IndexDatabaseHelper.IndexColumns.LOCALE, "en-us"); values.put(IndexDatabaseHelper.IndexColumns.DATA_RANK, 1); values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE, specialCase); @@ -313,4 +328,4 @@ public class DatabaseResultLoaderTest { mDb.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, null, values); } -} +} \ No newline at end of file