> ## Documentation Index
> Fetch the complete documentation index at: https://engineering.clinikally.work/llms.txt
> Use this file to discover all available pages before exploring further.

# Discovery Platform Low-Level Design

> Runtime contracts, query planning, TypeSense execution, ranking, merchandising compilation, event routing, and end-to-end discovery use cases.

# Discovery Platform Low-Level Design

This document is the implementation companion to [Discovery platform architecture](/architecture/discovery-platform-architecture). Compass owns policy and request construction. TypeSense executes primary retrieval and ranking.

## Runtime Contract

```text theme={null}
Client -> Compass
  -> normalize + understand query
  -> select profile/rules/eligibility
  -> compile TypeSense request
  -> TypeSense retrieves, filters, groups, facets, ranks, pins/hides
  -> Compass validates final safety/external constraints
  -> response + signed discovery context + operational evidence
```

Compass never reimplements primary search ranking in application code. It may apply a narrow final rerank only for data TypeSense cannot safely know at query time, such as pincode delivery or future seller-offer selection.

## Discovery Product Document

Index one canonical product family, not one document per SKU/size variant.

```ts theme={null}
type DiscoveryProduct = {
  id: string;
  sourceProductId: string;
  sourceVersion: string;
  productFamilyId: string;
  title: string;
  brandId: string;
  brandName: string;
  categoryIds: string[];
  categoryNames: string[];
  concernIds: string[];
  concernNames: string[];
  ingredientIds: string[];
  ingredientNames: string[];
  productType: string;
  routineStep?: string;
  formats: string[];
  skinHairTypes: string[];
  spf?: number;
  ingredientStrengths: Array<{ ingredientId: string; percentage?: number }>;
  searchableDescription: string;

  listable: boolean;
  purchasable: boolean;
  inStock: boolean;
  rxClassification: "none" | "prescription" | "consult_required";
  priceMinimum: number;
  rating?: number;
  searchPopularityScore?: number;

  displayVariants: Array<{
    variantId: string;
    label: string;
    price: number;
    inStock: boolean;
    attributes: Record<string, string | number | boolean>;
  }>;
  defaultDisplayVariantId: string;
  imageUrl: string;
  productUrl: string;
  updatedAt: string;
};
```

Index title, brand, category, concern, ingredient, product type, format, controlled description, facets, numeric values, and ranking fields. Leave display-only data unindexed. Price, stock, listability, and Rx state are separate fields.

## Query Understanding

### Pipeline

```text theme={null}
raw query
  -> Unicode/whitespace/case normalization
  -> protected-term recognition
  -> approved synonym and Hinglish alias matching
  -> numeric/attribute extraction
  -> intent classification
  -> query plan
  -> TypeSense request compilation
```

Use deterministic controlled dictionaries and parsers first. Do not begin with an LLM classifier.

```text theme={null}
vit c                   -> approved alias vitamin_c
SPF 50                  -> spf >= 50
under 500               -> priceMinimum <= 500
for oily skin           -> skinHairTypes contains oily
muhase                  -> concern acne
jhaiyan                 -> concern pigmentation
```

Protected brands, product families, and molecules are recognized before typo/rewrite behavior and must never be rewritten into generic words.

### Intent signals

| Intent           | Evidence                                              |
| ---------------- | ----------------------------------------------------- |
| Product/family   | Exact or near-exact title/family match                |
| Brand            | Brand dictionary/protected-term match                 |
| Category         | Category/product-type dictionary match                |
| Concern          | Concern dictionary or approved alias                  |
| Ingredient       | Ingredient dictionary or approved alias               |
| Combination      | Multiple canonical entities or structured attributes  |
| Natural language | No strong structured match and descriptive query form |

Ambiguous queries may retain multiple candidates. Log confidence and evidence in the Compass explain record.

### Numeric extraction

Use typed parser rules, not text search, for explicit values.

| Input                                 | Query-plan expression                      |
| ------------------------------------- | ------------------------------------------ |
| `spf 50`, `spf 50+`, `minimum spf 50` | `spf >= 50`                                |
| `spf above 50`, `spf over 50`         | `spf > 50`                                 |
| `spf under 50`                        | `spf < 50`                                 |
| `under 700`, `below 700`              | `priceMinimum <= 700` or `< 700` by phrase |
| `between 500 and 1000`                | `500 <= priceMinimum <= 1000`              |
| `10% niacinamide`                     | ingredient `niacinamide`, strength `10`    |
| `around 700`                          | ranking preference, not hard filter        |

