📖

Plan-and-Execute × Elasticsearch × Ollama で“惜しい検索”を卒業する

に公開

はじめに

「社内ドキュメントを探しても欲しい情報が見つからない...」
「全文検索は厳密な単語には強いけど、言い換えた表現が拾えない」
「ベクトル検索は幅広く拾うけど、ノイズが多すぎる」

こんな "惜しい検索体験" に悩んだことはありませんか?

この記事では、Plan-and-Execute型AIエージェントElasticsearch(全文検索)Qdrant(ベクトル検索) を組み合わせて、この問題を解決する検索システムの実装方法を紹介します。

特徴的なのは、Ollama(ローカルLLM) を使用することで OpenAIなしでも動作 する点です。プライバシーが重要な社内システムでも安心して使えます。

🎯 この記事で作るもの

3つの検索モードを持つ、インテリジェントな検索システムを構築します:

  1. Keyword Search - Elasticsearch による高速な全文検索
  2. Semantic Search - Qdrant による意味的な類似検索
  3. AI Agent - 両者を組み合わせた賢い検索

📸 デモ

実際の動作を見てみましょう。例えば「有給休暇の申請方法は?」という質問に対して:

Keyword Search(Elasticsearch)

  • 「有給休暇」という単語を含む文書を上位3件返す
  • 高速だが、表記揺れに弱い

Semantic Search(Qdrant)

  • 「休暇申請」「休みの取り方」など言い換えでもヒット
  • 幅広く拾うが、ノイズも多い

AI Agent

  1. まず Elasticsearch で規程名を検索
  2. 不足があれば Qdrant で補完
  3. 重複を除外して再評価
  4. 根拠の抜粋付きで最終回答を生成

ユーザーはモード選択に悩まず、最短で答えにたどり着ける のがポイントです。

🏗️ システムアーキテクチャ

┌─────────────┐
│  Next.js UI │
└──────┬──────┘
       ▼
┌──────────────┐
│  API Routes  │
└──────┬───────┘
       ▼
┌─────────────────────────┐
│ Plan-and-Execute Agent  │
└──┬──────────────────┬───┘
   ▼                  ▼
┌────────────┐   ┌──────────┐
│Elasticsearch│   │  Qdrant  │
│  (BM25)    │   │ (Vector) │
└────────────┘   └──────────┘
       ▼
┌──────────────────┐
│  LLM Abstraction │
└──┬───────────┬───┘
   ▼           ▼
┌────────┐  ┌─────────┐
│ Ollama │  │ OpenAI  │
│(Local) │  │(Option) │
└────────┘  └─────────┘

技術スタック

コンポーネント 技術 役割
フロントエンド Next.js UIとAPI Routes
検索エンジン Elasticsearch 厳密語・規程名検索
ベクトル検索 Qdrant 言い換え・曖昧表現対応
LLM Ollama (標準) / OpenAI (オプション) 計画立案と回答生成
エージェント Plan-and-Execute型 検索戦略の自動化

🚀 セットアップ手順

1. Docker で検索エンジンを起動

docker-compose.yml を作成:

version: "3.8"
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - ES_JAVA_OPTS=-Xms1g -Xmx1g
    ports:
      - "9200:9200"
    volumes:
      - esdata:/usr/share/elasticsearch/data

  qdrant:
    image: qdrant/qdrant:latest
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - qdrant:/qdrant/storage

volumes:
  esdata:
  qdrant:

起動:

docker-compose up -d

2. Ollama のインストール

# macOS の場合
brew install --cask ollama

# モデルの取得
ollama pull llama3
ollama pull nomic-embed-text

3. 環境変数の設定

.env ファイルを作成:

ELASTICSEARCH_URL=http://localhost:9200
QDRANT_URL=http://localhost:6333
LLM_PROVIDER=ollama
OLLAMA_MODEL=llama3
EMBEDDINGS_PROVIDER=ollama
OLLAMA_EMBED_MODEL=nomic-embed-text
OPENAI_API_KEY=sk-...  # オプション

4. ドキュメントの投入

# ドキュメントをインデックスに投入
node scripts/seed.js ./seed-data

