Skip to main content
Interactions capture how users engage with search results—clicks, purchases, skips, dwell time. This feedback automatically improves your retrievers through fine-tuning, personalization, and analytics.
Mixpeek Interactions Flow

Why Track Interactions

Automatic Quality Improvement: User behavior becomes training data for fine-tuning embedding models and ranking algorithms—no manual labeling required. Measure Real Performance: Traditional IR metrics (precision, recall) use test sets. Interactions reveal actual user satisfaction through click-through rates, conversion, and engagement. Personalization at Scale: Build user preference profiles from interaction history to deliver personalized results without managing separate recommendation systems. Identify Blind Spots: Find queries with low engagement, results that users skip despite high rankings, and content gaps causing zero-result queries.

What You Get

Training Data Generation

Interactions format into contrastive pairs (query, clicked_doc) for embedding fine-tuning. Position is recorded for future bias correction.

Relevance Analytics

CTR by position, documents with high skip rates, queries needing tuning, time-to-first-click—all without custom infrastructure.

Result Deduplication

Exclude previously purchased, viewed, or consumed content automatically by filtering on past interactions.

Popularity Boosting

Rerank results based on recent interaction signals (clicks, conversions) to surface trending content.

Capture Interactions

Record user behavior with a single API call:
POST /v1/retrievers/interactions
{
  "feature_id": "doc_xyz789",        # Document ID from retriever results
  "interaction_type": ["click"],     # One or more signal types
  "position": 3,                      # 0-based position in results (critical for bias correction)
  "metadata": {
    "duration_ms": 4500,              # Optional: dwell time, device, query text
    "query": "wireless earbuds"
  },
  "user_id": "user_123",              # For personalization and session tracking
  "session_id": "sess_456"
}
interaction_type is always a JSON array — even for a single signal (["click"], not "click"). The array captures the co-occurring signals of one user action, not several separate events: a single result that was shown, clicked, and dwelt on is ["view", "click", "long_view"] in one request. Send a separate request per discrete user action (each click, each purchase) — don’t batch unrelated events into one array.

Signal Types

TypeStrengthUse Case
impressionNeutralResult was rendered on screen
viewWeak positiveUser viewed a result
clickModerate positiveUser clicked result
dwellWeak positiveUser lingered on a result
long_viewStrong positiveSustained engagement (track via duration_ms)
purchaseStrong positiveConversion event
add_to_cartPositiveIntent to purchase
wishlistPositiveUser added to wishlist
positive_feedbackExplicit positiveThumbs up vote
negative_feedbackExplicit negativeThumbs down vote
shareStrong positiveUser shared the result
bookmarkModerate positiveUser saved for later
query_refinementNeutralUser modified their search
zero_resultsNeutralQuery yielded no results
filter_toggleNeutralUser modified filters
skipWeak negativeUser ignored result
return_to_resultsModerate negativeUser bounced back quickly
Combine multiple types per event: ["click", "long_view"] when a user clicks and stays engaged.

Signal Strategy

Which signals to capture, when, and why. Not all signals carry equal weight.

Signal Strength Matrix

This table maps each interaction type to how it’s used across the platform:
Signal TypeStrengthFusion LearningPersonalizationAnalyticsFine-Tuning
clickModeratePrimary positive signalSession historyCTR metricsPositive pairs
long_viewStrongHigh-weight positivePreference indicatorEngagement depthStrong positive
purchaseStrongestConversion signalPurchase exclusionRevenue attributionGold standard
add_to_cartStrongIntent signalCart-based recsFunnel metricsNear-positive
positive_feedbackExplicitDirect rewardProfile buildingQuality scoreGround truth
negative_feedbackExplicitDirect penaltyAnti-preferenceIssue detectionHard negative
skipWeak negativeImplicit penaltySkip rateSoft negative
return_to_resultsModerate negativeBounce signalBounce rateNegative pair
shareStrongSocial proofSharing patternsViralityStrong positive
bookmarkModerateSave intentSaved itemsSave ratePositive
dwellVariableDuration-weightedEngagement depthWeighted
query_refinementNeutral (0.0)Not in default reward mapQuery analysis

Use Case Patterns

Primary signals: purchase, add_to_cart, click — conversion is the strongest relevance indicator.Key patterns:
  • Track add_to_cart separately from purchase to measure funnel drop-off
  • Use position for learning-to-rank bias correction
  • Store order_value in metadata for revenue-weighted metrics

Position Tracking

Always capture position. Although position-based reward weighting is not yet implemented, position is stored for analytics and will be used for future position-aware modeling. Recording it now avoids a costly backfill later.
Position bias is a well-known problem — users tend to click higher-ranked results regardless of relevance. The system records position on every interaction for analytics and audit purposes. However, position-based reward weighting is not yet implemented — a click at position 8 currently receives the same reward as a click at position 0. Record position now so you won’t need to backfill when position-aware modeling is added.