Only high-confidence explicit values become hard filters. Ambiguous values, such as `light sunscreen`, become controlled ranking preferences or later hybrid-search signals.

### Query-plan type

```ts theme={null}
type QueryPlan = {
  rawQuery: string;
  normalizedQuery: string;
  intent: { primary: string; confidence: number; evidence: string[] };
  entities: {
    productFamilyIds?: string[];
    brandIds?: string[];
    categoryIds?: string[];
    concernIds?: string[];
    ingredientIds?: string[];
    productTypeIds?: string[];
  };
  filters: Array<{ field: string; operator: string; value: unknown; confidence: number }>;
  rankingPreferences: Array<{ field: string; values: unknown[]; weight: number }>;
  profile: string;
  retrievalMode: "lexical" | "hybrid";
};
```

For `spf above 50 sunscreen for oily skin under 700`, Compass creates:

```json theme={null}
{
  "intent": { "primary": "category", "confidence": 0.96 },
  "entities": { "categoryIds": ["sunscreen"] },
  "filters": [
    { "field": "spf", "operator": ">", "value": 50, "confidence": 0.99 },
    { "field": "skinHairTypes", "operator": "contains", "value": "oily", "confidence": 0.98 },
    { "field": "priceMinimum", "operator": "<=", "value": 700, "confidence": 0.99 }
  ],
  "profile": "sunscreen-search-v1",
  "retrievalMode": "lexical"
}
```

## TypeSense Request Compilation and Ranking

Compass loads the ranking profile and matching active rules, then compiles TypeSense parameters. TypeSense performs the actual relevance score, filtering, grouping, sorting, curation, and pagination.

```text theme={null}
q=sunscreen
query_by=title,brand_name,category_names,concern_names,ingredient_names,product_type,searchable_description
query_by_weights=10,9,7,7,7,6,2
filter_by=listable:=true && purchasable:=true && spf:>50 && skin_hair_types:=[oily] && price_min:<=700
facet_by=brand_name,formats,skin_hair_types,spf,price_min
group_by=product_family_id
sort_by=_text_match:desc,rating:desc,search_popularity_score:desc
pinned_hits=product_123:1
hidden_hits=product_999
```

### Ranking precedence

```text theme={null}
1. Eligibility and safety filters
2. TypeSense lexical/vector relevance
3. Bounded behavioral and declared business signals
4. Merchandising boost/bury adjustment
5. Pins
6. Compass final defensive validation
```

Compass profiles are governed configuration. TypeSense executes them. A profile defines searchable fields/weights, allowed sorts, out-of-stock policy, ranking signals, and lexical/hybrid mode.

Use native TypeSense controls first:

| Requirement                         | Preferred execution                                  |
| ----------------------------------- | ---------------------------------------------------- |
| Exact pin                           | Override or `pinned_hits`                            |
| Exact hide                          | Override or `hidden_hits`                            |
| Synonym                             | TypeSense synonym synchronized from Compass approval |
| Field relevance                     | `query_by`, weights, TypeSense ranking               |
| Structured filter                   | `filter_by`                                          |
| Facets                              | `facet_by`                                           |
| Group product families              | `group_by`                                           |
| Simple boost/bury                   | TypeSense ranking/evaluation parameter               |
| Complex external-context boost/bury | Bounded Compass rerank after over-fetch              |

## Eligibility and Final Validation

Push broad filters into TypeSense, then revalidate in Compass.

```ts theme={null}
function isEligible(product: DiscoveryProduct, context: DiscoveryContext): EligibilityDecision {
  if (!product.listable) return { eligible: false, reason: "not_listable" };
  if (!product.purchasable) return { eligible: false, reason: "not_purchasable" };
  if (context.widget && product.rxClassification !== "none") {
    return { eligible: false, reason: "rx_excluded_from_widget" };
  }
  if (context.promotion && product.rxClassification !== "none") {
    return { eligible: false, reason: "rx_excluded_from_promotion" };
  }
  if (context.excludeOutOfStock && !product.inStock) {
    return { eligible: false, reason: "out_of_stock" };
  }
  return { eligible: true };
}
```

