Documentation
¶
Overview ¶
Package enriche provides web content enrichment: fetch pages, extract text, parse structured data, search for context.
Index ¶
- Constants
- type ContentMeta
- type Enricher
- type Facts
- type FieldProvenance
- type Item
- type Metrics
- type Mode
- type Option
- func WithBrowserFetch(fn func(ctx context.Context, url string) (string, error)) Option
- func WithCache(c cache.Cache) Option
- func WithCacheTTL(d time.Duration) Option
- func WithConcurrency(n int) Option
- func WithFetcher(f *fetch.Fetcher) Option
- func WithFormat(f extract.Format) Option
- func WithLogger(l *slog.Logger) Option
- func WithMapsChecker(c maps.Checker) Option
- func WithMaxContentLen(n int) Option
- func WithMetrics(m *Metrics) Option
- func WithOxBrowser(baseURL string) Option
- func WithRenderSkipDisabled(disabled bool) Option
- func WithSearch(p search.Provider) Option
- func WithSearchFetchLimit(n int) Option
- func WithStealth(c *http.Client) Option
- func WithTargetGuard(fn func(ctx context.Context, rawURL string) error) Option
- type PageStatus
- type Provenance
- type Result
- type SeedFacts
Constants ¶
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.
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 (*Enricher) Enrich ¶
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 ¶
EnrichBatch enriches multiple items concurrently with bounded concurrency. Respects context cancellation — unstarted items are skipped.
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 Option ¶
type Option func(*Enricher)
Option configures an Enricher.
func WithBrowserFetch ¶ added in v1.2.0
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 WithCacheTTL ¶
WithCacheTTL sets the cache TTL for enrichment results.
func WithConcurrency ¶
WithConcurrency sets the max concurrent enrichments in EnrichBatch.
func WithFormat ¶ added in v0.8.0
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
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
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
WithMaxContentLen truncates extracted content to n runes (word-boundary preferred). Default: 0 (no truncation).
func WithMetrics ¶ added in v0.2.0
WithMetrics sets callback hooks for counters (cache hit/miss, errors).
func WithOxBrowser ¶ added in v1.2.2
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
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 ¶
WithSearch sets a search Provider for external context.
func WithSearchFetchLimit ¶ added in v1.2.0
WithSearchFetchLimit sets the max number of search result URLs to fetch when the item has no primary URL. Default: 5.
func WithStealth ¶
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
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 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.
Source Files
¶
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. |