Connect AI to Billions of Legal Documents — Simon Eskildsen, turbopuffer & Jacob Lauritzen, Legora

Read the talk

Connect AI to Billions of Legal Documents

Selected presentation frame from Connect AI to Billions of Legal Documents — Simon Eskildsen, turbopuffer & Jacob Lauritzen, Legora at 416 secondsOpen full source frame
Making the project the unit of legal search

Legora’s migration from Elasticsearch through Postgres to turbopuffer shows how project lifecycles, customer encryption requirements and storage round trips shape a search engine for billions of legal documents.

From a talk by Jacob Lauritzen and Simon Eskildsen

At a glance

Ideas worth remembering

  • Roughly 4,000 partitions still mixed active and inactive projects. One namespace per project let cold collections stay in object storage without sharing a large partition with active work.

  • Direct object-storage writes accept hundreds of milliseconds of latency. Responsive reads require indexes and query plans that do substantial work within a few storage round trips.

  • Customer encryption requirements can include SSD caches. Disabling the disk cache performed well enough for some Legora workloads, avoiding the need to implement encrypted caching first.

  • Legal research fans out because jurisdictional hierarchy, temporal validity and regulatory exceptions require additional retrieval. The application must resolve those relationships beyond finding similar passages.

  • Cluster trees let frequently consulted vector-routing information stay in DRAM while larger leaf data occupies cheaper tiers. Full-text search also needs to control posting-list transfers and memory-bandwidth costs.

  • The economic fit depends on a long tail of cold collections and tolerance for occasional cold-read delays. For Legora, that fit reduced infrastructure work and freed engineering time for the product.

Two kinds of legal search

A lawyer reviewing an acquisition needs to search the documents for that particular deal. A lawyer researching a dispute may need to search laws, cases and regulations, then follow relationships between them. Legora serves both workloads, alongside contract review, drafting and collaboration. Jacob Lauritzen, a Legora engineer, introduces the application; Simon Eskildsen, turbopuffer’s CEO and co-founder, explains the search engine underneath it.

  • Project search: Lauritzen imagines SpaceX acquiring Cursor. The law firm handling this hypothetical deal uploads employment agreements and supplier contracts into one project, then searches within that collection. Projects range from tens of documents to millions, but the scope stays clear: this matter’s documents.

  • Legal research: The collection spans laws, previous cases and regulations. Lawyers use it to answer legal questions or support litigation. Finding relevant material also means understanding which sources apply and how they relate.

The project is already the application’s unit of work. The first part of the migration is about making it a useful unit of storage, too. That matters because a project can be intensely active for a while, then close and almost never receive another query.

0:130:26
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

0:13 · section reference included

Residency and enterprise isolation multiply the infrastructure

Legora’s project-search journey runs from hundreds of thousands of documents to two billion. Initially, shared blob storage held the raw documents and one Elasticsearch cluster indexed and searched them for every tenant. Lauritzen says the arrangement worked reasonably well: it was simple, and the team had one search deployment to operate.

Customers in different regions wanted their processing to stay local. Legora reproduced its setup across the EU, US and Asia Pacific to meet requirements from European, American and Australian customers. The design still worked, but each regional copy brought more infrastructure to run. Residency changed the deployment topology before document volume forced a different search architecture.

Selected presentation frame from Connect AI to Billions of Legal Documents — Simon Eskildsen, turbopuffer & Jacob Lauritzen, Legora at 225 secondsOpen full source frame
A simple cluster becomes several systems

Large banks and law firms added two further requirements:

  • Physical isolation: Customers wanted their data separated, which Lauritzen describes here as effectively asking for their own database. He acknowledges that customers can mean different things by physical isolation.

  • Customer-managed encryption keys: A customer keeps an encryption key in its key vault and grants Legora access. Legora uses the key to encrypt and decrypt data at rest. Revoking access prevents Legora from using that key for further decryption, giving the customer control over access to the encrypted stored data.

These demands made a shared search cluster harder to fit into the rest of the system. Legora already had to separate its transactional databases and blob storage, so the next move tried to consolidate search into those existing databases.

2:363:07
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

2:36 · section reference included

Why 4,000 partitions still thrashed the cache

Moving search into Postgres had a practical appeal. Legora already ran it for transactional workloads and already needed multiple database and blob-storage arrangements. Adding vector and text search meant fewer kinds of systems to operate. The setup used pgvector for vector search and tsvector for text search. Text ranking no longer used BM25, and Lauritzen reports a loss in retrieval quality.

Hashing projects separated keys, but mixed their lifecycles

