What Is a Postings List in Elasticsearch?
A complete, plain-English guide to the humble list that makes full-text search fast: what is stored in it, how it is built and squeezed onto disk, how searches walk through it, and why its size and shape decide the speed and cost of your whole cluster.
Open a thick textbook and turn to the index at the very back. Next to the word “volcano” you find a short list of page numbers: 45, 78, 132. You do not read the whole book to find where volcanoes are discussed. You jump straight to those three pages. Now imagine a book with a hundred million pages, and an index that lists, for every word ever printed, every page where it appears. That index is what makes it possible to search the book in a blink.
Elasticsearch works exactly like that book. Behind every full-text search sits an index, and inside that index, for every word, sits a list of the documents that contain it. That list is called a postings list. It is one of the oldest and most important ideas in search, and it is the reason Elasticsearch can find a word among billions of documents in milliseconds. This guide starts from zero, opens the list to see what is inside, follows it onto the disk, watches searches walk through it, and ends with the numbers, the trade-offs, and the habits that keep it small and fast in production.
1What a Postings List Actually Is
Three plain ideas, put together: a term, a document, and a list. Once each is clear, the rest follows naturally.
Elasticsearch stores documents, such as product descriptions, news articles, or log lines. When a text document arrives, Elasticsearch runs it through an analyzer, which cuts the text into small pieces called terms. Roughly speaking, a term is a word that has been cleaned up: lowercased, stripped of punctuation, and sometimes reduced to its root form. The sentence “The Quick Brown Fox” might become the terms “the”, “quick”, “brown”, and “fox”.
For every distinct term, the index keeps a list of the documents in which that term occurs. That list is the postings list of the term. Each entry in the list is called a posting. At its simplest, a posting is just the number of a document. In richer forms, a posting also says how many times the term occurs in that document, and at which positions.
The collection of all terms, each with its postings list, is called an inverted index. It is “inverted” because a normal document is a list of words, while this structure turns things around: it is a list of words, each pointing back to documents. When you search for “fox”, Elasticsearch does not scan documents. It looks up the term “fox” in the sorted dictionary of terms, follows the pointer to its postings list, and reads off the documents.
Think of the card catalog of an old library. There is a drawer for each subject word, and inside each drawer, cards pointing to the shelves where matching books can be found. Nobody walks past all the shelves looking for books about volcanoes. They open the “volcano” drawer and read the cards. The drawer is the term, and the stack of cards is the postings list.
What it is not
A postings list is not the document itself. The original text lives in a different structure, the stored fields, from which Elasticsearch builds the source you see in results. It is not the structure used for sorting and grouping either, which is a column-style structure called doc values. And it is not the structure used for numbers and dates, which are stored in a special tree for fast range checks. The postings list has one job: to answer, quickly, which documents contain a given term. Chapter 8 shows how these structures sit side by side.
Elasticsearch does not build postings lists by itself. It relies on Lucene, the open-source search library that powers every shard. Each shard is a Lucene index, and the postings lists described in this guide are Lucene’s. That is why the names, the file formats, and the details come from Lucene, and why they change when Lucene changes.
The one-sentence version
If you remember one line from this chapter, make it this: a postings list is the sorted list of documents, inside one segment, that contain a particular term, optionally carrying how often and where the term appears, and it is the data structure that makes full-text search fast.
2Why It Exists: Finding Words Without Reading Everything
Every data structure is born from a painful problem. The problem here is that reading everything is far too slow.
Suppose you keep a hundred million documents, and a user searches for the word “volcano”. The simplest approach is to open every document and check whether it contains that word. This is called a full scan. If each document takes a microsecond to check, the scan takes a hundred seconds. And that is for one word, for one user, on one machine. With thousands of users searching at once, the approach collapses.
The inverted index changes the arithmetic. Instead of checking every document, it goes straight to the word. Finding the word in the sorted dictionary takes a few steps, because the dictionary is organized so that a lookup skips most of it. Reading the postings list costs time only in proportion to the number of documents that match, not the number that exist. If only a few thousand documents mention “volcano”, then only a few thousand entries are read. The cost is tied to the answer, not to the size of the collection.
Finding a name in a phone book by reading every entry would take all day. Finding it by jumping to the right letter, then the right page, takes seconds. The postings list is the extra step that says, once you have found the word, exactly which pages to visit, and no others.
A short history
The classic idea
Inverted indexes are decades older than Elasticsearch. They were the core of early information retrieval systems, and the idea of a dictionary of terms pointing to lists of documents has barely changed.
Lucene brings it to open source
Lucene made a fast, compact inverted index available to everyone, with sorted document numbers and compression, and it became the engine underneath many search products, including Elasticsearch.
Block-based compression
Later versions of Lucene moved to storing postings in fixed-size blocks, packed tightly with bit-level compression, which shrank indexes and sped up reading.
Better scoring and skipping
The default scoring formula was changed to a more modern one, and, around Elasticsearch 7, a technique that skips whole blocks of postings that cannot reach the top results made large searches much cheaper.
Today: a tuned, mature structure
The postings format has been refined in many steps. Modern Elasticsearch users mostly tune it by choosing what to index and how, not by touching the format.
The exact release for each step is worth double-checking in the release notes of your version. The direction of travel is what matters: the idea stayed the same, while the encoding became smaller and the reading became smarter.
Two ways to organize the same information
To see why the inversion matters, compare two views of the same three short documents. In the forward view, each document lists its words. In the inverted view, each word lists its documents. If you want to know “which documents contain brown?”, the forward view forces you to read every document. The inverted view gives you the list at once. The forward view is good for showing a whole document, and the inverted view is good for finding documents. Search needs the second, and Elasticsearch stores both, each in its own structure.
| Question | Forward view (document to words) | Inverted view (word to documents) |
|---|---|---|
| Which documents contain “fox”? | Read every document and check | Read one list |
| What words are in document 2? | Read document 2 | Check every list for document 2 |
| How many documents contain “brown”? | Read every document | Read the size of one list |
| Which documents contain both “quick” and “fox”? | Read every document | Compare two short lists |
Why not a database index on the text?
An ordinary database index on a text column stores the whole value in a sorted tree. It answers “find rows where the column equals this exact value” very well, but it cannot answer “find rows where the text contains this word” without scanning. Full-text search needs an index built from the words inside the text, and that is exactly what postings lists provide.
3Core Concepts You Need First
A dozen words explain nearly everything about postings lists. Learn them here, and later chapters will read like common sense.
Token and term
Analysis cuts text into tokens. A token, once cleaned and stored in the index, is called a term.
Analyzer
The machine that turns raw text into terms: it splits, lowercases, removes noise, and may reduce words to roots.
Term dictionary
The sorted collection of all distinct terms of a field in a segment, each pointing to its postings list.
Postings list
For one term, the sorted list of documents that contain it, with optional extra details.
Document ID
A small whole number that names a document inside one segment. It is not the same as the document’s _id.
Term frequency
How many times a term occurs in one document. It appears in the postings and feeds into scoring.
Document frequency
How many documents contain the term. It is the length of the postings list and feeds into scoring.
Position
The place of a term within a document’s text, counted in terms. Positions make phrase searches possible.
Offsets and payloads
Optional extras: start and end character positions, used for highlighting, and small custom data attached to a position.
Norms
A compact per-document number that records field length, so that matches in short fields can score higher.
Postings iterator
The object a search uses to walk a postings list, one document at a time, or to jump ahead to a target.
Skip data
Extra markers stored with a long list so that a reader can jump over stretches of it without reading them.
The unit of storage: the segment
Every shard is made of segments, small sealed sets of files that are never changed after they are written. Postings lists live inside a segment. This means that a term such as “fox” has not one postings list per shard but one per segment, each listing only the documents of that segment. When new documents arrive, they land in a new segment with its own fresh lists. When segments are merged, their lists are combined into new ones. The whole life of a postings list is tied to the life of its segment.
An encyclopedia published in several volumes has an index at the end of each volume. To find “volcano,” you look in every volume’s index, and each one lists only the pages of its own volume. When the publisher reprints the volumes as one big volume, a new combined index is made.
Document IDs are local and small
Inside a segment, documents are numbered from zero upward, in the order they were added. These numbers are what postings lists contain. They are small whole numbers, because a segment holds at most a couple of billion documents, and typically far fewer. They are local: document number 7 in one segment has nothing to do with document number 7 in another. They are also temporary in a sense: when segments are merged, the surviving documents are renumbered. The identifier you see in results, the _id, is a different, permanent thing, stored separately.
Frequency versus position
These two details are often confused. Term frequency answers “how many times does the term appear in this document?” Position answers “where, exactly, does it appear?” Frequency is a single small number per document. Positions are a list of numbers per document, and therefore cost far more space. Frequency helps scoring. Positions help phrase and proximity searches. Whether they are stored is a choice you make in the mapping.
The document number inside a postings list is not the value of the _id field. Do not expect the two to match, and do not expect them to stay the same after merges. The internal numbers are an implementation detail that Elasticsearch never asks you to use.
Quick glossary
| Word | Plain meaning |
|---|---|
| Inverted index | The whole structure: terms, each pointing to its postings list. |
| Posting | One entry of a postings list, describing one document for one term. |
| Field | A named part of a document, such as title or body. Each field has its own terms and lists. |
| Cardinality | The number of distinct values in a field. High cardinality means many terms. |
| Stop word | A very common word, such as “the,” sometimes removed by an analyzer. |
| Stemming | Reducing words to a root, so that “running” and “runs” become the same term. |
| Analysis chain | The steps applied to text, in order, to produce terms. |
| FST | A compact in-memory structure used to find terms in the dictionary quickly. |
4Architecture and Components
Where do postings lists come from, where do they live, and which parts of Elasticsearch and Lucene work together to build and use them?
A document that arrives at Elasticsearch travels through several stages before its words end up in postings lists. The analyzer cuts its text into terms. The indexing machinery of Lucene collects those terms in memory, building lists as it goes. When it is time to write, the collected data is sorted and saved as a new segment. From then on, the segment holds a term dictionary, the postings lists, and several neighbor structures, all sealed and read-only.
flowchart TB
Doc["Document arrives"] --> An["Analyzer: splits text into tokens and cleans them"]
An --> IC["Indexing chain: builds postings in memory"]
IC --> Ref["Refresh or flush writes a new segment"]
subgraph Seg["One segment on disk"]
TD["Term dictionary: sorted terms with statistics"]
PL["Postings lists: doc ids, frequencies, positions"]
NR["Norms: field length information"]
DV["Doc values and stored fields: other structures"]
end
Ref --> Seg
TD -->|points to| PL
Read the picture from top to bottom. The analyzer is the front door: it decides what the terms are, and therefore what postings lists will exist. The indexing chain gathers terms and grows lists in memory. A refresh, or a flush when the memory buffer is full, turns the gathered data into a segment. Inside the segment, the term dictionary is a sorted directory of terms, and each entry points to a postings list. Norms, doc values, and stored fields sit beside them, serving other purposes.
The main components
Analyzer
Turns text into terms. Its choices, such as lowercasing, stemming, and stop words, decide the vocabulary of the index.
Indexing chain
Gathers terms in memory for each field and builds the lists before they are written.
Term dictionary
A sorted directory of all terms in a field, kept in blocks, with a compact index into it held in memory.
Postings files
Files holding the lists themselves: document numbers with frequencies, positions, and optional offsets.
Norms
A small number per document and field, recording length information used in scoring.
Postings iterators
The readers that walk lists during a search, with the ability to jump ahead.
Query and scorer classes
The machinery that combines several lists, such as requiring several terms at once, and assigns scores.
Merge policy
Decides when to combine segments, which rewrites and combines their postings lists.
One set of lists per field
Terms are kept separately for each field. The word “apple” in a title field and the word “apple” in a description field are two different entries in two different dictionaries, each with its own postings list. This is why a query names a field, and why adding more fields to your mapping increases the number of lists, and the size of the index.
Imagine a newspaper with separate indexes for headlines and for the body text. The word “election” in the headline index points to the front pages that carry it in a headline, while the same word in the body index points to every article mentioning it. Two indexes, two lists, two purposes.
Walking one search through the machine
The query text is analyzed
The user’s words go through an analyzer, usually the same kind used at indexing time, to produce the terms to look for.
Each shard searches its segments
Every segment is searched separately, because each has its own dictionary and lists.
The term is looked up in the dictionary
The in-memory index leads to the right block of the dictionary, where the term and its statistics are found.
A postings iterator is opened
The pointer stored with the term tells the reader where the list begins on disk.
Lists are combined
If several terms are involved, their lists are intersected or merged according to the query.
Matches are scored and collected
The best documents are kept, and results from all segments and shards are merged into a final list.
Because a search visits every segment, the number of segments, the size of the lists in each, and the number of terms a query expands to all multiply the work. Chapters 11 and 13 return to this.
5Anatomy of a Postings List
Now we open the list and look at each part. A small example makes it concrete.
Take three tiny documents. Document 0 says “quick brown fox”. Document 1 says “lazy brown dog”. Document 2 says “quick brown cat”. After analysis, the index holds the terms shown in the picture below, each pointing to its list of documents.
flowchart LR
T1["Term: brown"] --> P1["Postings: doc 0, doc 1, doc 2"]
T2["Term: quick"] --> P2["Postings: doc 0, doc 2"]
T3["Term: fox"] --> P3["Postings: doc 0"]
T4["Term: dog"] --> P4["Postings: doc 1"]
T5["Term: lazy"] --> P5["Postings: doc 1"]
T6["Term: cat"] --> P6["Postings: doc 2"]
The term dictionary keeps terms in alphabetical order: brown, cat, dog, fox, lazy, quick. The list for “brown” has three postings, because the word appears in all three documents. The list for “fox” has one. The number of postings in a list is the term’s document frequency. A search for “quick brown” needs the lists for “quick” and “brown”, and asks which documents appear in both: documents 0 and 2.
What one posting can contain
In its simplest form, a posting is only a document number. Depending on how the field is mapped, it can carry more.
| Level | Each posting holds | Enables | Costs |
|---|---|---|---|
| Documents only | The document number | Finding which documents contain the term | Smallest |
| Plus frequencies | Document number and how many times the term occurs | Scoring that rewards repetition | Small |
| Plus positions | Document number, frequency, and each position | Phrase searches and proximity searches | Much larger |
| Plus offsets | Positions and the start and end characters of each occurrence | Fast highlighting from the index | Largest |
A book index can list only page numbers, or it can say how many times the word appears on each page, or it can point to the exact line. The more detail it gives, the longer the index, and the more useful it becomes for certain questions.
A richer example with positions
Now consider a document that says “the quick brown fox jumps over the lazy dog”. Counting from zero, the term “the” is at positions 0 and 6, “quick” at position 1, “brown” at position 2, “fox” at position 3, and so on. If the field stores positions, the posting for “the” in this document says: term frequency two, positions 0 and 6. A phrase search for “quick brown” finds documents that contain both terms and then checks that the position of “brown” is exactly one more than the position of “quick”. Here, positions turn a simple “both words appear” into “the words appear next to each other, in order.”
Statistics kept with each term
Besides the list, the dictionary entry for each term stores a few numbers. The document frequency is the number of documents in the list. The total term frequency is the sum of the term frequencies across all documents, meaning how many times the term occurs in the whole segment. These numbers are cheap to read, and they feed scoring: a term that appears in nearly every document says little about relevance, while a rare term says a lot.
The shape of real lists
In real text, the sizes of lists are very unequal. A tiny number of words, such as “the” and “and”, occur in nearly every document, so their lists are enormous. A great many words occur in only one or two documents, so their lists are tiny. This pattern, sometimes called Zipf’s law, is one of the most important facts about postings lists. It explains why some queries are cheap and others are costly, and why removing or handling very common words matters.
Common terms
Lists of millions of postings. Reading them fully is expensive, so searches try to avoid it.
Rare terms
Lists of a few postings. Cheap to read, and very informative for scoring.
One-document terms
Very numerous. Lucene stores the single document number directly with the term, without a separate list.
Sorted order is the secret
The document numbers in each list are always in increasing order. This is not an accident. It makes two very important things possible. First, numbers can be stored as small differences from one to the next, which compresses well, as Chapter 7 shows. Second, two lists can be combined by walking them side by side, like zipping two sorted rows, instead of comparing every pair. Nearly every clever thing that Lucene does with postings depends on this one rule.
The exact layout of postings on disk is an internal detail of Lucene and changes between versions. This guide describes the ideas, which have stayed stable for many years, and names the details only in general terms. Do not build tools that depend on the internal file layout.
6How Postings Lists Are Built
Postings lists are not typed by anyone. They grow, in memory, as documents flow through the analyzer. Here is how.
flowchart LR
A["Tokens from analysis"] --> B["In-memory table of terms with growing lists"]
B --> C{"Buffer full or refresh due?"}
C -- No --> B
C -- Yes --> D["Sort the terms alphabetically"]
D --> E["Write term dictionary and postings files"]
E --> F["New immutable segment, ready to search"]
Step one: analysis
The journey starts with the analyzer. It cuts the text into tokens, and each token receives a position, a counter that goes up by one for each token. Filters then transform the tokens: they lowercase them, remove stop words, reduce words to stems, or add synonyms. Whatever comes out at the end of this chain is the set of terms that will get postings. The analyzer is therefore the most powerful control you have over the shape of the index, because it decides what the vocabulary is.
Think of a librarian who prepares to index a book. She decides that “Volcano,” “volcanoes,” and “volcanic” should all be filed under one heading, that tiny words like “the” are not worth listing, and that capital letters do not matter. These decisions happen before a single page number is written down, and they decide what the final index looks like.
Step two: growing lists in memory
As tokens arrive, Lucene keeps, for each field, an in-memory table from terms to growing lists. When a term is seen for the first time in a document, a new entry is created, and the document number is added to its list. When the same term appears again in the same document, its frequency is increased and the new position is added. When the next document arrives, and contains the same term, its number is appended. Because documents are numbered in the order they arrive, each list is naturally in increasing order without any extra sorting.
Step three: writing a segment
When the memory buffer fills up, or when a refresh happens, Lucene writes the collected data to disk as a new segment. At this point, it sorts the terms alphabetically, writes the term dictionary in sorted order, and writes each term’s list, in compressed form, to the postings files. The segment is then sealed. From this moment, its lists are read-only.
A document arrives
It receives the next document number in the current in-memory segment.
Each field is analyzed
Text fields produce many terms. Keyword fields usually produce a single term.
Lists are extended
Each term’s list in memory gets one more posting, with its frequency and, if stored, positions.
The buffer fills or a refresh is due
The shard decides that it is time to write out the collected data.
Terms are sorted and written
The dictionary and the lists are compressed and saved.
The segment becomes searchable
A new, small, sealed segment joins the shard, with its own complete inverted index.
How positions are counted
Positions are more subtle than they look. Each token normally advances the position by one. But some filters change this. If a stop word is removed, the analyzer still leaves a gap where it was, so that the positions of the remaining words stay true to the original text. Synonyms are placed at the same position as the word they stand for, so that a phrase search works with either word. And when a field holds several values, such as an array of strings, Elasticsearch inserts a gap of a hundred positions between the values by default, so that a phrase search does not accidentally match words from the end of one value and the start of the next.
Lowercasing
Merges “Fox” and “fox” into one term, making lists longer and the dictionary smaller.
Stop words
Removes very common words. This deletes the longest lists, but breaks searches that need those words.
Stemming
Reduces related words to one root, shrinking the dictionary and merging their lists.
Synonyms
Adds extra terms at the same position. At indexing time, this enlarges the index, and it makes changes hard.
N-grams
Cuts each word into many overlapping pieces. It supports partial matching, but multiplies the number of terms and postings.
Whole-value terms
Keyword fields keep the whole value as one term, with a tiny list per distinct value.
The cost of building
Building postings is real work. Analysis uses processor time, growing lists use memory, sorting and compressing use more processor time, and writing uses disk. This is why heavy text fields make indexing slower than light ones, and why choices such as storing positions, or applying n-grams, show up directly in indexing throughput and in the size of the index.
See what the analyzer produces
Elasticsearch has an analysis endpoint that shows the terms and positions that a piece of text would produce under a given analyzer. It is the best way to understand why a document does or does not match a query, because the match happens between the terms of the query and the terms in the postings, not between the raw words.
When a search does not find a document you expect, do not stare at the document. Analyze the text of the document and the text of the query with the same analyzer, and compare the terms. Most surprises turn out to be a mismatch between what was indexed and what was searched.
7How Postings Are Stored on Disk
A postings list can have millions of entries. Storing it compactly, and reading it quickly, is where much of Lucene’s cleverness lies.
flowchart TD
Q["Look up a term"] --> FST["Term index in memory: finds the right block of the term dictionary"]
FST --> TB["Term dictionary block on disk: finds the term and its statistics"]
TB --> PP["Pointers to postings, positions, and skip data"]
PP --> DB["Document number blocks of 128, packed with few bits each"]
PP --> SK["Skip data: jump ahead over blocks"]
PP --> PS["Positions read only if the query needs them"]
Finding the term
The term dictionary of a segment is stored in sorted order, in blocks of terms that share a common beginning. A small index on top of the blocks, held in a very compact structure called a finite state transducer, lets Lucene find the right block for a given term in a handful of steps, without reading the whole dictionary. Once the block is read from disk, the term itself is found, and with it, its statistics and its pointers into the postings files. This design keeps the part that must be in memory small, even when the number of terms is huge.
Finding a word in a big dictionary is done in two steps. First you use the thumb tabs on the side to jump to the right letter section, which is the small in-memory index. Then you scan a page or two to find the exact word, which is the block on disk. You never read the whole dictionary.
Squeezing the document numbers
The first trick is delta encoding. Because the numbers in a list are sorted, each can be stored as the difference from the previous one, instead of as a whole number. A list of document numbers 3, 7, 8, 15, 20 becomes the differences 3, 4, 1, 7, 5. The differences are small numbers, and small numbers need few bits.
Instead of writing down the full house numbers of a long street, 1203, 1207, 1208, 1215, 1220, you could write the first one and then the gaps: 4, 1, 7, 5. The gaps are shorter to write and easier to remember.
Blocks and bit-packing
The second trick is to store the list in blocks of a fixed size, 128 postings in current formats. For each block, Lucene finds the largest difference in the block and works out how many bits are needed to write the biggest one. Then every difference in that block is written using exactly that many bits, packed tightly next to each other. If the biggest gap in a block is 30, five bits are enough for each entry, so 128 entries take 80 bytes, instead of hundreds of bytes in plain form. Fixed-size, bit-packed blocks are also very fast to decode with modern processors. Frequencies are packed in a similar way.
| Technique | What it does | Why it helps |
|---|---|---|
| Delta encoding | Stores differences between neighboring document numbers | Turns big numbers into small ones |
| Fixed blocks of 128 | Groups postings into equal chunks | Allows fast, uniform decoding |
| Bit-packing | Uses only as many bits per entry as the largest gap of the block needs | Shrinks each block |
| Variable-length tail | Writes the leftover postings, fewer than a block, with a simple variable-size code | Avoids wasting space on partial blocks |
| Single-document shortcut | Stores the lone document number inside the dictionary entry | Saves a separate list for rare terms |
| Skip data | Stores markers every block, and higher-level markers over groups of blocks | Lets readers jump over stretches of a list |
| Separate position data | Keeps positions in their own file, read only when needed | Queries that do not need positions never touch them |
A numbers walkthrough
Let us use invented numbers. Suppose a common term appears in one million documents of a segment with ten million documents. On average, the gap between consecutive documents in its list is about ten. A gap of ten fits in four bits, though a few larger gaps make some blocks need five or six bits. Plain, each posting would need four bytes, so the list would take four megabytes. With delta encoding and bit-packing at about five bits per posting, the same list takes roughly 600 kilobytes. That is a saving of nearly seven times, and the saving is even bigger for very dense lists, where the gaps are tiny.
Skip data
When a search needs to jump, for example to the first document at or after number 5,000,000, reading every posting from the start would be wasteful. So along with the list, Lucene stores skip data: for each block, the last document number in it and where the block starts on disk, with higher levels that summarize groups of blocks. A reader looks at the skip data first, jumps to the right block, and decodes only that block. This is like the tabs on the side of a directory, and it makes the “advance to document X” operation cheap even on lists with millions of entries.
The files
Term dictionary and its index
Hold the sorted terms, their statistics, and pointers. The small index helps find the right block.
Documents and frequencies
Hold the blocks of document numbers and frequencies, with skip data.
Positions
Hold, for each posting, the positions of the term inside the document.
Offsets and payloads
Hold offsets and custom data, if the field is configured to store them.
Norms
Hold the compact per-document length information used in scoring.
Names and layout change
The exact names and formats vary between Lucene versions. Treat them as internal.
Memory and the operating system
Postings files are read through the operating system’s file cache, by mapping them into memory. That is why Elasticsearch performs best when a good part of the machine’s memory is left to the operating system rather than given entirely to the Java heap. Frequently read lists stay in the cache, and the disk is touched only for the cold parts. The small in-memory index over the term dictionary has historically lived in memory too, and newer versions may read it through memory mapping instead, so check the notes for your version.
A very large heap leaves little memory for the file cache, and postings reads then go to disk. Keep the heap moderate, and let the operating system keep the hot postings in memory.
8Index Options and Field Types: Which Fields Get Postings
Not every field has a postings list, and not every postings list stores the same details. The mapping decides.
The choices for text fields
For fields that hold text, a mapping option chooses how much detail the postings store. The choice is a trade between features and size.
| Option | Stored in each posting | Supports | Typical use |
|---|---|---|---|
| Documents only | Document number | Matching, filtering | Fields used only to include or exclude documents |
| Frequencies | Document number, frequency | Matching and relevance scoring | Fields that need scoring but not phrases |
| Positions (default for text) | Number, frequency, positions | Phrases and proximity | Most full-text fields |
| Offsets | Positions plus character offsets | Faster highlighting from the index | Fields that are heavily highlighted |
Different field types, different structures
A common confusion is to assume that every field is stored as a postings list. It is not. Elasticsearch uses several structures, and each field type picks the ones it needs.
| Field type | Postings list? | Other structures | Good for |
|---|---|---|---|
| Text | Yes, with positions by default | Norms for length information | Full-text search and phrases |
| Keyword | Yes, documents only, one term per value | Doc values for sorting and grouping | Exact matching, filtering, grouping |
| Numbers, dates | No, uses a tree built for ranges | Doc values | Range checks, sorting, statistics |
| Geographic points | No, uses a tree | Doc values | Location filters |
| Fields marked not indexed | No | Possibly doc values or the source | Data that is only stored and returned |
A library has more than one kind of catalog. A subject-word card catalog helps find books about a topic, a shelf-order list helps sort books by number, and a map of the building helps find rooms by location. Each catalog fits a different question, and no library builds all of them for every kind of item.
Why numbers do not use postings
A postings list is excellent for finding documents with a specific term. But a question such as “price between 20 and 50” would need to look at every distinct price value between the two and combine all their lists, which is slow. For numbers and dates, Lucene therefore uses a different structure, a tree built to answer range questions quickly. A search for one exact number also uses that tree. So do not expect to see a postings list for an integer field, and do not choose an integer type for identifiers that you only ever match exactly. A keyword field, which does use a postings list, is usually the faster and more natural choice for such values.
Keyword fields: one term per value
A keyword field does not split its value. The whole value, such as a country code or a status, is one term. The postings list of that term names all documents with that value. For low-cardinality fields, such as status, the lists are long and few. For high-cardinality fields, such as user identifiers, the lists are short and many, and the dictionary becomes large. Both work, but the shape differs, and so does the cost.
Norms: length information beside the lists
Norms are not part of the postings, but they travel with them. For each document and each scored field, the index stores one small number that encodes the field length. It lets scoring treat a match in a short title as more significant than the same match inside a long body. Norms take about one byte per document per field. For fields that are never scored, such as filters, turning norms off saves space.
Options that add extra lists
Phrase indexing
An option can also index pairs of neighboring words as extra terms, so that phrase searches are answered from short lists instead of by checking positions. It uses more space.
Prefix indexing
An option can index the beginnings of terms, so that prefix searches read one list instead of many. It uses more space.
Space-saving text variants
Newer versions offer variants that skip positions and scoring data, and verify phrases against the source, trading speed for size.
Wildcard-friendly field type
A special field type indexes small pieces of the value to make substring and wildcard searches fast, with a verification step.
How to choose
List how each field will be used
Searched by words, matched exactly, filtered, sorted, grouped, or only returned.
Pick the type that matches
Text for words, keyword for exact values, numbers and dates for ranges.
Choose the least detail that works
Positions only where phrases are needed, frequencies only where scoring matters.
Turn off what you do not use
Norms for filter fields, indexing for fields never searched.
Measure the size
Look at the index size by field, before and after a change.
9How Searches Read Postings Lists
A search is, at heart, a small program that walks and combines lists. Understanding the walk explains why some queries are fast and others slow.
The iterator
To read a list, Lucene creates an iterator. It has two main abilities. It can move to the next document in the list. And it can advance to the first document at or after a given number, using the skip data to jump over stretches it does not need. These two abilities are enough to build every kind of query.
Requiring several terms: the leapfrog
Suppose a query requires documents that contain both “quick” and “brown”. Both lists are sorted. Instead of comparing every pair, Lucene picks the list that is expected to be shortest as the lead. It takes the lead’s first document, and asks the other list to advance to that document. If the other list lands exactly on it, the document is a match. If the other list lands on a later document, that later number becomes the new target, and the lead advances to it. The iterators leapfrog over each other until one is exhausted.
flowchart TD
A["Start with the rarest term as the lead iterator"] --> B["Take its next document"]
B --> C["Ask the other terms to advance to that document"]
C --> D{"Do all terms land on the same document?"}
D -- Yes --> E["Match: record the document"]
D -- No --> F["Use the largest document seen as the new target"]
E --> B
F --> C
Two friends each hold a sorted list of house numbers they have visited, and they want to find the houses both have visited. The friend with the shorter list reads a number out loud. The other friend jumps ahead in her list to that number or the next larger one. If it matches, they celebrate. If not, she reads her larger number, and the first friend jumps to that. Neither needs to read every entry.
Why the rarest term leads
The lead iterator walks its entire list. Every other iterator only needs to jump. So choosing the shortest list as the lead minimizes the number of steps. A query for a rare word and a common word is therefore cheap: it walks the short list and jumps through the long one. A query for two common words is costly, because even the shortest list is long. This is one reason that combining a rare term with a common one is much cheaper than combining two common ones.
Alternatives and exclusions
When a query asks for documents that contain either of several terms, Lucene walks all the lists at once, always moving on the list whose current document is smallest, so that each matching document is visited once. When a query excludes documents that contain a term, the list of that term is used as a filter that rejects matches. Both operations rely on the sorted order of the lists.
One document at a time
Lucene evaluates queries by going through documents in increasing order, and for each candidate document, finding out which terms match and computing its score, before moving to the next. This document-at-a-time style works well with sorted lists, keeps memory use small, and lets the search stop early when it has enough results. The alternative, term-at-a-time, would process one whole list before the next and needs a table of partial scores for all documents, which is heavy for large indexes.
Phrase searches use positions
A phrase search, such as “quick brown”, first finds documents where both terms appear, using the leapfrog. Only for those candidate documents does it read the positions data, and check whether the positions line up. This two-step approach keeps position reading, which is expensive, to a minimum. It also explains why phrase searches are slower than plain term searches, and why fields that never need phrase matching should not store positions.
| Query kind | How lists are used | Relative cost |
|---|---|---|
| One term | Walk one list | Proportional to the list length |
| All of several terms | Leapfrog, led by the shortest list | Proportional to the shortest list, plus jumps |
| Any of several terms | Walk all lists together | Proportional to the sum of lengths, unless skipping applies |
| Term but not another | Walk one list, reject using the other | Proportional to the first list |
| Phrase | Leapfrog, then check positions | Higher, because of position reading |
| Filter reused often | Stored bitset from the query cache | Very low after the first use |
Filters and caching
Filters, meaning yes-or-no conditions that do not need scores, are handled like any other list, but with a difference: the result can be remembered. Elasticsearch has a query cache that stores the set of matching documents of costly, repeated filters per segment, so that the postings lists behind them do not need to be walked again. This connects the two subjects: postings lists are the raw material, and the query cache is a shortcut for the lists that are read again and again.
Every segment, every time
A search must visit each segment of each shard it covers, since each has its own lists. A shard with many small segments needs many dictionary lookups and many small list reads. A shard with a few large segments needs fewer, larger reads. This is a large part of why heavy write activity, which creates many small segments, can slow searches until merging tidies up.
Elasticsearch can report, for a search, how long each part of each query took, including counts of how many times iterators moved to the next document or advanced. It is the best way to see whether a slow query is dominated by walking long lists.
10Scoring and Skipping: What Postings Give the Ranking
Finding matching documents is only half of search. The other half is ranking them, and postings supply most of the numbers for that.
The ingredients of the score
The default ranking formula in Elasticsearch is called BM25. It combines a few quantities, and most of them come straight from postings and their neighbors.
Term frequency
How often the term occurs in the document. More is better, but with diminishing returns, so that a term repeated a hundred times is not a hundred times better.
Document frequency
How many documents contain the term. Rare terms carry more weight, because matching them says more.
Field length
A match in a short field counts for more than the same match in a long one.
Average field length
The typical length in the collection, used to judge whether a field is short or long.
Imagine judging how well a book answers a question about volcanoes. A book that mentions volcanoes many times is probably about them, though the hundredth mention adds little. A mention of a rare, specific word counts for more than a mention of a common one. And a mention in a short leaflet means more than one in a thousand-page encyclopedia.
Statistics are per shard
By default, the document frequency and other statistics used in scoring are taken from the shard that holds the document, not from the whole index. For big indexes with evenly spread data, the numbers are close enough and nobody notices. For small indexes, or unevenly spread data, the numbers may differ noticeably between shards, and the same document may score differently depending on where it lives. A search mode exists that first collects statistics from all shards and then scores, at the price of an extra round trip.
The cost of scoring long lists
Consider a search for “the quick brown fox”. The word “the” has a list of millions of entries, and it contributes almost nothing to relevance. A naive engine would still score every document in that list. This is wasteful, because most of those documents will never reach the top ten. The cleverness of modern Lucene lies in avoiding it.
Block-max skipping
For each block of 128 postings, Lucene stores a summary of the best score that any document in that block could achieve for the term, based on the largest frequencies and shortest lengths inside it. While collecting the top results, the search keeps track of the lowest score among the current best ones. Before scoring a block, it asks: even if a document in this block had the highest possible score for each of its terms, could it beat the current lowest? If not, the whole block is skipped without decoding or scoring a single document.
flowchart TD
S["Keep the top k documents so far and their lowest score"] --> B{"Can the next block of postings beat the lowest score?"}
B -- No --> K["Skip the whole block without scoring"]
B -- Yes --> C["Score the documents in the block"]
C --> U["Update the top k"]
K --> N["Move to the next block"]
U --> N
N --> S
A teacher marking hundreds of exam papers only wants the top ten. Each pile comes with a note saying “the best possible mark in this pile is 40 out of 100.” If she already has ten papers marked above 60, she can put that whole pile aside without opening it.
The price: approximate counts
Skipping means that the search does not look at every matching document. As a result, it cannot say exactly how many documents matched. By default, Elasticsearch counts accurately only up to a threshold, ten thousand hits, and after that reports a lower bound with a note that the total is “at least” that number. If you need the exact total, you can ask for it, but then the skipping must be turned off, and the search reads far more of the lists. For most user-facing search, the lower bound is fine, and the speed gain is large.
| Situation | Effect of skipping |
|---|---|
| Ranked search, top ten, default settings | Skipping active, large savings on long lists |
| Exact total count requested | Skipping disabled, all matches read |
| Sorting by a field instead of relevance | Scores not needed, other shortcuts may apply |
| Filter-only query | No scoring, lists read through as filters, or served from the cache |
| Query with several optional terms | Skipping is most effective here |
A numbers walkthrough
Suppose a search has one common term with a list of five million documents and one rare term with a list of two thousand. Without skipping, a “match any” search would score up to five million documents. With block-max skipping, once the top ten documents have been found among those that contain the rare term, the lowest score among them is high. Blocks of the common term’s list that cannot reach that score are skipped entirely. In practice, only a small fraction of the long list is actually decoded. The figures are invented, but the effect is real, and it is why modern Elasticsearch handles common words far better than older versions did.
Requesting the exact number of hits on every search silently disables the skipping and can turn a fast query into a slow one. Ask for exact counts only where the number itself is needed, and let ordinary result pages use the default.
11Segments, Merges, Deletes, and Updates: The Life of a List
Postings lists are sealed and never edited. Yet documents change all the time. Here is how the two facts live together.
flowchart LR
W["New documents"] --> R["Refresh: new small segment with its own postings"]
R --> M["Merge: postings of several segments combined and document numbers renumbered"]
M --> B["Larger segment with fewer, longer lists"]
D["Deleted or updated documents"] --> L["Marked in a live-documents bitmap"]
L --> M
New documents: new lists
Each refresh writes the documents indexed since the last refresh into a new segment, with a complete little inverted index of its own. Existing segments are untouched. That is why new documents can become searchable quickly and cheaply: nothing has to be edited, only added. The price is that the shard now has one more segment, with its own lists, that every search must visit.
Deletes and updates: marking, not editing
Because lists cannot be edited, deleting a document does not remove its number from any list. Instead, the segment keeps a separate bitmap of live documents, and the deleted one is marked as gone. At search time, matches are checked against this bitmap and skipped if deleted. An update is treated as a delete of the old version and an add of the new one, in a newer segment. The old version lingers in the old segment’s lists until a merge removes it.
A printed book index cannot be corrected with an eraser. When a page is removed from the book, the publisher does not reprint the index at once. Readers are told to ignore references to that page, and the index is fixed when the next edition is printed. The deletion list is the “ignore these pages” note, and the merge is the next edition.
Merging: rewriting lists into bigger ones
In the background, Elasticsearch merges segments. The merge policy chooses several segments of similar size, and a merge process reads their term dictionaries together, in sorted order. For each term, it takes the lists from each input segment and combines them into one new, longer list. Along the way, it drops the postings of deleted documents and renumbers the surviving documents, so that the numbers in the new segment run from zero without gaps. Then it writes the new segment and, when it is ready, replaces the old ones.
The policy picks segments
Usually a group of similar size. Large segments are merged rarely, because it is costly.
Terms are walked in order
The dictionaries of the inputs are read together, so that the same term in each segment is handled at the same time.
Lists are combined
Each input’s list, with its documents shifted by an offset, is appended to the output list in order.
Deleted documents are dropped
Postings of documents marked as deleted are left out, and the remaining numbers are compacted.
The new segment is written
With fresh compression, fresh skip data, and a new term dictionary.
Old segments are removed
Once the new one is safely in place, the inputs are deleted from disk.
What merging changes
Fewer segments to visit
A search does fewer dictionary lookups and fewer small reads.
Dead entries removed
Deleted documents no longer occupy space in the lists.
Better compression
Longer lists share overhead, and skip data works better on big lists.
Heavy on disk and processor
Merging reads and rewrites data. It competes with indexing and searching.
Resets cached results
Cached filter results belong to old segments and are lost when those disappear.
Cleaner numbers
Document frequencies stop counting deleted documents once they are merged away.
Deleted documents still count, for a while
A subtle consequence is that the statistics stored with each term, such as its document frequency, keep counting deleted documents until a merge. This slightly distorts scoring in indexes with many recent deletes or updates. It usually goes unnoticed, but it can explain small scoring differences between a freshly merged index and a busy one.
Force merge for finished data
For an index that will not receive writes anymore, such as last month’s logs, you can ask for a force merge down to a small number of segments. This produces the most compact lists and the fastest searches. It is expensive, and it should only be done on data that is truly finished, because merging an index that still receives writes creates very large segments that are costly to keep tidy.
Index sorting and list compression
By default, documents are numbered in the order they arrive. An optional index setting can sort the documents inside each segment by a chosen field. Documents with similar values then get neighboring numbers, and their postings lists contain runs of close numbers, which compress better and allow some searches to stop early. The cost is slower indexing and merging, so use it only when a specific, frequent search pattern benefits.
Every update leaves a dead entry in old lists and adds a new one in a new segment. Indexes with constant updates therefore generate more merges, carry more dead postings, and cache less effectively than append-only ones. If you can model your data as append-only, the postings will stay smaller and calmer.
12Queries That Touch Many Terms: Wildcards, Prefixes, and Fuzzy Search
A plain term query reads one list. Some queries read hundreds or thousands. Knowing how they work helps you avoid nasty surprises.
Expanding a pattern into terms
A query such as “words starting with quick” does not correspond to one term. It corresponds to every term in the dictionary that starts with those letters: quick, quickly, quicker, quicksand, and possibly hundreds more. Lucene first walks the sorted term dictionary to find all matching terms. Then it combines the postings lists of all of them, as an “any of these” query. The work is therefore the cost of finding the terms plus the cost of reading every one of their lists.
Asking a librarian for “all books about anything starting with vol” means she must first find every subject heading that starts with those letters: volcano, volume, volunteer, and more. Then she must gather the cards from every one of those drawers. It takes far longer than opening a single drawer.
How each kind of pattern behaves
| Query kind | How terms are found | Cost |
|---|---|---|
| Prefix | Jump to the first term with that beginning and read forward | Moderate, depends on how many terms share the prefix |
| Wildcard with fixed beginning | Use the beginning to narrow the dictionary, then test each candidate | Moderate to high |
| Wildcard with leading star | Test every term in the dictionary | Very high on big dictionaries |
| Regular expression | Compile to an automaton and intersect with the dictionary | Depends on the pattern, can be very high |
| Fuzzy search | Automaton for words within a few edits, intersected with the dictionary | Moderate, controlled by edit distance and required prefix |
| Range over keywords | Walk the sorted dictionary between two terms | High if many terms fall in the range |
| Terms query with a list | Look up each listed term | Proportional to the number of terms and their list sizes |
Why leading wildcards hurt
The dictionary is sorted by the beginning of terms. A pattern that starts with a fixed piece can use this order to jump close to the right region. A pattern that starts with a star, such as “anything followed by fox”, cannot jump anywhere, because matching terms may be scattered throughout. Lucene must test every single term. On a dictionary with tens of millions of terms, this can take a long time. This is the most common way people accidentally build slow queries.
Combining many lists
Once the matching terms are known, Lucene has to combine their lists. If there are only a few terms, it treats the query as a scored “any of these.” If there are many, it takes a shortcut: it reads all their lists once, builds a single set of matching documents, and uses that set as a constant-score filter. This avoids enormous scoring work, at the price of giving all matches the same score. The switch happens automatically according to the number of terms.
Limits to protect the cluster
Maximum expansions
Some queries cap how many terms they expand to, so that a short prefix cannot pull in the whole dictionary.
Limit on the terms list
A terms query accepts only up to a set number of terms, by default tens of thousands.
Limit on clauses
The number of clauses in a combined query is capped to protect memory.
Timeouts
A search can be given a time limit so that a runaway query returns partial results instead of hanging.
Making these searches cheap: pay at indexing time
The remedy for slow pattern searches is usually to do the work when documents are indexed, not when queries run. Options include indexing the beginnings of terms for prefix search, indexing small overlapping pieces of words for partial matching, or using a special field type built for substring searches. Each of these multiplies the number of terms and postings, so the search becomes fast at the cost of a larger index and slower indexing. Choose them only for fields that truly need such searches.
If a search box passes user input straight into a wildcard or regular expression query, one user typing a leading star can slow the cluster for everyone. Sanitize input, forbid leading wildcards, set limits, and use purpose-built search-as-you-type fields.
13Size, Performance, and Tuning
The postings lists of a text-heavy index are often its biggest part. Shaping them well saves disk, memory, and time.
What determines the size
Number of tokens
More text means more postings. Long fields and many fields multiply the size.
Index options
Positions and offsets can be larger than the rest of the postings put together.
Number of distinct terms
Many distinct terms enlarge the dictionary. Identifiers and n-grams are the usual causes.
Analyzer choices
Stemming shrinks the vocabulary. Synonyms and n-grams enlarge it.
Segment structure
Many small segments compress worse than a few large ones.
Dead entries
Deleted documents keep their postings until merges remove them.
A numbers walkthrough
Let us use invented numbers. Suppose an index holds 50 million documents, each with about 100 tokens in its main text field, so 5 billion token occurrences in total. Suppose, after removing repeats within a document, there are about 3 billion pairs of a term and a document. With compression, storing only document numbers and frequencies might cost about 1 byte per pair, so around 3 gigabytes. Storing positions costs roughly another byte per occurrence, adding about 5 gigabytes. In this example, positions are more than half of the postings size. If phrase searches are not needed on that field, dropping positions cuts the postings by well over half.
Now imagine the same field also gets edge n-grams from 2 to 15 characters. A ten-letter word now yields nine or more extra terms, so token occurrences might grow eight to ten times, and the index may grow by a similar factor. It is easy to see why applying n-grams to long text is dangerous, and why they belong on short fields such as names and titles.
An index at the back of a book that lists only page numbers is thin. One that also lists the exact line of every mention is thicker. One that lists every partial spelling of every word, including “vo,” “vol,” “volc,” and so on, is thicker than the book itself.
Measuring where the space goes
Elasticsearch has an analysis endpoint for index disk usage that breaks an index down by field and by structure: how many bytes go to the inverted index, to norms, to stored fields, to doc values, and to the number trees. It is an expensive operation, so run it on a test copy or in a quiet period. It answers the most useful question in sizing: which fields are the big ones, and which structure inside them.
Tuning levers
| Lever | Effect on size | Effect on speed | Trade-off |
|---|---|---|---|
| Do not index fields you never search | Removes their lists entirely | Faster indexing | Field can no longer be searched |
| Drop positions where phrases are unneeded | Large reduction | Faster indexing, smaller reads | No phrase or proximity search |
| Turn off norms on filter-only fields | Saves about a byte per document | Slightly faster | No length-based scoring |
| Use keyword for exact values | Avoids position data and analysis | Faster matching | No word-level matching |
| Limit the length of keyword values | Avoids huge, useless terms | Avoids indexing errors | Long values are not searchable |
| Avoid n-grams on long text | Avoids huge growth | Faster indexing | No partial matching on that field |
| Force merge finished indexes | Somewhat smaller, dead entries removed | Faster searches | Costly operation, only for read-only data |
| Index sorting | Can compress lists better | Some searches stop early, indexing slower | Slower writes and merges |
| Extra indexing options for phrases or prefixes | Larger | Faster targeted searches | More space and slower indexing |
Queries that cost a lot
- Two common terms required together. Even the shorter list is long. Use filters, or rank on rarer terms where possible.
- Leading wildcards and heavy regular expressions. They test every term.
- Very large terms lists. Each term costs a lookup and a list read.
- Exact counts on every request. They disable skipping.
- Phrase searches on very common words. Many candidates, each needing a position check.
- Searching across very many segments. Each adds a lookup.
Memory: leave it for the file cache
Postings are read from files through the operating system’s cache. The more of the hot lists fit in that cache, the fewer disk reads a search needs. That is why guidance always says to keep the heap moderate and leave a generous share of the machine’s memory to the operating system. If searches are slow and disk reads are high, more memory for the cache, faster disks, or smaller lists will help more than any query trick.
Because so many levers interact, change one at a time, and measure index size, indexing speed, and search latency on realistic data and queries before and after. The best setting depends on your text, your queries, and your hardware.
14Advantages, Disadvantages, and Trade-offs
Every design carries a price. Here is the honest balance sheet of the postings list.
Advantages
- Search cost follows the size of the answer, not the size of the data.
- Sorted lists combine quickly using leapfrogging and skipping.
- Compression makes lists small, so more fit in the file cache.
- Frequencies, positions, and statistics enable scoring and phrase search.
- Immutable segments are safe to read from many threads and easy to cache.
- Block-level summaries let searches skip work on long lists.
- A mature, well-understood structure with decades of refinement.
Disadvantages
- Lists cannot be edited, so updates and deletes leave dead entries.
- Merges are needed, and they consume disk and processor time.
- Positions and n-grams can make the index very large.
- Pattern queries may need to touch a great many terms.
- Every segment has its own lists, so searches multiply by segment count.
- Fast searches by skipping give only approximate hit counts by default.
- Choices made at indexing time, such as analyzers, are hard to change later.
| Trade-off | Leaning toward speed of searching | Leaning toward cost of indexing and storage |
|---|---|---|
| Index detail | Positions and phrase-friendly options: fast phrase searches, bigger index | Documents only: smaller and faster to index, fewer features |
| Partial matching | N-grams at indexing time: fast searches, large index | Wildcards at query time: small index, slow searches |
| Segment count | Few large segments: fast searches, costly merges | Many small segments: cheap writes, slower searches |
| Refresh frequency | Short: new data visible sooner, more small segments | Long: fewer segments, data visible later |
| Hit counting | Approximate: skipping saves work | Exact: full reading of lists |
| Stop words | Removed: smaller lists, some searches impossible | Kept: fully faithful, much longer lists |
| Synonyms | At indexing time: fast searches, index bloat, hard to change | At search time: flexible, slightly slower queries |
A more detailed index makes a book easier to search but harder and longer to prepare, and bigger to print. A publisher decides how detailed the index should be by thinking about how readers will look things up.
How rich should the postings be?
| Situation | Recommended stance |
|---|---|
| Article or document search with phrase queries | Positions on the main text, norms on, moderate analyzers |
| Product catalog with filters and a search box | Text with positions on title and description, keywords for filters, no norms on filter fields |
| Logs searched by exact values and time | Keywords and dates, minimal text fields, positions only where message search is needed |
| Autocomplete on short names | N-grams or prefix indexing on the short field only |
| Fields returned but never searched | Not indexed at all |
15High Availability and Reliability
Postings lists live in files on disk. Their reliability comes from checks, copies, and careful upgrades.
Every copy builds its own lists
In the standard model, each replica applies the same operations as the primary, so each builds its own postings lists in its own segments. The segments on two copies of a shard therefore differ in layout and timing, although they describe the same documents. A search may be served by any copy, and returns the same matches, except for small differences caused by refresh timing and by statistics that depend on segment layout.
Two librarians in different buildings each receive the same new books in the same order and each prepares her own card catalog. The catalogs list the same books, but the cards may be filed in different drawers and reprinted at different times.
Checksums guard every file
Lucene writes a checksum at the end of every file it creates, including postings files. The checksum is verified in several situations: when files are copied to another node during recovery, when segments are merged, and, if requested, when a shard opens. If a file has been damaged by a disk fault, the check finds it, rather than letting a search read garbage or spread the damage into a merge.
When a segment is corrupted
If a shard copy discovers corruption, it is marked as failed, and the cluster rebuilds it from a healthy copy, in the same way as after any other failure. This is one of the practical reasons to keep at least one replica for important data. Without a replica, a corrupted shard can only be repaired from a snapshot, or, in the worst case, may lose the affected documents.
| Situation | What happens to the postings |
|---|---|
| A replica is rebuilt by file-based recovery | Segment files, including their postings, are copied from the primary and verified |
| A replica catches up by operation replay | The missed operations are re-indexed, producing new postings in new segments |
| A node restarts | Sealed segments are reopened as they are, and the recent translog is replayed into new lists |
| A snapshot is taken | Segment files, with their postings, are copied to the repository |
| A shard is found corrupted | The copy is failed and rebuilt from a healthy copy or a snapshot |
| A merge runs | Input files are read and verified, and a new segment is written |
Upgrades and the file format
The format of postings files changes when Lucene moves forward. Elasticsearch can read segments written by the previous major version of Lucene, and new segments and merged ones are written in the newer format, so old data is rewritten gradually. But it cannot read segments that are more than one major generation old. For very old indexes, upgrading requires reindexing the data into a new index, or using the special handling that the release notes describe. Plan large upgrades with this in mind, and check the compatibility rules of your target version early.
Habits that keep postings safe
Keep at least one replica for data you care about
A healthy copy is the fastest repair for a damaged one.
Use reliable disks and watch for errors
Disk faults are the usual source of corrupted segments.
Take and test snapshots
They protect against corruption on every copy, and against mistakes.
Plan upgrades around format compatibility
Reindex very old indexes before a major upgrade.
Leave headroom for merges
Merges temporarily need extra disk space. A full disk can stall them and cause trouble.
A merge writes a new segment before removing the old ones, so it temporarily needs room for both. On a nearly full disk, merges may fail or stall, and the index slowly accumulates small segments. Keep comfortable free space, especially before a force merge.
16Security Considerations
The words of your documents live in the dictionary and the lists. That deserves the same care as the documents themselves.
The index contains your text
A postings index is not an opaque blob. The term dictionary holds the words of your documents, and the lists, together with positions, can be used to reconstruct large parts of the original text. Anyone who can read the files of a shard can learn a great deal about the content. Protect the disks with encryption at rest, restrict who can log in to the machines, and treat snapshots, which contain the same files, with the same care.
A book’s index, with every word and the pages it appears on, tells a curious reader a lot about the book even without the book itself. The index must be locked away with the same care as the book.
Some APIs reveal indexed terms
Term listing endpoints
Some endpoints list terms in a field, with their frequencies. They can reveal the vocabulary of an index.
Term vectors
For a document, they return its terms, positions, and statistics.
Grouping by terms
Grouping on a field returns its most common values, which may be sensitive.
Explain and profile
They expose term statistics and internal timings for a search.
Restrict who can call these on sensitive indexes. Use the security features of Elasticsearch to limit users to the indexes and fields they need, and where your subscription supports it, use field-level and document-level restrictions, which work by combining the user’s filters with the lists behind each query.
Deleted does not mean gone yet
When a document is deleted, its terms and postings remain in the sealed segment until a merge removes them, and older history may be kept for recovery for a while longer. For strict privacy rules, such as erasing personal data on request, this means that the data may remain on disk for some time after deletion. Design your deletion process with this in mind: know how long merges and history retention take in your setup, consider forcing merges on affected indexes where appropriate, and remember that snapshots hold copies until they expire.
Sensitive terms and the dictionary
Even after all documents that contained a certain word are deleted and merged away, ordinary practice is fine, but before that happens the word remains in the term dictionary. If a word itself is sensitive, such as a person’s name, treat the index as containing it until the data is fully purged.
Query abuse
As Chapter 12 explained, some queries are very expensive. A user, or an attacker, who can send arbitrary wildcard, regular expression, or huge terms queries can slow the cluster for everyone. Limit what queries applications can construct, set timeouts and limits, and put rate limits in front of public search endpoints.
Timing and inference
The time a query takes depends on the length of the lists it reads. In theory, an observer who can run many queries and measure the timings may infer how common a certain word is. For public search boxes over sensitive data, this is a reason to limit result detail, throttle requests, and avoid exposing raw timings.
Turn on authentication and encryption, even for clusters that seem internal. Most exposures of indexed data start with a cluster that was described as “only internal” and had no protection at all.
17Monitoring, Logging, and Metrics
You cannot tune what you cannot see. These are the windows onto your postings lists and the signals worth watching.
| Window | What it shows | Use it to |
|---|---|---|
| Index disk usage analysis | Bytes per field for the inverted index, norms, stored fields, doc values, and number trees | Find which fields and structures use the space |
| Segment listing | Each segment’s size, number of documents, and deleted documents | Spot too many small segments or many deletes |
| Index and node statistics | Number of segments, merge activity, search and indexing times | Track trends in segment count, merges, and query time |
| Analysis endpoint | The terms and positions a text would produce | Debug why a search does or does not match |
| Explain | How a document’s score was computed, with term statistics | Understand ranking |
| Profile option on a search | Timings and counters for each part of a query | See where a slow query spends its time |
| Term vectors | Terms, frequencies, and positions of one document | Inspect what was indexed for a document |
| Slow logs | Searches and indexing operations slower than a threshold | Find expensive queries and heavy documents |
Metrics to chart
| Metric | What it tells you | Warning sign |
|---|---|---|
| Segments per shard | How many sets of lists each search must visit | Steady growth without merges catching up |
| Share of deleted documents | Dead entries in lists | A large share for long periods |
| Index size per document | Cost of your mapping choices | Sudden growth after a mapping change |
| Merge time and throttling | Whether merging keeps up with writes | Merges constantly running, indexing throttled |
| Search latency percentiles | How long queries take, including the slow tail | Rising slow tail |
| Disk reads versus cache hits | Whether hot lists fit in the file cache | High disk reads during searches |
| Slow query counts by type | Which query shapes cost the most | Wildcards, regular expressions, or huge terms lists appearing often |
| Free disk space | Room for merges | Below a comfortable margin |
Reading the patterns
Few segments, calm merges
Searches read a few large lists, latency is steady, disk reads are low.
Many small segments
Frequent refreshes and heavy writes create many lists to visit. Consider a longer refresh interval and check merge settings.
Index far larger than expected
Check positions, n-grams, dynamic fields, and long keyword values with the disk usage analysis.
A few queries dominate time
Look for leading wildcards, heavy patterns, and exact counts on every request.
High disk reads during searches
The hot lists do not fit in the file cache. Free memory for it or shrink the lists.
Many deleted documents
Update-heavy indexes leave dead entries. Consider the data model, and let merges tidy up.
A troubleshooting playbook
Find the slow query
Use slow logs to identify the exact query and the index it targets.
Profile it
See which clause takes the time and how many iterator steps it needs.
Check what terms it touches
Does it expand to many terms? Does it include a very common term?
Look at the segments
How many are there, and how many deleted documents do they carry?
Check the mapping
Are there positions, n-grams, or norms that this field does not need?
Check the hardware
Disk reads, file cache, and free space.
Fix the cause, then re-measure
Change the query or the mapping, and compare before and after.
Alert on a rising slow tail of search latency, on segment counts that keep growing, on merges that are always running, on free disk space getting low, on sudden jumps in index size per document, and on the appearance of expensive query types in slow logs.
18Design Patterns and Anti-patterns
Good habits keep postings lists small and searches fast. Bad habits are behind most bloated indexes and slow queries.
Patterns that work
One field, one job
Decide for each field whether it is for words, for exact values, for ranges, or only for display, and give it the type that fits.
Multiple views of one value, only where needed
Use a text field for word search and a keyword sub-field for exact matching and grouping, but only for fields that truly need both.
Explicit mappings
Define types and index options up front, instead of letting dynamic mapping create two lists for every string.
Filters in filter context
Put yes-or-no conditions where they can be cached and where no scores are computed.
Time-based indexes for append-only data
New indexes for new periods, older ones read-only and force-merged, keeping lists compact.
Purpose-built fields for autocomplete
Use fields designed for search-as-you-type on short values, not wildcards on long text.
Test analysis before loading
Use the analysis endpoint on real samples, and compare index-time and search-time terms.
A good publisher decides, before printing, which words deserve an index entry, how detailed it should be, and how to spell the headings. A publisher who indexes every word, in every possible spelling, with the exact line number, produces an index thicker than the book, and nobody can find anything in it.
Anti-patterns to avoid
What people do
They leave dynamic mapping on, so every string field is indexed both as full text and as a keyword, whether or not it is ever searched in both ways.
Why it hurts
Each string gets two dictionaries and two sets of lists, plus norms and doc values. The index is far larger than needed, and indexing is slower.
Better approach
Define mappings. Use text for word search, keyword for exact values, and skip indexing for fields that are never searched.
What people do
They allow wildcard queries that start with a star, often built from user input, on large text fields.
Why it hurts
Every term of the dictionary must be tested, and thousands of lists may be read. A few such queries can slow the whole cluster.
Better approach
Forbid leading wildcards, use fields designed for substring or prefix search, and set limits and timeouts.
What people do
They apply n-gram analysis to long text fields to support partial matching.
Why it hurts
The number of terms and postings multiplies many times over, and indexing and storage costs explode.
Better approach
Apply n-grams only to short fields such as names, and use a separate, lighter field for full text.
What people do
They ask for the exact total number of hits on every search, including simple result pages.
Why it hurts
Exact counting disables block skipping, so searches read far more of the long lists than necessary.
Better approach
Use the default approximate count, and request exact numbers only where the exact number is truly needed.
What people do
They index very long values, such as entire documents or encoded blobs, into keyword fields.
Why it hurts
Each such value becomes a giant term in the dictionary. Terms beyond a size limit cause indexing errors, and the rest bloat the dictionary for no searching benefit.
Better approach
Set a length limit on keyword values, and do not index data that will never be matched exactly.
What people do
They update the same documents constantly, for example to increment counters, inside a search index.
Why it hurts
Every update leaves a dead entry in an old list and adds a new document in a new segment. Merges work harder, and dead entries pile up.
Better approach
Keep fast-changing counters in a system built for them, or model changes as appended events, and keep the search index for mostly stable content.
What people do
They add synonyms at indexing time, and later discover that the synonym list must change.
Why it hurts
The extra terms are baked into the lists of every document. Changing the list requires reindexing the whole index.
Better approach
Prefer synonyms at search time, which can be updated without rewriting the index, unless a measured need justifies indexing them.
What people do
They use different analyzers at indexing time and at search time by accident.
Why it hurts
The terms produced from the query do not match the terms stored in the lists, and documents that seem to match are not found.
Better approach
Use the same analysis on both sides unless there is a deliberate reason, and verify with the analysis endpoint.
19Best Practices and Common Mistakes
A short list you could hand to a new teammate on their first day.
Best practices
Match the type to the use
Text for words, keyword for exact values, numbers and dates for ranges.
Store the least that works
Positions only for phrases, norms only for scored fields.
Define, do not guess
Explicit mappings, controlled dynamic behavior, limits on fields.
Use filter context and rare terms
Cache-friendly filters, and rare terms to lead the leapfrog.
Avoid leading wildcards
Use fields built for partial matching instead.
Let old data settle
Force merge finished indexes, and keep refresh intervals sensible.
Protect the file cache
Moderate heap, generous memory left for the operating system.
Check size by field
Use the disk usage analysis before and after every change.
Common mistakes
- Thinking postings hold the documents. They hold only document numbers, frequencies, and positions. The text lives elsewhere.
- Expecting the internal document number to be the _id. They are unrelated, and the internal number changes at merges.
- Using numeric types for identifiers. Numbers use range trees, not postings. Exact-match identifiers usually belong in keyword fields.
- Debugging documents instead of terms. Matching happens on analyzed terms. Compare the terms, not the raw text.
- Forgetting that filters and scores differ. Conditions that need no score belong in filter context.
- Ignoring segment counts. Many tiny segments slow every search.
- Force merging live indexes. It creates very large segments that are hard to keep tidy.
- Assuming a mapping can be changed in place. Changes to analyzers or index options usually require reindexing.
Myths versus facts
| Myth | Fact |
|---|---|
| Every field has a postings list. | Numbers, dates, and locations use other structures, and unindexed fields have none. |
| A postings list contains the document text. | It contains document numbers and small details. Text is kept in stored fields. |
| Search reads every document. | Search reads only the lists of the terms in the query. |
| Deleting a document removes its postings. | Postings stay until a merge, and the document is only marked as deleted. |
| More detail in the index is always better. | Extra detail such as positions or n-grams can double or multiply the size. |
| Long lists are always slow. | Skipping and caching often avoid reading most of a long list. |
| The exact hit count is free. | Exact counts disable the skipping that makes ranked search fast. |
| The index format is stable across versions. | It changes with Lucene, and only recent generations can be read. |
Every term you index and every detail you attach becomes a list that must be written, merged, stored, cached, and read for as long as the index lives. Index what your users will actually search, at the level of detail they will actually need.
20Real-World and Industry Examples
Where do postings lists quietly decide success or trouble?
Many well-known organizations have said publicly that Elasticsearch is part of their systems, including Wikipedia’s search, Netflix, Uber, and Stack Overflow. They rarely publish how they shape their indexes, so the scenarios below are typical patterns for such workloads, not descriptions of anyone’s private configuration.
Online marketplace: words plus filters
A marketplace of the size of Amazon or eBay has a search box and a side menu of filters. Product titles and descriptions are text fields with positions, so that phrase queries like “wireless headphones” work. Brand, category, and color are keyword fields, whose short, dense lists are used as cached filters. Numbers such as price use range trees. Fields that are only displayed are not indexed at all.
Log search: exact values and a message field
A streaming or ride-hailing service searches logs by service name, level, and time, which are keyword and date fields, and only occasionally by words in the message. The message field is a text field, and the team measured that its positions took more than half of the index. Because only a small share of searches used phrases, they moved to a lighter variant for older data, and saved a large amount of disk.
News archive: phrases and long history
A publisher searches millions of articles, where readers often search for exact phrases, such as a quoted sentence. Positions are essential here. Older, read-only yearly indexes are force merged into a few large segments, which makes lists compact and searches fast, while the current year’s index stays lively and small.
Autocomplete on names
A travel site suggests city names as users type. A short name field uses edge n-grams, so that each typed prefix matches a small list. Because the field is short, the extra terms are affordable. The team never applied the same technique to long descriptions, after testing showed a many-fold growth in index size.
A support tool that broke on leading wildcards
An internal search page passed whatever users typed to a wildcard query. One user typed a leading star and a common word into a large index, and search latency spiked for everyone. The team banned leading wildcards, set limits and timeouts, and added a dedicated field for the substring searches that support agents truly needed.
A mapping that doubled the index
A pipeline let dynamic mapping index every string as both text and keyword, including long free-text notes. The index size analysis showed that keyword copies of long notes and their doc values took a third of the disk. The team defined explicit mappings, kept only the necessary keyword fields, and applied a length limit to the rest.
In every example, the winning ideas are the same: the right structure for each field, the least detail that works, careful limits on expensive queries, tidy segments, and regular measurement of where the bytes and the milliseconds go.
21Frequently Asked Questions
Short answers to the questions that come up most often.
22Summary and Key Takeaways
If you skim only one part of this guide, make it this one.
The postings list is the quiet workhorse of Elasticsearch. For each word in a field, inside each segment, it keeps a sorted, compressed list of the documents that contain it, sometimes with how often and where. Searches read and combine these lists, ranking uses their numbers, merges rewrite them, and their size and shape decide much of the cost of your cluster.
Key Takeaways
- What it is: a sorted list of document numbers for one term, in one field, in one segment, with optional frequencies, positions, and offsets.
- Why it is fast: search cost follows the size of the answer, and sorted, block-compressed lists with skip data are combined by leapfrogging and skipping.
- How it is built and stored: analysis creates terms, memory tables grow lists, a segment is written and sealed, and lists are stored as delta-encoded, bit-packed blocks of 128 with a term dictionary in front.
- What it costs: positions, n-grams, dynamic string mappings, and long keyword values are the usual sources of oversized indexes, so index only what is searched, at the detail needed.
- How searches use it: rare terms lead the leapfrog, phrases check positions, filters can be cached, and block-max skipping saves work at the price of approximate hit counts.
- How it changes over time: lists are never edited, deletes only mark documents, updates add new entries, and merges rewrite lists into larger, cleaner segments.
- How to run it well: match field types to uses, avoid leading wildcards and needless exact counts, keep segments tidy, protect the file cache, secure the files and the APIs that reveal terms, and measure size by field.