💻

リアルタイムのコードベースインデックス作成

に公開

このブログでは、CocoIndex を使ってRAGのためのコードベースのインデックス作成方法をご紹介します。
CocoIndexは、ネイティブのTree-sitterサポートとリアルタイム更新機能を備えた、コードベースのチャンク化を標準でサポートしています。

内部的にインクリメンタル処理(変更があった部分のみ再処理)を採用しているため、インデックスはほぼリアルタイムで更新されます。
たとえばコードエディタのような、変更通知を直接プッシュするソースと連携すれば、完全なリアルタイム化も可能です。
この仕組みはコード検索アプリケーションや、低遅延が求められるAIコード生成システムに関連コンテキストを提供する用途など、シームレスな開発体験に最適です。

もしこのチュートリアルが役に立ったと感じたら、CocoIndexのGitHub にスター⭐を付けていただけると嬉しいです。

Tree-sitter

CocoIndexは、Tree-sitterの機能を活用して、単なる行区切りではなく実際の構文構造に基づいてコードをインテリジェントにチャンク化します。
このように構文的に一貫性のあるチャンクを使うことで、RAGシステム向けにより効果的なインデックスを構築でき、より精度の高いコード検索やコンテキストの保持が可能になります。

Tree-sitterはパーサジェネレーターツールであり、インクリメンタルパース用のライブラリです。
Rust 🦀 で利用できます。

このプロジェクトの全コードはこちらで公開しています。
インデックス作成部分はたった約50行のPythonで実装されています。ぜひチェックしてみてください :rocket:!

前提条件

Postgresがインストールされていない場合は、インストールガイドをご覧ください。
CocoIndexは、インクリメンタル処理のためにPostgresを使用します。

インデックス作成フローの定義

フローの設計

フローのダイアグラムは、コードベースをどのように処理するかを示しています:

  1. ローカルファイルシステムからコードファイルを読み込む
  2. Tree-sitterが解析するためのコードの言語を取得するために、ファイル拡張子を抽出する
  3. Tree-sitterを使用してコードをセマンティックチャンクに分割する
  4. 各チャンクの埋め込みを生成する
  5. ベクトルデータベースに保存し、検索に利用する

1. コードベースをソースとして追加する

@cocoindex.flow_def(name="CodeEmbedding")
def code_embedding_flow(flow_builder: cocoindex.FlowBuilder, data_scope: cocoindex.DataScope):
    """
    Define an example flow that embeds files into a vector database.
    """
    data_scope["files"] = flow_builder.add_source(
        cocoindex.sources.LocalFile(path="../..",
                                    included_patterns=["*.py", "*.rs", "*.toml", "*.md", "*.mdx"],
                                    excluded_patterns=[".*", "target", "**/node_modules"]))
    code_embeddings = data_scope.add_collector()

CocoIndexのコードベースをルートディレクトリからインポートします。
インデックス化したいコードベースのパスを変更できます。
.py, .rs, .toml, .md, .mdxの拡張子を持つファイルと、.から始まるディレクトリ、target(ルートディレクトリ)、node_modules(任意のディレクトリ)を除外して、すべてのファイルをインデックス化します。

flow_builder.add_source は、サブフィールド (filename, content) を持つテーブルを作成します。詳細はドキュメントをご覧ください。

2. 各ファイルを処理し、情報を収集する

2.1 ファイル名の拡張子を抽出する

Tree-sitterでコードを解析するために、言語(または拡張子)を渡す必要があります。
各ファイルを処理する際に、ファイル名の拡張子を抽出する関数を定義します。
カスタム関数のドキュメントはこちらをご覧ください。

@cocoindex.op.function()
def extract_extension(filename: str) -> str:
    """Extract the extension of a filename."""
    return os.path.splitext(filename)[1]

各ファイルを処理し、情報を収集します。

with data_scope["files"].row() as file:
    file["extension"] = file["filename"].transform(extract_extension)

ファイル名の拡張子を抽出し、extensionフィールドに保存します。

2.2 ファイルをチャンクに分割する

Tree-sitterを使用してコードをチャンクに分割します。
SplitRecursively関数を使用して、ファイルをチャンクに分割します。
Tree-sitterと統合されているため、languageパラメータに言語を渡すことができます。
サポートされている言語名と拡張子の一覧は、こちらをご覧ください。
すべての主要言語がサポートされています。たとえば、Python、Rust、JavaScript、TypeScript、Java、C++などです。
指定されていない場合や、指定された言語がサポートされていない場合は、プレーンテキストとして扱われます。