The team split its document-chunk table into roughly 4,000 partitions, hashed each project key and packed projects into those partitions. This spread projects across the database without keeping each project independently stored. Active projects and abandoned projects could land together. As the corpus grew, the partitions accumulated large amounts of data that almost never needed searching.

Selected presentation frame from Connect AI to Billions of Legal Documents — Simon Eskildsen, turbopuffer & Jacob Lauritzen, Legora at 332 secondsOpen full source frame
Why 4,000 partitions still thrashed the cache

Lauritzen describes a query bringing partition data into memory, followed by another query loading a different partition and displacing the previous data. The cache kept making room for large collections containing both useful active data and inactive data. Repeated queries therefore failed to benefit from a stable working set. Search and ingestion P99 latency rose from about 100 milliseconds to 20 seconds.

The failure was specific to the packing arrangement and access pattern. Increasing the partition count had distributed the data, but a lawyer’s project remained only part of a larger storage collection. A cold project could still compete for cache space whenever an active neighbor was searched.

One namespace per project

Legora moved to turbopuffer at approximately 400 million documents, a size Lauritzen gives as an estimate. Each project received its own namespace. An unused project could remain in blob storage while an active project was cached and searched independently. The storage unit now followed the application’s unit of work.

The migration also restored BM25, improved relevance and latency, lowered cost and let Legora operate a single turbopuffer cluster instead of its collection of separate search databases. These benefits arrived together with the new storage layout; they do not isolate the effect of any one indexing algorithm. The clear structural improvement was that inactive projects no longer had to share large partitions with active ones.

What a project query brings into the working set

Active and inactive projectsPostgres layout — Project keys hash into shared, growing partitions.
Partition data in memoryPostgres layout — Queries against other partitions displace the cached data.
Active project namespaceturbopuffer layout — The project is an independent search collection.
Cached project dataturbopuffer layout — Queries check caches for the requested namespace.
Inactive project namespaceturbopuffer layout — Unused projects remain in blob storage.
Hash-packed partitions coupled active projects to inactive neighbors. Project namespaces let inactive collections remain in object storage.
Read the diagram as text
  • Active and inactive projects. Postgres layout — Project keys hash into shared, growing partitions.
  • Partition data in memory. Postgres layout — Queries against other partitions displace the cached data.
  • Active project namespace. turbopuffer layout — The project is an independent search collection.
  • Cached project data. turbopuffer layout — Queries check caches for the requested namespace.
  • Inactive project namespace. turbopuffer layout — Unused projects remain in blob storage.
  • Active and inactive projects → Partition data in memory: Load shared partition data.
  • Active project namespace → Cached project data: Search this namespace.
4:485:18
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

4:48 · section reference included

Direct object-storage writes make round trips the main read constraint

Eskildsen begins with turbopuffer’s fundamental tradeoff: writes go directly to object storage, using S3 in his example. Turbopuffer does not first run its own disk-replication or Paxos path. Writes therefore take hundreds of milliseconds. He uses inventory reservations for a Kylie Jenner flash sale on Shopify as an example of a workload that would be a poor fit. Search ingestion can often accept that write delay if reads remain responsive.

A write enters a write-ahead log in object storage. Background workers build vector, text and columnar indexes to answer subsequent queries. Eskildsen pictures the log as successive files—one.json, two.json, three.json—while explicitly saying that JSON is an illustration. The useful model is a stored sequence of writes from which searchable structures are built.

Selected presentation frame from Connect AI to Billions of Legal Documents — Simon Eskildsen, turbopuffer & Jacob Lauritzen, Legora at 491 secondsOpen full source frame
Write to object storage, then make reads economical

Route toward cached data, then descend the hierarchy

A namespace acts much like a table; Eskildsen compares it to an isolated directory on S3. Any read-replica node can serve its queries, but routing favors the node most likely to have its data cached. The node checks memory, then an NVMe SSD cache, then object storage. This affinity encourages reuse without making one node the sole owner capable of answering.

For cold data, the costly part is waiting through dependent storage requests. Eskildsen cites an S3 P99 of around 200 milliseconds for a one-megabyte blob and describes roughly three round trips as an ideal target. If each fetch must finish before the next can begin, those waits accumulate. Turbopuffer instead tries to do as much work as possible concurrently within a few rounds. The same design helps use modern disks efficiently.

That requirement reaches into the indexes and query planner: they must make useful progress with the data fetched in each round. Eskildsen’s architecture article develops the same tradeoff between downloading more data per round and making additional requests. Object storage supplies cheap capacity; the search engine has to organize its work around the cost of reaching it.

Separate the write path from the cache-aware read path

