搜尋索引
倒排索引記錄每個詞出現在哪些文件:搜尋多個詞時,只要把幾串排好序的文件編號取交集,不用掃過每一篇文件。
範例
1/7
posting list,由短到長(文件編號)
memory
- 0
- 15
- 19
fast
- 0
- 9
- 15
- 18
- 0A cache keeps hot data in fast memory.
- 1The database stores data on disk in pages.
- 2A search engine builds an inverted index of words.
- 3Each word in the index points to a list of documents.
- 4Sorted lists can be intersected with two pointers.
- 5A load balancer spreads requests across servers.
- 6Servers keep a cache to avoid slow disk reads.
- 7Sharding splits data across many database servers.
- 8Replication copies data so a server can fail safely.
- 9A queue lets fast producers hand work to slow consumers.
- 10Bloom filters answer whether a key might be present.
- 11An LSM tree turns random writes into sequential disk writes.
- 12Compaction merges sorted files and drops old versions.
- 13The search index is rebuilt when documents change.
- 14Two pointers walk sorted lists of document numbers.
- 15Memory is fast but small; disk is slow but large.
- 16A hash table finds a key in one step on average.
- 17Engine logs show which servers answer slow requests.
- 18Fast reads need an index; fast writes need a log.
- 19Data in memory is lost when a server restarts.
整個索引(88 個詞)
- across
- 5, 7
- answer
- 10, 17
- average
- 16
- avoid
- 6
- balancer
- 5
- bloom
- 10
- builds
- 2
- cache
- 0, 6
- change
- 13
- compaction
- 12
- consumers
- 9
- copies
- 8
- data
- 0, 1, 7, 8, 19
- database
- 1, 7
- disk
- 1, 6, 11, 15
- document
- 14
- documents
- 3, 13
- drops
- 12
- each
- 3
- engine
- 2, 17
- fail
- 8
- fast
- 0, 9, 15, 18
- files
- 12
- filters
- 10
- finds
- 16
- hand
- 9
- hash
- 16
- hot
- 0
- index
- 2, 3, 13, 18
- intersected
- 4
- inverted
- 2
- keep
- 6
- keeps
- 0
- key
- 10, 16
- large
- 15
- lets
- 9
- list
- 3
- lists
- 4, 14
- load
- 5
- log
- 18
- logs
- 17
- lost
- 19
- lsm
- 11
- many
- 7
- memory
- 0, 15, 19
- merges
- 12
- might
- 10
- need
- 18
- numbers
- 14
- old
- 12
- pages
- 1
- pointers
- 4, 14
- points
- 3
- present
- 10
- producers
- 9
- queue
- 9
- random
- 11
- reads
- 6, 18
- rebuilt
- 13
- replication
- 8
- requests
- 5, 17
- restarts
- 19
- safely
- 8
- search
- 2, 13
- sequential
- 11
- server
- 8, 19
- servers
- 5, 6, 7, 17
- sharding
- 7
- show
- 17
- slow
- 6, 9, 15, 17
- small
- 15
- sorted
- 4, 12, 14
- splits
- 7
- spreads
- 5
- step
- 16
- stores
- 1
- table
- 16
- tree
- 11
- turns
- 11
- two
- 4, 14
- versions
- 12
- walk
- 14
- whether
- 10
- which
- 17
- word
- 3
- words
- 2
- work
- 9
- writes
- 11, 18
指標正指著符合已經走過
在索引裡查出每個詞的 posting list——每個詞一次雜湊查找,不管有多少文件。再依長度由短到長排:memory(3)、fast(4)。答案一定在最短的那串裡。
指標比較
0
posting list 總長
7
全文掃描要讀的詞
129
符合的文件
—
亮起來的是這一步執行的程式碼
const STOP = new Set(["a", "an", "the", "in", "of", "to", "on", "and", "or", "is", "are", "be", "can", "so", "when", "but", "it", "its", "into", "with", "one"]); function tokenize(text: string): string[] { return text.toLowerCase().split(/[^a-z]+/) .filter((w) => w && !STOP.has(w));} class InvertedIndex { private postings = new Map<string, number[]>(); // Add documents in id order and every posting list stays sorted. add(docId: number, text: string): void { for (const word of new Set(tokenize(text))) { if (!this.postings.has(word)) this.postings.set(word, []); this.postings.get(word)!.push(docId); } } search(query: string): number[] { const words = [...new Set(tokenize(query))]; const lists = words.map((w) => this.postings.get(w) ?? []); lists.sort((a, b) => a.length - b.length); let result = lists[0] ?? []; for (const list of lists.slice(1)) { result = intersect(result, list); } return result; }} function intersect(a: number[], b: number[]): number[] { const out: number[] = []; let i = 0, j = 0; while (i < a.length && j < b.length) { if (a[i] === b[j]) { out.push(a[i]); i++; j++; } else if (a[i] < b[j]) i++; else j++; } return out;}模型假設與範圍
- 這是可重現的教學模型;延遲、容量、故障率與工作負載是設定或樣本,不能直接當作正式系統的效能承諾。
- 小型語料、簡化斷詞與 AND posting-list 交集;不包含相關性排名、多語言分析或增量索引維護。
什麼時候用
- 要在大量文字裡依詞找文件:站內搜尋、商品搜尋、日誌搜尋。資料庫的
LIKE '%word%'每次都得掃全表。 - 查詢要的是「包含這些詞」再依相關度排序,而不是精確比對某個欄位。
和其他主題的關係
- 被這些用到
- 案例:搜尋引擎
- 延伸閱讀
- 資料結構 · 字首樹
出現在這些架構裡
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| 建索引 W 是所有文件的總詞數 | O(W) | O(W) |
| 查一個詞的 posting list 索引本身是雜湊表 | O(1) | O(1) |
| AND 查詢(兩串長 p、q) 真實引擎再用跳躍指標或 galloping 搜尋跳過一大段 | O(p + q) | O(p + q) |
| 不用索引:全文掃描 | O(W) | O(W) |
空間:O(W),每個詞在每篇文件出現就一筆 posting;實務上會壓縮
Big O 實測:n 變大時步數怎麼長
數的是:n 篇合成文件(每篇 10 個詞),一個兩詞查詢
| Big O | n = 1,000 | n = 4,000 | n = 16,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|
| 倒排索引:指標比較 | O(n) | 252 | 1,095 | 4,447 | ×18 (×16) |
| 全文掃描:讀的詞 | O(n) | 10,000 | 40,000 | 160,000 | ×16 (×16) |
兩者都跟著 n 線性成長——查詢的詞在每篇文件出現的機率固定,posting list 就跟文件數一起變長。但索引少做了約 36 倍的事,而且詞越少見差距越大。
和其他做法比
| 指標比較(有索引) | 用到的 posting 數 | 全文掃描讀的詞 | 結果 | |
|---|---|---|---|---|
| 「fast memory」 | 4 | 7 | 129 | [0, 15] |
| 「sorted lists」 | 3 | 5 | 129 | [4, 14] |
| 「data servers」 | 7 | 9 | 129 | [7] |
| 「slow disk reads」 | 6 | 10 | 129 | [6] |
同樣 20 篇文件、同樣的答案(測試確認兩種做法結果一樣)。全文掃描的成本只看文件有多少;索引的成本只看查詢的詞有多常見。
真實世界裡的它
- Elasticsearch、OpenSearch、Solr 都建在 Lucene 的倒排索引上。
- PostgreSQL 的全文搜尋用 GIN(Generalized Inverted Index)索引,本質上就是倒排索引。
- 網頁搜尋引擎把同樣的結構分片到成千上萬台機器上。
取捨與陷阱
- 斷詞決定了能找到什麼:大小寫、單複數(list/lists)、中文沒有空格要另外斷詞;這裡的簡單斷詞找不到 lists 的 list。
- 索引不是即時的:文件改了要重建或增量更新,搜尋結果會短暫落後。
- 非常常見的詞 posting list 長到幾乎等於全部文件,交集省不了多少事,所以會當停用詞處理或特別最佳化。