114 lines
4.6 KiB
Markdown
114 lines
4.6 KiB
Markdown
# Example: Find the best (lowest‑ping) peer in a DHT table
|
||
|
||
This example shows how to model a "made up" DHT routing table and use Poindexter's `KDTree` to quickly find:
|
||
|
||
- the single best peer by ping (nearest neighbor)
|
||
- the top N best peers by ping (k‑nearest neighbors)
|
||
- all peers under a ping threshold (radius search)
|
||
|
||
We keep it simple by mapping each peer to a 1‑dimensional coordinate: its ping in milliseconds. Using 1D means the KDTree's distance is just the absolute difference between pings.
|
||
|
||
> Tip: In a real system, you might expand to multiple dimensions (e.g., `[ping_ms, hop_count, geo_distance, score]`) and choose a metric (`L1`, `L2`, or `L∞`) that best matches your routing heuristic.
|
||
|
||
---
|
||
|
||
## Full example
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
poindexter "github.com/Snider/Poindexter"
|
||
)
|
||
|
||
// Peer is our DHT peer entry (made up for this example).
|
||
type Peer struct {
|
||
Addr string // multiaddr or host:port
|
||
Ping int // measured ping in milliseconds
|
||
}
|
||
|
||
func main() {
|
||
// A toy DHT routing table with made-up ping values
|
||
table := []Peer{
|
||
{Addr: "peer1.example:4001", Ping: 74},
|
||
{Addr: "peer2.example:4001", Ping: 52},
|
||
{Addr: "peer3.example:4001", Ping: 110},
|
||
{Addr: "peer4.example:4001", Ping: 35},
|
||
{Addr: "peer5.example:4001", Ping: 60},
|
||
{Addr: "peer6.example:4001", Ping: 44},
|
||
}
|
||
|
||
// Map peers to KD points in 1D where coordinate = ping (ms).
|
||
// Use stable string IDs so we can delete/update later.
|
||
pts := make([]poindexter.KDPoint[Peer], 0, len(table))
|
||
for i, p := range table {
|
||
pts = append(pts, poindexter.KDPoint[Peer]{
|
||
ID: fmt.Sprintf("peer-%d", i+1),
|
||
Coords: []float64{float64(p.Ping)},
|
||
Value: p,
|
||
})
|
||
}
|
||
|
||
// Build a KDTree. Euclidean metric is fine for 1D ping comparisons.
|
||
kdt, err := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.EuclideanDistance{}))
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
|
||
// 1) Find the best (lowest-ping) peer.
|
||
// Query is a 1D point representing desired ping target. Using 0 finds the min.
|
||
best, d, ok := kdt.Nearest([]float64{0})
|
||
if !ok {
|
||
fmt.Println("no peers found")
|
||
return
|
||
}
|
||
fmt.Printf("Best peer: %s (ping=%d ms), distance=%.0f\n", best.Value.Addr, best.Value.Ping, d)
|
||
// Example output: Best peer: peer4.example:4001 (ping=35 ms), distance=35
|
||
|
||
// 2) Top-N best peers by ping.
|
||
top, dists := kdt.KNearest([]float64{0}, 3)
|
||
fmt.Println("Top 3 peers by ping:")
|
||
for i := range top {
|
||
fmt.Printf(" #%d %s (ping=%d ms), distance=%.0f\n", i+1, top[i].Value.Addr, top[i].Value.Ping, dists[i])
|
||
}
|
||
|
||
// 3) All peers under a threshold (e.g., <= 50 ms): radius search.
|
||
within, wd := kdt.Radius([]float64{0}, 50)
|
||
fmt.Println("Peers with ping <= 50 ms:")
|
||
for i := range within {
|
||
fmt.Printf(" %s (ping=%d ms), distance=%.0f\n", within[i].Value.Addr, within[i].Value.Ping, wd[i])
|
||
}
|
||
|
||
// 4) Dynamic updates: if a peer improves ping, we can delete & re-insert with a new ID
|
||
// (or keep the same ID and just update the point if your application tracks indices).
|
||
// Here we simulate peer5 dropping from 60 ms to 30 ms.
|
||
if kdt.DeleteByID("peer-5") {
|
||
improved := poindexter.KDPoint[Peer]{
|
||
ID: "peer-5", // keep the same ID for simplicity
|
||
Coords: []float64{30},
|
||
Value: Peer{Addr: "peer5.example:4001", Ping: 30},
|
||
}
|
||
_ = kdt.Insert(improved)
|
||
}
|
||
|
||
// Recompute the best after update
|
||
best2, d2, _ := kdt.Nearest([]float64{0})
|
||
fmt.Printf("After update, best peer: %s (ping=%d ms), distance=%.0f\n", best2.Value.Addr, best2.Value.Ping, d2)
|
||
}
|
||
```
|
||
|
||
### Why does querying with `[0]` work?
|
||
We use Euclidean distance in 1D, so `distance = |ping - target|`. With target `0`, minimizing the distance is equivalent to minimizing the ping itself.
|
||
|
||
### Extending the metric/space
|
||
- Multi-objective: encode more routing features (lower is better) as extra dimensions, e.g. `[ping_ms, hops, queue_delay_ms]`.
|
||
- Metric choice:
|
||
- `EuclideanDistance` (L2): balances outliers smoothly.
|
||
- `ManhattanDistance` (L1): linear penalty; robust for sparsity.
|
||
- `ChebyshevDistance` (L∞): cares about the worst dimension.
|
||
- Normalization: when mixing units (ms, hops, km), normalize or weight dimensions so the metric reflects your priority.
|
||
|
||
### Notes
|
||
- This KDTree currently uses an internal linear scan for queries. The API is stable and designed so it can be swapped to use `gonum.org/v1/gonum/spatial/kdtree` under the hood later for sub-linear queries on large datasets.
|
||
- IDs are optional but recommended for O(1)-style deletes; keep them unique per tree.
|