recommend

package
v1.18.8 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package recommend serves the recommendation models the hermod-ml worker trains (ml/worker/hermod_ml/recommend.py) in-process: it parses a version's artifact once and answers "which items next for this person" and "which items are like this one" from it, without a call to the worker.

The artifact is little-endian throughout:

"HREC"            4 bytes, magic
format version    uint32, FormatVersion
header length H   uint32
header            H bytes of JSON: algorithm, users, items, factors,
                  neighbors, seen, popular
user factors      float32 [users x factors]      when factors > 0
item factors      float32 [items x factors]      when factors > 0
neighbor items    uint32  [items x neighbors]    when neighbors > 0
neighbor scores   float32 [items x neighbors]    when neighbors > 0
seen offsets      uint32  [users + 1]
seen items        uint32  [seen], ascending per person
popular items     uint32  [popular], most popular first
popular scores    float32 [popular]

Format 2 is a recommendation scenario's (ml/worker/hermod_ml/scenarios.py): the same sections, then what serving the scenario needs. Its header adds scenario, as_of, period_days, customer_neighbors, the names of the customer and item features, customer_attrs and item_attrs (one value or null per customer or item), salesmen, weights, rules, global_gap, global_visit_gap, has_visits and has_margin, and, since customer categories and sizes, has_peers and size_edges; since products' details, new_days and pair_details; since returns and quotes, signals. Its algorithm may be "none", with neither factors nor neighbors, for a scenario that needs no products. After popular scores come:

customer neighbors     uint32  [users x customer_neighbors] when > 0
customer nbr scores    float32 [users x customer_neighbors] when > 0
seen last day          uint32  [seen], days since 1970-01-01, or empty
seen gap               float32 [seen], median days between orders, or NaN
customer features      float32 [users x customer features], NaN unknown
item features          float32 [items x item features], NaN unknown
seen quantity          float32 [seen], median quantity per order, or NaN
                       (when pair_details)
seen spend             float32 [seen], what was spent on the pair
                       (when pair_details)
signal customers       uint32  [signals]                (when signals)
signal items           uint32  [signals]                (when signals)
signal days            uint32  [signals], days since 1970-01-01
signal kinds           uint32  [signals], 0 returned, 1 lost quote,
                       2 open quote                     (when signals)

An artifact comes from the worker's model store, so it is parsed as input that may be wrong: every count is checked against the bytes that back it before anything is allocated for it, every index against what it indexes, and every float for being finite. A bad artifact is refused with ErrInvalid; nothing in it can make Parse or a later call panic or allocate more than the artifact's own size allows.

Index

Constants

View Source
const (
	FormatVersion         = 1
	FormatVersionScenario = 2
	MaxArtifactBytes      = 128 << 20
	MaxFactors            = 1024
	MaxNeighbors          = 1024
	MaxPopular            = 1000
	MaxCustomerNeighbors  = 1024
	MaxFeatures           = 64
)

Limits of an artifact. MaxArtifactBytes matches the worker's own limit: a scenario's artifact carries per-customer features and neighbours besides the model, so it is 128 MiB, twice format 1's original 64.

View Source
const (
	AlgorithmALS            = "als"
	AlgorithmItemSimilarity = "item_similarity"
	AlgorithmNone           = "none"
)

The algorithms an artifact may hold. AlgorithmNone is a format 2 scenario's that needs no products (a visit plan or reorder dates without a product column).

View Source
const (
	NewDaysMin = 1
	NewDaysMax = 365
	MaxStock   = MaxBatch * 10
)

NewDaysMin and NewDaysMax bound a model's new_days, as the worker's NEW_DAYS_BOUNDS; MaxStock bounds the stock a request may give.

View Source
const (
	ReasonBrand      = "brand"
	ReasonNewProduct = "new_product"
	ReasonSubstitute = "substitute"
	ReasonSameKind   = "same_kind"
)

The reason codes of products' details.

View Source
const (
	DefaultK = 10
	MaxK     = 100
)

How many items a call returns: DefaultK when it does not say, at most MaxK.

View Source
const (
	RuleFilter    = "filter"
	RuleExclude   = "exclude"
	RuleBoost     = "boost"
	RuleDiversity = "diversity"
	RuleCapacity  = "capacity"
	RulePin       = "pin"
	RuleBlock     = "block"
)

The kinds of Rule.

View Source
const (
	OpEq  = "eq"
	OpIn  = "in"
	OpNeq = "neq"
	OpNin = "nin"
	OpGt  = "gt"
	OpLt  = "lt"
)

The comparisons a filter or boost makes.

View Source
const (
	FromSalesman  = "salesman"
	FromTerritory = "territory"
)

