Skip to content

Tuning Retrieval

Odin's defaults are chosen to work well out of the box, and most of the time you should leave them alone. When you do need to tune, only a few parameters matter, and understanding what each one trades away is more useful than any recommended value.

Parameter Where Default Trades
beam_width retrieve() 64 Recall ↔ latency
hop_limit retrieve() 3 Depth ↔ latency
max_paths retrieve() 50 Output size
cache_size OdinEngine() 5000 Memory ↔ database round-trips
community_mode OdinEngine() "none" Scope of exploration

Width and depth

The two dials that shape a search are beam_width and hop_limit. beam_width controls how many partial paths survive each hop of beam search: widen it to 128 or 256 for higher recall when you suspect relevant paths are being missed, or narrow it to 16-32 for faster, sharper results. Start at the default 64 and only move once you have a reason to.

hop_limit sets how far a path can reach. Two hops is enough for direct relationships and immediate context; the default of three covers most retrieval with good depth and no explosion; four or five is for genuinely deep chains, like tracing a multi-tier supply chain. Because each extra hop multiplies work by roughly the beam width, raise it deliberately rather than by default.

That multiplication is the key to using them together: latency tracks with beam_width × hop_limit, so the two are not independent. To go deeper without paying for it, narrow the beam; to go broader, keep hops shallow and widen it:

engine.retrieve(seeds, hop_limit=5, beam_width=24)    # deep but focused
engine.retrieve(seeds, hop_limit=2, beam_width=128)   # shallow but thorough

The lever most people miss

Before touching any of that, look at your seeds: they are the single biggest influence on quality. Specific, relevant seeds sharpen the PPR signal and produce high-triage results; broad or generic seeds dilute it and everything downstream feels noisy. When a retrieval disappoints, tighten the seeds first, or run an anchor-then-retrieve pass to discover better ones. Parameter tuning cannot rescue a bad starting point.

Tune with evidence, not guesswork

Every result tells you where its time and budget went, so you never have to guess which stage to adjust:

result["used_budget"]              # exploration budget consumed
result["trace"]["timings_ms"]      # per-stage timings, including 'total'

That turns tuning into a short loop. Start at the defaults and check whether triage["score"] is reasonable and the paths you expected showed up. If recall is low, widen the beam or improve the seeds. If latency is high, read timings_ms to find the culprit, then narrow the beam or cut a hop. If memory is the problem, lower cache_size and accept a few more database round-trips. Change one thing, re-measure, repeat.


The memory side of that loop is covered in Caching, and Production Deployment puts these knobs in the context of a real serving setup.