meguri

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 2, 2026 License: MIT Imports: 3 Imported by: 0

README

meguri

meguri (巡, to make the rounds and revisit on a cycle) is a distributed web-crawler frontier and rescheduler written in Go.

It is the decision layer of a crawl stack. It turns an endless stream of discovered links into a politely ordered, freshness-aware crawl schedule for a frontier that scales to a hundred billion URLs across a fleet of shared-nothing partitions. It sits between a fetcher (it dispatches work to one and consumes the outcomes) and a store and ranker (it hands them crawled pages and importance signals). It never opens a socket itself.

meguri has two embodiments: the engine that runs the crawl, and the .meguri file that the engine checkpoints to, redistributes by, and archives as. A partition owns a range of hosts and serializes to exactly one .meguri file: a self-describing, columnar, checksummed container of the per-URL and per-host crawl state.

What it does

  • Absorbs discoveries and dedups them. A stream of links from sitemaps and fetched pages folds into the frontier; a URL already known is dropped by an approximate seen-set, so the queue holds work, not duplicates.
  • Schedules politely. A URL's 128-bit key carries its host in the high half, so a host's URLs share a partition and a politeness bucket. At most one fetch per host and per IP is ever in flight, spaced by a crawl delay.
  • Keeps the crawl fresh. Every fetch outcome updates a per-URL change-rate estimate, so a page that changes hourly comes back often and one that never changes drifts to the back.
  • One file is a whole partition. The frontier serializes to a deterministic, columnar .meguri file behind a CRC-checked header and a tail footer, so a fleet rebalances by moving files and a tool reads a file's shape in two reads.
  • Pure Go, one binary. No cgo, no external queue, no database server. CGO_ENABLED=0 builds for every platform, and CI proves it.

Quick start

# Build a frontier from a Common Crawl URL list, then drain it.
ccrawl search '*.example.com/*' --limit 50000 -o jsonl \
  | meguri seed -o frontier.meguri
meguri run -i frontier.meguri -o crawled.meguri

# Read what a partition holds without decoding a column.
meguri inspect crawled.meguri

# See what is due to be crawled next.
meguri schedule --data crawled.meguri --limit 20

The full command surface (seed, run, serve, inspect, schedule, stats, map, pack, compact, bench) is documented in the CLI reference.

The .meguri file

A .meguri file is bracketed by the magic MEG1 at both ends. It opens with a fixed 64-byte header and closes with a footer the reader finds from the tail, so a tool learns a file's shape in two small reads regardless of its size. Between them sit five regions: the URL table, the host table, the schedule index, the seen-set filter, and a shared string arena, each column framed into checksummed pages. Every checksum is CRC32C by default.

Encoding is deterministic: the same partition value always produces the same bytes, which is what makes a checkpoint diffable and a redistribution verifiable.

Build

make build      # builds bin/meguri, CGO disabled
make test       # full suite with the race detector

The build is pure Go with CGO_ENABLED=0. There is no cgo anywhere in the tree.

Install

Release archives, Linux packages (deb, rpm, apk), a container image on GHCR, and Homebrew and Scoop entries are produced by a single tag push. See the documentation for the install matrix.

License

MIT. See LICENSE.

Documentation

Overview

Package meguri is a distributed web-crawler frontier and rescheduler.

meguri (巡, to make the rounds and revisit on a cycle) is the decision layer of a crawl stack: it turns an endless stream of discovered links into a politely ordered, freshness-aware crawl schedule for a frontier that scales to a hundred billion URLs across a fleet of shared-nothing partitions.

This top-level package holds the data model that every other package shares and that the .meguri file serializes straight: the URLKey and HostKey, the per-URL and per-host durable records, and the discovery and outcome messages. The container itself lives in package format, the in-partition frontier in package frontier, and so on per the package layout in the spec.

The design of record is Spec 2071. The pinned canon: the file extension is .meguri, the magic is MEG1, the URLKey is 128 bits (HostKey high, PathKey low), a partition owns a HostKey range and serializes to one .meguri file, and the build is pure Go with CGO disabled.

Index

Constants

View Source
const (
	HostFlagTrapSuspect uint16 = 1 << iota
	HostFlagRobotsMissing
	HostFlagDeadHost
	HostFlagGroupingOverride
)

Host flag bits, stored in HostRecord.Flags.

Variables

View Source
var (
	Version = "dev"
	Commit  = "none"
	Date    = "unknown"
)

