トラック A · タスク A1
ハイブリッド検索 アプリ
キーワード、意味、フィルターを 1 つの SQL クエリで扱います。Apache Doris ドキュメントを検索する検索ボックスを作りましょう。
- 60~120 分
- 初心者向け
- 言語自由 · CLI または Web
- AI ツールの利用歓迎
困ったときは? 質問はこちら: #dev 参加先: Apache Doris Slack。メンテナーやほかの参加者がサポートします。
環境を準備
何を作るか、Doris ならどう簡単になるかを学び、クラスターを起動します。
作成するもの
単語と意味の両方を理解する検索ボックスを作ります。
BM25 で順位付けしたキーワードの完全一致、ベクトルインデックスによる意味的な近傍、構造化フィルターを、1 つの Doris テーブルと 1 つの SQL 文で実現します。Elasticsearch や別のベクトルデータベースは不要です。
小さな検索アプリを作ります(CLI、ノートブック、Web ページのいずれでも構いません)。対象は Apache Doris ドキュメントの 299 セクションです。ユーザーは、たとえば次のような質問を入力し、 "how do I store messy JSON?"カテゴリを選び、次の 3 つのモードを切り替えます:
- キーワードBM25 で順位付けした全文一致。
- ベクトル埋め込みの類似度による最近傍。
- ハイブリッドキーワードによる事前フィルターとベクトル順位付け(追加の挑戦として Reciprocal Rank Fusion)。
各結果には、ページ、セクション、スコア(キーワード)または距離(ベクトル・ハイブリッド)、ドキュメントへのリンクを表示します。「SQL を表示」を切り替えると、Doris が実行したクエリを確認できます。
概略図はコードパネルの上部にあります
Doris で取り組む利点
1 つのテーブルに 2 つのインデックス。 テキストの転置インデックスとベクトルの HNSW ANN インデックスを同じテーブルに保持します。プランナーは転置インデックスで事前フィルターし、残った候補を ANN インデックスで順位付けします。
SQL で検索エンジンの順位付け。
score()で、Lucene と同じ関連度関数 BM25 を通常の次の文で利用できます:SELECT.フィルターは通常の SQL。 カテゴリ、ページ、
INのリスト、範囲条件を次の隣に記述します:MATCH_ANYと同じWHERE句内に記述します。
パネル:テーブル定義のインデックス行
始める前に
docker pull apache/doris:all-in-one-4.1.3docker run -d --name doris -p 9030:9030 -p 8030:8030 -p 8040:8040 apache/doris:all-in-one-4.1.3docker exec -it doris mysql -uroot -h127.0.0.1 -P9030curl -LO https://github.com/morningman/demo-env/releases/download/for-hackathon/doris-hackathon-glasgow-2026.zip && unzip doris-hackathon-glasgow-2026.zip && cd doris-hackathon-glasgow-2026
- ホスト
- 127.0.0.1
- ポート
- 9030
- ユーザー
- rootパスワードなし
- データベース
- hackathon
データ
hackathon.doc_chunks :Doris 4.x ドキュメントの 47 の主要機能ページです。セクションごとに 1 行です:
CREATE TABLE doc_chunks ( id INT NOT NULL, page VARCHAR(64) NOT NULL, -- doc slug, e.g. 'hybrid-search' page_title VARCHAR(128) NOT NULL, section VARCHAR(256) NOT NULL, category VARCHAR(32) NOT NULL, -- see the 7 values below url VARCHAR(256) NOT NULL, body STRING NOT NULL, embedding ARRAY<FLOAT> NOT NULL, -- 8-dim toy vector, unit length INDEX idx_body (body) USING INVERTED PROPERTIES ("parser" = "english", "support_phrase" = "true"), INDEX idx_category (category) USING INVERTED, INDEX idx_emb (embedding) USING ANN PROPERTIES ( "index_type" = "hnsw", "metric_type" = "l2_distance", "dim" = "8"))DUPLICATE KEY(id)DISTRIBUTED BY HASH(id) BUCKETS 1PROPERTIES ("replication_num" = "1");category の値 (フィルタードロップダウン用): search, ai, lakehouse, ingestion, performance, table-design, operations.
ベクトルについて。 完全にオフラインで動かせるよう、スターターキットには英語専用の小さな 簡易埋め込み関数, a1/toy_embed.py および a1/toy_embed.js が含まれています(出力は桁単位で同じです)。テキストを、手作業で定義した 8 つの概念次元に対応付けます: 検索、ベクトルと AI、JSON、レイクハウス、取り込み、性能、データモデル、運用 。同義語リストを使い、結果を単位長に正規化します。テーブル内のベクトルも同じ関数で作成しているため、クエリベクトルは同じ空間にあります。意図的に簡素な仕組みにしていますが、これだけでもベクトル検索の利点を示せます: "store messy nested records whose schema keeps changing" は、JSON や VARIANT と書かなくても VARIANT のページに到達します。追加の挑戦では本格的なモデルに置き換えます。
パネル → テーブル:テーブル定義の全体
AI を使って作る
AI コーディングエージェントとの共同作業を推奨しています。 Doris 4.x の検索機能と AI 機能は、多くのモデルの学習データより新しいため、SQL を書かせる前に適切な情報を渡してください:
次の コンテキストプロンプト をエージェントに貼り付けます。
参照先として次を指定: このページに掲載した参考資料 および
AGENTS.md(スターターキット内)。多くのコーディングエージェントはこのファイルを自動で読み込みます。コードに組み込む前に、すべての SQL 文を実際のクラスターで実行させてください。
さらに、次を接続すると便利です: Doris MCP Server (参照: タスク A2)。エージェント自身でスキーマを確認し、クエリをテストできるようになります。
I'm building a hybrid search app on Apache Doris 4.1 for a hackathon.Connection: MySQL protocol, host 127.0.0.1, port 9030, user root, empty password,database `hackathon`. Use a plain MySQL driver and raw SQL (no ORM).Table `doc_chunks` (299 rows): id, page, page_title, section, url,category (search / ai / lakehouse / ingestion / performance / table-design / operations),body (STRING, inverted index, english parser, phrase support),embedding (ARRAY<FLOAT> NOT NULL, 8 dims, unit length, HNSW index, l2_distance).Doris SQL rules — follow them exactly:1. Keyword search uses `body MATCH_ANY 'a b'` (OR), `MATCH_ALL` (AND), `MATCH_PHRASE`. Never LIKE. The parser keeps stopwords, so pass keywords(text) from toy_embed, not the raw question. Bind the user's text as a driver parameter.2. BM25 relevance is `score()`. It only works with a MATCH_* predicate in WHERE and `ORDER BY score() DESC LIMIT n`, selected plain (`score() AS relevance`): no ROUND(), no window function, no JOIN in that SELECT. Round, rank or join it in an outer query.3. Vector search: `ORDER BY l2_distance_approximate(embedding, <vector>) ASC LIMIT n`. Pass the vector as a string and cast it: CAST(%s AS ARRAY<FLOAT>) with '[f1, ..., f8]'. Never pass a list/array as a driver parameter.4. Hybrid = MATCH_* pre-filter + vector ORDER BY; show the distance, do not select score() in that query. For fusion, rank two lists in subqueries and combine with RRF (k = 60); a1/rrf.sql in the starter kit shows the pattern.5. Query vectors come from toy_embed(text) in a1/toy_embed.py (toyEmbed in a1/toy_embed.js). Do not invent embeddings.Build: a CLI (or small web page) with keyword / vector / hybrid modes, a category filter,and a "show SQL" option. Run every SQL statement against the live cluster first.AI エージェントが陥りやすい Doris の落とし穴
| Trap | What to do instead |
|---|---|
次の使用: LIKE '%term%' によるテキスト検索 | col MATCH_ANY 'a b' (OR)、 MATCH_ALL (AND)、 MATCH_PHRASE 'a b' 。これらは転置インデックスを使います |
次の呼び出し: score() を任意のクエリで使用 | score() には次が必要: MATCH_* の述語を次に指定: WHERE および ORDER BY score() DESC LIMIT n。指定しないと Doris はクエリを拒否します |
丸め、結合、順位付けの対象: score() を同じクエリ内で行う | 次はそのまま使います: score() AS relevance を単一テーブルのクエリ内でそのまま使います。丸めや結合、 ROW_NUMBER() は外側のクエリで行ってください。次の代わりにサブクエリを使います: WITH CTE |
BM25 とベクトル距離を 1 つの次の句でソート: ORDER BY、または次を選択: score() をベクトル順位付けクエリで使用 | サポートされていません。ハイブリッド検索は次の組み合わせです: MATCH_* の事前フィルター + ベクトル ORDER BYで距離を表示するか、2 つのランキングリストを RRF で融合します |
| クエリベクトルをドライバーのパラメーターとして渡す | リストは次に変換されます: (…) (pymysql)または 8 個の個別引数(mysql2)。文字列として次を渡します: '[0.1, …]' と記述します: CAST(%s AS ARRAY<FLOAT>)、またはリテラルを直接埋め込みます |
| コサイン ANN 指標を探す | ANN の指標は l2_distance および inner_productです。ベクトルを単位長に正規化すると、L2 距離の昇順とコサイン類似度の降順は同じ順位になります |
実装する
5 マイルストーン、 M0 → M4。パネルは読んでいるステップに追従します。
データをロード
スターターキットのフォルダー内で実行:
M0 · データをロードbashdocker exec -i doris mysql -uroot -h127.0.0.1 -P9030 < seed/a1_doc_chunks.sql確認ポイントロードの最後にカテゴリごとの件数が表示されます: 7 カテゴリ、299 セクション 、合計です。
BM25 によるキーワード検索
SQL シェルを開きます(
./doris.sh sql、または上記のdocker execコマンド)。次を実行します:M1 · BM25 によるキーワード検索sqlUSE hackathon;SELECT page_title, section, score() AS relevanceFROM doc_chunksWHERE body MATCH_ANY 'vector index recall'ORDER BY relevance DESCLIMIT 10;続いて次を試します:
MATCH_ALL(すべての語を含む)とMATCH_PHRASE 'inverted index'(隣接し、指定順)。入力ボックス → SQL → 結果リストの順に接続します。ユーザー入力はドライバーパラメーターで渡し(MATCH_ANY %s)、SQL に直接埋め込まないでください。アポストロフィが 1 つあるだけでクエリが壊れます。a1/starter.pyおよびa1/starter.jsはこの方法を実装しています。質問を全文入力しましたか?english パーサーはストップワードを残すため、
'how do I …'はテーブルの約 3 分の 1 に一致します。代わりに次を渡します:keywords(text)(簡易埋め込み関数から取得)。ストップワードに加え、次の語も除外します: apache および doris。これらはほぼすべてのセクションに現れます。確認ポイント次の入力の上位結果:
vector index recallは Vector Index と Hybrid Search のページです。先頭は How does the Apache Doris vector index work?フィルターを追加
M2 · フィルターを追加sqlSELECT page_title, section, category, score() AS relevanceFROM doc_chunksWHERE body MATCH_ANY 'vector index recall' AND category = 'search'ORDER BY relevance DESCLIMIT 10;カテゴリをドロップダウンまたは CLI フラグで選べるようにします(7 つの値は「データ」に掲載)。追加条件は通常の SQL です:
category IN (…),page = ….確認ポイントVector Index のセクションは表示されなくなります。このページの分類は
aiなので、Hybrid Search が先頭に移動します。ベクトル検索
コードまたはコマンドラインから簡易埋め込み関数で質問をベクトル化します:
M3 · 質問をベクトル化pythonfrom toy_embed import toy_embed, to_sql_arrayvec = to_sql_array(toy_embed("store messy nested records whose schema keeps changing"))# '[0.005740, 0.005740, 0.999885, ...]' · CLI: python a1/toy_embed.py "..."M3 · ベクトル検索sqlSELECT page_title, section, l2_distance_approximate(embedding, [0.005740, 0.005740, 0.999885, 0.005740, 0.005740, 0.005740, 0.005740, 0.005740]) AS distFROM doc_chunksORDER BY dist ASCLIMIT 10;コードからは SQL を固定し、ベクトル文字列をパラメーターで渡します:
l2_distance_approximate(embedding, CAST(%s AS ARRAY<FLOAT>))。Python のリストや JavaScript の配列をパラメーターにすると動作しません。確認ポイント質問には JSON も VARIANT も含まれていませんが、先頭 6 件はすべて VARIANT のセクションです。同じ語のキーワード検索では Metadata Cache や Binlog のセクションも混ざります。
ハイブリッド
質問: "stream kafka changes into doris exactly once"。ベクトルはこの質問の簡易埋め込みです。キーワードフィルターは次の語を含むセクションのみを残します: kafka:
M4 · ハイブリッドsqlSELECT page_title, section, l2_distance_approximate(embedding, [0.007020, 0.007020, 0.007020, 0.007020, 0.999827, 0.007020, 0.007020, 0.007020]) AS distFROM doc_chunksWHERE body MATCH_ANY 'kafka' -- keyword pre-filter (inverted index) AND category IN ('ingestion', 'lakehouse') -- structured filterORDER BY dist ASC -- vector ranking (ANN)LIMIT 10;- L4
WHERE body MATCH_ANY 'kafka'キーワードの事前フィルター(転置インデックス) - L5
AND category IN ('ingestion', 'lakehouse')構造化フィルター - L6
ORDER BY dist ASCベクトル順位付け(ANN)
アプリでは、1 つの入力を両方の処理に渡します:
keywords(text)を次の用途に:MATCH_ANY,toy_embed(text)をベクトルに使います。MATCH_ANYは複数語に対して緩いフィルターです。厳密な条件を求める場合は「必須語」入力欄か次を用意してください:MATCH_ALL。ここでは次を選択しないでください:score()。距離を表示してください。モード切り替え(キーワード / ベクトル / ハイブリッド)と次を追加します: SQL を表示 の切り替え。同じ質問で 3 つのリストを比較し、どこが異なり、なぜ異なるかを考えてみましょう。
確認ポイントすべての結果に Kafka が含まれ、先頭は How does the Apache Doris Kafka and CDC integration work? ベクトルモードだけの場合、先頭は Pipeline Execution Engine › Overview になります(同じ質問)。
- L4
完成させる
チェックを済ませ、成果を共有し、バッジを獲得しましょう。
完了条件
チェックリスト提出してバッジを獲得
参加 → 提出 → バッジを獲得。
成果物は次の名前のフォルダーにまとめてください: ご自身の GitHub ID。コードと README を含め、README に内容、実行方法、スクリーンショットまたは GIF 1 点、使用した Doris の機能を記載します。
プルリクエストを作成 次の場所に追加してください: morningman/demo-env 配置先:
doris-hackathon-glasgow-2026/<your-github-id>/。フォークして push するか、GitHub の次の操作を使ってください: Add file → Upload files。ほかに入力する項目はありません。共有する 共有先:
#dev参加先: Apache Doris Slack:プルリクエストまたはリポジトリへのリンクと、スクリーンショットまたは短いデモを共有します。質問、提出、バッジの案内はすべてこちらで行います。
タスクに参加した方全員に次のバッジを贈ります: Apache Doris Contributor バッジ。Doris 本体へのプルリクエストがマージされている必要はありません。締め切りはないので、ご自身のペースで進めてください。チーム参加も可能ですが必須ではありません。全員の GitHub ID を次に記載してください: README.
追加の挑戦
Reciprocal Rank Fusion。 BM25 のリストとベクトルのリストを次で融合します:
1/(60 + rank)(1 つの SQL 文内):a1/rrf.sql(スターターキット内)は各リストの順位を次で計算します:ROW_NUMBER()単純な次のクエリの 1 つ外側で処理します:score()。その後、次の句で融合します:FULL OUTER JOIN。参考資料: RRF のページ で、4 行の例を使って同じパターンを解説しています。本格的な埋め込みを次で生成:
EMBED()(API キーはご自身で用意):AI リソースを作成し、次を作ります:doc_chunks_v2、モデルの次の値に合わせます:dim。データを次で埋めます:INSERT INTO doc_chunks_v2 SELECT …, EMBED(body) FROM doc_chunks(ベクトル列はNOT NULLのため、更新ではなく挿入します)。クエリも SQL で埋め込みを生成します。参照: 埋め込み.入力中に検索 ともに歩む
MATCH_PHRASE_PREFIX.一致した語を強調表示 します(Doris はスコアを返しますが、ハイライトは返しません。アプリ側で描画してください)。
レイテンシバッジ:各モードの所要時間をミリ秒で表示します。
トラブルシューティングと参考資料
| Symptom | Fix |
|---|---|
docker: command not found (macOS) | Docker Desktop を起動してください。起動済みなら CLI へのリンクを作成します: sudo ln -s /Applications/Docker.app/Contents/Resources/bin/docker /usr/local/bin/docker |
error getting credentials (イメージ取得時) | 削除する項目: credsStore のフィールドを次から削除: ~/.docker/config.json (ローカル開発環境のみ) |
port is already allocated | 別のプロセス(多くは別の Doris)が 9030、8030、8040 のいずれかを使っています。実行中のコンテナーを確認してください(docker ps)。または別のホストポートを公開します。例: -p 19030:9030。その場合は 19030 に接続してください。 |
The container name "/doris" is already in use | 以前に起動したことがあります: docker start doris。最初からやり直すには、 docker rm -f doris (データが削除されます) |
コンテナーが終了する、または状態が変わらない: (healthy) | 確認: docker logs doris。多くはメモリが原因です。Docker Desktop に 6 GB 以上を割り当ててください(Settings → Resources)。Apple Silicon では次を追加しないでください: --platform linux/amd64 |
permission denied: ./doris.sh | 解凍ツールによって実行権限が失われています: chmod +x doris.sh、または次を実行: bash doris.sh start |
Current database is not set | 実行: USE hackathon; を先に実行するか、次で接続します: -Dhackathon |
score() function requires WHERE clause with MATCH function, ORDER BY and LIMIT | 次を追加: MATCH_* の述語、 ORDER BY score() DESC および LIMIT、さらに次は score() を式で包まないでください(上記の落とし穴を参照) |
Ann topn query vector cannot be empty | ベクトルリテラルが空、またはプレースホルダーのままです。次の出力を貼り付けてください: python a1/toy_embed.py "…" |
Can not found function 'l2_distance_approximate' which has 9 arity | ドライバーが配列パラメーターを 8 個の値に展開しました。次を使ってベクトルを文字列で渡してください: CAST(? AS ARRAY<FLOAT>) |
| 10 分以上解決できない場合 | 投稿先: #dev 参加先: Apache Doris Slack |
参考資料
Apache Doris Hackathon
