レック・テクノロジー・コンサルティング株式会社TECH BLOG

※自動翻訳 / Automated translation

【DuckDB 2.0】SQLでベクトル類似度結合ができる「NEAREST joins」を試してみた

duckdb2_0_blog.jpg

はじめに

Re:Q Techブログをご覧いただきありがとうございます。データエンジニアリング部のT.sです。

先日、軽量高速なOLAPエンジンとして人気の DuckDB においてバージョン 2.0 のプレビュー版(Nightly)が公開されました。 その中でも個人的に特に注目した新機能が、テーブル同士をベクトルの類似度(Top-K)で一括結合できる NEAREST joins 構文です。

これまで CROSS JOIN や Window関数を頑張って組み合わせていたSQLでのベクトル検索がJOINでシンプルに記述できるようになりました。 本記事ではダミーの日本語ニュース記事データとオープン埋め込みモデルを用いて、その挙動,レコメンド精度,内部の実行計画を検証した結果を解説します。


結論と要点

  • ポイント1(新構文):
    DuckDB 2.0 で APPROX NEAREST k BY SIMILARITY <expr> 構文が導入され、M対NのベクトルTop-Kの導出が結合部分のSQL 1行で完結するようになった。
  • ポイント2(省メモリ):
    従来の「CROSS JOIN + ROW_NUMBER + サブクエリ」に比べ、内部的に全件ソートを行わないBounded Top-K集約(ヒープ処理)によりメモリ消費が抑えられる。
  • ポイント3(日本語データ検証):
    日本語ニュース記事データ(384次元)を用いたTop-3関連記事結合において、意図通りのトピック抽出がインメモリ処理により約6.4msで動作することを確認した。

注意事項・前提条件

  • プレビュー版(Nightly)の機能です:
    本記事で取り上げている NEAREST joins は開発中の DuckDB 2.0(Preview / Nightly ビルド) に基づいています。今後の正式リリースに向けて、構文や仕様、オプティマイザの挙動が変更される可能性があります。
  • 検証環境:
    • OS: macOS (Apple Silicon)
    • Python: 3.12(データ準備・ベクトル生成)
    • DuckDB: v2.0.0-alpha(Cyanoptera / Nightly ビルド)
    • Embedding Model: intfloat/multilingual-e5-small(384次元)
  • テキスト埋め込みの事前生成が必要:
    本検証では、Pythonで事前にテキストをベクトル化したテーブル(FLOAT[384])を用意して検証しました。

1. 概要と背景:スカラ関数から専用JOIN構文への進化

DuckDBでは、バージョン 0.10.0 より固定長配列型(FLOAT[N])同士の類似度を計算するスカラ関数 array_cosine_similarityarray_distance が提供されていました。

しかし、従来の関数呼び出しだけでは「テーブルAの全レコードに対してテーブルBから最も意味が近いTop-3件を結合する」といった処理を行う際に、総当たり結合(CROSS JOIN)とWindow関数を組み合わせる必要がありました。

DuckDB 2.0 では、この類似度評価が 第1級のSQL結合句(JOIN構文) としてネイティブに統合されました。

duckdb2記事:従来のベクトル類似度結合との比較


2. 従来手法とのコード比較(Before & After)

「新着記事テーブル(5件)」の各記事に対して、「過去記事アーカイブ(25件)」からコサイン類似度が高い上位3件を一括抽出するクエリを比較します。

従来のSQL(CROSS JOIN + Window関数 + CTE)

従来のSQL(CROSS JOIN + Window関数 + CTE)

DuckDB 2.0 の新構文(NEAREST Join)

DuckDB 2.0 の新構文(NEAREST Join)

比較まとめ

スクリーンショット 2026-09-04 011551.png

【構文仕様のポイント】

  • ON 句の省略と事前フィルタ:
    類似度そのものが結合条件となるため、通常の ON 句なしで直感的に全結合Top-Kを実行できます(必要に応じて ON <condition>WHERE 句を併用した事前絞り込みも可能です)。

  • 距離メトリクスに応じた使い分け:
    コサイン類似度や内積など「値が大きいほど近い」ものは BY SIMILARITY、ユークリッド距離など「値が小さいほど近い」ものは BY DISTANCE を使用します。


3. 検証:日本語ニュースデータで関連記事Top-3をレコメンド

3.1 検証データセットの全体像(過去記事アーカイブ 25件 & 新着記事 5件)

JOINの対象エンティティは、新着記事5件と過去記事25件を想定します。 新着記事それぞれに対し、どの過去記事が近しいのかを記事タイトルと本文をベクトル化したデータを用いて比較しTop3を引き当てます。

検証結果の確認用に、過去記事データのカテゴリ、タイトルを掲載します。

【データ詳細】▼クリックして過去記事アーカイブ(全25件)の一覧を展開・確認する ダミーデータ:結合時の右側のエンティティのデータ一覧

新着記事(関連記事を検索する側のデータ:5件)

  1. [IT・テクノロジー] 最新スマートフォンに搭載されるオンデバイス言語モデルと専用AIコプロセッサの仕様
  2. [家電・ガジェット] 自律走行型ロボット掃除機における光学センサーベースの障害物マッピングと自動モップ清掃機構
  3. [映画・エンタメ] 長編劇場アニメーション作品の海外映画市場における興行収入動向と配給戦略
  4. [スポーツ] メジャーリーグにおける打者の打球速度(Exit Velocity)と長打率の定量的評価
  5. [グルメ・ライフ] 家庭用エスプレッソマシンにおけるボイラー圧力制御とスチームミルクの気泡微細化技術