Build stamps, set by the linker at release time (see the Makefile and the GoReleaser config). They default to dev values for a plain `go build`.

Functions

func HostKeyOf

func HostKeyOf(grouping string) uint64

HostKeyOf hashes a host grouping string (a registrable domain or a full host, per the host's HostGrouping) into the 64-bit HostKey. The hash is xxHash64 with seed 0, the fleet convention, so the same grouping always maps to the same partition.

The caller is responsible for producing the canonical grouping string; canonicalization (the CanonPolicy and Public Suffix List rules of doc 03) lands with the seen-set in M2. Until then this is the stable derivation the format and the router agree on.

func PathKeyOf

func PathKeyOf(pathAndQuery string) uint64

PathKeyOf hashes the path-and-query remainder of a canonical URL into the 64-bit PathKey. The seed differs from the host seed so the two halves are independent.

Types

type AnchorHint

type AnchorHint uint8

AnchorHint is a coarse anchor-text quality bucket carried on a discovery so the prioritizer and the spam guards read it without the full anchor string.

const (
	AnchorUnknown     AnchorHint = iota // not computed
	AnchorEmpty                         // no anchor text
	AnchorGeneric                       // "click here", "read more"
	AnchorDescriptive                   // real descriptive text
	AnchorSpammy                        // matches a spam-anchor pattern
)

type Discovery

type Discovery struct {
	URLKey URLKey // identity and routing key

	CanonicalURL string // the canonical URL text, inline (crosses partitions)

	Depth           uint16          // source depth + 1, candidate depth
	DiscoverySource DiscoverySource // seed | link | sitemap | redirect | manual

	SrcHostKey uint64     // HostKey of the page the link was found on
	LinkWeight float32    // OPIC cash this link carries from the source
	AnchorHint AnchorHint // coarse anchor-text quality bucket

	ObservedAt uint32 // epoch-hours, when discovered
}

Discovery is the one-way idempotent message a partition sends when it finds a link for a host it does not own (D16). It is not durable. Idempotency comes from the receiver's seen-set, not from any field here: the same discovery delivered twice deduplicates to one frontier entry, so the transport may be at-least-once.

type DiscoverySource

type DiscoverySource uint8

DiscoverySource records how a URL entered the frontier.

const (
	SourceSeed     DiscoverySource = iota // from a seed list
	SourceLink                            // extracted from a crawled page
	SourceSitemap                         // listed in a sitemap
	SourceRedirect                        // a redirect target
	SourceManual                          // injected by hand
)

type HostGrouping

type HostGrouping uint8

HostGrouping is the unit a HostKey groups by: the registrable domain (the default, PSL+1) or the full host.

const (
	GroupRegistrableDomain HostGrouping = iota // default: PSL+1
	GroupFullHost                              // each fully-qualified host stands alone
)

type HostRecord

type HostRecord struct {
	HostKey uint64 // the host group identity

	HostRef        uint64       // offset into string region, the host group key
	Grouping       HostGrouping // registrable-domain or full-host
	RegistrableRef uint64       // offset into string region, the registrable domain

	ResolvedIP [16]byte // cached DNS result, IPv4-mapped into 16 bytes
	IPExpiry   uint32   // epoch-hours, DNS cache expiry

	RobotsFetched uint32 // epoch-hours, robots.txt last fetched
	RobotsExpiry  uint32 // epoch-hours, robots.txt cache expiry
	RobotsRef     uint64 // offset into blob region, parsed robots rules
	CrawlDelay    uint16 // deciseconds, effective crawl delay

	HostNextEligible uint32 // epoch-seconds, per-host token bucket
	IPNextEligible   uint32 // epoch-seconds, per-IP token bucket

	URLBudget uint32 // per-host URL cap
	URLCount  uint32 // current URLs of this host in the frontier
	DepthCap  uint16 // max crawl depth for this host

	HostScore float32 // imported host quality / PageRank, 0 if none

	CrawlTotal uint32 // lifetime successful fetches
	ErrorTotal uint32 // lifetime failed fetches
	AvgLatency uint16 // milliseconds, smoothed fetch latency

	Flags uint16 // trap-suspect | robots-missing | dead-host | grouping-override
}

HostRecord is the durable per-host state, one row per host group, keyed and sorted by HostKey. It is the politeness, DNS, and robots state a URL's dispatch reads, plus the per-host budgets and imported quality signal. There are far fewer hosts than URLs, so the host table stays resident while the URL table stays mostly on disk (doc 03).

type HostSignal

type HostSignal struct {
	HostKey   uint64
	HostScore float32
}

HostSignal is one host's imported reputation: the dense per-HostKey host_score tsumugi computes over a prior crawl (doc 09). The blend reads it as the host quality term; a host with no signal falls back to its locally accumulated score.

type Outcome

type Outcome struct {
	URLKey URLKey // which URL this outcome is for

	HTTPStatus uint16 // the status returned
	FetchedAt  uint32 // epoch-hours, fetch completion
	LatencyMS  uint16 // round-trip latency, milliseconds

	NotModified bool   // 304 from the conditional GET
	ContentFP   uint64 // fingerprint of the body, 0 if none
	Simhash     uint64 // near-dup signature, 0 if none
	ContentLen  uint32 // body length in bytes

	ETag           string // ETag header, empty if none
	LastModified   uint32 // epoch-hours, 0 if none
	RedirectTarget string // resolved redirect URL, empty if none

	Retryable        bool   // transient failure, retry warranted
	RetryAfter       uint16 // deciseconds from a Retry-After header, 0 if none
	RobotsCrawlDelay uint16 // deciseconds, fresh Crawl-delay if robots re-read

	// RobotsBody is the raw robots.txt bytes, populated only for a robots fetch
	// (fetch.Request.Robots). Parsing is policy and stays in meguri (doc 07): the
	// fetcher hands back the bytes, the frontier turns them into rules.
	RobotsBody []byte

	Links []Discovery // extracted out-links, canonicalized and ready to route
}

Outcome is the typed result of one fetch, the first-class value that closes the loop (D18). It is not durable: the fetcher returns it through the SPI and the engine consumes it to update the URL's state, its change-rate estimate, its next-due time, its host's adaptive rate, and the importance signals.

A fetch is a request-outcome pair: the engine dispatches a URL and the fetcher returns exactly one Outcome. Freshness reads NotModified, ContentFP, Simhash, and FetchedAt; politeness reads LatencyMS, HTTPStatus, and RobotsCrawlDelay; prioritization reads Links and their per-link cash.

type Signal

type Signal struct {
	Epoch uint64       // the import's version, higher supersedes lower
	Hosts []HostSignal // dense host reputation
	URLs  []URLSignal  // sparse per-page rank
}

Signal is one tsumugi import bundle (doc 12, D16): a monotonically rising epoch, the dense host scores, and the sparse per-page ranks. It is never depended on: a partition that has not yet imported a bundle still crawls correctly, and a newer epoch overwrites an older one rather than summing, so a dropped or duplicated bundle costs only freshness of the signal, never correctness. The bundle a producer emits spans every partition's hosts; the router splits it so each partition imports only the entries for hosts it owns.

type URLKey

type URLKey struct {
	HostKey uint64
	PathKey uint64
}

URLKey is the 128-bit identity of a URL in the frontier (D3). The HostKey is the high 64 bits and the PathKey is the low 64 bits. The HostKey is at once the partition key, the politeness key, and the colocation key, so a host's URLs always live together: same partition, same politeness bucket, contiguous in the file.

func MakeURLKey

func MakeURLKey(grouping, pathAndQuery string) URLKey

MakeURLKey builds a URLKey from a host grouping string and the path-and-query remainder. It is the convenience the discovery path uses once a URL is canonicalized.

func URLKeyFromBytes

func URLKeyFromBytes(b [16]byte) URLKey

URLKeyFromBytes decodes the 16-byte form Bytes produces.

func (URLKey) Bytes

func (k URLKey) Bytes() [16]byte

Bytes returns the 16-byte on-the-wire form: HostKey then PathKey, each little-endian, matching the file's URLKey field layout (doc 10). The two halves read back as the two u64s with binary.LittleEndian.

func (URLKey) Compare

func (a URLKey) Compare(b URLKey) int

Compare returns -1, 0, or 1 as a sorts before, equal to, or after b, in the same big-endian 128-bit order as Less.

func (URLKey) Less

func (a URLKey) Less(b URLKey) bool

Less reports whether a sorts before b when the 128-bit key is read as a big-endian integer: HostKey first, then PathKey. This is the sort order of the URL table (doc 10), and it is what puts a host's rows in one contiguous range. Note the order is logical, not the little-endian byte image the file stores each half in.

type URLRecord

type URLRecord struct {
	URLKey  URLKey // 128 bits, HostKey || PathKey
	HostKey uint64 // high half of URLKey, kept for host joins

	Status          URLStatus       // state-machine state
	Priority        float32         // OPIC + optional imported PageRank
	Depth           uint16          // link distance from nearest seed
	DiscoverySource DiscoverySource // seed | link | sitemap | redirect | manual

	URLRef uint64 // offset into the string region for the canonical URL

	FirstSeen   uint32 // epoch-hours, first discovery
	LastCrawled uint32 // epoch-hours, last successful fetch
	LastChanged uint32 // epoch-hours, last observed content change
	NextDue     uint32 // epoch-hours, next scheduled crawl

	Lambda         float32 // Poisson change rate, changes/hour
	CrawlCount     uint32  // successful fetches
	ChangeCount    uint32  // fetches that saw a change
	NoChangeStreak uint16  // consecutive no-change fetches

	ETagRef      uint64 // offset into string region, 0 if no ETag
	LastModified uint32 // epoch-hours, 0 if no Last-Modified

	ContentFP uint64 // content fingerprint of last body
	Simhash   uint64 // near-dup signature of last body

	HTTPStatus  uint16 // status of last crawl
	RedirectRef uint64 // reference to redirect-target record, 0 if none

	RetryCount uint8  // consecutive transient failures
	ErrorCount uint16 // lifetime failed fetches
}

URLRecord is the durable per-URL crawl state, one row per frontier entry, keyed by URLKey and sorted by URLKey so a host's rows are contiguous. The field names are stable; the frontier, freshness, dedup, prioritization, format, and store packages all reference them. A checkpoint serializes this straight into the URL table columns with no remapping (D1, D12).

type URLSignal

type URLSignal struct {
	URLKey   URLKey
	PageRank float32
}

URLSignal is one page's imported PageRank: the sparse per-URLKey rank tsumugi delivers for the pages a prior crawl covered (doc 09). A URL with no signal scores from its host plus OPIC alone.

type URLStatus

type URLStatus uint8

URLStatus is the state-machine state of a frontier entry. The values are stable and stored in the status column (doc 03, doc 10).

const (
	StatusDiscovered     URLStatus = iota // seen, not yet scheduled
	StatusScheduled                       // queued for a first crawl
	StatusReady                           // eligible to dispatch now
	StatusInFlight                        // dispatched, awaiting an outcome
	StatusCrawled                         // fetched at least once
	StatusDueRecrawl                      // crawled, due for a refresh
	StatusGone                            // 410/404 stable, tombstoned
	StatusExcludedRobots                  // disallowed by robots.txt
	StatusTrapped                         // flagged by trap detection
)

func (URLStatus) String

func (s URLStatus) String() string

String names the status for human-readable reports (meguri stats, inspect). An out-of-range value prints its number so a forward-compatible reader never loses information.

Directories

Path Synopsis
Package bench assembles the deterministic per-partition costs of doc 14 into the hundred-billion projection.
Package bench assembles the deterministic per-partition costs of doc 14 into the hundred-billion projection.
cmd
corpusstat command
Command corpusstat derives the engine-consistent record, URL, host, and host-key-range counts over a url-only JSONL corpus, keyed the way the scale and bench harness key it: full-host grouping via frontier.HostOf and meguri.HostKeyOf.
Command corpusstat derives the engine-consistent record, URL, host, and host-key-range counts over a url-only JSONL corpus, keyed the way the scale and bench harness key it: full-host grouping via frontier.HostOf and meguri.HostKeyOf.
meguri command
Command meguri is the CLI front door to the frontier engine and its .meguri files.
Command meguri is the CLI front door to the frontier engine and its .meguri files.
strmeasure command
Command strmeasure is the Spec 2074 M1 string-blob bake-off.
Command strmeasure is the Spec 2074 M1 string-blob bake-off.
Package dataset converts a meguri live store to and from Apache Parquet, so a frontier snapshot can be published as a Hugging Face dataset and read back into a .meguri file.
Package dataset converts a meguri live store to and from Apache Parquet, so a frontier snapshot can be published as a Hugging Face dataset and read back into a .meguri file.
Package dedup is the seen-set and the content-dedup machinery: the layer that keeps the same URL discovered a thousand times to one frontier entry, and the same content served under a thousand URLs to one crawl (doc 08, D5, D7, D17).
Package dedup is the seen-set and the content-dedup machinery: the layer that keeps the same URL discovered a thousand times to one frontier entry, and the same content served under a thousand URLs to one crawl (doc 08, D5, D7, D17).
Package distribute is the multi-partition layer (doc 12): the partition map that says which partition owns a HostKey, the router that sends a discovery to that owner, the control plane that owns the map and its epoch, and the rebalance that moves a host slice from one partition to another by shipping a .meguri file.
Package distribute is the multi-partition layer (doc 12): the partition map that says which partition owns a HostKey, the router that sends a discovery to that owner, the control plane that owns the map and its epoch, and the rebalance that moves a host slice from one partition to another by shipping a .meguri file.
Package dns is the host-resolution cache: it maps a host to its resolved IP, honors TTLs, and feeds the per-IP politeness bucket so two hosts behind one address do not dodge rate limits.
Package dns is the host-resolution cache: it maps a host to its resolved IP, honors TTLs, and feeds the per-IP politeness bucket so two hosts behind one address do not dodge rate limits.
Package drum is the on-disk seen-set and URL index of a partition (spec 2072 doc 04, Stage B): a real Disk Repository with Update Management (IRLbot, Lee et al.
Package drum is the on-disk seen-set and URL index of a partition (spec 2072 doc 04, Stage B): a real Disk Repository with Update Management (IRLbot, Lee et al.
Package engine composes the resident frontier with a fetcher worker pool and the distribution router into the staged, single-writer partition loop of doc 04.
Package engine composes the resident frontier with a fetcher worker pool and the distribution router into the staged, single-writer partition loop of doc 04.
Package fetch defines the boundary between the frontier engine and whatever actually retrieves bytes from the network.
Package fetch defines the boundary between the frontier engine and whatever actually retrieves bytes from the network.
Package format is the .meguri single-file container: the 64-byte header, the five regions (URL table, host table, schedule index, seen-set filter, string and blob), and the footer written last, bracketed by the magic MEG1 at both ends.
Package format is the .meguri single-file container: the 64-byte header, the five regions (URL table, host table, schedule index, seen-set filter, string and blob), and the footer written last, bracketed by the magic MEG1 at both ends.
Package freshness is the rescheduler, the heart of meguri (doc 06).
Package freshness is the rescheduler, the heart of meguri (doc 06).
Package frontier is the in-partition crawl frontier: the bounded-memory Mercator-style queue that turns a partition's URL records into a stream of dispatch decisions in priority-then-politeness order.
Package frontier is the in-partition crawl frontier: the bounded-memory Mercator-style queue that turns a partition's URL records into a stream of dispatch decisions in priority-then-politeness order.
Package politeness enforces the rules that keep a crawl from hammering a host: the per-host and per-IP token buckets, the crawl-delay derived from robots.txt and adaptive backoff, and the gate that decides when a host's next fetch is eligible.
Package politeness enforces the rules that keep a crawl from hammering a host: the per-host and per-IP token buckets, the crawl-delay derived from robots.txt and adaptive backoff, and the gate that decides when a host's next fetch is eligible.
Package prioritize decides crawl order by importance (doc 09, the M5 milestone).
Package prioritize decides crawl order by importance (doc 09, the M5 milestone).
Package robots parses robots.txt per RFC 9309 and answers the one question the frontier asks: may meguri fetch this path?
Package robots parses robots.txt per RFC 9309 and answers the one question the frontier asks: may meguri fetch this path?
Package scale is the timed, resource-instrumented sibling of the bench package.
Package scale is the timed, resource-instrumented sibling of the bench package.
Package seed is the meguri binary seed format, .seed, the raw block-framed corpus input that replaces gzipped JSONL for the scale passes (Spec 2074 doc 08).
Package seed is the meguri binary seed format, .seed, the raw block-framed corpus input that replaces gzipped JSONL for the scale passes (Spec 2074 doc 08).
Package sitemap parses the Sitemaps 0.9 protocol (sitemaps.org) into the discoveries the frontier feeds on.
Package sitemap parses the Sitemaps 0.9 protocol (sitemaps.org) into the discoveries the frontier feeds on.
Package store is the durable live state store of a partition (doc 11): the hot mutable form of the frontier, log-structured so a crawl update is an append and the log is its own recovery journal (D11).
Package store is the durable live state store of a partition (doc 11): the hot mutable form of the frontier, log-structured so a crawl update is an append and the log is its own recovery journal (D11).

Jump to

Keyboard shortcuts

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