Skip to main content

Token-Aware Context Query

SochDB ships a context-query builder for assembling LLM prompt context that fits a token budget. You declare named sections with priorities, the builder fills them from the database (or from literal text), counts tokens, and drops or truncates the lowest-priority sections when the budget is exceeded. Output can be emitted as TOON, JSON, or Markdown.

Per-SDK availability

The context-query builder is a real, importable module in the Rust SDK (context_query), the Node.js SDK (context-builder), and the Go SDK (context_builder.go). In Python it is an example pattern, not an importable SDK class (see the Python SDK guide). The capabilities differ per SDK: the Rust and Node builders can pull live data from the database (get / last / search / whereEq / sql), while the Go builder is literal-text only.

What the builder does

  1. Token budgeting — keep assembled context within a model's context window.
  2. Priority ordering — sections are sorted by priority (lower number = higher priority).
  3. Truncation — when over budget, drop or shrink sections by a chosen strategy.
  4. Structured output — emit TOON (default), JSON, or Markdown ready for a prompt.

Token counting is a heuristic (roughly 4 characters per token) unless your SDK exposes a real tokenizer. Treat the budget as a guide, not a hard guarantee.

Quick start

The Rust builder lives in the sochdb crate's context_query module. Sections can be literal text or live queries; search references an embedding bound earlier with set_var.

use std::sync::Arc;
use sochdb::SochConnection;
use sochdb::context_query::{ContextQueryBuilder, ContextFormat, ContextValue};

let conn = Arc::new(SochConnection::open("./my_data")?);

let result = ContextQueryBuilder::with_connection(conn.clone())
.for_session("session_123")
.with_budget(4000)
.format(ContextFormat::Soch) // Soch == TOON, the default
.set_var("query_embedding", ContextValue::Embedding(query_embedding))
.literal("SYSTEM", 0, "You are a helpful assistant.")
.section("DOCS", 1)
.search("documents", "query_embedding", 10)
.done()
.execute()?;

println!("context:\n{}", result.context);
println!("tokens: {}/{}", result.token_count, result.token_budget);
println!("utilization: {:.0}%", result.utilization() * 100.0);

ContextFormat is Soch (TOON, default), Json, Markdown, or Text. ContextValue covers String, Int, Float, Bool, Embedding(Vec<f32>), and Binary.

Sections

A context is built from named, prioritized sections. Lower priority numbers are kept first when the budget is tight.

Literal sections

Available in every SDK. The text is inserted verbatim and counted against the budget.

let result = ContextQueryBuilder::with_connection(conn.clone())
.with_budget(2000)
.literal("SYSTEM", 0, "You are an expert data scientist.")
.literal("USER", 1, "How do I filter rows in a DataFrame?")
.execute()?;

Data-backed sections (Rust and Node only)

The Rust and Node builders can populate a section directly from the database. Open a section with section(name, priority), add one content directive, then done() (done is implicit on execute in Node).

let result = ContextQueryBuilder::with_connection(conn.clone())
.with_budget(4000)
.set_var("q", ContextValue::Embedding(query_embedding))
// GET a path expression
.section("PROFILE", 0).get("users/alice/profile").done()
// LAST N rows from a table
.section("HISTORY", 1).last(5, "messages").done()
// Vector SEARCH against a collection using a bound variable
.section("DOCS", 2).search("documents", "q", 10).done()
.execute()?;

SectionBuilder also supports select(columns, table), where_eq, where_gt, where_lt, where_like, limit, min_score, project, and summarize.

Output formats

All three builders support TOON (the default), JSON, and Markdown. TOON is SochDB's compact wire format and typically uses 40–66% fewer tokens than equivalent JSON.

FormatRust (ContextFormat)Node (ContextOutputFormat)Go (ContextOutputFormat)
TOON (default)SochTOONFormatTOON
JSONJsonJSONFormatJSON
MarkdownMarkdownMARKDOWNFormatMarkdown
use sochdb::context_query::ContextFormat;

let result = ContextQueryBuilder::with_connection(conn.clone())
.format(ContextFormat::Markdown)
.literal("DOCS", 0, "Deep learning models use neural networks...")
.execute()?;

Markdown output looks like:

## SYSTEM

You are a helpful assistant.

## DOCS

Deep learning models use neural networks...

Truncation strategies

When assembled sections exceed the budget, the builder applies a truncation strategy. The defaults differ slightly: Rust and Node keep highest-priority sections and drop the rest; the Go default is TailDrop.

StrategyRust / Node / GoBehavior
Tail dropTruncationStrategy::TailDrop / TruncationStrategy.TAIL_DROP / sochdb.TailDropKeep highest-priority sections; drop the lowest-priority ones that don't fit.
Head dropHeadDrop / HEAD_DROP / sochdb.HeadDropDrop highest-priority sections first (keep the most recent / lowest-priority).
ProportionalProportional / PROPORTIONAL / sochdb.ProportionalShrink every section's text proportionally to fit the budget.
use sochdb::context_query::TruncationStrategy;

let result = ContextQueryBuilder::with_connection(conn.clone())
.with_budget(100)
.truncation(TruncationStrategy::Proportional)
.literal("SYSTEM", 0, "You are a helpful assistant with broad expertise.")
.literal("CONTEXT", 1, "The user is working on a Python data-processing project.")
.execute()?;

Result metrics

Each builder returns the assembled context plus accounting fields.

let result = builder.execute()?;

println!("context:\n{}", result.context);
println!("tokens used: {}", result.token_count);
println!("token budget: {}", result.token_budget);
println!("utilization: {:.0}%", result.utilization() * 100.0);
println!("included: {:?}", result.included_sections());
println!("dropped: {:?}", result.dropped_sections());
println!("truncated: {:?}", result.truncated_sections());

Best practices

  1. Leave headroom. Budget below the model's true window so there is room for the system prompt and the model's reply (for example, 4000 for an 8K window).
  2. Order by importance. Give the system prompt and the user's latest question the lowest priority numbers so they survive truncation.
  3. Prefer TOON for data-heavy context. It typically uses 40–66% fewer tokens than JSON for tabular results.
  4. Monitor what got dropped. In Rust, check dropped_sections() / truncated_sections(); a high drop rate means the budget is too tight or sections are too large.
  5. In Go, pre-query and pass literals. The Go builder only assembles literal text, so do retrieval first and feed the formatted output to Literal(...).

See also