Cold Start

When you first deploy you have zero interactions. The system adapts automatically through the hierarchical fallback in Thompson Sampling (personal → demographic → global → uniform prior):
InteractionsFusion BehaviorWhat To Do
0Falls back to rrf (uniform weights)Ship with rrf, start collecting signals
1–50Global-level learned weights onlyMonitor analytics for obvious issues
50–500Demographic-level personalization beginsVerify evaluation metrics are trending up
500+Per-user personalization kicks inRun benchmarks to compare against baseline

Client-Side Capture

Track clicks and dwell time directly from the browser, using sendBeacon so dwell events survive page unload:
// Track clicks with position
document.querySelectorAll('.search-result').forEach((el, index) => {
  el.addEventListener('click', () => {
    fetch('/api/interactions', {
      method: 'POST',
      body: JSON.stringify({
        feature_id: el.dataset.documentId,
        interaction_type: ['click'],
        position: index,
        metadata: { query: currentQuery },
        user_id: userId, session_id: sessionId
      })
    });
  });
});

// Track dwell time on result detail pages
let startTime = Date.now();
window.addEventListener('beforeunload', () => {
  const dwellMs = Date.now() - startTime;
  navigator.sendBeacon('/api/interactions', JSON.stringify({
    feature_id: currentDocId,
    interaction_type: dwellMs > 5000 ? ['click', 'long_view'] : ['click'],
    position: resultPosition,
    metadata: { duration_ms: dwellMs, query: originalQuery },
    user_id: userId
  }));
});

Outcomes & Use Cases

1. Fine-Tune Embedding Models

# Collect 30 days of interaction data as training pairs
training_data = analytics.get_query_document_pairs(
    date_range="last_30_days",
    min_interactions=2  # Only docs with real engagement
)

# System formats as contrastive pairs:
# Positive: (query, clicked_doc)
# Hard negative: (query, high_ranked_but_skipped_doc)
# Position bias automatically corrected

fine_tuned_model = training.fine_tune(
    base_model="text-embedding-3-small",
    training_data=training_data
)
Outcome: 10-30% improvement in relevance metrics from domain-specific fine-tuning using real user preferences.

2. Identify & Fix Ranking Issues

# Find queries where top results are skipped
signals = analytics.get_retriever_signals(retriever_id="ret_123")
# Returns:
# - Documents ranked high but skipped (ranking issues)
# - Queries with <5% CTR (need tuning)
# - Average position of first click (relevance proxy)
Outcome: Data-driven decisions on which retrievers need adjustment, which taxonomy mappings to update, or which filters to refine.

3. Personalize Without Recommendation Infrastructure

# Exclude content user already consumed
previous_purchases = interactions.list(
    user_id="user_123",
    interaction_type=["purchase"],
    days=90
)

retriever.execute(
    query="new arrivals",
    filters={
        "document_id": {"$nin": [i.feature_id for i in previous_purchases]}
    }
)
Outcome: Improved user experience (no duplicate suggestions) without building separate collaborative filtering or recommendation systems.

4. Continuously Tune Ranking from Interactions

Set fusion: "learned" and click/purchase signals automatically re-weight your search over time — no manual reranking rules. Weight higher-intent signals above clicks with the reward_map:
{
  "stages": [
    {
      "stage_name": "search",
      "stage_type": "filter",
      "config": {
        "stage_id": "feature_search",
        "parameters": {
          "searches": [
            { "feature_uri": "mixpeek://text_extractor@v1/multilingual_e5_large_instruct_v1",
              "query": { "input_mode": "text", "value": "{{INPUT.query}}" }, "top_k": 100 },
            { "feature_uri": "mixpeek://multimodal_extractor@v1/vertex_multimodal_embedding",
              "query": { "input_mode": "text", "value": "{{INPUT.query}}" }, "top_k": 100 }
          ],
          "fusion": "learned",
          "learning_config": {
            "reward_map": { "click": 1.0, "add_to_cart": 2.0, "purchase": 3.0 },
            "decay_window_days": 30
          },
          "final_top_k": 25
        }
      }
    }
  ]
}
Outcome: click/purchase signals continuously re-tune feature weights via auto-tune — higher-intent interactions carry more weight — with no manual reranking rules. See Reward Signals for the full reward_map.

Query & Export

Retrieve interactions for analysis or external ML pipelines:
# Get all interactions for a document
POST /v1/retrievers/interactions/list
{
  "feature_id": "doc_xyz789"
}