対応フォーマット:

  • PDF/DOCX: ingest-attachment プラグインでテキスト抽出
  • TXT/MD: そのまま投入

5. アプリケーションの起動

npm install
npm run dev

http://localhost:3000 にアクセスして動作確認できます。

🔧 実装の詳細

ディレクトリ構造

src/
├── pages/
│   ├── index.tsx           # UI(モード切替、結果表示)
│   └── api/
│       ├── search/
│       │   ├── manual.ts   # ES全文検索エンドポイント
│       │   └── qa.ts       # ベクトル検索エンドポイント
│       └── agent/
│           └── run.ts      # Plan-and-Executeエージェント
├── lib/
│   ├── elasticsearch.ts    # ESクライアント
│   ├── llm.ts             # LLM抽象化レイヤー
│   ├── ollama.ts          # Ollama実装
│   └── embeddings.ts      # 埋め込みベクトル生成
scripts/
└── seed.js                # ドキュメント投入スクリプト

処理フロー

  1. Plan(計画立案)

    • ユーザーの質問を分析
    • 検索戦略を決定(ES優先 or Qdrant優先)
  2. Execute(実行)

    • 選択した検索エンジンでクエリ実行
    • スニペットをメモリに蓄積
  3. Re-evaluate(再評価)

    • 収集した情報が十分か判断
    • 不足なら別の検索手法で補完(最大3ステップ)
  4. Answer(回答生成)

    • 収集した情報を統合
    • 根拠付きの最終回答を生成

💡 実装のポイント

Elasticsearch と Qdrant の使い分け

// Elasticsearch: 厳密語検索
const esResults = await client.search({
  index: 'documents',
  body: {
    query: {
      multi_match: {
        query: userQuery,
        fields: ['title^2', 'content'],
        type: 'best_fields'
      }
    }
  }
});

// Qdrant: ベクトル類似検索
const embedding = await generateEmbedding(userQuery);
const qdrantResults = await qdrantClient.search({
  collection_name: 'documents',
  vector: embedding,
  limit: 5
});

Plan-and-Execute エージェントの実装

class SearchAgent {
  async run(query) {
    // 1. 計画立案
    const plan = await this.createPlan(query);
    
    // 2. 実行ループ(最大3ステップ)
    for (let i = 0; i < 3; i++) {
      const results = await this.executeSearch(plan.currentStep);
      this.memory.add(results);
      
      // 3. 再評価
      const evaluation = await this.evaluate(this.memory);
      if (evaluation.sufficient) break;
      
      plan.updateStrategy(evaluation.feedback);
    }
    
    // 4. 最終回答生成
    return await this.generateAnswer(this.memory);
  }
}

🎭 Cline との違い

よく似たアプローチとして、VSCode の拡張機能である Cline があります。両者の違いを整理してみましょう:

項目 Cline 本検索システム
対象 コード補助・開発作業 ドキュメント検索
実行環境 CLI、ユーザー承認付き Web UI
Plan-and-Execute 計画を提示してコード修正 計画→検索→再評価→回答
特化領域 開発作業の自動化 ナレッジ検索の最適化

共通点は「半自律エージェント」であることですが、用途と設計思想が異なります。

📚 参考資料

本実装は『現場で活用するためのAIエージェント実践入門』(KS情報科学専門書)を参考にしています。

🔮 今後の展望

  • RAG(Retrieval-Augmented Generation)の強化

    • チャンクサイズの最適化
    • リランキングの実装
  • マルチモーダル対応

    • 画像・図表を含むドキュメントの検索
  • フィードバックループ

    • ユーザーの選択から学習
    • 検索精度の継続的改善

まとめ

Elasticsearch と Qdrant を Plan-and-Execute 型エージェントで統合することで、従来の「惜しい検索」を大幅に改善できました。

主なメリット:

  • 精度向上: 厳密語と言い換えの両方に対応
  • プライバシー: Ollama でローカル完結可能
  • 柔軟性: OpenAI も併用可能な設計

ぜひ皆さんの社内システムでも試してみてください!

Discussion