Go/Golang は私のお気に入りの言語の 1 つです。私はミニマリズムとそのクリーンさが大好きです。構文的には非常にコンパクトで、物事をシンプルに保つために非常に努力しています (私は KISS 原則の大ファンです)。
私が最近直面した大きな課題の 1 つは、高速な検索エンジンを構築することです。確かに、SOLR や ElasticSearch などのオプションはあります。どちらも非常にうまく機能し、スケーラビリティが非常に高いですが、依存関係をほとんどまたはまったく持たずにデプロイをより速く簡単に行うことで、検索を簡素化する必要がありました。
再ランク付けできるように、結果をすぐに返すには十分な最適化が必要でした。 C/Rust はこれに適しているかもしれませんが、私は開発スピードと生産性を重視しています。 Golang は両方の長所を備えたものだと思います。
この記事では、Go を使用して独自の検索エンジンを構築する方法の簡単な例を説明します。驚くでしょう。思っているほど複雑ではありません。
理由はわかりませんが、Golang はある意味 Python に似ているように感じます。構文は非常に理解しやすいです。おそらく、随所にセミコロンや括弧がないこと、または見苦しい try-catch ステートメントがないことが挙げられます。おそらくそれは素晴らしい Go フォーマッタかもしれませんが、わかりません。
とにかく、Golang は単一の自己完結型バイナリを生成するため、運用サーバーにデプロイするのは非常に簡単です。 「ビルドに行って」実行可能ファイルを交換するだけです。
まさに私が必要としていたものです。
いいえ、それはタイプミスではありませんか? Bleve は、強力で使いやすく、非常に柔軟な Golang 用の検索ライブラリです。
Go 開発者は通常、サードパーティのパッケージを疫病のように避けます。サードパーティのパッケージを使用することが合理的な場合もあります。 Bleve は高速で、適切に設計されており、使用を正当化するのに十分な価値を提供します。
さらに、私が「Bleve」した理由は次のとおりです:
Golang の大きな利点の 1 つは自己完結型で、バイナリが 1 つだけであることです。そのため、その感覚を維持し、ドキュメントの保存とクエリに外部 DB やサービスを必要としないようにしたいと考えました。 Bleve はメモリ内で実行され、Sqlite と同様にディスクに書き込みます。
簡単に拡張できます。これは単なる Go コードなので、必要に応じてライブラリを簡単に調整したり、コードベースで拡張したりできます。
高速: 1,000 万件のドキュメントにわたる検索結果は、フィルタリングを含めてわずか 50 ~ 100 ミリ秒しかかかりません。
ファセット: ある程度のレベルのファセット サポートがなければ、最新の検索エンジンを構築することはできません。 Bleve は、範囲や単純なカテゴリ数などの一般的なファセット タイプを完全にサポートしています。
高速インデックス作成: Bleve は SOLR よりも若干遅いです。 SOLR は 30 分で 1,000 万件のドキュメントのインデックスを作成できますが、Bleve では 1 時間以上かかりますが、1 時間程度でも私のニーズには十分であり、十分な速さです。
高品質の結果。 Bleve はキーワード結果でうまく機能しますが、一部のセマンティック タイプの検索も Bleve で非常にうまく機能します。
高速スタートアップ: 再起動またはアップデートの展開が必要な場合、Bleve を再起動するのにかかる時間はわずか数ミリ秒です。メモリ内でインデックスを再構築するための読み取りのブロックがないため、再起動後わずか数ミリ秒で問題なくインデックスを検索できます。
Bleve では、「インデックス」はデータベース テーブルまたはコレクション (NoSQL) と考えることができます。通常の SQL テーブルとは異なり、すべての単一列を指定する必要はありません。基本的に、ほとんどのユースケースではデフォルトのスキーマを使用できます。
Bleve インデックスを初期化するには、次の手順を実行できます:
mappings := bleve.NewIndexMapping() index, err = bleve.NewUsing("/some/path/index.bleve", mappings, "scorch", "scorch", nil) if err != nil { log.Fatal(err) }
Bleve はいくつかの異なるインデックス タイプをサポートしていますが、いろいろ試した結果、「scorch」インデックス タイプが最高のパフォーマンスを提供することがわかりました。最後の 3 つの引数を渡さない場合、Bleve はデフォルトで BoltDB を使用します。
Bleve へのドキュメントの追加は簡単です。基本的に、インデックスには任意のタイプの構造体を保存できます:
type Book struct { ID int `json:"id"` Name string `json:"name"` Genre string `json:"genre"` } b := Book{ ID: 1234, Name: "Some creative title", Genre: "Young Adult", } idStr := fmt.Sprintf("%d", b.ID) // index(string, interface{}) index.index(idStr, b)
大量のドキュメントのインデックスを作成する場合は、バッチ処理を使用することをお勧めします。
// You would also want to check if the batch exists already // - so that you don't recreate it. batch := index.NewBatch() if batch.Size() >= 1000 { err := index.Batch(batch) if err != nil { // failed, try again or log etc... } batch = index.NewBatch() } else { batch.index(idStr, b) }
お気づきのとおり、レコードをバッチ処理してインデックスに書き込むなどの複雑なタスクは、ドキュメントのインデックスを一時的に作成するコンテナを作成する「index.NewBatch」を使用して簡素化されます。
その後はループしながらサイズを確認し、バッチ サイズの制限に達したらインデックスをフラッシュします。
Bleve は、検索ニーズに応じて選択できる複数の異なる検索クエリ パーサーを公開しています。この記事を短くわかりやすくするために、標準のクエリ文字列パーサーを使用することにします。
searchParser := bleve.NewQueryStringQuery("chicken reciepe books") maxPerPage := 50 ofsset := 0 searchRequest := bleve.NewSearchRequestOptions(searchParser, maxPerPage, offset, false) // By default bleve returns just the ID, here we specify // - all the other fields we would like to return. searchRequest.Fields = []string{"id", "name", "genre"} searchResults, err := index.Search(searchResult)
これらの数行だけで、メモリとリソースの使用量を抑えながら良好な結果を提供する強力な検索エンジンが完成します。
これは検索結果の JSON 表現です。「ヒット」には一致するドキュメントが含まれます。
{ "status": { "total": 5, "failed": 0, "successful": 5 }, "request": {}, "hits": [], "total_hits": 19749, "max_score": 2.221337297308545, "took": 99039137, "facets": null }
前述したように、Bleve は、スキーマで設定することなく、すぐに使える完全なファセット サポートを提供します。たとえば、書籍「ジャンル」をファセットするには、次の操作を行うことができます:
//... build searchRequest -- see previous section. // Add facets genreFacet := bleve.NewFacetRequest("genre", 50) searchRequest.AddFacet("genre", genreFacet) searchResults, err := index.Search(searchResult)
先ほどの searchRequest をわずか 2 行のコードで拡張します。 「NewFacetRequest」は 2 つの引数を受け取ります:
フィールド: ファセットのインデックス内のフィールド (文字列)。
サイズ: カウントするエントリの数 (整数)。したがって、この例では、最初の 50 ジャンルのみがカウントされます。
上記により、検索結果の「ファセット」が埋められます。
次に、ファセットを検索リクエストに追加するだけです。これは「ファセット名」と実際のファセットを受け取ります。 「ファセット名」は、検索結果でこの結果セットが表示される「キー」です。
「QueryStringQuery」パーサーを使用すると、かなりのメリットが得られます。場合によっては、「1 つが一致する必要がある」など、複数のフィールドに対して検索語を照合し、少なくとも 1 つのフィールドが一致する限り結果を返したい場合など、より複雑なクエリが必要になることがあります。
これを実現するには、「論理和」および「結合」クエリ タイプを使用できます。
結合クエリ: 基本的に、複数のクエリを連鎖させて 1 つの巨大なクエリを形成できます。すべての子クエリは少なくとも 1 つのドキュメントと一致する必要があります。
論理和クエリ: これにより、前述の「1 つが一致する必要がある」クエリを実行できるようになります。 x 個のクエリを渡し、少なくとも 1 つのドキュメントと一致する必要がある子クエリの数を設定できます。
論理和クエリの例:
mappings := bleve.NewIndexMapping() index, err = bleve.NewUsing("/some/path/index.bleve", mappings, "scorch", "scorch", nil) if err != nil { log.Fatal(err) }
前に「searchParser」を使用した方法と同様に、「disjunction Query」を「searchRequest」のコンストラクターに渡すことができます。
まったく同じではありませんが、これは次の SQL に似ています:
type Book struct { ID int `json:"id"` Name string `json:"name"` Genre string `json:"genre"` } b := Book{ ID: 1234, Name: "Some creative title", Genre: "Young Adult", } idStr := fmt.Sprintf("%d", b.ID) // index(string, interface{}) index.index(idStr, b)
「query.Fuzziness=[0 or 1 or 2]」を設定することで、検索のあいまいさを調整することもできます
接続クエリの例:
// You would also want to check if the batch exists already // - so that you don't recreate it. batch := index.NewBatch() if batch.Size() >= 1000 { err := index.Batch(batch) if err != nil { // failed, try again or log etc... } batch = index.NewBatch() } else { batch.index(idStr, b) }
構文が非常に似ていることがわかります。基本的に、「結合」クエリと「論理和」クエリを同じ意味で使用できます。
これは SQL では次のようになります:
searchParser := bleve.NewQueryStringQuery("chicken reciepe books") maxPerPage := 50 ofsset := 0 searchRequest := bleve.NewSearchRequestOptions(searchParser, maxPerPage, offset, false) // By default bleve returns just the ID, here we specify // - all the other fields we would like to return. searchRequest.Fields = []string{"id", "name", "genre"} searchResults, err := index.Search(searchResult)
要約すると、すべての子クエリを少なくとも 1 つのドキュメントと一致させたい場合は「結合クエリ」を使用し、少なくとも 1 つの子クエリと一致させる必要があるが、必ずしもすべての子クエリを一致させる必要はない場合は「分離クエリ」を使用します。
速度の問題が発生した場合、Bleve を使用すると、複数のインデックス シャードにデータを分散し、1 つのリクエストでそれらのシャードをクエリすることもできます。次に例を示します。
{ "status": { "total": 5, "failed": 0, "successful": 5 }, "request": {}, "hits": [], "total_hits": 19749, "max_score": 2.221337297308545, "took": 99039137, "facets": null }
シャーディングは非常に複雑になる可能性がありますが、上で見たように、Bleve はすべてのインデックスを自動的に「マージ」し、インデックス全体を検索し、検索した場合と同じように 1 つの結果セットで結果を返すため、多くの手間が軽減されます。単一のインデックス。
私はシャーディングを使用して 100 個のシャードを検索しています。検索プロセス全体は平均してわずか 100 ~ 200 ミリ秒で完了します。
次のようにシャードを作成できます:
//... build searchRequest -- see previous section. // Add facets genreFacet := bleve.NewFacetRequest("genre", 50) searchRequest.AddFacet("genre", genreFacet) searchResults, err := index.Search(searchResult)
各ドキュメントに必ず一意の ID を作成するか、インデックスを台無しにすることなくドキュメントを追加および更新する予測可能な方法を用意してください。
これを行う簡単な方法は、シャード名を含むプレフィックスをソース DB、またはドキュメントの取得元に保存することです。そのため、挿入または更新しようとするたびに、「.index」を呼び出すシャードを示す「プレフィックス」を検索する必要があります。
更新について言えば、「index.index(idstr, struct)」を呼び出すだけで既存のドキュメントが更新されます。
上記の基本的な検索テクニックを GIN または標準の Go HTTP サーバーの背後に配置するだけで、非常に強力な検索 API を構築し、複雑なインフラストラクチャを展開することなく何百万ものリクエストに対応できます。
ただし、注意点が 1 つあります。ただし、Bleve はレプリケーションを API でラップできるため、レプリケーションには対応していません。ソースから読み取り、ゴルーチンを使用してすべての Bleve サーバーに更新を「一斉送信」する cron ジョブを作成するだけです。
あるいは、ディスクへの書き込みを数秒間ロックしてから、データをスレーブ インデックスに「rsync」することもできます。ただし、毎回 go バイナリを再起動する必要があるため、そうすることはお勧めしません。 .
以上がBleve: ロケットのように高速な検索エンジンを構築するにはどうすればよいですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。