From f70435eca1add246a8bac96aa6304fb7e746b4ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Strzali=C5=84ski?= Date: Thu, 18 Jul 2024 19:27:27 +0200 Subject: [PATCH] Optimizer - Rewrite query using a materialized view (#540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds an optimizer that rewrites queries and replaces the original table with a materialized view. The rewrite rule is taken from the configuration. Suppose we've got the following table: ``` CREATE some_index ( `date` DateTime64(3), `type` String, ... ) ENGINE=MergeTree() ORDER BY (`date`) ``` We've discovered our dashboard generates a lot of queries with common expressions: ``` (type ILIKE '%foo%') ``` We may create a materialized view and query it instead of the original table. Notice that the materialized view is updated on INSERT, it must be created before ingest. ``` -- a table that holds materialized view data CREATE table some_index_foo_dest ( `date` DateTime64(3), `type` String, ... ) -- an index CREATE MATERIALIZED VIEW some_index_foo_mv to some_index_foo_dest AS SELECT * FROM some_index WHERE type ILIKE '%foo%' ``` Now we can configure the optimizer itself. ``` -- part of config.yaml some_index: enabled: true optimizers: materialized_view_replace: enabled: true properties: table: "some_index" condition: "(type ILIKE '%foo%')" view: "some_index_foo_mv" ``` So every query like ``` select count(*) from some_index where ((type ILIKE '%foo%') AND (`date` <= ..... AND `date` >= .....)) ``` Will be rewritten to: ``` select count(*) from some_index where (TRUE AND (`date` <= ..... AND `date` >= .....)) ``` Limitations: - we can have only one rewrite rule per index/table - a condition in the configuration must be the same as a `string` representation of the replaced expression - setup and configuration are complex --------- Signed-off-by: Rafał Strzaliński --- quesma/optimize/cache_group_by.go | 2 +- quesma/optimize/materialized_view_replace.go | 226 +++++++++++++++++ quesma/optimize/pipeline.go | 38 ++- quesma/optimize/pipeline_test.go | 254 ++++++++++++++++++- quesma/optimize/trunc_date.go | 5 +- quesma/quesma/config/config.go | 15 +- quesma/quesma/config/index_config.go | 4 +- 7 files changed, 510 insertions(+), 34 deletions(-) create mode 100644 quesma/optimize/materialized_view_replace.go diff --git a/quesma/optimize/cache_group_by.go b/quesma/optimize/cache_group_by.go index 598ebb907..a401e377b 100644 --- a/quesma/optimize/cache_group_by.go +++ b/quesma/optimize/cache_group_by.go @@ -31,7 +31,7 @@ func (s *cacheGroupByQueries) IsEnabledByDefault() bool { return false } -func (s *cacheGroupByQueries) Transform(queries []*model.Query) ([]*model.Query, error) { +func (s *cacheGroupByQueries) Transform(queries []*model.Query, properties map[string]string) ([]*model.Query, error) { for _, query := range queries { diff --git a/quesma/optimize/materialized_view_replace.go b/quesma/optimize/materialized_view_replace.go new file mode 100644 index 000000000..03f570c4d --- /dev/null +++ b/quesma/optimize/materialized_view_replace.go @@ -0,0 +1,226 @@ +// Copyright Quesma, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 +package optimize + +import ( + "quesma/logger" + "quesma/model" + "strings" +) + +type materializedViewReplaceRule struct { + tableName string // table name that we want to replace + condition string // this is string representation of the condition that we want to replace + materializedView string // target +} + +type materializedViewReplace struct { +} + +// it checks if the WHERE clause is `AND` tree only +func (s *materializedViewReplace) validateWhere(expr model.Expr) bool { + + var foundOR bool + var foundNOT bool + + visitor := model.NewBaseVisitor() + + visitor.OverrideVisitPrefixExpr = func(b *model.BaseExprVisitor, e model.PrefixExpr) interface{} { + + if strings.ToUpper(e.Op) == "NOT" { + foundNOT = true + return e + } + + b.VisitChildren(e.Args) + return e + } + + visitor.OverrideVisitInfix = func(b *model.BaseExprVisitor, e model.InfixExpr) interface{} { + if strings.ToUpper(e.Op) == "OR" { + foundOR = true + return e + } + e.Left.Accept(b) + e.Right.Accept(b) + return e + } + + expr.Accept(visitor) + + if foundNOT { + return false + } + + if foundOR { + return false + } + + return true +} + +func (s *materializedViewReplace) getTableName(tableName string) string { + + res := strings.Replace(tableName, `"`, "", -1) + if strings.Contains(res, ".") { + parts := strings.Split(res, ".") + if len(parts) == 2 { + return parts[1] + } + } + return res +} + +func (s *materializedViewReplace) matches(rule materializedViewReplaceRule, expr model.Expr) bool { + current := model.AsString(expr) + return rule.condition == current +} + +func (s *materializedViewReplace) applyRule(rule materializedViewReplaceRule, expr model.Expr) (model.Expr, bool) { + if s.matches(rule, expr) { + return model.NewLiteral("TRUE"), true + } + + return expr, false +} + +func (s *materializedViewReplace) traverse(rule materializedViewReplaceRule, where model.Expr) (model.Expr, bool) { + + var foundInNot bool + var replaced bool + var res model.Expr + + visitor := model.NewBaseVisitor() + + visitor.OverrideVisitInfix = func(b *model.BaseExprVisitor, e model.InfixExpr) interface{} { + + // since we replace with "TRUE" we need to check if the operator is "AND" + if strings.ToUpper(e.Op) == "AND" { + + left, leftReplaced := s.applyRule(rule, e.Left) + right, rightReplaced := s.applyRule(rule, e.Right) + + if !leftReplaced { + left, leftReplaced = e.Left.Accept(b).(model.Expr) + } + + if !rightReplaced { + right, rightReplaced = e.Right.Accept(b).(model.Expr) + } + + if leftReplaced || rightReplaced { + replaced = true + } + return model.NewInfixExpr(left, e.Op, right) + } + + return model.NewInfixExpr(e.Left.Accept(b).(model.Expr), e.Op, e.Right.Accept(b).(model.Expr)) + } + + res = where.Accept(visitor).(model.Expr) + + if foundInNot { + return nil, false + } + + return res, replaced +} + +func (s *materializedViewReplace) replace(rule materializedViewReplaceRule, query model.SelectCommand) (*model.SelectCommand, bool) { + + visitor := model.NewBaseVisitor() + var replaced bool + + visitor.OverrideVisitSelectCommand = func(v *model.BaseExprVisitor, query model.SelectCommand) interface{} { + + var ctes []*model.SelectCommand + if query.CTEs != nil { + ctes = make([]*model.SelectCommand, 0) + for _, cte := range query.CTEs { + ctes = append(ctes, cte.Accept(v).(*model.SelectCommand)) + } + } + + from := query.FromClause + + if from != nil { + if table, ok := from.(model.TableRef); ok { + + tableName := s.getTableName(table.Name) // todo: get table name from data + + // if we match the table name + if rule.tableName == tableName { // config param + + // we try to replace the where clause + newWhere, whereReplaced := s.applyRule(rule, query.WhereClause) + + if !whereReplaced { + // if we have AND tree, we try to traverse it + if s.validateWhere(query.WhereClause) { + // here we try to traverse the whole tree + newWhere, whereReplaced = s.traverse(rule, query.WhereClause) + } + } + + // if we replaced the where clause, we replace the from clause + if whereReplaced { + replaced = true + from = model.NewTableRef(rule.materializedView) // config param + return model.NewSelectCommand(query.Columns, query.GroupBy, query.OrderBy, from, newWhere, query.LimitBy, query.Limit, query.SampleLimit, query.IsDistinct, ctes) + } + } + } else { + from = query.FromClause.Accept(v).(model.Expr) + } + } + + where := query.WhereClause.Accept(v).(model.Expr) + + return model.NewSelectCommand(query.Columns, query.GroupBy, query.OrderBy, from, where, query.LimitBy, query.Limit, query.SampleLimit, query.IsDistinct, ctes) + + } + + newSelect := query.Accept(visitor).(*model.SelectCommand) + + return newSelect, replaced +} + +func (s *materializedViewReplace) readRule(properties map[string]string) materializedViewReplaceRule { + rule := materializedViewReplaceRule{ + tableName: properties["table"], + condition: properties["condition"], + materializedView: properties["view"], + } + return rule +} + +func (s *materializedViewReplace) Name() string { + return "materialized_view_replace" +} + +func (s *materializedViewReplace) IsEnabledByDefault() bool { + return false +} + +func (s *materializedViewReplace) Transform(queries []*model.Query, properties map[string]string) ([]*model.Query, error) { + + // + // TODO add list of rules maybe + // + rule := s.readRule(properties) + + for k, query := range queries { + + result, replaced := s.replace(rule, query.SelectCommand) + + // this is just in case if there was no truncation, we keep the original query + if result != nil && replaced { + logger.Info().Msgf(s.Name()+" triggered, input query: %s", query.SelectCommand.String()) + logger.Info().Msgf(s.Name()+" triggered, output query: %s", (*result).String()) + + queries[k].SelectCommand = *result + query.OptimizeHints.OptimizationsPerformed = append(query.OptimizeHints.OptimizationsPerformed, s.Name()) + } + } + return queries, nil +} diff --git a/quesma/optimize/pipeline.go b/quesma/optimize/pipeline.go index 683938b22..5af359d53 100644 --- a/quesma/optimize/pipeline.go +++ b/quesma/optimize/pipeline.go @@ -3,15 +3,18 @@ package optimize import ( + "fmt" "quesma/model" "quesma/plugins" "quesma/quesma/config" + "strings" "time" ) // OptimizeTransformer - an interface for query transformers that have a name. type OptimizeTransformer interface { - plugins.QueryTransformer + Transform(queries []*model.Query, properties map[string]string) ([]*model.Query, error) + Name() string // this name is used to enable/disable the transformer in the configuration IsEnabledByDefault() bool // should return true for "not aggressive" transformers only } @@ -29,6 +32,7 @@ func NewOptimizePipeline(config config.QuesmaConfiguration) plugins.QueryTransfo optimizations: []OptimizeTransformer{ &truncateDate{truncateTo: 5 * time.Minute}, &cacheGroupByQueries{}, + &materializedViewReplace{}, }, } } @@ -44,27 +48,33 @@ func (s *OptimizePipeline) getIndexName(queries []*model.Query) string { // ... // } - return queries[0].TableName + // we assume here that table_name is the index name + tableName := queries[0].TableName + res := strings.Replace(tableName, `"`, "", -1) + if strings.Contains(res, ".") { + parts := strings.Split(res, ".") + if len(parts) == 2 { + return parts[1] + } + } + return res } -func (s *OptimizePipeline) isEnabledFor(transformer OptimizeTransformer, queries []*model.Query) bool { +func (s *OptimizePipeline) findConfig(transformer OptimizeTransformer, queries []*model.Query) (bool, map[string]string) { indexName := s.getIndexName(queries) // first we check index specific settings if indexCfg, ok := s.config.IndexConfig[indexName]; ok { - if enabled, ok := indexCfg.EnabledOptimizers[transformer.Name()]; ok { - return enabled + fmt.Println("Index specific settings found", indexName) + if optimizerCfg, ok := indexCfg.EnabledOptimizers[transformer.Name()]; ok { + fmt.Println("Optimizer specific settings found", transformer.Name(), optimizerCfg.Enabled) + return optimizerCfg.Enabled, optimizerCfg.Properties } } - // then we check global settings - if enabled, ok := s.config.EnabledOptimizers[transformer.Name()]; ok { - return enabled - } - // default is not enabled - return transformer.IsEnabledByDefault() + return transformer.IsEnabledByDefault(), make(map[string]string) } func (s *OptimizePipeline) Transform(queries []*model.Query) ([]*model.Query, error) { @@ -79,12 +89,14 @@ func (s *OptimizePipeline) Transform(queries []*model.Query) ([]*model.Query, er // run optimizations on queries for _, optimization := range s.optimizations { - if !s.isEnabledFor(optimization, queries) { + enabled, properties := s.findConfig(optimization, queries) + + if !enabled { continue } var err error - queries, err = optimization.Transform(queries) + queries, err = optimization.Transform(queries, properties) if err != nil { return nil, err } diff --git a/quesma/optimize/pipeline_test.go b/quesma/optimize/pipeline_test.go index 4c35cd74d..f9e15e9c2 100644 --- a/quesma/optimize/pipeline_test.go +++ b/quesma/optimize/pipeline_test.go @@ -15,11 +15,13 @@ func Test_cacheGroupBy(t *testing.T) { tests := []struct { name string shouldCache bool + tableName string query model.SelectCommand }{ { "select all", false, + "foo", model.SelectCommand{ Columns: []model.Expr{model.NewColumnRef("*")}, FromClause: model.NewTableRef("foo"), @@ -29,6 +31,7 @@ func Test_cacheGroupBy(t *testing.T) { { "select a, count() from foo group by 1", true, + "foo", model.SelectCommand{ Columns: []model.Expr{model.NewColumnRef("a"), model.NewFunction("count", model.NewColumnRef("*"))}, FromClause: model.NewTableRef("foo"), @@ -39,8 +42,10 @@ func Test_cacheGroupBy(t *testing.T) { } cfg := config.QuesmaConfiguration{} - cfg.EnabledOptimizers = make(config.OptimizersConfiguration) - cfg.EnabledOptimizers["cache_group_by_queries"] = true + cfg.IndexConfig = make(map[string]config.IndexConfiguration) + cfg.IndexConfig["foo"] = config.IndexConfiguration{ + EnabledOptimizers: map[string]config.OptimizerConfiguration{"cache_group_by_queries": {Enabled: true}}, + } for _, tt := range tests { @@ -49,6 +54,7 @@ func Test_cacheGroupBy(t *testing.T) { queries := []*model.Query{ { SelectCommand: tt.query, + TableName: tt.tableName, }, } pipeline := NewOptimizePipeline(cfg) @@ -67,9 +73,7 @@ func Test_cacheGroupBy(t *testing.T) { } assert.Truef(t, enabled == tt.shouldCache, "expected use_query_cache to be %v, got %v", tt.shouldCache, enabled) - }) - } } @@ -191,14 +195,9 @@ func Test_dateTrunc(t *testing.T) { } cfg := config.QuesmaConfiguration{} - cfg.EnabledOptimizers = make(config.OptimizersConfiguration) - cfg.EnabledOptimizers["truncate_date"] = false - cfg.IndexConfig = make(map[string]config.IndexConfiguration) cfg.IndexConfig["foo"] = config.IndexConfiguration{ - EnabledOptimizers: config.OptimizersConfiguration{ - "truncate_date": true, - }, + EnabledOptimizers: map[string]config.OptimizerConfiguration{"truncate_date": {Enabled: true}}, } for _, tt := range tests { @@ -228,3 +227,238 @@ func Test_dateTrunc(t *testing.T) { } } + +func Test_materialized_view_replace(t *testing.T) { + + // DSL + date := func(s string) model.Expr { + return model.NewFunction("parseDateTime64BestEffort", model.NewLiteral(fmt.Sprintf("'%s'", s))) + } + + and := func(a, b model.Expr) model.Expr { + return model.NewInfixExpr(a, "and", b) + } + + or := func(a, b model.Expr) model.Expr { + return model.NewInfixExpr(a, "or", b) + } + + lt := func(a, b model.Expr) model.Expr { + return model.NewInfixExpr(a, "<", b) + } + + gt := func(a, b model.Expr) model.Expr { + return model.NewInfixExpr(a, ">", b) + } + + not := func(a model.Expr) model.Expr { + return model.NewPrefixExpr("not", []model.Expr{a}) + } + + col := func(s string) model.Expr { + return model.NewColumnRef(s) + } + + literal := func(a any) model.Expr { return model.NewLiteral(a) } + + condition := gt(col("a"), literal(10)) + TRUE := literal("TRUE") + + // tests + tests := []struct { + name string + tableName string + query model.SelectCommand + expected model.SelectCommand + }{ + { + "select all where date ", + "foo", + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: date("2024-06-04T13:08:53.675Z"), + }, + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: date("2024-06-04T13:08:53.675Z"), + }, + }, + + { + "select all with condition at top level", + "foo", + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: condition, + }, + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo_view"), + WhereClause: TRUE, + }, + }, + + { + "select all with condition 2", + "foo", + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: and(lt(col("c"), literal(1)), condition), + }, + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo_view"), + WhereClause: and(lt(col("c"), literal(1)), TRUE), + }, + }, + + { + "select all with condition 3", + "foo", + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: and(condition, and(lt(col("c"), literal(1)), condition)), + }, + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo_view"), + WhereClause: and(TRUE, and(lt(col("c"), literal(1)), TRUE)), + }, + }, + + { + "select all with condition 4", + "foo", + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: and(and(condition, condition), and(lt(col("c"), literal(1)), condition)), + }, + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo_view"), + WhereClause: and(and(TRUE, TRUE), and(lt(col("c"), literal(1)), TRUE)), + }, + }, + + { + "select all without condition", + "foo", + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: lt(col("a"), literal(10)), + }, + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: lt(col("a"), literal(10)), + }, + }, + + { + "select all from other table with condition at top level", + "foo", + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo1"), + WhereClause: condition, + }, + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo1"), + WhereClause: condition, + }, + }, + + { + "select all OR", + "foo", + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: or(condition, lt(col("b"), literal(1))), + }, + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: or(condition, lt(col("b"), literal(1))), + }, + }, + + { + "select all NOT", + "foo", + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: and(not(condition), lt(col("b"), literal(1))), + }, + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: and(not(condition), lt(col("b"), literal(1))), + }, + }, + + { + "select all NOT2", + "foo", + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: and(condition, and(not(lt(col("c"), literal(2))), lt(col("b"), literal(1)))), + }, + model.SelectCommand{ + Columns: []model.Expr{model.NewColumnRef("*")}, + FromClause: model.NewTableRef("foo"), + WhereClause: and(condition, and(not(lt(col("c"), literal(2))), lt(col("b"), literal(1)))), + }, + }, + } + + cfg := config.QuesmaConfiguration{} + cfg.IndexConfig = make(map[string]config.IndexConfiguration) + cfg.IndexConfig["foo"] = config.IndexConfiguration{ + EnabledOptimizers: map[string]config.OptimizerConfiguration{ + "materialized_view_replace": { + Enabled: true, + Properties: map[string]string{ + "table": "foo", + "condition": `"a">10`, + "view": "foo_view", + }, + }, + }, + } + + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + queries := []*model.Query{ + { + TableName: tt.tableName, + SelectCommand: tt.query, + }, + } + pipeline := NewOptimizePipeline(cfg) + optimized, err := pipeline.Transform(queries) + + if err != nil { + t.Fatalf("error optimizing query: %v", err) + } + + if len(optimized) != 1 { + t.Fatalf("expected 1 query, got %d", len(optimized)) + } + + assert.Equal(t, tt.expected, optimized[0].SelectCommand) + }) + } +} diff --git a/quesma/optimize/trunc_date.go b/quesma/optimize/trunc_date.go index b311124f6..af4d2e105 100644 --- a/quesma/optimize/trunc_date.go +++ b/quesma/optimize/trunc_date.go @@ -166,10 +166,11 @@ func (s *truncateDate) IsEnabledByDefault() bool { return false } -func (s *truncateDate) Transform(queries []*model.Query) ([]*model.Query, error) { +func (s *truncateDate) Transform(queries []*model.Query, properties map[string]string) ([]*model.Query, error) { for k, query := range queries { - visitor, processor := newTruncDate(s.truncateTo) + + visitor, processor := newTruncDate(s.truncateTo) // read from properties result := query.SelectCommand.Accept(visitor).(*model.SelectCommand) diff --git a/quesma/quesma/config/config.go b/quesma/quesma/config/config.go index 4bb0d9c12..2215ff12b 100644 --- a/quesma/quesma/config/config.go +++ b/quesma/quesma/config/config.go @@ -44,7 +44,6 @@ type QuesmaConfiguration struct { PublicTcpPort network.Port `koanf:"port"` IngestStatistics bool `koanf:"ingestStatistics"` QuesmaInternalTelemetryUrl *Url `koanf:"internalTelemetryUrl"` - EnabledOptimizers OptimizersConfiguration `koanf:"optimizers"` IndexSourceToInternalMappings map[string]IndexMappingsConfiguration `koanf:"indexMappings"` } @@ -65,7 +64,10 @@ type RelationalDbConfiguration struct { AdminUrl *Url `koanf:"adminUrl"` } -type OptimizersConfiguration map[string]bool +type OptimizerConfiguration struct { + Enabled bool `koanf:"enabled"` + Properties map[string]string `koanf:"properties"` +} func (c *RelationalDbConfiguration) IsEmpty() bool { return c != nil && c.Url == nil && c.User == "" && c.Password == "" && c.Database == "" @@ -241,13 +243,16 @@ func (c *QuesmaConfiguration) WritesToElasticsearch() bool { return c.Mode != ClickHouse } -func (c *QuesmaConfiguration) optimizersConfigAsString(s string, cfg OptimizersConfiguration) string { +func (c *QuesmaConfiguration) optimizersConfigAsString(s string, cfg map[string]OptimizerConfiguration) string { var lines []string lines = append(lines, fmt.Sprintf(" %s:", s)) for k, v := range cfg { - lines = append(lines, fmt.Sprintf(" %s: %v", k, v)) + lines = append(lines, fmt.Sprintf(" %s: %v", k, v.Enabled)) + if v.Properties != nil && len(v.Properties) > 0 { + lines = append(lines, fmt.Sprintf(" properties: %v", v.Properties)) + } } return strings.Join(lines, "\n") @@ -259,8 +264,6 @@ func (c *QuesmaConfiguration) OptimizersConfigAsString() string { lines = append(lines, "\n") - lines = append(lines, c.optimizersConfigAsString("Global", c.EnabledOptimizers)) - for indexName, indexConfig := range c.IndexConfig { if indexConfig.EnabledOptimizers != nil && len(indexConfig.EnabledOptimizers) > 0 { lines = append(lines, c.optimizersConfigAsString(indexName, indexConfig.EnabledOptimizers)) diff --git a/quesma/quesma/config/index_config.go b/quesma/quesma/config/index_config.go index cbc3b36c5..41f3416c7 100644 --- a/quesma/quesma/config/index_config.go +++ b/quesma/quesma/config/index_config.go @@ -23,8 +23,8 @@ type IndexConfiguration struct { TimestampField *string `koanf:"timestampField"` // this is hidden from the user right now // deprecated - SchemaConfiguration *SchemaConfiguration `koanf:"static-schema"` - EnabledOptimizers OptimizersConfiguration `koanf:"optimizers"` + SchemaConfiguration *SchemaConfiguration `koanf:"static-schema"` + EnabledOptimizers map[string]OptimizerConfiguration `koanf:"optimizers"` } func (c IndexConfiguration) HasFullTextField(fieldName string) bool {