mirror of
https://github.com/hanzoai/team.git
synced 2026-11-01 07:55:31 +00:00
* Support Elasticsearch v9 alongside v8 * Add CI workflow changes for Elasticsearch v8/v9 testing * Support Elasticsearch v7 in addition to v8/v9, add v7 CI test job Lowers the minimum supported ES version from 8 to 7 to avoid dropping v7 support in a dot release. Adds a dedicated CI job to verify v7 compatibility alongside the existing v8 and v9 (default) jobs. * Fix ES7 plugin install crash on cgroup v2 hosts * Fix ES 7 container startup on cgroup v2 Linux (GitHub Actions) ES 7 bundles JDK 11, which crashes with a NullPointerException in CgroupV2Subsystem.getMountPoint() on modern Linux kernels that use cgroup v2 (including GitHub Actions ubuntu-latest runners). The flag was already set during the Dockerfile RUN step, but not at runtime. Adding -XX:-UseContainerSupport to ES_JAVA_OPTS in docker-compose fixes the crash. The flag is harmless on ES 8/9 which ship JDK 17+ where the cgroup v2 bug is fixed (it simply opts out of container-aware JVM sizing). * Capture docker compose logs in CI test artifact * Use ES 7.17.29 for v7 CI test; remove cgroup v2 workarounds ES 7.17.0 bundled JDK 17.0.1 which had a cgroup v2 bug (CgroupV2Subsystem NPE) not fixable via -XX:-UseContainerSupport. ES 7.17.29 bundles JDK 22 where the bug is long fixed. Reverts the -XX:-UseContainerSupport workarounds added in the previous two commits as they were based on a wrong diagnosis and are no longer needed. --------- Co-authored-by: Mattermost Build <[email protected]>
1853 lines
64 KiB
Go
1853 lines
64 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.enterprise for license information.
|
|
|
|
package elasticsearch
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
"github.com/mattermost/mattermost/server/public/shared/request"
|
|
"github.com/mattermost/mattermost/server/v8/channels/app/platform"
|
|
"github.com/mattermost/mattermost/server/v8/enterprise/elasticsearch/common"
|
|
"github.com/mattermost/mattermost/server/v8/platform/services/searchengine"
|
|
|
|
elastic "github.com/elastic/go-elasticsearch/v8"
|
|
"github.com/elastic/go-elasticsearch/v8/typedapi/core/deletebyquery"
|
|
"github.com/elastic/go-elasticsearch/v8/typedapi/core/search"
|
|
"github.com/elastic/go-elasticsearch/v8/typedapi/types"
|
|
"github.com/elastic/go-elasticsearch/v8/typedapi/types/enums/highlighterencoder"
|
|
"github.com/elastic/go-elasticsearch/v8/typedapi/types/enums/operator"
|
|
"github.com/elastic/go-elasticsearch/v8/typedapi/types/enums/sortorder"
|
|
)
|
|
|
|
const elasticsearchMinVersion = 7
|
|
const elasticsearchMaxVersion = 9
|
|
|
|
var (
|
|
purgeIndexListAllowedIndexes = []string{common.IndexBaseChannels}
|
|
)
|
|
|
|
type ElasticsearchInterfaceImpl struct {
|
|
client *elastic.TypedClient
|
|
mutex sync.RWMutex
|
|
ready atomic.Int32
|
|
version int
|
|
fullVersion string
|
|
plugins []string
|
|
|
|
bulkProcessor *Bulk
|
|
Platform *platform.PlatformService
|
|
}
|
|
|
|
func getJSONOrErrorStr(obj any) string {
|
|
b, err := json.Marshal(obj)
|
|
if err != nil {
|
|
return err.Error()
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func (*ElasticsearchInterfaceImpl) UpdateConfig(cfg *model.Config) {
|
|
// Not needed, it use the `Server` stored internally to get always the last version
|
|
}
|
|
|
|
func (*ElasticsearchInterfaceImpl) GetName() string {
|
|
return "elasticsearch"
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) IsEnabled() bool {
|
|
return *es.Platform.Config().ElasticsearchSettings.EnableIndexing
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) IsActive() bool {
|
|
return *es.Platform.Config().ElasticsearchSettings.EnableIndexing && es.ready.Load() == 1
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) IsIndexingEnabled() bool {
|
|
return *es.Platform.Config().ElasticsearchSettings.EnableIndexing
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) IsSearchEnabled() bool {
|
|
return *es.Platform.Config().ElasticsearchSettings.EnableSearching
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) IsAutocompletionEnabled() bool {
|
|
return *es.Platform.Config().ElasticsearchSettings.EnableAutocomplete
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) IsIndexingSync() bool {
|
|
return *es.Platform.Config().ElasticsearchSettings.LiveIndexingBatchSize <= 1
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) Start() *model.AppError {
|
|
if license := es.Platform.License(); license == nil || !*license.Features.Elasticsearch || !*es.Platform.Config().ElasticsearchSettings.EnableIndexing {
|
|
return nil
|
|
}
|
|
|
|
es.mutex.Lock()
|
|
defer es.mutex.Unlock()
|
|
|
|
if es.ready.Load() != 0 {
|
|
// Elasticsearch is already started. We don't return an error
|
|
// because "Test Connection" already re-initializes the client. So this
|
|
// can be a valid scenario.
|
|
return nil
|
|
}
|
|
|
|
var appErr *model.AppError
|
|
if es.client, appErr = createTypedClient(es.Platform.Log(), es.Platform.Config(), es.Platform.FileBackend(), true); appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
version, major, appErr := checkVersion(es.client, es.Platform.Config())
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
// Since we are only retrieving plugins for the Support Packet generation, it doesn't make sense to kill the process if we get an error
|
|
// Instead, we will log it and move forward
|
|
resp, err := es.client.API.Cat.Plugins().Do(context.Background())
|
|
if err != nil {
|
|
es.Platform.Log().Warn("Error retrieving elasticsearch plugins", mlog.Err(err))
|
|
} else {
|
|
for _, p := range resp {
|
|
es.plugins = append(es.plugins, *p.Component)
|
|
}
|
|
}
|
|
|
|
es.version = major
|
|
es.fullVersion = version
|
|
|
|
ctx := context.Background()
|
|
|
|
if *es.Platform.Config().ElasticsearchSettings.LiveIndexingBatchSize > 1 {
|
|
es.bulkProcessor = NewBulk(es.Platform.Config().ElasticsearchSettings,
|
|
es.Platform.Log(),
|
|
es.client)
|
|
}
|
|
|
|
// Set up posts index template.
|
|
_, err = es.client.API.Indices.PutIndexTemplate(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBasePosts).
|
|
Request(common.GetPostTemplate(es.Platform.Config())).
|
|
Do(ctx)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.start", "ent.elasticsearch.create_template_posts_if_not_exists.template_create_failed", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
// Set up channels index template.
|
|
_, err = es.client.API.Indices.PutIndexTemplate(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseChannels).
|
|
Request(common.GetChannelTemplate(es.Platform.Config())).
|
|
Do(ctx)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.start", "ent.elasticsearch.create_template_channels_if_not_exists.template_create_failed", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
// Set up users index template.
|
|
_, err = es.client.API.Indices.PutIndexTemplate(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseUsers).
|
|
Request(common.GetUserTemplate(es.Platform.Config())).
|
|
Do(ctx)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.start", "ent.elasticsearch.create_template_users_if_not_exists.template_create_failed", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
// Set up files index template.
|
|
_, err = es.client.API.Indices.PutIndexTemplate(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseFiles).
|
|
Request(common.GetFileInfoTemplate(es.Platform.Config())).
|
|
Do(ctx)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.start", "ent.elasticsearch.create_template_file_info_if_not_exists.template_create_failed", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
es.ready.Store(1)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) Stop() *model.AppError {
|
|
es.mutex.Lock()
|
|
defer es.mutex.Unlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.start", "ent.elasticsearch.stop.already_stopped.app_error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
es.client = nil
|
|
// Flushing any pending requests
|
|
if es.bulkProcessor != nil {
|
|
if err := es.bulkProcessor.Stop(); err != nil {
|
|
es.Platform.Log().Warn("Error stopping bulk processor", mlog.Err(err))
|
|
}
|
|
es.bulkProcessor = nil
|
|
}
|
|
|
|
es.ready.Store(0)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) GetVersion() int {
|
|
return es.version
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) GetFullVersion() string {
|
|
return es.fullVersion
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) GetPlugins() []string {
|
|
return es.plugins
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) IndexPost(post *model.Post, teamId string) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.IndexPost", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
indexName := common.BuildPostIndexName(*es.Platform.Config().ElasticsearchSettings.AggregatePostsAfterDays,
|
|
*es.Platform.Config().ElasticsearchSettings.IndexPrefix+common.IndexBasePosts, *es.Platform.Config().ElasticsearchSettings.IndexPrefix+common.IndexBasePosts_MONTH, time.Now(), post.CreateAt)
|
|
|
|
searchPost, err := common.ESPostFromPost(post, teamId)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.IndexPost", "ent.elasticsearch.index_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if es.bulkProcessor != nil {
|
|
err = es.bulkProcessor.IndexOp(types.IndexOperation{
|
|
Index_: model.NewPointer(indexName),
|
|
Id_: model.NewPointer(searchPost.Id),
|
|
}, searchPost)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.IndexPost", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
} else {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
_, err = es.client.Index(indexName).
|
|
Id(post.Id).
|
|
Document(searchPost).
|
|
Do(ctx)
|
|
}
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.IndexPost", "ent.elasticsearch.index_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
metrics := es.Platform.Metrics()
|
|
if metrics != nil {
|
|
metrics.IncrementPostIndexCounter()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) getPostIndexNames() ([]string, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
indexes, err := es.client.API.Indices.Get("_all").Do(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
postIndexes := make([]string, 0)
|
|
for name := range indexes {
|
|
if strings.HasPrefix(name, *es.Platform.Config().ElasticsearchSettings.IndexPrefix+common.IndexBasePosts) {
|
|
postIndexes = append(postIndexes, name)
|
|
}
|
|
}
|
|
return postIndexes, nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) SearchPosts(channels model.ChannelList, searchParams []*model.SearchParams, page, perPage int) ([]string, model.PostSearchMatches, *model.AppError) {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return []string{}, nil, model.NewAppError("Elasticsearch.SearchPosts", "ent.elasticsearch.search_posts.disabled", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
var channelIds []string
|
|
for _, channel := range channels {
|
|
channelIds = append(channelIds, channel.Id)
|
|
}
|
|
|
|
var termQueries, notTermQueries, highlightQueries []types.Query
|
|
var filters, notFilters []types.Query
|
|
for i, params := range searchParams {
|
|
newTerms := []string{}
|
|
for _, term := range strings.Split(params.Terms, " ") {
|
|
if searchengine.EmailRegex.MatchString(term) {
|
|
term = `"` + term + `"`
|
|
}
|
|
newTerms = append(newTerms, term)
|
|
}
|
|
|
|
params.Terms = strings.Join(newTerms, " ")
|
|
|
|
termOperator := operator.And
|
|
if searchParams[0].OrTerms {
|
|
termOperator = operator.Or
|
|
}
|
|
|
|
// Date, channels and FromUsers filters come in all
|
|
// searchParams iteration, and as they are global to the
|
|
// query, we only need to process them once
|
|
if i == 0 {
|
|
if len(params.InChannels) > 0 {
|
|
filters = append(filters, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{"channel_id": params.InChannels}},
|
|
})
|
|
}
|
|
|
|
if len(params.ExcludedChannels) > 0 {
|
|
notFilters = append(notFilters, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{"channel_id": params.ExcludedChannels}},
|
|
})
|
|
}
|
|
|
|
if len(params.FromUsers) > 0 {
|
|
filters = append(filters, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{"user_id": params.FromUsers}},
|
|
})
|
|
}
|
|
|
|
if len(params.ExcludedUsers) > 0 {
|
|
notFilters = append(notFilters, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{"user_id": params.ExcludedUsers}},
|
|
})
|
|
}
|
|
|
|
if params.OnDate != "" {
|
|
before, after := params.GetOnDateMillis()
|
|
filters = append(filters, types.Query{
|
|
Range: map[string]types.RangeQuery{
|
|
"create_at": types.NumberRangeQuery{
|
|
Gte: model.NewPointer(types.Float64(before)),
|
|
Lte: model.NewPointer(types.Float64(after)),
|
|
},
|
|
},
|
|
})
|
|
} else {
|
|
if params.AfterDate != "" || params.BeforeDate != "" {
|
|
nrQuery := types.NumberRangeQuery{}
|
|
if params.AfterDate != "" {
|
|
nrQuery.Gte = model.NewPointer(types.Float64(params.GetAfterDateMillis()))
|
|
}
|
|
|
|
if params.BeforeDate != "" {
|
|
nrQuery.Lte = model.NewPointer(types.Float64(params.GetBeforeDateMillis()))
|
|
}
|
|
|
|
query := types.Query{
|
|
Range: map[string]types.RangeQuery{
|
|
"create_at": nrQuery,
|
|
},
|
|
}
|
|
filters = append(filters, query)
|
|
}
|
|
|
|
if params.ExcludedAfterDate != "" || params.ExcludedBeforeDate != "" || params.ExcludedDate != "" {
|
|
if params.ExcludedDate != "" {
|
|
before, after := params.GetExcludedDateMillis()
|
|
notFilters = append(notFilters, types.Query{
|
|
Range: map[string]types.RangeQuery{
|
|
"create_at": types.NumberRangeQuery{
|
|
Gte: model.NewPointer(types.Float64(before)),
|
|
Lte: model.NewPointer(types.Float64(after)),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if params.ExcludedAfterDate != "" {
|
|
notFilters = append(notFilters, types.Query{
|
|
Range: map[string]types.RangeQuery{
|
|
"create_at": types.NumberRangeQuery{
|
|
Gte: model.NewPointer(types.Float64(params.GetExcludedAfterDateMillis())),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if params.ExcludedBeforeDate != "" {
|
|
notFilters = append(notFilters, types.Query{
|
|
Range: map[string]types.RangeQuery{
|
|
"create_at": types.NumberRangeQuery{
|
|
Lte: model.NewPointer(types.Float64(params.GetExcludedBeforeDateMillis())),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if params.IsHashtag {
|
|
if params.Terms != "" {
|
|
query := types.Query{
|
|
SimpleQueryString: &types.SimpleQueryStringQuery{
|
|
Query: params.Terms,
|
|
Fields: []string{"hashtags"},
|
|
DefaultOperator: &termOperator,
|
|
},
|
|
}
|
|
termQueries = append(termQueries, query)
|
|
highlightQueries = append(highlightQueries, query)
|
|
} else if params.ExcludedTerms != "" {
|
|
query := types.Query{
|
|
SimpleQueryString: &types.SimpleQueryStringQuery{
|
|
Query: params.ExcludedTerms,
|
|
Fields: []string{"hashtags"},
|
|
DefaultOperator: &termOperator,
|
|
},
|
|
}
|
|
notTermQueries = append(notTermQueries, query)
|
|
}
|
|
} else {
|
|
if params.Terms != "" {
|
|
elements := []types.Query{
|
|
{
|
|
SimpleQueryString: &types.SimpleQueryStringQuery{
|
|
Query: params.Terms,
|
|
Fields: []string{"message"},
|
|
DefaultOperator: &termOperator,
|
|
},
|
|
}, {
|
|
SimpleQueryString: &types.SimpleQueryStringQuery{
|
|
Query: params.Terms,
|
|
Fields: []string{"attachments"},
|
|
DefaultOperator: &termOperator,
|
|
},
|
|
}, {
|
|
Term: map[string]types.TermQuery{
|
|
"urls": {Value: params.Terms},
|
|
},
|
|
},
|
|
}
|
|
query := types.Query{
|
|
Bool: &types.BoolQuery{Should: append([]types.Query(nil), elements...)},
|
|
}
|
|
|
|
termQueries = append(termQueries, query)
|
|
|
|
hashtagTerms := []string{}
|
|
for _, term := range strings.Split(params.Terms, " ") {
|
|
hashtagTerms = append(hashtagTerms, "#"+term)
|
|
}
|
|
|
|
hashtagQuery := types.Query{
|
|
SimpleQueryString: &types.SimpleQueryStringQuery{
|
|
Query: strings.Join(hashtagTerms, " "),
|
|
Fields: []string{"hashtags"},
|
|
DefaultOperator: &termOperator,
|
|
},
|
|
}
|
|
highlightQuery := types.Query{
|
|
Bool: &types.BoolQuery{Should: append(elements, hashtagQuery)},
|
|
}
|
|
|
|
highlightQueries = append(highlightQueries, highlightQuery)
|
|
}
|
|
|
|
if params.ExcludedTerms != "" {
|
|
query := types.Query{
|
|
Bool: &types.BoolQuery{Should: []types.Query{
|
|
{
|
|
SimpleQueryString: &types.SimpleQueryStringQuery{
|
|
Query: params.ExcludedTerms,
|
|
Fields: []string{"message"},
|
|
DefaultOperator: &termOperator,
|
|
},
|
|
}, {
|
|
SimpleQueryString: &types.SimpleQueryStringQuery{
|
|
Query: params.ExcludedTerms,
|
|
Fields: []string{"attachments"},
|
|
DefaultOperator: &termOperator,
|
|
},
|
|
}, {
|
|
Term: map[string]types.TermQuery{
|
|
"urls": {Value: params.ExcludedTerms},
|
|
},
|
|
},
|
|
}},
|
|
}
|
|
|
|
notTermQueries = append(notTermQueries, query)
|
|
}
|
|
}
|
|
}
|
|
|
|
allTermsQuery := &types.BoolQuery{
|
|
MustNot: append([]types.Query(nil), notTermQueries...),
|
|
}
|
|
if searchParams[0].OrTerms {
|
|
allTermsQuery.Should = append([]types.Query(nil), termQueries...)
|
|
} else {
|
|
allTermsQuery.Must = append([]types.Query(nil), termQueries...)
|
|
}
|
|
|
|
fullHighlightsQuery := &types.BoolQuery{
|
|
Filter: append([]types.Query(nil), filters...),
|
|
MustNot: append([]types.Query(nil), notFilters...),
|
|
}
|
|
|
|
if searchParams[0].OrTerms {
|
|
fullHighlightsQuery.Should = append([]types.Query(nil), highlightQueries...)
|
|
} else {
|
|
fullHighlightsQuery.Must = append([]types.Query(nil), highlightQueries...)
|
|
}
|
|
|
|
filters = append(filters,
|
|
types.Query{
|
|
Terms: &types.TermsQuery{
|
|
TermsQuery: map[string]types.TermsQueryField{"channel_id": channelIds},
|
|
},
|
|
},
|
|
types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Should: []types.Query{
|
|
{
|
|
Term: map[string]types.TermQuery{"type": {Value: "default"}},
|
|
}, {
|
|
Term: map[string]types.TermQuery{"type": {Value: "slack_attachment"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
)
|
|
|
|
highlight := &types.Highlight{
|
|
HighlightQuery: &types.Query{
|
|
Bool: fullHighlightsQuery,
|
|
},
|
|
Fields: map[string]types.HighlightField{
|
|
"message": {},
|
|
"attachments": {},
|
|
"url": {},
|
|
"hashtag": {},
|
|
},
|
|
Encoder: &highlighterencoder.Html,
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
query := &types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Filter: append([]types.Query(nil), filters...),
|
|
Must: []types.Query{{Bool: allTermsQuery}},
|
|
MustNot: append([]types.Query(nil), notFilters...),
|
|
},
|
|
}
|
|
|
|
search := es.client.Search().
|
|
Index(common.SearchIndexName(es.Platform.Config().ElasticsearchSettings, common.IndexBasePosts+"*")).
|
|
Request(&search.Request{
|
|
Query: query,
|
|
Highlight: highlight,
|
|
}).
|
|
Sort(NewSortOptions().SortOptions(map[string]types.FieldSort{
|
|
"create_at": {Order: &sortorder.Desc},
|
|
})).
|
|
From(page * perPage).
|
|
Size(perPage)
|
|
|
|
searchResult, err := search.Do(ctx)
|
|
if err != nil {
|
|
errorStr := "err=" + err.Error()
|
|
if *es.Platform.Config().ElasticsearchSettings.Trace == "error" {
|
|
errorStr = "Query=" + getJSONOrErrorStr(query) + ", " + errorStr
|
|
}
|
|
return []string{}, nil, model.NewAppError("Elasticsearch.SearchPosts", "ent.elasticsearch.search_posts.search_failed", nil, errorStr, http.StatusInternalServerError)
|
|
}
|
|
|
|
postIds := make([]string, len(searchResult.Hits.Hits))
|
|
matches := make(model.PostSearchMatches, len(searchResult.Hits.Hits))
|
|
|
|
for i, hit := range searchResult.Hits.Hits {
|
|
var post common.ESPost
|
|
err := json.Unmarshal(hit.Source_, &post)
|
|
if err != nil {
|
|
return postIds, matches, model.NewAppError("Elasticsearch.SearchPosts", "ent.elasticsearch.search_posts.unmarshall_post_failed", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
postIds[i] = post.Id
|
|
|
|
matchesForPost, err := common.GetMatchesForHit(hit.Highlight)
|
|
if err != nil {
|
|
return postIds, matches, model.NewAppError("Elasticsearch.SearchPosts", "ent.elasticsearch.search_posts.parse_matches_failed", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
matches[post.Id] = matchesForPost
|
|
}
|
|
|
|
return postIds, matches, nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) DeletePost(post *model.Post) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.DeletePost", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
// This is racy with index aggregation, but since the posts are verified in the database when returning search
|
|
// results, there's no risk of deleted posts getting sent back to the user in response to a search query, and even
|
|
// then the race is very unlikely because it would only occur when someone deletes a post that's due to be
|
|
// aggregated but hasn't been yet, which makes the time window small and the post likelihood very low.
|
|
indexName := common.BuildPostIndexName(*es.Platform.Config().ElasticsearchSettings.AggregatePostsAfterDays,
|
|
*es.Platform.Config().ElasticsearchSettings.IndexPrefix+common.IndexBasePosts, *es.Platform.Config().ElasticsearchSettings.IndexPrefix+common.IndexBasePosts_MONTH, time.Now(), post.CreateAt)
|
|
|
|
if err := es.deletePost(indexName, post.Id); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) DeleteChannelPosts(rctx request.CTX, channelID string) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.DeleteChannelPosts", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
postIndexes, err := es.getPostIndexNames()
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeleteChannelPosts", "ent.elasticsearch.delete_channel_posts.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
query := &types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Filter: []types.Query{{
|
|
Term: map[string]types.TermQuery{"channel_id": {Value: channelID}},
|
|
}},
|
|
},
|
|
}
|
|
deleteQuery := es.client.DeleteByQuery(strings.Join(postIndexes, ",")).
|
|
Request(&deletebyquery.Request{
|
|
Query: query,
|
|
})
|
|
response, err := deleteQuery.Do(ctx)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeleteChannelPosts", "ent.elasticsearch.delete_channel_posts.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
rctx.Logger().Info("Posts for channel deleted", mlog.String("channel_id", channelID), mlog.Int("deleted", *response.Deleted))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) DeleteUserPosts(rctx request.CTX, userID string) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.DeleteUserPosts", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
postIndexes, err := es.getPostIndexNames()
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeleteUserPosts", "ent.elasticsearch.delete_user_posts.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
query := &types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Filter: []types.Query{{
|
|
Term: map[string]types.TermQuery{"user_id": {Value: userID}},
|
|
}},
|
|
},
|
|
}
|
|
|
|
deleteQuery := es.client.DeleteByQuery(strings.Join(postIndexes, ",")).
|
|
Request(&deletebyquery.Request{
|
|
Query: query,
|
|
})
|
|
|
|
response, err := deleteQuery.Do(ctx)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeleteUserPosts", "ent.elasticsearch.delete_user_posts.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
rctx.Logger().Info("Posts for user deleted", mlog.String("user_id", userID), mlog.Int("deleted", *response.Deleted))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) deletePost(indexName, postID string) *model.AppError {
|
|
var err error
|
|
if es.bulkProcessor != nil {
|
|
err = es.bulkProcessor.DeleteOp(types.DeleteOperation{
|
|
Index_: model.NewPointer(indexName),
|
|
Id_: model.NewPointer(postID),
|
|
})
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeletePost", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
} else {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
_, err = es.client.Delete(indexName, postID).Do(ctx)
|
|
}
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeletePost", "ent.elasticsearch.delete_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) IndexChannel(rctx request.CTX, channel *model.Channel, userIDs, teamMemberIDs []string) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.IndexChannel", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
indexName := *es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseChannels
|
|
|
|
searchChannel := common.ESChannelFromChannel(channel, userIDs, teamMemberIDs)
|
|
|
|
var err error
|
|
if es.bulkProcessor != nil {
|
|
err = es.bulkProcessor.IndexOp(types.IndexOperation{
|
|
Index_: model.NewPointer(indexName),
|
|
Id_: model.NewPointer(searchChannel.Id),
|
|
}, searchChannel)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.IndexChannel", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
} else {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
_, err = es.client.Index(indexName).
|
|
Id(searchChannel.Id).
|
|
Document(searchChannel).
|
|
Do(ctx)
|
|
}
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.IndexChannel", "ent.elasticsearch.index_channel.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
metrics := es.Platform.Metrics()
|
|
if metrics != nil {
|
|
metrics.IncrementChannelIndexCounter()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) SearchChannels(teamId, userID string, term string, isGuest, includeDeleted bool) ([]string, *model.AppError) {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return []string{}, model.NewAppError("Elasticsearch.SearchChannels", "ent.elasticsearch.search_channels.disabled", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
boolNotPrivate := types.Query{
|
|
Bool: &types.BoolQuery{
|
|
MustNot: []types.Query{{
|
|
Term: map[string]types.TermQuery{"type": {Value: model.ChannelTypePrivate}},
|
|
}},
|
|
},
|
|
}
|
|
|
|
userQ := types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Filter: []types.Query{{
|
|
Term: map[string]types.TermQuery{"user_ids": {Value: userID}},
|
|
}},
|
|
Must: []types.Query{{
|
|
Term: map[string]types.TermQuery{"type": {Value: model.ChannelTypePrivate}},
|
|
}},
|
|
},
|
|
}
|
|
|
|
query := &types.BoolQuery{}
|
|
|
|
if teamId != "" {
|
|
query.Filter = append(query.Filter, types.Query{Term: map[string]types.TermQuery{"team_id": {Value: teamId}}})
|
|
} else {
|
|
query.Filter = append(query.Filter, types.Query{Term: map[string]types.TermQuery{"team_member_ids": {Value: userID}}})
|
|
}
|
|
|
|
if !isGuest {
|
|
query.Filter = append(query.Filter, types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Should: []types.Query{
|
|
boolNotPrivate, userQ,
|
|
},
|
|
Must: []types.Query{{
|
|
Prefix: map[string]types.PrefixQuery{
|
|
"name_suggestions": {Value: strings.ToLower(term)},
|
|
},
|
|
}},
|
|
MinimumShouldMatch: 1,
|
|
},
|
|
})
|
|
} else {
|
|
query.Filter = append(query.Filter, types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Must: []types.Query{
|
|
boolNotPrivate, {
|
|
Prefix: map[string]types.PrefixQuery{
|
|
"name_suggestions": {Value: strings.ToLower(term)},
|
|
},
|
|
}},
|
|
},
|
|
})
|
|
}
|
|
|
|
if !includeDeleted {
|
|
query.Filter = append(query.Filter, types.Query{
|
|
Term: map[string]types.TermQuery{
|
|
"delete_at": {Value: 0},
|
|
},
|
|
})
|
|
}
|
|
|
|
search := es.client.Search().
|
|
Index(common.SearchIndexName(es.Platform.Config().ElasticsearchSettings, common.IndexBaseChannels)).
|
|
Request(&search.Request{
|
|
Query: &types.Query{Bool: query},
|
|
}).
|
|
Size(model.ChannelSearchDefaultLimit)
|
|
|
|
searchResult, err := search.Do(ctx)
|
|
|
|
if err != nil {
|
|
errorStr := "err=" + err.Error()
|
|
if *es.Platform.Config().ElasticsearchSettings.Trace == "error" {
|
|
errorStr = "Query=" + getJSONOrErrorStr(query) + ", " + errorStr
|
|
}
|
|
return nil, model.NewAppError("Elasticsearch.SearchChannels", "ent.elasticsearch.search_channels.search_failed", nil, errorStr, http.StatusInternalServerError)
|
|
}
|
|
|
|
channelIds := []string{}
|
|
for _, hit := range searchResult.Hits.Hits {
|
|
var channel common.ESChannel
|
|
err := json.Unmarshal(hit.Source_, &channel)
|
|
if err != nil {
|
|
return nil, model.NewAppError("Elasticsearch.SearchChannels", "ent.elasticsearch.search_channels.unmarshall_channel_failed", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
channelIds = append(channelIds, channel.Id)
|
|
}
|
|
|
|
return channelIds, nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) DeleteChannel(channel *model.Channel) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.DeleteChannel", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
var err error
|
|
if es.bulkProcessor != nil {
|
|
err = es.bulkProcessor.DeleteOp(types.DeleteOperation{
|
|
Index_: model.NewPointer(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseChannels),
|
|
Id_: model.NewPointer(channel.Id),
|
|
})
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.IndexPost", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
} else {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
_, err = es.client.Delete(*es.Platform.Config().ElasticsearchSettings.IndexPrefix+common.IndexBaseChannels, channel.Id).
|
|
Do(ctx)
|
|
}
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeleteChannel", "ent.elasticsearch.delete_channel.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) IndexUser(rctx request.CTX, user *model.User, teamsIds, channelsIds []string) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.IndexUser", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
indexName := *es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseUsers
|
|
|
|
searchUser := common.ESUserFromUserAndTeams(user, teamsIds, channelsIds)
|
|
|
|
var err error
|
|
if es.bulkProcessor != nil {
|
|
err = es.bulkProcessor.IndexOp(types.IndexOperation{
|
|
Index_: model.NewPointer(indexName),
|
|
Id_: model.NewPointer(searchUser.Id),
|
|
}, searchUser)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.IndexPost", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
} else {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
_, err = es.client.Index(indexName).
|
|
Id(searchUser.Id).
|
|
Document(searchUser).
|
|
Do(ctx)
|
|
}
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.IndexUser", "ent.elasticsearch.index_user.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
metrics := es.Platform.Metrics()
|
|
if metrics != nil {
|
|
metrics.IncrementUserIndexCounter()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) autocompleteUsers(contextCategory string, categoryIds []string, term string, options *model.UserSearchOptions) ([]common.ESUser, *model.AppError) {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return nil, model.NewAppError("Elasticsearch.autocompleteUsers", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
query := &types.BoolQuery{}
|
|
|
|
if term != "" {
|
|
var suggestionField string
|
|
if options.AllowFullNames {
|
|
suggestionField = "suggestions_with_fullname"
|
|
} else {
|
|
suggestionField = "suggestions_without_fullname"
|
|
}
|
|
query.Must = append(query.Must, types.Query{
|
|
Prefix: map[string]types.PrefixQuery{
|
|
suggestionField: {Value: strings.ToLower(term)},
|
|
},
|
|
})
|
|
}
|
|
|
|
if len(categoryIds) > 0 {
|
|
var iCategoryIds []string
|
|
for _, id := range categoryIds {
|
|
if id != "" {
|
|
iCategoryIds = append(iCategoryIds, id)
|
|
}
|
|
}
|
|
if len(iCategoryIds) > 0 {
|
|
query.Filter = append(query.Filter, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{contextCategory: iCategoryIds}},
|
|
})
|
|
}
|
|
}
|
|
|
|
if !options.AllowInactive {
|
|
query.Filter = append(query.Filter, types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Should: []types.Query{
|
|
{
|
|
Range: map[string]types.RangeQuery{
|
|
"delete_at": types.DateRangeQuery{
|
|
Lte: model.NewPointer("0"),
|
|
},
|
|
},
|
|
}, {
|
|
Bool: &types.BoolQuery{
|
|
MustNot: []types.Query{{
|
|
Exists: &types.ExistsQuery{Field: "delete_at"},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if options.Role != "" {
|
|
query.Filter = append(query.Filter, types.Query{
|
|
Term: map[string]types.TermQuery{
|
|
"roles": {Value: options.Role},
|
|
},
|
|
})
|
|
}
|
|
|
|
search := es.client.Search().
|
|
Index(common.SearchIndexName(es.Platform.Config().ElasticsearchSettings, common.IndexBaseUsers)).
|
|
Request(&search.Request{
|
|
Query: &types.Query{Bool: query},
|
|
}).
|
|
Size(options.Limit)
|
|
|
|
searchResults, err := search.Do(ctx)
|
|
|
|
if err != nil {
|
|
errorStr := "err=" + err.Error()
|
|
if *es.Platform.Config().ElasticsearchSettings.Trace == "error" {
|
|
errorStr = "Query=" + getJSONOrErrorStr(query) + ", " + errorStr
|
|
}
|
|
return nil, model.NewAppError("Elasticsearch.autocompleteUsers", "ent.elasticsearch.search_users.search_failed", nil, errorStr, http.StatusInternalServerError)
|
|
}
|
|
|
|
users := []common.ESUser{}
|
|
for _, hit := range searchResults.Hits.Hits {
|
|
var user common.ESUser
|
|
err := json.Unmarshal(hit.Source_, &user)
|
|
if err != nil {
|
|
return nil, model.NewAppError("Elasticsearch.autocompleteUsers", "ent.elasticsearch.search_users.unmarshall_user_failed", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
users = append(users, user)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) autocompleteUsersInChannel(channelId, term string, options *model.UserSearchOptions) ([]common.ESUser, *model.AppError) {
|
|
return es.autocompleteUsers("channel_id", []string{channelId}, term, options)
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) autocompleteUsersInChannels(channelIds []string, term string, options *model.UserSearchOptions) ([]common.ESUser, *model.AppError) {
|
|
return es.autocompleteUsers("channel_id", channelIds, term, options)
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) autocompleteUsersInTeam(teamId, term string, options *model.UserSearchOptions) ([]common.ESUser, *model.AppError) {
|
|
return es.autocompleteUsers("team_id", []string{teamId}, term, options)
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) autocompleteUsersNotInChannel(teamId, channelId string, restrictedToChannels []string, term string, options *model.UserSearchOptions) ([]common.ESUser, *model.AppError) {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return nil, model.NewAppError("Elasticsearch.autocompleteUsersNotInChannel", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
filterMust := []types.Query{{Term: map[string]types.TermQuery{
|
|
"team_id": {Value: teamId},
|
|
}}}
|
|
if len(restrictedToChannels) > 0 {
|
|
filterMust = append(filterMust, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{"channel_id": restrictedToChannels}},
|
|
})
|
|
}
|
|
|
|
query := &types.BoolQuery{
|
|
Filter: []types.Query{{
|
|
Bool: &types.BoolQuery{
|
|
Must: filterMust,
|
|
},
|
|
}},
|
|
MustNot: []types.Query{{
|
|
Term: map[string]types.TermQuery{
|
|
"channel_id": {Value: channelId},
|
|
},
|
|
}},
|
|
}
|
|
|
|
if term != "" {
|
|
var suggestionField string
|
|
if options.AllowFullNames {
|
|
suggestionField = "suggestions_with_fullname"
|
|
} else {
|
|
suggestionField = "suggestions_without_fullname"
|
|
}
|
|
query.Must = append(query.Must, types.Query{
|
|
Prefix: map[string]types.PrefixQuery{
|
|
suggestionField: {Value: strings.ToLower(term)},
|
|
},
|
|
})
|
|
}
|
|
|
|
if !options.AllowInactive {
|
|
notExistField := types.Query{
|
|
Bool: &types.BoolQuery{
|
|
MustNot: []types.Query{{
|
|
Exists: &types.ExistsQuery{Field: "delete_at"},
|
|
}},
|
|
},
|
|
}
|
|
deleteRangeQuery := types.Query{
|
|
Range: map[string]types.RangeQuery{
|
|
"delete_at": types.DateRangeQuery{
|
|
Lte: model.NewPointer("0"),
|
|
},
|
|
},
|
|
}
|
|
inactiveQuery := types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Should: []types.Query{deleteRangeQuery, notExistField},
|
|
},
|
|
}
|
|
query.Filter = append(query.Filter, inactiveQuery)
|
|
}
|
|
|
|
if options.Role != "" {
|
|
query.Filter = append(query.Filter, types.Query{
|
|
Term: map[string]types.TermQuery{
|
|
"roles": {Value: options.Role},
|
|
},
|
|
})
|
|
}
|
|
|
|
search := es.client.Search().
|
|
Index(common.SearchIndexName(es.Platform.Config().ElasticsearchSettings, common.IndexBaseUsers)).
|
|
Request(&search.Request{
|
|
Query: &types.Query{Bool: query},
|
|
}).
|
|
Size(options.Limit)
|
|
|
|
searchResults, err := search.Do(ctx)
|
|
if err != nil {
|
|
errorStr := "err=" + err.Error()
|
|
if *es.Platform.Config().ElasticsearchSettings.Trace == "error" {
|
|
errorStr = "Query=" + getJSONOrErrorStr(query) + ", " + errorStr
|
|
}
|
|
return nil, model.NewAppError("Elasticsearch.autocompleteUsersNotInChannel", "ent.elasticsearch.search_users.search_failed", nil, errorStr, http.StatusInternalServerError)
|
|
}
|
|
|
|
users := []common.ESUser{}
|
|
for _, hit := range searchResults.Hits.Hits {
|
|
var user common.ESUser
|
|
err := json.Unmarshal(hit.Source_, &user)
|
|
if err != nil {
|
|
return nil, model.NewAppError("Elasticsearch.autocompleteUsersNotInChannel", "ent.elasticsearch.search_users.unmarshall_user_failed", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
users = append(users, user)
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) SearchUsersInChannel(teamId, channelId string, restrictedToChannels []string, term string, options *model.UserSearchOptions) ([]string, []string, *model.AppError) {
|
|
if restrictedToChannels != nil && len(restrictedToChannels) == 0 {
|
|
return []string{}, []string{}, nil
|
|
}
|
|
|
|
uchan, err := es.autocompleteUsersInChannel(channelId, term, options)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var nuchan []common.ESUser
|
|
nuchan, err = es.autocompleteUsersNotInChannel(teamId, channelId, restrictedToChannels, term, options)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
uchanIds := []string{}
|
|
for _, user := range uchan {
|
|
uchanIds = append(uchanIds, user.Id)
|
|
}
|
|
nuchanIds := []string{}
|
|
for _, user := range nuchan {
|
|
nuchanIds = append(nuchanIds, user.Id)
|
|
}
|
|
|
|
return uchanIds, nuchanIds, nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) SearchUsersInTeam(teamId string, restrictedToChannels []string, term string, options *model.UserSearchOptions) ([]string, *model.AppError) {
|
|
if restrictedToChannels != nil && len(restrictedToChannels) == 0 {
|
|
return []string{}, nil
|
|
}
|
|
|
|
var users []common.ESUser
|
|
var err *model.AppError
|
|
if restrictedToChannels == nil {
|
|
users, err = es.autocompleteUsersInTeam(teamId, term, options)
|
|
} else {
|
|
users, err = es.autocompleteUsersInChannels(restrictedToChannels, term, options)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
usersIds := []string{}
|
|
if len(users) >= options.Limit {
|
|
users = users[:options.Limit]
|
|
}
|
|
|
|
for _, user := range users {
|
|
usersIds = append(usersIds, user.Id)
|
|
}
|
|
|
|
return usersIds, nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) DeleteUser(user *model.User) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.DeleteUser", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
var err error
|
|
if es.bulkProcessor != nil {
|
|
err = es.bulkProcessor.DeleteOp(types.DeleteOperation{
|
|
Index_: model.NewPointer(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseUsers),
|
|
Id_: model.NewPointer(user.Id),
|
|
})
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeleteUser", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
} else {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
_, err = es.client.Delete(*es.Platform.Config().ElasticsearchSettings.IndexPrefix+common.IndexBaseUsers, user.Id).
|
|
Do(ctx)
|
|
}
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeleteUser", "ent.elasticsearch.delete_user.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) TestConfig(rctx request.CTX, cfg *model.Config) *model.AppError {
|
|
if license := es.Platform.License(); license == nil || !*license.Features.Elasticsearch {
|
|
return model.NewAppError("Elasticsearch.TestConfig", "ent.elasticsearch.test_config.license.error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if !*cfg.ElasticsearchSettings.EnableIndexing {
|
|
return model.NewAppError("Elasticsearch.TestConfig", "ent.elasticsearch.test_config.indexing_disabled.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
client, appErr := createTypedClient(rctx.Logger(), cfg, es.Platform.FileBackend(), true)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
_, _, appErr = checkVersion(client, cfg)
|
|
if appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
// Resetting the state.
|
|
if es.ready.CompareAndSwap(0, 1) {
|
|
// Re-assign the client.
|
|
// This is necessary in case elasticsearch was started
|
|
// after server start.
|
|
es.mutex.Lock()
|
|
es.client = client
|
|
es.mutex.Unlock()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) PurgeIndexes(rctx request.CTX) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if license := es.Platform.License(); license == nil || !*license.Features.Elasticsearch {
|
|
return model.NewAppError("Elasticsearch.PurgeIndexes", "ent.elasticsearch.test_config.license.error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.PurgeIndexes", "ent.elasticsearch.generic.disabled", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
indexPrefix := *es.Platform.Config().ElasticsearchSettings.IndexPrefix
|
|
indexesToDelete := indexPrefix + "*"
|
|
|
|
if ignorePurgeIndexes := *es.Platform.Config().ElasticsearchSettings.IgnoredPurgeIndexes; ignorePurgeIndexes != "" {
|
|
// we are checking if provided indexes exist. If an index doesn't exist,
|
|
// elasticsearch returns an error while trying to purge it even we intend to
|
|
// ignore it.
|
|
for _, ignorePurgeIndex := range strings.Split(ignorePurgeIndexes, ",") {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
_, err := es.client.Indices.Get(ignorePurgeIndex).Do(ctx)
|
|
if err != nil {
|
|
rctx.Logger().Warn("Elasticsearch index get error", mlog.String("index", ignorePurgeIndex), mlog.Err(err))
|
|
continue
|
|
}
|
|
indexesToDelete += ",-" + strings.TrimSpace(ignorePurgeIndex)
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
_, err := es.client.Indices.Delete(indexesToDelete).Do(ctx)
|
|
if err != nil {
|
|
rctx.Logger().Error("Elastic Search PurgeIndexes Error", mlog.Err(err))
|
|
return model.NewAppError("Elasticsearch.PurgeIndexes", "ent.elasticsearch.purge_index.delete_failed", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PurgeIndexList purges a list of specified indexes.
|
|
// For now it only allows purging the channels index as thats all that's needed,
|
|
// but the code is written in generic fashion to allow it to purge any index.
|
|
// It needs more logic around post indexes as their name isn't the same, but rather follow a pattern
|
|
// containing the date as well.
|
|
func (es *ElasticsearchInterfaceImpl) PurgeIndexList(rctx request.CTX, indexes []string) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if license := es.Platform.License(); license == nil || !*license.Features.Elasticsearch {
|
|
return model.NewAppError("Elasticsearch.PurgeIndexList", "ent.elasticsearch.test_config.license.error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.PurgeIndexList", "ent.elasticsearch.generic.disabled", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
indexPrefix := *es.Platform.Config().ElasticsearchSettings.IndexPrefix
|
|
indexToDeleteMap := map[string]bool{}
|
|
for _, index := range indexes {
|
|
isKnownIndex := false
|
|
for _, allowedIndex := range purgeIndexListAllowedIndexes {
|
|
if index == allowedIndex {
|
|
isKnownIndex = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !isKnownIndex {
|
|
return model.NewAppError("Elasticsearch.PurgeIndexList", "ent.elasticsearch.purge_indexes.unknown_index", map[string]any{"unknown_index": index}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
indexToDeleteMap[indexPrefix+index] = true
|
|
}
|
|
|
|
if ign := *es.Platform.Config().ElasticsearchSettings.IgnoredPurgeIndexes; ign != "" {
|
|
// make sure we're not purging any index configured to be ignored
|
|
for _, ix := range strings.Split(ign, ",") {
|
|
delete(indexToDeleteMap, ix)
|
|
}
|
|
}
|
|
|
|
indexToDelete := []string{}
|
|
for key := range indexToDeleteMap {
|
|
indexToDelete = append(indexToDelete, key)
|
|
}
|
|
|
|
if len(indexToDelete) > 0 {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
_, err := es.client.Indices.Delete(strings.Join(indexToDelete, ",")).Do(ctx)
|
|
if err != nil {
|
|
elasticErr, ok := err.(*types.ElasticsearchError)
|
|
if !ok || elasticErr.Status != http.StatusNotFound {
|
|
rctx.Logger().Error("Elastic Search PurgeIndex Error", mlog.Err(err))
|
|
return model.NewAppError("Elasticsearch.PurgeIndexList", "ent.elasticsearch.purge_index.delete_failed", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) RefreshIndexes(rctx request.CTX) *model.AppError {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
_, err := es.client.Indices.Refresh().Do(ctx)
|
|
if err != nil {
|
|
rctx.Logger().Error("Elastic Search RefreshIndexes Error", mlog.Err(err))
|
|
return model.NewAppError("Elasticsearch.RefreshIndexes", "ent.elasticsearch.refresh_indexes.refresh_failed", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) DataRetentionDeleteIndexes(rctx request.CTX, cutoff time.Time) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if license := es.Platform.License(); license == nil || !*license.Features.Elasticsearch {
|
|
return model.NewAppError("Elasticsearch.DataRetentionDeleteIndexes", "ent.elasticsearch.test_config.license.error", nil, "", http.StatusNotImplemented)
|
|
}
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.DataRetentionDeleteIndexes", "ent.elasticsearch.generic.disabled", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
dateFormat := *es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBasePosts + "_2006_01_02"
|
|
postIndexesResult, err := es.client.Indices.Get(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBasePosts + "_*").Do(ctx)
|
|
if err != nil {
|
|
return model.NewAppError("ElasticSearch.DataRetentionDeleteIndexes", "ent.elasticsearch.data_retention_delete_indexes.get_indexes.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
for index := range postIndexesResult {
|
|
if indexDate, err := time.Parse(dateFormat, index); err != nil {
|
|
rctx.Logger().Warn("Failed to parse date from posts index. Ignoring index.", mlog.String("index", index))
|
|
} else {
|
|
if indexDate.Before(cutoff) || indexDate.Equal(cutoff) {
|
|
if _, err := es.client.Indices.Delete(index).Do(ctx); err != nil {
|
|
return model.NewAppError("ElasticSearch.DataRetentionDeleteIndexes", "ent.elasticsearch.data_retention_delete_indexes.delete_index.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) IndexFile(file *model.FileInfo, channelId string) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.IndexFile", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
indexName := *es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseFiles
|
|
|
|
searchFile := common.ESFileFromFileInfo(file, channelId)
|
|
|
|
var err error
|
|
if es.bulkProcessor != nil {
|
|
err = es.bulkProcessor.IndexOp(types.IndexOperation{
|
|
Index_: model.NewPointer(indexName),
|
|
Id_: model.NewPointer(searchFile.Id),
|
|
}, searchFile)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.IndexFile", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
} else {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
_, err = es.client.Index(indexName).
|
|
Id(file.Id).
|
|
Document(searchFile).
|
|
Do(ctx)
|
|
}
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.IndexFile", "ent.elasticsearch.index_file.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
if metrics := es.Platform.Metrics(); metrics != nil {
|
|
metrics.IncrementFileIndexCounter()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) SearchFiles(channels model.ChannelList, searchParams []*model.SearchParams, page, perPage int) ([]string, *model.AppError) {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return []string{}, model.NewAppError("Elasticsearch.SearchPosts", "ent.elasticsearch.search_files.disabled", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
var channelIds []string
|
|
for _, channel := range channels {
|
|
channelIds = append(channelIds, channel.Id)
|
|
}
|
|
|
|
var termQueries, notTermQueries []types.Query
|
|
var filters, notFilters []types.Query
|
|
for i, params := range searchParams {
|
|
newTerms := []string{}
|
|
for _, term := range strings.Split(params.Terms, " ") {
|
|
if searchengine.EmailRegex.MatchString(term) {
|
|
term = `"` + term + `"`
|
|
}
|
|
newTerms = append(newTerms, term)
|
|
}
|
|
|
|
params.Terms = strings.Join(newTerms, " ")
|
|
|
|
termOperator := operator.And
|
|
if searchParams[0].OrTerms {
|
|
termOperator = operator.Or
|
|
}
|
|
|
|
// Date, channels and FromUsers filters come in all
|
|
// searchParams iteration, and as they are global to the
|
|
// query, we only need to process them once
|
|
if i == 0 {
|
|
if len(params.InChannels) > 0 {
|
|
filters = append(filters, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{"channel_id": params.InChannels}},
|
|
})
|
|
}
|
|
|
|
if len(params.ExcludedChannels) > 0 {
|
|
notFilters = append(notFilters, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{"channel_id": params.ExcludedChannels}},
|
|
})
|
|
}
|
|
|
|
if len(params.FromUsers) > 0 {
|
|
filters = append(filters, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{"creator_id": params.FromUsers}},
|
|
})
|
|
}
|
|
|
|
if len(params.ExcludedUsers) > 0 {
|
|
notFilters = append(notFilters, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{"creator_id": params.ExcludedUsers}},
|
|
})
|
|
}
|
|
|
|
if len(params.Extensions) > 0 {
|
|
filters = append(filters, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{"extension": params.Extensions}},
|
|
})
|
|
}
|
|
|
|
if len(params.ExcludedExtensions) > 0 {
|
|
notFilters = append(notFilters, types.Query{
|
|
Terms: &types.TermsQuery{TermsQuery: map[string]types.TermsQueryField{"extension": params.ExcludedExtensions}},
|
|
})
|
|
}
|
|
|
|
if params.OnDate != "" {
|
|
before, after := params.GetOnDateMillis()
|
|
filters = append(filters, types.Query{
|
|
Range: map[string]types.RangeQuery{
|
|
"create_at": types.NumberRangeQuery{
|
|
Gte: model.NewPointer(types.Float64(before)),
|
|
Lte: model.NewPointer(types.Float64(after)),
|
|
},
|
|
},
|
|
})
|
|
} else {
|
|
if params.AfterDate != "" || params.BeforeDate != "" {
|
|
nrQuery := types.NumberRangeQuery{}
|
|
if params.AfterDate != "" {
|
|
nrQuery.Gte = model.NewPointer(types.Float64(params.GetAfterDateMillis()))
|
|
}
|
|
|
|
if params.BeforeDate != "" {
|
|
nrQuery.Lte = model.NewPointer(types.Float64(params.GetBeforeDateMillis()))
|
|
}
|
|
query := types.Query{
|
|
Range: map[string]types.RangeQuery{
|
|
"create_at": nrQuery,
|
|
},
|
|
}
|
|
filters = append(filters, query)
|
|
}
|
|
|
|
if params.ExcludedAfterDate != "" || params.ExcludedBeforeDate != "" || params.ExcludedDate != "" {
|
|
if params.ExcludedDate != "" {
|
|
before, after := params.GetExcludedDateMillis()
|
|
notFilters = append(notFilters, types.Query{
|
|
Range: map[string]types.RangeQuery{
|
|
"create_at": types.NumberRangeQuery{
|
|
Gte: model.NewPointer(types.Float64(before)),
|
|
Lte: model.NewPointer(types.Float64(after)),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if params.ExcludedAfterDate != "" {
|
|
notFilters = append(notFilters, types.Query{
|
|
Range: map[string]types.RangeQuery{
|
|
"create_at": types.NumberRangeQuery{
|
|
Gte: model.NewPointer(types.Float64(params.GetExcludedAfterDateMillis())),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if params.ExcludedBeforeDate != "" {
|
|
notFilters = append(notFilters, types.Query{
|
|
Range: map[string]types.RangeQuery{
|
|
"create_at": types.NumberRangeQuery{
|
|
Lte: model.NewPointer(types.Float64(params.GetExcludedBeforeDateMillis())),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if params.Terms != "" {
|
|
elements := []types.Query{
|
|
{
|
|
SimpleQueryString: &types.SimpleQueryStringQuery{
|
|
Query: params.Terms,
|
|
Fields: []string{"content"},
|
|
DefaultOperator: &termOperator,
|
|
},
|
|
}, {
|
|
SimpleQueryString: &types.SimpleQueryStringQuery{
|
|
Query: params.Terms,
|
|
Fields: []string{"name"},
|
|
DefaultOperator: &termOperator,
|
|
},
|
|
},
|
|
}
|
|
query := types.Query{
|
|
Bool: &types.BoolQuery{Should: append([]types.Query(nil), elements...)},
|
|
}
|
|
termQueries = append(termQueries, query)
|
|
}
|
|
|
|
if params.ExcludedTerms != "" {
|
|
elements := []types.Query{
|
|
{
|
|
SimpleQueryString: &types.SimpleQueryStringQuery{
|
|
Query: params.ExcludedTerms,
|
|
Fields: []string{"content"},
|
|
DefaultOperator: &termOperator,
|
|
},
|
|
}, {
|
|
SimpleQueryString: &types.SimpleQueryStringQuery{
|
|
Query: params.ExcludedTerms,
|
|
Fields: []string{"name"},
|
|
DefaultOperator: &termOperator,
|
|
},
|
|
},
|
|
}
|
|
query := types.Query{
|
|
Bool: &types.BoolQuery{Should: append([]types.Query(nil), elements...)},
|
|
}
|
|
notTermQueries = append(notTermQueries, query)
|
|
}
|
|
}
|
|
|
|
allTermsQuery := &types.BoolQuery{
|
|
MustNot: append([]types.Query(nil), notTermQueries...),
|
|
}
|
|
if searchParams[0].OrTerms {
|
|
allTermsQuery.Should = append([]types.Query(nil), termQueries...)
|
|
} else {
|
|
allTermsQuery.Must = append([]types.Query(nil), termQueries...)
|
|
}
|
|
|
|
filters = append(filters,
|
|
types.Query{
|
|
Terms: &types.TermsQuery{
|
|
TermsQuery: map[string]types.TermsQueryField{"channel_id": channelIds},
|
|
},
|
|
},
|
|
)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
query := &types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Filter: append([]types.Query(nil), filters...),
|
|
Must: []types.Query{{Bool: allTermsQuery}},
|
|
MustNot: append([]types.Query(nil), notFilters...),
|
|
},
|
|
}
|
|
|
|
search := es.client.Search().
|
|
Index(common.SearchIndexName(es.Platform.Config().ElasticsearchSettings, common.IndexBaseFiles)).
|
|
Request(&search.Request{
|
|
Query: query,
|
|
}).
|
|
Sort(NewSortOptions().SortOptions(map[string]types.FieldSort{
|
|
"create_at": {Order: &sortorder.Desc},
|
|
})).
|
|
From(page * perPage).
|
|
Size(perPage)
|
|
|
|
searchResult, err := search.Do(ctx)
|
|
if err != nil {
|
|
errorStr := "err=" + err.Error()
|
|
if *es.Platform.Config().ElasticsearchSettings.Trace == "error" {
|
|
errorStr = "Query=" + getJSONOrErrorStr(query) + ", " + errorStr
|
|
}
|
|
return []string{}, model.NewAppError("Elasticsearch.SearchFiles", "ent.elasticsearch.search_files.search_failed", nil, errorStr, http.StatusInternalServerError)
|
|
}
|
|
|
|
fileIds := make([]string, len(searchResult.Hits.Hits))
|
|
|
|
for i, hit := range searchResult.Hits.Hits {
|
|
var file common.ESFile
|
|
if err := json.Unmarshal(hit.Source_, &file); err != nil {
|
|
return fileIds, model.NewAppError("Elasticsearch.SearchFiles", "ent.elasticsearch.search_files.unmarshall_file_failed", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
fileIds[i] = file.Id
|
|
}
|
|
|
|
return fileIds, nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) DeleteFile(fileID string) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.DeleteFile", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
var err error
|
|
if es.bulkProcessor != nil {
|
|
err = es.bulkProcessor.DeleteOp(types.DeleteOperation{
|
|
Index_: model.NewPointer(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseFiles),
|
|
Id_: model.NewPointer(fileID),
|
|
})
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeleteFile", model.NoTranslation, nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
} else {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
_, err = es.client.Delete(*es.Platform.Config().ElasticsearchSettings.IndexPrefix+common.IndexBaseFiles, fileID).
|
|
Do(ctx)
|
|
}
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeleteFile", "ent.elasticsearch.delete_file.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) DeleteUserFiles(rctx request.CTX, userID string) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.DeleteFilesBatch", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
query := &types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Filter: []types.Query{{
|
|
Term: map[string]types.TermQuery{"creator_id": {Value: userID}},
|
|
}},
|
|
},
|
|
}
|
|
|
|
deleteQuery := es.client.DeleteByQuery(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseFiles).
|
|
Request(&deletebyquery.Request{
|
|
Query: query,
|
|
})
|
|
response, err := deleteQuery.Do(ctx)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeleteUserFiles", "ent.elasticsearch.delete_user_files.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
rctx.Logger().Info("User files deleted", mlog.String("user_id", userID), mlog.Int("deleted", *response.Deleted))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) DeletePostFiles(rctx request.CTX, postID string) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.DeleteFilesBatch", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
query := &types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Filter: []types.Query{{
|
|
Term: map[string]types.TermQuery{"post_id": {Value: postID}},
|
|
}},
|
|
},
|
|
}
|
|
deleteQuery := es.client.DeleteByQuery(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseFiles).
|
|
Request(&deletebyquery.Request{
|
|
Query: query,
|
|
})
|
|
response, err := deleteQuery.Do(ctx)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeletePostFiles", "ent.elasticsearch.delete_post_files.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
rctx.Logger().Info("Post files deleted", mlog.String("post_id", postID), mlog.Int("deleted", *response.Deleted))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (es *ElasticsearchInterfaceImpl) DeleteFilesBatch(rctx request.CTX, endTime, limit int64) *model.AppError {
|
|
es.mutex.RLock()
|
|
defer es.mutex.RUnlock()
|
|
|
|
if es.ready.Load() == 0 {
|
|
return model.NewAppError("Elasticsearch.DeleteFilesBatch", "ent.elasticsearch.not_started.error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*es.Platform.Config().ElasticsearchSettings.RequestTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
query := &types.Query{
|
|
Bool: &types.BoolQuery{
|
|
Filter: []types.Query{{
|
|
Range: map[string]types.RangeQuery{
|
|
"create_at": types.NumberRangeQuery{
|
|
Lte: model.NewPointer(types.Float64(endTime)),
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
|
|
deleteQuery := es.client.DeleteByQuery(*es.Platform.Config().ElasticsearchSettings.IndexPrefix + common.IndexBaseFiles).
|
|
Request(&deletebyquery.Request{
|
|
Query: query,
|
|
}).
|
|
// Note that max_docs is slightly different than size.
|
|
// Size will just limit the number of elements returned, which is not
|
|
// what we want. We want to limit the number of elements to be deleted.
|
|
MaxDocs(limit)
|
|
response, err := deleteQuery.Do(ctx)
|
|
if err != nil {
|
|
return model.NewAppError("Elasticsearch.DeleteUserPosts", "ent.elasticsearch.delete_user_posts.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
rctx.Logger().Info("Files batch deleted", mlog.Int("end_time", endTime), mlog.Int("limit", limit), mlog.Int("deleted", *response.Deleted))
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkVersion(client *elastic.TypedClient, cfg *model.Config) (string, int, *model.AppError) {
|
|
resp, err := client.API.Core.Info().Do(context.Background())
|
|
if err != nil {
|
|
return "", 0, model.NewAppError("Elasticsearch.checkVersion", "ent.elasticsearch.start.get_server_version.app_error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError).Wrap(err)
|
|
}
|
|
|
|
major, _, _, esErr := common.GetVersionComponents(resp.Version.Int)
|
|
if esErr != nil {
|
|
return "", 0, model.NewAppError("Elasticsearch.checkVersion", "ent.elasticsearch.start.parse_server_version.app_error", map[string]any{"Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusInternalServerError).Wrap(esErr)
|
|
}
|
|
|
|
if major < elasticsearchMinVersion {
|
|
return "", 0, model.NewAppError("Elasticsearch.checkVersion", "ent.elasticsearch.min_version.app_error", map[string]any{"Version": major, "MinVersion": elasticsearchMinVersion, "Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusBadRequest)
|
|
}
|
|
if major > elasticsearchMaxVersion {
|
|
return "", 0, model.NewAppError("Elasticsearch.checkVersion", "ent.elasticsearch.max_version.app_error", map[string]any{"Version": major, "MaxVersion": elasticsearchMaxVersion, "Backend": model.ElasticsearchSettingsESBackend}, "", http.StatusBadRequest)
|
|
}
|
|
return resp.Version.Int, major, nil
|
|
}
|