You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2026/03/31 11:13:11 UTC

(skywalking-banyandb) branch fix/segment-range created (now 9e3332ced)

This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a change to branch fix/segment-range
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git


      at 9e3332ced fix(measure): always enforce query time range in index-mode inverted search

This branch includes the following new commits:

     new 9e3332ced fix(measure): always enforce query time range in index-mode inverted search

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(skywalking-banyandb) 01/01: fix(measure): always enforce query time range in index-mode inverted search

Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch fix/segment-range
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git

commit 9e3332ced0e5715a6b24f3e325e9576b5e8422b9
Author: Hongtao Gao <ha...@gmail.com>
AuthorDate: Mon Mar 30 14:31:42 2026 +0000

    fix(measure): always enforce query time range in index-mode inverted search
    
    BuildQuery silently dropped timeRange when no series matchers were
    provided, causing index-mode queries to return stale documents from
    older segments whose metadata range widened to cover the query window.
    Fix by unifying the no-matchers path in BuildQuery to apply timeRange,
    always passing TimeRange from buildIndexQueryResult instead of omitting
    it for fully-covered segments, and incorporating TimeRange into the
    SeriesSort query path in SearchWithoutSeries.
---
 CHANGES.md                                 |  1 +
 banyand/internal/storage/index.go          | 11 +++-
 banyand/measure/query.go                   |  6 +-
 pkg/index/inverted/inverted_series.go      | 94 +++++++++++++++---------------
 pkg/index/inverted/inverted_series_test.go | 59 +++++++++++++++++++
 5 files changed, 118 insertions(+), 53 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index ab476c04a..643deedac 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -14,6 +14,7 @@ Release Notes.
 ### Bug Fixes
 
 - Fix flaky trace query filtering caused by non-deterministic sidx tag ordering and add consistency checks for integration query cases.
+- Fix index-mode measure queries returning documents outside the requested time range when a widened segment overlaps the query window.
 
 ## 0.10.0
 
