AI

全文検索OSS比較:Meilisearch vs Typesense vs OpenSearch でAlgolia代替をセルフホスト

オープンソースラボ編集部2026年6月13日

全文検索OSS比較:Meilisearch vs Typesense vs OpenSearch でAlgolia代替をセルフホスト

Algoliaは月$50〜(10万レコード・100万オペレーション)で、サイトの成長とともにコストが急増します。Meilisearch(Rust製・超高速・簡単)・Typesense(C++製・低レイテンシー)・OpenSearch(Elasticsearch fork・大規模対応)はOSSの全文検索エンジンで、Eコマース・ドキュメント検索・SaaS内検索などでAlgolia代替として使えます。

全文検索エンジンの選定理由

  • コスト削減: Algoliaの月$5,000〜をVPS $10/月のMeilisearchで代替
  • インスタント検索: タイピングのたびに結果が変わるライブ検索(デバウンス不要な低レイテンシー)
  • タイポ許容: 「meilserach」→「meilisearch」のような入力ミスを自動補正
  • ファセット検索: カテゴリ・価格帯・評価での絞り込みをUI付きで実装
  • 日本語対応: ひらがな/カタカナ/漢字の全文検索・読み仮名検索

主要ツールの概要

Meilisearch

2018年からRustで開発されている全文検索エンジンです。GitHubスター47k+。「10行で使い始められる」のが特徴で、REST APIが直感的・セットアップが簡単・タイポ許容・ハイライト・ファセット・地理検索がデフォルトで使えます。日本語対応も早く、Next.jsでのインスタント検索実装が最も簡単です。

# MeilisearchをDockerで起動
docker run -d   --name meilisearch   -p 7700:7700   -e MEILI_MASTER_KEY=your-master-key   -v /path/to/meili_data:/meili_data   getmeili/meilisearch:v1.8

# APIキーの確認(マスターキーでアクセス)
curl http://localhost:7700/health
# => {"status":"available"}

# インデックスを作成してドキュメントを追加
curl -X POST 'http://localhost:7700/indexes/products/documents'   -H 'Authorization: Bearer your-master-key'   -H 'Content-Type: application/json'   --data '[
    {"id": 1, "name": "MacBook Pro M4", "category": "laptop", "price": 248800, "rating": 4.8},
    {"id": 2, "name": "iPad Air M2", "category": "tablet", "price": 74800, "rating": 4.6},
    {"id": 3, "name": "AirPods Pro", "category": "audio", "price": 39800, "rating": 4.7}
  ]'
// Next.js App Router + Meilisearch でインスタント検索を実装
// npm install meilisearch

// lib/meilisearch.ts
import { MeiliSearch } from 'meilisearch';

export const searchClient = new MeiliSearch({
  host: process.env.NEXT_PUBLIC_MEILISEARCH_URL!,
  apiKey: process.env.NEXT_PUBLIC_MEILISEARCH_API_KEY!,  // 検索専用キー
});

// app/search/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { searchClient } from '@/lib/meilisearch';

export default function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [filters, setFilters] = useState({ category: '', maxPrice: 100000 });

  useEffect(() => {
    const search = async () => {
      if (!query) return;
      const { hits } = await searchClient.index('products').search(query, {
        limit: 20,
        filter: [
          filters.category ? `category = "${filters.category}"` : null,
          `price <= ${filters.maxPrice}`,
        ].filter(Boolean),
        facets: ['category', 'rating'],
        attributesToHighlight: ['name'],
        highlightPreTag: '<mark class="bg-yellow-200">',
        highlightPostTag: '</mark>',
        typoTolerance: { enabled: true, minWordSizeForTypos: { oneTypo: 4 } },
      });
      setResults(hits);
    };
    const debounce = setTimeout(search, 150);
    return () => clearTimeout(debounce);
  }, [query, filters]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="商品を検索..."
        className="w-full border rounded px-4 py-2"
      />
      <div className="grid grid-cols-3 gap-4 mt-4">
        {results.map((hit: any) => (
          <div key={hit.id} className="border rounded p-4">
            <h3 dangerouslySetInnerHTML={{ __html: hit._formatted.name }} />
            <p>¥{hit.price.toLocaleString()}</p>
          </div>
        ))}
      </div>
    </div>
  );
}
// Meilisearchのインデックス設定(日本語対応)
// scripts/setup-meilisearch.ts

