Improve D&D registry normalization efficiency
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||||
@@ -58,9 +57,13 @@ func ComparisonKey(value string) string {
|
|||||||
// available, leaving validation to report the problem instead of manufacturing
|
// available, leaving validation to report the problem instead of manufacturing
|
||||||
// an ID.
|
// an ID.
|
||||||
func DeriveID(name string, refs []source.SourceRef) string {
|
func DeriveID(name string, refs []source.SourceRef) string {
|
||||||
comparisonName := ComparisonKey(name)
|
|
||||||
anchor, ok := earliestReference(refs)
|
anchor, ok := earliestReference(refs)
|
||||||
if comparisonName == "" || !ok {
|
return deriveIDFromAnchor(name, anchor, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveIDFromAnchor(name string, anchor source.SourceRef, hasAnchor bool) string {
|
||||||
|
comparisonName := ComparisonKey(name)
|
||||||
|
if comparisonName == "" || !hasAnchor {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,13 +114,14 @@ func ValidateRegistry(locations []dnd.Location) []Issue {
|
|||||||
if ComparisonKey(location.Name) == "" {
|
if ComparisonKey(location.Name) == "" {
|
||||||
issues = append(issues, Issue{Code: IssueEmptyCanonicalName, RecordIndex: recordIndex, Value: location.Name})
|
issues = append(issues, Issue{Code: IssueEmptyCanonicalName, RecordIndex: recordIndex, Value: location.Name})
|
||||||
}
|
}
|
||||||
if _, ok := earliestReference(location.SourceRefs); !ok {
|
anchor, hasAnchor := earliestReference(location.SourceRefs)
|
||||||
|
if !hasAnchor {
|
||||||
issues = append(issues, Issue{Code: IssueMissingEvidence, RecordIndex: recordIndex})
|
issues = append(issues, Issue{Code: IssueMissingEvidence, RecordIndex: recordIndex})
|
||||||
}
|
}
|
||||||
|
|
||||||
if !IsValidID(location.ID) {
|
if !IsValidID(location.ID) {
|
||||||
issues = append(issues, Issue{Code: IssueInvalidID, RecordIndex: recordIndex, Value: location.ID})
|
issues = append(issues, Issue{Code: IssueInvalidID, RecordIndex: recordIndex, Value: location.ID})
|
||||||
} else if expected := DeriveID(location.Name, location.SourceRefs); location.ID != expected {
|
} else if expected := deriveIDFromAnchor(location.Name, anchor, hasAnchor); location.ID != expected {
|
||||||
issues = append(issues, Issue{Code: IssueIDMismatch, RecordIndex: recordIndex, Value: location.ID})
|
issues = append(issues, Issue{Code: IssueIDMismatch, RecordIndex: recordIndex, Value: location.ID})
|
||||||
}
|
}
|
||||||
if location.ID != "" {
|
if location.ID != "" {
|
||||||
@@ -135,36 +139,28 @@ func ValidateRegistry(locations []dnd.Location) []Issue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func earliestReference(refs []source.SourceRef) (source.SourceRef, bool) {
|
func earliestReference(refs []source.SourceRef) (source.SourceRef, bool) {
|
||||||
canonical := canonicalReferences(refs)
|
var earliest source.SourceRef
|
||||||
if len(canonical) == 0 {
|
found := false
|
||||||
return source.SourceRef{}, false
|
for _, ref := range refs {
|
||||||
|
if !validIdentityReference(ref) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !found || referenceLess(ref, earliest) {
|
||||||
|
earliest = ref
|
||||||
|
found = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return canonical[0], true
|
return earliest, found
|
||||||
}
|
}
|
||||||
|
|
||||||
func canonicalReferences(refs []source.SourceRef) []source.SourceRef {
|
func referenceLess(left, right source.SourceRef) bool {
|
||||||
canonical := make([]source.SourceRef, 0, len(refs))
|
if left.SourceID != right.SourceID {
|
||||||
for _, ref := range refs {
|
return left.SourceID < right.SourceID
|
||||||
if validIdentityReference(ref) {
|
|
||||||
canonical = append(canonical, ref)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sort.Slice(canonical, func(left, right int) bool {
|
if left.StartUnitID != right.StartUnitID {
|
||||||
if canonical[left].SourceID != canonical[right].SourceID {
|
return left.StartUnitID < right.StartUnitID
|
||||||
return canonical[left].SourceID < canonical[right].SourceID
|
|
||||||
}
|
|
||||||
if canonical[left].StartUnitID != canonical[right].StartUnitID {
|
|
||||||
return canonical[left].StartUnitID < canonical[right].StartUnitID
|
|
||||||
}
|
|
||||||
return canonical[left].EndUnitID < canonical[right].EndUnitID
|
|
||||||
})
|
|
||||||
unique := canonical[:0]
|
|
||||||
for _, ref := range canonical {
|
|
||||||
if len(unique) == 0 || unique[len(unique)-1] != ref {
|
|
||||||
unique = append(unique, ref)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return unique
|
return left.EndUnitID < right.EndUnitID
|
||||||
}
|
}
|
||||||
|
|
||||||
func validIdentityReference(ref source.SourceRef) bool {
|
func validIdentityReference(ref source.SourceRef) bool {
|
||||||
|
|||||||
@@ -241,20 +241,15 @@ func normalizeRecord(input dnd.Item, order shared.SourceRefOrder) (dnd.Item, boo
|
|||||||
|
|
||||||
func comparisonNameGroups(records []normalizedRecord) [][]int {
|
func comparisonNameGroups(records []normalizedRecord) [][]int {
|
||||||
groups := make([][]int, 0, len(records))
|
groups := make([][]int, 0, len(records))
|
||||||
|
groupPositions := make(map[string]int, len(records))
|
||||||
for index, record := range records {
|
for index, record := range records {
|
||||||
key := identity.ComparisonKey(record.item.Name)
|
key := identity.ComparisonKey(record.item.Name)
|
||||||
found := false
|
if groupIndex, found := groupPositions[key]; found {
|
||||||
for groupIndex, members := range groups {
|
groups[groupIndex] = append(groups[groupIndex], index)
|
||||||
first := records[members[0]]
|
continue
|
||||||
if identity.ComparisonKey(first.item.Name) == key {
|
|
||||||
groups[groupIndex] = append(groups[groupIndex], index)
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
groups = append(groups, []int{index})
|
|
||||||
}
|
}
|
||||||
|
groupPositions[key] = len(groups)
|
||||||
|
groups = append(groups, []int{index})
|
||||||
}
|
}
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,20 @@ func TestNormalizeConsolidatesEqualNamesAcrossEvidenceWithoutMutation(t *testing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkComparisonNameGroupsManyDistinct(b *testing.B) {
|
||||||
|
records := make([]normalizedRecord, 1_000)
|
||||||
|
for index := range records {
|
||||||
|
records[index] = normalizedRecord{item: dnd.Item{Name: "Item " + strconv.Itoa(index)}}
|
||||||
|
}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for iteration := 0; iteration < b.N; iteration++ {
|
||||||
|
if groups := comparisonNameGroups(records); len(groups) != len(records) {
|
||||||
|
b.Fatalf("group count = %d, want %d", len(groups), len(records))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeAppliesSafeAliasProposal(t *testing.T) {
|
func TestNormalizeAppliesSafeAliasProposal(t *testing.T) {
|
||||||
doc := semanticDocument()
|
doc := semanticDocument()
|
||||||
input := dnd.ItemRegistry{Items: []dnd.Item{
|
input := dnd.ItemRegistry{Items: []dnd.Item{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package locationregistry
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -228,24 +229,44 @@ func normalizeRecord(input dnd.Location, order shared.SourceRefOrder) (dnd.Locat
|
|||||||
|
|
||||||
func exactDuplicateGroups(records []normalizedRecord) [][]int {
|
func exactDuplicateGroups(records []normalizedRecord) [][]int {
|
||||||
groups := make([][]int, 0, len(records))
|
groups := make([][]int, 0, len(records))
|
||||||
|
groupPositions := make(map[string][]int, len(records))
|
||||||
for index, record := range records {
|
for index, record := range records {
|
||||||
key := identity.ComparisonKey(record.location.Name)
|
key := exactDuplicateKey(record.location)
|
||||||
found := false
|
comparisonName := identity.ComparisonKey(record.location.Name)
|
||||||
for groupIndex, members := range groups {
|
matched := false
|
||||||
first := records[members[0]]
|
for _, groupIndex := range groupPositions[key] {
|
||||||
if identity.ComparisonKey(first.location.Name) == key && reflect.DeepEqual(first.location.SourceRefs, record.location.SourceRefs) {
|
first := records[groups[groupIndex][0]]
|
||||||
|
if identity.ComparisonKey(first.location.Name) == comparisonName && reflect.DeepEqual(first.location.SourceRefs, record.location.SourceRefs) {
|
||||||
groups[groupIndex] = append(groups[groupIndex], index)
|
groups[groupIndex] = append(groups[groupIndex], index)
|
||||||
found = true
|
matched = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if matched {
|
||||||
groups = append(groups, []int{index})
|
continue
|
||||||
}
|
}
|
||||||
|
groupPositions[key] = append(groupPositions[key], len(groups))
|
||||||
|
groups = append(groups, []int{index})
|
||||||
}
|
}
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type duplicateKey struct {
|
||||||
|
ComparisonName string `json:"comparison_name"`
|
||||||
|
SourceRefs []source.SourceRef `json:"source_refs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func exactDuplicateKey(location dnd.Location) string {
|
||||||
|
encoded, err := json.Marshal(duplicateKey{
|
||||||
|
ComparisonName: identity.ComparisonKey(location.Name),
|
||||||
|
SourceRefs: location.SourceRefs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic("encode location duplicate key")
|
||||||
|
}
|
||||||
|
return string(encoded)
|
||||||
|
}
|
||||||
|
|
||||||
func cloneLocation(input dnd.Location) dnd.Location {
|
func cloneLocation(input dnd.Location) dnd.Location {
|
||||||
input.SourceRefs = cloneSourceRefs(input.SourceRefs)
|
input.SourceRefs = cloneSourceRefs(input.SourceRefs)
|
||||||
return input
|
return input
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -60,6 +61,23 @@ func TestNormalizePreparesOnlyExactDuplicatesAndRetainsSameNameAndNestedPlaces(t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkExactDuplicateGroupsManyDistinct(b *testing.B) {
|
||||||
|
records := make([]normalizedRecord, 1_000)
|
||||||
|
for index := range records {
|
||||||
|
records[index] = normalizedRecord{location: dnd.Location{
|
||||||
|
Name: "Location " + strconv.Itoa(index),
|
||||||
|
SourceRefs: []source.SourceRef{{SourceID: "session", StartUnitID: index + 1, EndUnitID: index + 1}},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for iteration := 0; iteration < b.N; iteration++ {
|
||||||
|
if groups := exactDuplicateGroups(records); len(groups) != len(records) {
|
||||||
|
b.Fatalf("group count = %d, want %d", len(groups), len(records))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizeAppliesSafeAliasGroupAndUsesContextualInputs(t *testing.T) {
|
func TestNormalizeAppliesSafeAliasGroupAndUsesContextualInputs(t *testing.T) {
|
||||||
client := &recordingLocationNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`}
|
client := &recordingLocationNormalizerClient{response: `{"duplicate_groups":[{"members":["candidate-000001","candidate-000002"],"canonical":"candidate-000002"}]}`}
|
||||||
doc := semanticDocument()
|
doc := semanticDocument()
|
||||||
|
|||||||
Reference in New Issue
Block a user