enriche

package module
v1.40.3 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 20 Imported by: 0

README

go-enriche

Standalone Go library for web content enrichment: fetch pages, extract article text, parse structured data (JSON-LD/Microdata), search for context.

Status

Phase 1 complete — extract/ and structured/ packages ready.

Packages

Package Purpose
extract/ Article text (trafilatura), facts (structured→regex cascade), og:image, dates
structured/ Typed schema.org parsing: Place, Article, Event, Organization
fetch/ HTTP fetch with status detection, stealth, singleflight (Phase 2)
search/ Web search context (DDG, Startpage, Brave, Google providers)
cache/ Cache interface + Memory/Redis/Tiered (Phase 3)

Usage

import (
    "github.com/anatolykoptev/go-enriche/extract"
    "github.com/anatolykoptev/go-enriche/structured"
)

// Extract article text
result, _ := extract.ExtractText(reader, pageURL)
fmt.Println(result.Content, result.Title)

// Extract structured facts (JSON-LD → Microdata → regex cascade)
facts := extract.ExtractFacts(html, pageURL)
fmt.Println(facts.PlaceName, facts.Phone)

// Parse schema.org directly
data, _ := structured.Parse(reader, "text/html", pageURL)
if place := data.FirstPlace(); place != nil {
    fmt.Println(place.Name, place.Address)
}

// Extract og:image
img := extract.ExtractOGImage(html)

License

MIT

Documentation

Overview

Package enriche provides web content enrichment: fetch pages, extract text, parse structured data, search for context.

Index

Constants

View Source
const (
	PhaseHomepageFetch     = "homepage_fetch"
	PhaseHomepageOxBrowser = "homepage_ox_browser"
	PhaseHomepageRender    = "homepage_render"
	PhaseContactsFetch     = "contacts_fetch"
	PhaseContactsRender    = "contacts_render"
	PhaseTotal             = "total"
)

Phase names emitted through Metrics.OnPhaseTiming. Exported so a consumer's histogram labels (gowp_wp_enrich_phase_seconds{phase}) share one vocabulary with the emitter and cannot drift.

View Source
const (
	StatusActive          = fetch.StatusActive
	StatusNotFound        = fetch.StatusNotFound
	StatusRedirect        = fetch.StatusRedirect
	StatusUnreachable     = fetch.StatusUnreachable
	StatusWebsiteDown     = fetch.StatusWebsiteDown
	StatusClosed          = fetch.StatusClosed
	StatusTemporaryClosed = fetch.StatusTemporaryClosed
	// StatusTLSInvalid is fetch.StatusTLSInvalid re-exported — see its doc
	// comment (fetch/status.go) and Result.TLSFallbackUsed above.
	StatusTLSInvalid = fetch.StatusTLSInvalid
)

Re-exported PageStatus constants so consumers don't need to import fetch.

Variables

This section is empty.

Functions

This section is empty.

Types

type ContentMeta

type ContentMeta struct {
	Title       string
	Author      string
	Description string
	Language    string
	SiteName    string
}

ContentMeta holds article metadata extracted by trafilatura.

type Enricher

type Enricher struct {
	// contains filtered or unexported fields
}

Enricher orchestrates web content enrichment.

func New

func New(opts ...Option) *Enricher

New creates an Enricher with the given options.

func (*Enricher) Enrich

func (e *Enricher) Enrich(ctx context.Context, item Item) (*Result, error)

Enrich enriches a single item: fetch, extract, search, cache. Returns a partial result on failures (graceful degradation).

Source-priority ordering (the official-site-first invariant): all contact facts flow through one resolver whose precedence is official_site > aggregator > maps > search. The maps card fills facts as a LOWER-priority fallback; the official site is ALWAYS fetched (even when maps reports the venue closed) and its values OVERRIDE the maps card on conflict. A reachable, active site also refutes a maps-only "closed" status. This collapses the former two independent fact writers (the maps merge and the wholesale site-extraction assign) into a single authority — see resolve.go.

func (*Enricher) EnrichBatch

func (e *Enricher) EnrichBatch(ctx context.Context, items []Item) []*Result

