#1888·graphiti

[BUG] Episode search drops candidates before the cross-encoder can score them

Author: JiangLLMCreated Sep 12, 2026Updated Sep 12, 2026

Bug Description

episode_search() fetches up to 2 * limit episodes. But it keeps only the first limit episodes before the cross-encoder scores them.

With limit=1, the model sees just one episode. It cannot choose the second result, even if that result is more relevant.

The code is in search.py.

Steps to Reproduce

This example calls the real episode_search() function. It uses fixed search results and scores, so it needs no database or model service. Run it with the core package dependencies installed.

python
import os
os.environ['GRAPHITI_TELEMETRY_ENABLED'] = 'false'

import asyncio
from datetime import datetime, timezone
from types import SimpleNamespace
from unittest.mock import patch

from graphiti_core.nodes import EpisodeType, EpisodicNode
from graphiti_core.search.search import episode_search
from graphiti_core.search.search_config import (
    EpisodeReranker, EpisodeSearchConfig, EpisodeSearchMethod,
)
from graphiti_core.search.search_filters import SearchFilters

episodes = [
    EpisodicNode(
        uuid=uuid, name=uuid, group_id='alice', content=content,
        source=EpisodeType.message, source_description='conversation',
        valid_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
    )
    for uuid, content in [
        ('first', 'Alice discussed dinner plans.'),
        ('second', 'Alice prefers vegetarian dinners.'),
    ]
]

async def fulltext(*args):
    return episodes

class Reranker:
    async def rank(self, query, passages):
        scores = {episodes[0].content: 0.1, episodes[1].content: 0.9}
        return sorted(
            [(text, scores[text]) for text in passages],
            key=lambda item: item[1], reverse=True,
        )

async def main():
    with patch('graphiti_core.search.search.episode_fulltext_search', fulltext):
        result, scores = await episode_search(
            driver=SimpleNamespace(), cross_encoder=Reranker(),
            query='What dinner should I recommend to Alice?',
            _query_vector=[], group_ids=['alice'], limit=1,
            config=EpisodeSearchConfig(
                search_methods=[EpisodeSearchMethod.bm25],
                reranker=EpisodeReranker.cross_encoder,
            ),
            search_filter=SearchFilters(),
        )
    print([episode.uuid for episode in result], scores)

asyncio.run(main())

Expected Behavior

['second'] [0.9]

Both episodes reach the cross-encoder. The second one has the higher score, so it is returned.

Actual Behavior

['first'] [0.1]

The second episode is removed before the cross-encoder sees it.

Environment

  • Graphiti 0.30.2, main commit c035afb7990b6077331a81e98b04efcfd9bf8184
  • Python 3.11.6 on macOS
  • No live database or model in this test

Possible Solution

Change the episode shortlist from [:limit] to [: 2 * limit]. Then keep the final [:limit] on the output. The edge-search path already uses this approach.

I have a one-line fix and two regression cases for limits 1 and 2. Both cases fail on main and pass with the fix. All 35 tests in tests/utils/search pass after the change.

The cross-encoder may score twice as many episodes. The number of database queries stays the same. I have not tested this with a live model or database.

This issue concerns the episode shortlist size. #1817 concerns score thresholds, and #1816 concerns edge identities. I found no report for this specific case. I can send the fix as a PR if this approach looks right.

I used Codex for the fix and tests.