# Get user interaction history
POST /v1/retrievers/interactions/list
{
  "user_id": "user_123",
  "limit": 50
}

# Export for training (bulk pagination)
POST /v1/retrievers/interactions/list
{
  "page": 1,
  "page_size": 1000
}

Reward Values and Auto-Tune

When a retriever uses Auto-Tune (fusion: "learned"), every interaction automatically receives a computed reward_value stored in its metadata. This value is derived from the retriever’s reward_map configuration at write time:
{
  "interaction_type": ["click", "purchase"],
  "metadata": {
    "reward_value": 3.0,
    "query": "wireless earbuds"
  }
}
In this example, the reward is determined by the interaction type with the largest absolute value: purchase (3.0) wins over click (1.0), so reward_value is 3.0 (not summed). The reward value determines how much this interaction shifts the fusion weights for the associated feature. How it feeds into Auto-Tune:
  1. Each interaction records which feature URI surfaced the clicked result
  2. The reward_value adjusts the Thompson Sampling Beta distribution for that feature
  3. Positive rewards increase the feature’s weight; negative rewards decrease it
  4. The next search for this user reflects the updated weights
See Reward Signals for the full default reward map, customization options, and examples for different use cases (e-commerce, content, internal search).

Feature URI

The feature_uri field identifies which embedding model/feature produced the result the user interacted with. This is required for Auto-Tune — without it, the interaction cannot contribute to weight learning because the system doesn’t know which feature to reward.
{
  "feature_id": "doc_product_789",
  "interaction_type": ["click"],
  "feature_uri": "mixpeek://text_extractor@v1/multilingual_e5_large_instruct_v1",
  "user_id": "user_456"
}
You can find the feature_uri for each result in the execution response’s learned_fusion_context.feature_uris array, or extract it from the document’s metadata. If feature_uri is set as a top-level field, it is automatically promoted into metadata.feature_uri for backward compatibility.
The session_id field enables within-session adaptation. When provided, clicks in the current session influence the next search immediately via a session cache — without waiting for the ClickHouse write-then-read cycle.

Privacy & Compliance

GDPR Deletion: Remove all interactions for a user to honor right-to-deletion requests.
DELETE /v1/retrievers/interactions/<interaction_id>
Anonymization: Hash user_id before sending if you need consistent tracking without PII.

Python SDK

from mixpeek import Mixpeek

client = Mixpeek(api_key="sk_...")

# After executing a retriever search
results = client.retrievers.execute(
    "ret_abc123",
    inputs={
        "query": "wireless headphones",
        "user_id": "user_456",
        "session_id": "sess_789",
    },
)

# Record a click on the 3rd result
client.retrievers.create_interaction(
    feature_id=results["documents"][2]["feature_id"],
    interaction_type=["click"],
    position=2,
    user_id="user_456",
    session_id="sess_789",
    retriever_id="ret_abc123"
)

Bulk Backfill

Migrate existing interaction history into Mixpeek using the batch endpoint. Send up to 1,000 interactions per call with occurred_at timestamps so temporal decay weights them by their true age.
curl -X POST "$MP_API_URL/v1/retrievers/interactions/batch" \
  -H "Authorization: Bearer $MP_API_KEY" \
  -H "X-Namespace: $MP_NAMESPACE" \
  -H "Content-Type: application/json" \
  -d '{
    "interactions": [
      {
        "feature_id": "doc_001",
        "interaction_type": ["click"],
        "position": 0,
        "user_id": "user_123",
        "feature_uri": "mixpeek://text_extractor@v1/multilingual_e5_large_instruct_v1",
        "occurred_at": "2026-01-15T10:30:00Z"
      },
      {
        "feature_id": "doc_002",
        "interaction_type": ["purchase"],
        "position": 2,
        "user_id": "user_123",
        "feature_uri": "mixpeek://text_extractor@v1/multilingual_e5_large_instruct_v1",
        "occurred_at": "2026-01-16T14:00:00Z"
      }
    ]
  }'
The response includes per-item results: {created: 2, failed: 0, results: [{index: 0, interaction_id: "...", status: "created"}, ...]}. Backfilled interactions bypass the real-time session cache — they affect learned fusion weights only after the next ClickHouse aggregation.

Best Practices

  1. Always capture position — recorded for analytics and future position-aware modeling
  2. Store query text in metadata — enables query-document pair export for fine-tuning
  3. Track dwell time — separates genuine engagement (long_view) from accidental clicks
  4. Backfill with timestamps — use the batch endpoint with occurred_at so temporal decay weights by true age
  5. Monitor weekly — set up dashboards to track CTR trends and catch relevance regressions early
See Improve Relevance → Analytics for dashboards and alerting on interaction-driven metrics like CTR, engagement rates, and query quality.