Production serving¶
Keep one fitted AdaptiveRanker available to the serving process. For each
request:
- construct the eligible candidate set;
- recommend and persist the decision;
- return the selected item; and
- link the outcome when it arrives.
Recommend and log¶
ranked, decision = ranker.recommend_and_log(
user_id="user-42",
candidate_item_ids=["step-01", "step-02", "step-03"],
timestamp=123456,
top_k=3,
exploration=0.05,
decision_id="request-8c87", # a stable application idempotency key
)
The chosen item is first in ranked. The decision is deeply immutable and
records the exact candidate set, scores, action probabilities, chosen
propensity, resolved policy, and deployment-specific policy version.
Supplying a stable decision_id makes delivery retries safe: the same request
returns the original immutable decision instead of resampling. Reusing that ID
for different inputs fails explicitly.
For a single-host pilot, attach the supplied SQLite store when constructing the ranker. It transactionally persists immutable decisions and their one linked outcome without adding a runtime dependency:
from orchid_ranker import AdaptiveRanker
from orchid_ranker.decision_store import SQLiteDecisionStore
store = SQLiteDecisionStore("orchid-pilot.sqlite")
ranker = AdaptiveRanker(decision_store=store).fit(history)
This store is the durable decision/outcome audit boundary. It does not
persist a fitted model or live learner state: persist and restore those through
your application's model-artifact and event-replay workflow before serving
across restarts or workers. For a larger deployment, implement the same
DecisionOutcomeStore contract against the application's event store.
Link the outcome¶
linked = ranker.observe_decision(
decision.decision_id,
outcome=1,
timestamp=123500,
outcome_event_id="score-event-842",
)
The decision ID prevents an outcome from being attached to the wrong recommendation. Retrying the exact same linked outcome is a no-op; a conflicting second outcome and a timestamp earlier than the decision are rejected. Retain the original outcome payload on retries, including its timestamp.
If a request does not need an immutable decision record, the ordinary update is:
Candidate safety¶
Candidate eligibility belongs outside the ranker. Apply hard rules before calling Orchid:
eligible = [
item
for item in catalog
if item.available and item.allowed_for(user)
]
ranked = ranker.recommend(user.id, [item.id for item in eligible])
Do not expect a statistical ranking score to enforce legal, safety, inventory, or access-control rules.
An empty eligible list is a safe no-op: recommend(user.id, []) returns [].
It never falls back to the training catalog. Passing None also returns no
items unless you explicitly enable allow_catalog_fallback=True or attach a
candidate generator.
Timestamps and outcomes¶
Use one consistent numeric time unit (for example, Unix seconds or a monotonic
event sequence). Orchid preserves fractional timestamps and rejects negative,
non-finite, or non-numeric values. Outcomes are exactly 0 or 1; values such
as 0.9 are rejected rather than silently rounded.
New catalog items¶
Register items before serving them if they were not present in fitting history:
fit_semantic_items(catalog) registers the same catalog automatically. New
items use a learned global OOV prior until the next offline refit, but they can
be recommended, logged, observed, and included in that refit. An item returned
by a separately attached semantic encoder is not automatically registered.
Such a recommendation has feedback_supported=False and
recommend_and_log() rejects it by default. Register the item before serving
it, or use allow_unsupported_feedback=True only when an external system owns
the complete feedback path.
Exploration¶
Start with exploration=0.0. Introduce a small nonzero rate only after:
- decisions are persisted successfully;
- outcomes join back by decision ID;
- candidate sets are complete and exact;
- the fallback behavior has been reviewed; and
- monitoring can detect coverage or outcome regressions.
min_item_support is an explicit per-call safety floor. With exploration on,
it overrides the configured default exactly; set it to 0 only when newly
registered items are intentionally eligible for exploration.
Offline-policy promotion¶
fit_policy() is optional. It promotes a CQL overlay only after evaluating the
same blended adaptive-base+CQL action rule that Orchid serves:
The evaluation window must be strictly later, disjoint by both decision ID and
event content, and contain at least 30 events from 30 users by default. Orchid
uses a user-cluster bootstrap by default and preserves the unblended base scores
in every new decision record so the future evaluation can replay the actual
deployment rule. A successful promotion is logged as a distinct hybrid+cql
policy name and learned-state version.
Monitor¶
Review decision volume, outcome coverage, candidate coverage, exploration, propensities, calibration, and drift. Offline estimates can reject a weak policy, but they do not replace a controlled live rollout.
Operational checklist¶
- Preserve event order for every user.
- Use an application-generated
decision_idas the request idempotency key. - Retry an identical linked outcome safely; reject conflicting outcomes.
- Persist model artifacts and rebuild learner state from events—the decision store alone is not serving-state recovery.
- Version deployments and retain a known fallback.
- Monitor missing and delayed outcomes.
- Refit on chronological windows rather than random row splits.
- Treat user identifiers and outcome histories as sensitive data.
Concurrency¶
One AdaptiveRanker serializes its fit, recommendation, observation, decision
logging, and policy-promotion operations. This is safe for a modest serving
process and guarantees a request never sees a partially updated model. For
high-throughput systems, use one fitted ranker per worker and promote a freshly
fitted process or snapshot atomically at the application boundary.
The same AdaptiveRanker object is used from first fit through production
feedback; there is no separate serving model to configure.