Building Full-Text Search for Agent Traces with Custom Inverted Index on Object Storage

langchain · Tech · 2026 · source Agentic: yes · promotional bias: low · source-backed · extracted with prompt 0.5.5 (schema v2)

Generated from extraction.json — do not edit by hand. Every claim cites verbatim source evidence; stated means the quote directly asserts the claim, implied means it supports it indirectly.

Problem

Domain: Agent observability storage, full-text search, and object-storage query engines

LangChain needed SmithDB to support full-text search and JSON filtering over very large LangSmith agent traces stored in object storage. Traditional log-search assumptions did not fit: agent trace inputs and outputs can be 1 MB+ or hundreds of megabytes uncompressed, source:index economics are worse than ordinary logs, term frequencies are highly skewed, users need path/value/full-text predicates, and object-store round trips make random I/O and large postings fetches expensive.

Constraints:

Architecture

SmithDB implements a custom inverted index inside the Vortex columnar format, optimized for object storage. Vortex row groups, segment reads, statistics, masks, projections, and I/O coalescing prune and batch object-store reads. The v1 index used term_key/term_value columns, postings as List, positions as List<List>, and Vortex default encodings, but scale exposed per-column encoding inefficiency, term-skewed row groups, and expensive position reshaping during merge. The v2 layout sizes row groups by byte budgets, uses one FST per row group as the term dictionary, stores term_info offsets, encodes postings and positions as per-term block-bitpacked delta blobs, skips positions for path-only queries, and fetches only the byte ranges needed for the predicate.

Diagram confidence: reconstructed (reconstructed from prose).

flowchart LR
  langsmith-agent-traces[LangSmith agent traces] --> smithdb[SmithDB]
  smithdb --> object-storage[Object storage]
  smithdb --> vortex-columnar-files[Vortex columnar files]
  smithdb --> apache-datafusion-scan-pipeline[Apache DataFusion scan pipeline]
  smithdb --> inverted-index-file[Inverted index file]
  object-storage --> vortex-io-scheduler[Vortex IO scheduler]
  vortex-columnar-files --> row-group-pruning[Row group pruning]
  vortex-columnar-files --> vortex-io-scheduler
  vortex-columnar-files --> inverted-index-file
  query-predicates[Query predicates] --> apache-datafusion-scan-pipeline
  apache-datafusion-scan-pipeline --> row-group-pruning
  apache-datafusion-scan-pipeline --> inverted-index-file
  row-group-pruning --> fst-term-dictionary[FST term dictionary]
  inverted-index-file --> fst-term-dictionary
  inverted-index-file --> term-info-table[Term info table]
  inverted-index-file --> postings-blob[Postings blob]
  inverted-index-file --> positions-blob[Positions blob]
  fst-term-dictionary --> term-info-table
  term-info-table --> postings-blob
  term-info-table --> positions-blob
  postings-blob --> apache-datafusion-scan-pipeline
  positions-blob --> apache-datafusion-scan-pipeline
Drag to pan · scroll or use +/− to zoom · reconstructed from the source's described component topology

Components

Component Role Interacts with
LangSmith agent traces Large nested JSON events whose inputs and outputs dominate storage and search workload. SmithDB
SmithDB Agent observability database providing full-text search and JSON filtering over object-stored traces. Object storage, Vortex columnar files, Apache DataFusion scan pipeline, Inverted index file
Object storage Durable storage layer that keeps compute stateless but imposes request-latency and bytes-read constraints. Vortex IO scheduler
Vortex columnar files Extensible columnar format for core event data and index data, with custom encodings and file layouts. Row group pruning, Vortex IO scheduler, Inverted index file
Apache DataFusion scan pipeline Query execution path that receives pushed-down search and JSON predicates. Row group pruning, Inverted index file
Query predicates json_key, json_key_search, and search predicates for path existence, keyed value, and full-text queries. Apache DataFusion scan pipeline, FST term dictionary
Row group pruning Uses row-group statistics and term min/max/count to skip irrelevant row groups before reading index bytes. FST term dictionary
Inverted index file Sibling Vortex file that decouples index construction and merge from the core run-data write path. FST term dictionary, Term info table, Postings blob, Positions blob
FST term dictionary Per-row-group finite state transducer mapping indexed terms to row-group ordinals. Term info table
Term info table Metadata table holding doc counts and offsets into postings and positions. Postings blob, Positions blob
Postings blob Per-term block-bitpacked delta encoded document ID lists. Apache DataFusion scan pipeline
Positions blob Per-term token-position data used only for phrase queries. Apache DataFusion scan pipeline
Vortex IO scheduler Coalesces nearby segment reads to minimize object-store GET requests. Object storage