EnrichBatch enriches multiple items concurrently with bounded concurrency. Respects context cancellation — unstarted items are skipped.

func (*Enricher) Search added in v1.4.1

func (e *Enricher) Search(ctx context.Context, query, timeRange string) (*search.SearchResult, error)

Search exposes the configured search provider for direct queries.

type Facts

type Facts = extract.Facts

Facts is re-exported from extract package.

type FieldProvenance added in v1.18.0

type FieldProvenance struct {
	Source     string `json:"source,omitempty"`
	Confidence string `json:"confidence,omitempty"`
}

FieldProvenance is the resolved provenance of one fact: the winning source ("operator_verified" | "official_site" | "aggregator" | "maps" | "search") and the coarse confidence bucket ("high" | "medium" | "low"). Zero value (empty strings) means the field was absent / had no resolved source.

type Item

type Item struct {
	Name    string // required
	URL     string // optional — if empty, search-only enrichment
	City    string // optional — for places/events
	Address string // optional — known street address (e.g. "Невский проспект, 28"); used to anchor map lookups
	Mode    Mode
	Source  string // origin identifier
	Topic   string // classification tag

	// SkipMapsCheck is a verify-path lever — when true it suppresses the
	// ModePlaces maps closure-check (checkMapsStatus / mapsChecker.Check).
	// Suppressing it drops BOTH the closure status (StatusClosed /
	// StatusTemporaryClosed) AND every field mergeOrg writes at sourceMaps:
	// place name, address, phone, website, hours, and coordinates (resolve.go).
	// A SiteNumbers-membership verifier reads none of those (it consults only
	// the site's own candidate phone set), so a verify caller's VERDICT is
	// unaffected — but the enriched RESULT genuinely differs, which is why
	// SkipMapsCheck is part of the cache identity (cacheKey, enriche.go): a
	// skip=true blob must never be served to a skip=false caller.
	// wp_enrich (ModePlaces) leaves it false — the maps-check still runs there.
	// Default-false is byte-identical for every existing caller.
	SkipMapsCheck bool

	// Latitude and Longitude are authoritative coordinates provided by the
	// discovery source (e.g. KudaGo). When both are non-nil they take
	// precedence over any coords resolved by the maps checker or geocoder.
	// Must be provided as a pair (both or neither); a lone non-nil Latitude
	// with a nil Longitude is treated as absent.
	Latitude  *float64
	Longitude *float64

	// Seed carries operator-verified field values that MUST win over every
	// enrich-derived source. Empty fields impose no override. This is the
	// override-precedence channel (Phase 3): the content layer re-supplies a
	// previously operator-verified phone/price on every re-enrich so a rotating
	// DNI proxy or a maps card can never silently replace it.
	Seed SeedFacts
}

Item is the input for enrichment.

type Metrics added in v0.2.0

