github.com/weaviate/weaviate@v1.24.6/adapters/repos/db/vector/flat/index.go (about) 1 // _ _ 2 // __ _____ __ ___ ___ __ _| |_ ___ 3 // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ 4 // \ V V / __/ (_| |\ V /| | (_| | || __/ 5 // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| 6 // 7 // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. 8 // 9 // CONTACT: hello@weaviate.io 10 // 11 12 package flat 13 14 import ( 15 "context" 16 "encoding/binary" 17 "fmt" 18 "io" 19 "math" 20 "strings" 21 "sync" 22 "sync/atomic" 23 24 "github.com/pkg/errors" 25 "github.com/sirupsen/logrus" 26 "github.com/weaviate/weaviate/adapters/repos/db/helpers" 27 "github.com/weaviate/weaviate/adapters/repos/db/lsmkv" 28 "github.com/weaviate/weaviate/adapters/repos/db/priorityqueue" 29 "github.com/weaviate/weaviate/adapters/repos/db/vector/cache" 30 "github.com/weaviate/weaviate/adapters/repos/db/vector/common" 31 "github.com/weaviate/weaviate/adapters/repos/db/vector/compressionhelpers" 32 "github.com/weaviate/weaviate/adapters/repos/db/vector/hnsw/distancer" 33 "github.com/weaviate/weaviate/entities/schema" 34 flatent "github.com/weaviate/weaviate/entities/vectorindex/flat" 35 "github.com/weaviate/weaviate/usecases/floatcomp" 36 ) 37 38 const ( 39 compressionBQ = "bq" 40 compressionPQ = "pq" 41 compressionNone = "none" 42 ) 43 44 type flat struct { 45 sync.Mutex 46 id string 47 targetVector string 48 dims int32 49 store *lsmkv.Store 50 logger logrus.FieldLogger 51 distancerProvider distancer.Provider 52 trackDimensionsOnce sync.Once 53 rescore int64 54 bq compressionhelpers.BinaryQuantizer 55 56 pqResults *common.PqMaxPool 57 pool *pools 58 59 compression string 60 bqCache cache.Cache[uint64] 61 } 62 63 type distanceCalc func(vecAsBytes []byte) (float32, error) 64 65 func New(cfg Config, uc flatent.UserConfig, store *lsmkv.Store) (*flat, error) { 66 if err := cfg.Validate(); err != nil { 67 return nil, errors.Wrap(err, "invalid config") 68 } 69 70 logger := cfg.Logger 71 if logger == nil { 72 l := logrus.New() 73 l.Out = io.Discard 74 logger = l 75 } 76 77 index := &flat{ 78 id: cfg.ID, 79 targetVector: cfg.TargetVector, 80 logger: logger, 81 distancerProvider: cfg.DistanceProvider, 82 rescore: extractCompressionRescore(uc), 83 pqResults: common.NewPqMaxPool(100), 84 compression: extractCompression(uc), 85 pool: newPools(), 86 store: store, 87 } 88 index.initBuckets(context.Background()) 89 if uc.BQ.Enabled && uc.BQ.Cache { 90 index.bqCache = cache.NewShardedUInt64LockCache(index.getBQVector, uc.VectorCacheMaxObjects, cfg.Logger, 0) 91 } 92 93 return index, nil 94 } 95 96 func (flat *flat) getBQVector(ctx context.Context, id uint64) ([]uint64, error) { 97 key := flat.pool.byteSlicePool.Get(8) 98 defer flat.pool.byteSlicePool.Put(key) 99 binary.BigEndian.PutUint64(key.slice, id) 100 bytes, err := flat.store.Bucket(flat.getCompressedBucketName()).Get(key.slice) 101 if err != nil { 102 return nil, err 103 } 104 return uint64SliceFromByteSlice(bytes, make([]uint64, len(bytes)/8)), nil 105 } 106 107 func extractCompression(uc flatent.UserConfig) string { 108 if uc.BQ.Enabled && uc.PQ.Enabled { 109 return compressionNone 110 } 111 112 if uc.BQ.Enabled { 113 return compressionBQ 114 } 115 116 if uc.PQ.Enabled { 117 return compressionPQ 118 } 119 120 return compressionNone 121 } 122 123 func extractCompressionRescore(uc flatent.UserConfig) int64 { 124 compression := extractCompression(uc) 125 switch compression { 126 case compressionPQ: 127 return int64(uc.PQ.RescoreLimit) 128 case compressionBQ: 129 return int64(uc.BQ.RescoreLimit) 130 default: 131 return 0 132 } 133 } 134 135 func (index *flat) storeCompressedVector(id uint64, vector []byte) { 136 index.storeGenericVector(id, vector, index.getCompressedBucketName()) 137 } 138 139 func (index *flat) storeVector(id uint64, vector []byte) { 140 index.storeGenericVector(id, vector, index.getBucketName()) 141 } 142 143 func (index *flat) storeGenericVector(id uint64, vector []byte, bucket string) { 144 idBytes := make([]byte, 8) 145 binary.BigEndian.PutUint64(idBytes, id) 146 index.store.Bucket(bucket).Put(idBytes, vector) 147 } 148 149 func (index *flat) isBQ() bool { 150 return index.compression == compressionBQ 151 } 152 153 func (index *flat) isBQCached() bool { 154 return index.bqCache != nil 155 } 156 157 func (index *flat) Compressed() bool { 158 return index.compression != compressionNone 159 } 160 161 func (index *flat) getBucketName() string { 162 if index.targetVector != "" { 163 return fmt.Sprintf("%s_%s", helpers.VectorsBucketLSM, index.targetVector) 164 } 165 return helpers.VectorsBucketLSM 166 } 167 168 func (index *flat) getCompressedBucketName() string { 169 if index.targetVector != "" { 170 return fmt.Sprintf("%s_%s", helpers.VectorsCompressedBucketLSM, index.targetVector) 171 } 172 return helpers.VectorsCompressedBucketLSM 173 } 174 175 func (index *flat) initBuckets(ctx context.Context) error { 176 if err := index.store.CreateOrLoadBucket(ctx, index.getBucketName(), 177 lsmkv.WithForceCompation(true), 178 lsmkv.WithUseBloomFilter(false), 179 lsmkv.WithCalcCountNetAdditions(false), 180 ); err != nil { 181 return fmt.Errorf("Create or load flat vectors bucket: %w", err) 182 } 183 if index.isBQ() { 184 if err := index.store.CreateOrLoadBucket(ctx, index.getCompressedBucketName(), 185 lsmkv.WithForceCompation(true), 186 lsmkv.WithUseBloomFilter(false), 187 lsmkv.WithCalcCountNetAdditions(false), 188 ); err != nil { 189 return fmt.Errorf("Create or load flat compressed vectors bucket: %w", err) 190 } 191 } 192 return nil 193 } 194 195 func (index *flat) AddBatch(ctx context.Context, ids []uint64, vectors [][]float32) error { 196 if err := ctx.Err(); err != nil { 197 return err 198 } 199 if len(ids) != len(vectors) { 200 return errors.Errorf("ids and vectors sizes does not match") 201 } 202 if len(ids) == 0 { 203 return errors.Errorf("insertBatch called with empty lists") 204 } 205 for i := range ids { 206 if err := ctx.Err(); err != nil { 207 return err 208 } 209 if err := index.Add(ids[i], vectors[i]); err != nil { 210 return err 211 } 212 } 213 return nil 214 } 215 216 func byteSliceFromUint64Slice(vector []uint64, slice []byte) []byte { 217 for i := range vector { 218 binary.LittleEndian.PutUint64(slice[i*8:], vector[i]) 219 } 220 return slice 221 } 222 223 func byteSliceFromFloat32Slice(vector []float32, slice []byte) []byte { 224 for i := range vector { 225 binary.LittleEndian.PutUint32(slice[i*4:], math.Float32bits(vector[i])) 226 } 227 return slice 228 } 229 230 func uint64SliceFromByteSlice(vector []byte, slice []uint64) []uint64 { 231 for i := range slice { 232 slice[i] = binary.LittleEndian.Uint64(vector[i*8:]) 233 } 234 return slice 235 } 236 237 func float32SliceFromByteSlice(vector []byte, slice []float32) []float32 { 238 for i := range slice { 239 slice[i] = math.Float32frombits(binary.LittleEndian.Uint32(vector[i*4:])) 240 } 241 return slice 242 } 243 244 func (index *flat) Add(id uint64, vector []float32) error { 245 index.trackDimensionsOnce.Do(func() { 246 atomic.StoreInt32(&index.dims, int32(len(vector))) 247 248 if index.isBQ() { 249 index.bq = compressionhelpers.NewBinaryQuantizer(nil) 250 } 251 }) 252 if len(vector) != int(index.dims) { 253 return errors.Errorf("insert called with a vector of the wrong size") 254 } 255 vector = index.normalized(vector) 256 slice := make([]byte, len(vector)*4) 257 index.storeVector(id, byteSliceFromFloat32Slice(vector, slice)) 258 259 if index.isBQ() { 260 vectorBQ := index.bq.Encode(vector) 261 if index.isBQCached() { 262 index.bqCache.Grow(id) 263 index.bqCache.Preload(id, vectorBQ) 264 } 265 slice = make([]byte, len(vectorBQ)*8) 266 index.storeCompressedVector(id, byteSliceFromUint64Slice(vectorBQ, slice)) 267 } 268 return nil 269 } 270 271 func (index *flat) Delete(ids ...uint64) error { 272 for i := range ids { 273 if index.isBQCached() { 274 index.bqCache.Delete(context.Background(), ids[i]) 275 } 276 idBytes := make([]byte, 8) 277 binary.BigEndian.PutUint64(idBytes, ids[i]) 278 279 if err := index.store.Bucket(index.getBucketName()).Delete(idBytes); err != nil { 280 return err 281 } 282 283 if index.isBQ() { 284 if err := index.store.Bucket(index.getCompressedBucketName()).Delete(idBytes); err != nil { 285 return err 286 } 287 } 288 } 289 return nil 290 } 291 292 func (index *flat) searchTimeRescore(k int) int { 293 // load atomically, so we can get away with concurrent updates of the 294 // userconfig without having to set a lock each time we try to read - which 295 // can be so common that it would cause considerable overhead 296 if rescore := int(atomic.LoadInt64(&index.rescore)); rescore > k { 297 return rescore 298 } 299 return k 300 } 301 302 func (index *flat) SearchByVector(vector []float32, k int, allow helpers.AllowList) ([]uint64, []float32, error) { 303 switch index.compression { 304 case compressionBQ: 305 return index.searchByVectorBQ(vector, k, allow) 306 case compressionPQ: 307 // use uncompressed for now 308 fallthrough 309 default: 310 return index.searchByVector(vector, k, allow) 311 } 312 } 313 314 func (index *flat) searchByVector(vector []float32, k int, allow helpers.AllowList) ([]uint64, []float32, error) { 315 heap := index.pqResults.GetMax(k) 316 defer index.pqResults.Put(heap) 317 318 vector = index.normalized(vector) 319 320 if err := index.findTopVectors(heap, allow, k, 321 index.store.Bucket(index.getBucketName()).Cursor, 322 index.createDistanceCalc(vector), 323 ); err != nil { 324 return nil, nil, err 325 } 326 327 ids, dists := index.extractHeap(heap) 328 return ids, dists, nil 329 } 330 331 func (index *flat) createDistanceCalc(vector []float32) distanceCalc { 332 return func(vecAsBytes []byte) (float32, error) { 333 vecSlice := index.pool.float32SlicePool.Get(len(vecAsBytes) / 4) 334 defer index.pool.float32SlicePool.Put(vecSlice) 335 336 candidate := float32SliceFromByteSlice(vecAsBytes, vecSlice.slice) 337 distance, _, err := index.distancerProvider.SingleDist(vector, candidate) 338 return distance, err 339 } 340 } 341 342 func (index *flat) searchByVectorBQ(vector []float32, k int, allow helpers.AllowList) ([]uint64, []float32, error) { 343 rescore := index.searchTimeRescore(k) 344 heap := index.pqResults.GetMax(rescore) 345 defer index.pqResults.Put(heap) 346 347 vector = index.normalized(vector) 348 vectorBQ := index.bq.Encode(vector) 349 350 if index.isBQCached() { 351 if err := index.findTopVectorsCached(heap, allow, rescore, vectorBQ); err != nil { 352 return nil, nil, err 353 } 354 } else { 355 if err := index.findTopVectors(heap, allow, rescore, 356 index.store.Bucket(index.getCompressedBucketName()).Cursor, 357 index.createDistanceCalcBQ(vectorBQ), 358 ); err != nil { 359 return nil, nil, err 360 } 361 } 362 363 distanceCalc := index.createDistanceCalc(vector) 364 idsSlice := index.pool.uint64SlicePool.Get(heap.Len()) 365 defer index.pool.uint64SlicePool.Put(idsSlice) 366 367 for i := range idsSlice.slice { 368 idsSlice.slice[i] = heap.Pop().ID 369 } 370 for _, id := range idsSlice.slice { 371 candidateAsBytes, err := index.vectorById(id) 372 if err != nil { 373 return nil, nil, err 374 } 375 distance, err := distanceCalc(candidateAsBytes) 376 if err != nil { 377 return nil, nil, err 378 } 379 index.insertToHeap(heap, k, id, distance) 380 } 381 382 ids, dists := index.extractHeap(heap) 383 return ids, dists, nil 384 } 385 386 func (index *flat) createDistanceCalcBQ(vectorBQ []uint64) distanceCalc { 387 return func(vecAsBytes []byte) (float32, error) { 388 vecSliceBQ := index.pool.uint64SlicePool.Get(len(vecAsBytes) / 8) 389 defer index.pool.uint64SlicePool.Put(vecSliceBQ) 390 391 candidate := uint64SliceFromByteSlice(vecAsBytes, vecSliceBQ.slice) 392 return index.bq.DistanceBetweenCompressedVectors(candidate, vectorBQ) 393 } 394 } 395 396 func (index *flat) vectorById(id uint64) ([]byte, error) { 397 idSlice := index.pool.byteSlicePool.Get(8) 398 defer index.pool.byteSlicePool.Put(idSlice) 399 400 binary.BigEndian.PutUint64(idSlice.slice, id) 401 return index.store.Bucket(index.getBucketName()).Get(idSlice.slice) 402 } 403 404 // populates given heap with smallest distances and corresponding ids calculated by 405 // distanceCalc 406 func (index *flat) findTopVectors(heap *priorityqueue.Queue[any], 407 allow helpers.AllowList, limit int, cursorFn func() *lsmkv.CursorReplace, 408 distanceCalc distanceCalc, 409 ) error { 410 var key []byte 411 var v []byte 412 var id uint64 413 allowMax := uint64(0) 414 415 cursor := cursorFn() 416 defer cursor.Close() 417 418 if allow != nil { 419 // nothing allowed, skip search 420 if allow.IsEmpty() { 421 return nil 422 } 423 424 allowMax = allow.Max() 425 426 idSlice := index.pool.byteSlicePool.Get(8) 427 binary.BigEndian.PutUint64(idSlice.slice, allow.Min()) 428 key, v = cursor.Seek(idSlice.slice) 429 index.pool.byteSlicePool.Put(idSlice) 430 } else { 431 key, v = cursor.First() 432 } 433 434 // since keys are sorted, once key/id get greater than max allowed one 435 // further search can be stopped 436 for ; key != nil && (allow == nil || id <= allowMax); key, v = cursor.Next() { 437 id = binary.BigEndian.Uint64(key) 438 if allow == nil || allow.Contains(id) { 439 distance, err := distanceCalc(v) 440 if err != nil { 441 return err 442 } 443 index.insertToHeap(heap, limit, id, distance) 444 } 445 } 446 return nil 447 } 448 449 // populates given heap with smallest distances and corresponding ids calculated by 450 // distanceCalc 451 func (index *flat) findTopVectorsCached(heap *priorityqueue.Queue[any], 452 allow helpers.AllowList, limit int, vectorBQ []uint64, 453 ) error { 454 var id uint64 455 allowMax := uint64(0) 456 457 if allow != nil { 458 // nothing allowed, skip search 459 if allow.IsEmpty() { 460 return nil 461 } 462 463 allowMax = allow.Max() 464 465 id = allow.Min() 466 } else { 467 id = 0 468 } 469 all := index.bqCache.Len() 470 471 // since keys are sorted, once key/id get greater than max allowed one 472 // further search can be stopped 473 for ; id < uint64(all) && (allow == nil || id <= allowMax); id++ { 474 if allow == nil || allow.Contains(id) { 475 vec, err := index.bqCache.Get(context.Background(), id) 476 if err != nil { 477 return err 478 } 479 if len(vec) == 0 { 480 continue 481 } 482 distance, err := index.bq.DistanceBetweenCompressedVectors(vec, vectorBQ) 483 if err != nil { 484 return err 485 } 486 index.insertToHeap(heap, limit, id, distance) 487 } 488 } 489 return nil 490 } 491 492 func (index *flat) insertToHeap(heap *priorityqueue.Queue[any], 493 limit int, id uint64, distance float32, 494 ) { 495 if heap.Len() < limit { 496 heap.Insert(id, distance) 497 } else if heap.Top().Dist > distance { 498 heap.Pop() 499 heap.Insert(id, distance) 500 } 501 } 502 503 func (index *flat) extractHeap(heap *priorityqueue.Queue[any], 504 ) ([]uint64, []float32) { 505 len := heap.Len() 506 507 ids := make([]uint64, len) 508 dists := make([]float32, len) 509 for i := len - 1; i >= 0; i-- { 510 item := heap.Pop() 511 ids[i] = item.ID 512 dists[i] = item.Dist 513 } 514 return ids, dists 515 } 516 517 func (index *flat) normalized(vector []float32) []float32 { 518 if index.distancerProvider.Type() == "cosine-dot" { 519 // cosine-dot requires normalized vectors, as the dot product and cosine 520 // similarity are only identical if the vector is normalized 521 return distancer.Normalize(vector) 522 } 523 return vector 524 } 525 526 func (index *flat) SearchByVectorDistance(vector []float32, targetDistance float32, maxLimit int64, allow helpers.AllowList) ([]uint64, []float32, error) { 527 var ( 528 searchParams = newSearchByDistParams(maxLimit) 529 530 resultIDs []uint64 531 resultDist []float32 532 ) 533 534 recursiveSearch := func() (bool, error) { 535 totalLimit := searchParams.TotalLimit() 536 ids, dist, err := index.SearchByVector(vector, totalLimit, allow) 537 if err != nil { 538 return false, errors.Wrap(err, "vector search") 539 } 540 541 // if there is less results than given limit search can be stopped 542 shouldContinue := !(len(ids) < totalLimit) 543 544 // ensures the indexes aren't out of range 545 offsetCap := searchParams.OffsetCapacity(ids) 546 totalLimitCap := searchParams.TotalLimitCapacity(ids) 547 548 if offsetCap == totalLimitCap { 549 return false, nil 550 } 551 552 ids, dist = ids[offsetCap:totalLimitCap], dist[offsetCap:totalLimitCap] 553 for i := range ids { 554 if aboveThresh := dist[i] <= targetDistance; aboveThresh || 555 floatcomp.InDelta(float64(dist[i]), float64(targetDistance), 1e-6) { 556 resultIDs = append(resultIDs, ids[i]) 557 resultDist = append(resultDist, dist[i]) 558 } else { 559 // as soon as we encounter a certainty which 560 // is below threshold, we can stop searching 561 shouldContinue = false 562 break 563 } 564 } 565 566 return shouldContinue, nil 567 } 568 569 var shouldContinue bool 570 var err error 571 for shouldContinue, err = recursiveSearch(); shouldContinue && err == nil; { 572 searchParams.Iterate() 573 if searchParams.MaxLimitReached() { 574 index.logger. 575 WithField("action", "unlimited_vector_search"). 576 Warnf("maximum search limit of %d results has been reached", 577 searchParams.MaximumSearchLimit()) 578 break 579 } 580 } 581 if err != nil { 582 return nil, nil, err 583 } 584 585 return resultIDs, resultDist, nil 586 } 587 588 func (index *flat) UpdateUserConfig(updated schema.VectorIndexConfig, callback func()) error { 589 parsed, ok := updated.(flatent.UserConfig) 590 if !ok { 591 callback() 592 return errors.Errorf("config is not UserConfig, but %T", updated) 593 } 594 595 // Store automatically as a lock here would be very expensive, this value is 596 // read on every single user-facing search, which can be highly concurrent 597 atomic.StoreInt64(&index.rescore, extractCompressionRescore(parsed)) 598 599 callback() 600 return nil 601 } 602 603 func (index *flat) Drop(ctx context.Context) error { 604 // nothing to do here 605 // Shard::drop will take care of handling store's buckets 606 return nil 607 } 608 609 func (index *flat) Flush() error { 610 // nothing to do here 611 // Shard will take care of handling store's buckets 612 return nil 613 } 614 615 func (index *flat) Shutdown(ctx context.Context) error { 616 // nothing to do here 617 // Shard::shutdown will take care of handling store's buckets 618 return nil 619 } 620 621 func (index *flat) SwitchCommitLogs(context.Context) error { 622 return nil 623 } 624 625 func (index *flat) ListFiles(ctx context.Context, basePath string) ([]string, error) { 626 // nothing to do here 627 // Shard::ListBackupFiles will take care of handling store's buckets 628 return []string{}, nil 629 } 630 631 func (i *flat) ValidateBeforeInsert(vector []float32) error { 632 return nil 633 } 634 635 func (index *flat) PostStartup() { 636 if !index.isBQCached() { 637 return 638 } 639 cursor := index.store.Bucket(index.getCompressedBucketName()).Cursor() 640 defer cursor.Close() 641 642 for key, v := cursor.First(); key != nil; key, v = cursor.Next() { 643 id := binary.BigEndian.Uint64(key) 644 index.bqCache.Grow(id) 645 index.bqCache.Preload(id, uint64SliceFromByteSlice(v, make([]uint64, len(v)/8))) 646 } 647 } 648 649 func (index *flat) Dump(labels ...string) { 650 if len(labels) > 0 { 651 fmt.Printf("--------------------------------------------------\n") 652 fmt.Printf("-- %s\n", strings.Join(labels, ", ")) 653 } 654 fmt.Printf("--------------------------------------------------\n") 655 fmt.Printf("ID: %s\n", index.id) 656 fmt.Printf("--------------------------------------------------\n") 657 } 658 659 func (index *flat) DistanceBetweenVectors(x, y []float32) (float32, bool, error) { 660 return index.distancerProvider.SingleDist(x, y) 661 } 662 663 func (index *flat) ContainsNode(id uint64) bool { 664 return true 665 } 666 667 func (index *flat) DistancerProvider() distancer.Provider { 668 return index.distancerProvider 669 } 670 671 func newSearchByDistParams(maxLimit int64) *common.SearchByDistParams { 672 initialOffset := 0 673 initialLimit := common.DefaultSearchByDistInitialLimit 674 675 return common.NewSearchByDistParams(initialOffset, initialLimit, initialOffset+initialLimit, maxLimit) 676 } 677 678 type immutableParameter struct { 679 accessor func(c flatent.UserConfig) interface{} 680 name string 681 } 682 683 func validateImmutableField(u immutableParameter, 684 previous, next flatent.UserConfig, 685 ) error { 686 oldField := u.accessor(previous) 687 newField := u.accessor(next) 688 if oldField != newField { 689 return errors.Errorf("%s is immutable: attempted change from \"%v\" to \"%v\"", 690 u.name, oldField, newField) 691 } 692 693 return nil 694 } 695 696 func ValidateUserConfigUpdate(initial, updated schema.VectorIndexConfig) error { 697 initialParsed, ok := initial.(flatent.UserConfig) 698 if !ok { 699 return errors.Errorf("initial is not UserConfig, but %T", initial) 700 } 701 702 updatedParsed, ok := updated.(flatent.UserConfig) 703 if !ok { 704 return errors.Errorf("updated is not UserConfig, but %T", updated) 705 } 706 707 immutableFields := []immutableParameter{ 708 { 709 name: "distance", 710 accessor: func(c flatent.UserConfig) interface{} { return c.Distance }, 711 }, 712 { 713 name: "bq.cache", 714 accessor: func(c flatent.UserConfig) interface{} { return c.BQ.Cache }, 715 }, 716 { 717 name: "pq.cache", 718 accessor: func(c flatent.UserConfig) interface{} { return c.PQ.Cache }, 719 }, 720 { 721 name: "pq", 722 accessor: func(c flatent.UserConfig) interface{} { return c.PQ.Enabled }, 723 }, 724 { 725 name: "bq", 726 accessor: func(c flatent.UserConfig) interface{} { return c.BQ.Enabled }, 727 }, 728 } 729 730 for _, u := range immutableFields { 731 if err := validateImmutableField(u, initialParsed, updatedParsed); err != nil { 732 return err 733 } 734 } 735 return nil 736 }