AKARI Tech Blog

燈株式会社のエンジニア・開発メンバーによる技術ブログです

Elasticsearch + pgvectorで1000万ファイルに対してハイブリッド検索をする

こんにちは!

今週はDXソリューション事業本部 Devグループ の藤野が担当します。


「1000万件を超える膨大なファイル群から、いかに低コストで、かつ高精度に必要な情報を取り出すか?」
RAGシステムを運用・開発する上で、データ数に伴うコストと精度がネックになります。
弊社では業界特化のRAGシステムを数多く構築しており、その肝になる箇所は、やはり「Retrieval(検索)」にあります。
本記事では、コストを抑えつつ業務で真に使える検索パフォーマンスを実現したRetrievalアーキテクチャについてご紹介します。

TL;DR

  • Elasticsearch + pgvectorで構築
  • コストをAzure AI Searchと比較して1/5に抑えた
  • 実際に業務で使えるパフォーマンスを発揮できている

Azure AI Searchとの比較

 今回の検索システムは、Azure 上での構築が必須条件でした。Azure 環境で検索システムを構築する際、まず検討の対象となるのが Azure AI Search です。
azure.microsoft.com

 Azure AI Search は、OCR(光学的文字認識)やセマンティックランカーといった高度な機能を統合した検索パイプラインを構築でき、全てのシステムを Azure 上で完結させられるという大きなメリットがあります。
 しかしながら、以下の3つの主な理由から、今回は Azure AI Search の採用を見送りました。

コスト

Azure AI Search は高機能である一方、高コストになります。
2025年6月現在でStandard S3で1サービス(1TB) = 約380,000円/月になります。
azure.microsoft.com


独自アルゴリズムとの連携
 Azure AI Search のような統合パイプラインでは、統合されているがゆえに燈独自のアルゴリズムを組み込むことが難しくなる可能性があると考えました。

開発環境の構築
 Azure AI Search を利用する場合、ローカル環境での開発環境構築が難しく、現実的には開発環境用に Azure AI Search のインスタンスを別途用意し、エンジニア間で共有する必要が生じます。これは、開発の柔軟性やコスト効率の観点から課題と考えました。

これらの理由から、Azure AI Search は今回の要件にマッチせず、独自のRetrievalシステムを構築するという結論に至りました。

Elasticsearch + pgvectorの構成

こちらが今回構築した構成です。

全文検索による正確な結果、ベクトル検索(セマンティック検索)による関連性の高い結果を組み合わせたハイブリッド検索となっています。

検索手法 全文検索 ベクトル検索
使用技術 Elasticsearch pgvector
特徴 キーワードの完全または部分一致 テキストの意味的類似度による検索

各検索結果はRRF(Reciprocal Rank Fusion)アルゴリズムでランキングしたのち、クロスエンコーダーによるRerankをすることで、最終的な精度向上をしています。

Elasticsearch

ElasticsearchはElasticCloudを利用しておりAzureのVMで動かしています。
費用がAzureにまとまるため管理しやすくおすすめです。AWS, GCPにも対応しています。
www.elastic.co

こちらはインデックスの例です。

{
  "properties": {
    "document_uuid": {
      "type": "keyword"
    },
    "folder_path": {
      "type": "keyword"
    },
    "title": {
      "type": "text",
      "analyzer": "kuromoji_analyzer"
    },
    "content": {
      "type": "text",
      "analyzer": "kuromoji_analyzer"
    },
    "chunk_serial": {
      "type": "integer"
    }
  }
}

形態素解析にはkuromojiを使用しています。

sudachiも検討したのですが、ElasticCloudで運用しているため、sudachiの対応バージョンに合わせてElasticCloudのバージョンを管理するのが難しく、標準サポートされているkuromojiを採用しました。

専門用語の対応

業界における専門用語についてはユーザ辞書を定義してカバーしています。
この点は業界特化である燈の強みが生かされており、社内で蓄積された建設業界の専門用語集を活用しています。
例えば「出来形」という施工が完了した部分を指す言葉があります。
デフォルトではこのように「出来」と「形」にトークナイズされてしまい、「出来形」で検索しても「出来る」が含まれるドキュメントにヒットしてしまい検索精度が悪いです。

{
  "tokens": [
    {
      "token": "出来",
      "start_offset": 0,
      "end_offset": 2,
      "type": "word",
      "position": 0
    },
    {
      "token": "",
      "start_offset": 2,
      "end_offset": 3,
      "type": "word",
      "position": 1
    }
  ]
}

「出来形」としてユーザ辞書を登録することで単語を保ったまま検索することができます。

出来形,出来形,できがた,カスタム名詞
{
  "tokens": [
    {
      "token": "出来形",
      "start_offset": 0,
      "end_offset": 3,
      "type": "word",
      "position": 0
    }
  ]
}
pgvector

pgvectorとはベクトル検索が可能となるPostgreSQL拡張機能です。
github.com