type Metrics struct {
	OnCacheHit       func()
	OnCacheMiss      func()
	OnFetchError     func()
	OnSearchError    func()
	OnMapsCheckError func()

	// OnPhoneSource fires once per Enrich(ModePlaces) call that resolves a
	// phone, with the winning source ("official_site" | "aggregator" | "maps" |
	// "search"). Lets the consumer track enrich_phone_source_total{source}.
	OnPhoneSource func(source string)
	// OnSiteResolved fires once per Enrich call where the official site was
	// fetched and yielded at least one fact (enrich_site_resolved_total).
	OnSiteResolved func()
	// OnConflict fires once per field-adjudication where two DIFFERENT-priority
	// sources offered a genuinely DIFFERENT value — order-independently: both the
	// override case (a higher source overrides a present, differing lower value)
	// and the rejection case (a lower source is rejected because a higher source
	// already owns a differing value). field is the fact name
	// (enrich_conflict_total{field}).
	OnConflict func(field string)

	// OnBrowserRender fires once per Enrich call where the headless-browser
	// render was triggered for the official site, with the reason it fired:
	//   "thin_content"     — readability content was below minExtractChars
	//   "absent_contacts"  — the raw HTML carried no phone/address/hours fact,
	//                        so JS-injected contacts may be hiding behind render
	// Lets the consumer track enrich_browser_render_total{reason} and confirm
	// the full-JS render path actually fires in production (observability for
	// the JS-injected-contacts class).
	OnBrowserRender func(reason string)

	// OnBrowserRenderError fires once per render attempt that FAILED — the
	// headless browser returned an error or an empty/too-short error-shell body
	// (the 160-486 byte bot-protection shells some SPAs serve). Distinct from
	// OnBrowserRender, which fires only on a render that was triggered (whether
	// or not it ultimately yielded more facts). This surfaces the real go-wowa
	// hit-rate so a passthrough-to-raw degrade is observable, not just inferred
	// (enrich_browser_render_error_total).
	OnBrowserRenderError func()

	// OnContactsPageDiscovered fires once per Enrich call where a distinct
	// same-origin contacts page was discovered from the homepage links
	// (enrich_contacts_page_discovered_total). Lets the consumer track how often
	// the contacts-subpage discovery path engages.
	OnContactsPageDiscovered func()

	// OnContactsPageResolved fires once per Enrich call where the discovered
	// contacts page yielded STRICTLY MORE contact facts than the homepage did,
	// so its facts were adopted (enrich_contacts_page_resolved_total). The gap
	// between discovered and resolved is the contacts-page payoff rate.
	OnContactsPageResolved func()

	// OnLegalVsVenueAddress fires when the official site supplied a registered
	// LEGAL address while a (differing) VENUE address already owned the Address
	// slot — the split-identity-address signal. Before the legal/venue field split
	// this case silently overwrote the geo-correct venue address with the legal
	// office, pointing the card's map link at the wrong place. The counter
	// (enrich_address_legal_vs_venue_total) gives that previously-silent class a
	// signal so a wrong-map-link regression is observable, not just perceived.
	OnLegalVsVenueAddress func()

	// OnTargetBlocked fires once per URL the SSRF target guard
	// (Enricher.checkTarget) REFUSED before handing it to an external
	// render/extraction delegate (oxBrowser.Extract, browserFetch) — a
	// security refusal, not a render failure. Kept distinct from
	// OnBrowserRenderError (a genuine go-wowa render failure/error-shell) so
	// SSRF refusals never inflate the real render-error rate.
	// site identifies which render-delegate call site fired:
	//   "homepage_render"          — browserFetch(item.URL) in fetchAndExtract
	//   "contacts_page_render"     — browserFetch(contactsURL) in fetchContactsHTML
	//   "ox_browser_item"          — oxBrowser.Extract(item.URL) in fetchAndExtract
	//   "ox_browser_search_source" — oxBrowser.Extract(srcURL) in fetchOneSource
	// (enrich_target_blocked_total{site}).
	OnTargetBlocked func(site string)

	// OnBrowserRenderSkipped fires once per fetch leg where a headless-browser
	// render the OLD escalation gate WOULD have fired was SKIPPED because the raw
	// fetch already carried a trustworthy anchored site number
	// (rawContactsSufficient) — the render-avoidance win (mcmedok 30s→~2s). leg
	// names the leg that skipped ("homepage" | "contacts"); reason names why the
	// skip was safe ("raw_sufficient" — a non-poisoned anchored SiteNumber was
	// present in the raw HTML). Kept SEPARATE from OnBrowserRender (whose call
	// sites all mean "a render happened") so overloading it can never corrupt the
	// render-avoidance-rate signal (enrich_browser_render_skipped_total{leg,reason}).
	OnBrowserRenderSkipped func(leg, reason string)

	// OnPhaseTiming fires once per instrumented enrichment phase with the phase
	// name (one of the Phase* consts) and its wall-clock duration in seconds.
	// Phases: homepage_fetch / homepage_ox_browser / homepage_render /
	// contacts_fetch / contacts_render (each emitted only when that leg actually
	// runs) and total (the whole Enrich() wall-clock, recorded even on a
	// cache-hit early return). homepage_ox_browser runs in PARALLEL with
	// homepage_fetch, so it overlaps rather than adding to total. Lets the
	// consumer build a per-phase latency histogram (e.g.
	// gowp_wp_enrich_phase_seconds{phase}) to see WHERE enrich time goes. The
	// maps-check leg is intentionally NOT timed here — it has its own
	// gowp_wp_enrich_maps_check_seconds and is skipped on the verify path.
	OnPhaseTiming func(phase string, seconds float64)
}