No pin, override, experiment, campaign, or TypeSense parameter can resurrect an ineligible item.

## Core Surface Designs

The following sections are the implementation contracts for the four core discovery surfaces. They use one canonical product collection and one shared eligibility/ranking policy; they do not create separate product indexes per page type.

## Search

### Search API contract

```http theme={null}
POST /v1/discovery/search
```

```json theme={null}
{
  "query": "spf above 50 sunscreen for oily skin under 700",
  "surface": "search_results",
  "platform": "web",
  "filters": {
    "brandIds": ["brand_example"],
    "formats": ["gel"]
  },
  "sort": "featured",
  "page": 1,
  "pageSize": 24,
  "anonymousId": "anon_...",
  "sessionId": "ses_...",
  "userId": null,
  "location": null
}
```

Compass validates page bounds, allowed filter fields/values, sort, platform, session identity, and request size before query understanding begins. The client never sends TypeSense query parameters directly.

### Search execution sequence

```text theme={null}
1. Generate request_id and search_id.
2. Load active immutable configuration snapshot from Redis:
   protected terms, aliases, ranking profiles, active rules, redirects, flags.
3. Normalize query and build a QueryPlan.
4. Check redirect and medical/Rx policy before product retrieval.
5. Select search profile, required fields, filters, facets, sort, and rules.
6. Compile one TypeSense search request.
7. TypeSense retrieves/ranks/groups/facets/paginates products.
8. Compass validates returned product eligibility and external constraints.
9. If too few eligible visible results remain, over-fetch the next candidate window only when required.
10. Apply fallback ladder when the result quality threshold is not met.
11. Select display variants, badges, context tokens, applied filter chips, and response metadata.
12. Persist explain evidence; emit PostHog search exposure; let TypeSense auto-capture native search analytics.
13. Return response.
```

`search_id` identifies the exact returned list. The explain record stores the raw query, normalized query, QueryPlan, TypeSense request, active rule/profile/configuration versions, candidate exclusions, result positions, fallback step, provider, and latency.

### Search profiles

Profiles are stored in Compass and compiled to TypeSense request parameters. They are not code branches.

```json theme={null}
{
  "name": "search-default",
  "version": 1,
  "retrievalMode": "lexical",
  "queryBy": [
    "title",
    "brandName",
    "categoryNames",
    "concernNames",
    "ingredientNames",
    "productType",
    "searchableDescription"
  ],
  "queryByWeights": [10, 9, 7, 7, 7, 6, 2],
  "defaultSort": "featured",
  "allowedSorts": ["featured", "price_asc", "price_desc", "newest", "rating"],
  "outOfStockPolicy": "exclude",
  "behavioralInfluenceCap": 0,
  "semanticPolicy": "off"
}
```

Examples of profile selection:

| Query/surface         | Profile               | Notes                                                           |
| --------------------- | --------------------- | --------------------------------------------------------------- |
| Exact product/family  | `product-exact`       | Title/family/brand lexical dominance; hybrid off.               |
| Brand                 | `brand-faithful`      | Exact brand match/filter; brand PLP redirect permitted.         |
| Generic search        | `search-default`      | General lexical relevance and allowed facets.                   |
| Concern               | `concern-discovery`   | Concern/category/ingredient emphasis.                           |
| Natural-language need | `search-hybrid` later | Only for approved query groups after offline/shadow validation. |

### Filters, sorting, grouping, and pagination

Compass always adds mandatory filters, then adds explicit user filters and high-confidence extracted filters.

```text theme={null}
Mandatory:
listable:=true && purchasable:=true

Surface policy:
inStock:=true when the surface excludes out of stock
rxClassification:=none for promotion/recommendation contexts

Query-plan filters:
spf:>50 && skinHairTypes:=[oily] && priceMinimum:<=700

User filters:
brandId:=[brand_example] && formats:=[gel]
```

Product-family grouping is mandatory for search and browse. The product document is the canonical family; `displayVariants` provides the price/variant shown on the card. Pagination always occurs after TypeSense grouping. A filter or sort change resets to page one server-side.