import { MeiliSearch } from 'meilisearch';
const client = new MeiliSearch({ host: process.env.MEILISEARCH_URL!, apiKey: process.env.MEILISEARCH_MASTER_KEY! });

// インデックス設定
await client.index('products').updateSettings({
  searchableAttributes: ['name', 'description', 'tags'],
  filterableAttributes: ['category', 'price', 'rating', 'in_stock'],
  sortableAttributes: ['price', 'rating', 'created_at'],
  faceting: {
    maxValuesPerFacet: 50,
  },
  typoTolerance: {
    enabled: true,
    minWordSizeForTypos: { oneTypo: 4, twoTypos: 8 },
  },
  pagination: { maxTotalHits: 1000 },
  // 日本語の読み仮名検索をサポート
  dictionary: ['MacBook', 'iPad', 'AirPods'],
});

// 検索専用APIキーを生成(フロントエンドに渡す)
const searchKey = await client.generateTenantToken(
  process.env.MEILISEARCH_SEARCH_API_KEY_UID!,
  { indexes: [{ indexUid: 'products', rules: ['search'] }] }
);
console.log('Search API Key:', searchKey);

Typesense

2019年からC++で開発されている全文検索エンジンです。GitHubスター20k+。Algolia互換のAPIを持ち(typesense-instantsearch-adapterでAlgoliaのInstantSearch.jsが流用可能)、メモリ使用量が非常に少なく(Meilisearchの1/3以下)レイテンシーが一貫して低いです。マルチノードクラスター対応でHA(高可用性)構成が簡単です。

# Typesenseをdocker-composeで起動(シングルノード)
version: "3"

services:
  typesense:
    image: typesense/typesense:0.26.0
    restart: unless-stopped
    ports:
      - "8108:8108"
    volumes:
      - typesense_data:/data
    command: '--data-dir /data --api-key=your-api-key --enable-cors'

volumes:
  typesense_data:
// TypesenseでAlgolia InstantSearch.jsを流用する
// npm install typesense typesense-instantsearch-adapter instantsearch.js

import TypesenseInstantSearchAdapter from 'typesense-instantsearch-adapter';
import instantsearch from 'instantsearch.js';
import { searchBox, hits, refinementList, stats, pagination } from 'instantsearch.js/es/widgets';

const typesenseAdapter = new TypesenseInstantSearchAdapter({
  server: {
    apiKey: process.env.NEXT_PUBLIC_TYPESENSE_SEARCH_KEY!,
    nodes: [{ host: 'search.yoursite.com', port: 443, protocol: 'https' }],
  },
  additionalSearchParameters: {
    queryBy: 'name,description,tags',
    sortBy: '_text_match:desc,rating:desc',
    numTypos: 1,
    typoTokensThreshold: 1,
  },
});

const search = instantsearch({
  indexName: 'products',
  searchClient: typesenseAdapter.searchClient,
});

search.addWidgets([
  searchBox({ container: '#search-box', placeholder: '商品を検索...' }),
  hits({ container: '#hits', templates: {
    item: (hit) => `<div><h3>${hit._highlightResult.name.value}</h3><p>¥${hit.price}</p></div>`,
  }}),
  refinementList({ container: '#category-filter', attribute: 'category' }),
  stats({ container: '#stats' }),
  pagination({ container: '#pagination' }),
]);

search.start();

OpenSearch

ElasticsearchのBSL移行(2021年)を受けてAWSが主導でフォークしたOSSです。GitHubスター9.5k+。Elasticsearchと高い互換性を持ち、大規模(数億件以上)のドキュメント検索・複雑なアナリティクス・ログ解析に適しています。ベクター検索(k-NN)にも対応しており、セマンティック検索との組み合わせが可能です。

