feat: support query aggregtion(#36380) (#44394)

related: #36380

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
- Core invariant: aggregation is centralized and schema-aware — all
aggregate functions are created via the exec Aggregate registry
(milvus::exec::Aggregate) and validated by ValidateAggFieldType, use a
single in-memory accumulator layout (Accumulator/RowContainer) and
grouping primitives (GroupingSet, HashTable, VectorHasher), ensuring
consistent typing, null semantics and offsets across planner → exec →
reducer conversion paths (toAggregateInfo, Aggregate::create,
GroupingSet, AggResult converters).

- Removed / simplified logic: removed ad‑hoc count/group-by and reducer
code (CountNode/PhyCountNode, GroupByNode/PhyGroupByNode, cntReducer and
its tests) and consolidated into a unified AggregationNode →
PhyAggregationNode + GroupingSet + HashTable execution path and
centralized reducers (MilvusAggReducer, InternalAggReducer,
SegcoreAggReducer). AVG now implemented compositionally (SUM + COUNT)
rather than a bespoke operator, eliminating duplicate implementations.

- Why this does NOT cause data loss or regressions: existing data-access
and serialization paths are preserved and explicitly validated —
bulk_subscript / bulk_script_field_data and FieldData creation are used
for output materialization; converters (InternalResult2AggResult ↔
AggResult2internalResult, SegcoreResults2AggResult ↔
AggResult2segcoreResult) enforce shape/type/row-count validation; proxy
and plan-level checks (MatchAggregationExpression,
translateOutputFields, ValidateAggFieldType, translateGroupByFieldIds)
reject unsupported inputs (ARRAY/JSON, unsupported datatypes) early.
Empty-result generation and explicit error returns guard against silent
corruption.

- New capability and scope: end-to-end GROUP BY and aggregation support
added across the stack — proto (plan.proto, RetrieveRequest fields
group_by_field_ids/aggregates), planner nodes (AggregationNode,
ProjectNode, SearchGroupByNode), exec operators (PhyAggregationNode,
PhyProjectNode) and aggregation core (Aggregate implementations:
Sum/Count/Min/Max, SimpleNumericAggregate, RowContainer, GroupingSet,
HashTable) plus proxy/querynode reducers and tests — enabling grouped
and global aggregation (sum, count, min, max, avg via sum+count) with
schema-aware validation and reduction.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: MrPresent-Han <chun.han@gmail.com>
Co-authored-by: MrPresent-Han <chun.han@gmail.com>
This commit is contained in:
Chun Han
2026-01-06 16:29:25 +08:00
committed by GitHub
co-authored by MrPresent-Han
parent 4c6e33f326
commit b7ee93fc52
136 changed files with 17333 additions and 1986 deletions
+240
View File
@@ -0,0 +1,240 @@
package agg
import (
"fmt"
"reflect"
"regexp"
"strings"
"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
"github.com/milvus-io/milvus/pkg/v2/proto/planpb"
)
const (
kSum = "sum"
kCount = "count"
kAvg = "avg"
kMin = "min"
kMax = "max"
)
var (
// Define the regular expression pattern once to avoid repeated concatenation.
aggregationTypes = kSum + `|` + kCount + `|` + kAvg + `|` + kMin + `|` + kMax
aggregationPattern = regexp.MustCompile(`(?i)^(` + aggregationTypes + `)\s*\(\s*([\w\*]*)\s*\)$`)
)
// MatchAggregationExpression return isAgg, operator name, operator parameter
func MatchAggregationExpression(expression string) (bool, string, string) {
// FindStringSubmatch returns the full match and submatches.
matches := aggregationPattern.FindStringSubmatch(expression)
if len(matches) > 0 {
// Return true, the operator, and the captured parameter.
return true, strings.ToLower(matches[1]), strings.TrimSpace(matches[2])
}
return false, "", ""
}
type AggregateBase interface {
Name() string
Update(target *FieldValue, new *FieldValue) error
ToPB() *planpb.Aggregate
FieldID() int64
OriginalName() string
}
func isSupportedAggregateName(aggregateName string) bool {
switch aggregateName {
case kCount, kSum, kAvg, kMin, kMax:
return true
default:
return false
}
}
func NewAggregate(aggregateName string, aggFieldID int64, originalName string, fieldType schemapb.DataType) ([]AggregateBase, error) {
if !isSupportedAggregateName(aggregateName) {
return nil, fmt.Errorf("invalid Aggregation operator %s", aggregateName)
}
if err := ValidateAggFieldType(aggregateName, fieldType); err != nil {
return nil, err
}
switch aggregateName {
case kCount:
return []AggregateBase{&CountAggregate{fieldID: aggFieldID, originalName: originalName, isAvg: false}}, nil
case kSum:
return []AggregateBase{&SumAggregate{fieldID: aggFieldID, originalName: originalName, isAvg: false}}, nil
case kAvg:
// avg is implemented as sum and count, which will be computed as sum/count later
return []AggregateBase{
&SumAggregate{fieldID: aggFieldID, originalName: originalName, isAvg: true},
&CountAggregate{fieldID: aggFieldID, originalName: originalName, isAvg: true},
}, nil
case kMin:
return []AggregateBase{&MinAggregate{fieldID: aggFieldID, originalName: originalName}}, nil
case kMax:
return []AggregateBase{&MaxAggregate{fieldID: aggFieldID, originalName: originalName}}, nil
default:
// should never happen due to isSupportedAggregateName check
return nil, fmt.Errorf("invalid Aggregation operator %s", aggregateName)
}
}
func FromPB(pb *planpb.Aggregate) (AggregateBase, error) {
switch pb.Op {
case planpb.AggregateOp_count:
return &CountAggregate{fieldID: pb.GetFieldId(), isAvg: false}, nil
case planpb.AggregateOp_sum:
return &SumAggregate{fieldID: pb.GetFieldId(), isAvg: false}, nil
case planpb.AggregateOp_min:
return &MinAggregate{fieldID: pb.GetFieldId()}, nil
case planpb.AggregateOp_max:
return &MaxAggregate{fieldID: pb.GetFieldId()}, nil
default:
return nil, fmt.Errorf("invalid Aggregation operator %d", pb.Op)
}
}
func AccumulateFieldValue(target *FieldValue, new *FieldValue) error {
if target == nil || new == nil {
return fmt.Errorf("target or new field value is nil")
}
// Handle nil `val` for initialization
if target.val == nil {
target.val = new.val
return nil
}
// Verify types match
if reflect.TypeOf(target.val) != reflect.TypeOf(new.val) {
return fmt.Errorf("type mismatch: target is %T, new is %T", target.val, new.val)
}
switch target.val.(type) {
case int:
target.val = target.val.(int) + new.val.(int)
case int32:
target.val = target.val.(int32) + new.val.(int32)
case int64:
target.val = target.val.(int64) + new.val.(int64)
case float32:
target.val = target.val.(float32) + new.val.(float32)
case float64:
target.val = target.val.(float64) + new.val.(float64)
default:
return fmt.Errorf("unsupported type: %T", target.val)
}
return nil
}
func AggregatesToPB(aggregates []AggregateBase) []*planpb.Aggregate {
ret := make([]*planpb.Aggregate, len(aggregates))
for idx, agg := range aggregates {
ret[idx] = agg.ToPB()
}
return ret
}
type FieldValue struct {
val interface{}
}
func NewFieldValue(v interface{}) *FieldValue {
return &FieldValue{val: v}
}
type Row struct {
fieldValues []*FieldValue
}
func (r *Row) ColumnCount() int {
return len(r.fieldValues)
}
func (r *Row) ValAt(col int) interface{} {
return r.fieldValues[col].val
}
func (r *Row) Equal(other *Row, keyCount int) bool {
// Ensure both rows have enough fields for key comparison
if len(r.fieldValues) < keyCount || len(other.fieldValues) < keyCount || len(r.fieldValues) != len(other.fieldValues) {
return false
}
// Compare each field value for equality
for i := 0; i < keyCount; i++ {
if r.fieldValues[i].val != other.fieldValues[i].val {
return false
}
}
return true
}
func (r *Row) UpdateFieldValue(newRow *Row, col int, agg AggregateBase) {
agg.Update(r.fieldValues[col], newRow.fieldValues[col])
}
func (r *Row) ToString() string {
var builder strings.Builder
builder.WriteString("agg-row:")
for _, fv := range r.fieldValues {
builder.WriteString(fmt.Sprintf("%v,", fv.val))
}
return builder.String()
}
func NewRow(fieldValues []*FieldValue) *Row {
return &Row{fieldValues: fieldValues}
}
type Bucket struct {
rows []*Row
}
func (bucket *Bucket) AddRow(row *Row) {
bucket.rows = append(bucket.rows, row)
}
func (bucket *Bucket) RowAt(idx int) *Row {
return bucket.rows[idx]
}
func (bucket *Bucket) RowCount() int {
return len(bucket.rows)
}
func (bucket *Bucket) Accumulate(row *Row, idx int, keyCount int, aggs []AggregateBase) error {
if idx >= len(bucket.rows) || idx < 0 {
return fmt.Errorf("wrong idx:%d for bucket", idx)
}
targetRow := bucket.rows[idx]
if targetRow == nil {
return fmt.Errorf("nil row at the target idx:%d, cannot accumulate the row", idx)
}
if row.ColumnCount() != targetRow.ColumnCount() {
return fmt.Errorf("column count:%d in the row must be equal to the target row:%d", row.ColumnCount(), bucket.rows[idx].ColumnCount())
}
if row.ColumnCount() != keyCount+len(aggs) {
return fmt.Errorf("column count:%d in the row must be sum of keyCount:%d and the number of aggs:%d", row.ColumnCount(), keyCount, len(aggs))
}
for col := keyCount; col < row.ColumnCount(); col++ {
targetRow.UpdateFieldValue(row, col, aggs[col-keyCount])
}
return nil
}
const NONE int = -1
func (bucket *Bucket) Find(row *Row, keyCount int) int {
for idx, existingRow := range bucket.rows {
if existingRow.Equal(row, keyCount) {
return idx
}
}
return NONE
}
func NewBucket() *Bucket {
return &Bucket{rows: make([]*Row, 0, 1)}
}
+459
View File
@@ -0,0 +1,459 @@
package agg
import (
"context"
"fmt"
"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
typeutil2 "github.com/milvus-io/milvus/internal/util/typeutil"
"github.com/milvus-io/milvus/pkg/v2/proto/internalpb"
"github.com/milvus-io/milvus/pkg/v2/proto/planpb"
"github.com/milvus-io/milvus/pkg/v2/proto/segcorepb"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
type GroupAggReducer struct {
groupByFieldIds []int64
aggregates []*planpb.Aggregate
hashValsMap map[uint64]*Bucket
groupLimit int64
schema *schemapb.CollectionSchema
}
func NewGroupAggReducer(groupByFieldIds []int64, aggregates []*planpb.Aggregate, groupLimit int64, schema *schemapb.CollectionSchema) *GroupAggReducer {
return &GroupAggReducer{
groupByFieldIds: groupByFieldIds,
aggregates: aggregates,
hashValsMap: make(map[uint64]*Bucket), // Initialize hashValsMap
groupLimit: groupLimit,
schema: schema,
}
}
type AggregationResult struct {
fieldDatas []*schemapb.FieldData
allRetrieveCount int64
}
func NewAggregationResult(fieldDatas []*schemapb.FieldData, allRetrieveCount int64) *AggregationResult {
if fieldDatas == nil {
fieldDatas = make([]*schemapb.FieldData, 0)
}
return &AggregationResult{
fieldDatas: fieldDatas,
allRetrieveCount: allRetrieveCount,
}
}
// GetFieldDatas returns the fieldDatas slice
func (ar *AggregationResult) GetFieldDatas() []*schemapb.FieldData {
return ar.fieldDatas
}
func (ar *AggregationResult) GetAllRetrieveCount() int64 {
return ar.allRetrieveCount
}
func (reducer *GroupAggReducer) EmptyAggResult() (*AggregationResult, error) {
helper, err := typeutil.CreateSchemaHelper(reducer.schema)
if err != nil {
return nil, err
}
ret := NewAggregationResult(nil, 0)
appendEmptyField := func(fieldId int64) error {
field, err := helper.GetFieldFromID(fieldId)
if err != nil {
return err
}
emptyFieldData, err := typeutil.GenEmptyFieldData(field)
if err != nil {
return err
}
ret.fieldDatas = append(ret.fieldDatas, emptyFieldData)
return nil
}
for _, grpFid := range reducer.groupByFieldIds {
err := appendEmptyField(grpFid)
if err != nil {
return nil, err
}
}
for _, agg := range reducer.aggregates {
if agg.GetOp() == planpb.AggregateOp_count {
countField := genEmptyLongFieldData(schemapb.DataType_Int64, []int64{0})
ret.fieldDatas = append(ret.fieldDatas, countField)
} else {
field, err := helper.GetFieldFromID(agg.GetFieldId())
if err != nil {
return nil, fmt.Errorf("failed to get field schema for aggregate fieldID %d: %w", agg.GetFieldId(), err)
}
resultType, err := getAggregateResultType(agg.GetOp(), field.GetDataType())
if err != nil {
return nil, fmt.Errorf("failed to get result type for aggregate fieldID %d: %w", agg.GetFieldId(), err)
}
emptyFieldData, err := genEmptyFieldDataByType(resultType)
if err != nil {
return nil, fmt.Errorf("failed to generate empty field data for result type %s: %w", resultType.String(), err)
}
ret.fieldDatas = append(ret.fieldDatas, emptyFieldData)
}
}
return ret, nil
}
func genEmptyLongFieldData(dataType schemapb.DataType, data []int64) *schemapb.FieldData {
return &schemapb.FieldData{
Type: dataType,
Field: &schemapb.FieldData_Scalars{
Scalars: &schemapb.ScalarField{
Data: &schemapb.ScalarField_LongData{LongData: &schemapb.LongArray{Data: data}},
},
},
}
}
// genEmptyFieldDataByType generates empty field data based on the data type
func genEmptyFieldDataByType(dataType schemapb.DataType) (*schemapb.FieldData, error) {
switch dataType {
case schemapb.DataType_Int64:
return genEmptyLongFieldData(dataType, []int64{0}), nil
case schemapb.DataType_Double:
return &schemapb.FieldData{
Type: dataType,
Field: &schemapb.FieldData_Scalars{
Scalars: &schemapb.ScalarField{
Data: &schemapb.ScalarField_DoubleData{DoubleData: &schemapb.DoubleArray{Data: []float64{0}}},
},
},
}, nil
case schemapb.DataType_Int8, schemapb.DataType_Int16, schemapb.DataType_Int32:
return &schemapb.FieldData{
Type: dataType,
Field: &schemapb.FieldData_Scalars{
Scalars: &schemapb.ScalarField{
Data: &schemapb.ScalarField_IntData{IntData: &schemapb.IntArray{Data: []int32{0}}},
},
},
}, nil
case schemapb.DataType_Float:
return &schemapb.FieldData{
Type: dataType,
Field: &schemapb.FieldData_Scalars{
Scalars: &schemapb.ScalarField{
Data: &schemapb.ScalarField_FloatData{FloatData: &schemapb.FloatArray{Data: []float32{0}}},
},
},
}, nil
case schemapb.DataType_VarChar, schemapb.DataType_String, schemapb.DataType_Text:
return &schemapb.FieldData{
Type: dataType,
Field: &schemapb.FieldData_Scalars{
Scalars: &schemapb.ScalarField{
Data: &schemapb.ScalarField_StringData{StringData: &schemapb.StringArray{Data: []string{}}},
},
},
}, nil
case schemapb.DataType_Timestamptz:
return genEmptyLongFieldData(dataType, []int64{0}), nil
default:
// For other types, try to use the original field's GenEmptyFieldData
return nil, fmt.Errorf("unsupported data type for aggregate result: %s", dataType.String())
}
}
// getAggregateResultType returns the expected result type for an aggregate operation
// based on the aggregate operator type and the input field type.
func getAggregateResultType(op planpb.AggregateOp, inputType schemapb.DataType) (schemapb.DataType, error) {
switch op {
case planpb.AggregateOp_count:
// count aggregation always returns Int64
return schemapb.DataType_Int64, nil
case planpb.AggregateOp_avg:
// avg aggregation always returns Double
return schemapb.DataType_Double, nil
case planpb.AggregateOp_min, planpb.AggregateOp_max:
// min/max keep the original field type
return inputType, nil
case planpb.AggregateOp_sum:
// sum returns Int64 for integer types, Double for float types
switch inputType {
case schemapb.DataType_Int8, schemapb.DataType_Int16, schemapb.DataType_Int32, schemapb.DataType_Int64:
return schemapb.DataType_Int64, nil
case schemapb.DataType_Timestamptz:
return schemapb.DataType_Timestamptz, nil
case schemapb.DataType_Float, schemapb.DataType_Double:
return schemapb.DataType_Double, nil
default:
return schemapb.DataType_None, fmt.Errorf("unsupported input type %s for sum aggregation", inputType.String())
}
default:
return schemapb.DataType_None, fmt.Errorf("unknown aggregate operator: %d", op)
}
}
// validateAggregationResults validates the input AggregationResult slice
// It checks:
// 1. Each result's fieldDatas length equals numGroupingKeys + numAggs
// 2. No nil fieldData in any result
// 3. Each fieldData's Type matches the expected type from schema
func (reducer *GroupAggReducer) validateAggregationResults(results []*AggregationResult) error {
if reducer.schema == nil {
return fmt.Errorf("schema is nil, cannot validate field types")
}
helper, err := typeutil.CreateSchemaHelper(reducer.schema)
if err != nil {
return fmt.Errorf("failed to create schema helper: %w", err)
}
numGroupingKeys := len(reducer.groupByFieldIds)
numAggs := len(reducer.aggregates)
expectedColumnCount := numGroupingKeys + numAggs
// Build expected types for each column
expectedTypes := make([]schemapb.DataType, 0, expectedColumnCount)
// Add types for grouping keys
for _, fieldID := range reducer.groupByFieldIds {
field, err := helper.GetFieldFromID(fieldID)
if err != nil {
return fmt.Errorf("failed to get field schema for groupBy fieldID %d: %w", fieldID, err)
}
expectedTypes = append(expectedTypes, field.GetDataType())
}
// Add types for aggregates
for _, agg := range reducer.aggregates {
var expectedType schemapb.DataType
if agg.GetOp() == planpb.AggregateOp_count {
// count aggregation always returns Int64
expectedType = schemapb.DataType_Int64
} else {
field, err := helper.GetFieldFromID(agg.GetFieldId())
if err != nil {
return fmt.Errorf("failed to get field schema for aggregate fieldID %d: %w", agg.GetFieldId(), err)
}
expectedType, err = getAggregateResultType(agg.GetOp(), field.GetDataType())
if err != nil {
return fmt.Errorf("failed to get aggregate result type for aggregate fieldID %d: %w", agg.GetFieldId(), err)
}
}
expectedTypes = append(expectedTypes, expectedType)
}
// Validate each result
for resultIdx, result := range results {
if result == nil {
return fmt.Errorf("result at index %d is nil", resultIdx)
}
fieldDatas := result.GetFieldDatas()
// Check 1: fieldDatas length
if len(fieldDatas) != expectedColumnCount {
return fmt.Errorf("result at index %d has fieldDatas length %d, expected %d (numGroupingKeys=%d, numAggs=%d)",
resultIdx, len(fieldDatas), expectedColumnCount, numGroupingKeys, numAggs)
}
// Check 2: no nil fieldData and Check 3: type matching
for colIdx, fieldData := range fieldDatas {
if fieldData == nil {
return fmt.Errorf("result at index %d has nil fieldData at column %d", resultIdx, colIdx)
}
expectedType := expectedTypes[colIdx]
actualType := fieldData.GetType()
if actualType != expectedType {
return fmt.Errorf("result at index %d, column %d has type %s, expected %s",
resultIdx, colIdx, schemapb.DataType_name[int32(actualType)], schemapb.DataType_name[int32(expectedType)])
}
}
}
return nil
}
func (reducer *GroupAggReducer) Reduce(ctx context.Context, results []*AggregationResult) (*AggregationResult, error) {
if len(results) == 0 {
return reducer.EmptyAggResult()
}
// Validate input results before processing
if err := reducer.validateAggregationResults(results); err != nil {
return nil, err
}
if len(results) == 1 {
return results[0], nil
}
// 0. set up aggregates
aggs := make([]AggregateBase, len(reducer.aggregates))
for idx, aggPb := range reducer.aggregates {
agg, err := FromPB(aggPb)
if err != nil {
return nil, err
}
aggs[idx] = agg
}
// 1. set up hashers and accumulators
numGroupingKeys := len(reducer.groupByFieldIds)
numAggs := len(reducer.aggregates)
hashers := make([]FieldAccessor, numGroupingKeys)
accumulators := make([]FieldAccessor, numAggs)
firstFieldData := results[0].GetFieldDatas()
outputColumnCount := len(firstFieldData)
for idx, fieldData := range firstFieldData {
accessor, err := NewFieldAccessor(fieldData.GetType())
if err != nil {
return nil, err
}
if idx < numGroupingKeys {
hashers[idx] = accessor
} else {
accumulators[idx-numGroupingKeys] = accessor
}
}
reducedResult := NewAggregationResult(nil, 0)
isGlobal := numGroupingKeys == 0
if isGlobal {
reducedResult.fieldDatas = typeutil.PrepareResultFieldData(firstFieldData, 1)
rows := make([]*Row, len(results))
for idx, result := range results {
reducedResult.allRetrieveCount += result.GetAllRetrieveCount()
fieldValues := make([]*FieldValue, outputColumnCount)
for col := 0; col < outputColumnCount; col++ {
fieldData := result.GetFieldDatas()[col]
accumulators[col].SetVals(fieldData)
fieldValues[col] = NewFieldValue(accumulators[col].ValAt(0))
}
rows[idx] = NewRow(fieldValues)
}
for r := 1; r < len(rows); r++ {
for c := 0; c < outputColumnCount; c++ {
rows[0].UpdateFieldValue(rows[r], c, aggs[c])
}
}
AssembleSingleRow(outputColumnCount, rows[0], reducedResult.fieldDatas)
return reducedResult, nil
}
// 2. compute hash values for all rows in the result retrieved
var totalRowCount int64 = 0
processResults:
for _, result := range results {
// Check limit before processing each shard to avoid unnecessary work
if reducer.groupLimit != -1 && totalRowCount >= reducer.groupLimit {
break processResults
}
reducedResult.allRetrieveCount += result.GetAllRetrieveCount()
if result == nil {
return nil, fmt.Errorf("input result from any sources cannot be nil")
}
fieldDatas := result.GetFieldDatas()
if outputColumnCount != len(fieldDatas) {
return nil, fmt.Errorf("retrieved results from different segments have different size of columns")
}
if outputColumnCount == 0 {
return nil, fmt.Errorf("retrieved results have no column data")
}
rowCount := -1
for i := 0; i < outputColumnCount; i++ {
fieldData := fieldDatas[i]
if i < numGroupingKeys {
hashers[i].SetVals(fieldData)
} else {
accumulators[i-numGroupingKeys].SetVals(fieldData)
}
if rowCount == -1 {
rowCount = hashers[i].RowCount()
} else if i < numGroupingKeys {
if rowCount != hashers[i].RowCount() {
return nil, fmt.Errorf("field data:%d for different columns have different row count, %d vs %d, wrong state",
i, rowCount, hashers[i].RowCount())
}
} else if rowCount != accumulators[i-numGroupingKeys].RowCount() {
return nil, fmt.Errorf("field data:%d for different columns have different row count, %d vs %d, wrong state",
i, rowCount, accumulators[i-numGroupingKeys].RowCount())
}
}
for row := 0; row < rowCount; row++ {
// Check limit before processing each row to avoid unnecessary hashing and copying
if reducer.groupLimit != -1 && totalRowCount >= reducer.groupLimit {
break processResults
}
rowFieldValues := make([]*FieldValue, outputColumnCount)
var hashVal uint64
for col := 0; col < outputColumnCount; col++ {
if col < numGroupingKeys {
if col > 0 {
hashVal = typeutil2.HashMix(hashVal, hashers[col].Hash(row))
} else {
hashVal = hashers[col].Hash(row)
}
rowFieldValues[col] = NewFieldValue(hashers[col].ValAt(row))
} else {
rowFieldValues[col] = NewFieldValue(accumulators[col-numGroupingKeys].ValAt(row))
}
}
newRow := NewRow(rowFieldValues)
if bucket := reducer.hashValsMap[hashVal]; bucket == nil {
newBucket := NewBucket()
newBucket.AddRow(newRow)
totalRowCount++
reducer.hashValsMap[hashVal] = newBucket
} else {
if rowIdx := bucket.Find(newRow, numGroupingKeys); rowIdx == NONE {
bucket.AddRow(newRow)
totalRowCount++
} else {
bucket.Accumulate(newRow, rowIdx, numGroupingKeys, aggs)
}
}
// Don't guarantee specific groups to be returned before milvus support order by
}
}
// 3. assemble reduced buckets into retrievedResult
reducedResult.fieldDatas = typeutil.PrepareResultFieldData(firstFieldData, totalRowCount)
for _, bucket := range reducer.hashValsMap {
err := AssembleBucket(bucket, reducedResult.GetFieldDatas())
if err != nil {
return nil, err
}
}
return reducedResult, nil
}
func InternalResult2AggResult(results []*internalpb.RetrieveResults) []*AggregationResult {
aggResults := make([]*AggregationResult, len(results))
for i := 0; i < len(results); i++ {
aggResults[i] = NewAggregationResult(results[i].GetFieldsData(), results[i].GetAllRetrieveCount())
}
return aggResults
}
func AggResult2internalResult(aggRes *AggregationResult) *internalpb.RetrieveResults {
return &internalpb.RetrieveResults{FieldsData: aggRes.GetFieldDatas(), AllRetrieveCount: aggRes.GetAllRetrieveCount()}
}
func SegcoreResults2AggResult(results []*segcorepb.RetrieveResults) ([]*AggregationResult, error) {
aggResults := make([]*AggregationResult, len(results))
for i := 0; i < len(results); i++ {
if results[i] == nil {
return nil, fmt.Errorf("input segcore query results from any sources cannot be nil")
}
fieldsData := results[i].GetFieldsData()
allRetrieveCount := results[i].GetAllRetrieveCount()
aggResults[i] = NewAggregationResult(fieldsData, allRetrieveCount)
}
return aggResults, nil
}
func AggResult2segcoreResult(aggRes *AggregationResult) *segcorepb.RetrieveResults {
return &segcorepb.RetrieveResults{FieldsData: aggRes.GetFieldDatas(), AllRetrieveCount: aggRes.GetAllRetrieveCount()}
}
+494
View File
@@ -0,0 +1,494 @@
package agg
import (
"encoding/binary"
"fmt"
"hash"
"hash/fnv"
"math"
"unsafe"
"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
)
func NewFieldAccessor(fieldType schemapb.DataType) (FieldAccessor, error) {
switch fieldType {
case schemapb.DataType_Bool:
return newBoolFieldAccessor(), nil
case schemapb.DataType_Int8, schemapb.DataType_Int16, schemapb.DataType_Int32:
return newInt32FieldAccessor(), nil
case schemapb.DataType_Int64:
return newInt64FieldAccessor(), nil
case schemapb.DataType_Timestamptz:
return newTimestamptzFieldAccessor(), nil
case schemapb.DataType_VarChar, schemapb.DataType_String:
return newStringFieldAccessor(), nil
case schemapb.DataType_Float:
return newFloat32FieldAccessor(), nil
case schemapb.DataType_Double:
return newFloat64FieldAccessor(), nil
default:
return nil, fmt.Errorf("unsupported data type for hasher")
}
}
type FieldAccessor interface {
Hash(idx int) uint64
ValAt(idx int) interface{}
SetVals(fieldData *schemapb.FieldData)
RowCount() int
}
type Int32FieldAccessor struct {
vals []int32
hasher hash.Hash64
buffer []byte
}
func (i32Field *Int32FieldAccessor) Hash(idx int) uint64 {
if idx < 0 || idx >= len(i32Field.vals) {
panic(fmt.Sprintf("Int32FieldAccessor.Hash: index %d out of range [0,%d)", idx, len(i32Field.vals)))
}
i32Field.hasher.Reset()
val := i32Field.vals[idx]
binary.LittleEndian.PutUint32(i32Field.buffer, uint32(val))
i32Field.hasher.Write(i32Field.buffer)
ret := i32Field.hasher.Sum64()
return ret
}
func (i32Field *Int32FieldAccessor) SetVals(fieldData *schemapb.FieldData) {
i32Field.vals = fieldData.GetScalars().GetIntData().GetData()
}
func (i32Field *Int32FieldAccessor) RowCount() int {
return len(i32Field.vals)
}
func (i32Field *Int32FieldAccessor) ValAt(idx int) interface{} {
return i32Field.vals[idx]
}
func newInt32FieldAccessor() FieldAccessor {
return &Int32FieldAccessor{hasher: fnv.New64a(), buffer: make([]byte, 4)}
}
type Int64FieldAccessor struct {
vals []int64
hasher hash.Hash64
buffer []byte
}
func (i64Field *Int64FieldAccessor) Hash(idx int) uint64 {
if idx < 0 || idx >= len(i64Field.vals) {
panic(fmt.Sprintf("Int64FieldAccessor.Hash: index %d out of range [0,%d)", idx, len(i64Field.vals)))
}
i64Field.hasher.Reset()
val := i64Field.vals[idx]
binary.LittleEndian.PutUint64(i64Field.buffer, uint64(val))
i64Field.hasher.Write(i64Field.buffer)
return i64Field.hasher.Sum64()
}
func (i64Field *Int64FieldAccessor) SetVals(fieldData *schemapb.FieldData) {
i64Field.vals = fieldData.GetScalars().GetLongData().GetData()
}
func (i64Field *Int64FieldAccessor) RowCount() int {
return len(i64Field.vals)
}
func (i64Field *Int64FieldAccessor) ValAt(idx int) interface{} {
return i64Field.vals[idx]
}
func newInt64FieldAccessor() FieldAccessor {
return &Int64FieldAccessor{hasher: fnv.New64a(), buffer: make([]byte, 8)}
}
type TimestamptzFieldAccessor struct {
vals []int64
hasher hash.Hash64
buffer []byte
}
func (tzField *TimestamptzFieldAccessor) Hash(idx int) uint64 {
if idx < 0 || idx >= len(tzField.vals) {
panic(fmt.Sprintf("TimestamptzFieldAccessor.Hash: index %d out of range [0,%d)", idx, len(tzField.vals)))
}
tzField.hasher.Reset()
val := tzField.vals[idx]
binary.LittleEndian.PutUint64(tzField.buffer, uint64(val))
tzField.hasher.Write(tzField.buffer)
return tzField.hasher.Sum64()
}
func (tzField *TimestamptzFieldAccessor) SetVals(fieldData *schemapb.FieldData) {
tzField.vals = fieldData.GetScalars().GetTimestamptzData().GetData()
}
func (tzField *TimestamptzFieldAccessor) RowCount() int {
return len(tzField.vals)
}
func (tzField *TimestamptzFieldAccessor) ValAt(idx int) interface{} {
return tzField.vals[idx]
}
func newTimestamptzFieldAccessor() FieldAccessor {
return &TimestamptzFieldAccessor{hasher: fnv.New64a(), buffer: make([]byte, 8)}
}
// BoolFieldAccessor
type BoolFieldAccessor struct {
vals []bool
hasher hash.Hash64
buffer []byte
}
func (boolField *BoolFieldAccessor) Hash(idx int) uint64 {
if idx < 0 || idx >= len(boolField.vals) {
panic(fmt.Sprintf("BoolFieldAccessor.Hash: index %d out of range [0,%d)", idx, len(boolField.vals)))
}
boolField.hasher.Reset()
val := boolField.vals[idx]
if val {
boolField.buffer[0] = 1
} else {
boolField.buffer[0] = 0
}
boolField.hasher.Write(boolField.buffer[:1])
return boolField.hasher.Sum64()
}
func (boolField *BoolFieldAccessor) SetVals(fieldData *schemapb.FieldData) {
boolField.vals = fieldData.GetScalars().GetBoolData().GetData()
}
func (boolField *BoolFieldAccessor) RowCount() int {
return len(boolField.vals)
}
func (boolField *BoolFieldAccessor) ValAt(idx int) interface{} {
return boolField.vals[idx]
}
func newBoolFieldAccessor() FieldAccessor {
return &BoolFieldAccessor{hasher: fnv.New64a(), buffer: make([]byte, 1)}
}
// Float32FieldAccessor
type Float32FieldAccessor struct {
vals []float32
hasher hash.Hash64
buffer []byte
}
func (f32FieldAccessor *Float32FieldAccessor) Hash(idx int) uint64 {
if idx < 0 || idx >= len(f32FieldAccessor.vals) {
panic(fmt.Sprintf("Float32FieldAccessor.Hash: index %d out of range [0,%d)", idx, len(f32FieldAccessor.vals)))
}
f32FieldAccessor.hasher.Reset()
val := f32FieldAccessor.vals[idx]
binary.LittleEndian.PutUint32(f32FieldAccessor.buffer, math.Float32bits(val))
f32FieldAccessor.hasher.Write(f32FieldAccessor.buffer[:4])
return f32FieldAccessor.hasher.Sum64()
}
func (f32FieldAccessor *Float32FieldAccessor) SetVals(fieldData *schemapb.FieldData) {
f32FieldAccessor.vals = fieldData.GetScalars().GetFloatData().GetData()
}
func (f32FieldAccessor *Float32FieldAccessor) RowCount() int {
return len(f32FieldAccessor.vals)
}
func (f32FieldAccessor *Float32FieldAccessor) ValAt(idx int) interface{} {
return f32FieldAccessor.vals[idx]
}
func newFloat32FieldAccessor() FieldAccessor {
return &Float32FieldAccessor{hasher: fnv.New64a(), buffer: make([]byte, 4)}
}
// Float64FieldAccessor
type Float64FieldAccessor struct {
vals []float64
hasher hash.Hash64
buffer []byte
}
func (f64Field *Float64FieldAccessor) Hash(idx int) uint64 {
if idx < 0 || idx >= len(f64Field.vals) {
panic(fmt.Sprintf("Float64FieldAccessor.Hash: index %d out of range [0,%d)", idx, len(f64Field.vals)))
}
f64Field.hasher.Reset()
val := f64Field.vals[idx]
binary.LittleEndian.PutUint64(f64Field.buffer, math.Float64bits(val))
f64Field.hasher.Write(f64Field.buffer)
return f64Field.hasher.Sum64()
}
func (f64Field *Float64FieldAccessor) SetVals(fieldData *schemapb.FieldData) {
f64Field.vals = fieldData.GetScalars().GetDoubleData().GetData()
}
func (f64Field *Float64FieldAccessor) RowCount() int {
return len(f64Field.vals)
}
func (f64Field *Float64FieldAccessor) ValAt(idx int) interface{} {
return f64Field.vals[idx]
}
func newFloat64FieldAccessor() FieldAccessor {
return &Float64FieldAccessor{hasher: fnv.New64a(), buffer: make([]byte, 8)}
}
// StringFieldAccessor
type StringFieldAccessor struct {
vals []string
hasher hash.Hash64
}
func (stringField *StringFieldAccessor) Hash(idx int) uint64 {
if idx < 0 || idx >= len(stringField.vals) {
panic(fmt.Sprintf("StringFieldAccessor.Hash: index %d out of range [0,%d)", idx, len(stringField.vals)))
}
stringField.hasher.Reset()
val := stringField.vals[idx]
b := unsafe.Slice(unsafe.StringData(val), len(val))
stringField.hasher.Write(b)
return stringField.hasher.Sum64()
}
func (stringField *StringFieldAccessor) SetVals(fieldData *schemapb.FieldData) {
stringField.vals = fieldData.GetScalars().GetStringData().GetData()
}
func (stringField *StringFieldAccessor) RowCount() int {
return len(stringField.vals)
}
func (stringField *StringFieldAccessor) ValAt(idx int) interface{} {
return stringField.vals[idx]
}
func newStringFieldAccessor() FieldAccessor {
return &StringFieldAccessor{hasher: fnv.New64a()}
}
func AssembleBucket(bucket *Bucket, fieldDatas []*schemapb.FieldData) error {
colCount := len(fieldDatas)
for r := 0; r < bucket.RowCount(); r++ {
row := bucket.RowAt(r)
if err := AssembleSingleRow(colCount, row, fieldDatas); err != nil {
return err
}
}
return nil
}
func AssembleSingleRow(colCount int, row *Row, fieldDatas []*schemapb.FieldData) error {
for c := 0; c < colCount; c++ {
err := AssembleSingleValue(row.ValAt(c), fieldDatas[c])
if err != nil {
return err
}
}
return nil
}
func AssembleSingleValue(val interface{}, fieldData *schemapb.FieldData) error {
switch fieldData.GetType() {
case schemapb.DataType_Bool:
boolVal, ok := val.(bool)
if !ok {
return fmt.Errorf("type assertion failed: expected bool, got %T", val)
}
fieldData.GetScalars().GetBoolData().Data = append(fieldData.GetScalars().GetBoolData().GetData(), boolVal)
case schemapb.DataType_Int8, schemapb.DataType_Int16, schemapb.DataType_Int32:
intVal, ok := val.(int32)
if !ok {
return fmt.Errorf("type assertion failed: expected int32, got %T", val)
}
fieldData.GetScalars().GetIntData().Data = append(fieldData.GetScalars().GetIntData().GetData(), intVal)
case schemapb.DataType_Int64:
int64Val, ok := val.(int64)
if !ok {
return fmt.Errorf("type assertion failed: expected int64, got %T", val)
}
fieldData.GetScalars().GetLongData().Data = append(fieldData.GetScalars().GetLongData().GetData(), int64Val)
case schemapb.DataType_Timestamptz:
timestampVal, ok := val.(int64)
if !ok {
return fmt.Errorf("type assertion failed: expected int64 for Timestamptz, got %T", val)
}
fieldData.GetScalars().GetTimestamptzData().Data = append(fieldData.GetScalars().GetTimestamptzData().GetData(), timestampVal)
case schemapb.DataType_Float:
floatVal, ok := val.(float32)
if !ok {
return fmt.Errorf("type assertion failed: expected float32, got %T", val)
}
fieldData.GetScalars().GetFloatData().Data = append(fieldData.GetScalars().GetFloatData().GetData(), floatVal)
case schemapb.DataType_Double:
doubleVal, ok := val.(float64)
if !ok {
return fmt.Errorf("type assertion failed: expected float64, got %T", val)
}
fieldData.GetScalars().GetDoubleData().Data = append(fieldData.GetScalars().GetDoubleData().GetData(), doubleVal)
case schemapb.DataType_VarChar, schemapb.DataType_String:
stringVal, ok := val.(string)
if !ok {
return fmt.Errorf("type assertion failed: expected string, got %T", val)
}
fieldData.GetScalars().GetStringData().Data = append(fieldData.GetScalars().GetStringData().GetData(), stringVal)
default:
return fmt.Errorf("unsupported DataType:%d", fieldData.GetType())
}
return nil
}
type AggregationFieldMap struct {
userOriginalOutputFields []string
userOriginalOutputFieldIdxes [][]int // Each user output field can map to multiple field indices (e.g., avg maps to sum and count)
}
func (aggMap *AggregationFieldMap) Count() int {
return len(aggMap.userOriginalOutputFields)
}
// IndexAt returns the first index for the given user output field index.
// For avg aggregation, this returns the sum index.
// For backward compatibility, this method is kept.
func (aggMap *AggregationFieldMap) IndexAt(idx int) int {
if len(aggMap.userOriginalOutputFieldIdxes[idx]) > 0 {
return aggMap.userOriginalOutputFieldIdxes[idx][0]
}
return -1
}
// IndexesAt returns all indices for the given user output field index.
// For avg aggregation, this returns both sum and count indices.
// For other aggregations, this returns a slice with a single index.
func (aggMap *AggregationFieldMap) IndexesAt(idx int) []int {
return aggMap.userOriginalOutputFieldIdxes[idx]
}
func (aggMap *AggregationFieldMap) NameAt(idx int) string {
return aggMap.userOriginalOutputFields[idx]
}
func NewAggregationFieldMap(originalUserOutputFields []string, groupByFields []string, aggs []AggregateBase) *AggregationFieldMap {
numGroupingKeys := len(groupByFields)
groupByFieldMap := make(map[string]int, len(groupByFields))
for i, field := range groupByFields {
groupByFieldMap[field] = i
}
// Build a map from originalName to all indices (for avg, this will include both sum and count indices)
aggFieldMap := make(map[string][]int, len(aggs))
for i, agg := range aggs {
originalName := agg.OriginalName()
idx := i + numGroupingKeys
// Check if this aggregate is part of an avg aggregation
var isAvg bool
switch a := agg.(type) {
case *SumAggregate:
isAvg = a.isAvg
case *CountAggregate:
isAvg = a.isAvg
}
if isAvg {
// For avg aggregates, both sum and count share the same originalName
// Add this index to the list for this originalName
aggFieldMap[originalName] = append(aggFieldMap[originalName], idx)
} else {
// For non-avg aggregates, each originalName maps to a single index
aggFieldMap[originalName] = []int{idx}
}
}
userOriginalOutputFieldIdxes := make([][]int, len(originalUserOutputFields))
for i, outputField := range originalUserOutputFields {
if idx, exist := groupByFieldMap[outputField]; exist {
// Group by field maps to a single index
userOriginalOutputFieldIdxes[i] = []int{idx}
} else if indices, exist := aggFieldMap[outputField]; exist {
// Aggregate field may map to multiple indices (for avg: sum and count)
userOriginalOutputFieldIdxes[i] = indices
} else {
// Field not found, set empty slice
userOriginalOutputFieldIdxes[i] = []int{}
}
}
return &AggregationFieldMap{originalUserOutputFields, userOriginalOutputFieldIdxes}
}
// ComputeAvgFromSumAndCount computes average from sum and count field data.
// It takes sumFieldData and countFieldData, computes avg = sum / count for each row,
// and returns a new Double FieldData containing the average values.
func ComputeAvgFromSumAndCount(sumFieldData *schemapb.FieldData, countFieldData *schemapb.FieldData) (*schemapb.FieldData, error) {
if sumFieldData == nil || countFieldData == nil {
return nil, fmt.Errorf("sumFieldData and countFieldData cannot be nil")
}
sumType := sumFieldData.GetType()
countType := countFieldData.GetType()
if countType != schemapb.DataType_Int64 {
return nil, fmt.Errorf("count field must be Int64 type, got %s", countType.String())
}
countData := countFieldData.GetScalars().GetLongData().GetData()
rowCount := len(countData)
// Create result FieldData with Double type
result := &schemapb.FieldData{
Type: schemapb.DataType_Double,
Field: &schemapb.FieldData_Scalars{
Scalars: &schemapb.ScalarField{
Data: &schemapb.ScalarField_DoubleData{
DoubleData: &schemapb.DoubleArray{Data: make([]float64, 0, rowCount)},
},
},
},
}
resultData := make([]float64, 0, rowCount)
// Compute avg = sum / count for each row
switch sumType {
case schemapb.DataType_Int64:
sumData := sumFieldData.GetScalars().GetLongData().GetData()
if len(sumData) != rowCount {
return nil, fmt.Errorf("sum and count field data must have the same length, got sum:%d, count:%d", len(sumData), rowCount)
}
for i := 0; i < rowCount; i++ {
if countData[i] == 0 {
return nil, fmt.Errorf("division by zero: count is 0 at row %d", i)
}
resultData = append(resultData, float64(sumData[i])/float64(countData[i]))
}
case schemapb.DataType_Double:
sumData := sumFieldData.GetScalars().GetDoubleData().GetData()
if len(sumData) != rowCount {
return nil, fmt.Errorf("sum and count field data must have the same length, got sum:%d, count:%d", len(sumData), rowCount)
}
for i := 0; i < rowCount; i++ {
if countData[i] == 0 {
return nil, fmt.Errorf("division by zero: count is 0 at row %d", i)
}
resultData = append(resultData, sumData[i]/float64(countData[i]))
}
default:
return nil, fmt.Errorf("unsupported sum field type for avg computation: %s", sumType.String())
}
result.GetScalars().GetDoubleData().Data = resultData
return result, nil
}
+235
View File
@@ -0,0 +1,235 @@
package agg
import (
"fmt"
"github.com/milvus-io/milvus/pkg/v2/proto/planpb"
)
type SumAggregate struct {
fieldID int64
originalName string
isAvg bool
}
func (sum *SumAggregate) Name() string {
return kSum
}
func (sum *SumAggregate) Update(target *FieldValue, new *FieldValue) error {
return AccumulateFieldValue(target, new)
}
func (sum *SumAggregate) ToPB() *planpb.Aggregate {
return &planpb.Aggregate{Op: planpb.AggregateOp_sum, FieldId: sum.FieldID()}
}
func (sum *SumAggregate) FieldID() int64 {
return sum.fieldID
}
func (sum *SumAggregate) OriginalName() string {
return sum.originalName
}
type CountAggregate struct {
fieldID int64
originalName string
isAvg bool
}
func (count *CountAggregate) Name() string {
return kCount
}
func (count *CountAggregate) Update(target *FieldValue, new *FieldValue) error {
return AccumulateFieldValue(target, new)
}
func (count *CountAggregate) ToPB() *planpb.Aggregate {
return &planpb.Aggregate{Op: planpb.AggregateOp_count, FieldId: count.FieldID()}
}
func (count *CountAggregate) FieldID() int64 {
return count.fieldID
}
func (count *CountAggregate) OriginalName() string {
return count.originalName
}
type MinAggregate struct {
fieldID int64
originalName string
}
func (min *MinAggregate) Name() string {
return kMin
}
func (min *MinAggregate) Update(target *FieldValue, new *FieldValue) error {
if target == nil || new == nil {
return fmt.Errorf("target or new field value is nil")
}
// Handle nil `val` for initialization
if target.val == nil {
target.val = new.val
return nil
}
// Compare and keep the minimum value
switch targetVal := target.val.(type) {
case int:
newVal, ok := new.val.(int)
if !ok {
return fmt.Errorf("type mismatch: target is int, new is %T", new.val)
}
if newVal < targetVal {
target.val = newVal
}
case int32:
newVal, ok := new.val.(int32)
if !ok {
return fmt.Errorf("type mismatch: target is int32, new is %T", new.val)
}
if newVal < targetVal {
target.val = newVal
}
case int64:
newVal, ok := new.val.(int64)
if !ok {
return fmt.Errorf("type mismatch: target is int64, new is %T", new.val)
}
if newVal < targetVal {
target.val = newVal
}
case float32:
newVal, ok := new.val.(float32)
if !ok {
return fmt.Errorf("type mismatch: target is float32, new is %T", new.val)
}
if newVal < targetVal {
target.val = newVal
}
case float64:
newVal, ok := new.val.(float64)
if !ok {
return fmt.Errorf("type mismatch: target is float64, new is %T", new.val)
}
if newVal < targetVal {
target.val = newVal
}
case string:
newVal, ok := new.val.(string)
if !ok {
return fmt.Errorf("type mismatch: target is string, new is %T", new.val)
}
if newVal < targetVal {
target.val = newVal
}
default:
return fmt.Errorf("unsupported type for min aggregation: %T", target.val)
}
return nil
}
func (min *MinAggregate) ToPB() *planpb.Aggregate {
return &planpb.Aggregate{Op: planpb.AggregateOp_min, FieldId: min.FieldID()}
}
func (min *MinAggregate) FieldID() int64 {
return min.fieldID
}
func (min *MinAggregate) OriginalName() string {
return min.originalName
}
type MaxAggregate struct {
fieldID int64
originalName string
}
func (max *MaxAggregate) Name() string {
return kMax
}
func (max *MaxAggregate) Update(target *FieldValue, new *FieldValue) error {
if target == nil || new == nil {
return fmt.Errorf("target or new field value is nil")
}
// Handle nil `val` for initialization
if target.val == nil {
target.val = new.val
return nil
}
// Compare and keep the maximum value
switch targetVal := target.val.(type) {
case int:
newVal, ok := new.val.(int)
if !ok {
return fmt.Errorf("type mismatch: target is int, new is %T", new.val)
}
if newVal > targetVal {
target.val = newVal
}
case int32:
newVal, ok := new.val.(int32)
if !ok {
return fmt.Errorf("type mismatch: target is int32, new is %T", new.val)
}
if newVal > targetVal {
target.val = newVal
}
case int64:
newVal, ok := new.val.(int64)
if !ok {
return fmt.Errorf("type mismatch: target is int64, new is %T", new.val)
}
if newVal > targetVal {
target.val = newVal
}
case float32:
newVal, ok := new.val.(float32)
if !ok {
return fmt.Errorf("type mismatch: target is float32, new is %T", new.val)
}
if newVal > targetVal {
target.val = newVal
}
case float64:
newVal, ok := new.val.(float64)
if !ok {
return fmt.Errorf("type mismatch: target is float64, new is %T", new.val)
}
if newVal > targetVal {
target.val = newVal
}
case string:
newVal, ok := new.val.(string)
if !ok {
return fmt.Errorf("type mismatch: target is string, new is %T", new.val)
}
if newVal > targetVal {
target.val = newVal
}
default:
return fmt.Errorf("unsupported type for max aggregation: %T", target.val)
}
return nil
}
func (max *MaxAggregate) ToPB() *planpb.Aggregate {
return &planpb.Aggregate{Op: planpb.AggregateOp_max, FieldId: max.FieldID()}
}
func (max *MaxAggregate) FieldID() int64 {
return max.fieldID
}
func (max *MaxAggregate) OriginalName() string {
return max.originalName
}
+57
View File
@@ -0,0 +1,57 @@
package agg
import (
"fmt"
"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
)
func isSupportedAggFieldType(aggregateName string, dt schemapb.DataType) bool {
switch aggregateName {
case kCount:
// count(*) uses DataType_None; count(field) can work for any type since it
// doesn't depend on the field's scalar representation in reducer.
return true
case kSum, kAvg:
switch dt {
case schemapb.DataType_Int8,
schemapb.DataType_Int16,
schemapb.DataType_Int32,
schemapb.DataType_Int64,
schemapb.DataType_Float,
schemapb.DataType_Double:
return true
default:
return false
}
case kMin, kMax:
switch dt {
case schemapb.DataType_Int8,
schemapb.DataType_Int16,
schemapb.DataType_Int32,
schemapb.DataType_Int64,
schemapb.DataType_Float,
schemapb.DataType_Double,
schemapb.DataType_VarChar,
schemapb.DataType_String,
schemapb.DataType_Timestamptz:
return true
default:
return false
}
default:
// operator validity is handled by NewAggregate; keep this conservative.
return false
}
}
// ValidateAggFieldType checks whether the input field type can be used by the
// given aggregation operator.
//
// Note: operator name is expected to be normalized (lowercase) by caller.
func ValidateAggFieldType(aggregateName string, dt schemapb.DataType) error {
if !isSupportedAggFieldType(aggregateName, dt) {
return fmt.Errorf("aggregation operator %s does not support data type %s", aggregateName, dt.String())
}
return nil
}
+2
View File
@@ -52,6 +52,7 @@ add_subdirectory( exec )
add_subdirectory( bitset )
add_subdirectory( futures )
add_subdirectory( rescores )
add_subdirectory( plan )
milvus_add_pkg_config("milvus_core")
@@ -70,6 +71,7 @@ add_library(milvus_core SHARED
$<TARGET_OBJECTS:milvus_bitset>
$<TARGET_OBJECTS:milvus_futures>
$<TARGET_OBJECTS:milvus_rescores>
$<TARGET_OBJECTS:milvus_plan>
)
set(LINK_TARGETS
+11
View File
@@ -637,6 +637,17 @@ class ArrayView {
}
}
void
output_data(Array& array) const {
// Create a new Array object from ArrayView's data and assign it to the
// output array
array = Array(data_,
length_,
static_cast<size_t>(size_),
element_type_,
offsets_ptr_);
}
ScalarFieldProto
output_data() const {
ScalarFieldProto data_array;
+472
View File
@@ -0,0 +1,472 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <cassert>
#include <cstdint>
#include <limits>
#include <type_traits>
namespace milvus {
namespace bits {
/**
* @brief Safely rounds up a value to the nearest multiple of factor.
* @tparam T Integer type of the value.
* @tparam U Integer type of the factor.
* @param value The number to be rounded.
* @param factor The multiple to round up to.
* @return Rounded value. For positive values, rounds up; for negative values,
* rounds towards zero (e.g., roundUp(-5, 4) = -4).
* @note This function uses assert() to detect overflow conditions, which
* should not occur in normal usage. If overflow is detected, the program
* will terminate in debug builds.
*/
template <
typename T,
typename U,
typename std::enable_if_t<std::is_integral_v<T> && std::is_integral_v<U>,
int> = 0>
constexpr T
roundUp(T value, U factor) noexcept {
// 1. Handle base cases for safety
if (factor <= 0) {
return value;
}
if (value == 0) {
return 0;
}
// 2. Handle signed integer edge cases (T_MIN) before any type conversion
// This must be done before converting to CommonT to avoid sign issues
if constexpr (std::is_signed_v<T>) {
if (value == std::numeric_limits<T>::min()) {
T factor_t = static_cast<T>(factor);
// For T_MIN, if it's already a multiple of factor, return it
// Otherwise, we cannot round up without overflow
if (value % factor_t == 0) {
return value;
}
// Overflow: T_MIN that's not a multiple cannot be rounded up
assert(false && "roundUp: T_MIN value cannot be rounded up");
__builtin_trap();
}
}
// 3. Choose appropriate common type that preserves sign of T
// If T is signed, use a signed common type to avoid sign conversion issues
using CommonT =
std::conditional_t<std::is_signed_v<T>,
std::make_signed_t<std::common_type_t<T, U>>,
std::common_type_t<T, U>>;
const CommonT v = static_cast<CommonT>(value);
const CommonT f = static_cast<CommonT>(factor);
// 4. Optimization for Power of Two (Common in memory alignment)
// Check if factor is a power of two: (f & (f - 1)) == 0
if (f > 0 && (f & (f - 1)) == 0) {
// Check for potential overflow before bitwise operations
if constexpr (std::is_signed_v<CommonT>) {
if (v > 0 && v > (std::numeric_limits<CommonT>::max() - f + 1)) {
assert(false &&
"roundUp: overflow detected in power-of-two path");
__builtin_trap();
}
} else {
if (v > (std::numeric_limits<CommonT>::max() - f + 1)) {
assert(false &&
"roundUp: overflow detected in power-of-two path");
__builtin_trap();
}
}
return static_cast<T>((v + f - 1) & ~(f - 1));
}
// 5. General safe implementation for positive and negative values
if constexpr (std::is_signed_v<CommonT>) {
if (v > 0) {
// Positive values: round up using ((v-1)/f + 1) * f
CommonT num_factors = (v - 1) / f + 1;
if (num_factors > std::numeric_limits<CommonT>::max() / f) {
assert(false &&
"roundUp: overflow detected in positive value path");
__builtin_trap();
}
return static_cast<T>(num_factors * f);
} else {
// Negative values: round towards zero (e.g., -5 -> -4 for factor 4)
// This is the standard behavior for alignment operations
return static_cast<T>((v / f) * f);
}
} else {
// Unsigned types: only positive values possible
CommonT num_factors = (v - 1) / f + 1;
if (num_factors > std::numeric_limits<CommonT>::max() / f) {
assert(false && "roundUp: overflow detected in unsigned path");
__builtin_trap();
}
return static_cast<T>(num_factors * f);
}
}
constexpr uint64_t
nBytes(int32_t value) {
return roundUp(value, 8) / 8;
}
constexpr inline uint64_t
lowMask(int32_t bits) {
if (bits >= 64)
return ~0ULL;
if (bits <= 0)
return 0ULL;
return (1UL << bits) - 1;
}
/**
* @brief Gets and clears the last (least significant) set bit.
* @param bits Reference to the bitmask to modify.
* @return The position (0-based) of the cleared bit, counting from the least significant bit.
* @note The input bits must have at least one bit set. If bits is 0, the behavior is undefined
* and will trigger an assertion failure.
*/
inline int32_t
getAndClearLastSetBit(uint16_t& bits) {
// __builtin_ctz(0) is undefined behavior, so we must check for zero
// we will not call this function if bits is 0
assert(bits != 0 && "getAndClearLastSetBit: input bits must not be zero");
if (bits == 0) {
__builtin_trap();
}
int32_t trailingZeros = __builtin_ctz(bits);
bits &= bits - 1;
return trailingZeros;
}
constexpr inline uint64_t
highMask(int32_t bits) {
if (bits >= 64)
return ~0ULL;
if (bits <= 0)
return 0ULL;
return lowMask(bits) << (64 - bits);
}
/**
* Invokes a function for each batch of bits (partial or full words)
* in a given range.
*
* @param begin first bit to check (inclusive)
* @param end last bit to check (exclusive)
* @param partialWordFunc function to invoke for a partial word;
* takes index of the word and mask
* @param fullWordFunc function to invoke for a full word;
* takes index of the word
*/
template <typename PartialWordFunc, typename FullWordFunc>
inline void
forEachWord(int32_t begin,
int32_t end,
PartialWordFunc partialWordFunc,
FullWordFunc fullWordFunc) {
if (begin >= end) {
return;
}
int32_t firstWord = roundUp(begin, 64);
int32_t lastWord = end & ~63L;
if (lastWord < firstWord) {
partialWordFunc(lastWord / 64,
lowMask(end - lastWord) & highMask(firstWord - begin));
return;
}
if (begin != firstWord) {
partialWordFunc(begin / 64, highMask(firstWord - begin));
}
for (int32_t i = firstWord; i + 64 <= lastWord; i += 64) {
fullWordFunc(i / 64);
}
if (end != lastWord) {
partialWordFunc(lastWord / 64, lowMask(end - lastWord));
}
}
inline int32_t
countBitsScalar(const uint64_t* bits, int32_t begin, int32_t end) {
int32_t count = 0;
forEachWord(
begin,
end,
[&count, bits](int32_t idx, uint64_t mask) {
count += __builtin_popcountll(bits[idx] & mask);
},
[&count, bits](int32_t idx) {
count += __builtin_popcountll(bits[idx]);
});
return count;
}
#ifdef __AVX2__
#include <immintrin.h>
inline int32_t
countBitsAVX2(const uint64_t* bits, int32_t begin, int32_t end) {
int32_t count = 0;
// Convert bit indices to word-aligned bit indices (same logic as forEachWord)
int32_t firstWordBit = roundUp(begin, 64); // first word-aligned bit index
int32_t lastWordBit = end & ~63L; // last word-aligned bit index
// Handle case where range spans less than one word
if (lastWordBit < firstWordBit) {
// begin and end are in the same word
int32_t wordIdx = begin >> 6;
uint64_t offset = begin & 63;
uint64_t mask = highMask(64 - offset) & lowMask(end - (wordIdx << 6));
return __builtin_popcountll(bits[wordIdx] & mask);
}
// Handle partial word at the beginning (if begin is not word-aligned)
if (begin != firstWordBit) {
int32_t wordIdx = begin >> 6;
uint64_t offset = begin & 63;
uint64_t mask = highMask(64 - offset);
count += __builtin_popcountll(bits[wordIdx] & mask);
}
// Process full words using unrolled scalar loop
// Note: Modern CPUs have fast scalar POPCNT (1 cycle latency, 1 per cycle throughput).
// The previous AVX2 implementation only used SIMD for load/store, then did scalar
// popcount anyway, adding overhead without benefit. This simple unrolled loop is
// faster and more maintainable.
int32_t wordIdx = firstWordBit >> 6;
int32_t lastWordIdx = lastWordBit >> 6;
for (; wordIdx + 4 <= lastWordIdx; wordIdx += 4) {
count += __builtin_popcountll(bits[wordIdx]);
count += __builtin_popcountll(bits[wordIdx + 1]);
count += __builtin_popcountll(bits[wordIdx + 2]);
count += __builtin_popcountll(bits[wordIdx + 3]);
}
// Handle remaining full words
for (; wordIdx < lastWordIdx; ++wordIdx) {
count += __builtin_popcountll(bits[wordIdx]);
}
// Handle partial word at the end (if end is not word-aligned)
if (end != lastWordBit) {
int32_t wordIdx = lastWordBit >> 6;
uint64_t mask = lowMask(end & 63);
count += __builtin_popcountll(bits[wordIdx] & mask);
}
return count;
}
#endif
#ifdef __AVX512F__
#include <immintrin.h>
inline int32_t
countBitsAVX512(const uint64_t* bits, int32_t begin, int32_t end) {
if (begin >= end) {
return 0;
}
int32_t count = 0;
// Convert bit indices to word-aligned bit indices (same logic as forEachWord)
int32_t firstWordBit = roundUp(begin, 64); // first word-aligned bit index
int32_t lastWordBit = end & ~63L; // last word-aligned bit index
// Handle case where range spans less than one word
if (lastWordBit < firstWordBit) {
// begin and end are in the same word
int32_t wordIdx = begin >> 6;
uint64_t offset = begin & 63;
uint64_t mask = highMask(64 - offset) & lowMask(end - (wordIdx << 6));
return __builtin_popcountll(bits[wordIdx] & mask);
}
// Handle partial word at the beginning (if begin is not word-aligned)
if (begin != firstWordBit) {
int32_t wordIdx = begin >> 6;
uint64_t offset = begin & 63;
uint64_t mask = highMask(64 - offset);
count += __builtin_popcountll(bits[wordIdx] & mask);
}
// Process full words using AVX-512 (8 words at a time)
int32_t wordIdx = firstWordBit >> 6;
int32_t lastWordIdx = lastWordBit >> 6;
for (; wordIdx + 8 <= lastWordIdx; wordIdx += 8) {
__m512i v = _mm512_loadu_si512(bits + wordIdx);
__m512i c = _mm512_popcnt_epi64(v);
count += static_cast<int32_t>(_mm512_reduce_add_epi64(c));
}
// Handle remaining full words
for (; wordIdx < lastWordIdx; ++wordIdx) {
count += __builtin_popcountll(bits[wordIdx]);
}
// Handle partial word at the end (if end is not word-aligned)
if (end != lastWordBit) {
int32_t wordIdx = lastWordBit >> 6;
uint64_t mask = lowMask(end & 63);
count += __builtin_popcountll(bits[wordIdx] & mask);
}
return count;
}
#endif
inline int32_t
countBits(const uint64_t* bits, int32_t begin, int32_t end) {
const int32_t range = end - begin;
if (range < 256) {
return countBitsScalar(bits, begin, end);
}
#ifdef __AVX512F__
if (range >= 2048) {
return countBitsAVX512(bits, begin, end);
}
#endif
#ifdef __AVX2__
return countBitsAVX2(bits, begin, end);
#else
return countBitsScalar(bits, begin, end);
#endif
}
inline bool
isPowerOfTwo(uint64_t size) {
return (size > 0) && ((size & (size - 1)) == 0);
}
template <typename T = uint64_t>
inline int32_t
countLeadingZeros(T word) {
static_assert(std::is_same_v<T, uint64_t> ||
std::is_same_v<T, __uint128_t>);
/// Built-in Function: int __builtin_clz (unsigned int x) returns the number
/// of leading 0-bits in x, starting at the most significant bit position. If
/// x is 0, the result is undefined.
if (word == 0) {
return sizeof(T) * 8;
}
if constexpr (std::is_same_v<T, uint64_t>) {
return __builtin_clzll(word);
} else {
uint64_t hi = word >> 64;
uint64_t lo = static_cast<uint64_t>(word);
return (hi == 0) ? 64 + __builtin_clzll(lo) : __builtin_clzll(hi);
}
}
inline uint64_t
nextPowerOfTwo(uint64_t size) {
if (size == 0) {
return 0;
}
uint32_t bits = 63 - countLeadingZeros(size);
uint64_t lower = 1ULL << bits;
// Size is a power of 2.
if (lower == size) {
return size;
}
return 2 * lower;
}
// This is the Hash128to64 function from Google's cityhash (available
// under the MIT License). We use it to reduce multiple 64 bit hashes
// into a single hash.
#if defined(FOLLY_DISABLE_UNDEFINED_BEHAVIOR_SANITIZER)
FOLLY_DISABLE_UNDEFINED_BEHAVIOR_SANITIZER("unsigned-integer-overflow")
#endif
inline uint64_t
hashMix(const uint64_t upper, const uint64_t lower) noexcept {
// Murmur-inspired hashing.
const uint64_t kMul = 0x9ddfea08eb382d69ULL;
uint64_t a = (lower ^ upper) * kMul;
a ^= (a >> 47);
uint64_t b = (upper ^ a) * kMul;
b ^= (b >> 47);
b *= kMul;
return b;
}
/// Extract bits from integer 'a' at the corresponding bit locations specified
/// by 'mask' to contiguous low bits in return value; the remaining upper bits
/// in return value are set to zero.
template <typename T>
inline T
extractBits(T a, T mask);
#ifdef __BMI2__
template <>
inline uint32_t
extractBits(uint32_t a, uint32_t mask) {
return _pext_u32(a, mask);
}
template <>
inline uint64_t
extractBits(uint64_t a, uint64_t mask) {
return _pext_u64(a, mask);
}
#else
template <typename T>
inline T
extractBits(T a, T mask) {
static_assert(std::is_unsigned_v<T>, "extractBits requires unsigned type");
// 1. first try to use BMI2 intrinsic
#ifdef __BMI2__
if constexpr (sizeof(T) == 8) {
return _pext_u64(static_cast<uint64_t>(a), static_cast<uint64_t>(mask));
} else {
// For uint32_t, uint16_t, uint8_t, use 32-bit PEXT
return static_cast<T>(
_pext_u32(static_cast<uint32_t>(a), static_cast<uint32_t>(mask)));
}
#else
// 2. Scalar fallback implementation
T dst = 0;
for (int k = 0; mask != 0; ++k) {
// Use ctz implementation for different widths
int shift;
if constexpr (sizeof(T) <= sizeof(unsigned int)) {
shift = __builtin_ctz(static_cast<unsigned int>(mask));
} else {
shift = __builtin_ctzll(static_cast<unsigned long long>(mask));
}
// Core extraction logic
dst |= ((a >> shift) & 1) << k;
// Clear the lowest set bit in mask (efficient clearing method)
mask &= (mask - 1);
}
return dst;
#endif
}
#endif
} // namespace bits
} // namespace milvus
@@ -0,0 +1,60 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "Vector.h"
namespace milvus {
void
BaseVector::prepareForReuse(milvus::VectorPtr& vector,
milvus::vector_size_t size) {
// Guard against null shared_ptr dereference
AssertInfo(vector != nullptr,
"BaseVector::prepareForReuse: vector cannot be null");
// Use use_count() instead of unique() (deprecated in C++17, removed in C++20)
if (vector.use_count() != 1) {
// When vector is non-unique (shared by multiple owners), create a new
// instance of the same dynamic type using the virtual factory method to
// preserve the subclass type (e.g., ColumnVector, RowVector) instead of
// creating a BaseVector.
vector = vector->cloneEmpty(size);
} else {
// When vector is unique (only one owner), reuse it by resetting state
// and resizing. This preserves subclass state (e.g., ColumnVector's
// values_, RowVector's children_values_).
vector->prepareForReuse();
vector->resize(size);
}
}
void
BaseVector::prepareForReuse() {
null_count_ = std::nullopt;
}
void
RowVector::resize(milvus::vector_size_t new_size, bool setNotNull) {
BaseVector::resize(new_size, setNotNull);
// Always propagate resize to all children to maintain the invariant that
// child->size() == row->size(). This ensures consistency when shrinking
// as well as growing, preventing children from being longer than the row.
for (auto& child : childrens()) {
child->resize(new_size, setNotNull);
}
}
} // namespace milvus
+71
View File
@@ -731,4 +731,75 @@ InitScalarFieldData(const DataType& type, bool nullable, int64_t cap_rows) {
}
}
void
ResizeScalarFieldData(const DataType& type,
int64_t new_num_rows,
FieldDataPtr& field_data) {
switch (type) {
case DataType::BOOL: {
auto inner_field_data =
std::dynamic_pointer_cast<FieldData<bool>>(field_data);
inner_field_data->resize_field_data(new_num_rows);
return;
}
case DataType::INT8: {
auto inner_field_data =
std::dynamic_pointer_cast<FieldData<int8_t>>(field_data);
inner_field_data->resize_field_data(new_num_rows);
return;
}
case DataType::INT16: {
auto inner_field_data =
std::dynamic_pointer_cast<FieldData<int16_t>>(field_data);
inner_field_data->resize_field_data(new_num_rows);
return;
}
case DataType::INT32: {
auto inner_field_data =
std::dynamic_pointer_cast<FieldData<int32_t>>(field_data);
inner_field_data->resize_field_data(new_num_rows);
return;
}
case DataType::TIMESTAMPTZ:
case DataType::INT64: {
auto inner_field_data =
std::dynamic_pointer_cast<FieldData<int64_t>>(field_data);
inner_field_data->resize_field_data(new_num_rows);
return;
}
case DataType::FLOAT: {
auto inner_field_data =
std::dynamic_pointer_cast<FieldData<float>>(field_data);
inner_field_data->resize_field_data(new_num_rows);
return;
}
case DataType::DOUBLE: {
auto inner_field_data =
std::dynamic_pointer_cast<FieldData<double>>(field_data);
inner_field_data->resize_field_data(new_num_rows);
return;
}
case DataType::STRING:
case DataType::VARCHAR:
case DataType::TEXT: {
auto inner_field_data =
std::dynamic_pointer_cast<FieldData<std::string>>(field_data);
AssertInfo(inner_field_data != nullptr,
"Failed to cast field_data to FieldData<std::string>");
inner_field_data->resize_field_data(new_num_rows);
return;
}
case DataType::JSON: {
auto inner_field_data =
std::dynamic_pointer_cast<FieldData<Json>>(field_data);
inner_field_data->resize_field_data(new_num_rows);
return;
}
default:
ThrowInfo(DataTypeInvalid,
"ResizeScalarFieldData not support data type " +
GetDataTypeName(type));
}
}
} // namespace milvus
+6 -1
View File
@@ -446,4 +446,9 @@ using FieldDataChannelPtr = std::shared_ptr<FieldDataChannel>;
FieldDataPtr
InitScalarFieldData(const DataType& type, bool nullable, int64_t cap_rows);
} // namespace milvus
void
ResizeScalarFieldData(const DataType& type,
int64_t new_size,
FieldDataPtr& field_data);
} // namespace milvus
@@ -362,6 +362,7 @@ class FieldDataImpl : public FieldDataBase {
data_ = std::move(data);
Assert(data_.size() % dim == 0);
num_rows_ = data_.size() / dim;
length_ = num_rows_;
}
explicit FieldDataImpl(size_t dim,
@@ -376,6 +377,7 @@ class FieldDataImpl : public FieldDataBase {
valid_data_ = std::move(valid_data);
Assert(data_.size() % dim == 0);
num_rows_ = data_.size() / dim;
length_ = num_rows_;
}
void
+21 -1
View File
@@ -291,7 +291,9 @@ class Schema {
FieldId
get_field_id(const FieldName& field_name) const {
AssertInfo(name_ids_.count(field_name), "Cannot find field_name");
AssertInfo(name_ids_.count(field_name),
"Cannot find field_name:{}",
field_name.get());
return name_ids_.at(field_name);
}
@@ -424,6 +426,24 @@ class Schema {
const FieldMeta&
GetFirstArrayFieldInStruct(const std::string& struct_name) const;
DataType
GetFieldType(const FieldId& field_id) const {
AssertInfo(fields_.count(field_id),
"field_id:{} does not exist in the schema",
field_id.get());
auto& meta = fields_.at(field_id);
return meta.get_data_type();
}
const std::string&
GetFieldName(const FieldId& field_id) const {
AssertInfo(fields_.count(field_id),
"field_id:{} does not exist in the schema",
field_id.get());
auto& meta = fields_.at(field_id);
return meta.get_name().get();
}
private:
int64_t debug_id = START_USER_FIELDID;
std::vector<FieldId> field_ids_;
+173
View File
@@ -0,0 +1,173 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <xsimd/xsimd.hpp>
#include "common/BitUtil.h"
namespace milvus {
// Generic fallback implementation for toBitMask when no architecture-specific
// implementation is available
template <typename T, typename A>
int
genericToBitMask(xsimd::batch_bool<T, A> mask) {
constexpr size_t size = xsimd::batch_bool<T, A>::size;
int result = 0;
// Convert batch_bool to integer batch and extract each element
// Using select to convert boolean mask to integer values
auto ones = xsimd::batch<T, A>(T(1));
auto zeros = xsimd::batch<T, A>(T(0));
auto int_batch = xsimd::select(mask, ones, zeros);
// Extract each element and set corresponding bit in result
alignas(A::alignment()) T values[size];
int_batch.store_aligned(values);
for (size_t i = 0; i < size; ++i) {
if (values[i] != T(0)) {
result |= (1 << i);
}
}
return result;
}
template <typename T, typename A, size_t kSizeT = sizeof(T)>
struct BitMask;
template <typename T, typename A>
struct BitMask<T, A, 1> {
static constexpr int kAllSet =
milvus::bits::lowMask(xsimd::batch_bool<T, A>::size);
#if XSIMD_WITH_AVX2
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::avx2&) {
return _mm256_movemask_epi8(mask);
}
#endif
#if XSIMD_WITH_SSE2
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::sse2&) {
return _mm_movemask_epi8(mask);
}
#endif
#if XSIMD_WITH_NEON
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::neon&) {
alignas(A::alignment()) static const int8_t kShift[] = {
-7, -6, -5, -4, -3, -2, -1, 0, -7, -6, -5, -4, -3, -2, -1, 0};
int8x16_t vshift = vld1q_s8(kShift);
uint8x16_t vmask = vshlq_u8(vandq_u8(mask, vdupq_n_u8(0x80)), vshift);
return (vaddv_u8(vget_high_u8(vmask)) << 8) |
vaddv_u8(vget_low_u8(vmask));
}
#endif
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::generic&) {
return genericToBitMask(mask);
}
};
template <typename T, typename A>
struct BitMask<T, A, 2> {
static constexpr int kAllSet =
milvus::bits::lowMask(xsimd::batch_bool<T, A>::size);
#if XSIMD_WITH_AVX2
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::avx2&) {
// There is no intrinsic for extracting high bits of a 16x16
// vector. Hence take every second bit of the high bits of a 32x1
// vector.
//
// NOTE: TVL might have a more efficient implementation for this.
return bits::extractBits<uint32_t>(_mm256_movemask_epi8(mask),
0xAAAAAAAA);
}
#endif
#if XSIMD_WITH_SSE2
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::sse2&) {
return milvus::bits::extractBits<uint32_t>(_mm_movemask_epi8(mask),
0xAAAA);
}
#endif
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::generic&) {
return genericToBitMask(mask);
}
};
template <typename T, typename A>
struct BitMask<T, A, 4> {
static constexpr int kAllSet =
milvus::bits::lowMask(xsimd::batch_bool<T, A>::size);
#if XSIMD_WITH_AVX
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::avx&) {
return _mm256_movemask_ps(reinterpret_cast<__m256>(mask.data));
}
#endif
#if XSIMD_WITH_SSE2
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::sse2&) {
return _mm_movemask_ps(reinterpret_cast<__m128>(mask.data));
}
#endif
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::generic&) {
return genericToBitMask(mask);
}
};
template <typename T, typename A>
struct BitMask<T, A, 8> {
static constexpr int kAllSet =
milvus::bits::lowMask(xsimd::batch_bool<T, A>::size);
#if XSIMD_WITH_AVX
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::avx&) {
return _mm256_movemask_pd(reinterpret_cast<__m256d>(mask.data));
}
#endif
#if XSIMD_WITH_SSE2
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::sse2&) {
return _mm_movemask_pd(reinterpret_cast<__m128d>(mask.data));
}
#endif
static int
toBitMask(xsimd::batch_bool<T, A> mask, const xsimd::generic&) {
return genericToBitMask(mask);
}
};
template <typename T, typename A = xsimd::default_arch>
auto
toBitMask(xsimd::batch_bool<T, A> mask, const A& arch = {}) {
return BitMask<T, A>::toBitMask(mask, arch);
}
} // namespace milvus
+77
View File
@@ -0,0 +1,77 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "Types.h"
#include "common/Exception.h"
#include "common/EasyAssert.h"
const RowTypePtr RowType::None = std::make_shared<const RowType>(
std::vector<std::string>{}, std::vector<milvus::DataType>{});
namespace milvus {
bool
IsFixedSizeType(DataType type) {
switch (type) {
case DataType::NONE:
return false;
case DataType::BOOL:
return TypeTraits<DataType::BOOL>::IsFixedWidth;
case DataType::INT8:
return TypeTraits<DataType::INT8>::IsFixedWidth;
case DataType::INT16:
return TypeTraits<DataType::INT16>::IsFixedWidth;
case DataType::INT32:
return TypeTraits<DataType::INT32>::IsFixedWidth;
case DataType::INT64:
return TypeTraits<DataType::INT64>::IsFixedWidth;
case DataType::TIMESTAMPTZ:
return TypeTraits<DataType::TIMESTAMPTZ>::IsFixedWidth;
case DataType::FLOAT:
return TypeTraits<DataType::FLOAT>::IsFixedWidth;
case DataType::DOUBLE:
return TypeTraits<DataType::DOUBLE>::IsFixedWidth;
case DataType::STRING:
return TypeTraits<DataType::STRING>::IsFixedWidth;
case DataType::VARCHAR:
return TypeTraits<DataType::VARCHAR>::IsFixedWidth;
case DataType::ARRAY:
return TypeTraits<DataType::ARRAY>::IsFixedWidth;
case DataType::JSON:
return TypeTraits<DataType::JSON>::IsFixedWidth;
case DataType::ROW:
return TypeTraits<DataType::ROW>::IsFixedWidth;
case DataType::VECTOR_BINARY:
return TypeTraits<DataType::VECTOR_BINARY>::IsFixedWidth;
case DataType::VECTOR_FLOAT:
return TypeTraits<DataType::VECTOR_FLOAT>::IsFixedWidth;
case DataType::GEOMETRY:
return TypeTraits<DataType::GEOMETRY>::IsFixedWidth;
case DataType::TEXT:
return TypeTraits<DataType::TEXT>::IsFixedWidth;
case DataType::VECTOR_FLOAT16:
return TypeTraits<DataType::VECTOR_FLOAT16>::IsFixedWidth;
case DataType::VECTOR_BFLOAT16:
return TypeTraits<DataType::VECTOR_BFLOAT16>::IsFixedWidth;
case DataType::VECTOR_SPARSE_U32_F32:
return TypeTraits<DataType::VECTOR_SPARSE_U32_F32>::IsFixedWidth;
case DataType::VECTOR_INT8:
return TypeTraits<DataType::VECTOR_INT8>::IsFixedWidth;
case DataType::VECTOR_ARRAY:
return TypeTraits<DataType::VECTOR_ARRAY>::IsFixedWidth;
default:
ThrowInfo(DataTypeInvalid, "unknown data type: {}", type);
}
}
} // namespace milvus
+129
View File
@@ -66,6 +66,8 @@ using int8 = knowhere::int8;
using sparse_u32_f32 = knowhere::sparse_u32_f32;
// See also: https://github.com/milvus-io/milvus-proto/blob/master/proto/schema.proto
using vector_size_t = int32_t;
enum class DataType {
NONE = 0,
BOOL = 1,
@@ -806,6 +808,42 @@ struct TypeTraits<DataType::VECTOR_FLOAT> {
static constexpr const char* Name = "VECTOR_FLOAT";
};
template <>
struct TypeTraits<DataType::VECTOR_FLOAT16> {
using NativeType = float16;
static constexpr DataType TypeKind = DataType::VECTOR_FLOAT16;
static constexpr bool IsPrimitiveType = false;
static constexpr bool IsFixedWidth = true;
static constexpr const char* Name = "VECTOR_FLOAT16";
};
template <>
struct TypeTraits<DataType::VECTOR_BFLOAT16> {
using NativeType = bfloat16;
static constexpr DataType TypeKind = DataType::VECTOR_BFLOAT16;
static constexpr bool IsPrimitiveType = false;
static constexpr bool IsFixedWidth = true;
static constexpr const char* Name = "VECTOR_BFLOAT16";
};
template <>
struct TypeTraits<DataType::VECTOR_SPARSE_U32_F32> {
using NativeType = void;
static constexpr DataType TypeKind = DataType::VECTOR_SPARSE_U32_F32;
static constexpr bool IsPrimitiveType = false;
static constexpr bool IsFixedWidth = false;
static constexpr const char* Name = "VECTOR_SPARSE_U32_F32";
};
template <>
struct TypeTraits<DataType::VECTOR_INT8> {
using NativeType = int8_t;
static constexpr DataType TypeKind = DataType::VECTOR_INT8;
static constexpr bool IsPrimitiveType = false;
static constexpr bool IsFixedWidth = true;
static constexpr const char* Name = "VECTOR_INT8";
};
template <>
struct TypeTraits<DataType::VECTOR_ARRAY> {
using NativeType = void;
@@ -856,6 +894,9 @@ vector_bytes_per_element(const DataType data_type, int64_t dim) {
}
}
bool
IsFixedSizeType(DataType type);
} // namespace milvus
template <>
struct fmt::formatter<milvus::DataType> : formatter<string_view> {
@@ -1382,3 +1423,91 @@ struct fmt::formatter<milvus::ErrorCode> : fmt::formatter<std::string> {
return fmt::formatter<std::string>::format(name, ctx);
}
};
using column_index_t = uint32_t;
class RowType final {
public:
RowType(std::vector<std::string>&& names,
std::vector<milvus::DataType>&& types)
: names_(std::move(names)), columns_types_(std::move(types)) {
AssertInfo(names_.size() == columns_types_.size(),
"Name count:{} and column count:{} must be the same",
names_.size(),
columns_types_.size());
};
static const std::shared_ptr<const RowType> None;
column_index_t
GetChildIndex(const std::string& name) const {
std::optional<column_index_t> idx;
for (auto i = 0; i < names_.size(); i++) {
if (names_[i] == name) {
idx = i;
break;
}
}
AssertInfo(idx.has_value(),
"Cannot find target column in the rowType list");
return idx.value();
}
milvus::DataType
column_type(uint32_t idx) const {
AssertInfo(idx >= 0 && idx < names_.size(),
"try to get column type from row type with wrong index:{}",
idx);
return columns_types_.at(idx);
}
size_t
column_count() const {
return names_.size();
}
private:
const std::vector<std::string> names_;
const std::vector<milvus::DataType> columns_types_;
};
using RowTypePtr = std::shared_ptr<const RowType>;
#define MILVUS_DYNAMIC_TYPE_DISPATCH(TEMPLATE_FUNC, DATETYPE, ...) \
MILVUS_DYNAMIC_TYPE_DISPATCH_IMPL(TEMPLATE_FUNC, , DATETYPE, __VA_ARGS__)
#define MILVUS_DYNAMIC_TYPE_DISPATCH_IMPL(PREFIX, SUFFIX, DATATYPE, ...) \
[&]() { \
switch (DATATYPE) { \
case milvus::DataType::BOOL: \
return PREFIX<milvus::DataType::BOOL> SUFFIX(__VA_ARGS__); \
case milvus::DataType::INT8: \
return PREFIX<milvus::DataType::INT8> SUFFIX(__VA_ARGS__); \
case milvus::DataType::INT16: \
return PREFIX<milvus::DataType::INT16> SUFFIX(__VA_ARGS__); \
case milvus::DataType::INT32: \
return PREFIX<milvus::DataType::INT32> SUFFIX(__VA_ARGS__); \
case milvus::DataType::INT64: \
return PREFIX<milvus::DataType::INT64> SUFFIX(__VA_ARGS__); \
case milvus::DataType::TIMESTAMPTZ: \
return PREFIX<milvus::DataType::TIMESTAMPTZ> SUFFIX( \
__VA_ARGS__); \
case milvus::DataType::FLOAT: \
return PREFIX<milvus::DataType::FLOAT> SUFFIX(__VA_ARGS__); \
case milvus::DataType::DOUBLE: \
return PREFIX<milvus::DataType::DOUBLE> SUFFIX(__VA_ARGS__); \
case milvus::DataType::VARCHAR: \
return PREFIX<milvus::DataType::VARCHAR> SUFFIX(__VA_ARGS__); \
case milvus::DataType::STRING: \
return PREFIX<milvus::DataType::STRING> SUFFIX(__VA_ARGS__); \
case milvus::DataType::JSON: \
return PREFIX<milvus::DataType::JSON> SUFFIX(__VA_ARGS__); \
case milvus::DataType::ARRAY: \
return PREFIX<milvus::DataType::ARRAY> SUFFIX(__VA_ARGS__); \
case milvus::DataType::ROW: \
return PREFIX<milvus::DataType::ROW> SUFFIX(__VA_ARGS__); \
default: \
ThrowInfo(milvus::DataTypeInvalid, \
"UnsupportedDataType for " \
"MILVUS_DYNAMIC_TYPE_DISPATCH_IMPL"); \
} \
}()
+220
View File
@@ -20,9 +20,11 @@
#include <cstring>
#include <cmath>
#include <filesystem>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
#include "common/Consts.h"
@@ -352,4 +354,222 @@ class Defer {
#define DeferLambda(fn) Defer Defer_##__COUNTER__(fn);
template <typename T>
FOLLY_ALWAYS_INLINE int
comparePrimitiveAsc(const T& left, const T& right) {
if constexpr (std::is_floating_point<T>::value) {
bool leftNan = std::isnan(left);
bool rightNan = std::isnan(right);
if (leftNan) {
return rightNan ? 0 : 1;
}
if (rightNan) {
return -1;
}
}
return left < right ? -1 : left == right ? 0 : 1;
}
inline std::string
lowerString(const std::string& str) {
std::string ret;
ret.resize(str.size());
std::transform(str.begin(), str.end(), ret.begin(), [](unsigned char c) {
return std::tolower(c);
});
return ret;
}
template <typename T>
T
checkPlus(const T& a, const T& b, const char* typeName = "integer") {
static_assert(std::is_integral_v<T>, "checkPlus requires integral type");
#if defined(__GNUC__) || defined(__clang__)
// Use compiler builtin for GCC/Clang
T result;
bool overflow = __builtin_add_overflow(a, b, &result);
if (UNLIKELY(overflow)) {
ThrowInfo(DataTypeInvalid, "{} overflow: {} + {}", typeName, a, b);
}
return result;
#else
// Portable fallback for MSVC and other compilers
constexpr T max_val = std::numeric_limits<T>::max();
constexpr T min_val = std::numeric_limits<T>::min();
bool overflow = false;
if (b > 0) {
// Positive addition: check if a > max - b
if (a > max_val - b) {
overflow = true;
}
} else if (b < 0) {
// Negative addition: check if a < min - b
if (a < min_val - b) {
overflow = true;
}
}
// If b == 0, no overflow possible
if (UNLIKELY(overflow)) {
ThrowInfo(DataTypeInvalid, "{} overflow: {} + {}", typeName, a, b);
}
return a + b;
#endif
}
template <typename T>
T
checkedMultiply(const T& a, const T& b, const char* typeName = "integer") {
static_assert(std::is_integral_v<T>,
"checkedMultiply requires integral type");
#if defined(__GNUC__) || defined(__clang__)
// Use compiler builtin for GCC/Clang
T result;
bool overflow = __builtin_mul_overflow(a, b, &result);
if (UNLIKELY(overflow)) {
ThrowInfo(DataTypeInvalid, "{} overflow: {} * {}", typeName, a, b);
}
return result;
#else
// Portable fallback for MSVC and other compilers
constexpr T max_val = std::numeric_limits<T>::max();
constexpr T min_val = std::numeric_limits<T>::min();
// Handle zero case: no overflow if either operand is zero
if (a == 0 || b == 0) {
return 0;
}
bool overflow = false;
if constexpr (std::is_signed_v<T>) {
// Signed type: handle MIN edge case and use absolute values
if (a == min_val) {
// Special case: if a is MIN, only safe multiplications are with
// 0 (already handled), 1, or -1
if (b != 1 && b != -1) {
overflow = true;
}
} else if (b == min_val) {
// Special case: if b is MIN, only safe multiplications are with
// 0 (already handled), 1, or -1
if (a != 1 && a != -1) {
overflow = true;
}
} else {
// Use division-based check: |a| > max_val / |b| implies overflow
// Compute absolute values carefully to avoid overflow
T abs_a = (a < 0) ? -a : a;
T abs_b = (b < 0) ? -b : b;
// Check: abs_a > max_val / abs_b
// This works because both operands are not MIN (handled above)
// and not zero (handled above)
if (abs_a > max_val / abs_b) {
overflow = true;
}
}
} else {
// Unsigned type: simpler case
// Check: a > max_val / b
if (a > max_val / b) {
overflow = true;
}
}
if (UNLIKELY(overflow)) {
ThrowInfo(DataTypeInvalid, "{} overflow: {} * {}", typeName, a, b);
}
return a * b;
#endif
}
inline const char* const KSum = "sum";
inline const char* const KMin = "min";
inline const char* const KMax = "max";
inline const char* const KCount = "count";
inline const char* const KAvg = "avg";
inline DataType
GetAggResultType(std::string func_name, DataType input_type) {
if (func_name == KSum) {
switch (input_type) {
case DataType::INT8:
case DataType::INT16:
case DataType::INT32:
case DataType::INT64: {
return DataType::INT64;
}
case DataType::TIMESTAMPTZ: {
return DataType::TIMESTAMPTZ;
}
case DataType::FLOAT: {
return DataType::DOUBLE;
}
case DataType::DOUBLE: {
return DataType::DOUBLE;
}
default: {
ThrowInfo(DataTypeInvalid,
"Unsupported data type for type:{}",
input_type);
}
}
}
if (func_name == KAvg) {
switch (input_type) {
case DataType::INT8:
case DataType::INT16:
case DataType::INT32:
case DataType::INT64:
case DataType::FLOAT:
case DataType::DOUBLE: {
return DataType::DOUBLE;
}
default: {
ThrowInfo(DataTypeInvalid,
"Unsupported data type for {} aggregation: {}",
func_name,
input_type);
}
}
}
if (func_name == KMin || func_name == KMax) {
// min/max keep the original scalar type.
switch (input_type) {
case DataType::INT8:
case DataType::INT16:
case DataType::INT32:
case DataType::INT64:
case DataType::FLOAT:
case DataType::DOUBLE:
case DataType::VARCHAR:
case DataType::STRING:
case DataType::TEXT:
case DataType::TIMESTAMPTZ: {
return input_type;
}
default: {
ThrowInfo(DataTypeInvalid,
"Unsupported data type for {} aggregation: {}",
func_name,
input_type);
}
}
}
if (func_name == KCount) {
return DataType::INT64;
}
ThrowInfo(OpTypeInvalid, "Unsupported func type:{}", func_name);
}
inline int32_t
Align(int32_t number, int32_t alignment) {
AssertInfo(alignment > 0 && (alignment & (alignment - 1)) == 0,
"Alignment must be a power of 2, got {}",
alignment);
return (number + alignment - 1) & ~(alignment - 1);
}
} // namespace milvus
+205 -4
View File
@@ -26,6 +26,8 @@
#include "common/Types.h"
namespace milvus {
class BaseVector;
using VectorPtr = std::shared_ptr<BaseVector>;
/**
* @brief base class for different type vector
@@ -41,15 +43,45 @@ class BaseVector {
virtual ~BaseVector() = default;
int64_t
size() {
size() const {
return length_;
}
DataType
type() {
type() const {
return type_kind_;
}
int32_t
elementSize() const {
return GetDataTypeSize(type_kind_);
};
size_t
nullCount() const {
return null_count_.has_value() ? null_count_.value() : 0;
}
virtual void
resize(vector_size_t newSize, bool setNotNull = true) {
length_ = newSize;
}
static void
prepareForReuse(VectorPtr& vector, vector_size_t size);
/// Resets non-reusable buffers and updates child vectors by calling
/// BaseVector::prepareForReuse.
/// Base implementation checks and resets nulls buffer if needed. Keeps the
/// nulls buffer if singly-referenced, mutable and has at least one null bit set.
virtual void
prepareForReuse();
/// Creates a new empty instance of the same dynamic type with the given size.
/// Used when cloning non-unique vectors to preserve the dynamic type.
virtual VectorPtr
cloneEmpty(vector_size_t size) const = 0;
protected:
DataType type_kind_;
size_t length_;
@@ -57,8 +89,6 @@ class BaseVector {
std::optional<size_t> null_count_;
};
using VectorPtr = std::shared_ptr<BaseVector>;
/**
* SimpleVector abstracts over various Columnar Storage Formats,
* it is used in custom functions.
@@ -110,6 +140,23 @@ class ColumnVector final : public SimpleVector {
std::move(bitmap));
}
ColumnVector(FieldDataPtr&& value, TargetBitmap&& valid_bitmap)
: SimpleVector(value ? value->get_data_type() : DataType::NONE,
value ? value->Length() : 0),
is_bitmap_(false),
valid_values_(std::move(valid_bitmap)) {
AssertInfo(value, "ColumnVector value cannot be null");
values_ = std::move(value);
// Compute null_count_ from valid_bitmap: count false bits (nulls)
size_t nulls = 0;
for (size_t i = 0; i < valid_values_.size(); ++i) {
if (!valid_values_[i]) {
++nulls;
}
}
null_count_ = (nulls > 0) ? std::optional<size_t>(nulls) : std::nullopt;
}
virtual ~ColumnVector() override {
values_.reset();
valid_values_.reset();
@@ -120,6 +167,43 @@ class ColumnVector final : public SimpleVector {
return reinterpret_cast<char*>(GetRawData()) + index * size_of_element;
}
template <typename T>
T
ValueAt(size_t index) const {
AssertInfo(index < length_, "ValueAt index out of range");
return *(reinterpret_cast<T*>(GetRawData()) + index);
}
template <typename T>
void
SetValueAt(size_t index, const T& value) {
AssertInfo(index < length_, "SetValueAt index out of range");
*(reinterpret_cast<T*>(values_->Data()) + index) = value;
}
void
nullAt(size_t index) {
// Update null_count_ when setting a value to null
if (valid_values_[index]) {
// Was valid, now null: increment count
null_count_ = null_count_.value_or(0) + 1;
}
valid_values_.set(index, false);
}
void
clearNullAt(size_t index) {
// Update null_count_ when clearing a null value
if (!valid_values_[index]) {
// Was null, now valid: decrement count
if (null_count_.has_value()) {
null_count_ =
(null_count_.value() > 0) ? null_count_.value() - 1 : 0;
}
}
valid_values_.set(index, true);
}
bool
ValidAt(size_t index) override {
return valid_values_[index];
@@ -146,6 +230,92 @@ class ColumnVector final : public SimpleVector {
return is_bitmap_;
}
void
resize(vector_size_t new_size, bool setNotNull = true) override {
AssertInfo(!is_bitmap_, "Cannot resize bitmap column vector");
auto old_size = length_;
BaseVector::resize(new_size, setNotNull);
ResizeScalarFieldData(type(), new_size, values_);
if (new_size > old_size) {
// Growing: new bits are added
valid_values_.resize(new_size);
if (setNotNull) {
// All new bits are set to valid (true), no nulls added
for (auto i = old_size; i < new_size; ++i) {
valid_values_.set(i, true);
}
// null_count_ unchanged (no new nulls)
} else {
// New bits default to null (false)
for (auto i = old_size; i < new_size; ++i) {
valid_values_.set(i, false);
}
// Update null_count_: add (new_size - old_size) nulls
null_count_ = null_count_.value_or(0) + (new_size - old_size);
}
} else if (new_size < old_size) {
// Shrinking: need to count nulls in removed range and subtract
size_t removed_nulls = 0;
for (auto i = new_size; i < old_size; ++i) {
if (!valid_values_[i]) {
++removed_nulls;
}
}
valid_values_.resize(new_size);
// Update null_count_: subtract removed nulls
if (removed_nulls > 0 && null_count_.has_value()) {
null_count_ = (null_count_.value() >= removed_nulls)
? null_count_.value() - removed_nulls
: 0;
}
}
// If new_size == old_size, no change needed
}
void
append(const ColumnVector& other) {
// Validate that both vectors have the same type
AssertInfo(type() == other.type(),
"Cannot append ColumnVector with different type: {} != {}",
static_cast<int>(type()),
static_cast<int>(other.type()));
auto old_size = length_;
values_->FillFieldData(other.GetRawData(), other.size());
length_ += other.size();
valid_values_.resize(length_);
// Copy validity from other
for (size_t i = 0; i < other.size(); ++i) {
valid_values_.set(old_size + i, other.valid_values_[i]);
}
// Update null_count_ by accumulating the original null_count_ and other's null count
size_t other_nulls = 0;
if (other.null_count_.has_value()) {
// Use cached null_count_ if available
other_nulls = other.null_count_.value();
} else {
// Compute null count from other.valid_values_ if not cached
for (size_t i = 0; i < other.size(); ++i) {
if (!other.valid_values_[i]) {
++other_nulls;
}
}
}
// Accumulate null counts
size_t total_nulls = null_count_.value_or(0) + other_nulls;
null_count_ = (total_nulls > 0) ? std::optional<size_t>{total_nulls}
: std::nullopt;
}
VectorPtr
cloneEmpty(vector_size_t size) const override {
return std::make_shared<ColumnVector>(type(), size, std::nullopt);
}
private:
bool is_bitmap_; // TODO: remove the field after implementing BitmapVector
FieldDataPtr values_;
@@ -186,6 +356,12 @@ class ConstantVector : public SimpleVector {
return is_null_;
}
VectorPtr
cloneEmpty(vector_size_t size) const override {
return std::make_shared<ConstantVector<T>>(
type(), size, val_, std::nullopt);
}
private:
T val_;
bool is_null_;
@@ -226,6 +402,18 @@ class RowVector : public BaseVector {
}
}
RowVector(const RowTypePtr& rowType,
size_t size,
std::optional<size_t> nullCount = std::nullopt)
: BaseVector(DataType::ROW, size, nullCount) {
auto column_count = rowType->column_count();
for (auto i = 0; i < column_count; i++) {
auto column_type = rowType->column_type(i);
children_values_.emplace_back(
std::make_shared<ColumnVector>(column_type, size));
}
}
const std::vector<VectorPtr>&
childrens() const {
return children_values_;
@@ -237,6 +425,19 @@ class RowVector : public BaseVector {
return children_values_[index];
}
void
resize(vector_size_t new_size, bool setNotNull = true) override;
VectorPtr
cloneEmpty(vector_size_t size) const override {
// Extract data types from existing children to preserve structure
std::vector<DataType> data_types;
for (const auto& child : children_values_) {
data_types.push_back(child->type());
}
return std::make_shared<RowVector>(data_types, size, std::nullopt);
}
private:
std::vector<VectorPtr> children_values_;
};
+40
View File
@@ -0,0 +1,40 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <cmath>
#include <limits>
#include <type_traits>
#include <folly/Hash.h>
namespace milvus {
template <typename FLOAT,
std::enable_if_t<std::is_floating_point<FLOAT>::value, bool> = true>
struct NaNAwareHash {
std::size_t
operator()(const FLOAT& val) const noexcept {
static const std::size_t kNanHash =
folly::hasher<FLOAT>{}(std::numeric_limits<FLOAT>::quiet_NaN());
if (std::isnan(val)) {
return kNanHash;
}
return folly::hasher<FLOAT>{}(val);
}
};
} // namespace milvus
+46 -13
View File
@@ -20,10 +20,10 @@
#include <cassert>
#include <memory>
#include <atomic>
#include "common/EasyAssert.h"
#include "exec/operator/CallbackSink.h"
#include "exec/operator/CountNode.h"
#include "exec/operator/FilterBitsNode.h"
#include "exec/operator/IterativeFilterNode.h"
#include "exec/operator/MvccNode.h"
@@ -31,9 +31,11 @@
#include "exec/operator/RescoresNode.h"
#include "exec/operator/VectorSearchNode.h"
#include "exec/operator/RandomSampleNode.h"
#include "exec/operator/GroupByNode.h"
#include "exec/operator/ElementFilterNode.h"
#include "exec/operator/ElementFilterBitsNode.h"
#include "exec/operator/SearchGroupByNode.h"
#include "exec/operator/AggregationNode.h"
#include "exec/operator/ProjectNode.h"
#include "exec/Task.h"
#include "plan/PlanNode.h"
@@ -76,24 +78,30 @@ DriverFactory::CreateDriver(std::unique_ptr<DriverContext> ctx,
tracer::AddEvent("create_operator: MvccNode");
operators.push_back(
std::make_unique<PhyMvccNode>(id, ctx.get(), mvccnode));
} else if (auto countnode =
std::dynamic_pointer_cast<const plan::CountNode>(
plannode)) {
tracer::AddEvent("create_operator: CountNode");
operators.push_back(
std::make_unique<PhyCountNode>(id, ctx.get(), countnode));
} else if (auto vectorsearchnode =
std::dynamic_pointer_cast<const plan::VectorSearchNode>(
plannode)) {
tracer::AddEvent("create_operator: VectorSearchNode");
operators.push_back(std::make_unique<PhyVectorSearchNode>(
id, ctx.get(), vectorsearchnode));
} else if (auto groupbynode =
std::dynamic_pointer_cast<const plan::GroupByNode>(
} else if (auto searchGroupByNode =
std::dynamic_pointer_cast<const plan::SearchGroupByNode>(
plannode)) {
tracer::AddEvent("create_operator: GroupByNode");
tracer::AddEvent("create_operator: SearchGroupByNode");
operators.push_back(std::make_unique<PhySearchGroupByNode>(
id, ctx.get(), searchGroupByNode));
} else if (auto queryGroupByNode =
std::dynamic_pointer_cast<const plan::AggregationNode>(
plannode)) {
tracer::AddEvent("create_operator: AggregationNode");
operators.push_back(std::make_unique<PhyAggregationNode>(
id, ctx.get(), queryGroupByNode));
} else if (auto projectNode =
std::dynamic_pointer_cast<const plan::ProjectNode>(
plannode)) {
tracer::AddEvent("create_operator: ProjectNode");
operators.push_back(
std::make_unique<PhyGroupByNode>(id, ctx.get(), groupbynode));
std::make_unique<PhyProjectNode>(id, ctx.get(), projectNode));
} else if (auto samplenode =
std::dynamic_pointer_cast<const plan::RandomSampleNode>(
plannode)) {
@@ -171,6 +179,31 @@ Driver::Run(std::shared_ptr<Driver> self) {
}
}
void
Driver::initializeOperators() {
// Atomically check and set: only the first thread to call this will
// get false (the previous value) and proceed with initialization.
// Other threads will get true and skip initialization.
// Use memory barriers to ensure initialization writes are visible
// before the flag is seen by other threads.
if (!operatorsInitialized_.exchange(true, std::memory_order_acq_rel)) {
// This thread won the initialization race. Perform initialization.
for (auto& op : operators_) {
op->initialize();
}
// Use release semantics to ensure all initialization writes are
// visible to other threads before they see the flag as true.
// The flag was already set to true by exchange above, but this
// barrier ensures all writes from initialization are visible.
std::atomic_thread_fence(std::memory_order_release);
} else {
// Another thread is initializing or has completed initialization.
// Use acquire semantics to ensure we see all initialization writes
// after the flag becomes true.
operatorsInitialized_.load(std::memory_order_acquire);
}
}
void
Driver::Init(std::unique_ptr<DriverContext> ctx,
std::vector<std::unique_ptr<Operator>> operators) {
@@ -230,13 +263,13 @@ Driver::RunInternal(std::shared_ptr<Driver>& self,
std::shared_ptr<BlockingState>& blocking_state,
RowVectorPtr& result) {
try {
initializeOperators();
int num_operators = operators_.size();
ContinueFuture future;
for (;;) {
for (int32_t i = num_operators - 1; i >= 0; --i) {
auto op = operators_[i].get();
current_operator_index_ = i;
CALL_OPERATOR(
blocking_reason_ = op->IsBlocked(&future), op, "IsBlocked");
+8
View File
@@ -16,6 +16,7 @@
#pragma once
#include <atomic>
#include <functional>
#include <memory>
#include <string>
@@ -219,6 +220,11 @@ class Driver : public std::enable_shared_from_this<Driver> {
EnqueueInternal() {
}
/// Invoked to initialize the operators from this driver once on its first
/// execution.
void
initializeOperators();
static void
Run(std::shared_ptr<Driver> self);
@@ -238,6 +244,8 @@ class Driver : public std::enable_shared_from_this<Driver> {
size_t current_operator_index_{0};
std::atomic_bool operatorsInitialized_{false};
BlockingReason blocking_reason_{BlockingReason::kNotBlocked};
friend struct DriverFactory;
+275
View File
@@ -0,0 +1,275 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "HashTable.h"
#include <cstring>
#include <memory>
#include <new>
#include "common/SimdUtil.h"
namespace milvus {
namespace exec {
void
BaseHashTable::prepareForGroupProbe(HashLookup& lookup,
const RowVectorPtr& input) {
auto& hashers = lookup.hashers_;
int numKeys = hashers.size();
// set up column vector to each column
for (auto i = 0; i < numKeys; i++) {
auto& hasher = hashers[i];
auto column_idx = hasher->ChannelIndex();
ColumnVectorPtr column_ptr =
std::dynamic_pointer_cast<ColumnVector>(input->child(column_idx));
AssertInfo(column_ptr != nullptr,
"Failed to get column vector from row vector input");
hashers[i]->setColumnData(column_ptr);
}
lookup.reset(input->size());
const auto mode = hashMode();
for (auto i = 0; i < hashers.size(); i++) {
if (mode == BaseHashTable::HashMode::kHash) {
hashers[i]->hash(i > 0, lookup.hashes_);
} else {
ThrowInfo(
milvus::OpTypeInvalid,
"Not support target hashMode, only support kHash for now");
}
}
}
class ProbeState {
public:
enum class Operation { kProbe, kInsert, kErase };
static constexpr int32_t kFullMask = 0xffff;
int32_t
row() const {
return row_;
}
template <typename Table>
inline void
preProbe(const Table& table, uint64_t hash, int32_t row) {
row_ = row;
bucketOffset_ = table.bucketOffset(hash);
const auto tag = BaseHashTable::hashTag(hash);
wantedTags_ = BaseHashTable::TagVector::broadcast(tag);
group_ = nullptr;
__builtin_prefetch(reinterpret_cast<uint8_t*>(table.table_) +
bucketOffset_);
}
template <Operation op = Operation::kInsert, typename Table>
inline void
firstProbe(const Table& table) {
tagsInTable_ = BaseHashTable::loadTags(
reinterpret_cast<uint8_t*>(table.table_), bucketOffset_);
hits_ = milvus::toBitMask(tagsInTable_ == wantedTags_);
if (hits_) {
loadNextHit<op>(table);
}
}
template <Operation op, typename Compare, typename Insert, typename Table>
inline char*
fullProbe(Table& table, Compare compare, Insert insert) {
AssertInfo(op == Operation::kInsert,
"Only support insert operation for group cases");
if (group_ && compare(group_, row_)) {
return group_;
}
const auto kEmptyGroup = BaseHashTable::TagVector::broadcast(0);
for (int64_t numProbedBuckets = 0;
numProbedBuckets < table.numBuckets();
++numProbedBuckets) {
while (hits_ > 0) {
loadNextHit<op>(table);
if (compare(group_, row_)) {
return group_;
}
}
uint16_t empty =
milvus::toBitMask(tagsInTable_ == kEmptyGroup) & kFullMask;
// if there are still empty slot available, try to insert into existing empty slot or tombstone slot
if (empty > 0) {
auto pos = milvus::bits::getAndClearLastSetBit(empty);
return insert(row_, bucketOffset_ + pos);
}
bucketOffset_ = table.nextBucketOffset(bucketOffset_);
tagsInTable_ = table.loadTags(bucketOffset_);
hits_ = milvus::toBitMask(tagsInTable_ == wantedTags_);
}
ThrowInfo(UnexpectedError,
"Slots in hash table is not enough for hash operation, fail "
"the request");
}
private:
static constexpr uint8_t kNotSet = 0xff;
template <Operation op, typename Table>
inline void
loadNextHit(Table& table) {
const int32_t hit = milvus::bits::getAndClearLastSetBit(hits_);
group_ = table.row(bucketOffset_, hit);
__builtin_prefetch(group_);
}
char* group_;
BaseHashTable::TagVector wantedTags_;
BaseHashTable::TagVector tagsInTable_;
int32_t row_;
int64_t bucketOffset_;
BaseHashTable::MaskType hits_;
//uint8_t indexInTags_ = kNotSet;
};
void
HashTable::allocateTables(uint64_t size) {
AssertInfo(milvus::bits::isPowerOfTwo(size),
"Size:{} for allocating tables must be a power of two",
size);
AssertInfo(size > 0,
"Size:{} for allocating tables must be larger than zero",
size);
// Free existing table if present
if (table_ != nullptr) {
::operator delete(table_, std::align_val_t(64));
table_ = nullptr;
}
capacity_ = size;
const uint64_t byteSize = capacity_ * tableSlotSize();
AssertInfo(byteSize % kBucketSize == 0,
"byteSize:{} for hashTable must be a multiple of kBucketSize:{}",
byteSize,
kBucketSize);
numBuckets_ = byteSize / kBucketSize;
sizeMask_ = byteSize - 1;
sizeBits_ = __builtin_popcountll(sizeMask_);
bucketOffsetMask_ = sizeMask_ & ~(kBucketSize - 1);
// The total size is 8 bytes per slot, in groups of 16 slots with 16 bytes of
// tags and 16 * 6 bytes of pointers and a padding of 16 bytes to round up the
// cache line.
// Allocate aligned memory (64-byte cache line alignment) for the table buffer.
// TODO support memory pool here to avoid OOM
table_ = static_cast<char*>(::operator new(byteSize, std::align_val_t(64)));
std::memset(table_, 0, byteSize);
}
void
HashTable::checkSizeAndAllocateTable(int32_t numNew) {
AssertInfo(capacity_ == 0 || capacity_ > numDistinct_,
"capacity_ {}, numDistinct {}",
capacity_,
numDistinct_);
if (table_ == nullptr || capacity_ == 0) {
const auto newSize = newHashTableEntriesNumber(numDistinct_, numNew);
allocateTables(newSize);
}
}
bool
HashTable::compareKeys(const char* group,
milvus::exec::HashLookup& lookup,
milvus::vector_size_t row) {
int32_t numKeys = lookup.hashers_.size();
for (int32_t i = 0; i < numKeys; i++) {
auto& hasher = lookup.hashers_[i];
if (!rows_->equals(
group, rows()->columnAt(i), hasher->columnData(), row)) {
return false;
}
}
return true;
}
void
HashTable::storeKeys(milvus::exec::HashLookup& lookup,
milvus::vector_size_t row) {
for (int32_t i = 0; i < lookup.hashers_.size(); i++) {
auto& hasher = lookup.hashers_[i];
rows_->store(hasher->columnData(), row, lookup.hits_[row], i);
}
}
void
HashTable::storeRowPointer(uint64_t index, uint64_t hash, char* row) {
const int64_t bktOffset = bucketOffset(index);
auto* bucket = bucketAt(bktOffset);
const auto slotIndex = index & (sizeof(TagVector) - 1);
bucket->setTag(slotIndex, hashTag(hash));
bucket->setPointer(slotIndex, row);
}
char*
HashTable::insertEntry(milvus::exec::HashLookup& lookup,
uint64_t index,
milvus::vector_size_t row) {
char* group = rows_->newRow();
lookup.hits_[row] = group;
storeKeys(lookup, row);
storeRowPointer(index, lookup.hashes_[row], group);
numDistinct_++;
lookup.newGroups_.push_back(row);
return group;
}
FOLLY_ALWAYS_INLINE void
HashTable::fullProbe(HashLookup& lookup, ProbeState& state) {
constexpr ProbeState::Operation op = ProbeState::Operation::kInsert;
lookup.hits_[state.row()] = state.fullProbe<op>(
*this,
[&](char* group, int32_t row) {
return compareKeys(group, lookup, row);
},
[&](int32_t row, uint64_t index) {
return insertEntry(lookup, index, row);
});
}
void
HashTable::groupProbe(milvus::exec::HashLookup& lookup) {
AssertInfo(hashMode_ == HashMode::kHash, "Only support kHash mode for now");
checkSizeAndAllocateTable(0);
ProbeState state;
for (int32_t idx = 0; idx < lookup.hashes_.size(); idx++) {
state.preProbe(*this, lookup.hashes_[idx], idx);
state.firstProbe<ProbeState::Operation::kInsert>(*this);
fullProbe(lookup, state);
}
}
void
HashTable::setHashMode(HashMode mode, int32_t numNew) {
// TODO set hash mode kArray/kHash/kNormalizedKey
}
void
HashTable::clear(bool freeTable) {
if (table_) {
::operator delete(table_, std::align_val_t(64));
table_ = nullptr;
}
numDistinct_ = 0;
capacity_ = 0;
numBuckets_ = 0;
sizeMask_ = 0;
bucketOffsetMask_ = 0;
}
} // namespace exec
} // namespace milvus
+344
View File
@@ -0,0 +1,344 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <vector>
#include <memory>
#include <array>
#include <cstring>
#include "VectorHasher.h"
#include "exec/operator/query-agg/RowContainer.h"
#include "xsimd/xsimd.hpp"
#include "common/BitUtil.h"
namespace milvus {
namespace exec {
struct HashLookup {
explicit HashLookup(
const std::vector<std::unique_ptr<VectorHasher>>& hashers)
: hashers_(hashers) {
}
void
reset(vector_size_t size) {
rows_.resize(size);
hashes_.resize(size);
hits_.resize(size);
newGroups_.clear();
}
/// One entry per group-by
const std::vector<std::unique_ptr<VectorHasher>>& hashers_;
/// Set of row numbers of row to probe.
std::vector<vector_size_t> rows_;
/// Hashes or value IDs for rows in 'rows'. Not aligned with 'rows'. Index is
/// the row number.
std::vector<uint64_t> hashes_;
/// Contains one entry for each row in 'rows'. Index is the row number.
/// For groupProbe, a pointer to an existing or new row with matching grouping
/// keys.
std::vector<char*> hits_;
/// For groupProbe, row numbers for which a new entry was inserted (didn't
/// exist before the groupProbe). Empty for joinProbe.
std::vector<vector_size_t> newGroups_;
};
class BaseHashTable {
public:
#if XSIMD_WITH_SSE2
using TagVector = xsimd::batch<uint8_t, xsimd::sse2>;
#elif XSIMD_WITH_NEON
using TagVector = xsimd::batch<uint8_t, xsimd::neon>;
#else
// Fallback for non-SIMD builds
using TagVector = std::array<uint8_t, 16>;
#endif
using MaskType = uint16_t;
enum class HashMode { kHash, kArray, kNormalizedKey };
explicit BaseHashTable(std::vector<std::unique_ptr<VectorHasher>>&& hashers)
: hashers_(std::move(hashers)) {
}
virtual ~BaseHashTable() = default;
RowContainer*
rows() const {
return rows_.get();
}
/// Extracts a 7 bit tag from a hash number. The high bit is always set.
static uint8_t
hashTag(uint64_t hash) {
// This is likely all 0 for small key types (<= 32 bits). Not an issue
// because small types have a range that makes them normalized key cases.
// If there are multiple small type keys, they are mixed which makes them a
// 64 bit hash. Normalized keys are mixed before being used as hash
// numbers.
return static_cast<uint8_t>(hash >> 38) | 0x80;
}
static FOLLY_ALWAYS_INLINE size_t
tableSlotSize() {
// Each slot is 8 bytes.
return sizeof(void*);
}
static TagVector
loadTags(uint8_t* tags, int64_t tagIndex) {
auto src = tags + tagIndex;
#if XSIMD_WITH_SSE2
return TagVector(
_mm_loadu_si128(reinterpret_cast<__m128i const*>(src)));
#elif XSIMD_WITH_NEON
return TagVector(vld1q_u8(src));
#else
// Generic fallback: load bytes into std::array<uint8_t, 16>
TagVector result;
std::memcpy(result.data(), src, result.size());
return result;
#endif
}
const std::vector<std::unique_ptr<VectorHasher>>&
hashers() const {
return hashers_;
}
/// Returns the hash mode. This is needed for the caller to calculate
/// the hash numbers using the appropriate method of the
/// VectorHashers of 'this'.
virtual HashMode
hashMode() const = 0;
virtual void
setHashMode(HashMode mode, int32_t numNew) = 0;
/// Disables use of array or normalized key hash modes.
void
forceGenericHashMode() {
setHashMode(HashMode::kHash, 0);
}
/// Populates 'hashes' and 'rows' fields in 'lookup' in preparation for
/// 'groupProbe' call. Rehashes the table if necessary. Uses lookup.hashes to
/// decode grouping keys from 'input'.
void
prepareForGroupProbe(HashLookup& lookup, const RowVectorPtr& input);
/// Finds or creates a group for each key in 'lookup'. The keys are
/// returned in 'lookup.hits'.
virtual void
groupProbe(HashLookup& lookup) = 0;
virtual void
clear(bool freeTable = false) = 0;
protected:
std::vector<std::unique_ptr<VectorHasher>> hashers_;
std::unique_ptr<RowContainer> rows_;
};
class ProbeState;
class HashTable : public BaseHashTable {
public:
HashTable(std::vector<std::unique_ptr<VectorHasher>>&& hashers,
const std::vector<Accumulator>& accumulators)
: BaseHashTable(std::move(hashers)) {
std::vector<DataType> keyTypes;
for (auto& hasher : hashers_) {
keyTypes.push_back(hasher->ChannelDataType());
}
hashMode_ = HashMode::kHash;
rows_ = std::make_unique<RowContainer>(keyTypes, accumulators);
};
~HashTable() override {
clear();
}
void
setHashMode(HashMode mode, int32_t numNew) override;
void
groupProbe(HashLookup& lookup) override;
// The table in non-kArray mode has a power of two number of buckets each with
// 16 slots. Each slot has a 1 byte tag (a field of hash number) and a 48 bit
// pointer. All the tags are in a 16 byte SIMD word followed by the 6 byte
// pointers. There are 16 bytes of padding at the end to make the bucket
// occupy exactly two (64 bytes) cache lines.
class Bucket {
public:
uint8_t
tagAt(int32_t slotIndex) {
return reinterpret_cast<uint8_t*>(&tags_)[slotIndex];
}
char*
pointerAt(int32_t slotIndex) {
return reinterpret_cast<char*>(
*reinterpret_cast<uintptr_t*>(
&pointers_[kPointerSize * slotIndex]) &
kPointerMask);
}
void
setTag(int32_t slotIndex, uint8_t tag) {
reinterpret_cast<uint8_t*>(&tags_)[slotIndex] = tag;
}
void
setPointer(int32_t slotIndex, void* pointer) {
auto* const slot = reinterpret_cast<uintptr_t*>(
&pointers_[slotIndex * kPointerSize]);
*slot =
(*slot & ~kPointerMask) | reinterpret_cast<uintptr_t>(pointer);
}
private:
static constexpr uint8_t kPointerSignificantBits = 48;
static constexpr uint64_t kPointerMask =
milvus::bits::lowMask(kPointerSignificantBits);
static constexpr int32_t kPointerSize = kPointerSignificantBits / 8;
TagVector tags_;
char pointers_[sizeof(TagVector) * kPointerSize];
char padding_[16];
};
static_assert(sizeof(Bucket) == 128);
static constexpr uint64_t kBucketSize = sizeof(Bucket);
Bucket*
bucketAt(int64_t offset) const {
//AssertInfo(offset&(kBucketSize-1)==0, "Invalid offset:{} and kBucketSize:{}", offset, kBucketSize);
return reinterpret_cast<Bucket*>(table_ + offset);
}
int64_t
bucketOffset(uint64_t hash) const {
return hash & bucketOffsetMask_;
}
int64_t
nextBucketOffset(int64_t bucketOffset) const {
//AssertInfo(bucketOffset&(kBucketSize - 1) == 0, "Invalid bucketOffset:{} for nextBucketOffset", bucketOffset);
AssertInfo(bucketOffset < sizeMask_,
"BucketOffset:{} must be less than sizeMask_:{} for "
"nextBucketOffset",
bucketOffset,
sizeMask_);
return sizeMask_ & (bucketOffset + kBucketSize);
}
bool
compareKeys(const char* group, HashLookup& lookup, vector_size_t row);
char*
row(int64_t bucketOffset, int32_t slotIndex) const {
return bucketAt(bucketOffset)->pointerAt(slotIndex);
}
int64_t
numBuckets() const {
return numBuckets_;
}
TagVector
loadTags(int64_t bucketOffset) const {
return BaseHashTable::loadTags(reinterpret_cast<uint8_t*>(table_),
bucketOffset);
}
char*
insertEntry(HashLookup& lookup, uint64_t index, vector_size_t row);
void
storeKeys(HashLookup& lookup, vector_size_t row);
void
storeRowPointer(uint64_t index, uint64_t hash, char* row);
// Allocates new tables for tags and payload pointers. The size must
// a power of 2.
void
allocateTables(uint64_t size);
void
fullProbe(HashLookup& lookup, ProbeState& state);
void
clear(bool freeTable = false) override;
void
checkSizeAndAllocateTable(int32_t numNew);
// Returns the number of entries after which the table gets rehashed.
static uint64_t
rehashSize(int64_t size) {
// This implements the F14 load factor: Resize if less than 1/8 unoccupied.
return size - (size / 8);
}
uint64_t
rehashSize() const {
return rehashSize(capacity_);
}
static uint64_t
newHashTableEntriesNumber(uint64_t numDistinct, uint64_t numNew) {
auto numNewEntries =
std::max((uint64_t)2048,
milvus::bits::nextPowerOfTwo(numNew * 2 + numDistinct));
const auto newNumDistinct = numDistinct + numNew;
if (newNumDistinct > rehashSize(numNewEntries)) {
numNewEntries *= 2;
}
return numNewEntries;
}
private:
HashMode hashMode_ = HashMode::kHash;
int64_t bucketOffsetMask_{0};
int64_t numBuckets_{0};
int64_t numDistinct_{0};
// Number of slots across all buckets.
int64_t capacity_{0};
// Mask for extracting low bits of hash number for use as byte offsets into
// the table. This is set to 'capacity_ * sizeof(void*) - 1'.
int64_t sizeMask_{0};
int8_t sizeBits_;
int64_t numRehashes_{0};
char* table_ = nullptr;
HashMode
hashMode() const override {
return hashMode_;
}
friend class ProbeState;
};
} // namespace exec
} // namespace milvus
+77
View File
@@ -0,0 +1,77 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "VectorHasher.h"
#include "common/float_util_c.h"
#include <folly/Hash.h>
#include "common/BitUtil.h"
namespace milvus {
namespace exec {
std::vector<std::unique_ptr<VectorHasher>>
createVectorHashers(const RowTypePtr& rowType,
const std::vector<expr::FieldAccessTypeExprPtr>& exprs) {
std::vector<std::unique_ptr<VectorHasher>> hashers;
hashers.reserve(exprs.size());
for (const auto& expr : exprs) {
auto column_idx = rowType->GetChildIndex(expr->name());
hashers.emplace_back(VectorHasher::create(expr->type(), column_idx));
}
return hashers;
}
template <DataType Type>
void
VectorHasher::hashValues(const ColumnVectorPtr& column_data,
bool mix,
uint64_t* result) {
if constexpr (Type == DataType::ROW || Type == DataType::ARRAY ||
Type == DataType::JSON) {
ThrowInfo(milvus::DataTypeInvalid,
"hash not supported for complex types ROW/ARRAY/JSON: {}",
Type);
} else {
using T = typename TypeTraits<Type>::NativeType;
for (size_t row_idx = 0; row_idx < column_data->size(); ++row_idx) {
if (!column_data->ValidAt(row_idx)) {
result[row_idx] =
mix ? milvus::bits::hashMix(result[row_idx], kNullHash)
: kNullHash;
} else {
T raw_value = column_data->ValueAt<T>(row_idx);
uint64_t hash_value = kNullHash;
if constexpr (std::is_floating_point_v<T>) {
hash_value = milvus::NaNAwareHash<T>()(raw_value);
} else {
hash_value = folly::hasher<T>()(raw_value);
}
result[row_idx] =
mix ? milvus::bits::hashMix(result[row_idx], hash_value)
: hash_value;
}
}
}
}
void
VectorHasher::hash(bool mix, std::vector<uint64_t>& result) {
auto element_data_type = ChannelDataType();
MILVUS_DYNAMIC_TYPE_DISPATCH(
hashValues, element_data_type, columnData(), mix, result.data());
}
} // namespace exec
} // namespace milvus
+92
View File
@@ -0,0 +1,92 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include "common/Vector.h"
#include "common/Types.h"
#include "expr/ITypeExpr.h"
namespace milvus {
namespace exec {
class VectorHasher {
public:
VectorHasher(DataType data_type, column_index_t column_idx)
: channel_type_(data_type), channel_idx_(column_idx) {
}
static std::unique_ptr<VectorHasher>
create(DataType data_type, column_index_t col_idx) {
return std::make_unique<VectorHasher>(data_type, col_idx);
}
column_index_t
ChannelIndex() const {
return channel_idx_;
}
DataType
ChannelDataType() const {
return channel_type_;
}
void
hash(bool mix, std::vector<uint64_t>& result);
static constexpr uint64_t kNullHash = 1;
static bool
typeSupportValueIds(DataType type) {
switch (type) {
case DataType::BOOL:
case DataType::INT8:
case DataType::INT16:
case DataType::INT32:
case DataType::INT64:
case DataType::VARCHAR:
case DataType::STRING:
return true;
default:
return false;
}
}
template <DataType type>
void
hashValues(const ColumnVectorPtr& column_data, bool mix, uint64_t* result);
void
setColumnData(const ColumnVectorPtr& column_data) {
column_data_ = column_data;
}
const ColumnVectorPtr&
columnData() const {
return column_data_;
}
private:
const column_index_t channel_idx_;
const DataType channel_type_;
ColumnVectorPtr column_data_;
};
std::vector<std::unique_ptr<VectorHasher>>
createVectorHashers(const RowTypePtr& rowType,
const std::vector<expr::FieldAccessTypeExprPtr>& exprs);
} // namespace exec
} // namespace milvus
+22
View File
@@ -16,6 +16,8 @@
#pragma once
#include <algorithm>
#include <fmt/core.h>
#include "common/EasyAssert.h"
@@ -289,5 +291,25 @@ GetValueWithCastNumber(const milvus::proto::plan::GenericValue& value_proto) {
}
}
// Locale-independent ASCII lowercase conversion
// Converts only ASCII uppercase letters (A-Z) to lowercase (a-z)
// Non-ASCII characters and non-uppercase characters remain unchanged
inline unsigned char
asciiToLower(unsigned char c) {
if (c >= 'A' && c <= 'Z') {
return static_cast<unsigned char>('a' + (c - 'A'));
}
return c;
}
inline std::string
sanitizeName(const std::string& name) {
std::string sanitizedName;
sanitizedName.resize(name.size());
std::transform(
name.begin(), name.end(), sanitizedName.begin(), asciiToLower);
return sanitizedName;
}
} // namespace exec
} // namespace milvus
@@ -15,9 +15,12 @@
// limitations under the License.
#include "exec/expression/function/FunctionFactory.h"
#include <mutex>
#include "exec/expression/function/impl/StringFunctions.h"
#include "log/Log.h"
#include "exec/operator/query-agg/CountAggregateBase.h"
#include "exec/operator/query-agg/MinAggregateBase.h"
#include "exec/operator/query-agg/MaxAggregateBase.h"
#include "exec/operator/query-agg/SumAggregateBase.h"
namespace milvus {
namespace exec {
@@ -56,7 +59,8 @@ FunctionFactory::RegisterAllFunctions() {
RegisterFilterFunction("starts_with",
{DataType::VARCHAR, DataType::VARCHAR},
function::StartsWithVarchar);
LOG_INFO("{} functions registered", GetFilterFunctionNum());
LOG_INFO("{} filter functions registered", GetFilterFunctionNum());
RegisterAggregateFunction();
}
void
@@ -68,6 +72,14 @@ FunctionFactory::RegisterFilterFunction(
func_name, func_param_type_list}] = func;
}
void
FunctionFactory::RegisterAggregateFunction() {
milvus::exec::registerCountAggregate();
milvus::exec::registerMinAggregate();
milvus::exec::registerMaxAggregate();
milvus::exec::registerSumAggregate();
}
const FilterFunctionPtr
FunctionFactory::GetFilterFunction(
const FilterFunctionRegisterKey& func_sig) const {
@@ -78,6 +78,9 @@ class FunctionFactory {
std::vector<DataType> func_param_type_list,
FilterFunctionPtr func);
void
RegisterAggregateFunction();
const FilterFunctionPtr
GetFilterFunction(const FilterFunctionRegisterKey& func_sig) const;
@@ -0,0 +1,71 @@
// Copyright (C) 2019-2020 Zilliz. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under the License
//
// Created by hanchun on 24-10-18.
//
#include "AggregationNode.h"
#include "common/Utils.h"
namespace milvus {
namespace exec {
PhyAggregationNode::PhyAggregationNode(
int32_t operator_id,
milvus::exec::DriverContext* ctx,
const std::shared_ptr<const plan::AggregationNode>& node)
: Operator(
ctx, node->output_type(), operator_id, node->id(), "AggregationNode"),
aggregationNode_(node),
isGlobal_(node->GroupingKeys().empty()) {
}
void
PhyAggregationNode::initialize() {
Operator::initialize();
// aggregation operator will always have single one source
const auto& input_type = aggregationNode_->sources()[0]->output_type();
auto hashers =
createVectorHashers(input_type, aggregationNode_->GroupingKeys());
auto numHashers = hashers.size();
std::vector<AggregateInfo> aggregateInfos =
toAggregateInfo(*aggregationNode_, *operator_context_, numHashers);
grouping_set_ = std::make_unique<GroupingSet>(
input_type, std::move(hashers), std::move(aggregateInfos));
aggregationNode_.reset();
}
void
PhyAggregationNode::AddInput(RowVectorPtr& input) {
grouping_set_->addInput(input);
numInputRows_ += input->size();
}
RowVectorPtr
PhyAggregationNode::GetOutput() {
if (finished_ || !no_more_input_) {
input_ = nullptr;
return nullptr;
}
DeferLambda([&]() { finished_ = true; });
const auto outputRowCount = isGlobal_ ? 1 : grouping_set_->outputRowCount();
output_ = std::make_shared<RowVector>(output_type_, outputRowCount);
const bool hasData = grouping_set_->getOutput(output_);
if (!hasData) {
return nullptr;
}
numOutputRows_ += output_->size();
return output_;
}
}; // namespace exec
}; // namespace milvus
@@ -0,0 +1,86 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include "exec/operator/Operator.h"
#include "exec/operator/query-agg/GroupingSet.h"
#include "common/Types.h"
namespace milvus {
namespace exec {
class PhyAggregationNode : public Operator {
public:
PhyAggregationNode(
int32_t operator_id,
DriverContext* ctx,
const std::shared_ptr<const plan::AggregationNode>& node);
bool
NeedInput() const override {
return true;
}
void
AddInput(RowVectorPtr& input) override;
RowVectorPtr
GetOutput() override;
bool
IsFinished() override {
return finished_;
}
bool
IsFilter() const override {
return false;
}
BlockingReason
IsBlocked(ContinueFuture* future) override {
return BlockingReason::kNotBlocked;
}
void
Close() override {
input_ = nullptr;
results_.clear();
}
void
initialize() override;
std::string
ToString() const override {
return "PhyAggregationNode";
}
private:
RowVectorPtr output_;
std::unique_ptr<GroupingSet> grouping_set_;
std::shared_ptr<const plan::AggregationNode> aggregationNode_;
const bool isGlobal_;
// Count the number of input rows. It is reset on partial aggregation output
// flush.
int64_t numInputRows_ = 0;
// Count the number of output rows. It is reset on partial aggregation output
// flush.
int64_t numOutputRows_ = 0;
bool finished_ = false;
};
} // namespace exec
} // namespace milvus
@@ -26,7 +26,7 @@ class CallbackSink : public Operator {
int32_t operator_id,
DriverContext* ctx,
std::function<BlockingReason(RowVectorPtr, ContinueFuture*)> callback)
: Operator(ctx, DataType::NONE, operator_id, "N/A", "CallbackSink"),
: Operator(ctx, RowType::None, operator_id, "N/A", "CallbackSink"),
callback_(callback) {
}
@@ -52,7 +52,7 @@ class CallbackSink : public Operator {
}
bool
IsFilter() override {
IsFilter() const override {
return false;
}
@@ -1,80 +0,0 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "CountNode.h"
#include "common/Tracer.h"
#include "fmt/format.h"
namespace milvus {
namespace exec {
static std::unique_ptr<milvus::RetrieveResult>
wrap_num_entities(int64_t cnt, int64_t size) {
auto retrieve_result = std::make_unique<milvus::RetrieveResult>();
DataArray arr;
arr.set_type(milvus::proto::schema::Int64);
auto scalar = arr.mutable_scalars();
scalar->mutable_long_data()->mutable_data()->Add(cnt);
retrieve_result->field_data_ = {arr};
retrieve_result->total_data_cnt_ = size;
return retrieve_result;
}
PhyCountNode::PhyCountNode(int32_t operator_id,
DriverContext* driverctx,
const std::shared_ptr<const plan::CountNode>& node)
: Operator(driverctx,
node->output_type(),
operator_id,
node->id(),
"PhyCountNode") {
ExecContext* exec_context = operator_context_->get_exec_context();
query_context_ = exec_context->get_query_context();
segment_ = query_context_->get_segment();
query_timestamp_ = query_context_->get_query_timestamp();
active_count_ = query_context_->get_active_count();
}
void
PhyCountNode::AddInput(RowVectorPtr& input) {
input_ = std::move(input);
}
RowVectorPtr
PhyCountNode::GetOutput() {
milvus::exec::checkCancellation(query_context_);
if (is_finished_ || !no_more_input_) {
return nullptr;
}
tracer::AutoSpan span("PhyCountNode::Execute", tracer::GetRootSpan(), true);
auto col_input = GetColumnVector(input_);
TargetBitmapView view(col_input->GetRawData(), col_input->size());
auto cnt = view.size() - view.count();
query_context_->set_retrieve_result(
std::move(*(wrap_num_entities(cnt, view.size()))));
is_finished_ = true;
tracer::AddEvent(fmt::format("count_result: {}", cnt));
return input_;
}
bool
PhyCountNode::IsFinished() {
return is_finished_;
}
} // namespace exec
} // namespace milvus
@@ -29,9 +29,9 @@ PhyElementFilterBitsNode::PhyElementFilterBitsNode(
const std::shared_ptr<const plan::ElementFilterBitsNode>&
element_filter_bits_node)
: Operator(driverctx,
DataType::NONE,
element_filter_bits_node->output_type(),
operator_id,
"element_filter_bits_plan_node",
element_filter_bits_node->id(),
"PhyElementFilterBitsNode"),
struct_name_(element_filter_bits_node->struct_name()) {
ExecContext* exec_context = operator_context_->get_exec_context();
@@ -38,7 +38,7 @@ class PhyElementFilterBitsNode : public Operator {
element_filter_bits_node);
bool
IsFilter() override {
IsFilter() const override {
return true;
}
@@ -38,7 +38,7 @@ class PhyElementFilterNode : public Operator {
element_filter_node);
bool
IsFilter() override {
IsFilter() const override {
return true;
}
@@ -17,7 +17,6 @@
#include "FilterBitsNode.h"
#include "common/Tracer.h"
#include "fmt/format.h"
#include "monitor/Monitor.h"
namespace milvus {
@@ -34,7 +34,7 @@ class PhyFilterBitsNode : public Operator {
const std::shared_ptr<const plan::FilterBitsNode>& filter);
bool
IsFilter() override {
IsFilter() const override {
return true;
}
@@ -37,7 +37,7 @@ class PhyIterativeFilterNode : public Operator {
const std::shared_ptr<const plan::FilterNode>& filter);
bool
IsFilter() override {
IsFilter() const override {
return true;
}
+4 -3
View File
@@ -17,6 +17,7 @@
#include "MvccNode.h"
#include "common/Tracer.h"
#include "fmt/format.h"
#include <iostream>
namespace milvus {
namespace exec {
@@ -54,12 +55,12 @@ PhyMvccNode::GetOutput() {
tracer::AutoSpan span("PhyMvccNode::Execute", tracer::GetRootSpan(), true);
if (!is_source_node_ && input_ == nullptr) {
if (active_count_ == 0) {
is_finished_ = true;
return nullptr;
}
if (active_count_ == 0) {
is_finished_ = true;
if (!is_source_node_ && input_ == nullptr) {
return nullptr;
}
+1 -1
View File
@@ -34,7 +34,7 @@ class PhyMvccNode : public Operator {
const std::shared_ptr<const plan::MvccNode>& mvcc_node);
bool
IsFilter() override {
IsFilter() const override {
return false;
}
+6 -1
View File
@@ -17,5 +17,10 @@
#include "Operator.h"
namespace milvus {
namespace exec {}
namespace exec {
void
Operator::initialize() {
// TODO check memory and set up memory pool in the future
}
} // namespace exec
} // namespace milvus
+20 -5
View File
@@ -94,16 +94,26 @@ class OperatorContext {
class Operator {
public:
Operator(DriverContext* ctx,
DataType output_type,
RowTypePtr output_type,
int32_t operator_id,
const std::string& plannode_id,
const std::string& operator_type)
: operator_context_(std::make_unique<OperatorContext>(
ctx, plannode_id, operator_id, operator_type)) {
ctx, plannode_id, operator_id, operator_type)),
output_type_(output_type) {
}
virtual ~Operator() = default;
/// Does initialization work for this operator which requires memory
/// allocation from memory pool that can't be done under operator constructor.
///
/// NOTE: the default implementation set 'initialized_' to true to ensure we
/// never call this more than once. The overload initialize() implementation
/// must call this base implementation first.
virtual void
initialize();
virtual bool
NeedInput() const = 0;
@@ -122,7 +132,7 @@ class Operator {
IsFinished() = 0;
virtual bool
IsFilter() = 0;
IsFilter() const = 0;
virtual BlockingReason
IsBlocked(ContinueFuture* future) = 0;
@@ -158,10 +168,15 @@ class Operator {
return "Base Operator";
}
virtual const RowTypePtr&
OutputType() const {
return output_type_;
}
protected:
std::unique_ptr<OperatorContext> operator_context_;
DataType output_type_;
RowTypePtr output_type_;
RowVectorPtr input_;
@@ -173,7 +188,7 @@ class Operator {
class SourceOperator : public Operator {
public:
SourceOperator(DriverContext* driver_ctx,
DataType out_type,
RowTypePtr out_type,
int32_t operator_id,
const std::string& plannode_id,
const std::string& operator_type)
@@ -0,0 +1,82 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ProjectNode.h"
#include "exec/expression/Utils.h"
#include "segcore/Utils.h"
namespace milvus {
namespace exec {
PhyProjectNode::PhyProjectNode(
int32_t operator_id,
milvus::exec::DriverContext* ctx,
const std::shared_ptr<const plan::ProjectNode>& projectNode)
: Operator(ctx,
projectNode->output_type(),
operator_id,
projectNode->id(),
"Project"),
fields_to_project_(projectNode->FieldsToProject()) {
auto exec_context = operator_context_->get_exec_context();
segment_ = exec_context->get_query_context()->get_segment();
op_context_ = exec_context->get_query_context()->get_op_context();
AssertInfo(op_context_, "op_context_ cannot be nullptr for ProjectNode");
AssertInfo(segment_, "segment_ cannot be nullptr for ProjectNode");
}
void
PhyProjectNode::AddInput(milvus::RowVectorPtr& input) {
input_ = std::move(input);
}
RowVectorPtr
PhyProjectNode::GetOutput() {
if (is_finished_ || input_ == nullptr) {
return nullptr;
}
auto col_input = GetColumnVector(input_);
// raw data view
TargetBitmapView raw_data_view(col_input->GetRawData(), col_input->size());
auto result_pair = segment_->find_first(-1, raw_data_view);
auto& selected_offsets = result_pair.first;
auto selected_count = selected_offsets.size();
auto row_type = OutputType();
std::vector<VectorPtr> column_vectors;
column_vectors.reserve(fields_to_project_.size());
for (int i = 0; i < fields_to_project_.size(); i++) {
auto column_type = row_type->column_type(i);
auto field_id = fields_to_project_.at(i);
TargetBitmap valid_map(selected_count);
auto field_data = bulk_script_field_data(op_context_,
field_id,
column_type,
selected_offsets.data(),
selected_count,
segment_,
valid_map,
true);
auto column_vector = std::make_shared<ColumnVector>(
std::move(field_data), std::move(valid_map));
column_vectors.emplace_back(column_vector);
}
is_finished_ = true;
auto row_vector = std::make_shared<RowVector>(std::move(column_vectors));
return row_vector;
}
}; // namespace exec
}; // namespace milvus
@@ -15,45 +15,36 @@
// limitations under the License.
#pragma once
#include <memory>
#include <string>
#include "exec/Driver.h"
#include "exec/expression/Expr.h"
#include "exec/operator/Operator.h"
#include "exec/QueryContext.h"
#include "Operator.h"
#include "plan/PlanNode.h"
namespace milvus {
namespace exec {
class PhyCountNode : public Operator {
class PhyProjectNode : public Operator {
public:
PhyCountNode(int32_t operator_id,
DriverContext* ctx,
const std::shared_ptr<const plan::CountNode>& node);
PhyProjectNode(int32_t operator_id,
DriverContext* ctx,
const std::shared_ptr<const plan::ProjectNode>& projectNode);
bool
IsFilter() override {
IsFilter() const override {
return false;
}
bool
NeedInput() const override {
return !is_finished_;
return true;
}
void
AddInput(RowVectorPtr& input);
AddInput(RowVectorPtr& input) override;
RowVectorPtr
GetOutput() override;
bool
IsFinished() override;
void
Close() override {
IsFinished() override {
return is_finished_;
}
BlockingReason
@@ -61,18 +52,16 @@ class PhyCountNode : public Operator {
return BlockingReason::kNotBlocked;
}
virtual std::string
std::string
ToString() const override {
return "PhyCountNode";
return "Project Operator";
}
private:
const segcore::SegmentInternalInterface* segment_;
milvus::Timestamp query_timestamp_;
int64_t active_count_;
QueryContext* query_context_;
bool is_finished_{false};
const std::vector<FieldId> fields_to_project_;
OpContext* op_context_;
};
} // namespace exec
} // namespace milvus
} // namespace milvus
@@ -31,7 +31,7 @@ class PhyRandomSampleNode : public Operator {
random_sample_node);
bool
IsFilter() override {
IsFilter() const override {
return false;
}
@@ -37,7 +37,7 @@ class PhyRescoresNode : public Operator {
const std::shared_ptr<const plan::RescoresNode>& scorer);
bool
IsFilter() override {
IsFilter() const override {
return true;
}
@@ -14,24 +14,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "GroupByNode.h"
#include "common/Tracer.h"
#include "fmt/format.h"
#include "exec/operator/groupby/SearchGroupByOperator.h"
#include "SearchGroupByNode.h"
#include "exec/operator/search-groupby/SearchGroupByOperator.h"
#include "monitor/Monitor.h"
namespace milvus {
namespace exec {
PhyGroupByNode::PhyGroupByNode(
PhySearchGroupByNode::PhySearchGroupByNode(
int32_t operator_id,
DriverContext* driverctx,
const std::shared_ptr<const plan::GroupByNode>& node)
const std::shared_ptr<const plan::SearchGroupByNode>& node)
: Operator(driverctx,
node->output_type(),
operator_id,
node->id(),
"PhyGroupByNode") {
"PhySearchGroupByNode") {
ExecContext* exec_context = operator_context_->get_exec_context();
query_context_ = exec_context->get_query_context();
segment_ = query_context_->get_segment();
@@ -39,14 +38,13 @@ PhyGroupByNode::PhyGroupByNode(
}
void
PhyGroupByNode::AddInput(RowVectorPtr& input) {
PhySearchGroupByNode::AddInput(RowVectorPtr& input) {
input_ = std::move(input);
}
RowVectorPtr
PhyGroupByNode::GetOutput() {
PhySearchGroupByNode::GetOutput() {
milvus::exec::checkCancellation(query_context_);
if (is_finished_ || !no_more_input_) {
return nullptr;
}
@@ -104,7 +102,7 @@ PhyGroupByNode::GetOutput() {
}
bool
PhyGroupByNode::IsFinished() {
PhySearchGroupByNode::IsFinished() {
return is_finished_;
}
@@ -27,14 +27,15 @@
namespace milvus {
namespace exec {
class PhyGroupByNode : public Operator {
class PhySearchGroupByNode : public Operator {
public:
PhyGroupByNode(int32_t operator_id,
DriverContext* ctx,
const std::shared_ptr<const plan::GroupByNode>& node);
PhySearchGroupByNode(
int32_t operator_id,
DriverContext* ctx,
const std::shared_ptr<const plan::SearchGroupByNode>& node);
bool
IsFilter() override {
IsFilter() const override {
return false;
}
@@ -63,7 +64,7 @@ class PhyGroupByNode : public Operator {
virtual std::string
ToString() const override {
return "PhyGroupByNode";
return "PhySearchGroupByNode";
}
private:
@@ -35,7 +35,7 @@ class PhyVectorSearchNode : public Operator {
const std::shared_ptr<const plan::VectorSearchNode>& search_node);
bool
IsFilter() override {
IsFilter() const override {
return false;
}
@@ -0,0 +1,70 @@
// Copyright (C) 2019-2020 Zilliz. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under the License
#include "Aggregate.h"
#include "common/Utils.h"
#include "exec/expression/Utils.h"
namespace milvus {
namespace exec {
void
Aggregate::setOffsetsInternal(int32_t offset,
int32_t nullByte,
uint8_t nullMask,
int32_t rowSizeOffset) {
offset_ = offset;
nullByte_ = nullByte;
nullMask_ = nullMask;
rowSizeOffset_ = rowSizeOffset;
}
const AggregateFunctionFactory*
getAggregateFunctionEntry(const std::string& name) {
// Use the same normalization as registration (lowerString) to ensure
// consistent lookup for non-ASCII and locale-sensitive characters.
auto normalizedName = lowerString(name);
return aggregateFunctions().withRLock(
[&](const auto& functionsMap) -> const AggregateFunctionFactory* {
auto it = functionsMap.find(normalizedName);
if (it != functionsMap.end()) {
return &it->second;
}
return nullptr;
});
}
std::unique_ptr<Aggregate>
Aggregate::create(const std::string& name,
const std::vector<DataType>& argTypes,
const QueryConfig& query_config) {
if (auto func = getAggregateFunctionEntry(name)) {
return (*func)(argTypes, query_config);
}
ThrowInfo(UnexpectedError, "Aggregate function not registered: {}", name);
}
void
registerAggregateFunction(const std::string& name,
const AggregateFunctionFactory& factory) {
auto realName = lowerString(name);
aggregateFunctions().withWLock(
[&](auto& aggFunctionMap) { aggFunctionMap[realName] = factory; });
}
AggregateFunctionMap&
aggregateFunctions() {
static AggregateFunctionMap aggFunctionMap;
return aggFunctionMap;
}
} // namespace exec
} // namespace milvus
@@ -0,0 +1,180 @@
// Copyright (C) 2019-2020 Zilliz. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under the License
#pragma once
#include "common/Types.h"
#include "plan/PlanNode.h"
#include "exec/QueryContext.h"
#include <folly/Synchronized.h>
namespace milvus {
namespace exec {
class Aggregate {
protected:
explicit Aggregate(DataType result_type) : result_type_(result_type) {
}
private:
const DataType result_type_;
// Byte position of null flag in group row.
int32_t nullByte_;
uint8_t nullMask_;
// Offset of fixed length accumulator state in group row.
int32_t offset_;
// Offset of uint32_t row byte size of row. 0 if there are no
// variable width fields or accumulators on the row. The size is
// capped at 4G and will stay at 4G and not wrap around if growing
// past this. This serves to track the batch size when extracting
// rows. A size in excess of 4G would finish the batch in any case,
// so larger values need not be represented.
int32_t rowSizeOffset_ = 0;
public:
virtual ~Aggregate() = default;
DataType
resultType() const {
return result_type_;
}
static std::unique_ptr<Aggregate>
create(const std::string& name,
const std::vector<DataType>& argTypes,
const QueryConfig& query_config);
void
setOffsets(int32_t offset,
int32_t nullByte,
uint8_t nullMask,
int32_t rowSizeOffset) {
setOffsetsInternal(offset, nullByte, nullMask, rowSizeOffset);
}
virtual void
initializeNewGroups(char** groups,
folly::Range<const vector_size_t*> indices) {
initializeNewGroupsInternal(groups, indices);
}
virtual void
addSingleGroupRawInput(char* group,
const std::vector<VectorPtr>& input) = 0;
virtual void
addRawInput(char** groups,
int numGroups,
const std::vector<VectorPtr>& input) = 0;
virtual void
extractValues(char** groups, int32_t numGroups, VectorPtr* result) = 0;
template <typename T>
T*
value(char* group) const {
AssertInfo(reinterpret_cast<uintptr_t>(group + offset_) %
accumulatorAlignmentSize() ==
0,
"aggregation value in the groups is not aligned");
return reinterpret_cast<T*>(group + offset_);
}
bool
isNull(char* group) const {
return numNulls_ && (group[nullByte_] & nullMask_);
}
// Returns true if the accumulator never takes more than
// accumulatorFixedWidthSize() bytes. If this is false, the
// accumulator needs to track its changing variable length footprint
// using RowSizeTracker (Aggregate::trackRowSize), see ArrayAggAggregate for
// sample usage. A group row with at least one variable length key or
// aggregate will have a 32-bit slot at offset RowContainer::rowSize_ for
// keeping track of per-row size. The size is relevant for keeping caps on
// result set and spilling batch sizes with skewed data.
virtual bool
isFixedSize() const {
return true;
}
// Returns the fixed number of bytes the accumulator takes on a group
// row. Variable width accumulators will reference the variable
// width part of the state from the fixed part.
virtual int32_t
accumulatorFixedWidthSize() const = 0;
/// Returns the alignment size of the accumulator. Some types such as
/// int128_t require aligned access. This value must be a power of 2.
virtual int32_t
accumulatorAlignmentSize() const {
return 1;
}
protected:
virtual void
setOffsetsInternal(int32_t offset,
int32_t nullByte,
uint8_t nullMask,
int32_t rowSizeOffset);
virtual void
initializeNewGroupsInternal(char** groups,
folly::Range<const vector_size_t*> indices) = 0;
// Number of null accumulators in the current state of the aggregation
// operator for this aggregate. If 0, clearing the null as part of update
// is not needed.
uint64_t numNulls_ = 0;
inline bool
clearNull(char* group) {
if (numNulls_) {
uint8_t mask = group[nullByte_];
if (mask & nullMask_) {
group[nullByte_] = mask & ~nullMask_;
numNulls_--;
return true;
}
}
return false;
}
void
setAllNulls(char** groups, folly::Range<const vector_size_t*> indices) {
for (auto i : indices) {
groups[i][nullByte_] |= nullMask_;
}
numNulls_ += indices.size();
}
};
using AggregateFunctionFactory = std::function<std::unique_ptr<Aggregate>(
const std::vector<DataType>& argTypes, const QueryConfig& config)>;
const AggregateFunctionFactory*
getAggregateFunctionEntry(const std::string& name);
using AggregateFunctionMap = folly::Synchronized<
std::unordered_map<std::string, AggregateFunctionFactory>>;
AggregateFunctionMap&
aggregateFunctions();
/// Register an aggregate function with the specified name and signatures. If
/// registerCompanionFunctions is true, also register companion aggregate and
/// scalar functions with it. When functions with `name` already exist, if
/// overwrite is true, existing registration will be replaced. Otherwise, return
/// false without overwriting the registry.
void
registerAggregateFunction(const std::string& name,
const AggregateFunctionFactory& factory);
} // namespace exec
} // namespace milvus
@@ -0,0 +1,61 @@
// Copyright (C) 2019-2020 Zilliz. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under the License
//
// Created by hanchun on 24-10-22.
//
#include "AggregateInfo.h"
#include "common/Types.h"
namespace milvus {
namespace exec {
std::vector<AggregateInfo>
toAggregateInfo(const plan::AggregationNode& aggregationNode,
const milvus::exec::OperatorContext& operatorCtx,
uint32_t numKeys) {
const auto numAggregates = aggregationNode.aggregates().size();
std::vector<AggregateInfo> aggregates;
aggregates.reserve(numAggregates);
const auto& inputType = aggregationNode.sources()[0]->output_type();
for (auto i = 0; i < numAggregates; i++) {
const auto& aggregate = aggregationNode.aggregates()[i];
AggregateInfo info;
auto& inputColumnIdxes = info.input_column_idxes_;
for (const auto& inputExpr : aggregate.call_->inputs()) {
if (auto fieldExpr = dynamic_cast<const expr::FieldAccessTypeExpr*>(
inputExpr.get())) {
const auto& fieldName = fieldExpr->name();
if (fieldName.empty()) {
ThrowInfo(ExprInvalid,
"Field name cannot be empty in aggregate input");
}
inputColumnIdxes.emplace_back(
inputType->GetChildIndex(fieldName));
} else if (inputExpr != nullptr) {
ThrowInfo(ExprInvalid,
"Only support aggregation towards column for now");
}
}
auto index = numKeys + i;
info.function_ = Aggregate::create(
aggregate.call_->fun_name(),
aggregate.rawInputTypes_,
*(operatorCtx.get_exec_context()->get_query_config()));
info.output_ = index;
aggregates.emplace_back(std::move(info));
}
return aggregates;
}
} // namespace exec
} // namespace milvus
@@ -0,0 +1,42 @@
// Copyright (C) 2019-2020 Zilliz. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under the License
#pragma once
#include <memory>
#include <vector>
#include "common/Types.h"
#include "Aggregate.h"
#include "plan/PlanNode.h"
#include "exec/operator/Operator.h"
namespace milvus {
namespace exec {
/// Information needed to evaluate an aggregate function.
struct AggregateInfo {
/// Instance of the Aggregate class.
std::unique_ptr<Aggregate> function_;
/// Indices of the input columns in the input RowVector.
std::vector<column_index_t> input_column_idxes_;
/// Index of the result column in the output RowVector.
column_index_t output_;
};
std::vector<AggregateInfo>
toAggregateInfo(const plan::AggregationNode& aggregationNode,
const milvus::exec::OperatorContext& operatorCtx,
uint32_t numKeys);
} // namespace exec
} // namespace milvus
@@ -0,0 +1,41 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include "common/Types.h"
namespace milvus {
namespace exec {
// The result of aggregation function registration.
struct AggregateRegistrationResult {
bool mainFunction{false};
bool partialFunction{false};
bool mergeFunction{false};
bool extractFunction{false};
bool mergeExtractFunction{false};
bool
operator==(const AggregateRegistrationResult& other) const {
return mainFunction == other.mainFunction &&
partialFunction == other.partialFunction &&
mergeFunction == other.mergeFunction &&
extractFunction == other.extractFunction &&
mergeExtractFunction == other.mergeExtractFunction;
}
};
} // namespace exec
} // namespace milvus
@@ -0,0 +1,34 @@
// Copyright (C) 2019-2020 Zilliz. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under the License
#include "CountAggregateBase.h"
#include "common/Utils.h"
#include "log/Log.h"
namespace milvus {
namespace exec {
void
registerCount(const std::string& name) {
exec::registerAggregateFunction(
name,
[name](const std::vector<DataType>& /*argumentTypes*/,
const QueryConfig& /*config*/) -> std::unique_ptr<Aggregate> {
return std::make_unique<CountAggregate>();
});
}
void
registerCountAggregate() {
registerCount(milvus::KCount);
LOG_INFO("Registered Count Aggregate Function");
}
} // namespace exec
} // namespace milvus
@@ -0,0 +1,111 @@
// Copyright (C) 2019-2020 Zilliz. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under the License
#pragma once
#include "SimpleNumericAggregate.h"
namespace milvus {
namespace exec {
class CountAggregate : public SimpleNumericAggregate<bool, int64_t, int64_t> {
using BaseAggregate = SimpleNumericAggregate<bool, int64_t, int64_t>;
public:
explicit CountAggregate() : BaseAggregate(DataType::INT64) {
}
int32_t
accumulatorFixedWidthSize() const override {
return sizeof(int64_t);
}
void
extractValues(char** groups,
int32_t numGroups,
VectorPtr* result) override {
BaseAggregate::doExtractValues(
groups, numGroups, result, [&](char* group) {
return *value<int64_t>(group);
});
}
void
addRawInput(char** groups,
int numGroups,
const std::vector<VectorPtr>& input) override {
if (!input.empty()) {
ColumnVectorPtr input_column = nullptr;
AssertInfo(input.size() == 1,
fmt::format("input column count for count aggregation "
"must be one , but got:{}",
input.size()));
input_column = std::dynamic_pointer_cast<ColumnVector>(input[0]);
AssertInfo(input_column != nullptr,
"input[0] must be ColumnVector for count aggregation");
for (auto i = 0; i < input_column->size(); i++) {
if (input_column->ValidAt(i)) {
addToGroup(groups[i], 1);
}
}
return;
}
for (auto i = 0; i < numGroups; i++) {
addToGroup(groups[i], 1);
}
}
void
addSingleGroupRawInput(char* group,
const std::vector<VectorPtr>& input) override {
AssertInfo(input.size() == 1,
fmt::format("input column count for count aggregation "
"must be exactly one for now, but got:{}",
input.size()));
const auto& column = std::dynamic_pointer_cast<ColumnVector>(input[0]);
AssertInfo(column != nullptr,
"input[0] must be ColumnVector for count aggregation");
if (column->IsBitmap()) {
// Use validity bitmap to count non-null values
BitsetTypeView view(column->GetRawData(), column->size());
addToGroup(group, view.size() - view.count());
} else {
for (auto i = 0; i < column->size(); i++) {
if (column->ValidAt(i)) {
addToGroup(group, 1);
}
}
}
}
void
initializeNewGroupsInternal(
char** groups, folly::Range<const vector_size_t*> indices) override {
// no need to set nulls for count aggregate as count result is always a number
for (auto i : indices) {
// initialized result of count is always zero
*value<int64_t>(groups[i]) = static_cast<int64_t>(0);
}
}
private:
inline void
addToGroup(char* group, int64_t count) {
*value<int64_t>(group) += count;
}
};
void
registerCount(const std::string& name);
void
registerCountAggregate();
} // namespace exec
} // namespace milvus
@@ -0,0 +1,247 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "GroupingSet.h"
#include "common/Utils.h"
#include "SumAggregateBase.h"
namespace milvus {
namespace exec {
GroupingSet::~GroupingSet() {
if (isGlobal_ && lookup_) {
AssertInfo(lookup_->hits_.size() == 1,
"GlobalAggregation should have exactly one output line");
// globalAggregationBuffer_ is automatically cleaned up by unique_ptr
// No need to manually delete[] or set to nullptr
lookup_->hits_[0] = nullptr;
}
}
void
GroupingSet::addInput(const RowVectorPtr& input) {
auto numRows = input->size();
numInputRows_ += numRows;
if (isGlobal_) {
addGlobalAggregationInput(input);
return;
}
addInputForActiveRows(input);
}
void
GroupingSet::initializeGlobalAggregation() {
if (globalAggregationInitialized_) {
return;
}
lookup_ = std::make_unique<HashLookup>(hashers_);
lookup_->reset(1);
// Row layout is:
// - alternating null flag - one bit per flag, one pair per
// aggregation,
// - uint32_t row size,
// - fixed-width accumulators - one per aggregate
//
// Here we always make space for a row size since we only have one row and no
// RowContainer. The whole row is allocated to guarantee that alignment
// requirements of all aggregate functions are satisfied.
// Allocate space for the null and initialized flags.
size_t numAggregates = aggregates_.size();
int32_t rowSizeOffset = milvus::bits::nBytes(numAggregates);
int32_t offset = rowSizeOffset + sizeof(int32_t);
int32_t accumulatorFlagsOffset = 0;
int32_t alignment = 1;
for (auto& aggregate : aggregates_) {
auto& function = aggregate.function_;
Accumulator accumulator(function.get());
// Accumulator offset must be aligned by their alignment size.
offset = milvus::bits::roundUp(offset, accumulator.alignment());
function->setOffsets(offset,
RowContainer::nullByte(accumulatorFlagsOffset),
RowContainer::nullMask(accumulatorFlagsOffset),
rowSizeOffset);
offset += accumulator.fixedWidthSize();
accumulatorFlagsOffset += 1;
alignment =
RowContainer::combineAlignments(accumulator.alignment(), alignment);
}
AssertInfo(__builtin_popcount(alignment) == 1,
"alignment of aggregations must be power of two");
offset = milvus::Align(offset, alignment);
// Use unique_ptr for exception-safe memory management
// If initializeNewGroups throws, the buffer will be automatically cleaned up
globalAggregationBuffer_ = std::make_unique<char[]>(offset);
lookup_->hits_[0] = globalAggregationBuffer_.get();
const auto singleGroup = std::vector<vector_size_t>{0};
for (auto& aggregate : aggregates_) {
aggregate.function_->initializeNewGroups(lookup_->hits_.data(),
singleGroup);
}
globalAggregationInitialized_ = true;
}
void
GroupingSet::addGlobalAggregationInput(const milvus::RowVectorPtr& input) {
initializeGlobalAggregation();
auto* group = lookup_->hits_[0];
for (auto i = 0; i < aggregates_.size(); i++) {
auto& function = aggregates_[i].function_;
populateTempVectors(i, input);
function->addSingleGroupRawInput(group, tempVectors_);
}
tempVectors_.clear();
}
bool
GroupingSet::getGlobalAggregationOutput(milvus::RowVectorPtr& result) {
initializeGlobalAggregation(); // when input from upstream operator is empty, we need to initialize the accumulators for global aggregation
AssertInfo(lookup_->hits_.size() == 1,
"GlobalAggregation should have exactly one output line");
auto groups = lookup_->hits_.data();
for (auto i = 0; i < aggregates_.size(); i++) {
auto& function = aggregates_[i].function_;
auto resultVector = result->child(aggregates_[i].output_);
function->extractValues(groups, 1, &resultVector);
}
return true;
}
bool
GroupingSet::getOutput(milvus::RowVectorPtr& result) {
if (isGlobal_) {
return getGlobalAggregationOutput(result);
}
AssertInfo(hash_table_ != nullptr,
"hash_table_ should not be nullptr for non-global aggregation");
const auto& all_rows = hash_table_->rows()->allRows();
if (!all_rows.empty()) {
extractGroups(result);
return true;
}
return false;
}
std::vector<Accumulator>
GroupingSet::accumulators() {
std::vector<Accumulator> accumulators;
accumulators.reserve(aggregates_.size());
for (auto& aggregate : aggregates_) {
accumulators.emplace_back(Accumulator{aggregate.function_.get()});
}
return accumulators;
}
void
GroupingSet::ensureInputFits(const RowVectorPtr& input) {
//TODO memory check
}
void
GroupingSet::extractGroups(const milvus::RowVectorPtr& result) {
RowContainer* rows = hash_table_->rows();
const auto& groups = rows->allRows();
result->resize(groups.size());
auto totalKeys = rows->KeyTypes().size();
auto groups_range =
folly::Range<char**>(const_cast<char**>(groups.data()), groups.size());
for (auto i = 0; i < totalKeys; i++) {
auto keyVector = result->child(i);
rows->extractColumn(
groups_range.data(), groups_range.size(), i, keyVector);
}
for (auto i = 0; i < aggregates_.size(); i++) {
auto& function = aggregates_[i].function_;
auto aggregateVector = result->child(totalKeys + i);
function->extractValues(
groups_range.data(), groups_range.size(), &aggregateVector);
}
}
void
GroupingSet::addInputForActiveRows(const RowVectorPtr& input) {
AssertInfo(
!isGlobal_,
"Global aggregations should not reach add input for active rows");
if (!hash_table_) {
createHashTable();
}
ensureInputFits(input);
hash_table_->prepareForGroupProbe(*lookup_, input);
hash_table_->groupProbe(*lookup_);
auto& hits = lookup_->hits_;
auto* groups = hits.data();
auto numGroups = hits.size();
const auto& newGroups = lookup_->newGroups_;
for (auto i = 0; i < aggregates_.size(); i++) {
auto& function = aggregates_[i].function_;
if (!newGroups.empty()) {
function->initializeNewGroups(groups, newGroups);
}
populateTempVectors(i, input);
function->addRawInput(groups, numGroups, tempVectors_);
}
tempVectors_.clear();
}
void
GroupingSet::populateTempVectors(int32_t aggregateIndex,
const milvus::RowVectorPtr& input) {
const auto& channel_idxes = aggregates_[aggregateIndex].input_column_idxes_;
if (channel_idxes.empty() && input->childrens().size() == 1) {
tempVectors_.resize(1);
tempVectors_[0] = input->child(0);
} else {
tempVectors_.resize(channel_idxes.size());
for (auto i = 0; i < channel_idxes.size(); i++) {
tempVectors_[i] = input->child(channel_idxes[i]);
}
}
}
int32_t
GroupingSet::outputRowCount() const {
return lookup_->newGroups_.size();
}
void
initializeAggregates(const std::vector<AggregateInfo>& aggregates,
RowContainer& rows) {
const auto numKeys = rows.KeyTypes().size();
int i = 0;
for (auto& aggregate : aggregates) {
auto& function = aggregate.function_;
const auto& rowColumn = rows.columnAt(numKeys + i);
function->setOffsets(rowColumn.offset(),
rowColumn.nullByte(),
rowColumn.nullMask(),
rows.rowSizeOffset());
i++;
}
}
void
GroupingSet::createHashTable() {
hash_table_ =
std::make_unique<HashTable>(std::move(hashers_), accumulators());
auto& rows = *(hash_table_->rows());
initializeAggregates(aggregates_, rows);
lookup_ = std::make_unique<HashLookup>(hash_table_->hashers());
}
} // namespace exec
} // namespace milvus
@@ -0,0 +1,99 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include "common/Types.h"
#include "exec/VectorHasher.h"
#include "AggregateInfo.h"
#include "exec/HashTable.h"
#include "plan/PlanNode.h"
#include "RowContainer.h"
namespace milvus {
namespace exec {
class GroupingSet {
public:
GroupingSet(const RowTypePtr& input_type,
std::vector<std::unique_ptr<VectorHasher>>&& hashers,
std::vector<AggregateInfo>&& aggregates)
: hashers_(std::move(hashers)), aggregates_(std::move(aggregates)) {
isGlobal_ = hashers_.empty();
}
~GroupingSet();
void
addInput(const RowVectorPtr& input);
void
initializeGlobalAggregation();
void
addGlobalAggregationInput(const RowVectorPtr& input);
void
addInputForActiveRows(const RowVectorPtr& input);
void
createHashTable();
std::vector<Accumulator>
accumulators();
// Checks if input will fit in the existing memory and increases reservation
// if not. If reservation cannot be increased, spills enough to make 'input'
// fit.
void
ensureInputFits(const RowVectorPtr& input);
bool
getOutput(RowVectorPtr& result);
void
extractGroups(const RowVectorPtr& result);
void
populateTempVectors(int32_t aggregateIndex, const RowVectorPtr& input);
bool
getGlobalAggregationOutput(RowVectorPtr& result);
int32_t
outputRowCount() const;
private:
bool isGlobal_;
std::vector<std::unique_ptr<VectorHasher>> hashers_;
std::vector<AggregateInfo> aggregates_;
// Place for the arguments of the aggregate being updated.
std::vector<VectorPtr> tempVectors_;
std::unique_ptr<BaseHashTable> hash_table_;
std::unique_ptr<HashLookup> lookup_;
uint64_t numInputRows_ = 0;
// RAII-managed buffer for global aggregation to ensure exception safety
std::unique_ptr<char[]> globalAggregationBuffer_;
// Boolean indicating whether accumulators for a global aggregation (i.e. hashers_.empty()) are initialized
// This is used to avoid segv when getting output directly without input for empty output of upstream operator
bool globalAggregationInitialized_{false};
};
} // namespace exec
} // namespace milvus
@@ -0,0 +1,76 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "MaxAggregateBase.h"
#include "log/Log.h"
namespace milvus {
namespace exec {
template <template <typename U, typename V, typename W> class T>
void
registerMax(const std::string& name) {
exec::registerAggregateFunction(
name,
[name](const std::vector<DataType>& argumentTypes,
const QueryConfig& /*config*/) -> std::unique_ptr<Aggregate> {
AssertInfo(argumentTypes.size() == 1,
"function:{} only accept one argument",
name);
auto inputType = argumentTypes[0];
switch (inputType) {
case DataType::INT8:
return std::make_unique<T<int8_t, int8_t, int8_t>>(
DataType::INT8);
case DataType::INT16:
return std::make_unique<T<int16_t, int16_t, int16_t>>(
DataType::INT16);
case DataType::INT32:
return std::make_unique<T<int32_t, int32_t, int32_t>>(
DataType::INT32);
case DataType::INT64:
return std::make_unique<T<int64_t, int64_t, int64_t>>(
DataType::INT64);
case DataType::TIMESTAMPTZ:
return std::make_unique<T<int64_t, int64_t, int64_t>>(
DataType::TIMESTAMPTZ);
case DataType::DOUBLE:
return std::make_unique<T<double, double, double>>(
DataType::DOUBLE);
case DataType::FLOAT:
return std::make_unique<T<float, float, float>>(
DataType::FLOAT);
case DataType::VARCHAR:
case DataType::STRING:
case DataType::TEXT:
return std::make_unique<MaxStringAggregate>(inputType);
default:
ThrowInfo(DataTypeInvalid,
"Unknown input type for {} aggregation {}",
name,
GetDataTypeName(inputType));
}
});
}
void
registerMaxAggregate() {
registerMax<MaxAggregateBase>(milvus::KMax);
LOG_INFO("Registered Max Aggregate Function");
}
} // namespace exec
} // namespace milvus
@@ -0,0 +1,213 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <limits>
#include <string>
#include <type_traits>
#include <vector>
#include "SimpleNumericAggregate.h"
#include "common/Utils.h"
namespace milvus {
namespace exec {
template <typename TInput, typename TAccumulator, typename ResultType>
class MaxAggregateBase
: public SimpleNumericAggregate<TInput, TAccumulator, ResultType> {
using BaseAggregate =
SimpleNumericAggregate<TInput, TAccumulator, ResultType>;
public:
explicit MaxAggregateBase(DataType resultType) : BaseAggregate(resultType) {
}
constexpr int32_t
accumulatorFixedWidthSize() const override {
return sizeof(TAccumulator);
}
void
extractValues(char** groups,
int32_t numGroups,
VectorPtr* result) override {
BaseAggregate::template doExtractValues<TAccumulator>(
groups, numGroups, result, [&](char* group) {
return *BaseAggregate::Aggregate::template value<TAccumulator>(
group);
});
}
void
addRawInput(char** groups,
int numGroups,
const std::vector<VectorPtr>& input) override {
updateInternal<TAccumulator>(groups, input);
}
void
addSingleGroupRawInput(char* group,
const std::vector<VectorPtr>& input) override {
BaseAggregate::template updateOneGroup<TAccumulator>(
group, input[0], &updateSingleValue<TAccumulator>);
}
void
initializeNewGroupsInternal(
char** groups, folly::Range<const vector_size_t*> indices) override {
BaseAggregate::Aggregate::setAllNulls(groups, indices);
for (auto i : indices) {
(*BaseAggregate::Aggregate::template value<TAccumulator>(
groups[i])) = std::numeric_limits<TAccumulator>::lowest();
}
}
protected:
template <typename TData, typename TValue = TInput>
void
updateInternal(char** groups, const std::vector<VectorPtr>& input) {
const auto& input_column = input[0];
if (BaseAggregate::Aggregate::numNulls_) {
BaseAggregate::template updateGroups<true, TData, TValue>(
groups, input_column, &updateSingleValue<TData>);
} else {
BaseAggregate::template updateGroups<false, TData, TValue>(
groups, input_column, &updateSingleValue<TData>);
}
}
private:
template <typename TData>
static void
updateSingleValue(TData& result, TData value) {
if (value > result) {
result = value;
}
}
};
// String max aggregate: store pointer to std::string in group row.
// IMPORTANT: This class stores pointers to strings in the input vectors rather
// than copying the string content. The input vectors passed to addRawInput()
// and addSingleGroupRawInput() MUST outlive the aggregation lifecycle until
// extractValues() is called, otherwise the stored pointers will become
// dangling and cause undefined behavior.
class MaxStringAggregate final : public Aggregate {
public:
explicit MaxStringAggregate(DataType resultType) : Aggregate(resultType) {
}
int32_t
accumulatorFixedWidthSize() const override {
return sizeof(const std::string*);
}
void
extractValues(char** groups,
int32_t numGroups,
VectorPtr* result) override {
auto result_column = std::dynamic_pointer_cast<ColumnVector>(*result);
AssertInfo(result_column != nullptr,
"input vector for extracting aggregation must be of Type "
"ColumnVector");
result_column->resize(numGroups);
for (auto i = 0; i < numGroups; i++) {
char* group = groups[i];
if (isNull(group)) {
result_column->nullAt(i);
} else {
result_column->clearNullAt(i);
auto ptr = *value<const std::string*>(group);
AssertInfo(ptr != nullptr,
"max string aggregate should not have null pointer "
"when group is not null");
result_column->SetValueAt<std::string>(i, *ptr);
}
}
}
// NOTE: The input vector must remain valid until extractValues() is called,
// as this method stores pointers to strings in the input vector.
void
addRawInput(char** groups,
int numGroups,
const std::vector<VectorPtr>& input) override {
AssertInfo(input.size() == 1,
"max aggregate expects exactly one input column");
auto column = std::dynamic_pointer_cast<ColumnVector>(input[0]);
AssertInfo(column != nullptr,
"max aggregate input must be of type ColumnVector");
auto raw = column->RawAsValues<std::string>();
for (auto i = 0; i < column->size(); i++) {
if (!column->ValidAt(i)) {
continue;
}
updateOne(groups[i], &raw[i]);
}
}
// NOTE: The input vector must remain valid until extractValues() is called,
// as this method stores pointers to strings in the input vector.
void
addSingleGroupRawInput(char* group,
const std::vector<VectorPtr>& input) override {
AssertInfo(input.size() == 1,
"max aggregate expects exactly one input column");
auto column = std::dynamic_pointer_cast<ColumnVector>(input[0]);
AssertInfo(column != nullptr,
"max aggregate input must be of type ColumnVector");
auto raw = column->RawAsValues<std::string>();
for (auto i = 0; i < column->size(); i++) {
if (!column->ValidAt(i)) {
continue;
}
updateOne(group, &raw[i]);
}
}
void
initializeNewGroupsInternal(
char** groups, folly::Range<const vector_size_t*> indices) override {
setAllNulls(groups, indices);
for (auto i : indices) {
*value<const std::string*>(groups[i]) = nullptr;
}
}
private:
// Stores a pointer to the candidate string (not a copy).
// The candidate pointer must remain valid until extractValues() is called.
inline void
updateOne(char* group, const std::string* candidate) {
if (isNull(group)) {
clearNull(group);
*value<const std::string*>(group) = candidate;
return;
}
auto& current = *value<const std::string*>(group);
if (current == nullptr || (*candidate > *current)) {
current = candidate;
}
}
};
void
registerMaxAggregate();
} // namespace exec
} // namespace milvus
@@ -0,0 +1,76 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "MinAggregateBase.h"
#include "log/Log.h"
namespace milvus {
namespace exec {
template <template <typename U, typename V, typename W> class T>
void
registerMin(const std::string& name) {
exec::registerAggregateFunction(
name,
[name](const std::vector<DataType>& argumentTypes,
const QueryConfig& /*config*/) -> std::unique_ptr<Aggregate> {
AssertInfo(argumentTypes.size() == 1,
"function:{} only accept one argument",
name);
auto inputType = argumentTypes[0];
switch (inputType) {
case DataType::INT8:
return std::make_unique<T<int8_t, int8_t, int8_t>>(
DataType::INT8);
case DataType::INT16:
return std::make_unique<T<int16_t, int16_t, int16_t>>(
DataType::INT16);
case DataType::INT32:
return std::make_unique<T<int32_t, int32_t, int32_t>>(
DataType::INT32);
case DataType::INT64:
return std::make_unique<T<int64_t, int64_t, int64_t>>(
DataType::INT64);
case DataType::TIMESTAMPTZ:
return std::make_unique<T<int64_t, int64_t, int64_t>>(
DataType::TIMESTAMPTZ);
case DataType::DOUBLE:
return std::make_unique<T<double, double, double>>(
DataType::DOUBLE);
case DataType::FLOAT:
return std::make_unique<T<float, float, float>>(
DataType::FLOAT);
case DataType::VARCHAR:
case DataType::STRING:
case DataType::TEXT:
return std::make_unique<MinStringAggregate>(inputType);
default:
ThrowInfo(DataTypeInvalid,
"Unknown input type for {} aggregation {}",
name,
GetDataTypeName(inputType));
}
});
}
void
registerMinAggregate() {
registerMin<MinAggregateBase>(milvus::KMin);
LOG_INFO("Registered Min Aggregate Function");
}
} // namespace exec
} // namespace milvus
@@ -0,0 +1,213 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <limits>
#include <string>
#include <type_traits>
#include <vector>
#include "SimpleNumericAggregate.h"
#include "common/Utils.h"
namespace milvus {
namespace exec {
template <typename TInput, typename TAccumulator, typename ResultType>
class MinAggregateBase
: public SimpleNumericAggregate<TInput, TAccumulator, ResultType> {
using BaseAggregate =
SimpleNumericAggregate<TInput, TAccumulator, ResultType>;
public:
explicit MinAggregateBase(DataType resultType) : BaseAggregate(resultType) {
}
constexpr int32_t
accumulatorFixedWidthSize() const override {
return sizeof(TAccumulator);
}
void
extractValues(char** groups,
int32_t numGroups,
VectorPtr* result) override {
BaseAggregate::template doExtractValues<TAccumulator>(
groups, numGroups, result, [&](char* group) {
return *BaseAggregate::Aggregate::template value<TAccumulator>(
group);
});
}
void
addRawInput(char** groups,
int numGroups,
const std::vector<VectorPtr>& input) override {
updateInternal<TAccumulator>(groups, input);
}
void
addSingleGroupRawInput(char* group,
const std::vector<VectorPtr>& input) override {
BaseAggregate::template updateOneGroup<TAccumulator>(
group, input[0], &updateSingleValue<TAccumulator>);
}
void
initializeNewGroupsInternal(
char** groups, folly::Range<const vector_size_t*> indices) override {
BaseAggregate::Aggregate::setAllNulls(groups, indices);
for (auto i : indices) {
(*BaseAggregate::Aggregate::template value<TAccumulator>(
groups[i])) = std::numeric_limits<TAccumulator>::max();
}
}
protected:
template <typename TData, typename TValue = TInput>
void
updateInternal(char** groups, const std::vector<VectorPtr>& input) {
const auto& input_column = input[0];
if (BaseAggregate::Aggregate::numNulls_) {
BaseAggregate::template updateGroups<true, TData, TValue>(
groups, input_column, &updateSingleValue<TData>);
} else {
BaseAggregate::template updateGroups<false, TData, TValue>(
groups, input_column, &updateSingleValue<TData>);
}
}
private:
template <typename TData>
static void
updateSingleValue(TData& result, TData value) {
if (value < result) {
result = value;
}
}
};
// String min aggregate: store pointer to std::string in group row.
// IMPORTANT: This class stores pointers to strings in the input vectors rather
// than copying the string content. The input vectors passed to addRawInput()
// and addSingleGroupRawInput() MUST outlive the aggregation lifecycle until
// extractValues() is called, otherwise the stored pointers will become
// dangling and cause undefined behavior.
class MinStringAggregate final : public Aggregate {
public:
explicit MinStringAggregate(DataType resultType) : Aggregate(resultType) {
}
int32_t
accumulatorFixedWidthSize() const override {
return sizeof(const std::string*);
}
void
extractValues(char** groups,
int32_t numGroups,
VectorPtr* result) override {
auto result_column = std::dynamic_pointer_cast<ColumnVector>(*result);
AssertInfo(result_column != nullptr,
"input vector for extracting aggregation must be of Type "
"ColumnVector");
result_column->resize(numGroups);
for (auto i = 0; i < numGroups; i++) {
char* group = groups[i];
if (isNull(group)) {
result_column->nullAt(i);
} else {
result_column->clearNullAt(i);
auto ptr = *value<const std::string*>(group);
AssertInfo(ptr != nullptr,
"min string aggregate should not have null pointer "
"when group is not null");
result_column->SetValueAt<std::string>(i, *ptr);
}
}
}
// NOTE: The input vector must remain valid until extractValues() is called,
// as this method stores pointers to strings in the input vector.
void
addRawInput(char** groups,
int numGroups,
const std::vector<VectorPtr>& input) override {
AssertInfo(input.size() == 1,
"min aggregate expects exactly one input column");
auto column = std::dynamic_pointer_cast<ColumnVector>(input[0]);
AssertInfo(column != nullptr,
"min aggregate input must be of type ColumnVector");
auto raw = column->RawAsValues<std::string>();
for (auto i = 0; i < column->size(); i++) {
if (!column->ValidAt(i)) {
continue;
}
updateOne(groups[i], &raw[i]);
}
}
// NOTE: The input vector must remain valid until extractValues() is called,
// as this method stores pointers to strings in the input vector.
void
addSingleGroupRawInput(char* group,
const std::vector<VectorPtr>& input) override {
AssertInfo(input.size() == 1,
"min aggregate expects exactly one input column");
auto column = std::dynamic_pointer_cast<ColumnVector>(input[0]);
AssertInfo(column != nullptr,
"min aggregate input must be of type ColumnVector");
auto raw = column->RawAsValues<std::string>();
for (auto i = 0; i < column->size(); i++) {
if (!column->ValidAt(i)) {
continue;
}
updateOne(group, &raw[i]);
}
}
void
initializeNewGroupsInternal(
char** groups, folly::Range<const vector_size_t*> indices) override {
setAllNulls(groups, indices);
for (auto i : indices) {
*value<const std::string*>(groups[i]) = nullptr;
}
}
private:
// Stores a pointer to the candidate string (not a copy).
// The candidate pointer must remain valid until extractValues() is called.
inline void
updateOne(char* group, const std::string* candidate) {
if (isNull(group)) {
clearNull(group);
*value<const std::string*>(group) = candidate;
return;
}
auto& current = *value<const std::string*>(group);
if (current == nullptr || (*candidate < *current)) {
current = candidate;
}
}
};
void
registerMinAggregate();
} // namespace exec
} // namespace milvus
@@ -0,0 +1,125 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "RowContainer.h"
#include "common/BitUtil.h"
#include "common/Vector.h"
namespace milvus {
namespace exec {
RowContainer::RowContainer(const std::vector<DataType>& keyTypes,
const std::vector<Accumulator>& accumulators)
: keyTypes_(keyTypes), accumulators_(accumulators) {
int32_t offset = 0;
bool isVariableWidth = false;
int idx = 0;
for (auto type : keyTypes_) {
bool varLength = !IsFixedSizeType(type);
isVariableWidth |= varLength;
if (varLength) {
variable_offsets_.emplace_back(offset);
variable_idxes_.emplace_back(idx);
}
offsets_.push_back(offset);
if (type == DataType::VARCHAR || type == DataType::STRING) {
offset += 8; //use a pointer to store string
} else {
offset += GetDataTypeSize(type, 1);
}
idx++;
}
const int32_t firstAggregateOffset = offset;
for (const auto& accumulator : accumulators) {
isVariableWidth |= !accumulator.isFixedSize();
alignment_ = combineAlignments(accumulator.alignment(), alignment_);
}
// set up 1 null-bit for each key or accumulator
auto null_bit_count = keyTypes_.size() + accumulators.size();
flagBytes_ = milvus::bits::nBytes(null_bit_count);
offset += flagBytes_;
for (const auto& accumulator : accumulators) {
offset = milvus::bits::roundUp(offset, accumulator.alignment());
offsets_.push_back(offset);
offset += accumulator.fixedWidthSize();
}
AssertInfo(offsets_.size() == keyTypes_.size() + accumulators.size(),
"wrong size of offsets in RowContainer");
if (isVariableWidth) {
rowSizeOffset_ = offset;
offset += sizeof(uint32_t);
}
fixedRowSize_ = milvus::bits::roundUp(offset, alignment_);
for (auto i = 0; i < offsets_.size(); i++) {
rowColumns_.emplace_back(offsets_[i], firstAggregateOffset * 8 + i);
}
}
RowContainer::~RowContainer() {
clear();
}
char*
RowContainer::initializeRow(char* row) {
std::memset(row, 0, fixedRowSize_);
return row;
}
char*
RowContainer::newRow() {
char* row = new char[fixedRowSize_];
rows_.emplace_back(row);
++numRows_;
return initializeRow(row);
}
void
RowContainer::store(const milvus::ColumnVectorPtr& column_data,
milvus::vector_size_t index,
char* row,
int32_t column_index) {
auto numKeys = keyTypes_.size();
bool isKey = column_index < numKeys;
AssertInfo(isKey || accumulators_.empty(),
"Should only store into rows for key");
auto rowColumn = rowColumns_[column_index];
MILVUS_DYNAMIC_TYPE_DISPATCH(storeWithNull,
keyTypes_[column_index],
column_data,
index,
row,
rowColumn.offset(),
rowColumn.nullByte(),
rowColumn.nullMask());
}
Accumulator::Accumulator(bool isFixedSize, int32_t fixedSize, int32_t alignment)
: isFixedSize_(isFixedSize), fixedSize_(fixedSize), alignment_(alignment) {
}
Accumulator::Accumulator(milvus::exec::Aggregate* aggregate)
: isFixedSize_(false), fixedSize_(0), alignment_(0) {
AssertInfo(aggregate != nullptr,
"Input aggregate for accumulator cannot be nullptr!");
isFixedSize_ = aggregate->isFixedSize();
fixedSize_ = aggregate->accumulatorFixedWidthSize();
alignment_ = aggregate->accumulatorAlignmentSize();
}
} // namespace exec
} // namespace milvus
@@ -0,0 +1,442 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <vector>
#include <folly/Range.h>
#include "common/Types.h"
#include "common/Vector.h"
#include "common/Utils.h"
#include "Aggregate.h"
#include "storage/Util.h"
namespace milvus {
namespace exec {
class Accumulator {
public:
Accumulator(bool isFixedSize, int32_t fixedSize, int32_t alignment);
explicit Accumulator(Aggregate* aggregate);
bool
isFixedSize() const {
return isFixedSize_;
}
int32_t
alignment() const {
return alignment_;
}
int32_t
fixedWidthSize() const {
return fixedSize_;
}
private:
bool isFixedSize_;
int32_t fixedSize_;
int32_t alignment_;
};
/// Packed representation of offset, null byte offset and null mask for
/// a column inside a RowContainer.
class RowColumn {
public:
RowColumn(int32_t offset, int32_t nullOffset)
: packedOffsets_(PackOffsets(offset, nullOffset)) {
}
int32_t
offset() const {
return packedOffsets_ >> 32;
}
int32_t
nullByte() const {
return static_cast<uint32_t>(packedOffsets_) >> 8;
}
uint8_t
nullMask() const {
return packedOffsets_ & 0xff;
}
private:
static uint64_t
PackOffsets(int32_t offset, int32_t nullOffset) {
return (1UL << (nullOffset & 7)) | ((nullOffset & ~7UL) << 5) |
static_cast<uint64_t>(offset) << 32;
}
const uint64_t packedOffsets_;
};
class RowContainer {
public:
RowContainer(const std::vector<DataType>& keyTypes,
const std::vector<Accumulator>& accumulators);
~RowContainer();
/// Allocates a new row and initializes possible aggregates to null.
char*
newRow();
const std::vector<DataType>&
KeyTypes() const {
return keyTypes_;
}
const RowColumn&
columnAt(int32_t column_idx) const {
return rowColumns_[column_idx];
}
static int32_t
combineAlignments(int32_t a, int32_t b) {
AssertInfo(__builtin_popcount(a) == 1,
"Alignment can only be power of 2, but got{}",
a);
AssertInfo(__builtin_popcount(b) == 1,
"Alignment can only be power of 2, but got{}",
b);
return std::max(a, b);
}
int32_t
rowSizeOffset() const {
return rowSizeOffset_;
}
static inline bool
isNullAt(const char* row, int32_t nullByte, uint8_t nullMask) {
return (row[nullByte] & nullMask) != 0;
}
static inline const std::string*&
strAt(const char* group, int32_t offset) {
return *reinterpret_cast<const std::string**>(
const_cast<char*>(group + offset));
}
template <typename T>
static inline T
valueAt(const char* group, int32_t offset) {
return *reinterpret_cast<const T*>(group + offset);
}
template <DataType Type>
inline bool
equalsNoNulls(const char* row,
int32_t offset,
const ColumnVectorPtr& column,
vector_size_t index) {
if constexpr (Type == DataType::NONE || Type == DataType::ROW ||
Type == DataType::JSON || Type == DataType::ARRAY) {
ThrowInfo(DataTypeInvalid,
"Cannot support complex data type:[ROW/JSON/ARRAY] in "
"rows container for now");
} else {
using T = typename TypeTraits<Type>::NativeType;
T raw_value = column->ValueAt<T>(index);
bool equal = false;
if constexpr (std::is_same_v<T, std::string>) {
equal = (raw_value == *(strAt(row, offset)));
} else {
equal = (milvus::comparePrimitiveAsc(
raw_value, valueAt<T>(row, offset)) == 0);
}
return equal;
}
}
template <DataType Type>
inline bool
equalsWithNulls(const char* row,
int32_t offset,
int32_t nullByte,
uint8_t nullMask,
const ColumnVectorPtr& column,
vector_size_t index) {
bool rowIsNull = isNullAt(row, nullByte, nullMask);
bool columnIsNull = !column->ValidAt(index);
if (rowIsNull || columnIsNull) {
return rowIsNull == columnIsNull;
}
return equalsNoNulls<Type>(row, offset, column, index);
}
inline bool
equals(const char* row,
RowColumn column,
const ColumnVectorPtr& column_data,
vector_size_t index) {
return MILVUS_DYNAMIC_TYPE_DISPATCH(equalsWithNulls,
column_data->type(),
row,
column.offset(),
column.nullByte(),
column.nullMask(),
column_data,
index);
}
/// Stores the 'index'th value in 'columnVector' into 'row' at 'columnIndex'.
void
store(const ColumnVectorPtr& column_data,
vector_size_t index,
char* row,
int32_t column_index);
template <DataType Type>
inline void
storeWithNull(const ColumnVectorPtr& column,
vector_size_t index,
char* row,
int32_t offset,
int32_t nullByte,
uint8_t nullMask) {
static std::string null_string_val = "";
static std::string* null_string_val_ptr = &null_string_val;
if constexpr (Type == DataType::NONE || Type == DataType::ROW ||
Type == DataType::JSON || Type == DataType::ARRAY) {
ThrowInfo(DataTypeInvalid,
"Cannot support complex data type:[ROW/JSON/ARRAY] in "
"rows container for now");
} else {
using T = typename milvus::TypeTraits<Type>::NativeType;
if (!column->ValidAt(index)) {
row[nullByte] |= nullMask;
if constexpr (std::is_same_v<T, std::string>) {
*reinterpret_cast<std::string**>(row + offset) =
null_string_val_ptr;
} else {
*reinterpret_cast<T*>(row + offset) = T();
}
return;
}
storeNoNulls<Type>(column, index, row, offset);
}
}
template <DataType Type>
inline void
storeNoNulls(const ColumnVectorPtr& column,
vector_size_t index,
char* group,
int32_t offset) {
using T = typename milvus::TypeTraits<Type>::NativeType;
if constexpr (Type == DataType::NONE || Type == DataType::ROW ||
Type == DataType::JSON || Type == DataType::ARRAY) {
ThrowInfo(DataTypeInvalid,
"Cannot support complex data type:[ROW/JSON/ARRAY] in "
"rows container for now");
} else {
auto raw_val_ptr = column->RawValueAt(index, sizeof(T));
if constexpr (std::is_same_v<T, std::string>) {
// the string object and also the underlying char array are both allocated on the heap
// must call clear method to deallocate these memory allocated for varchar type to avoid memory leak
*reinterpret_cast<std::string**>(group + offset) =
new std::string(*static_cast<std::string*>(raw_val_ptr));
} else {
*reinterpret_cast<T*>(group + offset) =
*(static_cast<T*>(raw_val_ptr));
}
}
}
template <typename T>
static void
extractValuesWithNulls(const char* const* rows,
int32_t numRows,
int32_t offset,
int32_t nullByte,
uint8_t nullMask,
const VectorPtr& result) {
AssertInfo(numRows == result->size(),
"extracted rows number should be equal to the size of "
"result vector");
auto result_column_vec =
std::dynamic_pointer_cast<ColumnVector>(result);
AssertInfo(
result_column_vec != nullptr,
"Input column to extract result must be of ColumnVector type");
for (auto i = 0; i < numRows; i++) {
const char* row = rows[i];
auto resultIndex = i;
if (row == nullptr || isNullAt(row, nullByte, nullMask)) {
result_column_vec->nullAt(resultIndex);
} else {
if constexpr (std::is_same_v<T, std::string> ||
std::is_same_v<T, std::string_view>) {
auto* str_ptr = strAt(row, offset);
result_column_vec->SetValueAt<T>(resultIndex, *str_ptr);
} else {
result_column_vec->SetValueAt<T>(resultIndex,
valueAt<T>(row, offset));
}
}
}
}
template <typename T>
static void
extractValuesNoNulls(const char* const* rows,
int32_t numRows,
int32_t offset,
const VectorPtr& result) {
AssertInfo(numRows == result->size(),
"extracted rows number should be equal to the size of "
"result vector");
auto result_column_vec =
std::dynamic_pointer_cast<ColumnVector>(result);
AssertInfo(
result_column_vec != nullptr,
"Input column to extract result must be of ColumnVector type");
for (auto i = 0; i < numRows; i++) {
const char* row = rows[i];
if (row == nullptr) {
result_column_vec->nullAt(i);
} else {
if constexpr (std::is_same_v<T, std::string> ||
std::is_same_v<T, std::string_view>) {
auto* str_ptr = strAt(row, offset);
result_column_vec->SetValueAt<T>(i, *str_ptr);
} else {
result_column_vec->SetValueAt<T>(i,
valueAt<T>(row, offset));
}
}
}
}
template <DataType Type>
static void
extractColumnTypedInternal(const char* const* rows,
int32_t numRows,
RowColumn column,
const VectorPtr& result) {
result->resize(numRows);
if constexpr (Type == DataType::ROW || Type == DataType::JSON ||
Type == DataType::ARRAY || Type == DataType::NONE) {
ThrowInfo(DataTypeInvalid,
"Not Support Extract types:[ROW/JSON/ARRAY/NONE]");
} else {
using T = typename milvus::TypeTraits<Type>::NativeType;
auto nullMask = column.nullMask();
auto offset = column.offset();
if (nullMask) {
extractValuesWithNulls<T>(
rows, numRows, offset, column.nullByte(), nullMask, result);
} else {
extractValuesNoNulls<T>(rows, numRows, offset, result);
}
}
}
template <DataType Type>
static void
extractColumnTyped(const char* const* rows,
int32_t numRows,
RowColumn column,
const VectorPtr& result) {
extractColumnTypedInternal<Type>(rows, numRows, column, result);
}
static void
extractColumn(const char* const* rows,
int32_t num_rows,
RowColumn column,
const VectorPtr& result);
void
extractColumn(const char* const* rows,
int32_t numRows,
int32_t column_idx,
const VectorPtr& result) {
extractColumn(rows, numRows, columnAt(column_idx), result);
}
const std::vector<char*>&
allRows() const {
return rows_;
}
static inline int32_t
nullByte(int32_t nullOffset) {
return nullOffset / 8;
}
static inline uint8_t
nullMask(int32_t nullOffset) {
return 1 << (nullOffset & 7);
}
void
clear() {
for (auto row : rows_) {
for (auto i = 0; i < variable_offsets_.size(); i++) {
auto& off = variable_offsets_[i];
auto& row_col = columnAt(variable_idxes_[i]);
bool isStrNull =
isNullAt(row, row_col.nullByte(), row_col.nullMask());
auto str = *reinterpret_cast<std::string**>(row + off);
if (!isStrNull && str) {
delete str;
str = nullptr;
*reinterpret_cast<std::string**>(row + off) = nullptr;
}
}
delete[] row;
}
rows_.clear();
numRows_ = 0;
}
char*
initializeRow(char* row);
private:
const std::vector<DataType> keyTypes_;
std::vector<int> variable_offsets_{};
std::vector<int> variable_idxes_{};
std::vector<uint32_t> offsets_;
std::vector<RowColumn> rowColumns_;
// How many bytes do the flags (null, free) occupy.
uint32_t fixedRowSize_;
uint32_t flagBytes_;
// for rows containing variable width fields, we store row size at the end of the row
uint32_t rowSizeOffset_ = 0;
int alignment_ = 1;
std::vector<Accumulator> accumulators_;
uint64_t numRows_ = 0;
std::vector<char*> rows_{};
};
inline void
RowContainer::extractColumn(const char* const* rows,
int32_t num_rows,
milvus::exec::RowColumn column,
const milvus::VectorPtr& result) {
MILVUS_DYNAMIC_TYPE_DISPATCH(
extractColumnTyped, result->type(), rows, num_rows, column, result);
}
} // namespace exec
} // namespace milvus
@@ -0,0 +1,116 @@
// Copyright (C) 2019-2020 Zilliz. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under the License
#pragma once
#include "Aggregate.h"
namespace milvus {
namespace exec {
template <typename TInput, typename TAccumulator, typename TResult>
class SimpleNumericAggregate : public exec::Aggregate {
protected:
explicit SimpleNumericAggregate(DataType resultType)
: Aggregate(resultType) {
}
// TData is either TAccumulator or TResult, which in most cases are the same,
// but for sum(real) can differ.
template <typename TData = TResult, typename ExtractOneValue>
void
doExtractValues(char** groups,
int32_t numGroups,
VectorPtr* result,
ExtractOneValue extractOneValue) {
AssertInfo((*result)->elementSize() == sizeof(TData),
"Incorrect type size of input result vector");
ColumnVectorPtr result_column =
std::dynamic_pointer_cast<ColumnVector>(*result);
AssertInfo(result_column != nullptr,
"input vector for extracting aggregation must be of Type "
"ColumnVector");
result_column->resize(numGroups);
TData* rawValues = static_cast<TData*>(result_column->GetRawData());
for (auto i = 0; i < numGroups; i++) {
char* group = groups[i];
if (isNull(group)) {
result_column->nullAt(i);
} else {
result_column->clearNullAt(i);
rawValues[i] = extractOneValue(group);
}
}
}
template <bool tableHasNulls,
typename TData = TResult,
typename TValue = TInput,
typename UpdateSingle>
void
updateOneGroupAtIndex(char* group,
const ColumnVectorPtr& column_data,
int32_t index,
UpdateSingle updateSingleValue) {
if (column_data->ValidAt(index)) {
updateNonNullValue<tableHasNulls, TData>(
group,
TData(column_data->ValueAt<TValue>(index)),
updateSingleValue);
}
}
template <typename TData = TResult,
typename TValue = TInput,
typename UpdateSingle>
void
updateOneGroup(char* group,
const VectorPtr& vector,
UpdateSingle updateSingleValue) {
auto column_data = std::dynamic_pointer_cast<ColumnVector>(vector);
AssertInfo(
column_data != nullptr,
"input column data for upgrading groups should not be nullptr");
for (auto i = 0; i < column_data->size(); i++) {
updateOneGroupAtIndex<true, TData, TValue>(
group, column_data, i, updateSingleValue);
}
}
template <bool tableHasNulls,
typename TData = TResult,
typename TValue = TInput,
typename UpdateSingleValue>
void
updateGroups(char** groups,
const VectorPtr& vector,
UpdateSingleValue updateSingleValue) {
auto column_data = std::dynamic_pointer_cast<ColumnVector>(vector);
AssertInfo(
column_data != nullptr,
"input column data for upgrading groups should not be nullptr");
for (auto i = 0; i < column_data->size(); i++) {
updateOneGroupAtIndex<tableHasNulls, TData, TValue>(
groups[i], column_data, i, updateSingleValue);
}
}
template <bool tableHasNulls,
typename TDataType = TAccumulator,
typename Update>
inline void
updateNonNullValue(char* group, TDataType value, Update updateValue) {
if constexpr (tableHasNulls) {
Aggregate::clearNull(group);
}
updateValue(*Aggregate::value<TDataType>(group), value);
}
};
} // namespace exec
} // namespace milvus
@@ -0,0 +1,71 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "SumAggregateBase.h"
namespace milvus {
namespace exec {
template <typename TInput, typename TAccumulator, typename ResultType>
using SumAggregate = SumAggregateBase<TInput, TAccumulator, ResultType, false>;
template <template <typename U, typename V, typename W> class T>
void
registerSum(const std::string& name) {
exec::registerAggregateFunction(
name,
[name](const std::vector<DataType>& argumentTypes,
const QueryConfig& config) -> std::unique_ptr<Aggregate> {
AssertInfo(argumentTypes.size() == 1,
"function:{} only accept one argument",
name);
auto inputType = argumentTypes[0];
switch (inputType) {
case DataType::INT8:
return std::make_unique<T<int8_t, int64_t, int64_t>>(
DataType::INT64);
case DataType::INT16:
return std::make_unique<T<int16_t, int64_t, int64_t>>(
DataType::INT64);
case DataType::INT32:
return std::make_unique<T<int32_t, int64_t, int64_t>>(
DataType::INT64);
case DataType::TIMESTAMPTZ:
case DataType::INT64:
return std::make_unique<T<int64_t, int64_t, int64_t>>(
DataType::INT64);
case DataType::DOUBLE:
return std::make_unique<T<double, double, double>>(
DataType::DOUBLE);
case DataType::FLOAT:
return std::make_unique<T<float, double, double>>(
DataType::DOUBLE);
default:
ThrowInfo(DataTypeInvalid,
"Unknown input type for {} aggregation {}",
name,
GetDataTypeName(inputType));
}
});
};
void
registerSumAggregate() {
registerSum<SumAggregate>(milvus::KSum);
LOG_INFO("Registered Sum Aggregate Function");
}
} // namespace exec
} // namespace milvus
@@ -0,0 +1,107 @@
// Copyright (C) 2019-2020 Zilliz. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under the License
#pragma once
#include "SimpleNumericAggregate.h"
#include "common/Utils.h"
namespace milvus {
namespace exec {
template <typename TInput,
typename TAccumulator,
typename ResultType,
bool Overflow>
class SumAggregateBase
: public SimpleNumericAggregate<TInput, TAccumulator, ResultType> {
using BaseAggregate =
SimpleNumericAggregate<TInput, TAccumulator, ResultType>;
public:
explicit SumAggregateBase(DataType resultType)
: BaseAggregate(resultType){};
constexpr int32_t
accumulatorFixedWidthSize() const override {
return sizeof(TAccumulator);
}
constexpr int32_t
accumulatorAlignmentSize() const override {
return static_cast<int32_t>(alignof(TAccumulator));
}
void
extractValues(char** groups,
int32_t numGroups,
VectorPtr* result) override {
BaseAggregate::template doExtractValues<TAccumulator>(
groups, numGroups, result, [&](char* group) {
return (ResultType)(*BaseAggregate::Aggregate::template value<
TAccumulator>(group));
});
}
void
addRawInput(char** groups,
int numGroups,
const std::vector<VectorPtr>& input) override {
updateInternal<TAccumulator>(groups, input);
}
void
addSingleGroupRawInput(char* group,
const std::vector<VectorPtr>& input) override {
BaseAggregate::template updateOneGroup<TAccumulator>(
group, input[0], &updateSingleValue<TAccumulator>);
}
void
initializeNewGroupsInternal(
char** groups, folly::Range<const vector_size_t*> indices) override {
Aggregate::setAllNulls(groups, indices);
for (auto i : indices) {
(*Aggregate::value<TAccumulator>(groups[i])) = 0;
}
}
protected:
template <typename TData, typename TValue = TInput>
void
updateInternal(char** groups, const std::vector<VectorPtr>& input) {
const auto& input_column = input[0];
if (Aggregate::numNulls_) {
BaseAggregate::template updateGroups<true, TData, TValue>(
groups, input_column, &updateSingleValue<TData>);
} else {
BaseAggregate::template updateGroups<false, TData, TValue>(
groups, input_column, &updateSingleValue<TData>);
}
}
private:
template <typename TData>
#if defined(FOLLY_DISABLE_UNDEFINED_BEHAVIOR_SANITIZER)
FOLLY_DISABLE_UNDEFINED_BEHAVIOR_SANITIZER("signed-integer-overflow")
#endif
static void updateSingleValue(TData& result, TData value) {
if constexpr (std::is_same_v<TData, double> ||
std::is_same_v<TData, float> ||
std::is_same_v<TData, int64_t> && Overflow) {
result += value;
} else {
result = checkPlus(result, value);
}
}
};
void
registerSumAggregate();
} // namespace exec
} // namespace milvus
+23 -5
View File
@@ -271,14 +271,25 @@ using InputTypeExprPtr = std::shared_ptr<const InputTypeExpr>;
// NOTE: unused
class FieldAccessTypeExpr : public ITypeExpr {
public:
FieldAccessTypeExpr(DataType type, const std::string& name)
: ITypeExpr{type}, name_(name), is_input_column_(true) {
FieldAccessTypeExpr(DataType type, const std::string& name, FieldId fieldId)
: ITypeExpr{type},
name_(name),
field_id_(fieldId),
is_input_column_(true) {
}
FieldAccessTypeExpr(DataType type, FieldId fieldId)
: ITypeExpr{type},
name_(""),
field_id_(fieldId),
is_input_column_(true) {
}
FieldAccessTypeExpr(DataType type,
const TypedExprPtr& input,
const std::string& name)
: ITypeExpr{type, {std::move(input)}}, name_(name) {
const std::string& name,
FieldId fieldId)
: ITypeExpr{type, {std::move(input)}}, name_(name), field_id_(fieldId) {
is_input_column_ =
dynamic_cast<const InputTypeExpr*>(inputs_[0].get()) != nullptr;
}
@@ -288,17 +299,24 @@ class FieldAccessTypeExpr : public ITypeExpr {
return is_input_column_;
}
const std::string&
name() const {
return name_;
}
std::string
ToString() const override {
if (inputs_.empty()) {
return fmt::format("{}", name_);
}
return fmt::format("{}[{}]", inputs_[0]->ToString(), name_);
return fmt::format(
"{}[{}{}]", inputs_[0]->ToString(), name_, field_id_.get());
}
private:
std::string name_;
const FieldId field_id_;
bool is_input_column_;
};
@@ -959,6 +959,9 @@ TEST(TextMatch, SealedJieBaNullable) {
// Test that growing segment loading flushed binlogs will build text match index.
TEST(TextMatch, GrowingLoadData) {
milvus::exec::expression::FunctionFactory& factory =
milvus::exec::expression::FunctionFactory::Instance();
factory.Initialize();
int64_t N = 7;
auto schema = GenTestSchema({}, true);
schema->AddField(
+18 -6
View File
@@ -246,7 +246,8 @@ class ChunkedColumnBase : public ChunkedColumnInterface {
BulkPrimitiveValueAt(milvus::OpContext* op_ctx,
void* dst,
const int64_t* offsets,
int64_t count) override {
int64_t count,
bool small_int_raw_type = false) override {
ThrowInfo(ErrorCode::Unsupported,
"BulkPrimitiveValueAt only supported for ChunkedColumn");
}
@@ -446,16 +447,27 @@ class ChunkedColumn : public ChunkedColumnBase {
BulkPrimitiveValueAt(milvus::OpContext* op_ctx,
void* dst,
const int64_t* offsets,
int64_t count) override {
int64_t count,
bool small_int_raw_type) override {
switch (data_type_) {
case DataType::INT8: {
BulkPrimitiveValueAtImpl<int8_t, int32_t>(
op_ctx, dst, offsets, count);
if (small_int_raw_type) {
BulkPrimitiveValueAtImpl<int8_t, int8_t>(
op_ctx, dst, offsets, count);
} else {
BulkPrimitiveValueAtImpl<int8_t, int32_t>(
op_ctx, dst, offsets, count);
}
break;
}
case DataType::INT16: {
BulkPrimitiveValueAtImpl<int16_t, int32_t>(
op_ctx, dst, offsets, count);
if (small_int_raw_type) {
BulkPrimitiveValueAtImpl<int16_t, int16_t>(
op_ctx, dst, offsets, count);
} else {
BulkPrimitiveValueAtImpl<int16_t, int32_t>(
op_ctx, dst, offsets, count);
}
break;
}
case DataType::INT32: {
+16 -5
View File
@@ -454,16 +454,27 @@ class ProxyChunkColumn : public ChunkedColumnInterface {
BulkPrimitiveValueAt(milvus::OpContext* op_ctx,
void* dst,
const int64_t* offsets,
int64_t count) override {
int64_t count,
bool small_int_raw_type) override {
switch (data_type_) {
case DataType::INT8: {
BulkPrimitiveValueAtImpl<int8_t, int32_t>(
op_ctx, dst, offsets, count);
if (small_int_raw_type) {
BulkPrimitiveValueAtImpl<int8_t, int8_t>(
op_ctx, dst, offsets, count);
} else {
BulkPrimitiveValueAtImpl<int8_t, int32_t>(
op_ctx, dst, offsets, count);
}
break;
}
case DataType::INT16: {
BulkPrimitiveValueAtImpl<int16_t, int32_t>(
op_ctx, dst, offsets, count);
if (small_int_raw_type) {
BulkPrimitiveValueAtImpl<int16_t, int16_t>(
op_ctx, dst, offsets, count);
} else {
BulkPrimitiveValueAtImpl<int16_t, int32_t>(
op_ctx, dst, offsets, count);
}
break;
}
case DataType::INT32: {
@@ -171,7 +171,8 @@ class ChunkedColumnInterface {
BulkPrimitiveValueAt(milvus::OpContext* op_ctx,
void* dst,
const int64_t* offsets,
int64_t count) = 0;
int64_t count,
bool small_int_raw_type = false) = 0;
virtual void
BulkVectorValueAt(milvus::OpContext* op_ctx,
+13
View File
@@ -0,0 +1,13 @@
# Copyright (C) 2019-2020 Zilliz. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing permissions and limitations under the License
add_source_at_current_directory_recursively()
add_library(milvus_plan OBJECT ${SOURCE_FILES})
+64
View File
@@ -0,0 +1,64 @@
// Licensed to the LF AI & Data foundation under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "PlanNode.h"
namespace milvus {
namespace plan {
RowTypePtr
getAggregationOutputType(
const std::vector<expr::FieldAccessTypeExprPtr>& groupingKeys,
const std::vector<std::string>& aggregateNames,
const std::vector<AggregationNode::Aggregate>& aggregates) {
std::vector<std::string> names;
std::vector<milvus::DataType> types;
for (auto& key : groupingKeys) {
names.emplace_back(key->name());
types.emplace_back(key->type());
}
AssertInfo(aggregateNames.size() == aggregates.size(),
"aggregateNames and aggregates size mismatch: "
"aggregateNames.size()={}, "
"aggregates.size()={}",
aggregateNames.size(),
aggregates.size());
for (size_t i = 0; i < aggregateNames.size(); i++) {
names.emplace_back(aggregateNames[i]);
types.emplace_back(aggregates[i].resultType_);
}
return std::make_shared<RowType>(std::move(names), std::move(types));
}
AggregationNode::AggregationNode(
const milvus::plan::PlanNodeId& id,
std::vector<expr::FieldAccessTypeExprPtr>&& groupingKeys,
std::vector<std::string>&& aggNames,
std::vector<Aggregate>&& aggregates,
std::vector<PlanNodePtr> sources)
: PlanNode(id),
groupingKeys_(std::move(groupingKeys)),
aggregateNames_(std::move(aggNames)),
aggregates_(std::move(aggregates)),
sources_(std::move(sources)),
output_type_(getAggregationOutputType(
groupingKeys_, aggregateNames_, aggregates_)) {
}
} // namespace plan
} // namespace milvus
+134 -56
View File
@@ -49,7 +49,7 @@ class PlanNode {
return id_;
}
virtual DataType
virtual RowTypePtr
output_type() const = 0;
virtual std::vector<std::shared_ptr<PlanNode>>
@@ -85,7 +85,6 @@ class PlanNode {
};
using PlanNodePtr = std::shared_ptr<PlanNode>;
class FilterNode : public PlanNode {
public:
FilterNode(const PlanNodeId& id,
@@ -100,9 +99,9 @@ class FilterNode : public PlanNode {
filter_->type()));
}
DataType
RowTypePtr
output_type() const override {
return sources_[0]->output_type();
return RowType::None;
}
std::vector<PlanNodePtr>
@@ -145,9 +144,9 @@ class FilterBitsNode : public PlanNode {
filter_->type()));
}
DataType
RowTypePtr
output_type() const override {
return DataType::BOOL;
return RowType::None;
}
std::vector<PlanNodePtr>
@@ -200,9 +199,9 @@ class ElementFilterNode : public PlanNode {
element_filter_->type()));
}
DataType
RowTypePtr
output_type() const override {
return DataType::NONE;
return RowType::None;
}
std::vector<PlanNodePtr>
@@ -257,9 +256,9 @@ class ElementFilterBitsNode : public PlanNode {
element_filter_->type()));
}
DataType
RowTypePtr
output_type() const override {
return DataType::BOOL;
return RowType::None;
}
std::vector<PlanNodePtr>
@@ -303,6 +302,52 @@ class ElementFilterBitsNode : public PlanNode {
const std::string struct_name_;
};
class ProjectNode : public PlanNode {
public:
ProjectNode(const PlanNodeId& id,
std::vector<FieldId>&& field_ids,
std::vector<std::string>&& field_names,
std::vector<milvus::DataType>&& field_types,
std::vector<PlanNodePtr> sources = std::vector<PlanNodePtr>{})
: PlanNode(id),
sources_(std::move(sources)),
field_ids_(std::move(field_ids)),
output_type_(std::make_shared<RowType>(std::move(field_names),
std::move(field_types))) {
}
std::vector<PlanNodePtr>
sources() const override {
return sources_;
}
RowTypePtr
output_type() const override {
return output_type_;
}
std::string_view
name() const override {
return "ProjectNode";
}
std::string
ToString() const override {
return fmt::format("ProjectNode:\n\t[source node:{}]",
SourceToString());
}
const std::vector<FieldId>&
FieldsToProject() const {
return field_ids_;
}
private:
const std::vector<PlanNodePtr> sources_;
const std::vector<FieldId> field_ids_;
const RowTypePtr output_type_;
};
class MvccNode : public PlanNode {
public:
MvccNode(const PlanNodeId& id,
@@ -310,9 +355,9 @@ class MvccNode : public PlanNode {
: PlanNode(id), sources_{std::move(sources)} {
}
DataType
RowTypePtr
output_type() const override {
return DataType::BOOL;
return RowType::None;
}
std::vector<PlanNodePtr>
@@ -343,9 +388,9 @@ class RandomSampleNode : public PlanNode {
: PlanNode(id), factor_(factor), sources_(std::move(sources)) {
}
DataType
RowTypePtr
output_type() const override {
return DataType::BOOL;
return RowType::None;
}
std::vector<PlanNodePtr>
@@ -381,9 +426,9 @@ class VectorSearchNode : public PlanNode {
: PlanNode(id), sources_{std::move(sources)} {
}
DataType
RowTypePtr
output_type() const override {
return DataType::BOOL;
return RowType::None;
}
std::vector<PlanNodePtr>
@@ -406,48 +451,17 @@ class VectorSearchNode : public PlanNode {
const std::vector<PlanNodePtr> sources_;
};
class GroupByNode : public PlanNode {
class SearchGroupByNode : public PlanNode {
public:
GroupByNode(const PlanNodeId& id,
std::vector<PlanNodePtr> sources = std::vector<PlanNodePtr>{})
: PlanNode(id), sources_{std::move(sources)} {
}
DataType
output_type() const override {
return DataType::BOOL;
}
std::vector<PlanNodePtr>
sources() const override {
return sources_;
}
std::string_view
name() const override {
return "GroupByNode";
}
std::string
ToString() const override {
return fmt::format("GroupByNode:[source_node:{}]", SourceToString());
}
private:
const std::vector<PlanNodePtr> sources_;
};
class CountNode : public PlanNode {
public:
CountNode(
SearchGroupByNode(
const PlanNodeId& id,
const std::vector<PlanNodePtr>& sources = std::vector<PlanNodePtr>{})
std::vector<PlanNodePtr> sources = std::vector<PlanNodePtr>{})
: PlanNode(id), sources_{std::move(sources)} {
}
DataType
RowTypePtr
output_type() const override {
return DataType::INT64;
return RowType::None;
}
std::vector<PlanNodePtr>
@@ -457,12 +471,13 @@ class CountNode : public PlanNode {
std::string_view
name() const override {
return "CountNode";
return "SearchGroupByNode";
}
std::string
ToString() const override {
return fmt::format("CountNode:[source_node:{}]", SourceToString());
return fmt::format("SearchGroupByNode:\n\t[source node:{}]",
SourceToString());
}
private:
@@ -482,9 +497,11 @@ class RescoresNode : public PlanNode {
sources_{std::move(sources)} {
}
DataType
RowTypePtr
output_type() const override {
return DataType::INT64;
return std::make_shared<const RowType>(
std::vector<std::string>{"scores"},
std::vector<milvus::DataType>{DataType::INT64});
}
std::vector<PlanNodePtr>
@@ -518,6 +535,67 @@ class RescoresNode : public PlanNode {
const std::vector<std::shared_ptr<rescores::Scorer>> scorers_;
};
class AggregationNode : public PlanNode {
public:
struct Aggregate {
/// Function name and input column names.
expr::CallExprPtr call_;
/// Raw input types used to properly identify aggregate function.
std::vector<DataType> rawInputTypes_;
DataType resultType_;
public:
Aggregate(expr::CallExprPtr call) : call_(call) {
}
};
AggregationNode(
const PlanNodeId& id,
std::vector<expr::FieldAccessTypeExprPtr>&& groupingKeys,
std::vector<std::string>&& aggNames,
std::vector<Aggregate>&& aggregates,
std::vector<PlanNodePtr> sources = std::vector<PlanNodePtr>{});
RowTypePtr
output_type() const override {
return output_type_;
}
std::vector<PlanNodePtr>
sources() const override {
return sources_;
}
std::string
ToString() const override {
return "";
}
std::string_view
name() const override {
return "agg";
}
const std::vector<expr::FieldAccessTypeExprPtr>&
GroupingKeys() const {
return groupingKeys_;
}
const std::vector<Aggregate>&
aggregates() const {
return aggregates_;
}
private:
const std::vector<expr::FieldAccessTypeExprPtr> groupingKeys_;
const std::vector<std::string> aggregateNames_;
const std::vector<Aggregate> aggregates_;
const std::vector<PlanNodePtr> sources_;
const RowTypePtr output_type_;
};
enum class ExecutionStrategy {
// Process splits as they come in any available driver.
kUngrouped,
+204 -51
View File
@@ -24,6 +24,7 @@
#include "plan/PlanNode.h"
#include "exec/Task.h"
#include "segcore/SegmentInterface.h"
#include "segcore/Utils.h"
#include "common/Tracer.h"
namespace milvus::query {
@@ -36,7 +37,7 @@ empty_search_result(int64_t num_queries) {
return final_result;
}
BitsetType
RowVectorPtr
ExecPlanNodeVisitor::ExecuteTask(
plan::PlanFragment& plan,
std::shared_ptr<milvus::exec::QueryContext> query_context) {
@@ -48,38 +49,58 @@ ExecPlanNodeVisitor::ExecuteTask(
plan.plan_node_->ToString(),
query_context->get_active_count(),
query_context->get_query_timestamp());
auto task =
milvus::exec::Task::Create(DEFAULT_TASK_ID, plan, 0, query_context);
RowVectorPtr ret = nullptr;
int64_t processed_num = 0;
BitsetType bitset_holder;
for (;;) {
auto result = task->Next();
if (!result) {
if (query_context->get_active_element_count() > 0) {
Assert(processed_num ==
query_context->get_active_element_count());
} else {
Assert(processed_num == query_context->get_active_count());
if (ret && !ret->childrens().empty()) {
auto first_column =
std::dynamic_pointer_cast<ColumnVector>(ret->child(0));
AssertInfo(first_column,
"first column must be a column vector");
if (first_column->IsBitmap()) {
if (query_context->get_active_element_count() > 0) {
Assert(processed_num ==
query_context->get_active_element_count());
} else {
Assert(processed_num ==
query_context->get_active_count());
}
}
}
break;
}
auto childrens = result->childrens();
AssertInfo(childrens.size() == 1,
"plannode result vector's children size not equal one");
LOG_DEBUG("output result length:{}", childrens[0]->size());
if (auto vec = std::dynamic_pointer_cast<ColumnVector>(childrens[0])) {
processed_num += vec->size();
BitsetTypeView view(vec->GetRawData(), vec->size());
bitset_holder.append(view);
processed_num += result->size();
if (ret) {
auto childrens = result->childrens();
AssertInfo(childrens.size() == ret->childrens().size(),
"column count of row vectors in different rounds"
"should be consistent, ret_column_count:{}, "
"new_result_column_count:{}",
childrens.size(),
ret->childrens().size());
for (auto i = 0; i < childrens.size(); i++) {
if (auto column_vec =
std::dynamic_pointer_cast<ColumnVector>(childrens[i])) {
auto ret_column_vector =
std::dynamic_pointer_cast<ColumnVector>(ret->child(i));
ret_column_vector->append(*column_vec);
} else {
ThrowInfo(UnexpectedError, "expr return type not matched");
}
}
} else {
ThrowInfo(UnexpectedError, "expr return type not matched");
ret = result;
}
}
span.GetSpan()->SetAttribute("total_rows", processed_num);
span.GetSpan()->SetAttribute("matched_rows", bitset_holder.count());
return bitset_holder;
span.GetSpan()->SetAttribute("matched_rows",
ret ? processed_num - ret->nullCount() : 0);
return ret;
}
std::unique_ptr<RetrieveResult>
@@ -94,6 +115,116 @@ wrap_num_entities(int64_t cnt) {
return retrieve_result;
}
template <typename S, typename T>
void
fillTypedDataArray(void* src_raw_data, int64_t count, T* dst) {
static_assert(IsScalar<T>);
const S* src_data = static_cast<const S*>(src_raw_data);
for (auto i = 0; i < count; i++) {
dst[i] = src_data[i];
}
}
template <typename S, typename T>
void
fillTypedDataPtrArray(void* src_raw_data,
int64_t count,
google::protobuf::RepeatedPtrField<T>* dst) {
static_assert(IsScalar<T>);
const S* src_data = static_cast<const S*>(src_raw_data);
for (int i = 0; i < count; i++) {
dst->at(i) = T(src_data[i]);
}
}
void
fillDataArrayFromColumnVector(const ColumnVectorPtr& column_vector,
DataArray& data_array) {
auto column_raw_data = column_vector->GetRawData();
auto column_data_size = column_vector->size();
switch (column_vector->type()) {
case DataType::BOOL: {
auto bool_data = data_array.mutable_scalars()->mutable_bool_data();
fillTypedDataArray<bool>(column_raw_data,
column_data_size,
bool_data->mutable_data()->mutable_data());
break;
}
case DataType::INT8: {
auto int_data = data_array.mutable_scalars()->mutable_int_data();
fillTypedDataArray<int8_t, int32_t>(
column_raw_data,
column_data_size,
int_data->mutable_data()->mutable_data());
break;
}
case DataType::INT16: {
auto int_data = data_array.mutable_scalars()->mutable_int_data();
fillTypedDataArray<int16_t, int32_t>(
column_raw_data,
column_data_size,
int_data->mutable_data()->mutable_data());
break;
}
case DataType::INT32: {
auto int_data = data_array.mutable_scalars()->mutable_int_data();
fillTypedDataArray<int32_t>(
column_raw_data,
column_data_size,
int_data->mutable_data()->mutable_data());
break;
}
case DataType::INT64: {
auto longData = data_array.mutable_scalars()->mutable_long_data();
fillTypedDataArray<int64_t>(
column_raw_data,
column_data_size,
longData->mutable_data()->mutable_data());
break;
}
case DataType::TIMESTAMPTZ: {
auto timestamptzData =
data_array.mutable_scalars()->mutable_timestamptz_data();
fillTypedDataArray<int64_t>(
column_raw_data,
column_data_size,
timestamptzData->mutable_data()->mutable_data());
break;
}
case DataType::FLOAT: {
auto float_data =
data_array.mutable_scalars()->mutable_float_data();
fillTypedDataArray<float>(
column_raw_data,
column_data_size,
float_data->mutable_data()->mutable_data());
break;
}
case DataType::DOUBLE: {
auto double_data =
data_array.mutable_scalars()->mutable_double_data();
fillTypedDataArray<double>(
column_raw_data,
column_data_size,
double_data->mutable_data()->mutable_data());
break;
}
case DataType::VARCHAR:
case DataType::STRING: {
auto string_data =
data_array.mutable_scalars()->mutable_string_data();
fillTypedDataPtrArray<std::string>(
column_raw_data, column_data_size, string_data->mutable_data());
break;
}
default: {
ThrowInfo(
DataTypeInvalid,
fmt::format("unsupported data type {}", column_vector->type()));
}
}
}
void
ExecPlanNodeVisitor::visit(RetrievePlanNode& node) {
assert(!retrieve_result_opt_.has_value());
@@ -105,18 +236,6 @@ ExecPlanNodeVisitor::visit(RetrievePlanNode& node) {
auto active_count = segment->get_active_count(timestamp_);
// PreExecute: skip all calculation
if (active_count == 0 && !node.is_count_) {
retrieve_result_opt_ = std::move(retrieve_result);
return;
}
if (active_count == 0 && node.is_count_) {
retrieve_result = *(wrap_num_entities(0));
retrieve_result_opt_ = std::move(retrieve_result);
return;
}
// Get plan
auto plan = plan::PlanFragment(node.plannodes_);
@@ -135,27 +254,61 @@ ExecPlanNodeVisitor::visit(RetrievePlanNode& node) {
query_context->set_op_context(&op_context);
// Do task execution
auto bitset_holder = ExecuteTask(plan, query_context);
auto result = ExecuteTask(plan, query_context);
setupRetrieveResult(result, op_context, node, retrieve_result, segment);
}
// Store result
if (node.is_count_) {
retrieve_result_opt_ = std::move(query_context->get_retrieve_result());
retrieve_result_opt_->retrieve_storage_cost_.scanned_remote_bytes =
op_context.storage_usage.scanned_cold_bytes.load();
retrieve_result_opt_->retrieve_storage_cost_.scanned_total_bytes =
op_context.storage_usage.scanned_total_bytes.load();
} else {
retrieve_result.total_data_cnt_ = bitset_holder.size();
tracer::AutoSpan _("Find Limit Pk", tracer::GetRootSpan(), true);
auto results_pair = segment->find_first(node.limit_, bitset_holder);
retrieve_result.result_offsets_ = std::move(results_pair.first);
retrieve_result.has_more_result = results_pair.second;
retrieve_result_opt_ = std::move(retrieve_result);
retrieve_result_opt_->retrieve_storage_cost_.scanned_remote_bytes =
op_context.storage_usage.scanned_cold_bytes.load();
retrieve_result_opt_->retrieve_storage_cost_.scanned_total_bytes =
op_context.storage_usage.scanned_total_bytes.load();
void
ExecPlanNodeVisitor::setupRetrieveResult(
const milvus::RowVectorPtr& result,
const OpContext& op_context,
const RetrievePlanNode& node,
RetrieveResult& tmp_retrieve_result,
const segcore::SegmentInternalInterface* segment) {
if (result == nullptr) {
retrieve_result_opt_ = std::move(tmp_retrieve_result);
return;
}
AssertInfo(!result->childrens().empty(),
"Result row vector must have at least one column");
auto first_column =
std::dynamic_pointer_cast<ColumnVector>(result->child(0));
AssertInfo(first_column,
"children inside row vector must be of column vector for now");
tmp_retrieve_result.total_data_cnt_ = first_column->size();
if (first_column->IsBitmap()) {
tracer::AutoSpan _("Find Limit Pk", tracer::GetRootSpan());
BitsetTypeView view(first_column->GetRawData(), first_column->size());
auto results_pair = segment->find_first(node.limit_, view);
tmp_retrieve_result.result_offsets_ = std::move(results_pair.first);
tmp_retrieve_result.has_more_result = results_pair.second;
retrieve_result_opt_ = std::move(tmp_retrieve_result);
} else {
// load data in the result vector into retrieve_result
tracer::AutoSpan _("Load Column Data", tracer::GetRootSpan());
auto column_count = result->childrens().size();
tmp_retrieve_result.field_data_.resize(column_count);
for (auto i = 0; i < column_count; i++) {
auto column_vec =
std::dynamic_pointer_cast<ColumnVector>(result->child(i));
AssertInfo(
column_vec,
"children inside row vector must be of column vector for now");
DataArray data_array;
milvus::segcore::CreateScalarDataArray(data_array,
column_vec->size(),
column_vec->type(),
column_vec->type(),
column_vec->nullCount() > 0);
fillDataArrayFromColumnVector(column_vec, data_array);
tmp_retrieve_result.field_data_[i] = std::move(data_array);
}
retrieve_result_opt_ = std::move(tmp_retrieve_result);
}
retrieve_result_opt_->retrieve_storage_cost_.scanned_remote_bytes =
op_context.storage_usage.scanned_cold_bytes.load();
retrieve_result_opt_->retrieve_storage_cost_.scanned_total_bytes =
op_context.storage_usage.scanned_total_bytes.load();
}
void
+20 -7
View File
@@ -95,10 +95,17 @@ class ExecPlanNodeVisitor : public PlanNodeVisitor {
return expr_use_pk_index_;
}
static BitsetType
static RowVectorPtr
ExecuteTask(plan::PlanFragment& plan,
std::shared_ptr<milvus::exec::QueryContext> query_context);
void
setupRetrieveResult(const RowVectorPtr& result,
const OpContext& op_context,
const RetrievePlanNode& node,
RetrieveResult& tmp_retrieve_result,
const segcore::SegmentInternalInterface* segment);
private:
const segcore::SegmentInterface& segment_;
Timestamp timestamp_;
@@ -123,12 +130,18 @@ ExecuteQueryExpr(std::shared_ptr<milvus::plan::PlanNode> plannode,
auto query_context = std::make_shared<milvus::exec::QueryContext>(
DEAFULT_QUERY_ID, segment, active_count, timestamp);
auto bitset =
ExecPlanNodeVisitor::ExecuteTask(plan_fragment, query_context);
// For test case, bitset 1 indicates true but executor is verse
bitset.flip();
return bitset;
auto row = ExecPlanNodeVisitor::ExecuteTask(plan_fragment, query_context);
AssertInfo(row != nullptr,
"ExecuteTask returned null row vector for query expression");
AssertInfo(
row->childrens().size() == 1,
"query expr operator's result vector's children size not equal one");
auto col_vec = std::dynamic_pointer_cast<ColumnVector>(row->childrens()[0]);
AssertInfo(col_vec != nullptr, "failed to cast to ColumnVector");
BitsetTypeView view(col_vec->GetRawData(), col_vec->size());
BitsetType query_view(view);
query_view.flip();
return query_view;
}
} // namespace milvus::query
-1
View File
@@ -59,7 +59,6 @@ struct RetrievePlanNode : PlanNode {
std::shared_ptr<milvus::plan::PlanNode> plannodes_;
bool is_count_;
int64_t limit_;
};
+253 -47
View File
@@ -141,6 +141,210 @@ ProtoParser::ParseSearchInfo(const planpb::VectorANNS& anns_proto) {
return search_info;
}
std::string
getAggregateOpName(planpb::AggregateOp op) {
switch (op) {
case planpb::sum:
return "sum";
case planpb::count:
return "count";
case planpb::avg:
return "avg";
case planpb::min:
return "min";
case planpb::max:
return "max";
default:
ThrowInfo(OpTypeInvalid, "Unknown op type for aggregation");
}
}
namespace {
// Helper function to build FilterBitsNode and RandomSampleNode
plan::PlanNodePtr
BuildFilterAndSampleNodes(const proto::plan::QueryPlanNode& query,
const planpb::PlanNode& plan_node_proto,
const SchemaPtr& schema,
const std::vector<plan::PlanNodePtr>& sources,
ProtoParser* parser) {
if (!query.has_predicates()) {
return nullptr;
}
auto parse_expr_to_filter_node =
[&](const proto::plan::Expr& predicate_proto) -> plan::PlanNodePtr {
auto expr = parser->ParseExprs(predicate_proto);
if (plan_node_proto.has_namespace_()) {
expr = MergeExprWithNamespace(
schema, expr, plan_node_proto.namespace_());
}
return std::make_shared<plan::FilterBitsNode>(
milvus::plan::GetNextPlanNodeId(), expr, sources);
};
auto* predicate_proto = &query.predicates();
if (predicate_proto->expr_case() == proto::plan::Expr::kRandomSampleExpr) {
// Predicate exists in random_sample_expr means we encounter expression
// like "`predicate expression` && random_sample(...)". Extract it to construct
// FilterBitsNode and make it be executed before RandomSampleNode.
auto& sample_expr = predicate_proto->random_sample_expr();
plan::PlanNodePtr filter_node = nullptr;
if (sample_expr.has_predicate()) {
filter_node = parse_expr_to_filter_node(sample_expr.predicate());
}
std::vector<plan::PlanNodePtr> sample_sources;
if (filter_node) {
sample_sources = {filter_node};
} else {
sample_sources = sources;
}
return std::make_shared<plan::RandomSampleNode>(
milvus::plan::GetNextPlanNodeId(),
sample_expr.sample_factor(),
sample_sources);
} else {
return parse_expr_to_filter_node(query.predicates());
}
}
// Helper function to process group_by fields
void
ProcessGroupByFields(const proto::plan::QueryPlanNode& query,
const SchemaPtr& schema,
std::vector<expr::FieldAccessTypeExprPtr>& groupingKeys,
std::vector<FieldId>& project_id_list,
std::vector<std::string>& project_name_list,
std::vector<milvus::DataType>& project_type_list) {
auto group_by_field_count = query.group_by_field_ids_size();
groupingKeys.reserve(group_by_field_count);
project_id_list.reserve(group_by_field_count);
project_name_list.reserve(group_by_field_count);
project_type_list.reserve(group_by_field_count);
auto insert_project_field_if_not_exist = [&](FieldId field_id,
const std::string& field_name,
milvus::DataType field_type) {
if (std::count(project_id_list.begin(),
project_id_list.end(),
field_id) == 0) {
project_id_list.emplace_back(field_id);
project_name_list.emplace_back(field_name);
project_type_list.emplace_back(field_type);
}
};
for (int i = 0; i < group_by_field_count; i++) {
auto input_field_id = query.group_by_field_ids(i);
AssertInfo(input_field_id > 0,
"input field_id to group by must be positive, "
"but is:{}",
input_field_id);
auto field_id = FieldId(input_field_id);
auto field_type = schema->GetFieldType(field_id);
auto field_name = schema->GetFieldName(field_id);
groupingKeys.emplace_back(
std::make_shared<const expr::FieldAccessTypeExpr>(
field_type, field_name, field_id));
insert_project_field_if_not_exist(field_id, field_name, field_type);
}
}
// Helper function to process aggregates
void
ProcessAggregates(const proto::plan::QueryPlanNode& query,
const SchemaPtr& schema,
std::vector<plan::AggregationNode::Aggregate>& aggregates,
std::vector<std::string>& agg_names,
std::vector<FieldId>& project_id_list,
std::vector<std::string>& project_name_list,
std::vector<milvus::DataType>& project_type_list) {
aggregates.reserve(query.aggregates_size());
agg_names.reserve(query.aggregates_size());
auto insert_project_field_if_not_exist = [&](FieldId field_id,
const std::string& field_name,
milvus::DataType field_type) {
if (std::count(project_id_list.begin(),
project_id_list.end(),
field_id) == 0) {
project_id_list.emplace_back(field_id);
project_name_list.emplace_back(field_name);
project_type_list.emplace_back(field_type);
}
};
for (int i = 0; i < query.aggregates_size(); i++) {
auto aggregate = query.aggregates(i);
auto agg_name = getAggregateOpName(aggregate.op());
agg_names.emplace_back(agg_name);
auto input_agg_field_id = aggregate.field_id();
if (input_agg_field_id == 0) {
// count(*) do not need input project columns
auto call = std::make_shared<const expr::CallExpr>(
agg_name, std::vector<expr::TypedExprPtr>{}, nullptr);
aggregates.emplace_back(plan::AggregationNode::Aggregate{call});
aggregates.back().resultType_ =
GetAggResultType(agg_name, DataType::NONE);
} else {
AssertInfo(input_agg_field_id > 0,
"input field_id to aggregate must be "
"positive or zero, but is:{}",
input_agg_field_id);
auto field_id = FieldId(input_agg_field_id);
auto field_type = schema->GetFieldType(field_id);
auto field_name = schema->GetFieldName(field_id);
auto agg_input = std::make_shared<const expr::FieldAccessTypeExpr>(
field_type, field_name, field_id);
auto call = std::make_shared<const expr::CallExpr>(
agg_name, std::vector<expr::TypedExprPtr>{agg_input}, nullptr);
aggregates.emplace_back(plan::AggregationNode::Aggregate(call));
aggregates.back().rawInputTypes_.emplace_back(field_type);
aggregates.back().resultType_ =
GetAggResultType(agg_name, field_type);
insert_project_field_if_not_exist(field_id, field_name, field_type);
}
}
}
// Helper function to build ProjectNode and AggregationNode
plan::PlanNodePtr
BuildProjectAndAggregationNodes(
const proto::plan::QueryPlanNode& query,
const std::vector<plan::PlanNodePtr>& sources,
std::vector<expr::FieldAccessTypeExprPtr> groupingKeys,
std::vector<std::string> agg_names,
std::vector<plan::AggregationNode::Aggregate> aggregates,
std::vector<FieldId> project_id_list,
std::vector<std::string> project_name_list,
std::vector<milvus::DataType> project_type_list) {
plan::PlanNodePtr plannode = sources.empty() ? nullptr : sources[0];
// Build ProjectNode if needed
if (!project_id_list.empty()) {
auto project_field_id_list = std::vector<FieldId>(
project_id_list.begin(), project_id_list.end());
plannode = std::make_shared<plan::ProjectNode>(
milvus::plan::GetNextPlanNodeId(),
std::move(project_field_id_list),
std::move(project_name_list),
std::move(project_type_list),
sources);
}
// Build AggregationNode
std::vector<plan::PlanNodePtr> agg_sources =
plannode ? std::vector<plan::PlanNodePtr>{plannode} : sources;
return std::make_shared<plan::AggregationNode>(
milvus::plan::GetNextPlanNodeId(),
std::move(groupingKeys),
std::move(agg_names),
std::move(aggregates),
agg_sources);
}
} // namespace
std::unique_ptr<VectorPlanNode>
ProtoParser::PlanNodeFromProto(const planpb::PlanNode& plan_node_proto) {
Assert(plan_node_proto.has_vector_anns());
@@ -272,7 +476,7 @@ ProtoParser::PlanNodeFromProto(const planpb::PlanNode& plan_node_proto) {
}
if (plan_node->search_info_.group_by_field_id_ != std::nullopt) {
plannode = std::make_shared<milvus::plan::GroupByNode>(
plannode = std::make_shared<milvus::plan::SearchGroupByNode>(
milvus::plan::GetNextPlanNodeId(), sources);
sources = std::vector<milvus::plan::PlanNodePtr>{plannode};
}
@@ -311,7 +515,6 @@ ProtoParser::RetrievePlanNodeFromProto(
auto plan_node = [&]() -> std::unique_ptr<RetrievePlanNode> {
auto node = std::make_unique<RetrievePlanNode>();
if (plan_node_proto.has_predicates()) { // version before 2023.03.30.
node->is_count_ = false;
auto& predicate_proto = plan_node_proto.predicates();
auto expr_parser = [&]() -> plan::PlanNodePtr {
auto expr = ParseExprs(predicate_proto);
@@ -328,66 +531,70 @@ ProtoParser::RetrievePlanNodeFromProto(
milvus::plan::GetNextPlanNodeId(), sources);
node->plannodes_ = std::move(plannode);
} else {
// mvccNode--->FilterBitsNode or
// aggNode---> projectNode --->mvccNode--->FilterBitsNode
auto& query = plan_node_proto.query();
if (query.has_predicates()) {
auto parse_expr_to_filter_node =
[&](const proto::plan::Expr& predicate_proto)
-> plan::PlanNodePtr {
auto expr = ParseExprs(predicate_proto);
if (plan_node_proto.has_namespace_()) {
expr = MergeExprWithNamespace(
schema, expr, plan_node_proto.namespace_());
}
return std::make_shared<plan::FilterBitsNode>(
milvus::plan::GetNextPlanNodeId(), expr, sources);
};
auto* predicate_proto = &query.predicates();
if (predicate_proto->expr_case() ==
proto::plan::Expr::kRandomSampleExpr) {
// Predicate exists in random_sample_expr means we encounter expression
// like "`predicate expression` && random_sample(...)". Extract it to construct
// FilterBitsNode and make it be executed before RandomSampleNode.
auto& sample_expr = predicate_proto->random_sample_expr();
if (sample_expr.has_predicate()) {
auto expr_parser =
parse_expr_to_filter_node(sample_expr.predicate());
plannode = std::move(expr_parser);
sources =
std::vector<milvus::plan::PlanNodePtr>{plannode};
}
plannode = std::move(
std::make_shared<milvus::plan::RandomSampleNode>(
milvus::plan::GetNextPlanNodeId(),
sample_expr.sample_factor(),
sources));
sources = std::vector<milvus::plan::PlanNodePtr>{plannode};
} else {
plannode = parse_expr_to_filter_node(query.predicates());
sources = std::vector<milvus::plan::PlanNodePtr>{plannode};
}
// 1. Build FilterBitsNode and RandomSampleNode if needed
auto filter_node = BuildFilterAndSampleNodes(
query, plan_node_proto, schema, sources, this);
if (filter_node) {
plannode = filter_node;
sources = std::vector<milvus::plan::PlanNodePtr>{plannode};
}
// 2. Build MvccNode
plannode = std::make_shared<milvus::plan::MvccNode>(
milvus::plan::GetNextPlanNodeId(), sources);
sources = std::vector<milvus::plan::PlanNodePtr>{plannode};
node->is_count_ = query.is_count();
node->limit_ = query.limit();
if (node->is_count_) {
plannode = std::make_shared<milvus::plan::CountNode>(
milvus::plan::GetNextPlanNodeId(), sources);
sources = std::vector<milvus::plan::PlanNodePtr>{plannode};
// 3. Build ProjectNode and AggregationNode if needed
auto group_by_field_count = query.group_by_field_ids_size();
auto agg_functions_count = query.aggregates_size();
if (group_by_field_count > 0 || agg_functions_count > 0) {
std::vector<FieldId> project_id_list;
std::vector<std::string> project_name_list;
std::vector<milvus::DataType> project_type_list;
std::vector<expr::FieldAccessTypeExprPtr> groupingKeys;
std::vector<plan::AggregationNode::Aggregate> aggregates;
std::vector<std::string> agg_names;
// Process group_by fields
ProcessGroupByFields(query,
schema,
groupingKeys,
project_id_list,
project_name_list,
project_type_list);
// Process aggregates
ProcessAggregates(query,
schema,
aggregates,
agg_names,
project_id_list,
project_name_list,
project_type_list);
// Build ProjectNode and AggregationNode
plannode = BuildProjectAndAggregationNodes(
query,
sources,
std::move(groupingKeys),
std::move(agg_names),
std::move(aggregates),
std::move(project_id_list),
std::move(project_name_list),
std::move(project_type_list));
}
node->plannodes_ = plannode;
node->limit_ = query.limit();
}
return node;
}();
PlanOptionsFromProto(plan_node_proto.plan_options(),
plan_node->plan_options_);
return plan_node;
}
@@ -431,7 +638,6 @@ ProtoParser::CreateRetrievePlan(const proto::plan::PlanNode& plan_node_proto) {
for (auto dynamic_field : plan_node_proto.dynamic_fields()) {
retrieve_plan->target_dynamic_fields_.push_back(dynamic_field);
}
return retrieve_plan;
}
@@ -1364,7 +1364,7 @@ ChunkedSegmentSealedImpl::pk_binary_range(milvus::OpContext* op_ctx,
std::pair<std::vector<OffsetMap::OffsetType>, bool>
ChunkedSegmentSealedImpl::find_first(int64_t limit,
const BitsetType& bitset) const {
const BitsetTypeView& bitset) const {
if (!is_sorted_by_pk_) {
return insert_record_.pk2offset_->find_first(limit, bitset);
}
@@ -1477,6 +1477,145 @@ ChunkedSegmentSealedImpl::bulk_subscript(milvus::OpContext* op_ctx,
}
}
void
ChunkedSegmentSealedImpl::bulk_subscript(milvus::OpContext* op_ctx,
FieldId field_id,
DataType data_type,
const int64_t* seg_offsets,
int64_t count,
void* data,
TargetBitmap& valid_map,
bool small_int_raw_type) const {
auto& field_meta = schema_->operator[](field_id);
// DO NOT directly access the column by map like: `fields_.at(field_id)->Data()`,
// we have to clone the shared pointer, to make sure it won't get released
// if segment released
auto column = get_column(field_id);
AssertInfo(column != nullptr,
"field {} must exist when doing bulk_subscript",
field_id.get());
if (column->IsNullable()) {
for (auto i = 0; i < count; i++) {
valid_map.set(i, column->IsValid(op_ctx, seg_offsets[i]));
}
} else {
valid_map.set();
}
switch (data_type) {
case DataType::BOOL: {
bulk_subscript_impl<bool>(op_ctx,
column.get(),
seg_offsets,
count,
static_cast<bool*>(data));
break;
}
case DataType::INT8: {
bulk_subscript_impl<int8_t>(op_ctx,
column.get(),
seg_offsets,
count,
static_cast<int8_t*>(data),
small_int_raw_type);
break;
}
case DataType::INT16: {
bulk_subscript_impl<int16_t>(op_ctx,
column.get(),
seg_offsets,
count,
static_cast<int16_t*>(data),
small_int_raw_type);
break;
}
case DataType::INT32: {
bulk_subscript_impl<int32_t>(op_ctx,
column.get(),
seg_offsets,
count,
static_cast<int32_t*>(data));
break;
}
case DataType::TIMESTAMPTZ:
case DataType::INT64: {
bulk_subscript_impl<int64_t>(op_ctx,
column.get(),
seg_offsets,
count,
static_cast<int64_t*>(data));
break;
}
case DataType::FLOAT: {
bulk_subscript_impl<float>(op_ctx,
column.get(),
seg_offsets,
count,
static_cast<float*>(data));
break;
}
case DataType::DOUBLE: {
bulk_subscript_impl<double>(op_ctx,
column.get(),
seg_offsets,
count,
static_cast<double*>(data));
break;
}
case DataType::VARCHAR:
case DataType::STRING:
case DataType::TEXT: {
// dst must have at least count elements; the callback's offset
// parameter is guaranteed to be in [0, count)
bulk_subscript_ptr_impl<std::string>(
op_ctx,
column.get(),
seg_offsets,
count,
static_cast<std::string*>(data));
break;
}
case DataType::JSON: {
// dst must have at least count elements; the callback's offset
// parameter is guaranteed to be in [0, count)
bulk_subscript_ptr_impl<Json>(op_ctx,
column.get(),
seg_offsets,
count,
static_cast<Json*>(data));
break;
}
case DataType::GEOMETRY: {
// dst must have at least count elements; the callback's offset
// parameter is guaranteed to be in [0, count)
bulk_subscript_ptr_impl<std::string>(
op_ctx,
column.get(),
seg_offsets,
count,
static_cast<std::string*>(data));
break;
}
case DataType::ARRAY: {
// dst must have at least count elements; the callback's index
// parameter is guaranteed to be in [0, count)
auto dst = static_cast<Array*>(data);
column->BulkArrayAt(
op_ctx,
[dst](const ArrayView& view, size_t i) {
view.output_data(dst[i]);
},
seg_offsets,
count);
break;
}
default: {
ThrowInfo(DataTypeInvalid,
fmt::format("unsupported data type {}",
field_meta.get_data_type()));
}
}
}
template <typename S, typename T>
void
ChunkedSegmentSealedImpl::bulk_subscript_impl(milvus::OpContext* op_ctx,
@@ -1497,11 +1636,15 @@ ChunkedSegmentSealedImpl::bulk_subscript_impl(milvus::OpContext* op_ctx,
ChunkedColumnInterface* field,
const int64_t* seg_offsets,
int64_t count,
T* dst) {
T* dst,
bool small_int_raw_type) {
static_assert(std::is_fundamental_v<S> && std::is_fundamental_v<T>);
// use field->data_type_ to determine the type of dst
field->BulkPrimitiveValueAt(
op_ctx, static_cast<void*>(dst), seg_offsets, count);
field->BulkPrimitiveValueAt(op_ctx,
static_cast<void*>(dst),
seg_offsets,
count,
small_int_raw_type);
}
// for dense vector
@@ -1545,6 +1688,34 @@ ChunkedSegmentSealedImpl::bulk_subscript_ptr_impl(
}
}
template <typename S, typename T>
void
ChunkedSegmentSealedImpl::bulk_subscript_ptr_impl(
milvus::OpContext* op_ctx,
const ChunkedColumnInterface* column,
const int64_t* seg_offsets,
int64_t count,
T* dst) {
if constexpr (std::is_same_v<S, Json>) {
column->BulkRawJsonAt(
op_ctx,
[&](Json json, size_t offset, bool is_valid) {
dst[offset] = std::move(T(json));
},
seg_offsets,
count);
} else {
static_assert(std::is_same_v<S, std::string>);
column->BulkRawStringAt(
op_ctx,
[&](std::string_view value, size_t offset, bool is_valid) {
dst[offset] = std::move(T(value));
},
seg_offsets,
count);
}
}
template <typename T>
void
ChunkedSegmentSealedImpl::bulk_subscript_array_impl(
@@ -1904,14 +2075,15 @@ ChunkedSegmentSealedImpl::get_raw_data(milvus::OpContext* op_ctx,
break;
}
case DataType::TIMESTAMPTZ: {
bulk_subscript_impl<int64_t>(op_ctx,
column.get(),
seg_offsets,
count,
ret->mutable_scalars()
->mutable_timestamptz_data()
->mutable_data()
->mutable_data());
bulk_subscript_impl<int64_t, int64_t>(
op_ctx,
column.get(),
seg_offsets,
count,
ret->mutable_scalars()
->mutable_timestamptz_data()
->mutable_data()
->mutable_data());
break;
}
case DataType::VECTOR_FLOAT: {
@@ -301,7 +301,7 @@ class ChunkedSegmentSealedImpl : public SegmentSealed {
const Timestamp* timestamps) override;
std::pair<std::vector<OffsetMap::OffsetType>, bool>
find_first(int64_t limit, const BitsetType& bitset) const override;
find_first(int64_t limit, const BitsetTypeView& bitset) const override;
// Calculate: output[i] = Vec[seg_offset[i]]
// where Vec is determined from field_offset
@@ -387,6 +387,16 @@ class ChunkedSegmentSealedImpl : public SegmentSealed {
int64_t count,
void* output) const override;
void
bulk_subscript(milvus::OpContext* op_ctx,
FieldId field_id,
DataType data_type,
const int64_t* seg_offsets,
int64_t count,
void* data,
TargetBitmap& valid_map,
bool small_int_raw_type = false) const override;
void
check_search(const query::Plan* plan) const override;
@@ -840,7 +850,8 @@ class ChunkedSegmentSealedImpl : public SegmentSealed {
ChunkedColumnInterface* field,
const int64_t* seg_offsets,
int64_t count,
T* dst_raw);
T* dst_raw,
bool small_int_raw_type = false);
static void
bulk_subscript_impl(milvus::OpContext* op_ctx,
@@ -859,6 +870,14 @@ class ChunkedSegmentSealedImpl : public SegmentSealed {
int64_t count,
google::protobuf::RepeatedPtrField<std::string>* dst_raw);
template <typename S, typename T = S>
static void
bulk_subscript_ptr_impl(milvus::OpContext* op_ctx,
const ChunkedColumnInterface* field,
const int64_t* seg_offsets,
int64_t count,
T* dst);
template <typename T>
static void
bulk_subscript_array_impl(milvus::OpContext* op_ctx,
@@ -22,11 +22,16 @@
#include "segcore/Record.h"
#include "test_utils/DataGen.h"
#include "test_utils/storage_test_utils.h"
#include "exec/expression/function/FunctionFactory.h"
using namespace milvus;
using namespace milvus::segcore;
using namespace milvus::exec;
TEST(DeleteMVCC, common_case) {
milvus::exec::expression::FunctionFactory& factory =
milvus::exec::expression::FunctionFactory::Instance();
factory.Initialize();
auto schema = std::make_shared<Schema>();
auto pk = schema->AddDebugField("pk", DataType::INT64);
schema->set_primary_field_id(pk);
+5 -5
View File
@@ -69,7 +69,7 @@ class OffsetMap {
using OffsetType = int64_t;
// TODO: in fact, we can retrieve the pk here. Not sure which way is more efficient.
virtual std::pair<std::vector<OffsetMap::OffsetType>, bool>
find_first(int64_t limit, const BitsetType& bitset) const = 0;
find_first(int64_t limit, const BitsetTypeView& bitset) const = 0;
virtual void
clear() = 0;
@@ -184,7 +184,7 @@ class OffsetOrderedMap : public OffsetMap {
}
std::pair<std::vector<OffsetMap::OffsetType>, bool>
find_first(int64_t limit, const BitsetType& bitset) const override {
find_first(int64_t limit, const BitsetTypeView& bitset) const override {
std::shared_lock<std::shared_mutex> lck(mtx_);
if (limit == Unlimited || limit == NoLimit) {
@@ -210,7 +210,7 @@ class OffsetOrderedMap : public OffsetMap {
private:
std::pair<std::vector<OffsetMap::OffsetType>, bool>
find_first_by_index(int64_t limit, const BitsetType& bitset) const {
find_first_by_index(int64_t limit, const BitsetTypeView& bitset) const {
int64_t hit_num = 0; // avoid counting the number everytime.
auto size = bitset.size();
int64_t cnt = size - bitset.count();
@@ -364,7 +364,7 @@ class OffsetOrderedArray : public OffsetMap {
}
std::pair<std::vector<OffsetMap::OffsetType>, bool>
find_first(int64_t limit, const BitsetType& bitset) const override {
find_first(int64_t limit, const BitsetTypeView& bitset) const override {
check_search();
if (limit == Unlimited || limit == NoLimit) {
@@ -389,7 +389,7 @@ class OffsetOrderedArray : public OffsetMap {
private:
std::pair<std::vector<OffsetMap::OffsetType>, bool>
find_first_by_index(int64_t limit, const BitsetType& bitset) const {
find_first_by_index(int64_t limit, const BitsetTypeView& bitset) const {
int64_t hit_num = 0; // avoid counting the number everytime.
auto size = bitset.size();
int64_t cnt = size - bitset.count();
@@ -81,8 +81,10 @@ TYPED_TEST_P(TypedOffsetOrderedArrayTest, find_first) {
// all is satisfied.
{
BitsetType all(num);
BitsetTypeView all_view(all.data(), num);
{
auto [offsets, has_more_res] = this->map_.find_first(num / 2, all);
auto [offsets, has_more_res] =
this->map_.find_first(num / 2, all_view);
ASSERT_EQ(num / 2, offsets.size());
ASSERT_TRUE(has_more_res);
for (int i = 1; i < offsets.size(); i++) {
@@ -91,7 +93,7 @@ TYPED_TEST_P(TypedOffsetOrderedArrayTest, find_first) {
}
{
auto [offsets, has_more_res] =
this->map_.find_first(Unlimited, all);
this->map_.find_first(Unlimited, all_view);
ASSERT_EQ(num, offsets.size());
ASSERT_FALSE(has_more_res);
for (int i = 1; i < offsets.size(); i++) {
@@ -102,9 +104,10 @@ TYPED_TEST_P(TypedOffsetOrderedArrayTest, find_first) {
{
// corner case, segment offset exceeds the size of bitset.
BitsetType all_minus_1(num - 1);
BitsetTypeView all_minus_1_view(all_minus_1.data(), num - 1);
{
auto [offsets, has_more_res] =
this->map_.find_first(num / 2, all_minus_1);
this->map_.find_first(num / 2, all_minus_1_view);
ASSERT_EQ(num / 2, offsets.size());
ASSERT_TRUE(has_more_res);
for (int i = 1; i < offsets.size(); i++) {
@@ -113,7 +116,7 @@ TYPED_TEST_P(TypedOffsetOrderedArrayTest, find_first) {
}
{
auto [offsets, has_more_res] =
this->map_.find_first(Unlimited, all_minus_1);
this->map_.find_first(Unlimited, all_minus_1_view);
ASSERT_EQ(all_minus_1.size(), offsets.size());
ASSERT_FALSE(has_more_res);
for (int i = 1; i < offsets.size(); i++) {
@@ -125,10 +128,11 @@ TYPED_TEST_P(TypedOffsetOrderedArrayTest, find_first) {
// none is satisfied.
BitsetType none(num);
none.set();
auto result_pair = this->map_.find_first(num / 2, none);
BitsetTypeView none_view(none.data(), num);
auto result_pair = this->map_.find_first(num / 2, none_view);
ASSERT_EQ(0, result_pair.first.size());
ASSERT_FALSE(result_pair.second);
result_pair = this->map_.find_first(NoLimit, none);
result_pair = this->map_.find_first(NoLimit, none_view);
ASSERT_EQ(0, result_pair.first.size());
ASSERT_FALSE(result_pair.second);
}
@@ -76,9 +76,9 @@ TYPED_TEST_P(TypedOffsetOrderedMapTest, find_first) {
// all is satisfied.
BitsetType all(num);
all.reset();
BitsetTypeView all_view(all.data(), num);
{
auto [offsets, has_more_res] = this->map_.find_first(num / 2, all);
auto [offsets, has_more_res] = this->map_.find_first(num / 2, all_view);
ASSERT_EQ(num / 2, offsets.size());
ASSERT_TRUE(has_more_res);
for (int i = 1; i < offsets.size(); i++) {
@@ -86,7 +86,8 @@ TYPED_TEST_P(TypedOffsetOrderedMapTest, find_first) {
}
}
{
auto [offsets, has_more_res] = this->map_.find_first(Unlimited, all);
auto [offsets, has_more_res] =
this->map_.find_first(Unlimited, all_view);
ASSERT_EQ(num, offsets.size());
ASSERT_FALSE(has_more_res);
for (int i = 1; i < offsets.size(); i++) {
@@ -97,9 +98,10 @@ TYPED_TEST_P(TypedOffsetOrderedMapTest, find_first) {
// corner case, segment offset exceeds the size of bitset.
BitsetType all_minus_1(num - 1);
all_minus_1.reset();
BitsetTypeView all_minus_1_view(all_minus_1.data(), num - 1);
{
auto [offsets, has_more_res] =
this->map_.find_first(num / 2, all_minus_1);
this->map_.find_first(num / 2, all_minus_1_view);
ASSERT_EQ(num / 2, offsets.size());
ASSERT_TRUE(has_more_res);
for (int i = 1; i < offsets.size(); i++) {
@@ -108,7 +110,7 @@ TYPED_TEST_P(TypedOffsetOrderedMapTest, find_first) {
}
{
auto [offsets, has_more_res] =
this->map_.find_first(Unlimited, all_minus_1);
this->map_.find_first(Unlimited, all_minus_1_view);
ASSERT_EQ(all_minus_1.size(), offsets.size());
ASSERT_FALSE(has_more_res);
for (int i = 1; i < offsets.size(); i++) {
@@ -119,13 +121,16 @@ TYPED_TEST_P(TypedOffsetOrderedMapTest, find_first) {
// none is satisfied.
BitsetType none(num);
none.set();
BitsetTypeView none_view(none.data(), num);
{
auto [offsets, has_more_res] = this->map_.find_first(num / 2, none);
auto [offsets, has_more_res] =
this->map_.find_first(num / 2, none_view);
ASSERT_TRUE(has_more_res);
ASSERT_EQ(0, offsets.size());
}
{
auto [offsets, has_more_res] = this->map_.find_first(NoLimit, none);
auto [offsets, has_more_res] =
this->map_.find_first(NoLimit, none_view);
ASSERT_TRUE(has_more_res);
ASSERT_EQ(0, offsets.size());
}
@@ -1229,12 +1229,39 @@ SegmentGrowingImpl::bulk_subscript_ptr_impl(
google::protobuf::RepeatedPtrField<std::string>* dst) const {
auto vec = dynamic_cast<const ConcurrentVector<S>*>(vec_raw);
auto& src = *vec;
if constexpr (std::is_same_v<S, Json>) {
// For Json type, we must use operator[] instead of view_element()
// to ensure the Json object lives long enough. view_element() returns
// a string_view that may point to memory owned by a temporary Json
// object, which gets destroyed before we can construct std::string.
// Using operator[] copies the Json object, extending its lifetime.
for (int64_t i = 0; i < count; ++i) {
auto offset = seg_offsets[i];
Json json = src[offset]; // Copy Json object to extend lifetime
dst->at(i) = std::string(json.data());
}
} else {
for (int64_t i = 0; i < count; ++i) {
auto offset = seg_offsets[i];
dst->at(i) = std::string(src.view_element(offset));
}
}
}
template <typename S, typename T>
void
SegmentGrowingImpl::bulk_subscript_ptr_impl(const VectorBase* vec_raw,
const int64_t* seg_offsets,
int64_t count,
T* dst) const {
auto vec = dynamic_cast<const ConcurrentVector<S>*>(vec_raw);
auto& src = *vec;
for (int64_t i = 0; i < count; ++i) {
auto offset = seg_offsets[i];
if (IsVariableTypeSupportInChunk<S> && src.is_mmap()) {
dst->at(i) = std::move(std::string(src.view_element(offset)));
if (offset != INVALID_SEG_OFFSET) {
dst[i] = T(src.view_element(offset));
} else {
dst->at(i) = std::move(std::string(src[offset]));
dst[i] = T(); // Default-initialize for invalid offsets
}
}
}
@@ -1291,7 +1318,8 @@ SegmentGrowingImpl::bulk_subscript_impl(milvus::OpContext* op_ctx,
const VectorBase* vec_raw,
const int64_t* seg_offsets,
int64_t count,
T* output) const {
T* output,
bool small_int_raw_type) const {
static_assert(IsScalar<S>);
auto vec_ptr = dynamic_cast<const ConcurrentVector<S>*>(vec_raw);
AssertInfo(vec_ptr, "Pointer of vec_raw is nullptr");
@@ -1363,6 +1391,135 @@ SegmentGrowingImpl::bulk_subscript(milvus::OpContext* op_ctx,
}
}
void
SegmentGrowingImpl::bulk_subscript(milvus::OpContext* op_ctx,
FieldId field_id,
DataType data_type,
const int64_t* seg_offsets,
int64_t count,
void* data,
TargetBitmap& valid_map,
bool small_int_raw_type) const {
auto vec_ptr = insert_record_.get_data_base(field_id);
auto& field_meta = schema_->operator[](field_id);
valid_map.set();
if (field_meta.is_nullable()) {
auto valid_vec_ptr = insert_record_.get_valid_data(field_id);
for (auto i = 0; i < count; i++) {
valid_map.set(i, valid_vec_ptr->is_valid(seg_offsets[i]));
}
}
switch (field_meta.get_data_type()) {
case DataType::BOOL: {
bulk_subscript_impl<bool>(
op_ctx, vec_ptr, seg_offsets, count, static_cast<bool*>(data));
break;
}
case DataType::INT8: {
if (small_int_raw_type) {
bulk_subscript_impl<int8_t>(op_ctx,
vec_ptr,
seg_offsets,
count,
static_cast<int8_t*>(data));
} else {
bulk_subscript_impl<int8_t, int32_t>(
op_ctx,
vec_ptr,
seg_offsets,
count,
static_cast<int32_t*>(data));
}
break;
}
case DataType::INT16: {
if (small_int_raw_type) {
bulk_subscript_impl<int16_t>(op_ctx,
vec_ptr,
seg_offsets,
count,
static_cast<int16_t*>(data));
} else {
bulk_subscript_impl<int16_t, int32_t>(
op_ctx,
vec_ptr,
seg_offsets,
count,
static_cast<int32_t*>(data));
}
break;
}
case DataType::INT32: {
bulk_subscript_impl<int32_t>(op_ctx,
vec_ptr,
seg_offsets,
count,
static_cast<int32_t*>(data));
break;
}
case DataType::TIMESTAMPTZ:
case DataType::INT64: {
bulk_subscript_impl<int64_t>(op_ctx,
vec_ptr,
seg_offsets,
count,
static_cast<int64_t*>(data));
break;
}
case DataType::FLOAT: {
bulk_subscript_impl<float>(
op_ctx, vec_ptr, seg_offsets, count, static_cast<float*>(data));
break;
}
case DataType::DOUBLE: {
bulk_subscript_impl<double>(op_ctx,
vec_ptr,
seg_offsets,
count,
static_cast<double*>(data));
break;
}
case DataType::VARCHAR:
case DataType::TEXT: {
bulk_subscript_ptr_impl<std::string>(
vec_ptr, seg_offsets, count, static_cast<std::string*>(data));
break;
}
case DataType::JSON: {
bulk_subscript_ptr_impl<Json>(
vec_ptr, seg_offsets, count, static_cast<Json*>(data));
break;
}
case DataType::GEOMETRY: {
bulk_subscript_ptr_impl<std::string>(
vec_ptr, seg_offsets, count, static_cast<std::string*>(data));
break;
}
case DataType::ARRAY: {
auto vec = dynamic_cast<const ConcurrentVector<Array>*>(vec_ptr);
AssertInfo(vec, "Pointer of vec_ptr is nullptr for ARRAY type");
auto& src = *vec;
auto dst = static_cast<Array*>(data);
for (int64_t i = 0; i < count; ++i) {
auto offset = seg_offsets[i];
if (offset != INVALID_SEG_OFFSET) {
dst[i] = src[offset];
} else {
dst[i] =
Array(); // Default-construct empty Array for invalid offsets
}
}
break;
}
default: {
ThrowInfo(
DataTypeInvalid,
fmt::format("unsupported type {}", field_meta.get_data_type()));
}
}
}
void
SegmentGrowingImpl::search_ids(BitsetType& bitset,
const IdArray& id_array) const {
+20 -2
View File
@@ -227,7 +227,8 @@ class SegmentGrowingImpl : public SegmentGrowing {
const VectorBase* vec_raw,
const int64_t* seg_offsets,
int64_t count,
T* output) const;
T* output,
bool small_int_raw_type = false) const;
template <typename S>
void
@@ -238,6 +239,13 @@ class SegmentGrowingImpl : public SegmentGrowing {
int64_t count,
google::protobuf::RepeatedPtrField<std::string>* dst) const;
template <typename S, typename T = S>
void
bulk_subscript_ptr_impl(const VectorBase* vec_raw,
const int64_t* seg_offsets,
int64_t count,
T* dst) const;
// for scalar array vectors
template <typename T>
void
@@ -283,6 +291,16 @@ class SegmentGrowingImpl : public SegmentGrowing {
int64_t count,
void* output) const override;
void
bulk_subscript(milvus::OpContext* op_ctx,
FieldId field_id,
DataType data_type,
const int64_t* seg_offsets,
int64_t count,
void* data,
TargetBitmap& valid_map,
bool small_int_raw_type = false) const override;
std::unique_ptr<DataArray>
bulk_subscript(milvus::OpContext* op_ctx,
FieldId field_id,
@@ -454,7 +472,7 @@ class SegmentGrowingImpl : public SegmentGrowing {
}
std::pair<std::vector<OffsetMap::OffsetType>, bool>
find_first(int64_t limit, const BitsetType& bitset) const override {
find_first(int64_t limit, const BitsetTypeView& bitset) const override {
return insert_record_.pk2offset_->find_first(limit, bitset);
}
+44 -17
View File
@@ -148,25 +148,22 @@ SegmentInternalInterface::Retrieve(tracer::TraceContext* trace_ctx,
}
results->set_all_retrieve_count(retrieve_results.total_data_cnt_);
if (plan->plan_node_->is_count_) {
AssertInfo(retrieve_results.field_data_.size() == 1,
"count result should only have one column");
*results->add_fields_data() = retrieve_results.field_data_[0];
return results;
}
results->mutable_offset()->Add(retrieve_results.result_offsets_.begin(),
retrieve_results.result_offsets_.end());
std::chrono::high_resolution_clock::time_point get_target_entry_start =
std::chrono::high_resolution_clock::now();
FillTargetEntry(trace_ctx,
plan,
results,
retrieve_results.result_offsets_.data(),
retrieve_results.result_offsets_.size(),
ignore_non_pk,
true);
if (retrieve_results.field_data_.empty()) {
FillTargetEntry(trace_ctx,
plan,
results,
retrieve_results.result_offsets_.data(),
retrieve_results.result_offsets_.size(),
ignore_non_pk,
true);
} else {
FillTargetEntryDirectly(trace_ctx, results, retrieve_results);
}
std::chrono::high_resolution_clock::time_point get_target_entry_end =
std::chrono::high_resolution_clock::now();
double get_entry_cost = std::chrono::duration<double, std::micro>(
@@ -178,6 +175,22 @@ SegmentInternalInterface::Retrieve(tracer::TraceContext* trace_ctx,
return results;
}
void
SegmentInternalInterface::FillTargetEntryDirectly(
tracer::TraceContext* trace_ctx,
const std::unique_ptr<proto::segcore::RetrieveResults>& results,
RetrieveResult& retrieveResult) const {
auto fields_data = results->mutable_fields_data();
for (auto& field_data : retrieveResult.field_data_) {
// Dynamically allocate a copy of the field data
auto* allocated_data = new DataArray(std::move(field_data));
// Transfer ownership to protobuf
fields_data->AddAllocated(allocated_data);
}
retrieveResult.field_data_.clear();
}
void
SegmentInternalInterface::FillTargetEntry(
tracer::TraceContext* trace_ctx,
@@ -327,10 +340,24 @@ SegmentInternalInterface::get_real_count() const {
plannode = std::make_shared<milvus::plan::MvccNode>(
milvus::plan::GetNextPlanNodeId());
sources = std::vector<milvus::plan::PlanNodePtr>{plannode};
plannode = std::make_shared<milvus::plan::CountNode>(
milvus::plan::GetNextPlanNodeId(), sources);
std::string agg_name = "count";
std::vector<plan::AggregationNode::Aggregate> aggregates;
{
auto call = std::make_shared<const expr::CallExpr>(
agg_name, std::vector<expr::TypedExprPtr>{}, nullptr);
aggregates.emplace_back(plan::AggregationNode::Aggregate{call});
aggregates.back().resultType_ =
GetAggResultType(agg_name, DataType::NONE);
}
plannode = std::make_shared<plan::AggregationNode>(
milvus::plan::GetNextPlanNodeId(),
std::vector<expr::FieldAccessTypeExprPtr>{},
std::vector<std::string>{agg_name},
std::move(aggregates),
sources);
plan->plan_node_->plannodes_ = plannode;
plan->plan_node_->is_count_ = true;
auto res = Retrieve(nullptr,
plan.get(),
MAX_TIMESTAMP,
+17 -1
View File
@@ -551,7 +551,13 @@ class SegmentInternalInterface : public SegmentInterface {
* @return All candidates offsets.
*/
virtual std::pair<std::vector<OffsetMap::OffsetType>, bool>
find_first(int64_t limit, const BitsetType& bitset) const = 0;
find_first(int64_t limit, const BitsetTypeView& bitset) const = 0;
void
FillTargetEntryDirectly(
tracer::TraceContext* trace_ctx,
const std::unique_ptr<proto::segcore::RetrieveResults>& results,
RetrieveResult& retrieveResult) const;
void
FillTargetEntry(
@@ -634,6 +640,16 @@ class SegmentInternalInterface : public SegmentInterface {
int64_t count,
void* output) const = 0;
virtual void
bulk_subscript(milvus::OpContext* op_ctx,
FieldId field_id,
DataType data_type,
const int64_t* seg_offsets,
int64_t count,
void* data,
TargetBitmap& valid_map,
bool small_int_raw_type = false) const = 0;
// calculate output[i] = Vec[seg_offsets[i]}, where Vec binds to field_offset
virtual std::unique_ptr<DataArray>
bulk_subscript(milvus::OpContext* op_ctx,
+160 -4
View File
@@ -279,6 +279,31 @@ CreateEmptyScalarDataArray(int64_t count, const FieldMeta& field_meta) {
}
auto scalar_array = data_array->mutable_scalars();
SetUpScalarFieldData(
scalar_array, data_type, field_meta.get_element_type(), count);
return data_array;
}
void
CreateScalarDataArray(DataArray& data_array,
int64_t count,
DataType data_type,
DataType element_type,
bool nullable) {
data_array.set_type(
static_cast<milvus::proto::schema::DataType>(data_type));
if (nullable) {
data_array.mutable_valid_data()->Resize(count, false);
}
auto scalar_array = data_array.mutable_scalars();
SetUpScalarFieldData(scalar_array, data_type, element_type, count);
}
void
SetUpScalarFieldData(milvus::proto::schema::ScalarField*& scalar_array,
DataType data_type,
DataType element_type,
int64_t count) {
switch (data_type) {
case DataType::BOOL: {
auto obj = scalar_array->mutable_bool_data();
@@ -349,8 +374,8 @@ CreateEmptyScalarDataArray(int64_t count, const FieldMeta& field_meta) {
case DataType::ARRAY: {
auto obj = scalar_array->mutable_array_data();
obj->mutable_data()->Reserve(count);
obj->set_element_type(static_cast<milvus::proto::schema::DataType>(
field_meta.get_element_type()));
obj->set_element_type(
static_cast<milvus::proto::schema::DataType>(element_type));
for (int i = 0; i < count; i++) {
*(obj->mutable_data()->Add()) = proto::schema::ScalarField();
}
@@ -361,8 +386,6 @@ CreateEmptyScalarDataArray(int64_t count, const FieldMeta& field_meta) {
fmt::format("unsupported datatype {}", data_type));
}
}
return data_array;
}
std::unique_ptr<DataArray>
@@ -465,6 +488,11 @@ CreateScalarDataArrayFrom(const void* data_raw,
auto valid_data_ = reinterpret_cast<const bool*>(valid_data);
auto obj = data_array->mutable_valid_data();
obj->Add(valid_data_, valid_data_ + count);
} else {
FixedVector<bool> always_valid(count, true);
auto obj = data_array->mutable_valid_data();
obj->Add(reinterpret_cast<const bool*>(always_valid.data()),
reinterpret_cast<const bool*>(always_valid.data()) + count);
}
auto scalar_array = data_array->mutable_scalars();
@@ -1367,4 +1395,132 @@ LoadIndexData(milvus::tracer::TraceContext& ctx,
std::move(translator));
}
FieldDataPtr
bulk_script_field_data(milvus::OpContext* op_ctx,
FieldId fieldId,
DataType dataType,
const int64_t* seg_offsets,
int64_t count,
const segcore::SegmentInternalInterface* segment,
TargetBitmap& valid_view,
bool small_int_raw_type) {
FieldDataPtr ret = nullptr;
switch (dataType) {
case milvus::DataType::BOOL: {
FixedVector<bool> vec(count);
segment->bulk_subscript(op_ctx,
fieldId,
dataType,
seg_offsets,
count,
vec.data(),
valid_view);
ret = std::make_shared<FieldDataImpl<bool, true>>(
1, dataType, false, std::move(vec));
break;
}
case milvus::DataType::INT8: {
FixedVector<int8_t> vec(count);
segment->bulk_subscript(op_ctx,
fieldId,
dataType,
seg_offsets,
count,
vec.data(),
valid_view,
small_int_raw_type);
ret = std::make_shared<FieldDataImpl<int8_t, true>>(
1, dataType, false, std::move(vec));
break;
}
case milvus::DataType::INT16: {
FixedVector<int16_t> vec(count);
segment->bulk_subscript(op_ctx,
fieldId,
dataType,
seg_offsets,
count,
vec.data(),
valid_view,
small_int_raw_type);
ret = std::make_shared<FieldDataImpl<int16_t, true>>(
1, dataType, false, std::move(vec));
break;
}
case milvus::DataType::INT32: {
FixedVector<int32_t> vec(count);
segment->bulk_subscript(op_ctx,
fieldId,
dataType,
seg_offsets,
count,
vec.data(),
valid_view);
ret = std::make_shared<FieldDataImpl<int32_t, true>>(
1, dataType, false, std::move(vec));
break;
}
case milvus::DataType::TIMESTAMPTZ:
case milvus::DataType::INT64: {
FixedVector<int64_t> vec(count);
segment->bulk_subscript(op_ctx,
fieldId,
dataType,
seg_offsets,
count,
vec.data(),
valid_view);
ret = std::make_shared<FieldDataImpl<int64_t, true>>(
1, dataType, false, std::move(vec));
break;
}
case milvus::DataType::FLOAT: {
FixedVector<float> vec(count);
segment->bulk_subscript(op_ctx,
fieldId,
dataType,
seg_offsets,
count,
vec.data(),
valid_view);
ret = std::make_shared<FieldDataImpl<float, true>>(
1, dataType, false, std::move(vec));
break;
}
case milvus::DataType::DOUBLE: {
FixedVector<double> vec(count);
segment->bulk_subscript(op_ctx,
fieldId,
dataType,
seg_offsets,
count,
vec.data(),
valid_view);
ret = std::make_shared<FieldDataImpl<double, true>>(
1, dataType, false, std::move(vec));
break;
}
case milvus::DataType::STRING:
case milvus::DataType::VARCHAR:
case milvus::DataType::TEXT: {
FixedVector<std::string> vec(count);
segment->bulk_subscript(op_ctx,
fieldId,
dataType,
seg_offsets,
count,
vec.data(),
valid_view);
ret = std::make_shared<FieldDataImpl<std::string, true>>(
1, dataType, false, std::move(vec));
break;
}
default: {
ThrowInfo(DataTypeInvalid,
fmt::format("unsupported data type {}", dataType));
}
}
return std::move(ret);
}
} // namespace milvus::segcore
+23
View File
@@ -24,6 +24,7 @@
#include "segcore/ConcurrentVector.h"
#include "segcore/Types.h"
#include "common/Consts.h"
#include "segcore/SegmentInterface.h"
namespace milvus::segcore {
@@ -53,6 +54,19 @@ GetRawDataSizeOfDataArray(const DataArray* data,
std::unique_ptr<DataArray>
CreateEmptyScalarDataArray(int64_t count, const FieldMeta& field_meta);
void
SetUpScalarFieldData(milvus::proto::schema::ScalarField*& scalar_array,
DataType data_type,
DataType element_type,
int64_t count);
void
CreateScalarDataArray(DataArray& data_array,
int64_t count,
DataType data_type,
DataType element_type,
bool nullable);
std::unique_ptr<DataArray>
CreateEmptyVectorDataArray(int64_t count, const FieldMeta& field_meta);
@@ -191,4 +205,13 @@ TimestampToPhysicalMs(Timestamp timestamp) {
return timestamp >> LOGICAL_BITS;
}
FieldDataPtr
bulk_script_field_data(milvus::OpContext* op_ctx,
FieldId fieldId,
DataType dataType,
const int64_t* seg_offsets,
int64_t count,
const segcore::SegmentInternalInterface* segment,
TargetBitmap& valid_view,
bool small_int_raw_type = false);
} // namespace milvus::segcore
+2 -1
View File
@@ -37,7 +37,7 @@ set(MILVUS_TEST_FILES
test_exec.cpp
test_expr_materialized_view.cpp
test_float16.cpp
test_group_by.cpp
test_search_group_by.cpp
test_iterative_filter.cpp
test_indexing.cpp
test_index_wrapper.cpp
@@ -51,6 +51,7 @@ set(MILVUS_TEST_FILES
test_storage_v2_index_raw_data.cpp
test_group_by_json.cpp
test_element_filter.cpp
test_query_group_by.cpp
)
if ( NOT (INDEX_ENGINE STREQUAL "cardinal") )
File diff suppressed because it is too large Load Diff
-1
View File
@@ -9,7 +9,6 @@
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under the License
#include <boost/format.hpp>
#include <gtest/gtest.h>
#include <cstdint>
#include <memory>
@@ -0,0 +1,581 @@
// Copyright (C) 2019-2020 Zilliz. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under the License
#include <gtest/gtest.h>
#include "test_utils/DataGen.h"
#include "segcore/SegmentSealed.h"
#include "plan/PlanNode.h"
#include "exec/QueryContext.h"
#include "exec/Task.h"
#include "test_utils/storage_test_utils.h"
#include "exec/expression/function/FunctionFactory.h"
using namespace milvus;
using namespace milvus::segcore;
using namespace milvus::plan;
using namespace milvus::exec;
class QueryAggTest : public testing::TestWithParam<bool> {
public:
constexpr static const char bool_field[] = "bool";
constexpr static const char int8_field[] = "int8";
constexpr static const char int16_field[] = "int16";
constexpr static const char int32_field[] = "int32";
constexpr static const char int64_field[] = "int64";
constexpr static const char float_field[] = "float";
constexpr static const char double_field[] = "double";
constexpr static const char string_field[] = "string";
constexpr static const char vector_field[] = "vector";
protected:
void
SetUp() override {
schema_ = std::make_shared<Schema>();
auto vec_fid = schema_->AddDebugField(
"fakevec", DataType::VECTOR_FLOAT, 16, knowhere::metric::L2);
auto nullable = GetParam();
auto bool_fid =
schema_->AddDebugField(bool_field, DataType::BOOL, nullable);
auto int8_fid =
schema_->AddDebugField(int8_field, DataType::INT8, nullable);
auto int16_fid =
schema_->AddDebugField(int16_field, DataType::INT16, nullable);
auto int32_fid =
schema_->AddDebugField(int32_field, DataType::INT32, nullable);
auto int64_fid =
schema_->AddDebugField(int64_field, DataType::INT64, nullable);
auto float_fid =
schema_->AddDebugField(float_field, DataType::FLOAT, nullable);
auto double_fid =
schema_->AddDebugField(double_field, DataType::DOUBLE, nullable);
auto str_fid = schema_->AddDebugField(string_field, DataType::VARCHAR);
auto vector_fid = schema_->AddDebugField(
vector_field, DataType::VECTOR_FLOAT, 16, knowhere::metric::L2);
field_map_[bool_field] = bool_fid;
field_map_[int8_field] = int8_fid;
field_map_[int16_field] = int16_fid;
field_map_[int32_field] = int32_fid;
field_map_[int64_field] = int64_fid;
field_map_[float_field] = float_fid;
field_map_[double_field] = double_fid;
field_map_[string_field] = str_fid;
field_map_[vector_field] = vector_fid;
schema_->set_primary_field_id(str_fid);
num_rows_ = 10;
auto raw_data =
DataGen(schema_, num_rows_, 42, 0, 2, 10, false, false, false);
auto segment = CreateSealedWithFieldDataLoaded(schema_, raw_data);
segment_ = SegmentSealedSPtr(segment.release());
milvus::exec::expression::FunctionFactory& factory =
milvus::exec::expression::FunctionFactory::Instance();
factory.Initialize();
}
void
TearDown() override {
}
public:
int64_t num_rows_{0};
SegmentSealedSPtr segment_;
std::shared_ptr<Schema> schema_;
std::map<std::string, FieldId> field_map_;
};
INSTANTIATE_TEST_SUITE_P(TaskTestSuite,
QueryAggTest,
::testing::Values(true, false));
RowVectorPtr
execPlan(std::shared_ptr<Task>& task) {
RowVectorPtr ret = nullptr;
for (;;) {
auto result = task->Next();
if (!result) {
break;
}
if (ret) {
auto childrens = result->childrens();
AssertInfo(childrens.size() == ret->childrens().size(),
"column count of row vectors in different rounds"
"should be consistent, ret_column_count:{}, "
"new_result_column_count:{}",
childrens.size(),
ret->childrens().size());
for (auto i = 0; i < childrens.size(); i++) {
if (auto column_vec =
std::dynamic_pointer_cast<ColumnVector>(childrens[i])) {
auto ret_column_vector =
std::dynamic_pointer_cast<ColumnVector>(ret->child(i));
ret_column_vector->append(*column_vec);
} else {
ThrowInfo(UnexpectedError, "expr return type not matched");
}
}
} else {
ret = result;
}
}
return ret;
}
TEST_P(QueryAggTest, GroupFixedLengthType) {
std::vector<milvus::plan::PlanNodePtr> sources;
auto nullable = GetParam();
//set up mvcc_node + project_node + agg_node
// group by int16_field
// mvcc node
PlanNodePtr mvcc_node = std::make_shared<milvus::plan::MvccNode>(
milvus::plan::GetNextPlanNodeId(), sources);
sources = std::vector<milvus::plan::PlanNodePtr>{mvcc_node};
// project node
auto int16_id = field_map_[int16_field];
PlanNodePtr project_node = std::make_shared<milvus::plan::ProjectNode>(
milvus::plan::GetNextPlanNodeId(),
std::vector<FieldId>{int16_id},
std::vector<std::string>{int16_field},
std::vector<DataType>{DataType::INT16},
sources);
sources = std::vector<milvus::plan::PlanNodePtr>{project_node};
// agg node
std::vector<expr::FieldAccessTypeExprPtr> groupingKeys;
groupingKeys.emplace_back(std::make_shared<const expr::FieldAccessTypeExpr>(
DataType::INT16, int16_field, int16_id));
PlanNodePtr agg_node = std::make_shared<plan::AggregationNode>(
milvus::plan::GetNextPlanNodeId(),
std::move(groupingKeys),
std::vector<std::string>{},
std::vector<plan::AggregationNode::Aggregate>{},
sources);
auto plan = plan::PlanFragment(agg_node);
auto query_context = std::make_shared<milvus::exec::QueryContext>(
"test1", segment_.get(), num_rows_, MAX_TIMESTAMP);
auto op_context = milvus::OpContext();
query_context->set_op_context(&op_context);
auto task = Task::Create("task_query_group_by", plan, 0, query_context);
RowVectorPtr ret = execPlan(task);
EXPECT_EQ(1, ret->childrens().size());
auto column = std::dynamic_pointer_cast<ColumnVector>(ret->child(0));
if (nullable) {
// as there are 10 values repeating 2 times, after groupby, at most 7 valid unique values will be returned
EXPECT_TRUE(column->size() == 6);
} else if (!nullable) {
EXPECT_TRUE(column->size() == 5);
}
if (!nullable) {
auto count = column->size();
std::set<int16_t> set;
for (auto i = 0; i < count; i++) {
int16_t val = column->ValueAt<int16_t>(i);
if (set.count(val) > 0) {
EXPECT_TRUE(false);
// there should not be any duplicated vals in the returned column
}
set.insert(val);
}
EXPECT_TRUE(set.size() == column->size());
}
}
TEST_P(QueryAggTest, GroupFixedLengthMultipleColumn) {
std::vector<milvus::plan::PlanNodePtr> sources;
auto nullable = GetParam();
//set up mvcc_node + project_node + agg_node
// group by int16_field and int32_field
// mvcc node
PlanNodePtr mvcc_node = std::make_shared<milvus::plan::MvccNode>(
milvus::plan::GetNextPlanNodeId(), sources);
sources = std::vector<milvus::plan::PlanNodePtr>{mvcc_node};
// project node
auto int16_id = field_map_[int16_field];
auto int32_id = field_map_[int32_field];
auto int64_id = field_map_[int64_field];
PlanNodePtr project_node = std::make_shared<milvus::plan::ProjectNode>(
milvus::plan::GetNextPlanNodeId(),
std::vector<FieldId>{int16_id, int32_id, int64_id},
std::vector<std::string>{int16_field, int32_field, int64_field},
std::vector<DataType>{
DataType::INT16, DataType::INT32, DataType::INT64},
sources);
sources = std::vector<milvus::plan::PlanNodePtr>{project_node};
// agg node, group by int16, int32, sum(int64)
std::vector<expr::FieldAccessTypeExprPtr> groupingKeys;
groupingKeys.emplace_back(std::make_shared<const expr::FieldAccessTypeExpr>(
DataType::INT16, int16_field, int16_id));
groupingKeys.emplace_back(std::make_shared<const expr::FieldAccessTypeExpr>(
DataType::INT32, int32_field, int32_id));
std::string agg_name = "sum";
std::vector<plan::AggregationNode::Aggregate> aggregates;
auto agg_input = std::make_shared<expr::FieldAccessTypeExpr>(
DataType::INT64, int64_field, int64_id);
auto call = std::make_shared<const expr::CallExpr>(
agg_name, std::vector<expr::TypedExprPtr>{agg_input}, nullptr);
aggregates.emplace_back(plan::AggregationNode::Aggregate{call});
aggregates.back().rawInputTypes_.emplace_back(DataType::INT64);
aggregates.back().resultType_ = GetAggResultType(agg_name, DataType::INT64);
PlanNodePtr agg_node = std::make_shared<plan::AggregationNode>(
milvus::plan::GetNextPlanNodeId(),
std::move(groupingKeys),
std::vector<std::string>{"sum"},
std::move(aggregates),
sources);
auto plan = plan::PlanFragment(agg_node);
auto query_context = std::make_shared<milvus::exec::QueryContext>(
"test1", segment_.get(), num_rows_, MAX_TIMESTAMP);
auto op_context = milvus::OpContext();
query_context->set_op_context(&op_context);
auto task = Task::Create("task_query_group_by", plan, 0, query_context);
RowVectorPtr ret = execPlan(task);
EXPECT_EQ(3, ret->childrens().size());
int size = -1;
for (int i = 0; i < 3; i++) {
auto column = std::dynamic_pointer_cast<ColumnVector>(ret->child(i));
if (size == -1) {
size = column->size();
} else {
EXPECT_TRUE(size == column->size());
// all columns in the returned row vector should be the same size
}
}
if (nullable) {
EXPECT_TRUE(size == 6);
} else if (!nullable) {
EXPECT_TRUE(size == 5);
}
for (int i = 0; i < 3; i++) {
auto column = std::dynamic_pointer_cast<ColumnVector>(ret->child(i));
for (auto j = 0; j < size; j++) {
if (i == 0) {
auto val = column->ValueAt<int16_t>(j);
std::cout << "int16_val:" << val << std::endl;
}
if (i == 1) {
auto val = column->ValueAt<int32_t>(j);
std::cout << "int32_val:" << val << std::endl;
}
if (i == 2) {
auto val = column->ValueAt<int64_t>(j);
std::cout << "int64_val:" << val << std::endl;
}
}
}
}
TEST_P(QueryAggTest, GroupVariableLengthMultipleColumn) {
std::vector<milvus::plan::PlanNodePtr> sources;
auto nullable = GetParam();
//set up mvcc_node + project_node + agg_node
PlanNodePtr mvcc_node = std::make_shared<milvus::plan::MvccNode>(
milvus::plan::GetNextPlanNodeId(), sources);
sources = std::vector<milvus::plan::PlanNodePtr>{mvcc_node};
// project node
auto int8_id = field_map_[int8_field];
auto str_id = field_map_[string_field];
auto float_id = field_map_[float_field];
auto double_id = field_map_[double_field];
PlanNodePtr project_node = std::make_shared<milvus::plan::ProjectNode>(
milvus::plan::GetNextPlanNodeId(),
std::vector<FieldId>{int8_id, str_id, float_id, double_id},
std::vector<std::string>{
int8_field, string_field, float_field, double_field},
std::vector<DataType>{DataType::INT8,
DataType::VARCHAR,
DataType::FLOAT,
DataType::DOUBLE},
sources);
sources = std::vector<milvus::plan::PlanNodePtr>{project_node};
// group by int8_field, str_field, sum(float), sum(double)
std::vector<expr::FieldAccessTypeExprPtr> groupingKeys;
groupingKeys.emplace_back(std::make_shared<const expr::FieldAccessTypeExpr>(
DataType::INT8, int8_field, int8_id));
groupingKeys.emplace_back(std::make_shared<const expr::FieldAccessTypeExpr>(
DataType::VARCHAR, string_field, str_id));
std::string agg_name = "sum";
std::vector<plan::AggregationNode::Aggregate> aggregates;
// agg1: sum(float)
{
auto agg_input = std::make_shared<expr::FieldAccessTypeExpr>(
DataType::FLOAT, float_field, float_id);
auto call = std::make_shared<const expr::CallExpr>(
agg_name, std::vector<expr::TypedExprPtr>{agg_input}, nullptr);
aggregates.emplace_back(plan::AggregationNode::Aggregate{call});
aggregates.back().rawInputTypes_.emplace_back(DataType::FLOAT);
aggregates.back().resultType_ =
GetAggResultType(agg_name, DataType::FLOAT);
}
// agg2: sum(double)
{
auto agg_input = std::make_shared<expr::FieldAccessTypeExpr>(
DataType::DOUBLE, double_field, double_id);
auto call = std::make_shared<const expr::CallExpr>(
agg_name, std::vector<expr::TypedExprPtr>{agg_input}, nullptr);
aggregates.emplace_back(plan::AggregationNode::Aggregate{call});
aggregates.back().rawInputTypes_.emplace_back(DataType::DOUBLE);
aggregates.back().resultType_ =
GetAggResultType(agg_name, DataType::DOUBLE);
}
PlanNodePtr agg_node = std::make_shared<plan::AggregationNode>(
milvus::plan::GetNextPlanNodeId(),
std::move(groupingKeys),
std::vector<std::string>{"sum", "sum"},
std::move(aggregates),
sources);
auto plan = plan::PlanFragment(agg_node);
auto query_context = std::make_shared<milvus::exec::QueryContext>(
"test1", segment_.get(), num_rows_, MAX_TIMESTAMP);
auto op_context = milvus::OpContext();
query_context->set_op_context(&op_context);
auto task = Task::Create("task_query_group_by", plan, 0, query_context);
RowVectorPtr ret = execPlan(task);
EXPECT_EQ(4, ret->childrens().size());
int size = -1;
for (int i = 0; i < 4; i++) {
auto column = std::dynamic_pointer_cast<ColumnVector>(ret->child(i));
if (size == -1) {
size = column->size();
} else {
EXPECT_TRUE(size == column->size());
// all columns in the returned row vector should be the same size
}
}
if (nullable) {
EXPECT_TRUE(size == 10);
} else if (!nullable) {
EXPECT_EQ(size, 5);
}
for (int i = 0; i < 4; i++) {
auto column = std::dynamic_pointer_cast<ColumnVector>(ret->child(i));
for (auto j = 0; j < size; j++) {
if (i == 0) {
auto val = column->ValueAt<int8_t>(j);
std::cout << "int8_val:" << int32_t(val) << std::endl;
}
if (i == 1) {
auto val = column->ValueAt<std::string>(j);
std::cout << "str_val:" << val << std::endl;
}
if (i == 2) {
auto val = column->ValueAt<double>(j);
std::cout << "float_val:" << val << std::endl;
}
if (i == 3) {
auto val = column->ValueAt<double>(j);
std::cout << "double_val:" << val << std::endl;
}
}
}
}
TEST_P(QueryAggTest, CountAggTest) {
std::vector<milvus::plan::PlanNodePtr> sources;
auto nullable = GetParam();
//set up mvcc_node + project_node + agg_node
PlanNodePtr mvcc_node = std::make_shared<milvus::plan::MvccNode>(
milvus::plan::GetNextPlanNodeId(), sources);
sources = std::vector<milvus::plan::PlanNodePtr>{mvcc_node};
// project node
auto int8_id = field_map_[int8_field];
auto str_id = field_map_[string_field];
auto double_id = field_map_[double_field];
PlanNodePtr project_node = std::make_shared<milvus::plan::ProjectNode>(
milvus::plan::GetNextPlanNodeId(),
std::vector<FieldId>{int8_id, str_id, double_id},
std::vector<std::string>{int8_field, string_field, double_field},
std::vector<DataType>{
DataType::INT8, DataType::VARCHAR, DataType::DOUBLE},
sources);
sources = std::vector<milvus::plan::PlanNodePtr>{project_node};
// group by int8_field, str_field, count(*), count(double)
std::vector<expr::FieldAccessTypeExprPtr> groupingKeys;
groupingKeys.emplace_back(std::make_shared<const expr::FieldAccessTypeExpr>(
DataType::INT8, int8_field, int8_id));
groupingKeys.emplace_back(std::make_shared<const expr::FieldAccessTypeExpr>(
DataType::VARCHAR, string_field, str_id));
std::string agg_name = "count";
std::vector<plan::AggregationNode::Aggregate> aggregates;
// count(*)
{
auto call = std::make_shared<const expr::CallExpr>(
agg_name, std::vector<expr::TypedExprPtr>{}, nullptr);
aggregates.emplace_back(plan::AggregationNode::Aggregate{call});
aggregates.back().resultType_ =
GetAggResultType(agg_name, DataType::NONE);
}
// count(double)
{
auto agg_input = std::make_shared<expr::FieldAccessTypeExpr>(
DataType::DOUBLE, double_field, double_id);
auto call = std::make_shared<const expr::CallExpr>(
agg_name, std::vector<expr::TypedExprPtr>{agg_input}, nullptr);
aggregates.emplace_back(plan::AggregationNode::Aggregate{call});
aggregates.back().rawInputTypes_.emplace_back(DataType::DOUBLE);
aggregates.back().resultType_ =
GetAggResultType(agg_name, DataType::DOUBLE);
}
PlanNodePtr agg_node = std::make_shared<plan::AggregationNode>(
milvus::plan::GetNextPlanNodeId(),
std::move(groupingKeys),
std::vector<std::string>{agg_name, agg_name},
std::move(aggregates),
sources);
auto plan = plan::PlanFragment(agg_node);
auto query_context = std::make_shared<milvus::exec::QueryContext>(
"test1", segment_.get(), num_rows_, MAX_TIMESTAMP);
auto op_context = milvus::OpContext();
query_context->set_op_context(&op_context);
auto task = Task::Create("task_query_group_by", plan, 0, query_context);
RowVectorPtr ret = execPlan(task);
EXPECT_EQ(4, ret->childrens().size());
int size = -1;
for (int i = 0; i < 4; i++) {
auto column = std::dynamic_pointer_cast<ColumnVector>(ret->child(i));
if (size == -1) {
size = column->size();
} else {
EXPECT_TRUE(size == column->size());
// all columns in the returned row vector should be the same size
}
}
if (nullable) {
EXPECT_TRUE(size == 10);
} else if (!nullable) {
EXPECT_EQ(size, 5);
}
// Check the count values in column 2 (count(*))
auto count_column = std::dynamic_pointer_cast<ColumnVector>(ret->child(2));
for (int j = 0; j < size; j++) {
auto count_val = count_column->ValueAt<int64_t>(j);
if (nullable) {
// For nullable case, each count should be 1
EXPECT_EQ(count_val, 1);
} else {
// For non-nullable case, each count should be 2
EXPECT_EQ(count_val, 2);
}
}
for (int i = 0; i < 4; i++) {
auto column = std::dynamic_pointer_cast<ColumnVector>(ret->child(i));
for (auto j = 0; j < size; j++) {
if (i == 0) {
auto val = column->ValueAt<int8_t>(j);
std::cout << "int8_val:" << int32_t(val) << std::endl;
}
if (i == 1) {
auto val = column->ValueAt<std::string>(j);
std::cout << "str_val:" << val << std::endl;
}
if (i == 2) {
auto val = column->ValueAt<int64_t>(j);
std::cout << "int64_t_val:" << val << std::endl;
}
if (i == 3) {
auto val = column->ValueAt<int64_t>(j);
std::cout << "int64_t_val:" << val << std::endl;
}
}
}
}
TEST_P(QueryAggTest, GlobalCountAggTest) {
std::vector<milvus::plan::PlanNodePtr> sources;
auto nullable = GetParam();
//set up mvcc_node + agg_node: global aggregation no need project column
PlanNodePtr mvcc_node = std::make_shared<milvus::plan::MvccNode>(
milvus::plan::GetNextPlanNodeId(), sources);
sources = std::vector<milvus::plan::PlanNodePtr>{mvcc_node};
std::string agg_name = "count";
std::vector<plan::AggregationNode::Aggregate> aggregates;
// count(*)
{
auto call = std::make_shared<const expr::CallExpr>(
agg_name, std::vector<expr::TypedExprPtr>{}, nullptr);
aggregates.emplace_back(plan::AggregationNode::Aggregate{call});
aggregates.back().resultType_ =
GetAggResultType(agg_name, DataType::NONE);
}
PlanNodePtr agg_node = std::make_shared<plan::AggregationNode>(
milvus::plan::GetNextPlanNodeId(),
std::vector<expr::FieldAccessTypeExprPtr>{},
std::vector<std::string>{agg_name},
std::move(aggregates),
sources);
auto plan = plan::PlanFragment(agg_node);
auto query_context = std::make_shared<milvus::exec::QueryContext>(
"test1", segment_.get(), num_rows_, MAX_TIMESTAMP);
auto op_context = milvus::OpContext();
query_context->set_op_context(&op_context);
auto task = Task::Create("task_query_group_by", plan, 0, query_context);
RowVectorPtr ret = execPlan(task);
EXPECT_EQ(1, ret->childrens().size());
auto output = ret->childrens()[0];
EXPECT_EQ(1, output->size());
auto output_column = std::dynamic_pointer_cast<ColumnVector>(output);
auto actual_count = output_column->ValueAt<int64_t>(0);
std::cout << "count:" << actual_count << std::endl;
EXPECT_EQ(num_rows_, actual_count);
// count(*) will always get all results' count no matter nullable or not
}
// Test count(*) when activeCount is zero
TEST_P(QueryAggTest, GlobalCountEmptyTest) {
std::vector<milvus::plan::PlanNodePtr> sources;
PlanNodePtr mvcc_node = std::make_shared<milvus::plan::MvccNode>(
milvus::plan::GetNextPlanNodeId(), sources);
sources = std::vector<milvus::plan::PlanNodePtr>{mvcc_node};
std::string agg_name = "count";
std::vector<plan::AggregationNode::Aggregate> aggregates;
// count(*)
{
auto call = std::make_shared<const expr::CallExpr>(
agg_name, std::vector<expr::TypedExprPtr>{}, nullptr);
aggregates.emplace_back(plan::AggregationNode::Aggregate{call});
aggregates.back().resultType_ =
GetAggResultType(agg_name, DataType::NONE);
}
PlanNodePtr agg_node = std::make_shared<plan::AggregationNode>(
milvus::plan::GetNextPlanNodeId(),
std::vector<expr::FieldAccessTypeExprPtr>{},
std::vector<std::string>{agg_name},
std::move(aggregates),
sources);
auto plan = plan::PlanFragment(agg_node);
auto query_context = std::make_shared<milvus::exec::QueryContext>(
"test1", segment_.get(), 0, MAX_TIMESTAMP);
auto op_context = milvus::OpContext();
query_context->set_op_context(&op_context);
auto task = Task::Create("task_query_group_by", plan, 0, query_context);
RowVectorPtr ret = execPlan(task);
EXPECT_EQ(1, ret->childrens().size());
auto output = ret->childrens()[0];
EXPECT_EQ(1, output->size());
auto output_column = std::dynamic_pointer_cast<ColumnVector>(output);
auto actual_count = output_column->ValueAt<int64_t>(0);
EXPECT_EQ(0, actual_count);
// count(*) will get zero if no valid input into agg node
}
+7
View File
@@ -25,6 +25,7 @@
#include "test_utils/cachinglayer_test_utils.h"
#include "test_utils/DataGen.h"
#include "test_utils/storage_test_utils.h"
#include "exec/expression/function/FunctionFactory.h"
using namespace milvus;
using namespace milvus::query;
@@ -670,6 +671,9 @@ TEST(Sealed, LoadFieldData) {
}
TEST(Sealed, ClearData) {
milvus::exec::expression::FunctionFactory& factory =
milvus::exec::expression::FunctionFactory::Instance();
factory.Initialize();
auto dim = 4;
auto topK = 5;
auto N = ROW_COUNT;
@@ -1382,6 +1386,9 @@ TEST(Sealed, DeleteCount) {
}
TEST(Sealed, RealCount) {
milvus::exec::expression::FunctionFactory& factory =
milvus::exec::expression::FunctionFactory::Instance();
factory.Initialize();
auto schema = std::make_shared<Schema>();
auto pk = schema->AddDebugField("pk", DataType::INT64);
schema->set_primary_field_id(pk);
@@ -712,6 +712,8 @@ TEST(GroupBY, GrowingRawData) {
schema->set_primary_field_id(int64_field_id);
auto config = SegcoreConfig::default_config();
auto original_chunk_rows = config.get_chunk_rows();
DeferLambda([&]() { config.set_chunk_rows(original_chunk_rows); });
config.set_chunk_rows(128);
config.set_enable_interim_segment_index(
false); //no growing index, test brute force
@@ -810,6 +812,8 @@ TEST(GroupBY, GrowingIndex) {
std::make_shared<CollectionIndexMeta>(10000, std::move(fieldMap));
auto config = SegcoreConfig::default_config();
auto original_chunk_rows = config.get_chunk_rows();
DeferLambda([&]() { config.set_chunk_rows(original_chunk_rows); });
config.set_chunk_rows(128);
config.set_enable_interim_segment_index(
true); //no growing index, test growing inter index
+3 -2
View File
@@ -14,6 +14,7 @@
#include <boost/algorithm/string/predicate.hpp>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <memory>
#include <random>
#include <string>
@@ -557,13 +558,13 @@ DataGen(SchemaPtr schema,
auto insert_cols =
[&insert_data](
auto& data, int64_t count, auto& field_meta, bool random_valid) {
FixedVector<bool> valid_data(count);
FixedVector<bool> valid_data(count, true);
if (field_meta.is_nullable()) {
for (int i = 0; i < count; ++i) {
int x = i;
if (random_valid)
x = rand();
valid_data[i] = x % 2 == 0 ? true : false;
valid_data[i] = x % 2 == 0;
}
}
auto array = milvus::segcore::CreateDataArrayFrom(

Some files were not shown because too many files have changed in this diff Show More