Sort semantics:

| Sort                       | TypeSense execution                                                   |
| -------------------------- | --------------------------------------------------------------------- |
| `featured`                 | Active Compass profile plus valid business/rule adjustments and pins. |
| `price_asc` / `price_desc` | Product family selected display price with eligibility policy.        |
| `newest`                   | `launchedAtTimestamp`.                                                |
| `rating`                   | Rating plus review-count threshold guardrail where configured.        |

### Search fallback ladder

Fallback is explicit and measured. It is not a hidden retry loop.

```text theme={null}
Primary QueryPlan
  -> typo-safe lexical plan
  -> relax low-confidence ranking preferences/modifiers
  -> hybrid plan only when enabled and medically safe
  -> closest governed category/concern destination with honest copy
  -> explicit zero-result state
```

Medical/Rx and deliberately out-of-scope terms bypass generic semantic alternatives and follow consultation/clinical or honest out-of-scope policy.

Each response carries `fallback.step`; PostHog records it. TypeSense `nohits_queries` tracks raw engine no-hit demand, which is distinct from a fallback-rescued user result.

### Search failure behavior

| Failure                                | Behavior                                                                                  |
| -------------------------------------- | ----------------------------------------------------------------------------------------- |
| TypeSense timeout/5xx during migration | Serve Netcore when `typesense_with_netcore_fallback` is active; record fallback.          |
| TypeSense timeout after migration      | Safe cached/category fallback where eligibility remains valid; never a blank screen.      |
| Redis config cache unavailable         | Read last valid/versioned configuration from PostgreSQL at protected rate.                |
| Query parser failure                   | Run a conservative raw lexical search with mandatory safety filters; record parser error. |
| Invalid/unknown filter                 | Reject with client validation error; do not pass arbitrary filter syntax to TypeSense.    |

### Search measurement

Required PostHog properties for `search_executed` include `search_id`, raw/normalized query under access policy, intent, extracted entities/filters, profile/version, configuration version, provider, results and positions, result count, fallback, latency, platform, and surface.

Track zero result, fallback rescue, reformulation, search-to-PDP, search-to-cart, search-to-order, latency, rule exposure, and index freshness. Compare these against Netcore during migration.

## Autosuggest

### Design principle

Autosuggest is a grouped router, not a smaller flat search-results page. TypeSense identifies matching candidates and maintains native popular-query data. Compass determines group, destination, privacy/safety, curation, and final suggestion quality.

### Collections

Use one main product collection and small entity collections. Do not create a separate product collection for every suggestion type.

```text theme={null}
discovery_products_v1
discovery_brands_v1
discovery_categories_v1
discovery_concerns_v1
discovery_ingredients_v1
analytics_popular_queries_v1
analytics_nohits_queries_v1
```

`analytics_popular_queries_v1` is a normal TypeSense destination collection populated by its `popular_queries` native analytics rule. It is the popular-query source; no separate leaderboard service is required.

### Suggest API contract

```http theme={null}
POST /v1/discovery/suggest
```

```json theme={null}
{
  "prefix": "sun",
  "surface": "search_box",
  "platform": "web",
  "anonymousId": "anon_...",
  "sessionId": "ses_...",
  "limit": 8
}
```

The client debounces `150-250ms`, cancels an older request when a newer prefix exists, and does not call suggest for a stale input value. Compass uses a stricter latency budget than full search.

### Suggest execution sequence

```text theme={null}
1. Validate prefix length and request context.
2. Normalize/protect terms and match approved aliases, including Hinglish.
3. For a non-empty prefix, issue TypeSense multi_search over products, entities, and popular queries.
4. Attach candidate type/source/rank and apply eligibility to product candidates.
5. Apply Compass blocked, pinned, curated, privacy, and quality policies.
6. Deduplicate labels/destinations and choose a final group order.
7. Return grouped suggestions with stable suggestion IDs and destinations.
```

Group order is deterministic:

```text theme={null}
1. Exact product family
2. Exact brand
3. Exact category, concern, ingredient
4. High-quality product candidates
5. Approved popular queries
6. Curated campaign shortcuts
```

An exact `Acne UV` product family must outrank a broad `Acne` concern. A popular query never bypasses safety or quality gates.

