mirror of
https://github.com/mattermost/mattermost.git
synced 2026-10-30 22:55:29 +08:00
MM-45293 Index all attachment content fields in Elasticsearch (#35454)
* MM-45293 Index all Slack attachment content fields in Elasticsearch Previously only the "text" field from Slack attachments was indexed. This adds title, pretext, fallback, and field titles/values, making posts from integrations (JIRA, GitHub, CI bots) fully searchable.
This commit is contained in:
@@ -123,9 +123,34 @@ func ESPostFromPostForIndexing(post *model.PostForIndexing) *ESPost {
|
||||
attachmentsInterfaceArray, ok := attachments.([]any)
|
||||
if ok {
|
||||
for _, attachment := range attachmentsInterfaceArray {
|
||||
if attachment != nil {
|
||||
if attachmentText := attachment.(map[string]any)["text"]; attachmentText != nil {
|
||||
searchAttachments = append(searchAttachments, attachmentText.(string))
|
||||
if attachment == nil {
|
||||
continue
|
||||
}
|
||||
m, mOk := attachment.(map[string]any)
|
||||
if !mOk {
|
||||
continue
|
||||
}
|
||||
for _, key := range []string{"text", "title", "pretext", "fallback"} {
|
||||
if s, _ := m[key].(string); s != "" {
|
||||
searchAttachments = append(searchAttachments, s)
|
||||
}
|
||||
}
|
||||
if fields, fOk := m["fields"].([]any); fOk {
|
||||
for _, f := range fields {
|
||||
if fm, fmOk := f.(map[string]any); fmOk {
|
||||
if s, _ := fm["title"].(string); s != "" {
|
||||
searchAttachments = append(searchAttachments, s)
|
||||
}
|
||||
if v := fm["value"]; v != nil {
|
||||
if s, vOk := v.(string); vOk {
|
||||
if s != "" {
|
||||
searchAttachments = append(searchAttachments, s)
|
||||
}
|
||||
} else {
|
||||
searchAttachments = append(searchAttachments, fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,8 +159,30 @@ func ESPostFromPostForIndexing(post *model.PostForIndexing) *ESPost {
|
||||
attachmentsArray, ok := attachments.([]*model.SlackAttachment)
|
||||
if ok {
|
||||
for _, attachment := range attachmentsArray {
|
||||
if attachment != nil {
|
||||
searchAttachments = append(searchAttachments, attachment.Text)
|
||||
if attachment == nil {
|
||||
continue
|
||||
}
|
||||
for _, s := range []string{attachment.Text, attachment.Title, attachment.Pretext, attachment.Fallback} {
|
||||
if s != "" {
|
||||
searchAttachments = append(searchAttachments, s)
|
||||
}
|
||||
}
|
||||
for _, field := range attachment.Fields {
|
||||
if field == nil {
|
||||
continue
|
||||
}
|
||||
if field.Title != "" {
|
||||
searchAttachments = append(searchAttachments, field.Title)
|
||||
}
|
||||
if field.Value != nil {
|
||||
if s, ok := field.Value.(string); ok {
|
||||
if s != "" {
|
||||
searchAttachments = append(searchAttachments, s)
|
||||
}
|
||||
} else {
|
||||
searchAttachments = append(searchAttachments, fmt.Sprint(field.Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,75 +36,321 @@ func TestElasticsearchBuildPostIndexName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestESPostFromPostForIndexing(t *testing.T) {
|
||||
// Create one with attachments in 'any' form.
|
||||
|
||||
post1 := model.PostForIndexing{
|
||||
TeamId: model.NewId(),
|
||||
ParentCreateAt: nil,
|
||||
Post: model.Post{
|
||||
Id: model.NewId(),
|
||||
ChannelId: model.NewId(),
|
||||
UserId: model.NewId(),
|
||||
CreateAt: model.GetMillis(),
|
||||
Message: "message",
|
||||
Type: "",
|
||||
Hashtags: "",
|
||||
Props: map[string]any{
|
||||
"attachments": []any{
|
||||
map[string]any{
|
||||
"text": "text 1",
|
||||
t.Run("any form with text only", func(t *testing.T) {
|
||||
post := model.PostForIndexing{
|
||||
TeamId: model.NewId(),
|
||||
ParentCreateAt: nil,
|
||||
Post: model.Post{
|
||||
Id: model.NewId(),
|
||||
ChannelId: model.NewId(),
|
||||
UserId: model.NewId(),
|
||||
CreateAt: model.GetMillis(),
|
||||
Message: "message",
|
||||
Type: "",
|
||||
Hashtags: "",
|
||||
Props: map[string]any{
|
||||
model.PostPropsAttachments: []any{
|
||||
map[string]any{
|
||||
"text": "text 1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
espost1 := ESPostFromPostForIndexing(&post1)
|
||||
espost := ESPostFromPostForIndexing(&post)
|
||||
|
||||
assert.Equal(t, post1.Id, espost1.Id)
|
||||
assert.Equal(t, post1.TeamId, espost1.TeamId)
|
||||
assert.Equal(t, post1.ChannelId, espost1.ChannelId)
|
||||
assert.Equal(t, post1.UserId, espost1.UserId)
|
||||
assert.Equal(t, post1.CreateAt, espost1.CreateAt)
|
||||
assert.Equal(t, post1.Message, espost1.Message)
|
||||
assert.Equal(t, "default", espost1.Type)
|
||||
assert.Empty(t, espost1.Hashtags)
|
||||
assert.Equal(t, "text 1", espost1.Attachments)
|
||||
assert.Equal(t, post.Id, espost.Id)
|
||||
assert.Equal(t, post.TeamId, espost.TeamId)
|
||||
assert.Equal(t, post.ChannelId, espost.ChannelId)
|
||||
assert.Equal(t, post.UserId, espost.UserId)
|
||||
assert.Equal(t, post.CreateAt, espost.CreateAt)
|
||||
assert.Equal(t, post.Message, espost.Message)
|
||||
assert.Equal(t, "default", espost.Type)
|
||||
assert.Empty(t, espost.Hashtags)
|
||||
assert.Equal(t, "text 1", espost.Attachments)
|
||||
})
|
||||
|
||||
// Create one with attachments in model.SlackAttachment form.
|
||||
|
||||
post2 := model.PostForIndexing{
|
||||
TeamId: model.NewId(),
|
||||
ParentCreateAt: nil,
|
||||
Post: model.Post{
|
||||
Id: model.NewId(),
|
||||
ChannelId: model.NewId(),
|
||||
UserId: model.NewId(),
|
||||
CreateAt: model.GetMillis(),
|
||||
Message: "message",
|
||||
Type: "slack_attachment",
|
||||
Hashtags: "#buh #boh",
|
||||
Props: map[string]any{
|
||||
"attachments": []*model.SlackAttachment{
|
||||
{
|
||||
Text: "text 2",
|
||||
t.Run("SlackAttachment form with text only", func(t *testing.T) {
|
||||
post := model.PostForIndexing{
|
||||
TeamId: model.NewId(),
|
||||
ParentCreateAt: nil,
|
||||
Post: model.Post{
|
||||
Id: model.NewId(),
|
||||
ChannelId: model.NewId(),
|
||||
UserId: model.NewId(),
|
||||
CreateAt: model.GetMillis(),
|
||||
Message: "message",
|
||||
Type: "slack_attachment",
|
||||
Hashtags: "#buh #boh",
|
||||
Props: map[string]any{
|
||||
model.PostPropsAttachments: []*model.SlackAttachment{
|
||||
{
|
||||
Text: "text 2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
espost2 := ESPostFromPostForIndexing(&post2)
|
||||
espost := ESPostFromPostForIndexing(&post)
|
||||
|
||||
assert.Equal(t, post2.Id, espost2.Id)
|
||||
assert.Equal(t, post2.TeamId, espost2.TeamId)
|
||||
assert.Equal(t, post2.ChannelId, espost2.ChannelId)
|
||||
assert.Equal(t, post2.UserId, espost2.UserId)
|
||||
assert.Equal(t, post2.CreateAt, espost2.CreateAt)
|
||||
assert.Equal(t, post2.Message, espost2.Message)
|
||||
assert.Equal(t, "slack_attachment", espost2.Type)
|
||||
assert.Len(t, espost2.Hashtags, 2)
|
||||
assert.Equal(t, "text 2", espost2.Attachments)
|
||||
assert.Equal(t, post.Id, espost.Id)
|
||||
assert.Equal(t, post.TeamId, espost.TeamId)
|
||||
assert.Equal(t, post.ChannelId, espost.ChannelId)
|
||||
assert.Equal(t, post.UserId, espost.UserId)
|
||||
assert.Equal(t, post.CreateAt, espost.CreateAt)
|
||||
assert.Equal(t, post.Message, espost.Message)
|
||||
assert.Equal(t, "slack_attachment", espost.Type)
|
||||
assert.Len(t, espost.Hashtags, 2)
|
||||
assert.Equal(t, "text 2", espost.Attachments)
|
||||
})
|
||||
|
||||
t.Run("any form indexes title, pretext, fallback, and fields", 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{
|
||||
"title": "Build Failed",
|
||||
"pretext": "CI notification",
|
||||
"fallback": "Build Failed on main",
|
||||
"text": "Details here",
|
||||
"fields": []any{
|
||||
map[string]any{
|
||||
"title": "Branch",
|
||||
"value": "main",
|
||||
},
|
||||
map[string]any{
|
||||
"title": "Status",
|
||||
"value": "failed",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
espost := ESPostFromPostForIndexing(&post)
|
||||
|
||||
assert.Contains(t, espost.Attachments, "Details here")
|
||||
assert.Contains(t, espost.Attachments, "Build Failed")
|
||||
assert.Contains(t, espost.Attachments, "CI notification")
|
||||
assert.Contains(t, espost.Attachments, "Build Failed on main")
|
||||
assert.Contains(t, espost.Attachments, "Branch")
|
||||
assert.Contains(t, espost.Attachments, "main")
|
||||
assert.Contains(t, espost.Attachments, "Status")
|
||||
assert.Contains(t, espost.Attachments, "failed")
|
||||
})
|
||||
|
||||
t.Run("SlackAttachment form indexes title, pretext, fallback, and fields", 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.SlackAttachment{
|
||||
{
|
||||
Title: "Build Failed",
|
||||
Pretext: "CI notification",
|
||||
Fallback: "Build Failed on main",
|
||||
Text: "Details here",
|
||||
Fields: []*model.SlackAttachmentField{
|
||||
{Title: "Branch", Value: "main"},
|
||||
{Title: "Status", Value: "failed"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
espost := ESPostFromPostForIndexing(&post)
|
||||
|
||||
assert.Contains(t, espost.Attachments, "Details here")
|
||||
assert.Contains(t, espost.Attachments, "Build Failed")
|
||||
assert.Contains(t, espost.Attachments, "CI notification")
|
||||
assert.Contains(t, espost.Attachments, "Build Failed on main")
|
||||
assert.Contains(t, espost.Attachments, "Branch")
|
||||
assert.Contains(t, espost.Attachments, "main")
|
||||
assert.Contains(t, espost.Attachments, "Status")
|
||||
assert.Contains(t, espost.Attachments, "failed")
|
||||
})
|
||||
|
||||
t.Run("empty fields are excluded", 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.SlackAttachment{
|
||||
{
|
||||
Title: "Only Title",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
espost := ESPostFromPostForIndexing(&post)
|
||||
|
||||
assert.Equal(t, "Only Title", espost.Attachments)
|
||||
})
|
||||
|
||||
t.Run("nil fields and attachments are handled", 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.SlackAttachment{
|
||||
nil,
|
||||
{
|
||||
Text: "valid",
|
||||
Fields: []*model.SlackAttachmentField{
|
||||
nil,
|
||||
{Title: "field title", Value: "field value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
espost := ESPostFromPostForIndexing(&post)
|
||||
|
||||
assert.Contains(t, espost.Attachments, "valid")
|
||||
assert.Contains(t, espost.Attachments, "field title")
|
||||
assert.Contains(t, espost.Attachments, "field value")
|
||||
})
|
||||
|
||||
t.Run("non-string field values are indexed", 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": "metrics",
|
||||
"fields": []any{
|
||||
map[string]any{
|
||||
"title": "Count",
|
||||
"value": 42,
|
||||
},
|
||||
map[string]any{
|
||||
"title": "Rate",
|
||||
"value": 99.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
espost := ESPostFromPostForIndexing(&post)
|
||||
|
||||
assert.Contains(t, espost.Attachments, "metrics")
|
||||
assert.Contains(t, espost.Attachments, "Count")
|
||||
assert.Contains(t, espost.Attachments, "42")
|
||||
assert.Contains(t, espost.Attachments, "Rate")
|
||||
assert.Contains(t, espost.Attachments, "99.5")
|
||||
})
|
||||
|
||||
t.Run("non-string field values in SlackAttachment form are indexed", 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.SlackAttachment{
|
||||
{
|
||||
Text: "metrics",
|
||||
Fields: []*model.SlackAttachmentField{
|
||||
{Title: "Count", Value: 42},
|
||||
{Title: "Rate", Value: 99.5},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
espost := ESPostFromPostForIndexing(&post)
|
||||
|
||||
assert.Contains(t, espost.Attachments, "metrics")
|
||||
assert.Contains(t, espost.Attachments, "Count")
|
||||
assert.Contains(t, espost.Attachments, "42")
|
||||
assert.Contains(t, espost.Attachments, "Rate")
|
||||
assert.Contains(t, espost.Attachments, "99.5")
|
||||
})
|
||||
|
||||
t.Run("multiple attachments are combined", 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{
|
||||
"title": "First",
|
||||
"text": "one",
|
||||
},
|
||||
map[string]any{
|
||||
"title": "Second",
|
||||
"text": "two",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
espost := ESPostFromPostForIndexing(&post)
|
||||
|
||||
assert.Contains(t, espost.Attachments, "First")
|
||||
assert.Contains(t, espost.Attachments, "one")
|
||||
assert.Contains(t, espost.Attachments, "Second")
|
||||
assert.Contains(t, espost.Attachments, "two")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMatchesForHit(t *testing.T) {
|
||||
|
||||
+4
-1
@@ -272,7 +272,7 @@ export default class MessageAttachment extends React.PureComponent<Props, State>
|
||||
let rowPos = 0;
|
||||
let lastWasLong = false;
|
||||
let nrTables = 0;
|
||||
const markdown = {markdown: false, mentionHighlight: false, atMentions: false};
|
||||
const markdown = {...this.props.options, markdown: false, mentionHighlight: false, atMentions: false};
|
||||
|
||||
fields.forEach((field, i) => {
|
||||
if (rowPos === 2 || !(field.short === true) || lastWasLong) {
|
||||
@@ -319,6 +319,7 @@ export default class MessageAttachment extends React.PureComponent<Props, State>
|
||||
>
|
||||
<Markdown
|
||||
message={String(field.value)}
|
||||
options={this.props.options}
|
||||
postId={this.props.postId}
|
||||
/>
|
||||
</td>,
|
||||
@@ -391,6 +392,7 @@ export default class MessageAttachment extends React.PureComponent<Props, State>
|
||||
<div className='attachment__thumb-pretext'>
|
||||
<Markdown
|
||||
message={attachment.pretext}
|
||||
options={options}
|
||||
postId={this.props.postId}
|
||||
/>
|
||||
</div>
|
||||
@@ -461,6 +463,7 @@ export default class MessageAttachment extends React.PureComponent<Props, State>
|
||||
<Markdown
|
||||
message={attachment.title}
|
||||
options={{
|
||||
...options,
|
||||
atMentions: false,
|
||||
mentionHighlight: false,
|
||||
renderer: new LinkOnlyRenderer(),
|
||||
|
||||
Reference in New Issue
Block a user