Add trie-based user search cache to replace non-performant linear scans

Changelog-Added: Speed up user search
Tested-by: William Casarin <jb55@jb55.com>
Fixes: #1219
Closes: #1342
This commit is contained in:
2023-07-01 14:42:36 -04:00
committed by William Casarin
parent 4b7444f338
commit 6e964f71ff
13 changed files with 519 additions and 95 deletions

View File

@@ -8,7 +8,6 @@
import SwiftUI
struct SearchedUser: Identifiable {
let petname: String?
let profile: Profile?
let pubkey: String
@@ -28,11 +27,7 @@ struct UserSearch: View {
@EnvironmentObject var tagModel: TagModel
var users: [SearchedUser] {
guard let contacts = damus_state.contacts.event else {
return search_profiles(profiles: damus_state.profiles, search: search)
}
return search_users_for_autocomplete(profiles: damus_state.profiles, tags: contacts.tags, search: search)
return search_profiles(profiles: damus_state.profiles, search: search)
}
func on_user_tapped(user: SearchedUser) {
@@ -99,56 +94,6 @@ struct UserSearch_Previews: PreviewProvider {
}
}
func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search _search: String) -> [SearchedUser] {
var seen_user = Set<String>()
let search = _search.lowercased()
var matches = tags.reduce(into: Array<SearchedUser>()) { arr, tag in
guard tag.count >= 2 && tag[0] == "p" else {
return
}
let pubkey = tag[1]
guard !seen_user.contains(pubkey) else {
return
}
seen_user.insert(pubkey)
var petname: String? = nil
if tag.count >= 4 {
petname = tag[3]
}
let profile = profiles.lookup(id: pubkey)
guard ((petname?.lowercased().hasPrefix(search) ?? false) ||
(profile?.name?.lowercased().hasPrefix(search) ?? false) ||
(profile?.display_name?.lowercased().hasPrefix(search) ?? false)) else {
return
}
let searched_user = SearchedUser(petname: petname, profile: profile, pubkey: pubkey)
arr.append(searched_user)
}
// search profile cache as well
for tup in profiles.enumerated() {
let pk = tup.element.key
let prof = tup.element.value.profile
guard !seen_user.contains(pk) else {
continue
}
if let match = profile_search_matches(profiles: profiles, profile: prof, pubkey: pk, search: search) {
matches.append(match)
}
}
return matches
}
func user_tag_attr_string(profile: Profile?, pubkey: String) -> NSMutableAttributedString {
let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
let name = display_name.username.truncate(maxLength: 50)

View File

@@ -162,7 +162,8 @@ func get_profile_url(picture: String?, pubkey: String, profiles: Profiles) -> UR
}
func make_preview_profiles(_ pubkey: String) -> Profiles {
let profiles = Profiles()
let user_search_cache = UserSearchCache()
let profiles = Profiles(user_search_cache: user_search_cache)
let picture = "http://cdn.jb55.com/img/red-me.jpg"
let profile = Profile(name: "jb55", display_name: "William Casarin", about: "It's me", picture: picture, banner: "", website: "https://jb55.com", lud06: nil, lud16: nil, nip05: "jb55.com", damus_donation: nil)
let ts_profile = TimestampedProfile(profile: profile, timestamp: 0, event: test_event)

View File

@@ -180,33 +180,21 @@ func make_hashtagable(_ str: String) -> String {
}
func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] {
// Search by hex pubkey.
if search.count == 64 && hex_decode(search) != nil, let profile = profiles.lookup(id: search) {
return [SearchedUser(profile: profile, pubkey: search)]
}
// Search by npub pubkey.
if search.starts(with: "npub"), let bech32_key = decode_bech32_key(search), case Bech32Key.pub(let hex) = bech32_key, let profile = profiles.lookup(id: hex) {
return [SearchedUser(profile: profile, pubkey: hex)]
}
let new = search.lowercased()
return profiles.enumerated().reduce(into: []) { acc, els in
let pk = els.element.key
let prof = els.element.value.profile
if let searched = profile_search_matches(profiles: profiles, profile: prof, pubkey: pk, search: new) {
acc.append(searched)
}
}
}
let matched_pubkeys = profiles.user_search_cache.search(key: new)
func profile_search_matches(profiles: Profiles, profile prof: Profile, pubkey pk: String, search new: String) -> SearchedUser? {
let lowname = prof.name.map { $0.lowercased() }
let lownip05 = profiles.is_validated(pk).map { $0.host.lowercased() }
let lowdisp = prof.display_name.map { $0.lowercased() }
let ok = new.count == 1 ?
((lowname?.starts(with: new) ?? false) ||
(lownip05?.starts(with: new) ?? false) ||
(lowdisp?.starts(with: new) ?? false)) : (pk.starts(with: new) || String(new.dropFirst()) == pk
|| lowname?.contains(new) ?? false
|| lownip05?.contains(new) ?? false
|| lowdisp?.contains(new) ?? false)
if ok {
return SearchedUser(petname: nil, profile: prof, pubkey: pk)
}
return nil
return matched_pubkeys
.map { ($0, profiles.lookup(id: $0)) }
.filter { $1 != nil }
.map { SearchedUser(profile: $1, pubkey: $0) }
}