### Suggest response contract

```json theme={null}
{
  "requestId": "req_...",
  "prefix": "sun",
  "groups": [
    {
      "type": "category",
      "items": [{
        "suggestionId": "sug_...",
        "label": "Sunscreen",
        "destination": { "type": "plp", "slug": "sunscreen" },
        "rank": 1,
        "source": "category_entity"
      }]
    },
    {
      "type": "product",
      "items": [{
        "suggestionId": "sug_...",
        "productId": "product_123",
        "label": "Sunscreen SPF 50 Gel",
        "price": 599,
        "imageUrl": "...",
        "destination": { "type": "pdp", "productId": "product_123" },
        "rank": 1,
        "source": "product_prefix"
      }]
    },
    {
      "type": "query",
      "items": [{
        "suggestionId": "sug_...",
        "label": "Sunscreen for oily skin",
        "destination": { "type": "search", "query": "sunscreen for oily skin" },
        "rank": 1,
        "source": "typesense_popular_query"
      }]
    }
  ]
}
```

### Popular query quality gate

Before a TypeSense popular query is displayed, Compass checks:

* It is not a prescription, medical, sensitive, abusive, or blocked phrase.
* It has an acceptable recent successful-result rate and availability profile.
* It is not a near duplicate of an existing entity/product suggestion.
* It is not expired/blocked by a merchandising or compliance rule.
* It has enough recent traffic according to the configured threshold.

TypeSense handles popularity aggregation and prefix matching; Compass decides whether a popular query is eligible to surface.

### Empty-prefix suggestions

For an empty prefix, do not run normal entity multi-search. Return, in policy order:

```text theme={null}
1. Privacy-safe recent searches from session/client state
2. Approved trending TypeSense popular queries
3. Curated seasonal/campaign shortcuts
4. Optional category/concern shortcuts
```

Prescription and sensitive health searches never reappear in a shared/recent surface without explicit privacy approval.

### Suggest analytics

The client sends visible suggestion impressions only after the dropdown is visible. Suggestion clicks include `suggestion_id`, group/type, rank, source, prefix, destination, session, and resulting search/page context. `suggestion_shown` and `suggestion_clicked` go to PostHog; native TypeSense analytics continues to aggregate actual search submissions rather than every keystroke.

## Browse and PLP

### Page definition

Browse pages are query templates over `discovery_products_v1`, not separate product indices.

```ts theme={null}
type PageDefinition = {
  id: string;
  slug: string;
  pageType: "category" | "brand" | "concern" | "ingredient" | "campaign" | "combination";
  title: string;
  description?: string;
  inclusionFilter: StructuredProductFilter;
  rankingProfileId: string;
  filterConfigurationId: string;
  allowedSorts: string[];
  contentSlots: ContentSlotDefinition[];
  seo: { canonicalUrl: string; indexPolicy: "index" | "noindex"; redirectsFrom: string[] };
};
```

Examples:

```text theme={null}
/category/sunscreen
  inclusion: categoryIds contains sunscreen
  profile: category-sunscreen
  facets: brand, SPF, format, skin type, price

/brand/cetaphil
  inclusion: brandId equals cetaphil
  profile: brand-faithful

/concerns/acne
  inclusion: concernIds contains acne
  profile: concern-discovery
```

### Browse request flow

```text theme={null}
1. Client requests page slug plus user filters/sort/page.
2. Compass/Tetris resolves PageDefinition and active page rules.
3. Compass validates requested filters/sort against FilterConfiguration.
4. Compass compiles q=* or a page-scoped query with:
   page inclusion filter + mandatory eligibility + user filters + profile sort + native pins/hides.
5. TypeSense retrieves, groups, facets, sorts, and paginates.
6. Compass applies final eligibility/external constraints and emits page evidence.
7. Tetris adds CMS slots, banners, guides, consultation module, and badges.
```

### Filter configuration

```ts theme={null}
type FilterConfiguration = {
  id: string;
  pageType?: string;
  pageId?: string;
  facets: Array<{
    field: string;
    label: string;
    order: number;
    searchable: boolean;
    selectionMode: "multi_or" | "single";
    pinnedValues?: string[];
  }>;
  quickFilters: Array<{
    label: string;
    filter: StructuredProductFilter;
    order: number;
    startsAt?: string;
    endsAt?: string;
  }>;
  allowedSorts: string[];
};
```