diff --git a/banyand/internal/storage/index.go b/banyand/internal/storage/index.go
index ad8afc1ca..dff32d742 100644
--- a/banyand/internal/storage/index.go
+++ b/banyand/internal/storage/index.go
@@ -101,7 +101,7 @@ func (s *seriesIndex) Stats() (dataCount int64, dataSizeBytes int64) {
 func (s *seriesIndex) filter(ctx context.Context, series []*pbv1.Series,
 	projection []index.FieldKey, secondaryQuery index.Query, timeRange *timestamp.TimeRange,
 ) (data SeriesData, err error) {
-	if len(series) == 0 && secondaryQuery == nil {
+	if len(series) == 0 && secondaryQuery == nil && timeRange == nil {
 		return data, nil
 	}
 	var seriesMatchers []index.SeriesMatcher
@@ -343,7 +343,14 @@ func (s *seriesIndex) SearchWithoutSeries(ctx context.Context, opts IndexSearchO
 		}()
 	}
 
-	iter, err := s.store.SeriesSort(ctx, opts.Query, opts.Order,
+	sortQuery := opts.Query
+	if opts.TimeRange != nil {
+		sortQuery, err = s.store.BuildQuery(nil, opts.Query, opts.TimeRange)
+		if err != nil {
+			return sd, nil, err
+		}
+	}
+	iter, err := s.store.SeriesSort(ctx, sortQuery, opts.Order,
 		opts.PreloadSize, opts.Projection)
 	if err != nil {
 		return sd, nil, err
diff --git a/banyand/measure/query.go b/banyand/measure/query.go
index 794d6e6c6..35ddb6e09 100644
--- a/banyand/measure/query.go
+++ b/banyand/measure/query.go
@@ -501,14 +501,10 @@ func (m *measure) buildIndexQueryResult(ctx context.Context, mqo model.MeasureQu
 		Order:       mqo.Order,
 		PreloadSize: preloadSize,
 		Projection:  indexProjection,
+		TimeRange:   mqo.TimeRange,
 	}
 	seriesFilter := roaring.NewPostingList()
 	for i := range segments {
-		if mqo.TimeRange.Include(segments[i].GetTimeRange()) {
-			opts.TimeRange = nil
-		} else {
-			opts.TimeRange = mqo.TimeRange
-		}
 		sr := &segResult{}
 		sr.SeriesData, sr.sortedValues, err = segments[i].IndexDB().SearchWithoutSeries(ctx, opts)
 		if err != nil {
diff --git a/pkg/index/inverted/inverted_series.go b/pkg/index/inverted/inverted_series.go
index e14426ae7..5a64a4283 100644
--- a/pkg/index/inverted/inverted_series.go
+++ b/pkg/index/inverted/inverted_series.go
@@ -139,66 +139,68 @@ func toDoc(d index.Document, toParseFieldNames bool) (*bluge.Document, []string)
 
 // BuildQuery implements index.SeriesStore.
 func (s *store) BuildQuery(seriesMatchers []index.SeriesMatcher, secondaryQuery index.Query, timeRange *timestamp.TimeRange) (index.Query, error) {
-	if len(seriesMatchers) == 0 {
+	if len(seriesMatchers) == 0 && timeRange == nil {
 		return secondaryQuery, nil
 	}
 
-	qs := make([]bluge.Query, len(seriesMatchers))
-	nodes := make([]node, len(seriesMatchers))
-	for i := range seriesMatchers {
-		switch seriesMatchers[i].Type {
-		case index.SeriesMatcherTypeExact:
-			match := convert.BytesToString(seriesMatchers[i].Match)
-			q := bluge.NewTermQuery(match)
-			q.SetField(docIDField)
-			qs[i] = q
-			nodes = append(nodes, newTermNode(match, nil))
-		case index.SeriesMatcherTypePrefix:
-			match := convert.BytesToString(seriesMatchers[i].Match)
-			q := bluge.NewPrefixQuery(match)
-			q.SetField(docIDField)
-			qs[i] = q
-			nodes = append(nodes, newPrefixNode(match))
-		case index.SeriesMatcherTypeWildcard:
-			match := convert.BytesToString(seriesMatchers[i].Match)
-			q := bluge.NewWildcardQuery(match)
-			q.SetField(docIDField)
-			qs[i] = q
-			nodes = append(nodes, newWildcardNode(match))
-		default:
-			return nil, errors.Errorf("unsupported series matcher type: %v", seriesMatchers[i].Type)
+	query := bluge.NewBooleanQuery()
+	rootNode := newMustNode()
+	if len(seriesMatchers) > 0 {
+		qs := make([]bluge.Query, len(seriesMatchers))
+		matcherNodes := make([]node, len(seriesMatchers))
+		for i := range seriesMatchers {
+			switch seriesMatchers[i].Type {
+			case index.SeriesMatcherTypeExact:
+				match := convert.BytesToString(seriesMatchers[i].Match)
+				q := bluge.NewTermQuery(match)
+				q.SetField(docIDField)
+				qs[i] = q
+				matcherNodes = append(matcherNodes, newTermNode(match, nil))
+			case index.SeriesMatcherTypePrefix:
+				match := convert.BytesToString(seriesMatchers[i].Match)
+				q := bluge.NewPrefixQuery(match)
+				q.SetField(docIDField)
+				qs[i] = q
+				matcherNodes = append(matcherNodes, newPrefixNode(match))
+			case index.SeriesMatcherTypeWildcard:
+				match := convert.BytesToString(seriesMatchers[i].Match)
+				q := bluge.NewWildcardQuery(match)
+				q.SetField(docIDField)
+				qs[i] = q
+				matcherNodes = append(matcherNodes, newWildcardNode(match))
+			default:
+				return nil, errors.Errorf("unsupported series matcher type: %v", seriesMatchers[i].Type)
+			}
 		}
-	}
-	var primaryQuery bluge.Query
-	var n node
-	if len(qs) > 1 {
-		bq := bluge.NewBooleanQuery()
-		bq.AddShould(qs...)
-		bq.SetMinShould(1)
-		primaryQuery = bq
-		n = newShouldNode()
-		for i := range nodes {
-			n.(*shouldNode).Append(nodes[i])
+		var primaryQuery bluge.Query
+		var primaryNode node
+		if len(qs) > 1 {
+			bq := bluge.NewBooleanQuery()
+			bq.AddShould(qs...)
+			bq.SetMinShould(1)
+			primaryQuery = bq
+			primaryNode = newShouldNode()
+			for i := range matcherNodes {
+				primaryNode.(*shouldNode).Append(matcherNodes[i])
+			}
+		} else {
+			primaryQuery = qs[0]
+			primaryNode = matcherNodes[0]
 		}
-	} else {
-		primaryQuery = qs[0]
-		n = nodes[0]
+		query.AddMust(primaryQuery)
+		rootNode.Append(primaryNode)
 	}
-
-	query := bluge.NewBooleanQuery().AddMust(primaryQuery)
-	node := newMustNode()
-	node.Append(n)
 	if secondaryQuery != nil && secondaryQuery.(*queryNode).query != nil {
 		query.AddMust(secondaryQuery.(*queryNode).query)
-		node.Append(secondaryQuery.(*queryNode).node)
+		rootNode.Append(secondaryQuery.(*queryNode).node)
 	}
 	if timeRange != nil {
 		q := bluge.NewDateRangeInclusiveQuery(timeRange.Start, timeRange.End, timeRange.IncludeStart, timeRange.IncludeEnd)
 		q.SetField(timestampField)
 		query.AddMust(q)
-		node.Append(newTimeRangeNode(timeRange))
+		rootNode.Append(newTimeRangeNode(timeRange))
 	}
-	return &queryNode{query, node}, nil
+	return &queryNode{query, rootNode}, nil
 }
 
 // Search implements index.SeriesStore.
diff --git a/pkg/index/inverted/inverted_series_test.go b/pkg/index/inverted/inverted_series_test.go
index 2621c3900..3814915eb 100644
--- a/pkg/index/inverted/inverted_series_test.go
+++ b/pkg/index/inverted/inverted_series_test.go
@@ -943,6 +943,65 @@ func field(key index.FieldKey, value []byte, indexed bool) index.Field {
 	return f
 }
 
+// TestStore_BuildQueryTimeRangeWithoutSeriesMatchers verifies that BuildQuery applies the time range
+// filter even when no series matchers are provided, both with and without a secondary query.
+func TestStore_BuildQueryTimeRangeWithoutSeriesMatchers(t *testing.T) {
+	tester := require.New(t)
+	path, fn := setUp(tester)
+	s, err := NewStore(StoreOpts{
+		Path:   path,
+		Logger: logger.GetLogger("test"),
+	})
+	tester.NoError(err)
+	defer func() {
+		tester.NoError(s.Close())
+		fn()
+	}()
+
+	insertData(tester, s)
+
+	// series2 has Timestamp=101, series3 has Timestamp=1001, series4 has Timestamp=2001.
+	// A time range [100, 2000) should match series2 and series3 only.
+	tr := timestamp.NewTimeRange(time.Unix(0, 100), time.Unix(0, 2000), true, false)
+
+	t.Run("nil secondary query", func(t *testing.T) {
+		query, buildErr := s.BuildQuery(nil, nil, &tr)
+		require.NoError(t, buildErr)
+		require.NotNil(t, query)
+
+		got, searchErr := s.Search(context.Background(), nil, query, 0)
+		require.NoError(t, searchErr)
+
+		sort.Slice(got, func(i, j int) bool {
+			return string(got[i].Key.EntityValues) < string(got[j].Key.EntityValues)
+		})
+		want := []index.SeriesDocument{
+			{Key: index.Series{EntityValues: []byte("test2")}, Timestamp: int64(101)},
+			{Key: index.Series{EntityValues: []byte("test3")}, Timestamp: int64(1001)},
+		}
+		assert.Equal(t, want, got)
+	})
+
+	t.Run("with secondary query", func(t *testing.T) {
+		// Build a secondary query matching entity "test4" (timestamp=2001).
+		// The time range [100, 2000) excludes its timestamp, so nothing should match.
+		durQuery, buildErr := s.BuildQuery([]index.SeriesMatcher{{
+			Type:  index.SeriesMatcherTypeExact,
+			Match: []byte("test4"),
+		}}, nil, nil)
+		require.NoError(t, buildErr)
+
+		// Now query with no series matchers, that secondary query, and the narrow time range.
+		combined, buildErr := s.BuildQuery(nil, durQuery, &tr)
+		require.NoError(t, buildErr)
+		require.NotNil(t, combined)
+
+		got, searchErr := s.Search(context.Background(), nil, combined, 0)
+		require.NoError(t, searchErr)
+		assert.Empty(t, got, "time range [100,2000) must exclude test4 (timestamp=2001)")
+	})
+}
+
 func updateData(tester *require.Assertions, s index.SeriesStore) {
 	b1, b2 := generateDocs()
 	tester.NoError(s.UpdateSeriesBatch(b1))