ES search: include footer and author name in indexed fields. (#35603)

* Include footer and author name in indexed fields.
This commit is contained in:
Doug Lauder
2026-03-16 15:35:02 -04:00
committed by GitHub
parent 1db8697315
commit 3a2cc5e242
2 changed files with 176 additions and 2 deletions
@@ -133,7 +133,7 @@ func ESPostFromPostForIndexing(post *model.PostForIndexing) *ESPost {
if !mOk {
continue
}
for _, key := range []string{"text", "title", "pretext", "fallback"} {
for _, key := range []string{"text", "title", "pretext", "fallback", "footer", "author_name"} {
if s, _ := m[key].(string); s != "" {
searchAttachments = append(searchAttachments, s)
}
@@ -165,7 +165,7 @@ func ESPostFromPostForIndexing(post *model.PostForIndexing) *ESPost {
if attachment == nil {
continue
}
for _, s := range []string{attachment.Text, attachment.Title, attachment.Pretext, attachment.Fallback} {
for _, s := range []string{attachment.Text, attachment.Title, attachment.Pretext, attachment.Fallback, attachment.Footer, attachment.AuthorName} {
if s != "" {
searchAttachments = append(searchAttachments, s)
}
@@ -4,6 +4,7 @@
package common
import (
"encoding/json"
"testing"
"time"
@@ -319,6 +320,64 @@ func TestESPostFromPostForIndexing(t *testing.T) {
assert.Contains(t, espost.Attachments, "99.5")
})
t.Run("any form indexes footer and author_name", func(t *testing.T) {
post := model.PostForIndexing{
TeamId: model.NewId(),
Post: model.Post{
Id: model.NewId(),
ChannelId: model.NewId(),
UserId: model.NewId(),
CreateAt: model.GetMillis(),
Message: "",
Type: "slack_attachment",
Props: map[string]any{
model.PostPropsAttachments: []any{
map[string]any{
"text": "body text",
"footer": "Opportunity #OPP-000035341 • United States",
"author_name": "Salesforce",
},
},
},
},
}
espost := ESPostFromPostForIndexing(&post)
assert.Contains(t, espost.Attachments, "body text")
assert.Contains(t, espost.Attachments, "Opportunity #OPP-000035341 • United States")
assert.Contains(t, espost.Attachments, "Salesforce")
})
t.Run("MessageAttachment form indexes footer and author_name", func(t *testing.T) {
post := model.PostForIndexing{
TeamId: model.NewId(),
Post: model.Post{
Id: model.NewId(),
ChannelId: model.NewId(),
UserId: model.NewId(),
CreateAt: model.GetMillis(),
Message: "",
Type: "slack_attachment",
Props: map[string]any{
model.PostPropsAttachments: []*model.MessageAttachment{
{
Text: "body text",
Footer: "Opportunity #OPP-000035341 • United States",
AuthorName: "Salesforce",
},
},
},
},
}
espost := ESPostFromPostForIndexing(&post)
assert.Contains(t, espost.Attachments, "body text")
assert.Contains(t, espost.Attachments, "Opportunity #OPP-000035341 • United States")
assert.Contains(t, espost.Attachments, "Salesforce")
})
t.Run("multiple attachments are combined", func(t *testing.T) {
post := model.PostForIndexing{
TeamId: model.NewId(),
@@ -353,6 +412,121 @@ func TestESPostFromPostForIndexing(t *testing.T) {
})
}
// TestESPostFromPost_CreatePostJSONRoundTrip simulates the exact flow that happens
// in production: App.CreatePost converts []*MessageAttachment to []any via JSON
// marshal/unmarshal (Post.Attachments in post.go), then the search layer calls
// ESPostFromPost which does ShallowCopy → ESPostFromPostForIndexing.
func TestESPostFromPost_CreatePostJSONRoundTrip(t *testing.T) {
// Step 1: Start with typed MessageAttachment (as a plugin/webhook would create)
original := &model.Post{
Id: model.NewId(),
ChannelId: model.NewId(),
UserId: model.NewId(),
CreateAt: model.GetMillis(),
Message: "#closedwon #renewal",
Type: "slack_attachment",
}
original.AddProp(model.PostPropsAttachments, []*model.MessageAttachment{
{
Title: "Account: Acme Corp / Widget Industries, LLC",
Text: "Renewal ARR: $49,140.00",
Pretext: "#closedwon #renewal",
Footer: "Opportunity #OPP-000035341 • United States",
AuthorName: "CRM Bot",
Fields: []*model.MessageAttachmentField{
{Title: "Sales Rep", Value: "Jane Smith"},
{Title: "Account Manager", Value: "John Doe"},
{Title: "Opportunity", Value: "Acme Corp / Widget Industries, LLC - Enterprise - 350 Seats - '26 Renewal"},
{Title: "Seats", Value: 350.0},
},
},
})
// Step 2: Simulate CreatePost JSON round-trip (post.go:305-315)
if attachments, ok := original.GetProp(model.PostPropsAttachments).([]*model.MessageAttachment); ok {
jsonAttachments, err := json.Marshal(attachments)
require.NoError(t, err)
attachmentsInterface := []any{}
err = json.Unmarshal(jsonAttachments, &attachmentsInterface)
require.NoError(t, err)
original.AddProp(model.PostPropsAttachments, attachmentsInterface)
} else {
require.Fail(t, "expected []*MessageAttachment type assertion to succeed")
}
// Step 3: Simulate ESPostFromPost (what the search layer calls)
teamId := model.NewId()
esPost, err := ESPostFromPost(original, teamId, "O")
require.NoError(t, err)
// Step 4: Verify all attachment fields are indexed
assert.Contains(t, esPost.Attachments, "Account: Acme Corp / Widget Industries, LLC", "title should be indexed")
assert.Contains(t, esPost.Attachments, "Renewal ARR: $49,140.00", "text should be indexed")
assert.Contains(t, esPost.Attachments, "#closedwon #renewal", "pretext should be indexed")
// Field titles and values
assert.Contains(t, esPost.Attachments, "Sales Rep", "field title should be indexed")
assert.Contains(t, esPost.Attachments, "Jane Smith", "field value should be indexed")
assert.Contains(t, esPost.Attachments, "Account Manager", "field title should be indexed")
assert.Contains(t, esPost.Attachments, "John Doe", "field value should be indexed")
assert.Contains(t, esPost.Attachments, "Opportunity", "field title should be indexed")
assert.Contains(t, esPost.Attachments, "Acme Corp / Widget Industries, LLC - Enterprise - 350 Seats - '26 Renewal", "field value should be indexed")
assert.Contains(t, esPost.Attachments, "350", "numeric field value should be indexed")
// Footer and AuthorName
assert.Contains(t, esPost.Attachments, "Opportunity #OPP-000035341 • United States", "footer should be indexed")
assert.Contains(t, esPost.Attachments, "CRM Bot", "author_name should be indexed")
t.Logf("Attachments field content: %s", esPost.Attachments)
}
// TestESPostFromPost_APIJSONUnmarshal simulates a post created via the REST API
// where Props are deserialized from JSON (attachments arrive as []any directly).
func TestESPostFromPost_APIJSONUnmarshal(t *testing.T) {
// Simulate what happens when a bot POSTs JSON to /api/v4/posts
postJSON := `{
"channel_id": "` + model.NewId() + `",
"message": "",
"props": {
"attachments": [{
"title": "Account: Acme Corp",
"text": "Renewal ARR: $49,140.00",
"footer": "Opportunity #OPP-000035341",
"author_name": "CRM Bot",
"fields": [
{"title": "Sales Rep", "value": "Jane Smith"},
{"title": "Seats", "value": 350}
]
}]
}
}`
var post model.Post
err := json.Unmarshal([]byte(postJSON), &post)
require.NoError(t, err)
post.Id = model.NewId()
post.UserId = model.NewId()
post.CreateAt = model.GetMillis()
// The CreatePost conversion at post.go:305-315 would NOT trigger here
// because the type is already []any, not []*MessageAttachment
_, typeOk := post.GetProp(model.PostPropsAttachments).([]*model.MessageAttachment)
assert.False(t, typeOk, "API-created post should have []any, not []*MessageAttachment")
esPost, err := ESPostFromPost(&post, model.NewId(), "O")
require.NoError(t, err)
assert.Contains(t, esPost.Attachments, "Account: Acme Corp", "title should be indexed")
assert.Contains(t, esPost.Attachments, "Renewal ARR: $49,140.00", "text should be indexed")
assert.Contains(t, esPost.Attachments, "Sales Rep", "field title should be indexed")
assert.Contains(t, esPost.Attachments, "Jane Smith", "field value should be indexed")
assert.Contains(t, esPost.Attachments, "350", "numeric field value should be indexed")
assert.Contains(t, esPost.Attachments, "Opportunity #OPP-000035341", "footer should be indexed")
assert.Contains(t, esPost.Attachments, "CRM Bot", "author_name should be indexed")
t.Logf("Attachments field content: %s", esPost.Attachments)
}
func TestGetMatchesForHit(t *testing.T) {
snippets := map[string][]string{
"message": {