The request fields a rule's value may be bound to.

View Source
const (
	MaxRules       = 50
	MaxRuleIDs     = 1000
	MaxRuleValues  = 1000
	MaxExcludeDays = 3650
	MaxPerValue    = 100
)

Bounds of a rule.

View Source
const (
	// MaxPlan bounds a visit plan: its items, and visits per day.
	MaxPlan = 500
	// MaxBatch bounds the customers one call may ask for.
	MaxBatch = 1000
	// DefaultVisitsPerDay is a salesman's capacity when nothing says.
	DefaultVisitsPerDay = 8

	// MaxWithinDays bounds a reorder window, as a visit plan's period.
	MaxWithinDays = 366
)

Bounds of a scenario call.

View Source
const (
	NearKmMin    = 0.1
	NearKmMax    = 500
	NearPeersMin = peerMin
	NearPeersMax = 200
)

NearKmMin, NearKmMax, NearPeersMin and NearPeersMax bound a model's nearby peers (the training's near_km and near_peers), as the worker's NEAR_KM_BOUNDS and NEAR_PEERS_BOUNDS.

View Source
const (
	ReasonReorderDue       = "reorder_due"
	ReasonChurnRisk        = "churn_risk"
	ReasonHighValue        = "high_value"
	ReasonWhitespace       = "whitespace"
	ReasonNotVisited       = "not_visited"
	ReasonSimilarCustomers = "similar_customers"
	ReasonPeerGroup        = "peer_group"
	ReasonBoughtTogether   = "bought_together"
	ReasonBoosted          = "boosted"
	ReasonPinned           = "pinned"
)

The reason codes of a Pick.

View Source
const (
	ScenarioVisits   = "visits"
	ScenarioProducts = "products"
	ScenarioReorder  = "reorder"
	ScenarioAudience = "audience"
	ScenarioSimilar  = "similar"
)

The scenarios a format 2 artifact answers. A format 1 artifact answers ScenarioProducts and ScenarioSimilar.

View Source
const (
	SignalReorderDue  = "reorder_due"
	SignalChurnRisk   = "churn_risk"
	SignalValue       = "value"
	SignalOpportunity = "opportunity"
	SignalNeglect     = "neglect"
)

The visit signals, which the weights name.

View Source
const AllCustomers = "all_customers"

AllCustomers is the For of a reorder answer for every customer.

View Source
const ReasonPopular = "popular"

ReasonPopular marks an item recommended because many people have it, not because of who is asking: for a person or item the model does not know, or to fill a list the model's own scores leave short.

View Source
const ReasonQuoted = "quoted"

ReasonQuoted is the reason of a product offered for its open quote.

Variables

View Source
var DefaultWeights = map[string]float64{
	SignalReorderDue: 0.35, SignalChurnRisk: 0.2, SignalValue: 0.2, SignalOpportunity: 0.1, SignalNeglect: 0.15,
}

DefaultWeights are a visit plan's weights when its training names none.

View Source
var ErrBadQuery = errors.New("invalid recommendation request")

ErrBadQuery marks a scenario call that is wrong in itself or for the model's scenario.

View Source
var ErrBadRule = errors.New("invalid rule")

ErrBadRule marks a rule that is wrong in itself, or for its scenario.

View Source
var ErrInvalid = errors.New("not a valid recommendation artifact")

ErrInvalid is returned by Parse for bytes that are not a well-formed recommendation artifact. The error says what is wrong.

View Source
var ErrLoosens = errors.New("a request rule may only tighten the model's rules")

ErrLoosens marks a request rule that would loosen a rule the model was trained with. A request may only tighten them.

Scenarios lists every scenario, in the order the worker documents them.

Functions

func CheckTightens

func CheckTightens(defaults, request []Rule) error

CheckTightens refuses request rules that would loosen the defaults, as far as that can be told from the rules alone: a higher capacity or working days the defaults do not allow, a higher diversity cap on the same field, or a pin of what a default blocks. A pin of what a default filter or exclude drops is refused when the call is answered, where the items are known. Filters, excludes, blocks and boosts are added to the defaults, so they can only narrow or reorder what the defaults allow.

func ClampK

func ClampK(k int) int

ClampK is the k a call uses: DefaultK for zero or less, at most MaxK.

func ID

func ID(v any) (string, bool)

ID is the text a person or item id is matched on, as the worker turned the column's values into text (hermod_ml.datasets.to_text): a string as it is, true or false, and a whole number without ".0". It reports false for anything that cannot be an id: nothing, an empty string, an object or list, or a number that is not finite.

func ValidateRules

func ValidateRules(scenario string, rules []Rule) error

ValidateRules reports the first rule that is wrong in itself or for the scenario, and refuses more than MaxRules.