Azure Database for PostgreSQLで動かしています。
azure.microsoft.com
4vCPU, 16GiBでストレージ4TiBだと276,447円/月です。

テーブルにはファイルのタイトルベクトルと本文ベクトルに加えてメタデータとしてフォルダパスも持たせています。

CREATE TABLE public.documents (
  document_uuid uuid DEFAULT gen_random_uuid() NOT NULL,
  folder_path ltree NOT NULL, -- 祖先フォルダを.で繋いだ文字列
  chunk_serial int8 NOT NULL, -- チャンクの順番
  title_vector vector NOT NULL, -- ファイル名のベクトル
  content_vector vector NOT NULL, -- 本文のベクトル
  created_at timestamptz DEFAULT now() NOT NULL,
  updated_at timestamptz DEFAULT now() NOT NULL,
  CONSTRAINT documents_pkey PRIMARY KEY (document_uuid)
);

CREATE INDEX idx_documents_folder_path ON public.documents USING btree (folder_path);
CREATE INDEX idx_documents_title_vector_hnsw ON public.documents USING hnsw (((title_vector)::halfvec(3072)) halfvec_cosine_ops);
CREATE INDEX idx_documents_content_vector_hnsw ON public.documents USING hnsw (((content_vector)::halfvec(3072)) halfvec_cosine_ops);

フォルダで絞り込む
特定のフォルダ以下だけを検索対象としたい場合があります。
これはltree型によりフォルダパスを表現することで絞り込みを実現しています。

例えば以下のようなフォルダ構成の場合、建設現場と取引先の両方のフォルダから東京都以下のファイルだけを検索対象としたい場合があります。

建設現場
├── 北海道
├── 青森県
├── 東京都
├── 神奈川県
└── ...(その他都道府県)

取引先
├── 北海道
├── 青森県
├── 東京都
├── 神奈川県
└── ...(その他都道府県)

ltreeを使ったクエリはこのようになります。
LIKE検索と比較すると高速に検索が可能です。

SELECT
   *
FROM
  documents
WHERE
  folder_path ~ '*.東京都.*'
;

www.postgresql.jp



インデックス
インデックスにはHNSWとコサイン類似度を採用しています。
pgvectorのvector型は2000次元までしか対応していませんが、2バイト浮動小数点数で表すhalfvec型では4000次元まで対応しています。
例えばopenaiのtext-embedding-3-largeモデルは3072次元なのでhalfvecを使います。
Scalar and binary quantization for pgvector vector search and storage | Jonathan Katz


検索クエリ例
UNION ALLでタイトルの検索結果と本文の検索結果をマージしています。
このクエリ結果に対して、タイトルと本文で重みをつけて最終的なランキングを行っています。

SELECT
    document_uuid
    ,1 - (title_vector::halfvec(3072) <=> $1) AS title_cosine_similarity
    ,1 - (content_vector::halfvec(3072) <=> $2) AS content_cosine_similarity
FROM (
    SELECT * FROM documents
    WHERE
        folder_path ~ $3
    ORDER BY
        (title_vector::halfvec(3072) <=> $1) ASC
    LIMIT
        $4
    UNION ALL
    SELECT * FROM documents
    WHERE
        folder_path ~ $3
    ORDER BY
        (content_vector::halfvec(3072) <=> $2) ASC
    LIMIT
        $4
)
;

コスト
ストレージ4TBで試算した1ヶ月あたりのコストを比較すると、圧倒的に安く抑えられました!

Elasticsearch + pgvector 311,940円
Azure AI Search 1,520,000円

課題

この構成で運用するにあたって重要な課題はデータの整合性を自前で管理する必要があるということです。
Azure AI Searchであれば一つのドキュメントを作成するだけでよいのですが、今回は同じドキュメントをElasticsearchとpgvectorで管理するため、どちらにも同じドキュメントが必ず存在することを担保する必要があります。
これはデータ投入時のアプリケーション側でロジックを組む必要があるのですが、テストを綿密に組んでここは乗り越えています。

またpgvectorを利用するにあたっては、PostgreSQLの知見が必要です。
インデックス戦略や、スロークエリの特定とチューニング、コネクション数の管理などあまりPostgreSQLを触ったことがない場合はかなりハードルが高いと思います。私はPostgreSQLをずっと扱ってきましたが、pgvectorによるインデックス生成が想定以上に重くチューニングには苦労しました。

まとめ

 LLMやRAG手法が目覚ましいスピードで進化している現状を考えると、このような構成は新しいLLMやランキングアルゴリズム、検索手法が登場した際にも、関連するコンポーネントの差し替えや更新が容易になっています。

 今後もAIの発展に追従可能な堅牢でハイパフォーマンスなシステムをアーキテクトしていきます。

We’re Hiring!

燈では共に開発を進めていけるエンジニア仲間を積極採用中です!興味がある方、ぜひカジュアル面談でお話しましょう!お待ちしています!
akariinc.co.jp

今回の記事を書いた燈メンバー🙌

www.wantedly.com