mirror of
https://github.com/milvus-io/milvus.git
synced 2026-07-21 10:15:43 +00:00
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:
co-authored by
MrPresent-Han
parent
4c6e33f326
commit
b7ee93fc52
@@ -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)}
|
||||
}
|
||||
@@ -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()}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"); \
|
||||
} \
|
||||
}()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+16
-27
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+8
-10
@@ -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_;
|
||||
}
|
||||
|
||||
+7
-6
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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})
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -59,7 +59,6 @@ struct RetrievePlanNode : PlanNode {
|
||||
|
||||
std::shared_ptr<milvus::plan::PlanNode> plannodes_;
|
||||
|
||||
bool is_count_;
|
||||
int64_t limit_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+4
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user