hash

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 14, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package hash provides a sharded hash index from arbitrary comparable property values to the set of NodeIDs that carry them, represented as a 64-bit Roaring bitmap.

The structure answers exact-match property predicates (for example "every node where email == 'x@y.com'") in O(1) average time. For range predicates use the B+ tree index in package github.com/FlavioCFOliveira/GoGraph/graph/index/btree (Sprint 2, T19).

Index is safe for concurrent use by any number of goroutines; the shard sharding aligns with graph.NodeID's low-bit shard scheme.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Binding added in v0.3.0

type Binding[V comparable] struct {
	// PropertyID is the interned property-key identifier this index
	// covers. Property changes whose Change.Property differs are
	// ignored.
	PropertyID uint32

	// LabelID is the interned label identifier this index is scoped
	// to. Label changes whose Change.Label differs are ignored. Note
	// that interned IDs start at zero, so this field alone cannot mark
	// an unscoped binding; bindings are always label-scoped.
	LabelID uint32

	// Label and Property are the source names behind PropertyID and
	// LabelID. They let a query planner match the index against a
	// (label, property) predicate without access to the registries.
	Label, Property string

	// Project converts a Change.OldValue / Change.NewValue payload to
	// the index key type. ok is false when the payload is absent or
	// not indexable (wrong kind), in which case the event is skipped
	// for that direction.
	Project func(v any) (V, bool)

	// Eligible reports whether the node should currently be present in
	// the index: it must be live (not deleted) and carry the bound
	// label, evaluated against the graph's final state.
	Eligible func(node graph.NodeID) bool

	// CurrentValue returns the node's current value for the bound
	// property, projected to the key type. ok is false when the node
	// is not live, lacks the property, or the value is not indexable.
	// It is consulted on label add/remove events, which carry no
	// property payload.
	CurrentValue func(node graph.NodeID) (V, bool)
}

Binding ties an Index to a single (label, property) pair of a live node graph. A bound index (see NewBound) maintains itself from the index.Manager change fan-out: property writes insert/delete typed keys, and label add/remove events attach/detach a node's current value. An unbound index (see New) ignores the fan-out entirely and is maintained by explicit Index.Insert / Index.Delete calls.

The identifier fields carry interned IDs from the owning graph's registries; the callbacks close over the graph so this package stays free of a dependency on any concrete graph implementation. Because changes are fanned out at commit time — after the transaction's mutations were applied eagerly to the graph — the callbacks observe the transaction's FINAL state, which is exactly the state the index must converge to.

type Index

type Index[V comparable] struct {
	// contains filtered or unexported fields
}

Index maps property values of type V to the NodeIDs that carry them.

Example

ExampleIndex shows a hash index answering an exact-match property predicate: insert (value, NodeID) pairs keyed by a string property, then read back the NodeID set carrying one exact value.

package main

import (
	"fmt"

	"github.com/FlavioCFOliveira/GoGraph/graph"
	"github.com/FlavioCFOliveira/GoGraph/graph/index/hash"
)

func main() {
	// An index over a string "email domain" property.
	idx := hash.New[string]()
	idx.Insert("example.com", graph.NodeID(1))
	idx.Insert("example.org", graph.NodeID(2))
	idx.Insert("example.com", graph.NodeID(3))

	// Lookup answers "every node where domain == example.com".
	bm := idx.Lookup("example.com")
	fmt.Println("example.com nodes:", bm.ToArray())
	fmt.Println("example.com cardinality:", idx.Cardinality("example.com"))
	fmt.Println("distinct domains:", idx.DistinctValues())
}
Output:
example.com nodes: [1 3]
example.com cardinality: 2
distinct domains: 2

func New

func New[V comparable]() *Index[V]

New returns an empty hash index.

func NewBound added in v0.3.0

func NewBound[V comparable](b Binding[V]) (*Index[V], error)

NewBound returns an empty hash index bound to b. Unlike New, the returned index has a functional Index.Apply: it subscribes to the node property and label changes selected by b and keeps itself consistent with the graph. Returns an error when b is missing its Label, Property, or any of the three callbacks.

func (*Index[V]) Apply

func (i *Index[V]) Apply(c index.Change)

Apply maintains a bound index (see NewBound) from the index.Manager change fan-out; it is a no-op for an unbound index (see New), which cannot reliably interpret arbitrary index.Change values without the caller-supplied binding (property key + value-type coercion).