Filter semantics are fixed:

```text theme={null}
multiple values inside one facet: OR
values across separate facets: AND
filter/sort change: reset to page 1
applied filters: returned as removable chips
high-cardinality facet: TypeSense prefix search over entity collection or facet-value endpoint
```

Compass rejects unsupported filters rather than allowing arbitrary `filter_by` expressions from clients. Empty-filter states return clear removal/relaxation choices; they never silently change user filters.

### Browse merchandising and content

Page-level rules use the same control plane as search:

* Pins/hides compile to TypeSense overrides or request-time controls.
* Simple boosts/buries compile to profile/evaluation parameters.
* Content slots are resolved by Tetris/CMS after product retrieval.
* Card badges derive from controlled product flags and Compass rules; badge priority/max count is configuration.

Do not put banners or CMS content in TypeSense documents. TypeSense returns product data; Tetris owns content composition.

### Browse SEO and events

Stable page IDs and canonical URLs are independent of display names. Renames preserve redirects. Filtered URLs receive explicit `index`/`noindex` policy.

PostHog `plp_viewed` records page ID/type, products/positions, profile/config version, active filters, sort, and content-slot IDs. Client events record visible cards, filter/sort changes, content interactions, and PDP clicks.

## Recommendations

### Design boundary

Recommendations use a pipeline, not generic “similar products” search. TypeSense can retrieve attribute-compatible candidates, but Compass applies the strategy, routine logic, safety, exclusions, overrides, fallback, and measurement.

```text theme={null}
Widget request
  -> Compass loads strategy for widget + surface
  -> candidate sources
  -> universal eligibility/Rx filters
  -> widget/routine constraints
  -> TypeSense candidate retrieval/ranking where applicable
  -> Compass pins/hides/fallback
  -> Tetris render + event context
```

### Widget request contract

```http theme={null}
POST /v1/discovery/widgets/{widgetKey}
```

```json theme={null}
{
  "widgetKey": "best_used_with",
  "surface": "pdp",
  "anchor": { "productId": "product_123", "variantId": "variant_123" },
  "sessionId": "ses_...",
  "userId": null,
  "limit": 6
}
```

### Strategy configuration

```ts theme={null}
type WidgetStrategy = {
  id: string;
  widgetKey: string;
  surface: string;
  candidateSources: Array<"curated" | "relationships" | "co_purchase" | "typesense_similarity" | "trending">;
  fallbackOrder: string[];
  rankingProfileId?: string;
  hardExclusions: {
    excludeRx: boolean;
    excludeOutOfStock: boolean;
    excludeAnchor: boolean;
    excludedCategories?: string[];
  };
  routinePolicy?: "none" | "complementary_steps" | "no_conflicts";
  version: number;
};
```

Candidate source order should begin with deterministic sources:

```text theme={null}
1. Curated lists
2. Catalogue relationship data
3. Attribute-based TypeSense candidates
4. Co-purchase data after attribution quality is proven
5. Model candidates only after offline quality review
6. Category/concern trending fallback
```

An LLM is never the primary product recommender.

### TypeSense recommendation retrieval

For attribute-based similarity, Compass builds a constrained TypeSense query from the anchor product:

```text theme={null}
Anchor: niacinamide serum for acne

Candidate query:
filter_by=
  listable:=true && purchasable:=true && inStock:=true &&
  id:!=anchor_product &&
  concernIds:=[acne]

query_by=concernNames,ingredientNames,productType,routineStep,brandName
group_by=productFamilyId
```

For `best_used_with`, the query must prefer a complementary routine step and exclude duplicate actives. For `similar`, it can retain the same product type/concern. The strategy decides this; a generic vector-nearest-neighbor query is insufficient.

### Routine and safety policy

Compass stores controlled relationship tables:

```text theme={null}
product_relationships
  anchor_product_id, related_product_id, relationship_type, confidence, source

routine_step_rules
  anchor_step, allowed_complement_steps, prohibited_same_steps

ingredient_conflicts
  ingredient_a, ingredient_b, severity, rationale, applies_to

safety_cautions
  attribute_or_ingredient, caution_type, policy
```

