Skip to content
MrJev

Truffler

Intent search for Rails: the typed questions you declare about a record are asked when it is saved and the answers stored, so the query at read time is ordinary SQL.

View on GitHub →

Hands-on review

Labels every record with typed questions once, then ranks in SQL. Searching sends the query and no record content, and the read-list really holds.

Good for

  • Rails apps that want intent search over their own tables, not a vector service
  • Teams who need the search path to send a query and nothing else
  • Anyone who wants the spend bounded before a backfill touches a million rows

Watch out for

  • Its request budget still defaults to the pre-2026-09-30 1,200 per minute, so it runs slower than allowed
  • There is no token-rate budget, and the token ceiling dropped 60% on the same date
  • Searching needs a shared cache store with `increment`, and Postgres for vector columns

Tested Oct 1, 2026 at 87e373c3cf15 · Ruby 3.4 in Docker; its 653 tests, plus our own probes through its fake client to count what leaves

How we reviewed this: we ran its 653 tests in Docker, then wrote our own probes against its fake client to count requests and inspect every payload it would have sent. We made no Jev calls and used no API key.

What it does

Truffler is a Rails gem that turns typed questions into a search index you own. You declare labels on a model:

truffler do
  tenant :account_id
  reads :subject, :body, :sender_name
  label :needs_action, :noul, question: "Does this email need the reader to act or reply?"
  label :category, :choice, question: "Which category fits this email?",
    options: { billing: "Invoices, receipts, and payments", travel: "Trips and bookings", other: nil }
end

When a record is saved, each label’s answer is stored as a float in truffler_labels. Together they are a named-dimension embedding — one number per label, one per option for a choice. A query is encoded the same way: for each label, Jev decides whether the query filters on it, prefers it, or ignores it. Ranking is then one SQL statement adding up a weighted dot product, text similarity, keyword hits and exact hits.

Two measurements that matter

Indexing sends only what you declared. We gave a record four canaries, three in columns named by reads and one in a column that is not. The payload was exactly:

{"subject" => "Invoice canary-subject-11", "body" => "Please pay canary-body-22",
 "sender_name" => "canary-sendername-33"}

sender_email never left, and the record is identified by a tag (r001) rather than its primary key. Four labels went out as one request with four questions, not four requests.

Searching sends no record content at all. A keystroke search produced one request with eight questions, carrying the query, its tokens and the label descriptions — and nothing from the rows. We checked for an indexed record’s body on the wire and it was absent. The ranking happens in Postgres over numbers that were already there.

That is the architectural point, and it is the reason to prefer this over asking a model to read your candidates on every keystroke: your data goes out once per write, never per search.

The project’s own test/privacy_test.rb goes further than most — it plants a secret in an Active Record-encrypted column and asserts it appears in no job argument, no Truffler table, no log line and no notification payload. We ran it; it passes.

The budget

Indexing a table through a paid API needs a bound, and this has an elaborate one: a gem-wide per-minute allowance with 25% headroom, converted to a per-second ceiling and split by priority (live, encode, rerank, backfill), plus per-user caps, a per-tenant live cap, demotion of backfill work instead of denial, and a backfill spend cap that persists across runs in its own table, defaulting to $5 per tenant.

The one stale piece is the number at the top. DEFAULT_REQUESTS_PER_MINUTE = 1_200 was TypeSafe’s published figure until 2026-09-30, when the limit became 40 requests per second. Because the gem divides by sixty and takes headroom, its live ceiling works out at 15 requests a second against an allowance of 40 — so the effect is lost throughput rather than 429s, and TYPESAFE_REQUESTS_PER_MINUTE overrides it. Worth raising deliberately rather than leaving at a default that now means something else.

More interesting: the budget counts requests, not tokens, and the token ceiling is the one that fell, from 250,000 a second to 100,000. A backfill over long records is the case where that bites, and nothing here paces against it.

Verdict

A week old, 50 stars, MIT, and already carrying a privacy test, a spend cap with its own migration, and a docs/host-integration.md that maps every returned field to the UI state it drives. It ships no views, which is the right call for a gem that wants to live in somebody else’s app.

Read the budget section of the initializer before the first backfill, and set TYPESAFE_REQUESTS_PER_MINUTE yourself now that the published limit has moved. For the smaller version of the same idea — a named question as a predicate on one field — see jev-feels.

See how it compares with other tools in Best Jev tools, tested hands-on.

Review updated Oct 1, 2026. Numbers quoted from the project are its author's own; we don't publish our own measurements of Jev.

More in Libraries & Integrations

agentrun

★ 423▲ 13

Parcha-ai/agentrun

A workflow DSL for agents with a Jev package for the steps that are decisions rather than generation.

TypeScript

switch

★ 420▲ 170

osolmaz/switch

TypeScript framework for decision-model harnesses: your program offers a short list of next steps and the model picks one at a time until it can answer, with chess, wikirace and rubric-judge examples run on Jev.

TypeScript

Ten Levels of Jev

★ 264▲ 10

disler/ten-levels-of-jev

Thirty worked uses arranged in ten levels, from one if statement to an agent that reaches for Jev itself, with a video walkthrough.

TypeScriptReviewed

Get new Jev projects every week

New Jev releases, pricing changes, and the best new projects, once a week. No spam; unsubscribe anytime.

Powered by Buttondown. See our privacy policy.