Incoming writeWrite path — Accept hundreds of milliseconds of write latency.
Object-storage write-ahead logWrite path — Store the sequence of writes.
Background index constructionWrite path — Build vector, text and columnar indexes.
Namespace queryRead path — Favor the replica likely to hold cached data.
Memory cacheRead path — First place to look for requested objects.
NVMe SSD cacheRead path — Next tier when enabled for the workload.
Object storageRead path — Fetch cold objects while limiting dependent round trips.
Writes enter object storage directly. Reads favor a node with cached namespace data and fall through storage tiers on misses.
Read the diagram as text
  • Incoming write. Write path — Accept hundreds of milliseconds of write latency.
  • Object-storage write-ahead log. Write path — Store the sequence of writes.
  • Background index construction. Write path — Build vector, text and columnar indexes.
  • Namespace query. Read path — Favor the replica likely to hold cached data.
  • Memory cache. Read path — First place to look for requested objects.
  • NVMe SSD cache. Read path — Next tier when enabled for the workload.
  • Object storage. Read path — Fetch cold objects while limiting dependent round trips.
  • Incoming write → Object-storage write-ahead log: Write directly.
  • Object-storage write-ahead log → Background index construction: Build in the background.
  • Namespace query → Memory cache: Check cached objects.
  • Memory cache → NVMe SSD cache: On a memory miss.
  • NVMe SSD cache → Object storage: On an SSD miss.
  • Memory cache → Object storage: When SSD caching is disabled.
7:287:58
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

7:28 · section reference included

Per-namespace encryption includes the cache question

The namespace also gives turbopuffer a natural place to apply customer-specific storage and encryption settings:

  • Separate keys: Each namespace can use a different encryption key, and data can be re-encrypted with another key.

  • Separate buckets: Namespaces can live in different buckets, including buckets in customers’ own cloud accounts. Eskildsen describes customers using thousands of buckets.

  • Shared storage where permitted: Namespaces can also share a bucket. The engine can change storage arrangements at namespace granularity rather than requiring a whole search deployment for each collection.

Legora needed its namespaces separately encrypted at rest. S3, GCS and Azure Blob Storage could meet the storage requirement, but the NVMe SSD cache created a disagreement. Turbopuffer treated that cache as volatile, like memory. Legora’s customers treated the SSD contents as data stored at rest. The word cache did not exempt those bytes from their encryption requirements.

Selected presentation frame from Connect AI to Billions of Legal Documents — Simon Eskildsen, turbopuffer & Jacob Lauritzen, Legora at 660 secondsOpen full source frame
The namespace also becomes the unit of encryption

Disable first, then measure

The team expected to implement encryption in the disk cache. Before doing that work, it disabled the cache and measured performance. Memory caching plus object storage performed well enough that some Legora workloads kept the SSD cache disabled. This was a useful simplification for those workloads; it does not establish that SSD caching is unnecessary in general. Encrypted disk-cache support remained future work in Eskildsen’s account.

Returning to project-search results, Lauritzen reports approximately an order-of-magnitude improvement in median latency and a larger improvement at P99. He gives no resulting absolute latency values in this explanation. The gains matter across an agent’s work: an agent making 20 or 100 searches repeatedly encounters retrieval delay, so search performance affects how much investigation it can complete within a useful response time.

9:4210:12
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

9:42 · section reference included

Clustered vector trees put routing information in fast memory

Eskildsen describes turbopuffer as puffing data into and out of the memory hierarchy. Frequently used data can justify DRAM, SSDs offer another balance of capacity and latency, and cold data stays in object storage. The goal is to keep as much data as possible in cheaper tiers while still meeting performance needs. Each tier changes the economics of random access, sequential access and waiting for a round trip.

Selected presentation frame from Connect AI to Billions of Legal Documents — Simon Eskildsen, turbopuffer & Jacob Lauritzen, Legora at 908 secondsOpen full source frame
Why clustered vectors suit the storage hierarchy

Graph navigation creates dependent waits

For vector search, he contrasts navigating a graph with organizing vectors into clusters. In the graph example, the engine visits a node to discover where to go next. If each step requires an uncached object-storage request, the query can repeatedly encounter the roughly 200-millisecond S3 P99 he cited earlier. Shrinking the graph’s diameter helps reduce steps, but the access pattern still creates dependencies between fetches. A layout effective in memory can become expensive farther down the storage hierarchy.

Turbopuffer instead groups nearby vectors into clusters, groups those clusters into larger clusters, and repeats this to form a tree. Eskildsen likens it to a complicated B-tree over the geometry of the vector space. The comparison describes the hierarchy: approximate geometric clustering lets a query narrow down which parts of the collection to inspect.