Types

type DueProduct added in v1.18.3

type DueProduct struct {
	ID      string         `json:"id"`
	DueDate string         `json:"due_date"`
	Overdue bool           `json:"overdue,omitempty"`
	Fields  map[string]any `json:"fields,omitempty"`
}

DueProduct is a product a customer is due to buy again: when, from the customer's own gap between buying it, and whether that day has passed.

type Item

type Item struct {
	Item   string  `json:"item"`
	Score  float64 `json:"score"`
	Reason string  `json:"reason,omitempty"`
}

Item is one recommended item. Score is the model's: a dot product for als, a sum of similarities for item_similarity, a cosine similarity for similar items, and the share of the most popular item's count for a popular one.

type Model

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

Model is a parsed artifact. It is immutable, so one Model serves every caller at once.

func Parse

func Parse(raw []byte) (*Model, error)

Parse reads and checks an artifact.

func (*Model) Algorithm

func (m *Model) Algorithm() string

Algorithm is the algorithm that trained the model.

func (*Model) Answer

func (m *Model) Answer(q Query) ([]Result, error)

Answer answers a call of the model's scenario: one Result per subject.

  • visits: for a salesman, their customers to visit, ranked and planned across the working days of the period.
  • products: for a customer (or a batch of customers), products to offer; for a product, products like it.
  • reorder: for a customer, a batch of them, a salesman's customers or every customer, when each will order again (DueDate), most overdue first; for a product, its repeat buyers by when they will buy it again. With WithinDays, only those due before the window ends, overdue ones included, most likely to order first.
  • audience: for a product, the customers likely to want it.
  • similar: for a product, products like it.

func (*Model) AsOf

func (m *Model) AsOf() time.Time

AsOf is the day of the latest transaction the model learned from; zero for a format 1 model.

func (*Model) CustomerAttrs added in v1.18.5

func (m *Model) CustomerAttrs(id string) (segment, size string)

CustomerAttrs is a customer's category (segment) and size; "" for what the model does not know, or a customer it does not.

func (*Model) CustomerIDs

func (m *Model) CustomerIDs() []string

Customer and product ids, for building every result of a model.

func (*Model) CustomerMarket added in v1.18.8

func (m *Model) CustomerMarket(id string) (priceTier, channel, region string)

CustomerMarket is a customer's price tier, channel and region; "" for what the model does not know, or a customer it does not.

func (*Model) DefaultRules

func (m *Model) DefaultRules() []Rule

DefaultRules are the rules trained with the model, which every call obeys.

func (*Model) ForUser

func (m *Model) ForUser(user string, k int, excludeSeen bool) (items []Item, known bool)

ForUser is a person's top k items, best first, leaving out what they already have when excludeSeen is set. A person the model does not know gets the popular items, and known reports false. A list the model's own scores leave short is filled with popular items the person does not have.

func (*Model) Items

func (m *Model) Items() int

Items is how many items the model can recommend: its catalogue.

func (*Model) PeriodDays

func (m *Model) PeriodDays() int

PeriodDays is the model's planning or evaluation period in days; 7 for a format 1 model.

func (*Model) Popular

func (m *Model) Popular(k int) []Item

Popular is the k most popular items.

func (*Model) ProductAttrs added in v1.18.6

func (m *Model) ProductAttrs(id string) (brand, subcategory string)

ProductAttrs is a product's brand and subcategory; "" for what the model does not know, or a product it does not.

func (*Model) ProductIDs

func (m *Model) ProductIDs() []string

func (*Model) Salesmen

func (m *Model) Salesmen() []string

Salesmen lists the salesmen the model knows, from its salesmen table and its customers' salesman column, sorted.

func (*Model) Scenario

func (m *Model) Scenario() string

Scenario is the question the model answers: one of Scenarios. A format 1 model answers ScenarioProducts (and similar items).

func (*Model) Similar

func (m *Model) Similar(item string, k int) (items []Item, known bool)

Similar is the k items most like item, best first. An item the model does not know gets the popular items, and known reports false; a list its neighbors leave short is filled with popular items.

func (*Model) SizeBucket added in v1.18.5

func (m *Model) SizeBucket(size string) string

SizeBucket is a customer size as the model stores it: small, medium or large in lower case; a number cut at SizeEdges as training cut the customers' sizes (up to the first edge small, up to the second medium, above it large); any other label as given, trimmed.

func (*Model) SizeEdges added in v1.18.5

func (m *Model) SizeEdges() (lo, hi float64, ok bool)

SizeEdges are the two tertiles the model's numeric customer sizes were cut at into small, medium and large; ok is false when its sizes were labels, or it has none.

func (*Model) Users

