add elastic8 filer store for Elasticsearch 8 (#9916)

* elastic: fix listing against a missing or empty directory index

The refresh 404 leaked into the named return, so the first listing of a
directory whose index does not exist yet returned an error instead of an
empty result. Sorting also fails on an index with no documents
("No mapping found for [_id] in order to sort on"); unmapped_type
keeps the resumed-listing path working there.

* add elastic8 filer store for Elasticsearch 8

Elasticsearch 8 disables _id fielddata by default, so the elastic7
store's directory listings fail with "Fielddata access on the _id
field is disallowed". elastic8 uses the same client and configuration
options, but also indexes the document id as an Id field and sorts
listings on Id.keyword.
This commit is contained in:
Chris Lu
2026-06-10 12:10:49 -07:00
committed by GitHub
parent 689b5b61bf
commit 2ac5aa72c7
2 changed files with 50 additions and 4 deletions
+15
View File
@@ -366,6 +366,21 @@ healthcheck_enabled = false
# increase the value is recommend, be sure the value in Elastic is greater or equal here # increase the value is recommend, be sure the value in Elastic is greater or equal here
index.max_result_window = 10000 index.max_result_window = 10000
# for Elasticsearch 8.x clusters
[elastic8]
enabled = false
servers = [
"http://localhost1:9200",
"http://localhost2:9200",
"http://localhost3:9200",
]
username = ""
password = ""
sniff_enabled = false
healthcheck_enabled = false
# increase the value is recommend, be sure the value in Elastic is greater or equal here
index.max_result_window = 10000
[arangodb] # in development dont use it [arangodb] # in development dont use it
enabled = false enabled = false
+35 -4
View File
@@ -34,6 +34,7 @@ var (
type ESEntry struct { type ESEntry struct {
ParentId string `json:"ParentId"` ParentId string `json:"ParentId"`
Id string `json:"Id,omitempty"`
Entry *filer.Entry Entry *filer.Entry
} }
@@ -43,17 +44,33 @@ type ESKVEntry struct {
func init() { func init() {
filer.Stores = append(filer.Stores, &ElasticStore{}) filer.Stores = append(filer.Stores, &ElasticStore{})
filer.Stores = append(filer.Stores, &Elastic8Store{})
} }
type ElasticStore struct { type ElasticStore struct {
client *elastic.Client client *elastic.Client
maxPageSize int maxPageSize int
es8 bool
} }
func (store *ElasticStore) GetName() string { func (store *ElasticStore) GetName() string {
return "elastic7" return "elastic7"
} }
// Elastic8Store sorts listings on an indexed Id field since Elasticsearch 8 disallows _id fielddata.
type Elastic8Store struct {
ElasticStore
}
func (store *Elastic8Store) GetName() string {
return "elastic8"
}
func (store *Elastic8Store) Initialize(configuration weed_util.Configuration, prefix string) (err error) {
store.es8 = true
return store.ElasticStore.Initialize(configuration, prefix)
}
func (store *ElasticStore) Initialize(configuration weed_util.Configuration, prefix string) (err error) { func (store *ElasticStore) Initialize(configuration weed_util.Configuration, prefix string) (err error) {
options := []elastic.ClientOptionFunc{} options := []elastic.ClientOptionFunc{}
servers := configuration.GetStringSlice(prefix + "servers") servers := configuration.GetStringSlice(prefix + "servers")
@@ -110,6 +127,9 @@ func (store *ElasticStore) InsertEntry(ctx context.Context, entry *filer.Entry)
ParentId: weed_util.Md5String([]byte(dir)), ParentId: weed_util.Md5String([]byte(dir)),
Entry: entry, Entry: entry,
} }
if store.es8 {
esEntry.Id = id
}
value, err := jsoniter.Marshal(esEntry) value, err := jsoniter.Marshal(esEntry)
if err != nil { if err != nil {
glog.ErrorfCtx(ctx, "insert entry(%s) %v.", string(entry.FullPath), err) glog.ErrorfCtx(ctx, "insert entry(%s) %v.", string(entry.FullPath), err)
@@ -219,8 +239,10 @@ func (store *ElasticStore) listDirectoryEntries(
parentId := weed_util.Md5String([]byte(fullpath)) parentId := weed_util.Md5String([]byte(fullpath))
if _, err = store.client.Refresh(index).Do(ctx); err != nil { if _, err = store.client.Refresh(index).Do(ctx); err != nil {
if elastic.IsNotFound(err) { if elastic.IsNotFound(err) {
store.client.CreateIndex(index).Do(ctx) if _, err := store.client.CreateIndex(index).Do(ctx); err != nil {
return return lastFileName, fmt.Errorf("create index(%s) %v", index, err)
}
return lastFileName, nil
} }
} }
for { for {
@@ -278,6 +300,15 @@ func (store *ElasticStore) listDirectoryEntries(
return return
} }
func (store *ElasticStore) listSorter() elastic.Sorter {
field := "_id"
if store.es8 {
field = "Id.keyword"
}
// unmapped_type tolerates indexes with no documents yet
return elastic.NewFieldSort(field).Desc().UnmappedType("keyword")
}
func (store *ElasticStore) search(ctx context.Context, index, parentId string) (result *elastic.SearchResult, err error) { func (store *ElasticStore) search(ctx context.Context, index, parentId string) (result *elastic.SearchResult, err error) {
if count, err := store.client.Count(index).Do(ctx); err == nil && count == 0 { if count, err := store.client.Count(index).Do(ctx); err == nil && count == 0 {
return &elastic.SearchResult{ return &elastic.SearchResult{
@@ -289,7 +320,7 @@ func (store *ElasticStore) search(ctx context.Context, index, parentId string) (
Index(index). Index(index).
Query(elastic.NewMatchQuery("ParentId", parentId)). Query(elastic.NewMatchQuery("ParentId", parentId)).
Size(store.maxPageSize). Size(store.maxPageSize).
Sort("_id", false). SortBy(store.listSorter()).
Do(ctx) Do(ctx)
return queryResult, err return queryResult, err
} }
@@ -300,7 +331,7 @@ func (store *ElasticStore) searchAfter(ctx context.Context, index, parentId, aft
Query(elastic.NewMatchQuery("ParentId", parentId)). Query(elastic.NewMatchQuery("ParentId", parentId)).
SearchAfter(after). SearchAfter(after).
Size(store.maxPageSize). Size(store.maxPageSize).
Sort("_id", false). SortBy(store.listSorter()).
Do(ctx) Do(ctx)
return queryResult, err return queryResult, err