For a bound index the rules are, per change:

  • SetNodeProperty on the bound property: the old value (when present and projectable) is deleted unconditionally, and the new value is inserted when the node is eligible in the graph's final state. The unconditional old-value delete is what clears a stale entry even when a label removal in the same batch is replayed before the property change.
  • DelNodeProperty on the bound property: the old value is deleted.
  • Add/RemoveNodeLabel on the bound label: the node's CURRENT property value is inserted / deleted. Because changes are applied at commit time the current value is the transaction's final value, so an interleaved property change in the same batch converges to the same final state regardless of replay order.

Apply is idempotent (bitmap add/remove) and safe for concurrent use with readers; writers are serialised upstream by the engine's single-writer transaction contract. Edge changes and changes for other properties/labels are ignored.

On recovery from a corrupted snapshot, the index is left empty; callers re-populate via Index.Insert from the live LPG.

func (*Index[V]) BoundNode added in v0.3.0

func (i *Index[V]) BoundNode() (label, property string, ok bool)

BoundNode returns the (label, property) pair this index is bound to, with ok reporting whether the index is bound at all. Query planners use it to decide whether the index may serve a predicate: a bound index covers exactly its (label, property) pair, while an unbound index carries no coverage metadata.

func (*Index[V]) Cardinality

func (i *Index[V]) Cardinality(value V) uint64

Cardinality returns the number of NodeIDs associated with value. It is exposed for query planners to choose between index lookup and full-scan plans.

func (*Index[V]) Contains

func (i *Index[V]) Contains(value V, node graph.NodeID) bool

Contains reports whether node is in the set associated with value. Faster than Lookup when only existence matters.

Example

ExampleIndex_Contains shows the point-membership query: Contains reports whether one specific NodeID carries a given value, without materialising the whole NodeID set.

package main

import (
	"fmt"

	"github.com/FlavioCFOliveira/GoGraph/graph"
	"github.com/FlavioCFOliveira/GoGraph/graph/index/hash"
)

func main() {
	idx := hash.New[int]()
	idx.Insert(404, graph.NodeID(7))

	fmt.Println("node 7 has 404:", idx.Contains(404, graph.NodeID(7)))
	fmt.Println("node 8 has 404:", idx.Contains(404, graph.NodeID(8)))

	// Delete removes one membership; the value disappears once its last
	// NodeID is gone.
	idx.Delete(404, graph.NodeID(7))
	fmt.Println("node 7 has 404 after delete:", idx.Contains(404, graph.NodeID(7)))
}
Output:
node 7 has 404: true
node 8 has 404: false
node 7 has 404 after delete: false

func (*Index[V]) Delete

func (i *Index[V]) Delete(value V, node graph.NodeID)

Delete removes node from the set associated with value. No-op if absent or if value is a NaN (see Index.Insert for the rationale).

func (*Index[V]) Deserialize

func (i *Index[V]) Deserialize(r io.Reader) error

Deserialize replaces the receiver's state with the contents of r. Returns index.ErrIndexCorrupted on structural or CRC errors and index.ErrIndexValueTypeUnsupported when V cannot be decoded.

func (*Index[V]) DistinctValues

func (i *Index[V]) DistinctValues() uint64

DistinctValues returns the number of distinct values currently indexed. Exposed for cardinality estimation by the query planner.

func (*Index[V]) Insert

func (i *Index[V]) Insert(value V, node graph.NodeID)

Insert records that node carries the given value. Insert is a no-op when value is a float32 or float64 NaN: Go map equality is language- fixed (NaN != NaN), so a NaN map key can never be looked up or deleted; skipping it prevents unbounded accumulation (task #1408).

func (*Index[V]) Kind

func (*Index[V]) Kind() string

Kind returns "hash" — satisfies index.Subscriber.

func (*Index[V]) Lookup

func (i *Index[V]) Lookup(value V) *roaring64.Bitmap

Lookup returns a clone of the Roaring bitmap of NodeIDs that carry the given value, or an empty bitmap when the value is unknown or is a NaN (see Index.Insert for the rationale). Clone avoids returning the live bitmap to the caller, which could otherwise be mutated by concurrent writers.

func (*Index[V]) Serialize

func (i *Index[V]) Serialize(w io.Writer) error

Serialize writes every (value, NodeID-set) pair currently in the index to w in the format documented in docs/persistence.md:

uint32 magic ('SHSH')
uint32 formatVersion
uint64 entryCount
repeat entryCount times:
  uint32 valueLen
  [valueLen]byte value (kind-specific encoding)
  uint64 idCount
  [idCount]uint64 NodeIDs (sorted ascending)
uint32 crc32c (little-endian, covers every byte above)

Returns index.ErrIndexValueTypeUnsupported when V is not one of the documented supported types.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL