Skip to content

Commit

Permalink
Sort all suggestions by score regardless of their type
Browse files Browse the repository at this point in the history
  • Loading branch information
CrisBarreiro committed Apr 29, 2024
1 parent 1c41e69 commit 27d7dc4
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import com.duckduckgo.app.accessibility.data.AccessibilitySettingsDataStore
import com.duckduckgo.app.accessibility.data.AccessibilitySettingsSharedPreferences
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistorySearchSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion
import com.duckduckgo.app.autocomplete.api.AutoCompleteApi
import com.duckduckgo.app.autocomplete.api.AutoCompleteService
Expand Down
208 changes: 101 additions & 107 deletions app/src/main/java/com/duckduckgo/app/autocomplete/api/AutoComplete.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ import androidx.core.net.toUri
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistorySearchSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistorySuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion
import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.RankedItem.RankedBookmark
import com.duckduckgo.app.autocomplete.api.AutoCompleteApi.RankedItem.RankedHistoryEntry
import com.duckduckgo.app.browser.UriString
import com.duckduckgo.common.utils.baseHost
import com.duckduckgo.common.utils.toStringDropScheme
Expand All @@ -41,6 +40,7 @@ import com.duckduckgo.savedsites.api.models.SavedSite.Favorite
import com.squareup.anvil.annotations.ContributesBinding
import io.reactivex.Observable
import javax.inject.Inject
import kotlin.math.max
import org.jetbrains.annotations.VisibleForTesting

const val maximumNumberOfSuggestions = 12
Expand Down Expand Up @@ -69,16 +69,18 @@ interface AutoComplete {
val isFavorite: Boolean = false,
) : AutoCompleteSuggestion(phrase)

data class AutoCompleteHistorySuggestion(
override val phrase: String,
val title: String,
val url: String,
val isAllowedInTopHits: Boolean,
) : AutoCompleteSuggestion(phrase)

data class AutoCompleteHistorySearchSuggestion(
override val phrase: String,
) : AutoCompleteSuggestion(phrase)
sealed class AutoCompleteHistoryRelatedSuggestion(phrase: String) : AutoCompleteSuggestion(phrase) {
data class AutoCompleteHistorySuggestion(
override val phrase: String,
val title: String,
val url: String,
val isAllowedInTopHits: Boolean,
) : AutoCompleteHistoryRelatedSuggestion(phrase)

data class AutoCompleteHistorySearchSuggestion(
override val phrase: String,
) : AutoCompleteHistoryRelatedSuggestion(phrase)
}
}
}

Expand All @@ -98,44 +100,13 @@ class AutoCompleteApi @Inject constructor(
.zipWith(
getAutoCompleteFavoritesResults(query),
) { bookmarks, favorites ->
(favorites + bookmarks.filter { favorites.none { favorite -> it.savedSite.url == favorite.savedSite.url } })
}.map {
it.sortedByDescending { it.score }.mapNotNull {
val savedSite = it.savedSite
AutoCompleteBookmarkSuggestion(
phrase = savedSite.url.toUri().toStringDropScheme(),
title = savedSite.title,
url = savedSite.url,
isAllowedInTopHits = savedSite is Favorite,
isFavorite = savedSite is Favorite,
)
}
(favorites + bookmarks.filter { favorites.none { favorite -> (it.suggestion).url == favorite.suggestion.url } })
}.zipWith(
getHistoryResults(query).map {
it.sortedByDescending { it.score }.mapNotNull {
it.historyEntry.let { entry ->
when (entry) {
is VisitedPage -> {
AutoCompleteHistorySuggestion(
phrase = entry.url.toStringDropScheme(),
title = entry.title,
url = entry.url.toString(),
isAllowedInTopHits = isAllowedInTopHits(entry),
)
}
is VisitedSERP -> {
AutoCompleteHistorySearchSuggestion(
phrase = entry.query,
)
}
}
}
}
},
getHistoryResults(query),
) { bookmarksAndFavorites, historyItems ->
val searchHistory = historyItems.filterIsInstance<AutoCompleteHistorySearchSuggestion>()
val navigationHistory = historyItems.filterIsInstance<AutoCompleteHistorySuggestion>()
removeDuplicates(navigationHistory, bookmarksAndFavorites) + searchHistory
val searchHistory = historyItems.filter { it.suggestion is AutoCompleteHistorySearchSuggestion }
val navigationHistory = historyItems.filterIsInstance<RankedSuggestion<AutoCompleteHistorySuggestion>>()
(removeDuplicates(navigationHistory, bookmarksAndFavorites) + searchHistory).sortedByDescending { it.score }.map { it.suggestion }
}

return savedSitesObservable.zipWith(
Expand Down Expand Up @@ -168,16 +139,21 @@ class AutoCompleteApi @Inject constructor(
}

private fun removeDuplicates(
historySuggestions: List<AutoCompleteHistorySuggestion>,
bookmarkSuggestions: List<AutoCompleteBookmarkSuggestion>,
): List<AutoCompleteSuggestion> {
val bookmarkMap = bookmarkSuggestions.associateBy { it.phrase }
historySuggestions: List<RankedSuggestion<AutoCompleteHistorySuggestion>>,
bookmarkSuggestions: List<RankedSuggestion<AutoCompleteBookmarkSuggestion>>,
): List<RankedSuggestion<*>> {
val bookmarkMap = bookmarkSuggestions.associateBy { it.suggestion.phrase }

val uniqueHistorySuggestions = historySuggestions.filter { !bookmarkMap.containsKey(it.phrase) }
val uniqueHistorySuggestions = historySuggestions.filter { !bookmarkMap.containsKey(it.suggestion.phrase) }
val updatedBookmarkSuggestions = bookmarkSuggestions.map { bookmarkSuggestion ->
val historySuggestion = historySuggestions.find { it.phrase == bookmarkSuggestion.phrase }
val historySuggestion = historySuggestions.find { it.suggestion.phrase == bookmarkSuggestion.suggestion.phrase }
if (historySuggestion != null) {
bookmarkSuggestion.copy(isAllowedInTopHits = historySuggestion.isAllowedInTopHits)
bookmarkSuggestion.copy(
score = max(historySuggestion.score, bookmarkSuggestion.score),
suggestion = bookmarkSuggestion.suggestion.copy(
isAllowedInTopHits = historySuggestion.suggestion.isAllowedInTopHits,
),
)
} else {
bookmarkSuggestion
}
Expand All @@ -200,7 +176,7 @@ class AutoCompleteApi @Inject constructor(
.onErrorReturn { emptyList() }
.toObservable()

private fun getAutoCompleteBookmarkResults(query: String): Observable<MutableList<RankedBookmark>> =
private fun getAutoCompleteBookmarkResults(query: String): Observable<MutableList<RankedSuggestion<AutoCompleteBookmarkSuggestion>>> =
repository.getBookmarksObservable()
.map { rankBookmarks(query, it) }
.flattenAsObservable { it }
Expand All @@ -209,7 +185,7 @@ class AutoCompleteApi @Inject constructor(
.onErrorReturn { emptyList() }
.toObservable()

private fun getAutoCompleteFavoritesResults(query: String) =
private fun getAutoCompleteFavoritesResults(query: String): Observable<MutableList<RankedSuggestion<AutoCompleteBookmarkSuggestion>>> =
repository.getFavoritesObservable()
.map { rankFavorites(query, it) }
.flattenAsObservable { it }
Expand All @@ -218,7 +194,7 @@ class AutoCompleteApi @Inject constructor(
.onErrorReturn { emptyList() }
.toObservable()

private fun getHistoryResults(query: String) =
private fun getHistoryResults(query: String): Observable<List<RankedSuggestion<AutoCompleteHistoryRelatedSuggestion>>> =
navigationHistory.getHistorySingle()
.map { rankHistory(query, it) }
.flattenAsObservable { it }
Expand All @@ -230,22 +206,22 @@ class AutoCompleteApi @Inject constructor(
private fun rankBookmarks(
query: String,
bookmarks: List<Bookmark>,
): List<RankedBookmark> {
): List<RankedSuggestion<AutoCompleteBookmarkSuggestion>> {
return bookmarks.asSequence()
.sortByRank(query)
}

private fun rankFavorites(
query: String,
favorites: List<Favorite>,
): List<RankedBookmark> {
): List<RankedSuggestion<AutoCompleteBookmarkSuggestion>> {
return favorites.asSequence().sortByRank(query)
}

private fun rankHistory(
query: String,
history: List<HistoryEntry>,
): List<RankedHistoryEntry> {
): List<RankedSuggestion<AutoCompleteHistoryRelatedSuggestion>> {
return history.asSequence().sortHistoryByRank(query)
}

Expand All @@ -260,7 +236,7 @@ class AutoCompleteApi @Inject constructor(
// To optimize, query tokens can be precomputed
val tokens = queryTokens ?: tokensFrom(query)

var score = 0
var score = DEFAULT_SCORE
val lowercasedTitle = title?.lowercase() ?: ""
val queryCount = query.count()
val nakedUrl = url.naked()
Expand Down Expand Up @@ -348,85 +324,103 @@ class AutoCompleteApi @Inject constructor(
return builder.build().toString().removePrefix("//")
}

private fun Sequence<SavedSite>.sortByRank(query: String): List<RankedBookmark> {
return this.map { RankedBookmark(savedSite = it) }
private fun Sequence<SavedSite>.sortByRank(query: String): List<RankedSuggestion<AutoCompleteBookmarkSuggestion>> {
return this.map { savedSite ->
RankedSuggestion(
AutoCompleteBookmarkSuggestion(
phrase = savedSite.url.toUri().toStringDropScheme(),
title = savedSite.title,
url = savedSite.url,
isAllowedInTopHits = savedSite is Favorite,
isFavorite = savedSite is Favorite,
),
)
}
.map { scoreTitle(it, query) }
.map { scoreTokens(it, query) }
.filter { it.score >= 0 }
.sortedByDescending { it.score }
.filter { it.score > 0 }
.toList()
}

private fun Sequence<HistoryEntry>.sortHistoryByRank(query: String): List<RankedHistoryEntry> {
private fun Sequence<HistoryEntry>.sortHistoryByRank(query: String): List<RankedSuggestion<AutoCompleteHistoryRelatedSuggestion>> {
return this
.map { RankedHistoryEntry(it, score(it.title, it.url, it.visits.size, query)) }
.filter { it.score > 0 }
.sortedByDescending { it.score }
.map { entry ->
when (entry) {
is VisitedPage -> {
AutoCompleteHistorySuggestion(
phrase = entry.url.toStringDropScheme(),
title = entry.title,
url = entry.url.toString(),
isAllowedInTopHits = isAllowedInTopHits(entry),
)
}
is VisitedSERP -> {
AutoCompleteHistorySearchSuggestion(
phrase = entry.query,
)
}
}.let { suggestion ->
RankedSuggestion(suggestion, score(entry.title, entry.url, entry.visits.size, query))
}
}.filter { it.score > 0 }
.toList()
}

private fun scoreTitle(
rankedBookmark: RankedBookmark,
rankedBookmark: RankedSuggestion<AutoCompleteBookmarkSuggestion>,
query: String,
): RankedBookmark {
if (rankedBookmark.savedSite.title.startsWith(query, ignoreCase = true)) {
rankedBookmark.score += 200
} else if (rankedBookmark.savedSite.title.contains(" $query", ignoreCase = true)) {
rankedBookmark.score += 100
): RankedSuggestion<AutoCompleteBookmarkSuggestion> {
return if (rankedBookmark.suggestion.title.startsWith(query, ignoreCase = true)) {
rankedBookmark.copy(score = rankedBookmark.score + 200)
} else if (rankedBookmark.suggestion.title.contains(" $query", ignoreCase = true)) {
rankedBookmark.copy(score = rankedBookmark.score + 100)
} else {
rankedBookmark
}

return rankedBookmark
}

private fun scoreTokens(
rankedBookmark: RankedBookmark,
rankedBookmark: RankedSuggestion<AutoCompleteBookmarkSuggestion>,
query: String,
): RankedBookmark {
val savedSite = rankedBookmark.savedSite
val domain = savedSite.url.toUri().baseHost
): RankedSuggestion<AutoCompleteBookmarkSuggestion> {
val suggestion = rankedBookmark.suggestion
val domain = suggestion.url.toUri().baseHost
val tokens = query.split(" ")
var toReturn = rankedBookmark
if (tokens.size > 1) {
tokens.forEach { token ->
if (!savedSite.title.startsWith(token, ignoreCase = true) &&
!savedSite.title.contains(" $token", ignoreCase = true) &&
if (!suggestion.title.startsWith(token, ignoreCase = true) &&
!suggestion.title.contains(" $token", ignoreCase = true) &&
domain?.startsWith(token, ignoreCase = true) == false
) {
return rankedBookmark
}
}

rankedBookmark.score += 10
toReturn = toReturn.copy(score = toReturn.score + 10)

if (domain?.startsWith(tokens.first(), ignoreCase = true) == true) {
rankedBookmark.score += 300
} else if (savedSite.title.startsWith(tokens.first(), ignoreCase = true)) {
rankedBookmark.score += 50
toReturn = toReturn.copy(score = toReturn.score + 300)
} else if (suggestion.title.startsWith(tokens.first(), ignoreCase = true)) {
toReturn = toReturn.copy(score = toReturn.score + 50)
}
} else if (savedSite.url.redactSchemeAndWwwSubDomain().startsWith(tokens.first().trimEnd { it == '/' }, ignoreCase = true)) {
rankedBookmark.score += 300
} else if (suggestion.url.redactSchemeAndWwwSubDomain().startsWith(tokens.first().trimEnd { it == '/' }, ignoreCase = true)) {
toReturn = toReturn.copy(score = toReturn.score + 300)
}

return rankedBookmark
return toReturn
}

private fun String.redactSchemeAndWwwSubDomain(): String {
return this.toUri().toStringDropScheme().removePrefix("www.")
}

private sealed class RankedItem(open var score: Int) {
data class RankedBookmark(
val savedSite: SavedSite,
override var score: Int = BOOKMARK_SCORE,
) : RankedItem(score)

data class RankedHistoryEntry(
val historyEntry: HistoryEntry,
override var score: Int = HISTORY_ENTRY_SCORE,
) : RankedItem(score)
}

companion object {
private const val BOOKMARK_SCORE = -1
private const val HISTORY_ENTRY_SCORE = -1
private const val DEFAULT_SCORE = -1
}

private data class RankedSuggestion<T : AutoCompleteSuggestion> (
val suggestion: T,
val score: Int = DEFAULT_SCORE,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.accessibility.data.AccessibilitySettingsDataStore
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistorySearchSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistorySuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion
import com.duckduckgo.app.bookmarks.ui.BookmarksBottomSheetDialog
import com.duckduckgo.app.bookmarks.ui.EditSavedSiteDialogFragment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ import com.duckduckgo.app.autocomplete.api.AutoComplete
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteBookmarkSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistorySearchSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistorySuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySearchSuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteHistoryRelatedSuggestion.AutoCompleteHistorySuggestion
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion
import com.duckduckgo.app.bookmarks.ui.EditSavedSiteDialogFragment.DeleteBookmarkListener
import com.duckduckgo.app.bookmarks.ui.EditSavedSiteDialogFragment.EditSavedSiteListener
Expand Down

0 comments on commit 27d7dc4

Please sign in to comment.