検索機能の課題:SQL LIKEとElasticsearchに悩まされる理由
ECサイトやブログの検索機能を初めて実装する際、多くのエンジニアが真っ先に思いつくのがPostgreSQLやMySQLでのLIKE '%keyword%'検索です。この方法は数千件程度のデータであれば問題なく動作します。しかし、テーブルが50万件に達すると、クエリの実行に1秒以上かかるようになり、フルテーブルスキャンによってデータベースのコネクションプールが枯渇してしまいます。
次のアップグレード候補としてよく選ばれるのがElasticsearchです。非常に強力なソリューションですが、運用保守は容易ではありません。JVMのチューニングや複雑なマッピング設定に追われ、クラスタはアイドル状態でも最低4GB〜8GBのメモリを消費します。中小規模のプロジェクトにとって、このインフラコストと運用負荷は大きな負担となります。
Meilisearchはまさにその隙間を埋めるために登場しました。Rustで書かれたこの検索エンジンは、わずか数百MBのメモリ消費で50ms未満のクエリ応答速度を実現します。さらに、複雑なアナライザー設定を行うことなく、インストール直後からタイポ(打ち間違い)を自動補正するタイポ耐性(typo-tolerance)を備えています。
Meilisearchのコアコンセプト
コードを書く前に、以下の5つの重要用語を押さえておきましょう。
- Index: SQLのテーブルやMongoDBのコレクションに相当します。同じ種類のドキュメントを格納する場所です。
- Document: 1つのJSONレコードです。各ドキュメントには一意のプライマリキー(デフォルトは
id)が必須となります。 - Typo-tolerance: ユーザーの入力ミス(タイプミス)を自動修正する仕組みです。例えば「iphoen」と入力しても「iPhone」がヒットします。クエリ実行時にレーベンシュタイン距離を高速に計算するため、検索速度が低下することはありません。
- Searchable Attributes: 検索エンジンがキーワード検索の対象とするテキストフィールドの一覧です。
- Filterable & Sortable Attributes: 絞り込み(categoryやstatusなど)や並び替え(priceやcreatedAtなど)を許可するフィールドです。
実践:Node.jsへのMeilisearchの統合
ステップ1:DockerでMeilisearchを起動する
ローカル環境を構築するにはDockerを使用するのが最も手軽です。ターミナルを開き、以下のコマンドを実行します。
docker run -d --name meilisearch \
-p 7700:7700 \
-e MEILI_MASTER_KEY=my_secure_master_key_123 \
-v $(pwd)/meili_data:/meili_data \
getmeili/meilisearch:v1.7
コンテナが起動したら、cURLでヘルスチェックを行います:curl http://localhost:7700/health。{"status":"available"}が返ってくれば準備完了です。
ステップ2:SDKのインストールとクライアント接続の初期化
プロジェクトディレクトリを作成し、公式SDKをインストールします。
mkdir node-meilisearch-demo && cd node-meilisearch-demo
npm init -y
npm install meilisearch dotenv
接続クライアントを初期化するためにmeiliClient.jsファイルを作成します。
// meiliClient.js
const { MeiliSearch } = require('meilisearch');
const client = new MeiliSearch({
host: 'http://localhost:7700',
apiKey: 'my_secure_master_key_123',
});
module.exports = client;
ステップ3:インデックスの作成とタイポ耐性の設定
次に、productsインデックスを設定するためのsetupIndex.jsファイルを作成します。
// setupIndex.js
const client = require('./meiliClient');
async function configureIndex() {
const index = client.index('products');
// 優先順位を指定して検索対象フィールドを設定
await index.updateSearchableAttributes([
'name',
'description',
'category'
]);
// 絞り込みとソートに使用するフィールドを指定
await index.updateFilterableAttributes(['category', 'inStock']);
await index.updateSortableAttributes(['price']);
// タイポ耐性の微調整
await index.updateTypoTolerance({
enabled: true,
minWordSizeForTypos: {
oneTypo: 4, // 4文字以上の単語で1文字のタイポを許容
twoTypos: 8 // 8文字以上の単語で2文字のタイポを許容
},
disableOnWords: [],
disableOnAttributes: []
});
console.log('インデックスの設定が完了しました!');
}
configureIndex();
設定スクリプトを実行します:node setupIndex.js。
ステップ4:サンプルデータの投入(ドキュメントのインデックス登録)
検索エンジンに商品データを投入するため、seedData.jsファイルを作成します。
// seedData.js
const client = require('./meiliClient');
const sampleProducts = [
{
id: 'prod_1',
name: 'Keychron K2 ワイヤレスメカニカルキーボード',
description: '75%レイアウトのメカニカルキーボード。BluetoothまたはType-C接続、Gateronスイッチ搭載。',
category: 'PC周辺機器',
price: 1850000,
inStock: true
},
{
id: 'prod_2',
name: 'Logitech MX Master 3S ワイヤレスマウス',
description: '高機能エルゴノミクスマウス。8000 DPIセンサー、MagSpeed高速スクロール対応。',
category: 'PC周辺機器',
price: 2450000,
inStock: true
},
{
id: 'prod_3',
name: 'Dell UltraSharp U2723QE 4Kモニター',
description: '27インチ IPS Blackグラフィック向けモニター。4K解像度、Type-C 90W給電対応。',
category: 'モニター',
price: 12900000,
inStock: false
}
];
async function seed() {
const index = client.index('products');
const response = await index.addDocuments(sampleProducts);
console.log('Enqueued task UID:', response.taskUid);
}
seed();
Meilisearchのデータ投入処理は非同期で行われます。addDocumentsメソッドは即座にtaskUidを返すため、バックグラウンドのワーカーがインデックス処理を継続している間もアプリケーションがブロックされることはありません。
ステップ5:フィルターとキーワードハイライトを備えた検索関数の実装
実際の検索機能をテストするためにsearch.jsファイルを作成します。
// search.js
const client = require('./meiliClient');
async function searchProducts(keyword, categoryFilter = null) {
const index = client.index('products');
const searchParams = {
limit: 10,
attributesToHighlight: ['name', 'description'],
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
};
if (categoryFilter) {
searchParams.filter = `category = "${categoryFilter}"`;
}
const results = await index.search(keyword, searchParams);
console.log(`${results.estimatedTotalHits}件の結果が${results.processingTimeMs}msで見つかりました:\n`);
results.hits.forEach((item, idx) => {
console.log(`${idx + 1}. [${item.name}] - 価格: ${item.price.toLocaleString('vi-VN')} VND`);
console.log(` ハイライト一致: ${item._formatted.name}`);
});
}
// あえて「Logitech」ではなく「Logitehc」とタイポして検索
searchProducts('Logitehc');
テストを実行します:node search.js。Meilisearchはわずか2ms〜4msでマウス「Logitech MX Master 3S」を即座に返し、フロントエンドでハイライト表示できるよう<mark>タグを自動付与してくれます。
筆者のチームが最近手がけた12万SKUを超えるECプロジェクトでは、PostgreSQLのILIKEからMeilisearchへ移行したことで、検索レイテンシが850msからわずか12msへと大幅に削減されました。さらに重要なのは、Elasticsearchクラスタの設定に何週間も費やす代わりに、わずか2日で検索機能全体を完成させることができた点です。
まとめ
Meilisearchは、SQL LIKEの遅さとElasticsearchの複雑さの間にあるギャップを見事に埋めてくれます。軽量で使いやすいSDK、俊敏なタイポ耐性、そして極めて低いメモリ消費量(数万件のドキュメントでも約150MB)を備えており、Node.jsバックエンドにとって最適な選択肢の1つです。ぜひ今すぐコンテナを立ち上げて、プロジェクトへの導入を試してみてください。