The tree separates two kinds of data with different access frequencies:

  • Upper-level centroids: Searches repeatedly consult these cluster representatives to choose a path through the index. Their frequent use makes them good candidates for DRAM.

  • Leaf data: The much larger collection of underlying records need not all occupy DRAM. Eskildsen describes leaves on SSDs with an illustrative one-millisecond final round trip. Colder objects can remain in object storage.

The index’s shape therefore helps determine what stays cached. Compact routing information is useful across many searches, while any particular leaf is needed less often. Keeping them in different tiers saves fast-memory capacity without forcing every query to discover its route through a succession of cold reads.

Eskildsen calls this the cheapest way to run a database and extends the argument to customers indexing large portions of the web. That is his architectural judgment, rather than a universal result established by a comparative benchmark here. The mechanism is more specific and useful: even within one searchable collection, routing structures and underlying records have different access patterns and can justify different storage tiers.

Follow a vector query through cached cluster representatives

Upper-level centroidsRouting information — Frequently consulted cluster representatives, mostly in DRAM.
Clusters within clustersRouting information — Narrow the approximate geometric search through the tree.
Selected leaf dataUnderlying collection — Larger underlying records can remain on SSDs rather than all occupying DRAM.
The tree repeatedly narrows the vector space using upper-level centroids, then reaches the selected leaf data.
Read the diagram as text
  • Upper-level centroids. Routing information — Frequently consulted cluster representatives, mostly in DRAM.
  • Clusters within clusters. Routing information — Narrow the approximate geometric search through the tree.
  • Selected leaf data. Underlying collection — Larger underlying records can remain on SSDs rather than all occupying DRAM.
  • Upper-level centroids → Clusters within clusters: Choose relevant clusters.
  • Clusters within clusters → Selected leaf data: Reach the data to inspect.
14:2014:50
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

14:20 · section reference included

Full-text search must fetch, combine and score enormous lists

Eskildsen closes the storage explanation with a simplified model of full-text search: a map from each token to the document IDs containing it. For New York population, the engine looks up three entries and combines their document-ID sets. It also ranks candidates. A rarer term such as York can carry more ranking information than a common term such as New; term rarity is the aspect of BM25 that he uses to explain the scoring.

Two costs: storage waits and local list processing

The engine first fetches the relevant portions of the term dictionary, possibly after an additional lookup to locate them. It then fetches the document-ID lists, also called posting lists. The stages limit how much work can happen before the previous information arrives. Once those lists reach the node, combining and scoring them creates another cost: reading and processing large amounts of data through memory.

  • Compress the lists: Smaller representations reduce the amount of posting-list data fetched from storage.

  • Reduce dependent requests: Organizing dictionary and list access into a few rounds limits remote-storage waits.

  • Avoid weak candidates: Once the engine has enough high-scoring documents matching population and York, documents matching only New may no longer affect the result. Scoring can therefore help avoid unnecessary list processing.

Selected presentation frame from Connect AI to Billions of Legal Documents — Simon Eskildsen, turbopuffer & Jacob Lauritzen, Legora at 1110 secondsOpen full source frame
Full-text search still has to move enormous lists

The last example gives the intuition for pruning work, without specifying the algorithm used to prove that a candidate cannot enter the results. It also makes Eskildsen’s closing comparison less surprising: he says full-text search at web scale is more difficult and computationally expensive than vector search. A familiar keyword query can involve enormous document-ID lists, storage transfers and memory-bandwidth demands.

The product payoff depends on the workload fit

Lauritzen returns to why this matters for Legora. Retrieval is central to legal reasoning, but running a separate Elasticsearch database for every enterprise tenant would create substantial operational work. Namespace-level storage and encryption let the team accommodate residency and customer-key requirements without multiplying independent search deployments in the same way.

The cost advantage follows a particular shape: many independent collections, a few active ones, and a long tail of cold indexes whose occasional fetch delay is acceptable. Slower ingestion is also a reasonable tradeoff for these search workloads. Projects and jurisdictions give the engine useful units to cache, store and encrypt independently.

Lauritzen ends with the engineering consequence: the team can spend more time improving Legora and less time working on scalability and infrastructure. Its CFO is pleased with the cost, too. Making storage follow how legal work actually arrives and goes idle gave the team room to focus on the application.

17:1917:50
Suggest correction

This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.

17:19 · section reference included

Resources

  • Eskildsen’s companion explanation of object-storage-first search, cache locality and the tradeoff between data fetched per round and additional storage requests.

  • Performance numbers and estimation techniques for reasoning about storage latency, throughput and memory costs. Its rounded figures are guides for estimation, rather than guarantees for a workload.

  • Agents need more than a chat

    Extends the legal-work discussion from retrieval infrastructure to collaboration interfaces, task verifiability and controllable human-agent workflows.

Transcript download
Read the transcript and explore slides

Loading transcript and slides…