feat: harden search ranking and deduplication
This commit is contained in:
@@ -30,24 +30,32 @@ func SearchContactsFromMessages(messages []Message, query SearchContactsQuery) S
|
||||
addMessageContact(unique, message.ReceiverID)
|
||||
}
|
||||
|
||||
queryText := strings.ToLower(strings.TrimSpace(query.Query))
|
||||
ids := make([]string, 0, len(unique))
|
||||
for id := range unique {
|
||||
if queryText == "" || strings.Contains(strings.ToLower(id), queryText) || strings.Contains(strings.ToLower(unique[id].DisplayName), queryText) {
|
||||
ids = append(ids, id)
|
||||
queryText := normalizedSearchText(query.Query)
|
||||
contacts := make([]Contact, 0, len(unique))
|
||||
for _, contact := range unique {
|
||||
if contactMatchesQuery(contact, queryText) {
|
||||
contacts = append(contacts, contact)
|
||||
}
|
||||
}
|
||||
sort.Strings(ids)
|
||||
sort.Slice(contacts, func(i, j int) bool {
|
||||
leftRank := contactSearchRank(contacts[i], queryText)
|
||||
rightRank := contactSearchRank(contacts[j], queryText)
|
||||
if leftRank != rightRank {
|
||||
return leftRank < rightRank
|
||||
}
|
||||
leftID := strings.ToLower(contacts[i].ContactID)
|
||||
rightID := strings.ToLower(contacts[j].ContactID)
|
||||
if leftID != rightID {
|
||||
return leftID < rightID
|
||||
}
|
||||
return contacts[i].ContactID < contacts[j].ContactID
|
||||
})
|
||||
|
||||
limit := query.Limit
|
||||
if limit <= 0 || limit > len(ids) {
|
||||
limit = len(ids)
|
||||
if limit <= 0 || limit > len(contacts) {
|
||||
limit = len(contacts)
|
||||
}
|
||||
contacts := make([]Contact, 0, limit)
|
||||
for _, id := range ids[:limit] {
|
||||
contacts = append(contacts, unique[id])
|
||||
}
|
||||
return SearchContactsResult{Contacts: contacts}
|
||||
return SearchContactsResult{Contacts: contacts[:limit]}
|
||||
}
|
||||
|
||||
func addMessageContact(unique map[string]Contact, id string) {
|
||||
@@ -55,10 +63,11 @@ func addMessageContact(unique map[string]Contact, id string) {
|
||||
if trimmed == "" {
|
||||
return
|
||||
}
|
||||
if _, exists := unique[trimmed]; exists {
|
||||
key := strings.ToLower(trimmed)
|
||||
if _, exists := unique[key]; exists {
|
||||
return
|
||||
}
|
||||
unique[trimmed] = Contact{
|
||||
unique[key] = Contact{
|
||||
ContactID: trimmed,
|
||||
DisplayName: trimmed,
|
||||
Account: trimmed,
|
||||
@@ -67,3 +76,38 @@ func addMessageContact(unique map[string]Contact, id string) {
|
||||
RawRef: "message_jid",
|
||||
}
|
||||
}
|
||||
|
||||
func normalizedSearchText(value string) string {
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func contactMatchesQuery(contact Contact, queryText string) bool {
|
||||
if queryText == "" {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(strings.ToLower(contact.ContactID), queryText) ||
|
||||
strings.Contains(strings.ToLower(contact.DisplayName), queryText) ||
|
||||
strings.Contains(strings.ToLower(contact.Account), queryText)
|
||||
}
|
||||
|
||||
func contactSearchRank(contact Contact, queryText string) int {
|
||||
if queryText == "" {
|
||||
return 0
|
||||
}
|
||||
values := []string{
|
||||
strings.ToLower(contact.ContactID),
|
||||
strings.ToLower(contact.DisplayName),
|
||||
strings.ToLower(contact.Account),
|
||||
}
|
||||
for _, value := range values {
|
||||
if value == queryText {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(value, queryText) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
@@ -20,3 +20,19 @@ func TestSearchContactsFromMessagesMatchesBareJIDs(t *testing.T) {
|
||||
t.Fatalf("SearchContactsFromMessages() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContactsRanksExactAndDeduplicates(t *testing.T) {
|
||||
messages := []Message{
|
||||
{ID: "msg-1", SenderID: "zzz-target@imopenfire1-lanzhou", ReceiverID: "target@imopenfire1-lanzhou"},
|
||||
{ID: "msg-2", SenderID: "TARGET@imopenfire1-lanzhou", ReceiverID: "other@imopenfire1-lanzhou"},
|
||||
}
|
||||
|
||||
got := SearchContactsFromMessages(messages, SearchContactsQuery{Query: "target@imopenfire1-lanzhou", Limit: 10})
|
||||
want := SearchContactsResult{Contacts: []Contact{
|
||||
{ContactID: "target@imopenfire1-lanzhou", DisplayName: "target@imopenfire1-lanzhou", Account: "target@imopenfire1-lanzhou", Source: "local_readonly", Confidence: 0.6, RawRef: "message_jid"},
|
||||
{ContactID: "zzz-target@imopenfire1-lanzhou", DisplayName: "zzz-target@imopenfire1-lanzhou", Account: "zzz-target@imopenfire1-lanzhou", Source: "local_readonly", Confidence: 0.6, RawRef: "message_jid"},
|
||||
}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("SearchContactsFromMessages() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,24 +38,32 @@ func SearchGroupsFromMessages(messages []Message, query SearchGroupsQuery) Searc
|
||||
}
|
||||
}
|
||||
|
||||
queryText := strings.ToLower(strings.TrimSpace(query.Query))
|
||||
ids := make([]string, 0, len(unique))
|
||||
for id := range unique {
|
||||
if queryText == "" || strings.Contains(strings.ToLower(id), queryText) || strings.Contains(strings.ToLower(unique[id].DisplayName), queryText) {
|
||||
ids = append(ids, id)
|
||||
queryText := normalizedSearchText(query.Query)
|
||||
groups := make([]Group, 0, len(unique))
|
||||
for _, group := range unique {
|
||||
if groupMatchesQuery(group, queryText) {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
sort.Strings(ids)
|
||||
sort.Slice(groups, func(i, j int) bool {
|
||||
leftRank := groupSearchRank(groups[i], queryText)
|
||||
rightRank := groupSearchRank(groups[j], queryText)
|
||||
if leftRank != rightRank {
|
||||
return leftRank < rightRank
|
||||
}
|
||||
leftID := strings.ToLower(groups[i].GroupID)
|
||||
rightID := strings.ToLower(groups[j].GroupID)
|
||||
if leftID != rightID {
|
||||
return leftID < rightID
|
||||
}
|
||||
return groups[i].GroupID < groups[j].GroupID
|
||||
})
|
||||
|
||||
limit := query.Limit
|
||||
if limit <= 0 || limit > len(ids) {
|
||||
limit = len(ids)
|
||||
if limit <= 0 || limit > len(groups) {
|
||||
limit = len(groups)
|
||||
}
|
||||
groups := make([]Group, 0, limit)
|
||||
for _, id := range ids[:limit] {
|
||||
groups = append(groups, unique[id])
|
||||
}
|
||||
return SearchGroupsResult{Groups: groups}
|
||||
return SearchGroupsResult{Groups: groups[:limit]}
|
||||
}
|
||||
|
||||
func addMessageGroup(unique map[string]Group, id string) {
|
||||
@@ -63,10 +71,11 @@ func addMessageGroup(unique map[string]Group, id string) {
|
||||
if trimmed == "" || !isLikelyGroupJID(trimmed) {
|
||||
return
|
||||
}
|
||||
if _, exists := unique[trimmed]; exists {
|
||||
key := strings.ToLower(trimmed)
|
||||
if _, exists := unique[key]; exists {
|
||||
return
|
||||
}
|
||||
unique[trimmed] = Group{
|
||||
unique[key] = Group{
|
||||
GroupID: trimmed,
|
||||
DisplayName: trimmed,
|
||||
Source: "local_readonly",
|
||||
@@ -79,3 +88,32 @@ func isLikelyGroupJID(id string) bool {
|
||||
lower := strings.ToLower(strings.TrimSpace(id))
|
||||
return strings.Contains(lower, "conference") || strings.Contains(lower, "muc") || strings.Contains(lower, "group")
|
||||
}
|
||||
|
||||
func groupMatchesQuery(group Group, queryText string) bool {
|
||||
if queryText == "" {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(strings.ToLower(group.GroupID), queryText) ||
|
||||
strings.Contains(strings.ToLower(group.DisplayName), queryText)
|
||||
}
|
||||
|
||||
func groupSearchRank(group Group, queryText string) int {
|
||||
if queryText == "" {
|
||||
return 0
|
||||
}
|
||||
values := []string{
|
||||
strings.ToLower(group.GroupID),
|
||||
strings.ToLower(group.DisplayName),
|
||||
}
|
||||
for _, value := range values {
|
||||
if value == queryText {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
for _, value := range values {
|
||||
if strings.HasPrefix(value, queryText) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
@@ -20,3 +20,20 @@ func TestSearchGroupsFromMessagesMatchesGroupchatJIDs(t *testing.T) {
|
||||
t.Fatalf("SearchGroupsFromMessages() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchGroupsRanksExactAndDeduplicates(t *testing.T) {
|
||||
messages := []Message{
|
||||
{ID: "msg-1", ConversationType: "groupchat", SenderID: "zzz-project@conference.imopenfire1-lanzhou", ReceiverID: "member@imopenfire1-lanzhou"},
|
||||
{ID: "msg-2", ConversationType: "groupchat", SenderID: "project@conference.imopenfire1-lanzhou", ReceiverID: "member@imopenfire1-lanzhou"},
|
||||
{ID: "msg-3", ConversationType: "groupchat", SenderID: "PROJECT@conference.imopenfire1-lanzhou", ReceiverID: "member@imopenfire1-lanzhou"},
|
||||
}
|
||||
|
||||
got := SearchGroupsFromMessages(messages, SearchGroupsQuery{Query: "project@conference.imopenfire1-lanzhou", Limit: 10})
|
||||
want := SearchGroupsResult{Groups: []Group{
|
||||
{GroupID: "project@conference.imopenfire1-lanzhou", DisplayName: "project@conference.imopenfire1-lanzhou", Source: "local_readonly", Confidence: 0.6, RawRef: "groupchat_jid"},
|
||||
{GroupID: "zzz-project@conference.imopenfire1-lanzhou", DisplayName: "zzz-project@conference.imopenfire1-lanzhou", Source: "local_readonly", Confidence: 0.6, RawRef: "groupchat_jid"},
|
||||
}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("SearchGroupsFromMessages() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user