Data flow

LangSmith events arrive as large nested JSON traces in SmithDB/Vortex object-storage files. Query predicates enter the DataFusion/Vortex scan path as json_key, json_key_search, or search. Vortex prunes row groups, filters surviving rows, projects required columns, and coalesces segment reads. For a surviving index row group, the query walks the FST term dictionary, uses the ordinal to read term_info offsets, fetches the relevant postings byte range, and only fetches positions for phrase queries. Matching row IDs map directly to Vortex row positions, avoiding a translation table.

4 supporting quotes
“SmithDB supports full-text search and JSON filtering over agent traces with a median (P50) latency of 400 ms, even though the underlying data consists of large, deeply nested JSON documents stored in object storage. Full-text search is well-trodden ground.”article, stated
“We wanted search to push down through the same scan pipeline as every other predicate, not run as a parallel query stack with its own segment model and IO assumptions. 3. Doc IDs aligned with Vortex rows. Tantivy's writer assigns its own segment-local”article, stated
“filters surviving rows down to a mask, and projects only the columns the query actually needs. The unit of I/O in a Vortex file is a segment: a contiguous physical byte range. On object storage”article, stated
“A lookup is one walk through the dictionary, one offset table read, and one byte-range fetch. The FST resolves the term to an ordinal. The ordinal indexes into term_info, which gives an offset into postings and (for phrase queries) an offset into”article, stated

Why this architecture

LangChain chose a custom Vortex-native inverted index because SmithDB's workload combines huge agent trace payloads, object-store latency, columnar scan pushdown, and row-position identity requirements. Tantivy/Lucene-style ideas were useful, but local-disk mmap assumptions, separate segment/doc-ID semantics, and parallel query-stack integration would have fought SmithDB's storage and execution model. V2 then reworked the physical layout around byte budgets and per-term encodings because v1's textbook layout amplified skew and merge costs.

4 supporting quotes
“we decided to approach the problem from first principles because indexing agent traces for search workloads presents a unique challenge.”article, stated
“Tantivy is built around mmap; every byte is microseconds away and random I/O is effectively free. We're on object storage with ~100 ms round trips, where layout and coalescing decide query latency, not CPU. 2. Embedded in a columnar engine. SmithDB”article, stated
“SmithDB needs the index to point directly at row positions in the corresponding Vortex data file (we use Vortex for our core event data files), so a doc ID is a row index — no translation table, no second identity to reconcile at query time, and merges that”article, stated
“Our v2 layout addresses all three v1 problems by changing the unit of organization from "N terms per row group" to a byte-budgeted row group”article, stated

Models used: SmithDB · LangSmith · Apache DataFusion · Vortex · Finite State Transducer · FSST · zstd · Tantivy · Lucene-style inverted index

Scale

Alternatives considered (roads not taken)

Adopt Tantivy directly.

Rejected because: Tantivy assumes mmap/local-disk random I/O, separate segment-local doc IDs, and a search stack that would not push down through SmithDB's Vortex/DataFusion scan path.

“excellent search indexing library and the obvious reference point for Lucene-style search in Rust. We started by asking whether we could adopt it directly. The design we ended up with is heavily inspired by Tantivy, but a few constraints made it an awkward fit for our use-case”article, stated
“we could adopt it directly. The design we ended up with is heavily inspired by Tantivy, but a few constraints made it an awkward fit for our use-case directly: 1. Object storage, not local disk. Tantivy is built around mmap; every byte is microseconds away and random I/O is”article, stated

Scan content payloads without an index.

Rejected because: A content filter would scan many gigabytes just to return a few rows.

“traces in LangSmith, we observed the average to be closer to 1:1.9. Three things follow: * A content filter without an index is catastrophically slow. "Find runs whose tool output mentions a timeout" cannot scan every payload in the candidate range otherwise it would scan”article, stated
“tool output mentions a timeout" cannot scan every payload in the candidate range otherwise it would scan many gigabytes to return three rows. * Term frequencies follow a Zipfian distribution. Natural-language and JSON payloads follow a power law: a handful of”article, stated

Use fixed term-count row groups.

Rejected because: Term skew let one high-frequency term push a row group past 100 MB or even 500 MB compressed, producing oversized GETs and merge memory spikes.

“We batched a fixed number of terms per row group, which meant a single high-frequency term could push one row group past 100 MB compressed while another sat at a few MB.”article, stated
“one high-frequency term in a v1 row group could push it past even 500 MB compressed. The byte budgets give us an upper bound on every row group for the amount of bytes fetched from object store or memory footprint while executing queries. Per-row-group”article, stated