3.2 テキストのベクトル化とテーブル定義(DDL)

本検証では、Pythonのオープンソース埋め込みモデル(intfloat/multilingual-e5-small / 384次元) を用いて事前にテキストをベクトル化しDuckDBのテーブルに格納しています。

【ベクトル化の対象テキストについて】 本検証では、「カテゴリ名(IT、家電等)」という明示的なメタデータはベクトル化の入力テキストに含めていません。純粋に 「記事タイトル + 本文(body)」の自然言語テキストのみ を結合して埋め込みモデルに渡しています。

【実装コード】▼クリックしてベクトル化スクリプト&DDLを展開・確認する

Pythonによる事前ベクトル化コード

E5系モデルの仕様に従い検索対象(過去記事)には passage:、検索元(新着記事)には query: というプレフィックスを付与してベクトルを生成しL2正規化を行っています(※カテゴリ文字列は含めず、タイトル+本文のみを渡しています)。 Pythonによる事前ベクトル化コード

DuckDB テーブル定義

生成した384次元のベクトルを、DuckDBの固定長配列型 FLOAT[384] としてテーブルに格納します。

DuckDB側の検証用テーブル定義


3.3 全結合結果(各新着記事に対する Top-1〜Top-3)

以下は、DuckDB 2.0 CLI(v2.0.0-alpha) 上で実際に news_vectors.duckdb を開きNEAREST 3 結合クエリを実行した画面です。

duckdb2_nearest_joins_sql_result.png

実行結果を見ると、5ジャンル25件のアーカイブの中から すべての新着記事で同じカテゴリの関連記事が上位3件にヒット しています。

カテゴリ名を使わず「タイトル+本文」の文章だけでベクトル化していますが、文脈や専門用語を捉えて的確にマッチングできていることがわかります。

各記事に紐づいたTop3の過去記事(コサイン類似度が高い上位3件)

記事に紐づいたTop3の過去記事1

記事に紐づいたTop3の過去記事2

記事に紐づいたTop3の過去記事3

記事に紐づいたTop3の過去記事4

記事に紐づいたTop3の過去記事5

4. 仕組みと実行計画の解剖

EXPLAIN文を用いて、DuckDB 2.0 のオプティマイザがこのクエリをどのように実行しているかを確認しました。 NEAREST joins実行時のクエリオプティマイザのログ解説インフォグラフィック画像

処理時間の比較(実測値)

今回の検証データセット(新着5件 × 過去25件 = 125直積)を用いて、同一マシン環境(macOS / Apple Silicon)上で従来手法(CROSS JOIN + Window関数)と新手法(NEAREST Join)の処理時間を測定しました。

ダミーデータでの検証結果

今回の検証データ(125件の組み合わせ)のような極小規模なケースでは、クエリのパースや実行計画のプランニングオーバーヘッドが支配的となるため実行時間に大きな差は現れません。

しかし、アルゴリズムの計算量としては従来の全件ソート (O(N \log N)) に対し、NEAREST join はサイズ (K) のヒープを維持する (O(N \log K)) となるためデータ件数が増大するほど中間メモリの消費量とソート負荷を抑えられる設計になっているようです。

シミュレーションしてみました。

【実装コード】▼クリックして近似計算用Pythonスクリプトを展開・確認する carbon (5).png

nearest_join_simulation_graph.png

  • 左図:理論上の比較回数の比較(計算量)
    全件ソート ($O(N \log N)$): データ件数 $N$ の増加に伴い、比較回数が急速に増加。
    ヒープ取得 ($O(N \log K)$): 比較回数の増加傾向が緩やかで、全件ソートに比べて処理負荷が大幅に抑えられる。
    例 ($N = 1,000,000$ 件の場合): ヒープ構造を使うことで、比較回数は全件ソートの約 1/6(約1,993万回 vs 約332万回)に削減。

  • 右図:中間データ保持メモリ量の比較(メモリ消費)
    全件ソート: データ全件をメモリ上に保持するため、メモリ消費量が $N$ に比例して線形に増加($N = 1,000,000$ 件で約488 MB)。
    ヒープ取得: メモリ上に保持するのは上位 $K$ 件(10件)のみのため、データ件数がどれだけ増えてもメモリ使用量は約5 KB(0.005 MB)で一定に保たれる。


5 まとめ

  • 専用ベクトルDB不要のTop-K結合:
    データに対しベクトル化を事前に行っておけば、外部の専用ベクトルデータベースを別途構築せずとも、DuckDB単体のSQLだけでテーブル同士のTop-K類似度結合がシンプルに完結します。

  • 今後の注目点:
    ベクトル化されたデータをどう用意するかが肝になりそうですが、その点に関しても将来的にDuckDBの vss(Vector Similarity Search)拡張等とのネイティブ連携が進むことで、数百万件規模の結合におけるさらなる高速化が期待されます。

このほかにもDuck DB 2.0には魅力的な機能がたくさん追加されていくようですので、正式版がリリースされたらまた暇を見つけて試してみたいと思います!

ここまで読んでいただきありがとうございました。

参考文献・関連リンク

[1] DuckDB 2.0 Highlights: SQL Dialect Additions (NEAREST joins)

[2] DuckDB Official Documentation: Array Functions

[3] Hugging Face: intfloat/multilingual-e5-small

[4] DuckDB Official Documentation: Python Function API

[5] DuckDB Installation Preview Guide

この記事をシェアする

  • Facebook
  • X
  • Pocket
  • Line
  • Hatena
  • Linkedin

資料請求・お問い合わせはこちら

ページトップへ戻る