Skip to content
This repository was archived by the owner on Nov 7, 2025. It is now read-only.

Commit 5a41028

Browse files
authored
A/B testing - fixes (#963)
Some fixes.
1 parent 86050ac commit 5a41028

File tree

8 files changed

+108
-36
lines changed

8 files changed

+108
-36
lines changed

quesma/ab_testing/collector/diff.go

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,33 +37,60 @@ func (t *diffTransformer) mostCommonMismatchType(mismatches []jsondiff.JSONMisma
3737

3838
func (t *diffTransformer) process(in EnrichedResults) (out EnrichedResults, drop bool, err error) {
3939

40+
mismatches := jsondiff.Mismatches{}
41+
4042
d, err := jsondiff.NewElasticResponseJSONDiff()
4143
if err != nil {
4244
return in, false, err
4345
}
4446

45-
jsonA, err := types.ParseJSON(in.A.Body)
46-
if err != nil {
47-
in.Mismatch.IsOK = false
48-
in.Mismatch.Message = fmt.Sprintf("failed to parse A response: %v", err)
49-
err = fmt.Errorf("failed to parse A response: %w", err)
50-
in.Errors = append(in.Errors, err.Error())
51-
return in, false, nil
52-
}
47+
if in.A.Error != "" || in.B.Error != "" {
5348

54-
jsonB, err := types.ParseJSON(in.B.Body)
55-
if err != nil {
56-
in.Mismatch.IsOK = false
57-
in.Mismatch.Message = fmt.Sprintf("failed to parse B response: %v", err)
58-
err = fmt.Errorf("failed to parse B response: %w", err)
59-
in.Errors = append(in.Errors, err.Error())
60-
return in, false, nil
61-
}
49+
if in.A.Error != "" {
50+
mismatches = append(mismatches, jsondiff.JSONMismatch{
51+
Type: "error",
52+
Message: fmt.Sprintf("\nA response has an error: %s", in.A.Error),
53+
Path: "n/a",
54+
Expected: "n/a",
55+
Actual: "n/a",
56+
})
57+
}
58+
59+
if in.B.Error != "" {
60+
mismatches = append(mismatches, jsondiff.JSONMismatch{
61+
Type: "error",
62+
Message: fmt.Sprintf("\nB response has an error: %s", in.B.Error),
63+
Path: "n/a",
64+
Expected: "n/a",
65+
Actual: "n/a",
66+
})
67+
}
6268

63-
mismatches, err := d.Diff(jsonA, jsonB)
69+
} else {
70+
71+
jsonA, err := types.ParseJSON(in.A.Body)
72+
if err != nil {
73+
in.Mismatch.IsOK = false
74+
in.Mismatch.Message = fmt.Sprintf("failed to parse A response: %v", err)
75+
err = fmt.Errorf("failed to parse A response: %w", err)
76+
in.Errors = append(in.Errors, err.Error())
77+
return in, false, nil
78+
}
79+
80+
jsonB, err := types.ParseJSON(in.B.Body)
81+
if err != nil {
82+
in.Mismatch.IsOK = false
83+
in.Mismatch.Message = fmt.Sprintf("failed to parse B response: %v", err)
84+
err = fmt.Errorf("failed to parse B response: %w", err)
85+
in.Errors = append(in.Errors, err.Error())
86+
return in, false, nil
87+
}
88+
89+
mismatches, err = d.Diff(jsonA, jsonB)
90+
if err != nil {
91+
return in, false, err
92+
}
6493

65-
if err != nil {
66-
return in, false, err
6794
}
6895

6996
if len(mismatches) > 0 {

quesma/jsondiff/elastic_response_diff.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,22 @@ import "fmt"
66

77
// NewElasticResponseJSONDiff creates a JSONDiff instance that is tailored to compare Elasticsearch response JSONs.
88
func NewElasticResponseJSONDiff() (*JSONDiff, error) {
9-
d, err := NewJSONDiff("^id$", ".*Quesma_key_.*", "^took$", ".*__quesma_total_count", ".*\\._id", "^_shards.*", ".*\\._score", ".*\\._source", ".*\\.__quesma_originalKey")
9+
10+
var ignorePaths []string
11+
12+
// quesma specific fields that we want to ignore
13+
ignorePaths = append(ignorePaths, ".*Quesma_key_.*", ".*__quesma_total_count", ".*\\.__quesma_originalKey")
14+
15+
// well known fields that we want to ignore
16+
ignorePaths = append(ignorePaths, "^id$", "^took$", ".*\\._id", "^_shards.*", ".*\\._score", ".*\\._source", ".*\\._version$")
17+
18+
// elastic has some fields that are suffixed with ".keyword" that we want to ignore
19+
ignorePaths = append(ignorePaths, ".*\\.keyword$")
20+
21+
// ignore some fields that are related to location, just for now (we want to compare them in the future)
22+
ignorePaths = append(ignorePaths, ".*Location$", ".*\\.lat$", ".*\\.lon$")
23+
24+
d, err := NewJSONDiff(ignorePaths...)
1025

1126
if err != nil {
1227
return nil, fmt.Errorf("could not create JSONDiff: %v", err)

quesma/jsondiff/jsondiff.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -325,10 +325,10 @@ func (d *JSONDiff) compareArray(expected []any, actual []any) {
325325
}
326326

327327
if lenDiff > 1 {
328-
d.addMismatch(invalidArrayLength, fmt.Sprintf("%d", len(actual)), fmt.Sprintf("%d", len(expected)))
328+
d.addMismatch(invalidArrayLength, fmt.Sprintf("%d", len(expected)), fmt.Sprintf("%d", len(actual)))
329329
return
330330
} else if lenDiff == 1 {
331-
d.addMismatch(invalidArrayLengthOffByOne, fmt.Sprintf("%d", len(actual)), fmt.Sprintf("%d", len(expected)))
331+
d.addMismatch(invalidArrayLengthOffByOne, fmt.Sprintf("%d", len(expected)), fmt.Sprintf("%d", len(actual)))
332332
return
333333
}
334334

@@ -345,7 +345,7 @@ func (d *JSONDiff) compareArray(expected []any, actual []any) {
345345

346346
for i := range len(actual) {
347347
d.pushPath(fmt.Sprintf("[%d]", i))
348-
d.compare(actual[i], expected[i])
348+
d.compare(expected[i], actual[i])
349349
d.popPath()
350350
}
351351
}
@@ -361,10 +361,10 @@ func (d *JSONDiff) asType(a any) string {
361361
var dateRx = regexp.MustCompile(`\d{4}-\d{2}-\d{2}.\d{2}:\d{2}:`)
362362

363363
func (d *JSONDiff) uniformTimeFormat(date string) string {
364-
returnFormat := "2006-01-02T15:04:05.000Z"
364+
returnFormat := time.RFC3339Nano
365365

366366
inputFormats := []string{
367-
"2006-01-02T15:04:05.000+02:00",
367+
"2006-01-02T15:04:05.000-07:00",
368368
"2006-01-02T15:04:05.000Z",
369369
"2006-01-02T15:04:05.000",
370370
"2006-01-02 15:04:05",
@@ -375,7 +375,7 @@ func (d *JSONDiff) uniformTimeFormat(date string) string {
375375
for _, format := range inputFormats {
376376
parsedDate, err = time.Parse(format, date)
377377
if err == nil {
378-
return parsedDate.Format(returnFormat)
378+
return parsedDate.UTC().Format(returnFormat)
379379
}
380380
}
381381
return date

quesma/jsondiff/jsondiff_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,15 @@ func TestJSONDiff(t *testing.T) {
132132
problems: []JSONMismatch{},
133133
},
134134
{
135-
name: "SKIP dates 3", // TODO fix this, not sure how we handle TZ
135+
name: "dates 3",
136136
expected: `{"a": "2024-10-24T10:00:00.000"}`,
137137
actual: `{"a": "2024-10-24T12:00:00.000+02:00"}`,
138138
},
139+
{
140+
name: "dates 4",
141+
expected: `{"a": "2024-10-31T11:00:00.000"}`,
142+
actual: `{"a": "2024-10-31T12:00:00.000+01:00"}`,
143+
},
139144
}
140145

141146
for _, tt := range tests {

quesma/quesma/schema_transformer.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,15 @@ func (s *SchemaCheckPass) applyWildcardExpansion(indexSchema schema.Schema, quer
470470
cols = append(cols, col.InternalPropertyName.AsString())
471471
}
472472
}
473+
474+
if query.RuntimeMappings != nil {
475+
// add columns that are not in the schema but are in the runtime mappings
476+
// these columns will be transformed later
477+
for name := range query.RuntimeMappings {
478+
cols = append(cols, name)
479+
}
480+
}
481+
473482
sort.Strings(cols)
474483

475484
for _, col := range cols {
@@ -683,10 +692,22 @@ func (s *SchemaCheckPass) applyRuntimeMappings(indexSchema schema.Schema, query
683692
return query, nil
684693
}
685694

686-
visitor := model.NewBaseVisitor()
695+
cols := query.SelectCommand.Columns
687696

688-
visitor.OverrideVisitColumnRef = func(b *model.BaseExprVisitor, e model.ColumnRef) interface{} {
697+
// replace column refs with runtime mappings with proper name
698+
for i, col := range cols {
699+
switch c := col.(type) {
700+
case model.ColumnRef:
701+
if mapping, ok := query.RuntimeMappings[c.ColumnName]; ok {
702+
cols[i] = model.NewAliasedExpr(mapping.Expr, c.ColumnName)
703+
}
704+
}
705+
}
706+
query.SelectCommand.Columns = cols
689707

708+
// replace other places where column refs are used
709+
visitor := model.NewBaseVisitor()
710+
visitor.OverrideVisitColumnRef = func(b *model.BaseExprVisitor, e model.ColumnRef) interface{} {
690711
if mapping, ok := query.RuntimeMappings[e.ColumnName]; ok {
691712
return mapping.Expr
692713
}

quesma/quesma/search_ab_testing.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"quesma/elasticsearch"
1414
"quesma/logger"
1515
"quesma/model"
16-
"quesma/queryparser"
1716
"quesma/quesma/async_search_storage"
1817
"quesma/quesma/config"
1918
"quesma/quesma/recovery"
@@ -330,7 +329,10 @@ func (q *QueryRunner) storeAsyncSearchWithRaw(qmc *ui.QuesmaManagementConsole, i
330329
asyncResponse := WrapElasticResponseAsAsync(resultJSON, asyncId, false, &okStatus)
331330
responseBody, err = json.MarshalIndent(asyncResponse, "", " ")
332331
} else {
333-
responseBody, _ = queryparser.EmptyAsyncSearchResponse(asyncId, false, 503)
332+
responseBody, err = resultJSON.Bytes()
333+
if err == nil {
334+
logger.Warn().Msgf("error while marshalling async search response: %v: ", err)
335+
}
334336
err = resultError
335337
}
336338

quesma/quesma/ui/ab_testing.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -497,19 +497,21 @@ func (qmc *QuesmaManagementConsole) renderABTestingMismatch(buffer *builder.Html
497497
buffer.Html(`</code>`)
498498
{ // poor man's HTML indent
499499
buffer.Html(`<ul>`)
500+
500501
buffer.Html(`<li>`)
501502
buffer.Html(`<code>`)
502-
buffer.Text("Actual: ")
503-
buffer.Text(mismatch.Actual)
503+
buffer.Text("Expected: ")
504+
buffer.Text(mismatch.Expected)
504505
buffer.Html(`</code>`)
505506
buffer.Html(`</li>`)
506507

507508
buffer.Html(`<li>`)
508509
buffer.Html(`<code>`)
509-
buffer.Text("Expected: ")
510-
buffer.Text(mismatch.Expected)
510+
buffer.Text("Actual: ")
511+
buffer.Text(mismatch.Actual)
511512
buffer.Html(`</code>`)
512513
buffer.Html(`</li>`)
514+
513515
buffer.Html(`</ul>`)
514516
}
515517
}

quesma/testdata/requests.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2354,7 +2354,7 @@ var TestSearchRuntimeMappings = []SearchTestCase{
23542354
model.ListAllFields,
23552355
////[]model.Query{newSimplestQuery()},
23562356
[]string{
2357-
`SELECT toHour("@timestamp") FROM ` + TableName + ` LIMIT 10`,
2357+
`SELECT toHour("@timestamp") AS "hour_of_day" FROM ` + TableName + ` LIMIT 10`,
23582358
},
23592359
[]string{},
23602360
},

0 commit comments

Comments
 (0)