Skip to content

Rank emoji results in autocomplete #1112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d9a49d2
emoji test: Avoid coincidentally using "popular" emoji
gnprice Dec 8, 2024
906545f
emoji test: Avoid depending on any popular emoji being absent
gnprice Dec 8, 2024
ab50a5d
emoji: Short-circuit matching an empty query
gnprice Dec 7, 2024
e4b5604
emoji: Compute separator-plus-query just once per query
gnprice Dec 7, 2024
b67e172
algorithms: Add bucketSort, for stable, linear-time sorting
gnprice Dec 7, 2024
eb26c84
emoji [nfc]: Move _testCandidate logic onto query class
gnprice Dec 8, 2024
3cd8825
emoji [nfc]: Mark query-matches method visibleForTesting, conceptuall…
gnprice Dec 8, 2024
a077e9a
emoji [nfc]: Make testCandidate visible for testing
gnprice Dec 8, 2024
09813b2
emoji test [nfc]: Pull matchesNames up to wider scope
gnprice Dec 8, 2024
5cc5142
emoji test [nfc]: Tighten query-matches tests
gnprice Dec 8, 2024
70e132b
emoji [nfc]: Add ranking framework for emoji autocomplete results
gnprice Dec 7, 2024
604ed1e
emoji test [nfc]: Make the EmojiMatchQuality values explicit
gnprice Dec 8, 2024
37652db
emoji: Rank by quality of match (exact, prefix, other)
gnprice Dec 7, 2024
70004b0
emoji test [nfc]: Extract helpers realmCandidate, zulipCandidate
gnprice Dec 8, 2024
6c93b40
emoji: Add list of the "popular" emoji
gnprice Dec 8, 2024
bb8935a
emoji: Rank "popular" > custom > other emoji
gnprice Dec 7, 2024
a885520
emoji: Recognize word-aligned matches in ranking
gnprice Dec 7, 2024
7045afe
emoji: Order "popular" emoji canonically amongst themselves
gnprice Dec 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions lib/model/algorithms.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,67 @@ QueueList<int> setUnion(Iterable<int> xs, Iterable<int> ys) {
}
return result;
}

/// Sort the items by bucket, stably,
/// and if the buckets are few then in linear time.
///
/// The returned list will have the same elements as [xs], ordered by bucket,
/// and elements in each bucket will appear in the same order as in [xs].
/// In other words, the list is the result of a stable sort of [xs] by bucket.
/// (By contrast, Dart's [List.sort] is not guaranteed to be stable.)
///
/// For each element of [xs], the bucket identified by [bucketOf]
/// must be in the range `0 <= bucket < numBuckets`.
/// Repeated calls to [bucketOf] on the same element must return the same value.
///
/// If [bucketOf] returns different answers when called twice for some element,
/// this function's behavior is undefined:
/// it may throw, or may return an arbitrary list.
///
/// The cost of this function is linear in `xs.length` plus [numBuckets].
/// In particular if [numBuckets] is a constant
/// (or more generally is at most a constant multiple of `xs.length`),
/// then this function sorts the items in linear time, O(n).
/// On the other hand if there are many more buckets than elements,
/// consider using a different sorting algorithm.
List<T> bucketSort<T>(Iterable<T> xs, int Function(T) bucketOf, {
required int numBuckets,
}) {
if (xs.isEmpty) return [];
if (numBuckets <= 0) throw StateError("bucketSort: non-positive numBuckets");

final counts = List.generate(numBuckets, (_) => 0);
for (final x in xs) {
final key = bucketOf(x);
_checkBucket(key, numBuckets);
counts[key]++;
}
// Now counts[k] is the number of values with key k.

var partialSum = 0;
for (var k = 0; k < numBuckets; k++) {
final count = counts[k];
counts[k] = partialSum;
partialSum += count;
}
assert(partialSum == xs.length);
// Now counts[k] is the index where the first value with key k should go.

final result = List.generate(xs.length, (_) => xs.first);
for (final x in xs) {
// Each counts[k] is the index where the next value with key k should go.
final key = bucketOf(x);
_checkBucket(key, numBuckets);
final index = counts[key]++;
if (index >= result.length) {
throw StateError("bucketSort: bucketOf gave varying answers on same value");
}
result[index] = x;
}
return result;
}

void _checkBucket(int key, int numBuckets) {
if (key < 0) throw StateError("bucketSort: negative bucket");
if (key >= numBuckets) throw StateError("bucketSort: bucket out of range");
}
7 changes: 6 additions & 1 deletion lib/model/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -758,10 +758,15 @@ sealed class ComposeAutocompleteResult extends AutocompleteResult {}

/// An emoji chosen in an autocomplete interaction, via [EmojiAutocompleteView].
class EmojiAutocompleteResult extends ComposeAutocompleteResult {
EmojiAutocompleteResult(this.candidate);
EmojiAutocompleteResult(this.candidate, this.rank);

final EmojiCandidate candidate;

/// A measure of the result's quality in the context of the query.
///
/// Used internally by [EmojiAutocompleteView] for ranking the results.
final int rank;

@override
String toString() {
return 'EmojiAutocompleteResult(${candidate.description()})';
Expand Down
Loading