Before a widget can render, Compass excludes:

* Anchor product itself.
* Rx products from all promotional widgets.
* Out-of-stock or unlistable products.
* Forbidden category/widget combinations.
* Same routine step where complementarity is required.
* Duplicate active ingredients where redundancy is forbidden.
* Known ingredient conflicts and declared safety cautions.

A shorter widget is preferable to weak padding. Every selected item records which source/fallback supplied it.

### Recommendation merchandising, fallback, and events

Merchandisers can pin, hide, cap, floor, or fully replace a widget with a curated list through the same Compass rule model. Widget rules are scoped by widget type, surface, anchor product/family/category, and schedule.

```text theme={null}
model/relationship candidates
  -> curated pin/hide/replace
  -> candidate source fallback order
  -> short or empty safe widget if no quality result exists
```

Compass/Tetris emits widget payload context: widget ID, strategy/version, anchor, slots, selected products, source/fallback, profile/configuration version. The client sends a visible widget impression only when rendered in viewport, then click/add-to-cart context. PostHog measures impression-to-click, click-to-cart, attributed revenue, fallback serve rate, empty rate, and session-level effect.

## End-to-End Use Cases

### Exact product

```text theme={null}
Acne UV Gel
  -> Compass: protected family, product intent, exact lexical profile
  -> TypeSense: title/family/brand weighted retrieval, grouping, pin/hide
  -> Compass: display variant, final eligibility, context token
```

Vector search is disabled or strongly downweighted.

### Brand

```text theme={null}
Cetaphil
  -> Compass: brand dictionary match
  -> redirect to brand PLP if configured, otherwise brand lexical profile
  -> TypeSense: brand-weighted query or exact brand filter
  -> Compass: brand header, active rules, eligibility
```

### Category, ingredient, concern

```text theme={null}
retinol serum
  -> Compass: ingredient=retinol, productType=serum
  -> TypeSense: structured lexical query, facets, profile
  -> Compass: claims/Rx policy and response shaping

dark spots
  -> Compass: concern=pigmentation
  -> TypeSense: concern/category/title retrieval
  -> Compass: concern PLP entry point and safety policy
```

### Hinglish

```text theme={null}
muhase ki cream
  -> Compass aliases: muhase -> acne
  -> optional product-type preference: treatment/cream
  -> TypeSense: structured lexical search

oily skin ke liye sunscreen
  -> Compass: sunscreen + oily skin type
  -> TypeSense: sunscreen retrieval + skin-type filter
```

Hinglish aliases are Compass-controlled rows with locale, canonical entity, source, confidence, version, and approval state. They are not solely vector-model guesses.

### Typo and structured search

```text theme={null}
sunscreem spf 50
  -> Compass: protected terms and spf >= 50 extraction
  -> TypeSense: native typo-tolerant sunscreen retrieval + SPF filter

spf above 50 sunscreen for oily skin under 700
  -> Compass: sunscreen, SPF > 50, oily, price <= 700 QueryPlan
  -> TypeSense: fielded lexical query + filters/facets/ranking
  -> Client: removable filter chips
```

### Autosuggest

```text theme={null}
prefix "sun"
  -> client debounce 150-250 ms
  -> Compass normalization/protection/alias policy
  -> TypeSense product, brand, category, concern, ingredient, popular-query candidates
  -> Compass pinned/blocked/quality rules
  -> grouped destination-aware suggestions
```

### Browse/PLP

```text theme={null}
/concerns/acne
  -> Compass/Tetris page definition
  -> TypeSense q=* + concern inclusion + eligibility + facets + profile
  -> Compass/Tetris content, badges, consult modules
```

### Semantic/hybrid search

```text theme={null}
cream for dark spots
  -> Compass deterministic parsing first
  -> natural-language/weak-lexical classification
  -> query embedding + hybrid TypeSense profile
  -> TypeSense combines lexical relevance and vector similarity
  -> Compass applies exact-match protection, rules, and safety
```

Use hybrid retrieval only after lexical, Hinglish, fixtures, and baseline metrics are proven. Exact product/brand/ingredient and approved alias queries remain lexical dominant. Medical/Rx queries do not become semantic merchandising.

