1ページに100万文字書くとElasticsearchが検索エラーになる
100万文字 === 8MBなので当然検索できてほしいshokai.icon
2021年6月に/Nota/1ページに100万文字書くとElasticsearchが検索エラーになるに書いたページをコピペしてきた
修正
100万文字書いたページが検索にヒットするとElasticsearchがエラーを返す問題を修正 #5201
何がおきていたか
100万文字以上のdocumentを検索できないわけではない
むしろ検索はできてる
Elasticsearchは、検索にヒットしたdocumentをhighlightする時に、highlight対象のfieldが100万文字以上あるdocumentが1つでも含まれていればエラーを返す
この動作はElasticsearch 7.2から突然入ったらしい
解決
ES clientのsearch APIで、highlightオプションにmax_analyzed_offset: 1000000 - 1を指定する
index作成時に指定されるmax_analyzed_offsetのデフォルト値は100万
検索時のオプションの方のmax_analyzed_offsetが未指定の場合に、maxを超えるとエラーが発生する
ESのforumではこちらをもっと大きな値にするとか書かれているが、その必要はなかった
なんとmax_analyzed_offset: 1000000 - 1を指定するだけで100万文字以上のdocumentもhighlightできるようになる
今後GyazoOCRやアップロードファイルの検索で1000万文字ぐらいは検索したくなるはずなので、調査しましたshokai.icon
1000万文字は問題なく検索できる
1億文字だと今度はMongoDBのエラーで保存できなかった
以下、調査の記録
1行に100万文字かと思っていたが、1ページ内の合計で100万文字だった
Scrapboxでまれに発生しているElasticsearchエラー
https://gyazo.com/a6d9cb1e0720b14cce7a7fb52f343bf0
code:error
ResponseError: search_phase_execution_exception: illegal_argument_exception Reason: The length 1255934 of field lines in doc250579/indexscrapbox-pages-v7 exceeds the index.highlight.max_analyzed_offset limit 1000000. To avoid this error, set the query parameter max_analyzed_offset to a value less than index setting 1000000 and this will tolerate long field values by truncating them.
タイミングとしては、ScrapboxでNFDを編集・検索できるようにするを実装した頃から発生するようになった気がするshokai.icon
調査した結果、これは関係なかった
125万5934文字の行がある
index.highlight.max_analyzed_offsetのlimit 1000000を超えているらしい
https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html#highlighting-settings
2021/6/7にuiu.iconの個人project内で発生していたので、条件に心当たりないか聞いてみるのが早そうshokai.icon
1行に書ける文字数に上限あるはずだけど
minifiしたjsをcodeblockに書いてuserscript化してたりしそう
昔base64 encodeしたURLをペーストして、それが残ってるらしい
https://gyazo.com/77534015020b401a1a65193fc4364a21
base64に限らず、とにかく長いテキストがあるとエラーが発生するはず
修正案
max_analyzed_offsetを100万文字以上に上げる
もっと長い文字列を保存した場合に意味がない
1行に100万文字以上保存させない
データ処理の都合上はいい
テキストエディタとしてはダメだと思う
Elasticsearchにindexする時に、長い行は10万文字程度に分割する
これを
page.lines: [ "foo", "35万文字", "bar" ]
こうindexする
page.lines: [ "foo", "10万文字", "10万文字", "10万文字", "5万文字", "bar" ]
安全マージンをとって10万文字
今回の問題は100万文字単位で分割すれば解決するが
何かまた別のlimitに引っかかったりしそうな予感がするので
MongoDBに保存するデータに変更は無い
変更するデータはES上だけ
問題
たまたま区切られた部分にあった文字列に検索がヒットしなくなってしまう
分割位置を重複させればいい
これを
"ここが10万文字目→←ここから10万1文字目"
こう分割すると、例えば文字目→←ここで検索した時にヒットしない
[ "ここが10万文字目→", "←ここから10万1文字目" ]
1〜10万、10万1〜20万で分割している
重複させて分割する
[ "ここが10万文字目→", "文字目→←ここから10万1文字目" ]
1〜10万、9万9900〜19万9900、19万9800〜29万9800、で分割していく
重複する部分が十分に長ければ、境目が検索できないという事態は発生しない
1〜10万100文字、10万〜20万100文字、の方がきれいなコードになりそう
細かい所は実装しながら考える
これがいいと思うshokai.icon
と思ったが、実際に100万文字を投入してみたらこの実装ではダメだった
1行に100万文字ではなく、1ページの行合計で100万文字がlimitだった
エラーを再現する
100万文字の行をDBに入れる
準備
事前に/shokai/超長い1行というページを作っておく
batchで2行目に100万文字を追加する
code:tmp/insert-1000000-chars.js
import mongoose from 'mongoose'
import models from '../src/server/models/'
import { IdGenerator } from '../src/share/id'
const Project = mongoose.model('project')
const Page = mongoose.model('page')
const generateNewId = IdGenerator()
main()
async function main () {
console.log('insert-1000000-chars start')
console.time('insert-1000000-chars done')
await models.connect()
try {
// あらかじめこのページを作っておく
const project = await Project.findOne({ nameLc: 'shokai' })
const page = await Page.findOne({ projectId: project._id, titleLc: '超長い1行' })
const now = Math.floor(Date.now() / 1000)
page.lines.push({
id: generateNewId(),
userId: page.lines0.userId,
created: now,
updated: now,
text: '0123456789'.repeat(100000) + 'a' // 1000001 chars
})
await page.save()
await project.updateOne({ $unset: { esIndexVersion: true } })
} catch (err) {
console.error(err.stack || err)
}
await models.disconnect()
console.timeEnd('insert-1000000-chars done')
}
データ投入
$ docker-compose run --rm worker babel-node tmp/insert-1000000-chars.js
project.esIndexVersionもクリアされる
/shokaiにアクセスするとES index再構築が走る
「超長い」で検索
https://gyazo.com/7cb6c57f21ca1ebe49f00f15d252d4bd
3回実行した
code:error
ResponseError: search_phase_execution_exception: illegal_argument_exception Reason: The length 3000011 of field lines in doc855/indexscrapbox-pages-v7 exceeds the index.highlight.max_analyzed_offset limit 1000000. To avoid this error, set the query parameter max_analyzed_offset to a value less than index setting 1000000 and this will tolerate long field values by truncating them.
300万11文字になってる
1行が100万超えではなく、ページ全体で100万文字までみたいだな
ES側の設定で、検索結果が100万文字以上だったらhighlightしないとか勝手にやってほしい
ESに投入するdocumentを分割する
1つのmongodb documentを複数のES documentに分割
100万文字毎
ページ編集後のupdateが難しい
page rankも
いっそ1行ずつ別々のES documentに分割した方が楽?
そもそも表示すらできないページのバックエンドを気合い入れるのわけわからん
Scrapboxに100万文字書くとページが開けなくなり、削除もできなくなる
とりあえずESに投入する時点で100万文字以上は捨てる?
searchする時にnode.jsでmax_analyzed_offset: 1000000 - 1オプションをセット
https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html#highlighting-settings より
正数値を与えると、その位置まで検索する
index.highlight.max_analyzed_offsetを上書きするオプションではない
超長いで検索
問題なく検索できるようになったshokai.icon*10
https://gyazo.com/0b9330b939b066cbdf74d4e3244c5c91
レスポンスも速い
超長い 1234で検索
検索できるが、Reactがフリーズする
https://gyazo.com/36588aa787232e00307870fd4dbad178
検索結果のハイライト部分の文字列が長すぎるようだ
解決方法
検索結果のハイライトは100文字ぐらいで切る
CSSのoverflow: hidden;で右側は隠れているし
fragment_size: Default 100は関係ないパラメータだった
変更しても100万文字返ってくる
$ curl 'http://localhost:4000/api/pages/shokai/search/query?q=%E8%B6%85%E9%95%B7%E3%81%84%201234'
1000万文字入れても検索はできてほしい
highlightされるのが先頭の100万文字までというだけで、それ以降にある文字も検索にヒットはする
100万文字なんてたった8MBなんだし
できた
検証
10万文字を100回書き込み、最後に「ここが最後の行」という行を追加する
合計1000万文字以上
準備
事前に/shokai/超長い1行というページを作っておく
batchを実行すると、2行目以降に1000万文字のデータが書き込まれます
code:tmp/insert-10m-chars.js
import mongoose from 'mongoose'
import models from '../src/server/models/'
import { IdGenerator } from '../src/share/id'
const Project = mongoose.model('project')
const Page = mongoose.model('page')
const generateNewId = IdGenerator()
main()
async function main () {
console.log('insert-10m-chars start')
console.time('insert-10m-chars done')
await models.connect()
try {
const project = await Project.findOne({ nameLc: 'shokai' })
const page = await Page.findOne({ projectId: project._id, titleLc: '超長い1行' })
page.lines.splice(1) // タイトル行以外を削除
const now = Math.floor(Date.now() / 1000)
for (let i = 0; i < 100; i++) {
page.lines.push({
id: generateNewId(),
userId: page.lines0.userId,
created: now,
updated: now,
text: '0123456789'.repeat(10000) + 'a' // 100001 chars
})
}
page.lines.push({
id: generateNewId(),
userId: page.lines0.userId,
created: now,
updated: now,
text: 'ここが最後の行'
})
await page.save()
await project.updateOne({ $unset: { esIndexVersion: true } }) // ES indexを初期化
} catch (err) {
console.error(err.stack || err)
}
await models.disconnect()
console.timeEnd('insert-10m-chars done')
}
実行
$ docker-compose run --rm worker babel-node tmp/insert-10m-chars.js
["超長い1行", 10万1文字 × 100行, "ここが最後の行"]というデータができる
合計1000万112文字
超長い 最後で検索
https://gyazo.com/ed7a8689c8da76420dfb3629402de687
検索ヒットし、どちらもハイライトされた
もちろん10万文字の行に含まれている単語も検索にヒットする
https://gyazo.com/d40890f79b1649aaf6fbdeed1c4f0cea
今後
もっとElasticsearchのエラーを理解可能な文字列にしたい
Scrapboxに100万文字書くとページが開けなくなり、削除もできなくなる