Repository: VictoriaMetrics/VictoriaMetrics
Pull request #1647 (2021-09-23 13:50:27)

app/vmselect: make sorting for query result similar to Prometheus
Updated sorting allows to get the order of series in result similar or equal
to what Prometheus returns.
The change is needed for compatibility reasons.


Patch:
diff --git a/app/vmagent/README.md b/app/vmagent/README.md
index cd2fc3b8f..d630d6f34 100644
--- a/app/vmagent/README.md
+++ b/app/vmagent/README.md
@@ -750,7 +750,7 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
     	The maximum size of scrape response in bytes to process from Prometheus targets. Bigger responses are rejected
     	Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 16777216)
   -promscrape.noStaleMarkers
-    	Whether to disable seding Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. See also https://docs.victoriametrics.com/vmagent.html#stream-parsing-mode
+    	Whether to disable sending Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. See also https://docs.victoriametrics.com/vmagent.html#stream-parsing-mode
   -promscrape.openstackSDCheckInterval duration
     	Interval for checking for changes in openstack API server. This works only if openstack_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config for details (default 30s)
   -promscrape.seriesLimitPerTarget int
diff --git a/app/vmalert/config/config.go b/app/vmalert/config/config.go
index 8a21cc90b..ce873ccf7 100644
--- a/app/vmalert/config/config.go
+++ b/app/vmalert/config/config.go
@@ -24,7 +24,7 @@ type Group struct {
 	Type        datasource.Type `yaml:"type,omitempty"`
 	File        string
 	Name        string             `yaml:"name"`
-	Interval    utils.PromDuration `yaml:"interval,omitempty"`
+	Interval    utils.PromDuration `yaml:"interval"`
 	Rules       []Rule             `yaml:"rules"`
 	Concurrency int                `yaml:"concurrency"`
 	// ExtraFilterLabels is a list label filters applied to every rule
diff --git a/app/vmalert/group.go b/app/vmalert/group.go
index 1e5e8c8ac..e80b1e6e8 100644
--- a/app/vmalert/group.go
+++ b/app/vmalert/group.go
@@ -190,6 +190,9 @@ func (g *Group) updateWith(newGroup *Group) error {
 	for _, nr := range rulesRegistry {
 		newRules = append(newRules, nr)
 	}
+	// note that g.Interval is not updated here
+	// so the value can be compared later in
+	// group.Start function
 	g.Type = newGroup.Type
 	g.Concurrency = newGroup.Concurrency
 	g.ExtraFilterLabels = newGroup.ExtraFilterLabels
diff --git a/app/vmctl/README.md b/app/vmctl/README.md
index 6ced12294..5f8ed187e 100644
--- a/app/vmctl/README.md
+++ b/app/vmctl/README.md
@@ -479,8 +479,8 @@ To avoid such situation try to filter out VM process metrics via `--vm-native-fi
 [Backfilling tips](https://github.com/VictoriaMetrics/VictoriaMetrics#backfilling) section.
 3. `vmctl` doesn't provide relabeling or other types of labels management in this mode.
 Instead, use [relabeling in VictoriaMetrics](https://github.com/VictoriaMetrics/vmctl/issues/4#issuecomment-683424375).
-4. When importing into cluster version it is additionally required to specify the `--vm-account-id` flag.
-See more details for cluster version [here](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster).
+4. When importing in or from cluster version remember to use correct [URL format](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#url-format)
+and specify `accountID` param.
 
 ## Tuning
 
diff --git a/app/vmselect/promql/binary_op.go b/app/vmselect/promql/binary_op.go
index 512d32663..37300ab9c 100644
--- a/app/vmselect/promql/binary_op.go
+++ b/app/vmselect/promql/binary_op.go
@@ -303,12 +303,18 @@ func binaryOpAnd(bfa *binaryOpFuncArg) ([]*timeseries, error) {
 		if tssLeft == nil {
 			continue
 		}
-		// Add gaps to tssLeft if there are gaps at valuesRight.
-		valuesRight := tssRight[0].Values
+		// Add gaps to tssLeft if there are gaps at tssRight.
 		for _, tsLeft := range tssLeft {
 			valuesLeft := tsLeft.Values
-			for i, v := range valuesRight {
-				if math.IsNaN(v) {
+			for i := range valuesLeft {
+				hasValue := false
+				for _, tsRight := range tssRight {
+					if !math.IsNaN(tsRight.Values[i]) {
+						hasValue = true
+						break
+					}
+				}
+				if !hasValue {
 					valuesLeft[i] = nan
 				}
 			}
@@ -333,12 +339,18 @@ func binaryOpOr(bfa *binaryOpFuncArg) ([]*timeseries, error) {
 		}
 		// Fill gaps in tssLeft with values from tssRight as Prometheus does.
 		// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/552
-		valuesRight := tssRight[0].Values
 		for _, tsLeft := range tssLeft {
 			valuesLeft := tsLeft.Values
 			for i, v := range valuesLeft {
-				if math.IsNaN(v) {
-					valuesLeft[i] = valuesRight[i]
+				if !math.IsNaN(v) {
+					continue
+				}
+				for _, tsRight := range tssRight {
+					vRight := tsRight.Values[i]
+					if !math.IsNaN(vRight) {
+						valuesLeft[i] = vRight
+						break
+					}
 				}
 			}
 		}
@@ -355,13 +367,15 @@ func binaryOpUnless(bfa *binaryOpFuncArg) ([]*timeseries, error) {
 			rvs = append(rvs, tssLeft...)
 			continue
 		}
-		// Add gaps to tssLeft if the are no gaps at valuesRight.
-		valuesRight := tssRight[0].Values
+		// Add gaps to tssLeft if the are no gaps at tssRight.
 		for _, tsLeft := range tssLeft {
 			valuesLeft := tsLeft.Values
-			for i, v := range valuesRight {
-				if !math.IsNaN(v) {
-					valuesLeft[i] = nan
+			for i := range valuesLeft {
+				for _, tsRight := range tssRight {
+					if !math.IsNaN(tsRight.Values[i]) {
+						valuesLeft[i] = nan
+						break
+					}
 				}
 			}
 		}
diff --git a/app/vmselect/promql/exec.go b/app/vmselect/promql/exec.go
index daa5b4f22..b533872e8 100644
--- a/app/vmselect/promql/exec.go
+++ b/app/vmselect/promql/exec.go
@@ -94,7 +94,7 @@ func timeseriesToResult(tss []*timeseries, maySort bool) ([]netstorage.Result, e
 	m := make(map[string]struct{}, len(tss))
 	bb := bbPool.Get()
 	for i, ts := range tss {
-		bb.B = marshalMetricNameSorted(bb.B[:0], &ts.MetricName)
+		bb.B = metricNameToBytes(bb.B[:0], &ts.MetricName)
 		if _, ok := m[string(bb.B)]; ok {
 			return nil, fmt.Errorf(`duplicate output timeseries: %s`, stringMetricName(&ts.MetricName))
 		}
diff --git a/app/vmselect/promql/timeseries.go b/app/vmselect/promql/timeseries.go
index b9c35ff40..fa30349aa 100644
--- a/app/vmselect/promql/timeseries.go
+++ b/app/vmselect/promql/timeseries.go
@@ -318,6 +318,18 @@ func marshalMetricTagsFast(dst []byte, tags []storage.Tag) []byte {
 	return dst
 }
 
+func metricNameToBytes(dst []byte, mn *storage.MetricName) []byte {
+	dst = marshalBytesFast(dst, mn.MetricGroup)
+	sortMetricTags(mn.Tags)
+	for i := range mn.Tags {
+		tag := &mn.Tags[i]
+		dst = append(dst, tag.Key...)
+		dst = append(dst, tag.Value...)
+		dst = append(dst, ","...)
+	}
+	return dst
+}
+
 func marshalMetricNameSorted(dst []byte, mn *storage.MetricName) []byte {
 	dst = marshalBytesFast(dst, mn.MetricGroup)
 	sortMetricTags(mn.Tags)
diff --git a/deployment/docker/Makefile b/deployment/docker/Makefile
index 0315d2e35..61b859387 100644
--- a/deployment/docker/Makefile
+++ b/deployment/docker/Makefile
@@ -4,7 +4,7 @@ DOCKER_NAMESPACE := victoriametrics
 
 ROOT_IMAGE ?= alpine:3.14.2
 CERTS_IMAGE := alpine:3.14.2
-GO_BUILDER_IMAGE := golang:1.17.1
+GO_BUILDER_IMAGE := golang:1.17.1-alpine
 BUILDER_IMAGE := local/builder:2.0.0-$(shell echo $(GO_BUILDER_IMAGE) | tr : _)
 BASE_IMAGE := local/base:1.1.3-$(shell echo $(ROOT_IMAGE) | tr : _)-$(shell echo $(CERTS_IMAGE) | tr : _)
 
@@ -36,7 +36,7 @@ app-via-docker: package-builder
 		$(BUILDER_IMAGE) \
 		go build $(RACE) -mod=vendor -trimpath \
 			-ldflags "-extldflags '-static' $(GO_BUILDINFO)" \
-			-tags 'netgo osusergo nethttpomithttp2' \
+			-tags 'netgo osusergo nethttpomithttp2 musl' \
 			-o bin/$(APP_NAME)$(APP_SUFFIX)-prod $(PKG_PREFIX)/app/$(APP_NAME)
 
 app-via-docker-windows: package-builder
diff --git a/deployment/docker/builder/Dockerfile b/deployment/docker/builder/Dockerfile
index d156d4248..c3af52c50 100644
--- a/deployment/docker/builder/Dockerfile
+++ b/deployment/docker/builder/Dockerfile
@@ -1,3 +1,4 @@
 ARG go_builder_image
 FROM $go_builder_image
 STOPSIGNAL SIGINT
+RUN apk add gcc musl-dev make --no-cache
\ No newline at end of file
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 6373cbe80..47c6c5a06 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -6,8 +6,16 @@ sort: 15
 
 ## tip
 
+* BUGFIX: align behavior of the queries `a or on (labels) b`, `a and on (labels) b` and `a unless on (labels) b` where `b` has multiple time series with the given `labels` to Prometheus behavior. See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/1643).
+
+
+## [v1.66.2](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.66.2)
+
 * FEATURE: vmagent: add `vm_promscrape_max_scrape_size_exceeded_errors_total` metric for counting of the failed scrapes due to the exceeded response size (the response size limit can be configured via `-promscrape.maxScrapeSize` command-line flag). See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1639).
 
+* BUGFIX: vmalert: properly reload rule groups if only the `interval` config option is changed. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1641).
+* BUGFIX: properly handle `{__name__=~"prefix(suffix1|suffix2)",other_label="..."}` queries. They may return unexpected empty responses since v1.66.0. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1644).
+
 
 ## [v1.66.1](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.66.1)
 
diff --git a/docs/vmagent.md b/docs/vmagent.md
index 340a83503..66b5398b3 100644
--- a/docs/vmagent.md
+++ b/docs/vmagent.md
@@ -754,7 +754,7 @@ See the docs at https://docs.victoriametrics.com/vmagent.html .
     	The maximum size of scrape response in bytes to process from Prometheus targets. Bigger responses are rejected
     	Supports the following optional suffixes for size values: KB, MB, GB, KiB, MiB, GiB (default 16777216)
   -promscrape.noStaleMarkers
-    	Whether to disable seding Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. See also https://docs.victoriametrics.com/vmagent.html#stream-parsing-mode
+    	Whether to disable sending Prometheus stale markers for metrics when scrape target disappears. This option may reduce memory usage if stale markers aren't needed for your setup. See also https://docs.victoriametrics.com/vmagent.html#stream-parsing-mode
   -promscrape.openstackSDCheckInterval duration
     	Interval for checking for changes in openstack API server. This works only if openstack_sd_configs is configured in '-promscrape.config' file. See https://prometheus.io/docs/prometheus/latest/configuration/configuration/#openstack_sd_config for details (default 30s)
   -promscrape.seriesLimitPerTarget int
diff --git a/docs/vmctl.md b/docs/vmctl.md
index 49ba085fb..ac12b8acd 100644
--- a/docs/vmctl.md
+++ b/docs/vmctl.md
@@ -483,8 +483,8 @@ To avoid such situation try to filter out VM process metrics via `--vm-native-fi
 [Backfilling tips](https://github.com/VictoriaMetrics/VictoriaMetrics#backfilling) section.
 3. `vmctl` doesn't provide relabeling or other types of labels management in this mode.
 Instead, use [relabeling in VictoriaMetrics](https://github.com/VictoriaMetrics/vmctl/issues/4#issuecomment-683424375).
-4. When importing into cluster version it is additionally required to specify the `--vm-account-id` flag.
-See more details for cluster version [here](https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster).
+4. When importing in or from cluster version remember to use correct [URL format](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html#url-format)
+and specify `accountID` param.
 
 ## Tuning
 
diff --git a/lib/storage/tag_filters.go b/lib/storage/tag_filters.go
index 5511d3fd7..5087d5fbb 100644
--- a/lib/storage/tag_filters.go
+++ b/lib/storage/tag_filters.go
@@ -29,6 +29,7 @@ func convertToCompositeTagFilters(tfs *TagFilters) []*TagFilters {
 	var tfssCompiled []*TagFilters
 	// Search for filters on metric name, which will be used for creating composite filters.
 	var names [][]byte
+	namePrefix := ""
 	hasPositiveFilter := false
 	for _, tf := range tfs.tfs {
 		if len(tf.key) == 0 {
@@ -42,6 +43,7 @@ func convertToCompositeTagFilters(tfs *TagFilters) []*TagFilters {
 				for _, orSuffix := range tf.orSuffixes {
 					names = append(names, []byte(orSuffix))
 				}
+				namePrefix = tf.regexpPrefix
 			}
 		} else if !tf.isNegative && !tf.isEmptyMatch {
 			hasPositiveFilter = true
@@ -53,7 +55,7 @@ func convertToCompositeTagFilters(tfs *TagFilters) []*TagFilters {
 	}
 
 	// Create composite filters for the found names.
-	var compositeKey []byte
+	var compositeKey, nameWithPrefix []byte
 	for _, name := range names {
 		compositeFilters := 0
 		tfsNew := make([]tagFilter, 0, len(tfs.tfs))
@@ -94,7 +96,9 @@ func convertToCompositeTagFilters(tfs *TagFilters) []*TagFilters {
 				continue
 			}
 			// Create composite filter on (name, tf)
-			compositeKey = marshalCompositeTagKey(compositeKey[:0], name, tf.key)
+			nameWithPrefix = append(nameWithPrefix[:0], namePrefix...)
+			nameWithPrefix = append(nameWithPrefix, name...)
+			compositeKey = marshalCompositeTagKey(compositeKey[:0], nameWithPrefix, tf.key)
 			var tfNew tagFilter
 			if err := tfNew.Init(tfs.commonPrefix, compositeKey, tf.value, tf.isNegative, tf.isRegexp); err != nil {
 				logger.Panicf("BUG: unexpected error when creating composite tag filter for name=%q and key=%q: %s", name, tf.key, err)
@@ -229,13 +233,18 @@ type tagFilter struct {
 	// matchCost is a cost for matching a filter against a single string.
 	matchCost uint64
 
+	// contains the prefix for regexp filter if isRegexp==true.
+	regexpPrefix string
+
 	// Prefix always contains {nsPrefixTagToMetricIDs, key}.
 	// Additionally it contains:
 	//  - value if !isRegexp.
-	//  - non-regexp prefix if isRegexp.
+	//  - regexpPrefix if isRegexp.
 	prefix []byte
 
-	// or values obtained from regexp suffix if it equals to "foo|bar|..."
+	// `or` values obtained from regexp suffix if it equals to "foo|bar|..."
+	//
+	// the regexp prefix is stored in regexpPrefix.
 	//
 	// This array is also populated with matching Graphite metrics if key="__graphite__"
 	orSuffixes []string
@@ -334,6 +343,7 @@ func (tf *tagFilter) InitFromGraphiteQuery(commonPrefix, query []byte, paths []s
 	tf.value = append(tf.value[:0], query...)
 	tf.isNegative = isNegative
 	tf.isRegexp = true // this is needed for tagFilter.matchSuffix
+	tf.regexpPrefix = prefix
 	tf.prefix = append(tf.prefix[:0], commonPrefix...)
 	tf.prefix = marshalTagValue(tf.prefix, nil)
 	tf.prefix = marshalTagValueNoTrailingTagSeparator(tf.prefix, []byte(prefix))
@@ -379,6 +389,7 @@ func (tf *tagFilter) Init(commonPrefix, key, value []byte, isNegative, isRegexp
 	tf.isRegexp = isRegexp
 	tf.matchCost = 0
 
+	tf.regexpPrefix = ""
 	tf.prefix = tf.prefix[:0]
 
 	tf.orSuffixes = tf.orSuffixes[:0]
@@ -396,6 +407,8 @@ func (tf *tagFilter) Init(commonPrefix, key, value []byte, isNegative, isRegexp
 		if len(expr) == 0 {
 			tf.value = append(tf.value[:0], prefix...)
 			tf.isRegexp = false
+		} else {
+			tf.regexpPrefix = string(prefix)
 		}
 	}
 	tf.prefix = marshalTagValueNoTrailingTagSeparator(tf.prefix, prefix)


Test patch:
diff --git a/app/vmalert/config/config_test.go b/app/vmalert/config/config_test.go
index bb7f1e4a5..5a2bbea94 100644
--- a/app/vmalert/config/config_test.go
+++ b/app/vmalert/config/config_test.go
@@ -436,7 +436,7 @@ rules:
 `)
 	})
 
-	t.Run("Ok, `for` must change cs", func(t *testing.T) {
+	t.Run("`for` change", func(t *testing.T) {
 		f(t, `
 name: TestGroup
 rules:
@@ -450,5 +450,34 @@ rules:
     expr: sum by(job) (up == 1)
 `)
 	})
-
+	t.Run("`interval` change", func(t *testing.T) {
+		f(t, `
+name: TestGroup
+interval: 2s
+rules:
+  - alert: ExampleAlertWithFor
+    expr: sum by(job) (up == 1)
+`, `
+name: TestGroup
+interval: 4s
+rules:
+  - alert: ExampleAlertWithFor
+    expr: sum by(job) (up == 1)
+`)
+	})
+	t.Run("`concurrency` change", func(t *testing.T) {
+		f(t, `
+name: TestGroup
+concurrency: 2
+rules:
+  - alert: ExampleAlertWithFor
+    expr: sum by(job) (up == 1)
+`, `
+name: TestGroup
+concurrency: 16
+rules:
+  - alert: ExampleAlertWithFor
+    expr: sum by(job) (up == 1)
+`)
+	})
 }
diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go
index f0c6fe446..c3d7ee57b 100644
--- a/app/vmselect/promql/exec_test.go
+++ b/app/vmselect/promql/exec_test.go
@@ -1,6 +1,7 @@
 package promql
 
 import (
+	"fmt"
 	"testing"
 	"time"
 
@@ -6954,7 +6955,8 @@ func TestExecSuccess(t *testing.T) {
 				Value: []byte("10"),
 			},
 		}
-		resultExpected := []netstorage.Result{r1, r2, r3, r4}
+		// expected sorted output for strings 1, 10, 2, 3
+		resultExpected := []netstorage.Result{r1, r4, r2, r3}
 		f(q, resultExpected)
 	})
 	t.Run(`count_values without (baz)`, func(t *testing.T) {
@@ -7008,6 +7010,44 @@ func TestExecSuccess(t *testing.T) {
 		resultExpected := []netstorage.Result{r1, r2, r3}
 		f(q, resultExpected)
 	})
+	t.Run(`result sorting`, func(t *testing.T) {
+		t.Parallel()
+		q := `label_set(1, "instance", "localhost:1001", "type", "free")
+			or label_set(1, "instance", "localhost:1001", "type", "buffers")
+			or label_set(1, "instance", "localhost:1000", "type", "buffers")
+			or label_set(1, "instance", "localhost:1000", "type", "free")
+`
+		r1 := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{1, 1, 1, 1, 1, 1},
+			Timestamps: timestampsExpected,
+		}
+		testAddLabels(t, &r1.MetricName,
+			"instance", "localhost:1000", "type", "buffers")
+		r2 := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{1, 1, 1, 1, 1, 1},
+			Timestamps: timestampsExpected,
+		}
+		testAddLabels(t, &r2.MetricName,
+			"instance", "localhost:1000", "type", "free")
+		r3 := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{1, 1, 1, 1, 1, 1},
+			Timestamps: timestampsExpected,
+		}
+		testAddLabels(t, &r3.MetricName,
+			"instance", "localhost:1001", "type", "buffers")
+		r4 := netstorage.Result{
+			MetricName: metricNameExpected,
+			Values:     []float64{1, 1, 1, 1, 1, 1},
+			Timestamps: timestampsExpected,
+		}
+		testAddLabels(t, &r4.MetricName,
+			"instance", "localhost:1001", "type", "free")
+		resultExpected := []netstorage.Result{r1, r2, r3, r4}
+		f(q, resultExpected)
+	})
 }
 
 func TestExecError(t *testing.T) {
@@ -7285,3 +7325,16 @@ func testMetricNamesEqual(t *testing.T, mn, mnExpected *storage.MetricName, pos
 		}
 	}
 }
+
+func testAddLabels(t *testing.T, mn *storage.MetricName, labels ...string) {
+	t.Helper()
+	if len(labels)%2 > 0 {
+		t.Fatalf("uneven number of labels passed: %v", labels)
+	}
+	for i := 0; i < len(labels); i += 2 {
+		mn.Tags = append(mn.Tags, storage.Tag{
+			Key:   []byte(labels[i]),
+			Value: []byte(labels[i+1]),
+		})
+	}
+}
diff --git a/lib/storage/tag_filters_test.go b/lib/storage/tag_filters_test.go
index 9fe164f86..820ba46d6 100644
--- a/lib/storage/tag_filters_test.go
+++ b/lib/storage/tag_filters_test.go
@@ -410,6 +410,44 @@ func TestConvertToCompositeTagFilters(t *testing.T) {
 		},
 	})
 
+	// Multiple values regexp filter, which can be converted to non-regexp.
+	f([]TagFilter{
+		{
+			Key:        nil,
+			Value:      []byte("bar|foo"),
+			IsNegative: false,
+			IsRegexp:   true,
+		},
+	}, [][]TagFilter{
+		{
+			{
+				Key:        nil,
+				Value:      []byte("bar|foo"),
+				IsNegative: false,
+				IsRegexp:   true,
+			},
+		},
+	})
+
+	// Multiple values regexp filter with common prefix, which can be converted to non-regexp.
+	f([]TagFilter{
+		{
+			Key:        nil,
+			Value:      []byte("xxx(bar|foo)"),
+			IsNegative: false,
+			IsRegexp:   true,
+		},
+	}, [][]TagFilter{
+		{
+			{
+				Key:        nil,
+				Value:      []byte("xxx(bar|foo)"),
+				IsNegative: false,
+				IsRegexp:   true,
+			},
+		},
+	})
+
 	// Multiple values regexp filter, which can be converted to non-regexp, with non-name filter.
 	f([]TagFilter{
 		{
@@ -443,6 +481,39 @@ func TestConvertToCompositeTagFilters(t *testing.T) {
 		},
 	})
 
+	// Multiple values regexp filter with common prefix, which can be converted to non-regexp, with non-name filter.
+	f([]TagFilter{
+		{
+			Key:        nil,
+			Value:      []byte("xxx(bar|foox)"),
+			IsNegative: false,
+			IsRegexp:   true,
+		},
+		{
+			Key:        []byte("foo"),
+			Value:      []byte("abc"),
+			IsNegative: false,
+			IsRegexp:   false,
+		},
+	}, [][]TagFilter{
+		{
+			{
+				Key:        []byte("\xfe\x06xxxbarfoo"),
+				Value:      []byte("abc"),
+				IsNegative: false,
+				IsRegexp:   false,
+			},
+		},
+		{
+			{
+				Key:        []byte("\xfe\x07xxxfooxfoo"),
+				Value:      []byte("abc"),
+				IsNegative: false,
+				IsRegexp:   false,
+			},
+		},
+	})
+
 	// Two multiple values regexp filter, which can be converted to non-regexp, with non-name filter.
 	f([]TagFilter{
 		{