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

# Interactions

> Turn user behavior into better retrieval through automated feedback loops

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](https://mintcdn.com/mixpeek/TwtTrae3Fi3EFJ72/assets/mixpeek-interactions.svg?fit=max\&auto=format\&n=TwtTrae3Fi3EFJ72\&q=85\&s=90d32424716608350755e20c6344ffab)

## 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

> [card] Training Data Generation
>
> Interactions format into contrastive pairs (query, clicked\_doc) for embedding fine-tuning. Position is recorded for future bias correction.

> [card] Relevance Analytics
>
> CTR by position, documents with high skip rates, queries needing tuning, time-to-first-click—all without custom infrastructure.

> [card] Result Deduplication
>
> Exclude previously purchased, viewed, or consumed content automatically by filtering on past interactions.

> [card] 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:

```bash theme={null}
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"
}
```

> [!NOTE]
>
> **`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

| Type | Strength | Use Case |
| - | - | - |
| `impression` | Neutral | Result was rendered on screen |
| `view` | Weak positive | User viewed a result |
| `click` | Moderate positive | User clicked result |
| `dwell` | Weak positive | User lingered on a result |
| `long_view` | Strong positive | Sustained engagement (track via `duration_ms`) |
| `purchase` | Strong positive | Conversion event |
| `add_to_cart` | Positive | Intent to purchase |
| `wishlist` | Positive | User added to wishlist |
| `positive_feedback` | Explicit positive | Thumbs up vote |
| `negative_feedback` | Explicit negative | Thumbs down vote |
| `share` | Strong positive | User shared the result |
| `bookmark` | Moderate positive | User saved for later |
| `query_refinement` | Neutral | User modified their search |
| `zero_results` | Neutral | Query yielded no results |
| `filter_toggle` | Neutral | User modified filters |
| `skip` | Weak negative | User ignored result |
| `return_to_results` | Moderate negative | User bounced back quickly |

> [!TIP]
>
> Combine multiple types per event: `["click", "long_view"]` when a user clicks *and* stays engaged.

## Where signals are stored

Every retriever execution and every interaction you record is written to
[system collections](https://docs.mixpeek.com/docs/vector-store/system-collections.md) in your namespace. Mixpeek
creates each one the first time it has something to store. They are normal,
visible collections: you can list and filter their documents, and you can point
retrievers at them. They hold your own search signals, which is what makes
training a relevance model on your own usage possible without exporting anything.

### What lands in them

| Collection | `signal_type` | Written when | Carries |
| - | - | - | - |
| `mxp_retriever_queries` | `retriever_query` | A retriever executes | `query_text`, `results_count`, `latency_ms`, `filter_fields`, the per-stage rows, and the query embedding |
| `mxp_retriever_executions` | `retriever_execution` | A retriever executes | `status`, `duration_ms`, `results_count`, `session_id` and the request's `inputs` |
| `mxp_retriever_interactions` | `retriever_interaction` | You `POST /v1/retrievers/interactions` | `target_document_id`, `interaction_type`, `position`, `session_id`, `user_id`, `retriever_id`, `execution_id` |

All three carry the run's `execution_id`, so an interaction joins to the query
and the execution it came from.

### Logging a query without a click

You do not log queries yourself. Every retriever execution writes its query to
`mxp_retriever_queries`, including a query that returned nothing and a query
nobody clicked on. To find zero-result queries, filter that collection on
`results_count = 0`.

An interaction records what a user did with one result, so every interaction
names that result (`feature_id`, or its alias `document_id`). That includes the
types that describe the search rather than a result (`zero_results`,
`query_refinement`, `filter_toggle`). A search with nothing to click needs no
interaction: its row in `mxp_retriever_queries` is the log. Send interactions
for results a user acted on, with the `execution_id` the search returned.

> [!TIP]
>
> **The query embedding is reused, not recomputed.** A `retriever_query` row
> stores the dense embedding the execution already produced, so vector search or
> clustering over your past queries costs no new inference.

### What already reads it

* [Learned fusion](https://docs.mixpeek.com/docs/relevance/learned-fusion.md) derives arm weights from
  interaction signals.
* Index recommendations use the query and execution record.
* [Evaluation](https://docs.mixpeek.com/docs/retrieval/evaluations.md) datasets can be mined from real queries.

> [!WARNING]
>
> **Per-tenant reranker and LTR training is demonstrated, not automated.** The
> signals needed to train one are captured and the path has been shown to work.
> There is no production loop training a model on them for you, so treat it as
> something you can build rather than something running.

## 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 Type | Strength | Fusion Learning | Personalization | Analytics | Fine-Tuning |
| - | - | :-: | :-: | :-: | :-: |
| `click` | Moderate | Primary positive signal | Session history | CTR metrics | Positive pairs |
| `long_view` | Strong | High-weight positive | Preference indicator | Engagement depth | Strong positive |
| `purchase` | Strongest | Conversion signal | Purchase exclusion | Revenue attribution | Gold standard |
| `add_to_cart` | Strong | Intent signal | Cart-based recs | Funnel metrics | Near-positive |
| `positive_feedback` | Explicit | Direct reward | Profile building | Quality score | Ground truth |
| `negative_feedback` | Explicit | Direct penalty | Anti-preference | Issue detection | Hard negative |
| `skip` | Weak negative | Implicit penalty | — | Skip rate | Soft negative |
| `return_to_results` | Moderate negative | Bounce signal | — | Bounce rate | Negative pair |
| `share` | Strong | Social proof | Sharing patterns | Virality | Strong positive |
| `bookmark` | Moderate | Save intent | Saved items | Save rate | Positive |
| `dwell` | Variable | Duration-weighted | — | Engagement depth | Weighted |
| `query_refinement` | Neutral (0.0) | Not in default reward map | — | Query analysis | — |

### Use Case Patterns

> [tab] E-commerce
>
> **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

> [tab] Media & Entertainment
>
> **Primary signals:** `long_view`, `click`, `share` — engagement depth matters more than the click.
>
> **Key patterns:**
>
> * Combine `click` and `long_view` in one event when engagement is sustained
> * Use `duration_ms` and `completion_pct` to distinguish genuine views from bounces
> * Track `share` as a strong positive signal for content quality

> [tab] Enterprise Search
>
> **Primary signals:** `click`, `positive_feedback`, `bookmark` — clicks alone may indicate obligation, not satisfaction.
>
> **Key patterns:**
>
> * Add thumbs-up/down UI to capture explicit feedback
> * Store `department` or `role` in metadata for demographic-level learned fusion
> * Track `query_refinement` to find queries where results fall short

> [tab] Recommendations
>
> **Primary signals:** `click`, `purchase`, `negative_feedback` — you need negative signals to avoid recommending disliked content.
>
> **Key patterns:**
>
> * Capture "not interested" / "hide" as `negative_feedback` to suppress similar content
> * Use purchase history to filter already-bought items from results
> * Track position carefully — recommendation position bias is stronger than search

### Position Tracking

> [!WARNING]
>
> **Always capture `position`.** It is stored on every interaction for analytics and audit,
> and recording it from the start saves you a backfill.

Position bias is a well-known problem: users click higher-ranked results regardless of relevance. Mixpeek records `position` on every interaction, and reward computation does not weight by it, so a click at position 8 earns the same reward as a click at position 0. Correct for position bias in your own analysis if you need it.

### Cold Start

When you first deploy you have zero interactions. The system adapts automatically through the [hierarchical fallback](https://docs.mixpeek.com/docs/relevance/learned-fusion.md#hierarchical-fallback) in Thompson Sampling (personal → demographic → global → uniform prior):

| Interactions | Fusion Behavior | What To Do |
| - | - | - |
| 0 | Falls back to `rrf` (uniform weights) | Ship with `rrf`, start collecting signals |
| 1–50 | Global-level learned weights only | Monitor analytics for obvious issues |
| 50–500 | Demographic-level personalization begins | Verify evaluation metrics are trending up |
| 500+ | Per-user personalization kicks in | Run 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:

```javascript theme={null}
// 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

```python theme={null}
# 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

```python theme={null}
# 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 &lt;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

```python theme={null}
# 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. The loop learns one weight per feature URI and keeps no per-document state (see [What Learned Fusion Optimizes](https://docs.mixpeek.com/docs/relevance/learned-fusion.md#what-learned-fusion-optimizes)). Weight higher-intent signals above clicks with the `reward_map`:

```json theme={null}
{
  "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](https://docs.mixpeek.com/docs/retrieval/auto-tune.md) — higher-intent interactions carry more weight — with no manual reranking rules. See [Reward Signals](https://docs.mixpeek.com/docs/retrieval/reward-signals.md) for the full `reward_map`.

## Query & Export

Retrieve interactions for analysis or external ML pipelines:

```bash theme={null}
# Get all interactions for a document
POST /v1/retrievers/interactions/list
{
  "feature_id": "doc_xyz789"
}

# Get user interaction history. Pagination goes in the QUERY STRING.
POST /v1/retrievers/interactions/list?limit=50
{
  "user_id": "user_123"
}

# Export for training (bulk pagination)
POST /v1/retrievers/interactions/list?page=1&page_size=1000
{}
```

> [!WARNING]
>
> `limit`, `page`, `page_size`, `offset`, `cursor` and `after` are **query
> parameters** on this route. `ListInteractionsRequest` accepts `user_id`,
> `session_id`, `retriever_id`, `execution_id`, `feature_id`,
> `interaction_type`, `filters`, `search` and `sort`, and nothing else.
>
> A pagination key sent in the body is dropped rather than rejected, so the call
> returns 200 with the default page size. An export asking for `page_size: 1000`
> in the body silently gets the default instead, and the short result reads as
> "that is all the interactions there are."

## Reward Values and Auto-Tune

When a retriever uses [Auto-Tune](https://docs.mixpeek.com/docs/retrieval/auto-tune.md) (`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:

```json theme={null}
{
  "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](https://docs.mixpeek.com/docs/retrieval/reward-signals.md) 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.

```json theme={null}
{
  "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.

> [!TIP]
>
> 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 next weight refresh.

## Privacy & Compliance

**GDPR Deletion**: Remove all interactions for a user to honor right-to-deletion requests.

```bash theme={null}
DELETE /v1/retrievers/interactions/<interaction_id>
```

**Anonymization**: Hash `user_id` before sending if you need consistent tracking without PII.

## Python SDK

```python Create Interaction theme={null}
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"
)
```

```python Manage Interactions theme={null}
# List interactions for a retriever
interactions = client.retrievers.list_interactions("ret_abc123", page=1, page_size=50)

# Get a specific interaction
interaction = client.retrievers.get_interaction("int_abc123")

# Delete an interaction (GDPR right-to-deletion)
client.retrievers.delete_interaction("int_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.

```bash cURL theme={null}
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"
      }
    ]
  }'
```

```python Python SDK theme={null}
client.retrievers.backfill_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, so they affect learned fusion weights only after the next weight refresh.

## 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](#bulk-backfill) 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

> [!NOTE]
>
> See [Improve Relevance → Analytics](https://docs.mixpeek.com/docs/platform/improve-relevance.md#analytics) for dashboards and alerting on interaction-driven metrics like CTR, engagement rates, and query quality.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.