Rely on Vortex default per-column encoding for postings and positions.

Rejected because: A common token forced a larger bit width on every term in the column, hurting cache behavior and increasing reads.

“Vortex picked the encoding for the whole column, not per term, so a single common token (agent, langchain) forces a larger bit width on every term in the entire chunk”article, stated

Use zstd-compressed term dictionaries.

Rejected because: zstd can be smaller for high-cardinality term_value data, but it is opaque and requires decompression for lookup; the FST can be searched directly.

“On the high-cardinality term_value column, FST is ~1.5× larger than zstd but still beats FSST. The crucial point is that zstd is opaque: every lookup requires decompressing the block. The FST is the index itself — exact lookup, prefix and range”article, stated
“zstd is opaque: every lookup requires decompressing the block. The FST is the index itself — exact lookup, prefix and range scans, and automaton walks (LIKE, fuzzy, regex) all run directly against the compressed bytes with O(|term|) cost and no”article, stated

Patterns

observability-tracing (operations)

SmithDB indexes LangSmith agent traces so developers can search large agent inputs, outputs, tool data, and errors for debugging and observability.

“SmithDB supports full-text search and JSON filtering over agent traces with a median (P50) latency of 400 ms”article, stated
“Users query by path ("does this run have inputs.content.messages?"), by value”article, stated

latency-optimization (operations)

The index layout minimizes object-store requests and bytes read through row-group pruning, byte budgets, coalesced segment reads, FST lookup, and direct postings/positions byte-range fetches.

“The cost of a query is roughly proportional to (requests issued to object storage) × (bytes read per request).”article, stated
“the primary lever for query latency is minimizing the number of requests.”article, stated

cost-aware-routing (operations)

SmithDB keeps durable data in object storage and designs index reads around object-store request and byte costs rather than local-disk assumptions.

“SmithDB keeps all durable data in object storage so compute is relatively stateless and the system scales by adding nodes without having to manage local disks.”article, stated
“Every aspect of SmithDB’s inverted index, from its storage layout to query execution is designed with these constraints in mind.”article, stated

batch-offline-processing (data)

Index construction and merge are decoupled from core run data by storing positions/postings in a separate sibling Vortex file.

“Positions (required for phrase search) alone were an order of magnitude larger than every other column, so we kept the index in a separate file from the core run data. This let us decouple index construction and merge from the core write path.”article, stated
“This let us decouple index construction and merge from the core write path.”article, stated

Tradeoffs

Search library integration

Chose custom Vortex-native inverted index over direct Tantivy adoption.

Rationale: The custom design preserves object-storage-aware layout, DataFusion pushdown, and row-index identity with Vortex data files.

“Tantivy is built around mmap; every byte is microseconds away and random I/O is effectively free.”article, stated
“SmithDB needs the index to point directly at row positions in the corresponding Vortex data file”article, stated

Storage model

Chose object storage with stateless compute over local-disk search engine assumptions.

Rationale: Object storage simplifies durable scaling by adding compute nodes, but requires careful request and byte minimization.

“compute is relatively stateless and the system scales by adding nodes without having to manage local disks.”article, stated
“Each object store request carries tens of milliseconds to hundreds of milliseconds of latency.”article, stated

Row group sizing

Chose fixed byte budgets over fixed number of terms per row group.

Rationale: Byte budgets bound worst-case object-store reads and memory footprint under term skew.

“we determine the row group sizes with fixed independent byte budgets instead of a fixed row count.”article, stated
“Sizing in bytes, not term count, is what fixes v1's third problem.”article, stated

Term dictionary encoding

Chose FST per row group over zstd/FSST-only compressed strings.

Rationale: FST can be searched directly for exact, prefix, range, LIKE, fuzzy, and regex-style walks without decompressing a block first.

“The FST is the index itself — exact lookup, prefix and range scans, and automaton walks (LIKE, fuzzy, regex) all run directly against the compressed bytes”article, stated
“The trade-off is that a single lookup may touch multiple FSTs per file, but pruning makes that cost rare in practice”article, stated

Posting and position representation

Chose block-bitpacked deltas plus VInt tails over List and List<List> Vortex cells.

Rationale: Per-term blocks give better compression/control, allow byte-range fetches, and avoid full Arrow decode on reads.

“Postings and positions both use the same Tantivy/Lucene-style two-tier encoding.”article, stated
“Within a block we store deltas between successive doc IDs, not the IDs themselves, and bitpack the block to the minimum width that fits its max delta.”article, stated

