Files
2026-10-01 12:46:29 -07:00

562 lines
14 KiB
Go

package storage
import (
"cmp"
"errors"
"fmt"
"log"
"math/bits"
"simd"
"simd/archsimd"
"slices"
"sort"
"sync"
ch "gitea.narnian.us/lordwelch/comic-hasher"
"gitea.narnian.us/lordwelch/goimagehash"
)
type RuntimeHash struct {
Hash uint64
ID *ch.ID
}
type basicMapStorage struct {
hashMutex *sync.RWMutex
ids IDMap
aHashes Table
dHashes Table
pHashes Table
}
type Table struct {
IDs []ch.ID
Hashes []uint64
}
func (t *Table) Len() int {
if len(t.IDs) != len(t.Hashes) {
panic(fmt.Sprintf("Hashes and Indexes got out of sync: hashes: %d, indexes: %d", len(t.IDs), len(t.Hashes)))
}
return len(t.IDs)
}
func (t *Table) Less(i, j int) bool {
return cmp.Or(
cmp.Compare(t.Hashes[i], t.Hashes[j]),
t.IDs[i].Compare(t.IDs[j]),
) < 0
}
func (t *Table) Swap(i, j int) {
t.IDs[j], t.IDs[i] = t.IDs[i], t.IDs[j]
t.Hashes[j], t.Hashes[i] = t.Hashes[i], t.Hashes[j]
}
type IDs struct {
id ch.ID
idList *[]*ch.ID
}
type IDMap struct {
ids []IDs
}
func (m *IDMap) InsertID(id ch.ID) *ch.ID {
index, found := m.FindID(&id)
if !found {
m.ids = slices.Insert(m.ids, index, IDs{
id,
&[]*ch.ID{},
})
}
return &m.ids[index].id
}
func (m *IDMap) insertID(id ch.ID, idList *[]*ch.ID) *ch.ID {
index, found := m.FindID(&id)
if !found {
m.ids = slices.Insert(m.ids, index, IDs{
id,
idList,
})
}
return &m.ids[index].id
}
func (m *IDMap) sort() {
slices.SortFunc(m.ids, func(a, b IDs) int {
return a.id.Compare(b.id)
})
}
func (m *IDMap) FindID(id *ch.ID) (int, bool) {
return slices.BinarySearchFunc(m.ids, id, func(id IDs, target *ch.ID) int {
return id.id.Compare(*target)
})
}
func (m *IDMap) GetIDs(id *ch.ID) []ch.ID {
index, found := m.FindID(id)
if !found {
return nil
}
ids := make([]ch.ID, 0, len(*m.ids[index].idList))
ids = append(ids, m.ids[index].id)
for _, id := range *m.ids[index].idList {
ids = append(ids, *id)
}
return ids
}
func (m *IDMap) GetIDsAt(index int) []ch.ID {
ids := make([]ch.ID, 0, len(*m.ids[index].idList))
for _, id := range *m.ids[index].idList {
ids = append(ids, *id)
}
return ids
}
func (m *IDMap) AssociateIDs(newids []ch.NewIDs) error {
for _, newid := range newids {
index, found := m.FindID(&newid.OldID)
if !found {
return ErrIDNotFound
}
*m.ids[index].idList = ch.InsertIDp(*m.ids[index].idList, &newid.NewID)
m.insertID(newid.NewID, m.ids[index].idList)
}
return nil
}
var ErrIDNotFound = errors.New("ID not found on this server")
func (b *basicMapStorage) RLock() {
b.hashMutex.RLock()
}
func (b *basicMapStorage) RUnlock() {
b.hashMutex.RUnlock()
}
var distShared []byte
const chunkSize = 64
var work = make([]bool, chunkSize)
// atleast must have a read lock before using
func (b *basicMapStorage) atleast(kind goimagehash.Kind, maxDistance int, searchHash uint64) []ch.Result {
currentTable := b.getCurrentTable(kind)
foundHashes := make([]struct{i , d int;h uint64}, 0, 30)
i := 0
var values archsimd.Uint64x2
r := make([]uint64, simd.VectorBitSize()/64)
hashLen := currentTable.Len()
if len(distShared) < hashLen {
// this reuse is only for testing this would cause issues if two requests happened at the same time
distShared = make([]byte, hashLen)
}
dist := distShared[:hashLen]
distance := 9
searchSimd := archsimd.BroadcastUint64x2(searchHash)
vectorSize := simd.VectorBitSize() / 64
if vectorSize >= 8 {
var zz int
for ; i+vectorSize < hashLen; i += vectorSize {
values = archsimd.LoadUint64x2(currentTable.Hashes[i : i+vectorSize])
values.Xor(searchSimd).Store(r)
for zz = range r {
dist[i+zz] = byte(bits.OnesCount64(r[zz])) // Max Ones Count is 64
}
}
}
// x := 0
// for ; i+chunkSize < hashLen; i += chunkSize {
// for x = range chunkSize { // currentTable.Hashes[i:i+chunkSize] {
// // Max Ones Count is 64
// distance = bits.OnesCount64(searchHash^currentTable.Hashes[i+x])
// if distance <= maxDistance {
// foundHashes = append(foundHashes, struct{i , d int;h uint64}{i:i+x, d:distance, h: currentTable.Hashes[i+x]})
// }
// }
// }
for ; i < hashLen; i++ {
distance = bits.OnesCount64(searchHash ^ currentTable.Hashes[i])
if distance <= maxDistance {
foundHashes = append(foundHashes, struct{i , d int;h uint64}{i:i, d:distance, h: currentTable.Hashes[i]})
}
}
matchingHashes := make([]ch.Result, 0, len(foundHashes))
var f struct{i , d int;h uint64}
for _, f = range foundHashes {
matchingHashes = append(matchingHashes, ch.Result{
Hash: ch.Hash{Hash: f.h, Kind: kind},
ID: currentTable.IDs[f.i],
Distance: f.d,
EquivalentIDs: b.ids.GetIDs(&currentTable.IDs[f.i]),
})
}
return matchingHashes
}
func (b *basicMapStorage) exactMatches(hashes []ch.Hash) []ch.Result {
var foundMatches []ch.Result
for _, hash := range hashes {
mappedIds := map[int]bool{}
index, count := b.findHash(hash)
if count < 1 {
continue
}
var (
storedHash uint64
storedID ch.ID
i int
currentTable = b.getCurrentTable(hash.Kind)
)
for i, storedHash = range currentTable.Hashes[index : index+count] {
storedID = currentTable.IDs[index+i]
index, _ := b.ids.FindID(&storedID)
if mappedIds[index] {
continue
}
mappedIds[index] = true
foundMatches = append(foundMatches, ch.Result{
Hash: ch.Hash{Hash: storedHash, Kind: hash.Kind},
ID: storedID,
Distance: 0,
EquivalentIDs: b.ids.GetIDsAt(index),
})
}
}
return foundMatches
}
var tl *ch.TimeLog = ch.NewTimeLog()
func (b *basicMapStorage) GetMatches(hashes []ch.Hash, max int, exactOnly bool) ([]ch.Result, error) {
var (
foundMatches []ch.Result
)
tl.ResetTime()
defer tl.ResetTime()
defer tl.LogTime(fmt.Sprintf("Search Complete: max: %v ExactOnly: %v", max, exactOnly))
b.hashMutex.RLock()
defer b.hashMutex.RUnlock()
if exactOnly { // exact matches are also found by partial matches. Don't bother with exact matches so we don't have to de-duplicate
foundMatches = b.exactMatches(hashes)
tl.LogTime("Search Exact")
if len(foundMatches) > 0 {
return foundMatches, nil
}
}
if max == 0 {
return foundMatches, nil
}
totalPartialHashes := 0
for _, hash := range hashes {
totalPartialHashes += b.getCurrentTable(hash.Kind).Len()
foundMatches = append(foundMatches, b.atleast(hash.Kind, max, hash.Hash)...)
}
return foundMatches, nil
}
// getCurrentHashes must have a read lock before using
func (b *basicMapStorage) getCurrentTable(kind goimagehash.Kind) *Table {
if kind == goimagehash.AHash {
return &b.aHashes
}
if kind == goimagehash.DHash {
return &b.dHashes
}
if kind == goimagehash.PHash {
return &b.pHashes
}
panic("Unknown hash type: " + kind.String())
}
// findHash looks for exact matches
// must have a read lock before using
// return value is index, count
// if count < 1 then no results were found
func (b *basicMapStorage) findHash(hash ch.Hash) (int, int) {
currentTable := *b.getCurrentTable(hash.Kind)
index, found := slices.BinarySearchFunc(currentTable.Hashes, hash, func(existing uint64, target ch.Hash) int {
return cmp.Compare(existing, target.Hash)
})
if !found {
return index, 0
}
count := 1 // Skip the index we already found because we know it's equal
for i := index + 1; i < len(currentTable.Hashes) && currentTable.Hashes[i] == hash.Hash; i++ {
count++
}
return index, count
}
// insertHash must already have a lock
func (b *basicMapStorage) insertHash(hash ch.Hash, id ch.ID) bool {
currentTable := b.getCurrentTable(hash.Kind)
start, count := b.findHash(hash)
max := start + count
index := start
for ; index < max; index++ {
if currentTable.IDs[index] == id {
return true
}
}
rh := RuntimeHash{
Hash: hash.Hash,
ID: b.ids.InsertID(id),
}
currentTable.Hashes = slices.Insert(currentTable.Hashes, index, sh.Hash.Hash)
currentTable.IDs = slices.Insert(currentTable.IDs, index, sh.ID)
b.ids.InsertID(sh.ID)
// This slice only contains items that have the same hash so they must be unique and can be sorted
slices.SortFunc(currentTable.IDs[start:max+1], idCmp)
return false
}
func (b *basicMapStorage) MapHashes(hash ch.ImageHash) ch.Metrics {
b.hashMutex.Lock()
defer b.hashMutex.Unlock()
count := ch.Metrics{
Hashes: make(map[ch.Metric]int, 3),
}
for _, ih := range hash.Hashes {
if b.insertHash(ih, hash.ID) {
count.Hashes[ch.Metric{Source: hash.ID.Domain, Kind: ih.Kind}] += 1
}
}
return count
}
// DecodeHashes must already have a lock
func (b *basicMapStorage) DecodeHashes(hashes *ch.SavedHashes) (ch.Metrics, error) {
if hashes == nil {
return ch.Metrics{}, nil
}
b.ids.ids = make([]IDs, 0, len(hashes.Hashes))
// Initialize all the known equal IDs
for _, ids := range hashes.IDs {
newIDs := make([]*ch.ID, 0, len(ids))
for _, id := range ids {
newIDs = append(newIDs, &id)
}
for _, id := range newIDs {
b.ids.ids = append(b.ids.ids, IDs{
*id, // should this be a pointer?????
// I would have duplicate ID pointers hanging around if there was anything to load in this loop
&newIDs,
})
}
}
b.ids.sort()
// Remove duplicates
slices.SortFunc(hashes.Hashes, hashCmp)
hashCompact := func(existing ch.SavedHash, target ch.SavedHash) bool {
return hashCmp(existing, target) == 0
}
hashes.Hashes = slices.CompactFunc(hashes.Hashes, hashCompact)
// Sort for ID insertion
// There are definitely duplicate IDs and re-searching every ID isn't efficient
slices.SortFunc(hashes.Hashes, func(existing, target ch.SavedHash) int { return existing.ID.Compare(target.ID) })
// this is now sorted and compacted in the correct order we just need to separate out by hash type
aHashCount := 0
dHashCount := 0
pHashCount := 0
// Use space efficiently
for _, savedHash := range hashes.Hashes {
if savedHash.Hash.Kind == goimagehash.AHash {
aHashCount += 1
}
if savedHash.Hash.Kind == goimagehash.DHash {
dHashCount += 1
}
if savedHash.Hash.Kind == goimagehash.PHash {
pHashCount += 1
}
}
b.aHashes.IDs = make([]ch.ID, 0, aHashCount)
b.aHashes.Hashes = make([]uint64, 0, aHashCount)
b.dHashes.IDs = make([]ch.ID, 0, dHashCount)
b.dHashes.Hashes = make([]uint64, 0, dHashCount)
b.pHashes.IDs = make([]ch.ID, 0, pHashCount)
b.pHashes.Hashes = make([]uint64, 0, pHashCount)
var hash ch.SavedHash
b.ids.ids = make([]IDs, 0, len(hashes.Hashes)/3)
for _, hash = range hashes.Hashes {
// TODO: Investigate better options to load in the ID
if hash.Hash.Kind == goimagehash.AHash {
b.aHashes.IDs = append(b.aHashes.IDs, hash.ID)
b.aHashes.Hashes = append(b.aHashes.Hashes, hash.Hash.Hash)
}
if hash.Hash.Kind == goimagehash.DHash {
b.dHashes.IDs = append(b.dHashes.IDs, hash.ID)
b.dHashes.Hashes = append(b.dHashes.Hashes, hash.Hash.Hash)
}
if hash.Hash.Kind == goimagehash.PHash {
b.pHashes.IDs = append(b.pHashes.IDs, hash.ID)
b.pHashes.Hashes = append(b.pHashes.Hashes, hash.Hash.Hash)
}
if hash.ID == (ch.ID{}) {
log.Panic("Empty ID detected", hash)
}
if len(b.ids.ids) > 0 && b.ids.ids[len(b.ids.ids)-1].id.Compare(hash.ID) == 0 {
continue
}
// This still does a binary search but should always be a negative match
// Probably won't waste too much time as it should always be equal to an append and we should already have the space available
b.ids.InsertID(hash.ID)
}
// Should already be sorted but shouldn't hurt
b.ids.sort()
// Hashes are currently in the wrong order they must be sorted
sort.Sort(&b.aHashes)
sort.Sort(&b.dHashes)
sort.Sort(&b.pHashes)
var count = ch.Metrics{
Hashes: make(map[ch.Metric]int, 6),
}
var id ch.ID
for _, id = range b.aHashes.IDs {
count.Hashes[ch.Metric{Source: id.Domain, Kind: goimagehash.AHash}] += 1
}
for _, id = range b.dHashes.IDs {
count.Hashes[ch.Metric{Source: id.Domain, Kind: goimagehash.DHash}] += 1
}
for _, id = range b.pHashes.IDs {
count.Hashes[ch.Metric{Source: id.Domain, Kind: goimagehash.PHash}] += 1
}
return count, nil
}
func hashCmp(existing, target RuntimeHash) int {
return cmp.Or(
cmp.Compare(existing.Hash.Kind, target.Hash.Kind),
cmp.Compare(existing.Hash.Hash, target.Hash.Hash),
existing.ID.Compare(target.ID),
)
}
func appendHashes(kind goimagehash.Kind,rhashes []RuntimeHash, savedHashes *ch.SavedHashes) {
var rhash RuntimeHash
for _, rhash = range rhashes {
savedHashes.Hashes = append(savedHashes.Hashes, ch.SavedHash{
Hash: ch.Hash{
Hash: rhash.Hash,
Kind: kind,
},
ID: *rhash.ID,
})
}
func idCmp(existing, target ch.ID) int {
return existing.Compare(target)
}
// EncodeHashes should already have a lock
func (b *basicMapStorage) EncodeHashes() (*ch.SavedHashes, error) {
savedHashes := ch.SavedHashes{
Hashes: make([]ch.SavedHash, 0, b.aHashes.Len()+b.dHashes.Len()+b.pHashes.Len()),
}
for i := 0; i < b.aHashes.Len(); i++ {
savedHashes.Hashes = append(savedHashes.Hashes, ch.SavedHash{
Hash: ch.Hash{
Hash: b.aHashes.Hashes[i],
Kind: goimagehash.AHash,
},
ID: b.aHashes.IDs[i],
})
}
for i := 0; i < b.dHashes.Len(); i++ {
savedHashes.Hashes = append(savedHashes.Hashes, ch.SavedHash{
Hash: ch.Hash{
Hash: b.dHashes.Hashes[i],
Kind: goimagehash.DHash,
},
ID: b.dHashes.IDs[i],
})
}
for i := 0; i < b.pHashes.Len(); i++ {
savedHashes.Hashes = append(savedHashes.Hashes, ch.SavedHash{
Hash: ch.Hash{
Hash: b.pHashes.Hashes[i],
Kind: goimagehash.PHash,
},
ID: b.pHashes.IDs[i],
})
}
// Only keep groups len>1 as they are mapped in SavedHashes.Hashes
for _, ids := range b.ids.ids {
if len(*ids.idList) > 1 {
idl := make([]ch.ID, 0, len(*ids.idList))
for _, id := range *ids.idList {
idl = append(idl, *id)
}
savedHashes.IDs = append(savedHashes.IDs, idl)
}
}
return &savedHashes, nil
}
func (b *basicMapStorage) AssociateIDs(newids []ch.NewIDs) error {
b.hashMutex.RLock()
defer b.hashMutex.RUnlock()
return b.ids.AssociateIDs(newids)
}
func (b *basicMapStorage) GetIDs(id ch.ID) ch.IDList {
b.hashMutex.RLock()
defer b.hashMutex.RUnlock()
ids := b.ids.GetIDs(&id)
return ch.ToIDList(ids)
}
func NewBasicMapStorage() (ch.HashStorage, error) {
storage := &basicMapStorage{
hashMutex: &sync.RWMutex{},
ids: IDMap{
ids: []IDs{},
},
aHashes: Table{
IDs: []ch.ID{},
Hashes: []uint64{},
},
dHashes: Table{
IDs: []ch.ID{},
Hashes: []uint64{},
},
pHashes: Table{
IDs: []ch.ID{},
Hashes: []uint64{},
},
}
return storage, nil
}