func (m *Model) Users() int

Users is how many people the model knows.

type Pick

type Pick struct {
	ID       string         `json:"id"`
	Score    float64        `json:"score"`
	Rank     int            `json:"rank"`
	Reasons  []Reason       `json:"reasons"`
	Fields   map[string]any `json:"fields"`
	DueDate  string         `json:"due_date,omitempty"`
	Overdue  bool           `json:"overdue,omitempty"`
	Products []DueProduct   `json:"products,omitempty"`
}

Pick is one result: a customer or a product, its score (higher is stronger; scores compare within one list only), its rank from 1, why, and what the model knows of it. DueDate is set by the reorder scenario, and Overdue when that day has passed with no order since. Products are the products a reorder customer is due to buy again: those due within the window, or the one product asked about.

type PlanDay

type PlanDay struct {
	Date  string `json:"date"`
	Items []Pick `json:"items"`
}

PlanDay is one working day of a visit plan.

type Query

type Query struct {
	Customer, Product, Salesman, Territory string
	Customers                              []string
	AllCustomers                           bool
	From, To                               time.Time
	Today                                  time.Time
	K                                      int
	Rules                                  []Rule
	ExcludeSeen                            bool
	WithinDays                             int
	Segment, Size                          string
	PriceTier, Channel, Region             string
	Brand, Subcategory                     string
	Stock                                  map[string]float64
}

Query is one scenario call. Exactly one subject is set: Customer, Customers, Product or Salesman, as the scenario allows. Territory, when set, is what a rule's from_request "territory" binds to; otherwise the territory most of the salesman's customers are in. From and To bound a visit plan (both days included); zero means the model's period from Today. K is how many results, 0 for the default. Rules are the request's, added to the model's own. AllCustomers asks a reorder model about every customer. WithinDays, for reorder, keeps the customers due to order before Today plus that many days, overdue ones included, ranked by how likely they are to order; 0 is no window. Segment and Size, for a list of customers (visits, reorder, audience), keep only the customers of that category and size, either matched in any case; a numeric Size is bucketed as the model's training cut its customers' sizes (SizeBucket). PriceTier, Channel and Region keep them likewise to one price tier, channel and region. Brand and Subcategory, for a list of products (products, similar), keep only the products of that brand and subcategory, matched in any case. Stock is stock on hand by product id, in place of the model's (as of its training): a product with none is not offered.

type Reason

type Reason struct {
	Code string `json:"code"`
	Text string `json:"text"`
}

Reason says in words, with numbers, why a result is listed.

type Result

type Result struct {
	For     string    `json:"for"`
	Items   []Pick    `json:"items"`
	Plan    []PlanDay `json:"plan,omitempty"`
	Summary string    `json:"summary,omitempty"`
}

Result answers one subject of a call: For is the customer, product or salesman asked about. Summary, for a customer's products, says in a line what the customer's nearby peers buy, such as "Minimarket customers within 5 km buy: A, B, C"; empty otherwise.

type Rule

type Rule struct {
	Kind        string   `json:"kind"`
	Field       string   `json:"field,omitempty"`
	Op          string   `json:"op,omitempty"`
	Value       any      `json:"value,omitempty"`
	FromRequest string   `json:"from_request,omitempty"`
	Days        int      `json:"days,omitempty"`
	Weight      float64  `json:"weight,omitempty"`
	MaxPer      int      `json:"max_per,omitempty"`
	PerDay      int      `json:"per_day,omitempty"`
	IDs         []string `json:"ids,omitempty"`
}

Rule is one business rule, applied when serving: on the model as a default for every call, or on one request, where it is added to the defaults and may only tighten them.

  • filter keeps only what matches Field Op Value.
  • exclude drops what was interacted with (bought; visited, for a visit plan) in the last Days days, or ever when Days is 0.
  • boost adds Weight x the normalised Field to the score, or a flat Weight to what matches Field Op Value or is listed in IDs.
  • diversity keeps at most MaxPer results per value of Field.
  • capacity (visits only) plans at most PerDay visits a day, on the weekdays Value lists ("mon", ...) when it lists any.
  • pin always includes IDs; block never does.

Field is "customers.<name>" or "products.<name>": an attribute the training mapped (salesman, territory, segment, blocked, name, category, active) or a feature (monetary, price, margin, ...), or id. FromRequest binds Value to the request's salesman or territory instead.

type SalesmanInfo

type SalesmanInfo struct {
	VisitsPerDay *float64 `json:"visits_per_day"`
	WorkingDays  []string `json:"working_days"`
}

SalesmanInfo is a salesman's capacity as the salesmen table gave it; a nil or empty field was not given.

Jump to

Keyboard shortcuts

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