Gotchas (what bit them)

Agent traces invert traditional log-search economics.

Root cause: LangSmith traces are enormous JSON documents where a single document can produce more index data than many log lines.

Resolution: Specialize the inverted index layout for huge documents, skewed term frequencies, and multiple JSON/text query modalities.

“We index billions of enormous documents, where one document can produce more index data than many small log lines.”article, stated
“The index must stay compact and prunable across many orders of magnitude of term frequency, all inside one file.”article, stated

Object storage makes naive index reads expensive.

Root cause: Object-store round trips and per-request throughput make large postings or positions fetches dominate query time.

Resolution: Minimize requests and bytes read with coalesced segment reads, pruning, byte budgets, and range fetches only after lookup.

“Each object store request carries tens of milliseconds to hundreds of milliseconds of latency.”article, stated
“fetching a large postings list or positions list before you know you need it can dominate the query.”article, stated

Per-column encoding punished all terms for a few frequent tokens.

Root cause: Vortex v1 chose encoding for the whole column, so common tokens forced larger bit widths across the chunk.

Resolution: Use per-term block-bitpacked deltas so each term chooses its own bit widths block by block.

“Vortex picked the encoding for the whole column, not per term, so a single common token (agent, langchain) forces a larger bit width on every term in the entire chunk”article, stated
“Per-term encoding, not per-column. Each term picks its own bit widths block-by-block: a frequent term like agent packs at 3–4 bits per doc, a rare term never leaves its VInt tail.”article, stated

Fixed-size row groups produced pathological object-store GETs.

Root cause: Term count was a poor proxy for I/O size under Zipfian term skew.

Resolution: Size row groups by independent posting-byte and raw-term-string byte budgets.

“Term skew makes term count a poor proxy for IO size, as one high-frequency term in a v1 row group could push it past even 500 MB compressed.”article, stated
“The byte budgets give us an upper bound on every row group for the amount of bytes fetched from object store or memory footprint while executing queries.”article, stated

Position reshaping dominated compaction cost.

Root cause: Merging v1 segments required decoding full nested position lists, reshuffling inner lists, and recomputing outer offsets.

Resolution: Store postings and positions as opaque per-term encoded blobs that queries fetch and decode on demand.

“Merging two segments meant decoding the full positions List<List<u32>>, reshuffling inner lists into the new document order, and recomputing every outer offset.”article, stated
“Vortex sees the encoded bytes as a single binary blob; it never decodes them into Arrow on the read path.”article, stated

Evaluation

LangChain evaluated the design through system-level search latency, payload and index-size ratios, object-store request/byte costs, row-group sizing bounds, encoding-size comparisons, and v1/v2 operational bottleneck analysis.

Metrics: Median P50 query latency of 400 ms. · 1 MB+ common payloads and hundreds-of-megabytes uncompressed outliers. · Source:index ratio of about 1:1.9 for LangSmith agent traces. · Object-store round-trip latency around 100 ms. · 32 MB posting-byte and 64 MB raw-term-string row-group budgets. · Positions making up 70%+ of index bytes in v1. · FST dictionary size comparisons against raw, FSST, and zstd.

4 supporting quotes
“SmithDB supports full-text search and JSON filtering over agent traces with a median (P50) latency of 400 ms”article, stated
“with some of these stretching to hundreds of megabytes uncompressed.”article, stated
“for agent traces in LangSmith, we observed the average to be closer to 1:1.9.”article, stated
“MiB 81.7 MiB 31.3 MiB 37.6 MiB On term_key, where a few hundred JSON paths repeat across millions of rows, the FST collapses the entire dictionary to 3.8 KiB: four orders of magnitude smaller than the raw bytes and ~4× smaller than zstd. On the high-cardinality term_value column, FST”article, stated

Outcomes

The workload is large-scale observability infrastructure: billions of enormous documents, common 1 MB+ payloads, hundreds-of-megabytes outliers, object storage, 32/64 MB row-group budgets, and a reported 400 ms P50 search/filter latency.

4 supporting quotes
“SmithDB supports full-text search and JSON filtering over agent traces with a median (P50) latency of 400 ms”article, stated
“Our v2 layout addresses all three v1 problems by changing the unit of organization from "N terms per row group" to a byte-budgeted row group”article, stated
“SmithDB's query surface boils down to three predicate families, and they differ in what they match against and what pattern syntax they admit.”article, stated
“A lookup is one walk through the dictionary, one offset table read, and one byte-range fetch.”article, stated