CompositeScorer

The CompositeScorer combines multiple child scorers into a single score using a weighted average.

Overview

CompositeScorer is a composite scorer — it delegates to a list of child scorers, each with an associated weight, and returns the weighted average of their outputs. This enables building rich scoring strategies from simple building blocks.

CompositeScorer is the primary mechanism for combining multiple scoring signals (e.g., “60% recency + 30% priority + 10% kind”) into a single relevance score.

Algorithm

Scoring

COMPOSITE-SCORE(item, allItems, scorers, normalizedWeights):
    // scorers: array of Scorer
    // normalizedWeights: array of float64, sum = 1.0

    result <- 0.0
    for i <- 0 to length(scorers) - 1:
        result <- result + scorers[i].Score(item, allItems) * normalizedWeights[i]
    return result

Construction

At construction time, the raw weights are normalized so they sum to 1.0:

CONSTRUCT-COMPOSITE(entries):
    // entries: list of (scorer, weight) pairs

    if length(entries) = 0:
        ERROR("At least one scorer entry is required")

    totalWeight <- 0.0
    for i <- 0 to length(entries) - 1:
        (scorer, weight) <- entries[i]
        if scorer = null:
            ERROR("Scorer must not be null")
        if weight <= 0.0:
            ERROR("Weight must be positive")
        if weight is not finite:
            ERROR("Weight must be finite")
        totalWeight <- totalWeight + weight

    normalizedWeights <- new array of length(entries)
    for i <- 0 to length(entries) - 1:
        normalizedWeights[i] <- entries[i].weight / totalWeight

    DETECT-CYCLES(entries)

    self.scorers           <- [entries[i].scorer for i in 0..length(entries)]
    self.normalizedWeights <- normalizedWeights

Weight Normalization

Weights are relative, not absolute. The following configurations produce identical results:

  • [(RecencyScorer, 3.0), (PriorityScorer, 1.0)]
  • [(RecencyScorer, 0.75), (PriorityScorer, 0.25)]

Both normalize to weights [0.75, 0.25].

Cycle Detection

CompositeScorer supports nesting — a CompositeScorer may contain other CompositeScorer or ScaledScorer instances as children. However, circular references are invalid and MUST be detected at construction time.

DETECT-CYCLES(entries):
    visited <- empty set (by reference identity)
    inPath  <- empty set (by reference identity)
    for i <- 0 to length(entries) - 1:
        DETECT-CYCLES-DFS(entries[i].scorer, visited, inPath)

DETECT-CYCLES-DFS(node, visited, inPath):
    if node is in visited:
        if node is in inPath:
            ERROR("Cycle detected: scorer appears in its own dependency graph")
        return
    add node to visited
    add node to inPath

    if node is a CompositeScorer:
        for each child in node.children:
            DETECT-CYCLES-DFS(child, visited, inPath)
    else if node is a ScaledScorer:
        DETECT-CYCLES-DFS(node.inner, visited, inPath)

    remove node from inPath

The cycle detection traverses the scorer graph using depth-first search with reference identity comparison. A cycle exists when a scorer appears in its own ancestor path.

Edge Cases

ConditionResult
Empty entries listConstruction error
Single entryNormalized weight is 1.0; effectively delegates to the child
All children return 0.0Composite returns 0.0
All children return 1.0Composite returns 1.0 (weights sum to 1.0)
Nested CompositeScorerValid if acyclic; each child is scored recursively
Self-referential cycleConstruction error

Complexity

  • Construction: O(V + E) for cycle detection, where V is the number of distinct scorer nodes and E is the number of parent-child edges.
  • Scoring: O(C * cost_per_child) per item, where C is the number of child scorers. For a flat composite with simple children, this is O(C) per item.
  • Space: O(C) for normalized weight storage, O(V) for cycle detection.

Conformance Notes

  • Weights MUST be positive (strictly greater than 0.0) and finite. Zero weights are rejected at construction time.
  • Weight normalization MUST divide each weight by the sum of all weights. The normalized weights MUST sum to approximately 1.0 (subject to floating-point precision).
  • Cycle detection MUST use reference identity, not structural equality. Two distinct CompositeScorer instances with identical configurations are not considered cyclic.
  • Child scorers are invoked in order (index 0, 1, 2, …). While the weighted sum is mathematically commutative, the invocation order is fixed for determinism.