Metrics provides callback hooks for observability. Any nil field is safely ignored (no-op).

type Mode

type Mode int

Mode specifies the enrichment mode.

const (
	ModeNews   Mode = iota // News articles
	ModePlaces             // Places and businesses
	ModeEvents             // Events and happenings
)

type Option

type Option func(*Enricher)

Option configures an Enricher.

func WithBrowserFetch added in v1.2.0

func WithBrowserFetch(fn func(ctx context.Context, url string) (string, error)) Option

WithBrowserFetch sets a headless browser fallback for JS-heavy pages. When content extracted via HTTP fetch is thin (< 200 chars), the browser function re-renders the page and extraction is retried on the rendered HTML.

func WithCache

func WithCache(c cache.Cache) Option

WithCache sets a Cache for enrichment results.

func WithCacheTTL

func WithCacheTTL(d time.Duration) Option

WithCacheTTL sets the cache TTL for enrichment results.

func WithConcurrency

func WithConcurrency(n int) Option

WithConcurrency sets the max concurrent enrichments in EnrichBatch.

func WithFetcher

func WithFetcher(f *fetch.Fetcher) Option

WithFetcher sets a custom Fetcher.

func WithFormat added in v0.8.0

func WithFormat(f extract.Format) Option

WithFormat sets the output format for extracted content. Default is extract.FormatText (plain text, current behavior). Use extract.FormatMarkdown to preserve links, headings, and formatting.

func WithLogger added in v0.2.0

func WithLogger(l *slog.Logger) Option

WithLogger sets a structured logger for debug-level observability. If l is nil, the default no-op logger is kept.

func WithMapsChecker added in v0.6.0

func WithMapsChecker(c maps.Checker) Option

WithMapsChecker sets a maps.Checker for place status verification. Only effective for ModePlaces items. If the checker reports a place as permanently or temporarily closed, enrichment short-circuits with StatusClosed or StatusTemporaryClosed respectively.

func WithMaxContentLen added in v0.2.0

func WithMaxContentLen(n int) Option

WithMaxContentLen truncates extracted content to n runes (word-boundary preferred). Default: 0 (no truncation).

func WithMetrics added in v0.2.0

func WithMetrics(m *Metrics) Option

WithMetrics sets callback hooks for counters (cache hit/miss, errors).

func WithOxBrowser added in v1.2.2

func WithOxBrowser(baseURL string) Option

WithOxBrowser enables ox-browser readability as an additional content extractor. Runs in parallel with trafilatura; the longer result wins.

func WithRenderSkipDisabled added in v1.31.0

func WithRenderSkipDisabled(disabled bool) Option

WithRenderSkipDisabled toggles the ADR-8 render-skip kill-switch. When disabled is true, the trust-gated render-skip (rawContactsSufficient anchored-SiteNumber escape hatch, go-enriche v1.30.0) is bypassed and the headless render escalates exactly as it did pre-v1.30.0 — on thin content OR a missing single-winner Facts contact — regardless of any anchored raw SiteNumber. Default (disabled=false) keeps the skip ENABLED (the mcmedok 30s->~2s win). This is an OPS revert lever: the DATA consequence of a wrong skip is one-way (a number auto-written to a live paid card cannot be un-published by a code rollback), so the skip must be disable-able without a code change. Intended wiring: go-wp reads a container env (e.g. WP_RENDER_SKIP=off) and passes the boolean here — this package stays env-free (library hygiene).

func WithSearch

func WithSearch(p search.Provider) Option

WithSearch sets a search Provider for external context.

func WithSearchFetchLimit added in v1.2.0

func WithSearchFetchLimit(n int) Option

WithSearchFetchLimit sets the max number of search result URLs to fetch when the item has no primary URL. Default: 5.

