Node.jsへのMeilisearch統合ガイド:Elasticsearch代替となる50ms未満の超高速全文検索とタイポ耐性の実現

Development tutorial - IT technology blog
Development tutorial - IT technology blog

検索機能の課題: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つです。ぜひ今すぐコンテナを立ち上げて、プロジェクトへの導入を試してみてください。

Share: