Files
dolthub--dolt/go/store/prolly/proximity_mutable_map.go
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

482 lines
15 KiB
Go

// Copyright 2024 Dolthub, Inc.
//
// 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.
package prolly
import (
"context"
"fmt"
"github.com/dolthub/go-mysql-server/sql/expression/function/vector"
"github.com/dolthub/dolt/go/gen/fb/serial"
"github.com/dolthub/dolt/go/store/hash"
"github.com/dolthub/dolt/go/store/prolly/message"
"github.com/dolthub/dolt/go/store/prolly/tree"
"github.com/dolthub/dolt/go/store/skip"
"github.com/dolthub/dolt/go/store/val"
)
type ProximityMutableMap = GenericMutableMap[ProximityMap, tree.ProximityMap[val.Tuple, val.Tuple, *val.TupleDesc]]
type ProximityFlusher struct {
distanceType vector.DistanceType
logChunkSize uint8
}
var _ MutableMapFlusher[ProximityMap, tree.ProximityMap[val.Tuple, val.Tuple, *val.TupleDesc]] = ProximityFlusher{}
func (f ProximityFlusher) ApplyMutationsWithSerializer(
ctx context.Context,
serializer message.Serializer,
mutableMap *GenericMutableMap[ProximityMap, tree.ProximityMap[val.Tuple, val.Tuple, *val.TupleDesc]],
) (tree.ProximityMap[val.Tuple, val.Tuple, *val.TupleDesc], error) {
// Identify what parts of the tree need to be rebuilt:
// For each edit, identify the node closest to the root that is affected.
// Then, walk the tree creating a new one.
// In order to skip walking parts of the tree that aren't modified, we need to know when a node
// has no edits in any of its children.
// We can have a cursor that fast-forwards to the affected value.
// - How does this work with inserts?
// Do it recursively, starting with the root. Sort each edit into the affected child node (or the current node).
// If the current node if affected, rebuild.
// Otherwise visit each child node.
keyDesc := mutableMap.keyDesc
valDesc := mutableMap.valDesc
ns := mutableMap.NodeStore()
convertFunc, err := getConvertToVectorFunction(keyDesc, ns)
if err != nil {
return tree.ProximityMap[val.Tuple, val.Tuple, *val.TupleDesc]{}, err
}
edits := make([]VectorIndexKV, 0, mutableMap.tuples.Edits.Count())
editIter := mutableMap.tuples.Mutations()
mutation := editIter.NextMutation(ctx)
maxEditLevel := uint8(0)
for mutation.Key != nil {
keyLevel := tree.DeterministicHashLevel(f.logChunkSize, mutation.Key)
if keyLevel > maxEditLevel {
maxEditLevel = keyLevel
}
edits = append(edits, VectorIndexKV{
key: mutation.Key,
value: mutation.Value,
level: int(keyLevel),
})
mutation = editIter.NextMutation(ctx)
}
var newRoot *tree.Node
root := mutableMap.tuples.Static.Root
distanceType := mutableMap.tuples.Static.DistanceType
if root.Count() == 0 {
// Original index was empty. We need to make a new index based on the edits.
newRoot, err = makeNewProximityMap(ctx, ns, edits, distanceType, keyDesc, valDesc, f.logChunkSize)
} else if maxEditLevel >= uint8(root.Level()) {
// The root node has changed, or there may be a new level to the tree. We need to rebuild the tree.
newRoot, _, err = f.rebuildNode(ctx, ns, root, edits, distanceType, keyDesc, valDesc, maxEditLevel)
} else {
root, err = root.LoadSubtrees()
if err != nil {
return tree.ProximityMap[val.Tuple, val.Tuple, *val.TupleDesc]{}, err
}
newRoot, _, err = f.visitNode(ctx, serializer, ns, root, edits, convertFunc, distanceType, keyDesc, valDesc)
}
if err != nil {
return tree.ProximityMap[val.Tuple, val.Tuple, *val.TupleDesc]{}, err
}
return tree.ProximityMap[val.Tuple, val.Tuple, *val.TupleDesc]{
Root: newRoot,
NodeStore: ns,
DistanceType: distanceType,
Convert: convertFunc,
Order: keyDesc,
}, nil
}
type VectorIndexKV struct {
key, value tree.Item
level int
}
type childEditList struct {
edits []VectorIndexKV
mustRebuild bool
}
func makeNewProximityMap(
ctx context.Context,
ns tree.NodeStore,
edits []VectorIndexKV,
distanceType vector.DistanceType,
keyDesc *val.TupleDesc,
valDesc *val.TupleDesc,
logChunkSize uint8,
) (newNode *tree.Node, err error) {
proximityMapBuilder, err := NewProximityMapBuilder(ctx, ns, distanceType, keyDesc, valDesc, logChunkSize)
if err != nil {
return nil, err
}
for _, edit := range edits {
// If the original index was empty, then all edits are inserts.
if edit.key != nil {
err = proximityMapBuilder.InsertAtLevel(ctx, edit.key, edit.value, uint8(edit.level))
if err != nil {
return nil, err
}
}
}
proximityMap, err := proximityMapBuilder.Flush(ctx)
if err != nil {
return nil, err
}
return proximityMap.Node(), nil
}
// visitNode produces a new tree.Node that incorporates the provided edits to the provided node.
// As a precondition, we have confirmed that the keys in the provided node will not change, but the
// keys in children nodes might. If the keys in a child node would change, we call rebuildNode on that child.
// Otherwise, we recursively called visitNode on the children.
func (f ProximityFlusher) visitNode(
ctx context.Context,
serializer message.Serializer,
ns tree.NodeStore,
node *tree.Node,
edits []VectorIndexKV,
convert tree.ConvertToVectorFunction,
distanceType vector.DistanceType,
keyDesc *val.TupleDesc,
valDesc *val.TupleDesc,
) (newNode *tree.Node, subtrees int, err error) {
var keys [][]byte
var values [][]byte
var nodeSubtrees []uint64
if node.IsLeaf() {
keys, values, nodeSubtrees, err = f.rebuildLeafNodeWithEdits(ctx, node, edits, keyDesc)
if err != nil {
return nil, 0, err
}
} else {
// sort the list of edits based on which child node contains them.
childEdits := make(map[int]childEditList)
for _, edit := range edits {
key := edit.key
editVector, err := convert(ctx, key)
if err != nil {
return nil, 0, err
}
level := edit.level
// visit each child in the node to determine which is closest
closestIdx := 0
childKey := node.GetKey(0)
childVector, err := convert(ctx, childKey)
if err != nil {
return nil, 0, err
}
closestDistance, err := distanceType.Eval(childVector, editVector)
if err != nil {
return nil, 0, err
}
for i := 1; i < node.Count(); i++ {
childKey = node.GetKey(i)
childVector, err = convert(ctx, childKey)
if err != nil {
return nil, 0, err
}
newDistance, err := distanceType.Eval(childVector, editVector)
if err != nil {
return nil, 0, err
}
if newDistance < closestDistance {
closestDistance = newDistance
closestIdx = i
}
}
childEditList := childEdits[closestIdx]
childEditList.edits = append(childEditList.edits, edit)
if level == node.Level()-1 {
childEditList.mustRebuild = true
}
childEdits[closestIdx] = childEditList
}
// Recursively build the new tree.
// We need keys, values, subtrees, and levels.
for i := 0; i < node.Count(); i++ {
childKey := node.GetKey(i)
keys = append(keys, childKey)
childValue := node.GetValue(i)
childEditList := childEdits[i]
if len(childEditList.edits) == 0 {
// No edits affected this node, leave it as is.
values = append(values, childValue)
childSubtrees := node.GetSubtreeCount(i)
nodeSubtrees = append(nodeSubtrees, uint64(childSubtrees))
} else {
childNodeAddress := hash.New(childValue)
childNode, err := ns.Read(ctx, childNodeAddress)
if err != nil {
return nil, 0, err
}
var newChildNode *tree.Node
var childSubtrees int
if childEditList.mustRebuild {
newChildNode, childSubtrees, err = f.rebuildNode(ctx, ns, childNode, childEditList.edits, distanceType, keyDesc, valDesc, uint8(childNode.Level()))
} else {
childNode, err = childNode.LoadSubtrees()
if err != nil {
return nil, 0, err
}
newChildNode, childSubtrees, err = f.visitNode(ctx, serializer, ns, childNode, childEditList.edits, convert, distanceType, keyDesc, valDesc)
}
if err != nil {
return nil, 0, err
}
newChildAddress := newChildNode.HashOf()
values = append(values, newChildAddress[:])
nodeSubtrees = append(nodeSubtrees, uint64(childSubtrees))
}
}
}
newNode, err = serializeVectorIndexNode(ctx, serializer, ns, keys, values, nodeSubtrees, node.Level())
if err != nil {
return nil, 0, err
}
subtrees, err = newNode.TreeCount()
if err != nil {
return nil, 0, err
}
return newNode, subtrees, err
}
func serializeVectorIndexNode(
ctx context.Context,
serializer message.Serializer,
ns tree.NodeStore,
keys [][]byte,
values [][]byte,
nodeSubtrees []uint64,
level int,
) (*tree.Node, error) {
msg := serializer.Serialize(keys, values, nodeSubtrees, level)
newNode, fileId, err := tree.NodeFromBytes(msg)
if err != nil {
return nil, err
}
if fileId != serial.VectorIndexNodeFileID {
return nil, fmt.Errorf("expected file id %s, received %s", serial.VectorIndexNodeFileID, fileId)
}
_, err = ns.Write(ctx, newNode)
return newNode, err
}
// rebuildLeafNodeWithEdits creates a new leaf node by applying a list of edits to an existing node.
func (f ProximityFlusher) rebuildLeafNodeWithEdits(
ctx context.Context,
originalNode *tree.Node,
edits []VectorIndexKV,
keyDesc *val.TupleDesc,
) (keys [][]byte, values [][]byte, nodeSubtrees []uint64, err error) {
// combine edits with node keys. Use merge sort.
editIdx := 0
nodeIdx := 0
for editIdx < len(edits) || nodeIdx < originalNode.Count() {
// Edit doesn't match an existing key: it must be an insert.
if editIdx >= len(edits) {
keys = append(keys, originalNode.GetKey(nodeIdx))
values = append(values, originalNode.GetValue(nodeIdx))
nodeSubtrees = append(nodeSubtrees, 0)
nodeIdx++
continue
}
if nodeIdx >= originalNode.Count() {
keys = append(keys, edits[editIdx].key)
values = append(values, edits[editIdx].value)
nodeSubtrees = append(nodeSubtrees, 0)
editIdx++
continue
}
editKey := val.Tuple(edits[editIdx].key)
nodeKey := val.Tuple(originalNode.GetKey(nodeIdx))
cmp, cmpErr := keyDesc.Compare(ctx, editKey, nodeKey)
if cmpErr != nil {
return nil, nil, nil, cmpErr
}
if cmp < 0 {
//edit comes first
// Edit doesn't match an existing key: it must be an insert.
keys = append(keys, edits[editIdx].key)
values = append(values, edits[editIdx].value)
nodeSubtrees = append(nodeSubtrees, 0)
editIdx++
continue
}
if cmp > 0 {
// node comes first
keys = append(keys, originalNode.GetKey(nodeIdx))
values = append(values, originalNode.GetValue(nodeIdx))
nodeSubtrees = append(nodeSubtrees, 0)
nodeIdx++
continue
}
// edit to an existing key.
newValue := edits[editIdx].value
editIdx++
nodeIdx++
if newValue == nil {
// This is a delete. We simply skip to the next key, excluding this key from the new node.
continue
}
keys = append(keys, editKey)
values = append(values, newValue)
nodeSubtrees = append(nodeSubtrees, 0)
}
return
}
var DefaultLogChunkSize = uint8(8)
func (f ProximityFlusher) rebuildNode(ctx context.Context, ns tree.NodeStore, node *tree.Node, edits []VectorIndexKV, distanceType vector.DistanceType, keyDesc *val.TupleDesc, valDesc *val.TupleDesc, maxLevel uint8) (newNode *tree.Node, subtrees int, err error) {
proximityMapBuilder, err := NewProximityMapBuilder(ctx, ns, distanceType, keyDesc, valDesc, f.logChunkSize)
if err != nil {
return nil, 0, err
}
editSkipList := skip.NewSkipList(func(ctx context.Context, left, right []byte) (int, error) {
return keyDesc.Compare(ctx, left, right)
})
for _, edit := range edits {
if err := editSkipList.Put(ctx, edit.key, edit.value); err != nil {
return nil, 0, err
}
}
insertFromNode := func(nd *tree.Node, i int) error {
key := nd.GetKey(i)
value := nd.GetValue(i)
_, hasNewVal, err := editSkipList.Get(ctx, key)
if err != nil {
return err
}
if !hasNewVal {
// TODO: Is it faster if we fetch the level from the current tree?
keyLevel := tree.DeterministicHashLevel(f.logChunkSize, key)
if keyLevel > maxLevel {
keyLevel = maxLevel
}
err = proximityMapBuilder.InsertAtLevel(ctx, key, value, keyLevel)
if err != nil {
return err
}
}
return nil
}
var walk func(nd *tree.Node) error
walk = func(nd *tree.Node) (err error) {
if nd.IsLeaf() {
for i := 0; i < nd.Count(); i++ {
err = insertFromNode(nd, i)
if err != nil {
return err
}
}
} else {
for i := 0; i < nd.Count(); i++ {
childAddr := hash.New(nd.GetValue(i))
if i != 0 {
// walkLevel = nd.Level()
}
child, err := ns.Read(ctx, childAddr)
if err != nil {
return err
}
err = walk(child)
}
}
return nil
}
err = walk(node)
if err != nil {
return nil, 0, err
}
for _, edit := range edits {
key := edit.key
value := edit.value
if value != nil {
err = proximityMapBuilder.Insert(ctx, key, value)
if err != nil {
return nil, 0, err
}
}
}
newMap, err := proximityMapBuilder.Flush(ctx)
if err != nil {
return nil, 0, err
}
newRoot := newMap.tuples.Root
newTreeCount, err := newRoot.TreeCount()
if err != nil {
return nil, 0, err
}
return newRoot, newTreeCount, nil
}
func (f ProximityFlusher) GetDefaultSerializer(ctx context.Context, mutableMap *GenericMutableMap[ProximityMap, tree.ProximityMap[val.Tuple, val.Tuple, *val.TupleDesc]]) message.Serializer {
return message.NewVectorIndexSerializer(mutableMap.NodeStore().Pool(), f.logChunkSize, f.distanceType)
}
// newMutableMap returns a new MutableMap.
func newProximityMutableMap(m ProximityMap) *ProximityMutableMap {
return &ProximityMutableMap{
tuples: m.tuples.Mutate(),
keyDesc: m.keyDesc,
valDesc: m.valDesc,
maxPending: defaultMaxPending,
flusher: ProximityFlusher{logChunkSize: m.logChunkSize, distanceType: m.tuples.DistanceType},
}
}
func (f ProximityFlusher) MapInterface(ctx context.Context, mut *ProximityMutableMap) (MapInterface, error) {
return f.Map(ctx, mut)
}
// TreeMap materializes all pending and applied mutations in the MutableMap.
func (f ProximityFlusher) TreeMap(ctx context.Context, mut *ProximityMutableMap) (tree.ProximityMap[val.Tuple, val.Tuple, *val.TupleDesc], error) {
s := message.NewVectorIndexSerializer(mut.NodeStore().Pool(), f.logChunkSize, f.distanceType)
return mut.flushWithSerializer(ctx, s)
}
// TreeMap materializes all pending and applied mutations in the MutableMap.
func (f ProximityFlusher) Map(ctx context.Context, mut *ProximityMutableMap) (ProximityMap, error) {
treeMap, err := f.TreeMap(ctx, mut)
if err != nil {
return ProximityMap{}, err
}
return ProximityMap{
tuples: treeMap,
keyDesc: mut.keyDesc,
valDesc: mut.valDesc,
logChunkSize: f.logChunkSize,
}, nil
}