# OpenSearch docker-compose(開発用・シングルノード)
version: "3"

services:
  opensearch:
    image: opensearchproject/opensearch:2.13
    restart: unless-stopped
    environment:
      - discovery.type=single-node
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=Admin@1234!
      - DISABLE_SECURITY_PLUGIN=false
    ports:
      - "9200:9200"
      - "9600:9600"
    volumes:
      - opensearch_data:/usr/share/opensearch/data

  opensearch-dashboards:
    image: opensearchproject/opensearch-dashboards:2.13
    ports:
      - "5601:5601"
    environment:
      OPENSEARCH_HOSTS: '["https://opensearch:9200"]'
    depends_on:
      - opensearch

volumes:
  opensearch_data:
// OpenSearch TypeScript クライアント
// npm install @opensearch-project/opensearch

import { Client } from '@opensearch-project/opensearch';

const client = new Client({
  node: process.env.OPENSEARCH_URL!,
  auth: {
    username: 'admin',
    password: process.env.OPENSEARCH_PASSWORD!,
  },
  ssl: { rejectUnauthorized: false },
});

// 日本語対応のインデックス作成
await client.indices.create({
  index: 'articles',
  body: {
    settings: {
      analysis: {
        analyzer: {
          japanese: {
            type: 'custom',
            tokenizer: 'kuromoji_tokenizer',  // analysis-kuromoji プラグイン必要
            filter: ['kuromoji_baseform', 'lowercase'],
          },
        },
      },
    },
    mappings: {
      properties: {
        title: { type: 'text', analyzer: 'japanese' },
        content: { type: 'text', analyzer: 'japanese' },
        published_at: { type: 'date' },
        category: { type: 'keyword' },
        embedding: {   // ベクター検索(セマンティック検索)
          type: 'knn_vector',
          dimension: 1536,  // OpenAI text-embedding-3-small
          method: { name: 'hnsw', space_type: 'cosinesimil' },
        },
      },
    },
  },
});

// ハイブリッド検索(キーワード + ベクター)
const result = await client.search({
  index: 'articles',
  body: {
    query: {
      hybrid: {
        queries: [
          { match: { content: { query: '機械学習', boost: 1.0 } } },
          { knn: { embedding: { vector: await getEmbedding('機械学習'), k: 10 } } },
        ],
      },
    },
  },
});

機能比較表

比較項目MeilisearchTypesenseOpenSearch
ライセンスSSPL(v1.6+)GPL-3Apache-2.0
言語RustC++Java
最小RAM256MB64MB1GB+
日本語対応設定要✅ プラグイン
タイポ許容設定要
ベクター検索✅ k-NN
Algolia互換API✅ アダプター
スケール小〜中
GitHub Stars47k+20k+9.5k+

検索エンジンのインフラはDevOpsカテゴリ/categories/devopsの他のツールと組み合わせて運用します。LLMを使ったセマンティック検索の実装はAIカテゴリ/categories/llm-toolsで解説しています。

FAQ

Q. Next.js App RouterのServer Componentで検索はどう実装すればよいですか?

A. Server ComponentでMeilisearchを使う場合、APIキーをサーバー側のみで使用でき安全です。実装パターン:

// app/search/page.tsx (Server Component)
// URLのsearchParamsで検索クエリを受け取る
export default async function SearchPage({
  searchParams,
}: {
  searchParams: { q?: string; category?: string; page?: string };
}) {
  const query = searchParams.q || '';
  const category = searchParams.category;
  const page = parseInt(searchParams.page || '1') - 1;

  // サーバーサイドでMeilisearchを直接呼び出し
  const { MeiliSearch } = await import('meilisearch');
  const client = new MeiliSearch({
    host: process.env.MEILISEARCH_URL!,
    apiKey: process.env.MEILISEARCH_MASTER_KEY!, // サーバーでのみ使用
  });

  const { hits, facetDistribution, totalHits } = await client
    .index('products')
    .search(query, {
      hitsPerPage: 20,
      page,
      filter: category ? `category = "${category}"` : undefined,
      facets: ['category', 'price_range'],
    });

  return (
    <div>
      <SearchInput defaultValue={query} />  {/* Client Component */}
      <p>{totalHits}件の結果</p>
      <ProductGrid hits={hits} />
      <Pagination total={totalHits} perPage={20} current={page + 1} />
    </div>
  );
}

