跳到主要內容

系統設計

把資料結構放大到好幾台機器

主題 · 搜尋索引

搜尋索引

倒排索引記錄每個詞出現在哪些文件:搜尋多個詞時,只要把幾串排好序的文件編號取交集,不用掃過每一篇文件。

範例
1/7
posting list,由短到長(文件編號)
memory
  1. 0
  2. 15
  3. 19
fast
  1. 0
  2. 9
  3. 15
  4. 18
  1. 0A cache keeps hot data in fast memory.
  2. 1The database stores data on disk in pages.
  3. 2A search engine builds an inverted index of words.
  4. 3Each word in the index points to a list of documents.
  5. 4Sorted lists can be intersected with two pointers.
  6. 5A load balancer spreads requests across servers.
  7. 6Servers keep a cache to avoid slow disk reads.
  8. 7Sharding splits data across many database servers.
  9. 8Replication copies data so a server can fail safely.
  10. 9A queue lets fast producers hand work to slow consumers.
  11. 10Bloom filters answer whether a key might be present.
  12. 11An LSM tree turns random writes into sequential disk writes.
  13. 12Compaction merges sorted files and drops old versions.
  14. 13The search index is rebuilt when documents change.
  15. 14Two pointers walk sorted lists of document numbers.
  16. 15Memory is fast but small; disk is slow but large.
  17. 16A hash table finds a key in one step on average.
  18. 17Engine logs show which servers answer slow requests.
  19. 18Fast reads need an index; fast writes need a log.
  20. 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 On = 1,000n = 4,000n = 16,000成長倍數:實測(理論)
倒排索引:指標比較O(n)2521,0954,447×18 (×16)
全文掃描:讀的詞O(n)10,00040,000160,000×16 (×16)

兩者都跟著 n 線性成長——查詢的詞在每篇文件出現的機率固定,posting list 就跟文件數一起變長。但索引少做了約 36 倍的事,而且詞越少見差距越大。

和其他做法比

指標比較(有索引)用到的 posting 數全文掃描讀的詞結果
「fast memory」47129[0, 15]
「sorted lists」35129[4, 14]
「data servers」79129[7]
「slow disk reads」610129[6]

同樣 20 篇文件、同樣的答案(測試確認兩種做法結果一樣)。全文掃描的成本只看文件有多少;索引的成本只看查詢的詞有多常見。

真實世界裡的它

  • Elasticsearch、OpenSearch、Solr 都建在 Lucene 的倒排索引上。
  • PostgreSQL 的全文搜尋用 GIN(Generalized Inverted Index)索引,本質上就是倒排索引。
  • 網頁搜尋引擎把同樣的結構分片到成千上萬台機器上。

取捨與陷阱

  • 斷詞決定了能找到什麼:大小寫、單複數(list/lists)、中文沒有空格要另外斷詞;這裡的簡單斷詞找不到 lists 的 list。
  • 索引不是即時的:文件改了要重建或增量更新,搜尋結果會短暫落後。
  • 非常常見的詞 posting list 長到幾乎等於全部文件,交集省不了多少事,所以會當停用詞處理或特別最佳化。