OpenSearch Index 教學:Mapping、text 與 keyword
本文件說明 thortron_one 全文檢索(Full-Text Search)如何建立 OpenSearch index,以及 mapping 裡 text / keyword 的差異與選用原則。
對應實作:internal/pkgs/utilities/fulltextsearch/opensearch/client.go
1. 這套 FTS 在存什麼?
每個 PDF 頁面會寫成一筆 OpenSearch document,大致欄位如下:
| 欄位 | OpenSearch type | 說明 |
|---|---|---|
document_id | long | 文件儲存系統的 document ID |
filename | text + filename.keyword | 檔名(全文搜尋 + 精確值) |
content | text | 該頁擷取出來的文字 |
page | integer | 頁碼 |
source | keyword | 來源識別,目前固定 thortron_one |
indexed_at | date | 寫入時間(RFC3339) |
Document _id 格式:thortron_one-{documentID}-p{page}
2. Index 什麼時候建立?
連線時由 Client.New 呼叫 ensureIndex:
Client.New(url, index)
└─ ensureIndex
├─ Indices.Exists
│ ├─ 200 → index 已存在,不做任何事
│ └─ 404 → Indices.Create(套用下方 mappings)
└─ 連線成功後才能 IndexPages / Query
重點:
- 第一次連到空的 OpenSearch 時,會自動建立 index 與 mappings。
- Index 一旦存在就不會更新 mapping。若要改欄位型別(例如
keyword→text),必須刪除重建(見RecreateIndex/ migration 流程)。 DeleteByQuery只能清資料,不能改 mapping。
3. 實際 Mapping
建立 index 時送出的 body(精簡版):
{
"mappings": {
"properties": {
"document_id": { "type": "long" },
"filename": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"content": { "type": "text" },
"page": { "type": "integer" },
"source": { "type": "keyword" },
"indexed_at": { "type": "date" }
}
}
}
搜尋時使用 multi_match,查詢欄位為:
content(text)filename(text)
也就是:使用者關鍵字走全文檢索路徑,不是 filename.keyword 的精確比對。
4. text 與 keyword 差在哪?
兩者都是「字串相關」的型別,但索引與查詢行為完全不同。
對照表
text | keyword | |
|---|---|---|
| 主要用途 | 全文搜尋(full-text search) | 精確比對、排序、聚合(filter / sort / agg) |
| 寫入時 | 會經過 analyzer 分詞 | 整段字串當成一個 term,不分詞 |
| 查詢方式 | match / multi_match 等 | term / terms 等 |
| 典型例子 | 內文、標題、可模糊搜的檔名 | ID、狀態碼、來源、標籤、固定枚舉值 |
用例子理解
假設寫入:
content = "Install the battery pack carefully"
filename = "User Manual Rev2.pdf"
source = "thortron_one"
content(text)
- Analyzer 可能拆成:
install,battery,pack,carefully… - 搜尋
"battery"→ 可以命中 - 搜尋
"battery pack"→ 也可以命中(依 analyzer / 查詢型態)
source(keyword)
- 索引裡只有一個 term:
thortron_one - 適合:
termfiltersource = "thortron_one" - 若誤用
match去查"thortron",行為與預期常不一致(因為它本來就不是為全文搜尋設計)
filename(text + keyword multi-field)
同一份原始字串存成兩種索引:
| 欄位路徑 | 型別 | 行為 |
|---|---|---|
filename | text | 檔名可被分詞,給 multi_match 用 |
filename.keyword | keyword | 整段檔名精確值,可做 sort / aggregation / exact filter |
ignore_above: 256 的意思:
- 單位是 bytes(不是字元數)
- 超過 256 bytes 的值 不會寫進
filename.keyword filename的 text 全文搜尋仍然可用- 目的:避免超長檔名把 terms dictionary 撐很大
5. 什麼時候該用哪一種?
選 text 若你需要
- 使用者輸入關鍵字,要「包含某詞就命中」
- 句子、段落、說明文字、可模糊搜的標題/檔名
本專案:content、filename
選 keyword 若你需要
- 完全相等才算命中(ID、狀態、來源、tag)
- 依該欄位排序
- 做 aggregation(例如依
source分組統計)
本專案:source;以及 filename.keyword
兩種都要 → Multi-field(本專案 filename 的做法)
"filename": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
查詢時:
- 全文搜 → 用
filename - 精確比對/排序 → 用
filename.keyword
6. 寫入與搜尋路徑(對應程式)
寫入(Index)
API IndexPDFs
→ fulltextsearch.IndexPDFs
→ 讀文件、確認是 PDF
→ pdf 擷取每頁文字
→ Client.IndexPages
→(可選)先刪除同 document_id 舊頁
→ bulk index:每頁一筆 document
搜尋(Query)
API Search
→ Client.Query
→ multi_match
query: <使用者關鍵字>
fields: ["content", "filename"]
注意:搜尋 沒有 打到 filename.keyword。filename.keyword 是為精確場景預留的子欄位。
7. 常見誤解
-
「字串就該用 keyword」
錯。要全文搜尋就用text;要精確值才用keyword。 -
「改 mapping 只要重新 index 資料就好」
錯。既有 index 的 field type 不能就地變更;型別變更需要刪除重建 index(本專案有RecreateIndex/ migration)。 -
「
ignore_above會讓檔名搜不到」
錯。它只影響filename.keyword;filename(text)仍可搜。 -
「
text也能拿來做精確 filter」
不建議。分詞後精確比對不可靠,精確場景請用keyword(或.keyword子欄位)。
8. 快速決策流程
這個欄位要給使用者「打關鍵字搜內容」嗎?
├─ 是 → text
│ └─ 同時也要精確比對 / 排序 / 聚合?
│ └─ 是 → text + fields.keyword(multi-field)
└─ 否 → 只要完全相等 / 排序 / 聚合?
└─ 是 → keyword
套用到本專案:
- PDF 內文 →
content=text - 檔名要搜、也可能要精確處理 →
filename=text+.keyword - 來源標記 →
source=keyword
9. 相關程式位置
| 主題 | 路徑 |
|---|---|
| Index mapping / IndexPages / Query | internal/pkgs/utilities/fulltextsearch/opensearch/client.go |
| 業務:讀 PDF、批次 index、搜尋 | internal/pkgs/utilities/fulltextsearch/impl.go |
| Document 結構 | entity/utilities/fulltextsearch/v1/models/models.go |
| REST API | adapters/api/endpoints/restful/v1/fulltextsearch/impl.go |
| Index 重建遷移 | internal/pkgs/utilities/fulltextsearch/migration.go |
| 本地 OpenSearch | app/infra/local/containers/opensearch/docker-compose.yml |
| 遷移工具說明 | playground/thomas/full-text-search/README.md |
10. 一句話記住
text:拆開來找(全文檢索)keyword:整段比對(精確值 / 排序 / 聚合)filename:兩種都要,所以做成 multi-field