インクリメンタル検索(タイピング中に即時更新)はClient Componentが必要ですが、初期ロードはServer Componentで返すハイブリッドが最適です。

Q. MeilisearchとTypesenseのインポート(大量データ投入)速度を比較するとどうですか?

A. ベンチマーク(1億件のドキュメント・1レコード1KB)での参考値: Meilisearch: インデックス速度 約10万件/秒・100GB SSDを使用。Typesense: インデックス速度 約15万件/秒・消費メモリ2GB(Meilisearchより低メモリ)。大量データ投入のベストプラクティス: ①バッチサイズは1,000〜10,000件(1万件以上は効果が薄い)②インデックス中は検索可能(Meilisearch v1.xはインデックスが完了した時点でのみ検索可能→v1.10+で改善)③updateSettingsでsearchableAttributes・filterableAttributesを先に設定してからデータ投入。Supabaseとの同期: データ変更時にWebhookを使ってリアルタイムでMeilisearchを更新するパターンが一般的です(Supabase Realtime → Edge Function → Meilisearch)。

Q. OpenSearchで日本語全文検索を正しく設定する方法は?

A. OpenSearchはデフォルトで日本語の形態素解析をサポートしていません。analysis-kuromojiプラグインをインストールする必要があります。

# kuromoji プラグインのインストール
docker exec -it opensearch bin/opensearch-plugin install analysis-kuromoji

# カスタムアナライザーの設定(インデックス作成時)
curl -X PUT https://localhost:9200/articles   -H 'Content-Type: application/json'   -d '{
    "settings": {
      "analysis": {
        "tokenizer": {
          "kuromoji": {"type": "kuromoji_tokenizer", "mode": "search"}
        },
        "filter": {
          "katakana_readingform": {"type": "kuromoji_readingform"},
          "pos_filter": {"type": "kuromoji_part_of_speech", "stoptags": ["助詞", "助動詞"]}
        },
        "analyzer": {
          "japanese": {
            "tokenizer": "kuromoji",
            "filter": ["kuromoji_baseform", "pos_filter", "lowercase"]
          }
        }
      }
    }
  }'

mode: searchは長い複合語(「東京都知事」→「東京」「都」「知事」「東京都知事」)に分割します。mode: normalは単語境界のみ分割します。検索クエリと同じアナライザーを使うことが重要です。

Q. AlgoliaからMeilisearchへの移行手順を教えてください。

A. 移行は4ステップで行います。①データ移行: Algolia APIからインデックスのレコードをエクスポート(algoliasearchbrowseAll()で全件取得)→Meilisearch APIにインポート②設定移行: AlgoliaのcustomRankingsearchableAttributesfacetsをMeilisearchの対応する設定に変換③SDK差し替え: algoliasearchmeilisearch パッケージに変更。APIの構造が異なるので呼び出し方を修正④フロントエンド: react-instantsearchはMeilisearch用の@meilisearch/react-instantsearchに差し替え(またはネイティブMeilisearch SDKで自前実装)。注意点: AlgoliaのRules(クエリルール・Virtual Browse等)はMeilisearchにはないため、カスタムフィルタロジックで代替が必要です。

まとめ

ユースケース推奨ツール
簡単に始めたい・Algolia代替Meilisearch
低メモリ・高性能・Algolia互換APITypesense
大規模・ログ解析・ベクター検索OpenSearch
Next.jsのサイト内検索Meilisearch

関連外部リソース

他の記事も読む

Let's Build Together

OSS導入、自社だけで悩まない。

ツール選定から構築・運用・AI活用まで、オープンソースラボ運営元のClasslessが伴走します。初回のご相談は無料です。