func WithStealth

func WithStealth(c *http.Client) Option

WithStealth creates a Fetcher using a stealth HTTP client.

fetch.WithClient REPLACES the Fetcher's client wholesale, which would otherwise silently bypass NewFetcher's default SSRF guard — a real escape hatch, since a stealth client is this org's production HTTP pattern (go-stealth/go-wowa) and the caller-supplied URL it fetches is exactly the untrusted input the guard exists for. httputil.NewSSRFGuardedClient composes the guard into c's Transport instead (connect-time DialContext wrap for a plain *http.Transport, or a request-level pre-check wrap for an opaque RoundTripper such as go-stealth's fingerprinting client) without touching c's TLS/JA3/proxy/middleware configuration.

func WithTargetGuard added in v1.24.0

func WithTargetGuard(fn func(ctx context.Context, rawURL string) error) Option

WithTargetGuard overrides the SSRF safety check run on a URL before it is handed to an external render/extraction delegate (oxBrowser, browserFetch) — a hop this package does not control the outbound dial for, so fetch.Fetcher's own guarded transport cannot protect it. Defaults to go-kit httputil.CheckRawURL, the single, framework-owned SSRF check, which refuses loopback, private (RFC1918/RFC4193), link-local (including the 169.254.169.254 cloud-metadata address), unspecified, multicast, CGNAT, NAT64, 6to4, IPv4-compatible-IPv6, and non-http(s)-scheme targets.

Production callers should not override this — it exists so tests can point oxBrowser/browserFetch at a local httptest server. Passing a nil fn is a no-op (the current guard, or the New() default, is kept).

type PageStatus

type PageStatus = fetch.PageStatus

PageStatus is re-exported from fetch package.

type Provenance added in v1.18.0

type Provenance struct {
	PlaceName FieldProvenance `json:"place_name,omitempty"`
	Address   FieldProvenance `json:"address,omitempty"`
	// LegalAddress is the resolved provenance of a registered/legal-entity address
	// (юридический адрес) extracted from the official site. It is an additive
	// sidecar, separate from Address (the venue/geo slot that drives the card's map
	// link). Present only when the site supplied a legal address; consumers render
	// it as «Реквизиты», NEVER as the map slot.
	LegalAddress FieldProvenance `json:"legal_address,omitempty"`
	Phone        FieldProvenance `json:"phone,omitempty"`
	Website      FieldProvenance `json:"website,omitempty"`
	Hours        FieldProvenance `json:"hours,omitempty"`
	Email        FieldProvenance `json:"email,omitempty"`
	Price        FieldProvenance `json:"price,omitempty"`
}

Provenance is the per-field provenance sidecar for a Result's contact facts. Only fields that resolved to a value carry a non-empty FieldProvenance.

type Result

type Result struct {
	Name          string
	URL           string
	Status        PageStatus   // page availability status
	Content       string       // extracted article text
	Image         *string      // og:image URL
	PublishedAt   *time.Time   // extracted publication date
	Facts         Facts        // structured data
	SearchContext string       // search engine context
	SearchSources []string     // source URLs from search
	Metadata      *ContentMeta // title/author/language

	// Provenance carries the resolved per-field {source, confidence} for the
	// contact/business facts (Phase 3, ONE_WAY). It rides ALONGSIDE the bare
	// *string Facts (which keep their wire shape) as an additive sidecar so the
	// content layer can persist provenance and protect operator_verified values
	// on re-enrich. Absent fields stay zero-valued.
	Provenance Provenance

	// SiteNumbers is the additive (Phase P2) SET of every distinct, valid
	// site-own phone number found across the official-site fetch (the
	// homepage plus any discovered /contacts subpage), each tagged
	// Anchored/DNI/Trustworthy by the SAME fail-closed gate that picks
	// Facts.Phone — see extract.PhoneNumberFact. pickPhoneCandidate collapses
	// the candidate set to ONE winner for Facts.Phone; SiteNumbers exposes
	// the rest, so a consumer can recognize a valid-but-different site number
	// instead of reading it as WRONG. Read-only sidecar: it never feeds
	// Facts.Phone. nil when the site fetch found no phone candidate at all.
	SiteNumbers []extract.PhoneNumberFact

	// RenderSkipped is true when the result rests on the RAW fetch ONLY, with NO
	// successful render corroboration — EITHER the escalation render was
	// intentionally SKIPPED because the raw fetch already carried a trustworthy
	// anchored site number (see rawContactsSufficient), OR a render was ATTEMPTED
	// but FAILED / returned a sub-minRenderShellBytes shell and degraded back to
	// raw. Both land on the same single-source-raw data, so the flag marks both.
	// Additive provenance sidecar, NOT yet wired to any reader (like SiteNumbers):
	// it is EMITTED for a go-wp Phase-1.5 Correctable gate (to be wired at go-wp
	// verify.go's classifyContactVerdict/Correctable) that will force a
	// wrong-verdict correction resting on a render-skipped single-source-raw value
	// to be NON-auto-applicable (human-confirm), so a false-negative skip cannot
	// auto-publish a laundered number to a paid card. This code does NOT yet
	// enforce that property. Zero value (false) = a successful render corroborated
	// the page (or none was avoidable).
	RenderSkipped bool

	// TLSFallbackUsed is true when EITHER the homepage OR the discovered
	// /contacts subpage (see resolveContactsPage) was recovered through
	// fetch.Fetcher's TLS-cert-error fallback (fetch.FetchResult.
	// TLSFallbackUsed — see that field's doc comment) instead of a normal
	// HTTPS fetch: the origin's certificate failed validation (hostname
	// mismatch, untrusted CA, ...) but a plain-HTTP retry through the same
	// SSRF-guarded transport recovered real content. OR'd across both legs —
	// same fail-closed reasoning, and the same OR pattern, as RenderSkipped
	// above — because either leg alone can be the one that supplied the
	// value a caller ends up using.
	//
	// This is the additive, backward-compatible signal go-wp's follow-up
	// task consumes to distinguish a "reachable, cert broken" site from a
	// truly unreachable one (see Status == StatusTLSInvalid for the case
	// where NEITHER the primary fetch NOR the fallback produced content).
	// Same fail-closed contract as RenderSkipped: a field resolved on a
	// TLSFallbackUsed result MUST NOT be auto-applied to a paid/live card
	// without human confirmation (a wrong-cert page could serve
	// attacker-controlled content). This code does NOT yet enforce that
	// property — enforcement is the go-wp Correctable-gate follow-up. Zero
	// value (false) = every fetch behind this result validated its cert
	// normally.
	TLSFallbackUsed bool
}

Result is the output of enrichment.

type SeedFacts added in v1.18.0

type SeedFacts struct {
	PlaceName string
	Address   string
	Phone     string
	Website   string
	Hours     string
	Email     string
	Price     string
}

SeedFacts holds operator-verified field values injected at the top source priority (operator_verified) before any fetch/maps/search runs. A non-empty value pins that field; an empty value leaves the field to normal resolution.

Directories

Path Synopsis
Package cache provides a Cache interface with in-memory (L1) and Redis (L2) implementations.
Package cache provides a Cache interface with in-memory (L1) and Redis (L2) implementations.
Package extract provides content extraction from HTML: article text, structured facts, og:image, publication date.
Package extract provides content extraction from HTML: article text, structured facts, og:image, publication date.
Package fetch provides HTTP page fetching with status detection, stealth support, and singleflight deduplication.
Package fetch provides HTTP page fetching with status detection, stealth support, and singleflight deduplication.
Package maps provides place status verification via map services.
Package maps provides place status verification via map services.
Package search provides external search context aggregation via direct scrapers (DDG, Startpage, Brave, Google) and other providers.
Package search provides external search context aggregation via direct scrapers (DDG, Startpage, Brave, Google) and other providers.
Package structured provides typed schema.org parsing from JSON-LD and HTML Microdata.
Package structured provides typed schema.org parsing from JSON-LD and HTML Microdata.

Jump to

Keyboard shortcuts

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