### Redirect, medical, and fallback

```text theme={null}
offers -> Compass redirect -> sale page

tretinoin -> Compass Rx/consult policy -> permitted direct discovery or consultation flow

blood pressure tablet -> Compass medical/out-of-scope policy -> consultation route

normal poor result -> typo-safe correction -> relax low-confidence modifiers
  -> hybrid fallback when enabled -> closest honest category/concern response
```

Every redirect, fallback, and consultation decision is recorded in Compass explain evidence and PostHog.

### Recommendations

```text theme={null}
PDP niacinamide serum -> best-used-with widget
  -> Compass strategy/candidate source
  -> TypeSense controlled candidate retrieval where applicable
  -> Compass excludes Rx, stock failures, anchor product, duplicate steps, ingredient conflicts
  -> Compass widget pins/fallback -> Tetris render
```

## Merchandising Compilation

Compass PostgreSQL is the rule source of truth. TypeSense is an execution target.

```text theme={null}
Panel -> Compass draft -> validation -> preview -> approval
  -> immutable configuration version
  -> TypeSense override/synonym sync or request-time parameters
  -> Compass final validation and explain record
```

Core records:

```text theme={null}
merchandising_rules
typesense_rule_sync
rule_audits
ranking_profiles
synonym_sets
protected_terms
redirect_rules
campaigns
```

`typesense_rule_sync` records TypeSense resource type/ID, source rule version, `pending|synced|failed` status, time, and error. Disabling a rule immediately removes it from Compass active configuration and queues TypeSense deletion.

For a pin:

```text theme={null}
User searches sunscreen
  -> Compass loads matching config/rules
  -> sends eligibility filter + native pin/override to TypeSense
  -> TypeSense ranks and places eligible pin
  -> Compass validates, records rule/resource/version, returns result
```

## Event Contract and Routing

Portable event envelope:

```json theme={null}
{
  "event_id": "uuid",
  "event_name": "search_result_clicked",
  "schema_version": 1,
  "occurred_at": "timestamp",
  "received_at": "server timestamp",
  "producer": "web_sdk | ios_sdk | android_sdk | compass | vector | medusa",
  "anonymous_id": "optional",
  "session_id": "required where applicable",
  "user_id": "optional",
  "request_id": "optional",
  "discovery_context": { "search_id": "optional", "surface": "search_results" },
  "properties": {}
}
```

| Event                             | TypeSense                                              | PostHog                    | Compass PostgreSQL                     |
| --------------------------------- | ------------------------------------------------------ | -------------------------- | -------------------------------------- |
| Search                            | Auto-capture popular/no-hit; disabled for shadow/tests | Server `search_executed`   | Explain/profile/rule/provider evidence |
| Verified click/visit              | Counter/log if configured                              | Validated UI interaction   | No raw clickstream                     |
| Filter/sort/suggestion/impression | Usually no                                             | Validated UI interaction   | No raw clickstream                     |
| Cart/order/refund                 | Optional verified counter conversion                   | Authoritative server event | Attribution/config evidence            |

Compass returns an opaque signed `discovery_context_token` per result. Clients pass it unchanged to clicks and add-to-cart; Vector/Medusa validates it and persists primary discovery attribution on the cart line. The order event uses that persisted line attribution.

## Provider Migration Runtime

```text theme={null}
Compass provider router
  netcore
  shadow
  typesense
  typesense_with_netcore_fallback
```

Assignment is server-side and sticky:

```text theme={null}
assignment_id = user_id, else anonymous_id, else session_id
bucket = hash("discovery-provider-v1" + assignment_id) % 100
```

Shadow returns Netcore, calls TypeSense asynchronously, disables TypeSense analytics, and records normalized comparison data. Public TypeSense traffic falls back to Netcore on invalid response, timeout, index freshness breach, or safety failure.

## Tests and Operational Evidence

Golden cases include exact products/brands, typos, protected terms, Hinglish aliases, structured filters, redirects, zero-result recovery, Rx handling, pins/hides, and recommendation exclusions.

For every search, retain a short-lived explain record with raw/normalized query, query plan, TypeSense parameters, filters, active profile/configuration version, applied rules, excluded candidates/reasons, score contributions where available, result positions, fallback, provider, and latency.