with data_scope["files"].row() as file:
    file["chunks"] = file["content"].transform(
          cocoindex.functions.SplitRecursively(),
          language=file["extension"], chunk_size=1000, chunk_overlap=300) 

2.3 チャンクを埋め込む

SentenceTransformerEmbedを使用して、チャンクを埋め込みます。
ドキュメントはこちらをご覧ください。
🤗 Hugging Faceでは、12kモデルがサポートされています。
お気に入りのモデルを選択できます。

@cocoindex.transform_flow()
def code_to_embedding(text: cocoindex.DataSlice[str]) -> cocoindex.DataSlice[list[float]]:
    """
    Embed the text using a SentenceTransformer model.
    """
    return text.transform(
        cocoindex.functions.SentenceTransformerEmbed(
            model="sentence-transformers/all-MiniLM-L6-v2"))

各チャンクをcode_to_embedding関数を使用して埋め込み、code_embeddingsコレクターに収集します。

@cocoindex.transform_flow() は、インデックス作成とクエリ間で変換を共有するために必要です。
ベクトルインデックスを構築し、それに対してクエリを実行します。
埋め込み計算は、インデックス作成とクエリ間で一貫性が必要です。詳細はドキュメントをご覧ください。

with data_scope["files"].row() as file:
    with file["chunks"].row() as chunk:
        chunk["embedding"] = chunk["text"].call(code_to_embedding)
        code_embeddings.collect(filename=file["filename"], location=chunk["location"],
                                code=chunk["text"], embedding=chunk["embedding"])

2.4 埋め込みを収集する

埋め込みをテーブルにエクスポートします。

code_embeddings.export(
    "code_embeddings",
    cocoindex.storages.Postgres(),
    primary_key_fields=["filename", "location"],
    vector_indexes=[cocoindex.VectorIndex("embedding", cocoindex.VectorSimilarityMetric.COSINE_SIMILARITY)])

Cosine Similarityを使用して、クエリとインデックス化されたデータの類似度を測定します。
Cosine Similarityの詳細はWikiをご覧ください。

3. インデックスに対してクエリを実行する

ユーザーが提供したテキストに対して、インデックス作成フローで使用した埋め込み操作を再利用して、SQLクエリで一致させます。

def search(pool: ConnectionPool, query: str, top_k: int = 5):
    # Get the table name, for the export target in the code_embedding_flow above.
    table_name = cocoindex.utils.get_target_storage_default_name(code_embedding_flow, "code_embeddings")
    # Evaluate the transform flow defined above with the input query, to get the embedding.
    query_vector = code_to_embedding.eval(query)
    # Run the query and get the results.
    with pool.connection() as conn:
        with conn.cursor() as cur:
            cur.execute(f"""
                SELECT filename, code, embedding <=> %s::vector AS distance
                FROM {table_name} ORDER BY distance LIMIT %s
            """, (query_vector, top_k))
            return [
                {"filename": row[0], "code": row[1], "score": 1.0 - row[2]}
                for row in cur.fetchall()
            ]

ターミナルでクエリを実行するためのメイン関数を定義します。

@cocoindex.main_fn()
def _run():
    # Initialize the database connection pool.
    pool = ConnectionPool(os.getenv("COCOINDEX_DATABASE_URL"))
    # Run queries in a loop to demonstrate the query capabilities.
    while True:
        try:
            query = input("Enter search query (or Enter to quit): ")
            if query == '':
                break
            # Run the query function with the database connection pool and the query.
            results = search(pool, query)
            print("\nSearch results:")
            for result in results:
                print(f"[{result['score']:.3f}] {result['filename']}")
                print(f"    {result['code']}")
                print("---")
            print()
        except KeyboardInterrupt:
            break

@cocoindex.main_fn() デコレーターは、環境変数から設定を読み込んでライブラリを初期化します。
詳細はドキュメントをご覧ください。

インデックスのセットアップと更新

🎉 これですべての準備ができました!

インデックスのセットアップと更新を行うには、以下のコマンドを実行します。

python main.py cocoindex setup
python main.py cocoindex update

ターミナルでインデックスの更新状態を確認できます。

クエリのテスト

この時点で、CocoIndexサーバーを起動し、データに対してRAGランタイムを開発できます。
インデックスをテストするには、以下のコマンドを実行します。

python main.py

プロンプトが表示されたら、検索クエリを入力できます。たとえば: spec

ターミナルで検索結果を確認できます。

返された結果 - 各エントリには、スコア(コサイン類似度)、ファイル名、一致したコードスニペットが含まれています。

サポート

私たちは常に改善しています。また、機能や例がさらに追加されています。
この記事が気に入ったら、GitHubで⭐スターをお願いします。

ありがとうございました!

Discussion