트랙 A · 과제 A1
하이브리드 검색 앱
하나의 SQL 쿼리로 키워드, 의미 검색, 필터를 처리합니다. Apache Doris 문서를 검색하는 검색창을 만들어 보세요.
- 60~120분
- 입문자 환영
- 언어 자유 · CLI 또는 웹
- AI 도구 사용 환영
막히는 부분이 있나요? 질문할 곳: #dev 참여할 곳: Apache Doris Slack. 유지 관리자와 다른 참가자가 도와드립니다.
환경 준비
만들 결과물과 Doris로 간단히 구현하는 이유를 알아보고 클러스터를 실행합니다.
만들 결과물
단어와 의미를 모두 이해하는 검색창을 만듭니다.
BM25로 순위를 매긴 정확한 키워드 일치, 벡터 인덱스의 의미적 이웃, 구조화된 필터를 Doris 테이블 하나와 SQL 문 하나로 처리합니다. Elasticsearch나 별도의 벡터 데이터베이스가 필요 없습니다.
작은 검색 앱을 만듭니다(CLI, 노트북, 웹 페이지 중 자유롭게 선택). 검색 대상은 Apache Doris 문서의 299개 섹션입니다. 사용자가 다음과 같은 질문을 입력하고 "how do I store messy JSON?", 범주를 선택한 뒤 다음 세 모드를 전환합니다:
- 키워드BM25로 순위를 매긴 전문 검색 결과.
- 벡터임베딩 유사도에 따른 최근접 이웃.
- 하이브리드키워드 사전 필터 후 벡터 순위 계산(추가 과제로 Reciprocal Rank Fusion).
각 결과에는 페이지, 섹션, 점수(키워드) 또는 거리(벡터·하이브리드), 문서 링크가 표시됩니다. ‘SQL 보기’를 켜면 Doris가 실행한 정확한 쿼리를 확인할 수 있습니다.
개략도는 코드 패널 상단에 있습니다
Doris로 구현할 때의 장점
테이블 하나에 인덱스 두 개. 텍스트의 역색인과 벡터의 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개 주요 기능 페이지이며 섹션마다 한 행입니다:
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와 벡터 거리를 하나의 다음 절로 정렬: ORDER BY, 또는 다음 선택: score() 를 벡터 순위 쿼리에서 사용 | 지원되지 않습니다. 하이브리드 검색은 다음 조합입니다: MATCH_* 사전 필터 + 벡터 ORDER BY, 거리를 표시하거나 두 순위 목록을 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에 직접 붙여 넣지 마세요. 작은따옴표 하나만 있어도 쿼리가 깨집니다.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)
앱에서는 하나의 입력을 두 처리에 전달합니다:
keywords(text)를 다음에 사용:MATCH_ANY,toy_embed(text)를 벡터로 사용합니다.MATCH_ANY는 여러 단어에 대해 느슨한 필터입니다. 엄격한 조건이 필요하면 ‘반드시 포함’ 입력창이나 다음을 제공하세요:MATCH_ALL. 여기서는 다음을 선택하지 마세요:score(). 거리를 표시하세요.모드 전환(키워드 / 벡터 / 하이브리드)과 다음을 추가하세요: SQL 보기 전환 기능. 같은 질문의 세 결과 목록을 비교해 어디서, 왜 차이가 나는지 살펴보세요.
확인 사항모든 결과에 Kafka가 포함되며 첫 결과는 How does the Apache Doris Kafka and CDC integration work? 벡터 모드만 사용하면 Pipeline Execution Engine › Overview 가 같은 질문에서 첫 결과가 됩니다.
- L4
완성하기
체크리스트를 확인하고 결과물을 공유한 뒤 배지를 받으세요.
완료 기준
체크리스트제출하고 배지 받기
참여 → 제출 → 배지 받기.
다음 이름의 폴더에 결과물을 넣으세요: 본인의 GitHub ID. 코드와 README를 포함하고, README에 기능, 실행 방법, 스크린샷 또는 GIF 한 개, 사용한 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)(하나의 SQL 문 안에서):a1/rrf.sql(스타터 키트 내)는 각 목록의 순위를 다음으로 계산합니다:ROW_NUMBER()단순한 다음 쿼리의 한 단계 바깥에서: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 해커톤
