아카이브 · 당시 기준

2026-10-26 보관. 1.2.0 → 1.3.0 · MINOR: Text Capacity 설명 블록과 §4 Stage·shape bit 시각 자료 추가, 머리의 머지 요약 상자 제거 본문과 자동 동기화 표는 보관 당시 내용입니다.

현재 기준과 원본 보기 → · 보관 목록

← 미리디 기술 사전 / Visual Summary

ALGORITHM CLASS2026-10-26 갱신 · 저자 홍지혜(jhhong) · 문서 v1.2.0 · iui origin/main 04585231 재대조 · §11 PR #1561 probe 순서 추가 · 이전판

comp-frame 검색과 대치
Rust 실행 기준

검색 실행은 Rust, 색인·공통 계약·클라이언트·화면은 TypeScript가 맡는다. 2026-10-24 iui origin/main(04585231) 고정 소스로 tier·stage·중첩·대치 정책과 표·나눠 담기를 함께 읽는다.

v1.1.0에 반영한 머지 merged

2026-10-17 → 2026-10-24 사이 main에 머지된 PR 13건: #1296(글자 든 도형) · #1452(셀 지도 칸 판정) · #1497·#1520·#1533·#1539(ListingTable 표 나눠 담기) · #1538(bullet 재계획) · #1541(서버 재정렬) · #1543·#1549(relation 422) · #1545(placeholderNodeIds) · #1551(Background) · #1559(D.O. 지정). PR은 origin/main 04585231의 머지 커밋으로 2026-10-26에 확인했다. 관련 Jira 상태는 조회가 막혀(403) 확인하지 못했다. PR 머지는 배포 완료를 뜻하지 않는다.

v1.2.0에 더한 진행 중 작업 · PR #1561진행 중 · 미머지 나눠 담기 probe 순서 바꾸기(MOR-2575, Jira 상태 확인 필요). 전후 비교와 측정을 §11에 더했다. main 04585231은 아직 예전 순서다. 2026-10-26 확인.

comp-frame은 사용자 내용이 기존 디자인의 어느 슬롯에 들어갈지 검색 단계에서 미리 계산한다. 2026-10-12 이후 검색·랭킹·중첩·Content Threading은 Rust 서버만 구현한다. 브라우저가 후보 DB를 직접 조회하고 TS 검색을 실행한다는 이전 설명은 현재 경로가 아니다. 2026-10-24 기준으로 화면은 서버가 매긴 결과 순서를 다시 섞지 않고 그대로 쓴다(PR #1541 merged).

사용자 글 / Visual Summary → 서버 생성·문서 검색 → Query Frame ↔ 색인 Frame → binding·검사·순위 → 클라이언트 대치 → 렌더
구분무엇을 바꾸나
Tier 1·2·3검색할 카테고리: 요청 type / 자기 행 fallback / 종단 하나
Stage 0·1·2같은 카테고리 안에서 슬롯 모양 비트 차이 0 / 1 / 2
N1~N5중첩 구조와 노드별 카테고리를 어느 순서로 포기하나
0군·1군사용자 지정 종류 결과 / 추천 결과. Tier와 다른 축
D.O. 지정요청에 고른 design object 하나를 L1에서 따로 검색해 붙인다(2026-10-24). 순위 목록과 별개인 카드 하나

코드 근거 · 문서 한 번 요청

코드 근거 · TS 검색 제거 기록

공개 ComponentSearchClient.search는 raw-text 또는 visual-summary를 받는다. 원문 경로는 생성·검색·병합·랭킹을 서버가 한 요청 안에서 끝낸다. entries에는 단일 후보와 여러 컴포넌트의 묶음 후보가 함께 들어올 수 있다.

출력읽는 법
binding실제 sheet_id와 사용자 글. 표·차트 grid는 matrix를 함께 갖는다
blank / preserve미대치 슬롯의 분류. preserve도 최종 원문 보존 확정은 아니다
outside_text / inside_unbound_text선택된 경계 밖 / 경계 안의 미매핑 글
threading.pages여러 장으로 나눠 담은 경우의 장별 binding. 상위 binding은 첫 장
meta / tier / matched_category검색·배제·투영을 진단하는 정보. 운영 품질 점수와 다르다

제품은 별도 백엔드에서 받은 searchKey·활성 componentVersion을 전달하고, 디자인 문서(sheetJson)를 component.uuid로 받는다. 그다음 원본 RLSC(design_objects.structure_json)와 함께 buildReplacement({ component, sheetJson, rlsc })에 넣는다. rlsc는 어느 노드가 글자 노드(Text 또는 글자 든 도형)인지 가르는 데 쓰인다. 빼면 어느 노드도 자기 상자 폴백을 받지 못하므로 늘 넘긴다. 검색 통과 후에도 실제 렌더의 겹침·잘림·문장 의미를 확인해야 한다.

2026-10-17 이후 바뀐 계약이 셋 있다. ① 질의의 relations[].of가 가리키는 항목을 찾지 못하면 경고만 남기고 넘어가지 않는다. HTTP 422 unresolved_relation으로 요청 전체가 실패한다. unresolvedRelations는 deprecated이고 늘 빈 목록이다(PR #1543 merged·PR #1549 merged). ② buildReplacement 결과에 임시 문구로 채운 노드 id 목록 placeholderNodeIds가 붙는다(MOR-2845 상태 확인 필요, PR #1545 merged). ③ 요청 옵션 pinned_do_id를 주면 응답 pinned에 그 design object의 결과와 상태(listed·added·not_found·no_l1)가 실린다. 서버가 지원하는지는 /v1/info의 features로 확인한다(PR #1559 merged). 클라이언트 패키지는 comp-frame-client 0.11.0이다.

코드 근거 · 제품 호출 흐름

코드 근거 · unresolvedRelations deprecated · pinned_do_id · PinnedWire

질의 Frame은 넣을 글을, 색인 Frame은 실제 노드 주소와 글 용량을 담는다. units는 항목, fields는 label·value·items 등의 슬롯, satellites는 공통 머리말, dataset은 표·차트 데이터다. aliases는 같은 물리적 칸의 다른 필드 이름이며 한 노드를 두 번 채우는 허가가 아니다.

현재 FRAME_VERSION = 5다. 적재 측 buildIisFrame은 IIS 구조 대응을 실제 노드와 연결하고, 만들 수 없으면 규칙 기반 Frame을 시도한다. 표 셀 지도와 표 기하도 색인에 들어간다. 규칙 기반 Frame도 이제 IIS 속성(properties)을 params에 싣는다(MOR-2683 상태 확인 필요).

2026-10-19부터 슬롯 판정이 넓어졌다(MOR-2542 상태 확인 필요, PR #1296 merged). 적재 Frame 빌더와 벤더 extractTextSlots는 isTextBearingNode로 Text와, content가 문자열인 SVG(글자 든 도형)를 함께 글자 슬롯으로 본다. 또 Frame의 관계는 문서의 Relation과 갈라져 FrameRelation이 됐다. of는 선택 필드라 색인 Frame에는 싣지 않고 질의 Frame만 읽는다. Frame 판 번호는 그대로 5다. Frame 스키마 버전, IIS 발행분, 제품 componentVersion, 서버 배포 버전을 혼동하지 않는다.

코드 근거 · packages/comp-frame-shared/src/frame.ts

코드 근거 · packages/comp-frame-index/src/frame/component-frame.ts

Tier현재 Rust 규칙
1type의 성립 조건을 통과한 카테고리. alternatives는 읽지 않는다
2원래 type의 fallback 행을 순서대로. 각 도착지는 원본 내용에서 projection한 뒤 성립 검사
3List·LongFormTextLayout·HighlightedMessage 중 내용으로 고른 종단 하나. 이미 앞 군에 있으면 생략하고, 여기서도 성립 검사를 한다

예: Timeline → Steps → Staircase → List. Steps의 행으로 갈아타지 않고 Timeline의 행을 끝까지 본다. 날짜 전용 필드를 못 읽는 도착지에서는 P3이 날짜를 items로 옮긴다. P2는 읽지 않는 params를 제거하고 P4/P5/P6은 데이터 형태를 바꾸며 P7은 관계의 설명을 옮긴다. 관계 전체의 의미가 언제나 무손실이라는 보장은 아니다. 2026-10-24 기준으로 P7이 설명을 붙일 관계 주인은 of를 모두 label로 읽어 찾고, 못 찾은 참조는 빼지 않고 422로 막는다(§1). 투영 함수 이름은 project_structure_keeping_content로 바뀌었다.

P8은 폐기됐다. 상한 15인 카테고리에 17항목을 억지로 접어 맞추지 않는다. 이후 align이 상세 문장을 한 슬롯에 합치는 일, 여러 장으로 나눠 담는 일은 별개의 처리다. 생성기가 처음부터 읽지 않는 필드에 내용을 쓰면 fallback P3이 자동 구제한다는 보장도 없다(B-33).

코드 근거 · rust/comp-pass/crates/comp-frame-core/src/query_frame.rs

코드 근거 · rust/comp-pass/crates/comp-frame-core/src/visual_summary/summary_projection.rs

후보는 서버 메모리 스냅샷에서 카테고리·중첩 카테고리·unit 수·dataset 종류·shape bits·용량 하한으로 먼저 좁힌다. Stage 0은 모양 비트가 같고, Stage 1·2는 각각 정확히 1·2비트 차이다. 20비트 가운데 앞 17개만 완화하며 dataset과 extraGrids는 하드 조건으로 남는다.

이것은 정확한 배치 일치나 최종 가독성 검사가 아니다. 제목 글자 수에 맞춘 사전 하한은 후보를 줄이는 근사 조건이고, 본문과 실제 줄바꿈·슬롯 대응은 후속 배정과 용량 검사가 본다. 과거 DB의 gc_passed 옵션은 현재 검색의 품질 통과와 같지 않으며 해당 컬럼 제거 마이그레이션이 존재한다.

shape bit: 어떤 칸이 「있는지」만 적은 20자리 지문

Frame 하나를 숫자 하나(u32)로 줄인 값이다. 질의 Frame에서도, 색인의 디자인 행에서도 같은 함수 compute_shape_bits로 계산한다. 각 자리는 「이 Frame 어딘가에 그 칸이 하나라도 있으면 1」이다. 몇 개 있는지, 글이 얼마나 긴지는 적지 않는다.

비트무엇이 있으면 1인가Stage에서 완화
0–12유닛 필드 13종. 순서대로 label·value·unit·period·items·emphasis·weight·speaker·author·source·avatar·organization·values. 하위 유닛(children)까지 전위 순회로 훑는다완화
13어느 유닛이든 children이 있다완화
14 · 15 · 16공통 머리말 satellites.title · subtitle · desc완화
17 · 18dataset이 measure(수치) · matrix(표·차트 격자)하드
19다중 격자의 나머지 격자(extraGrids)가 1개 이상하드

세 가지를 같이 기억한다. 첫째, marker는 14번째 필드지만 비트를 받지 않는다. 순번 글자는 내용이 아니라서다. 둘째, 비트 번호는 FIELD_ORDER 순서에 묶여 있어 필드 순서를 바꾸면 색인 전체의 비트가 달라진다. 코드가 「재배열 금지」로 못 박아 둔 이유다. 셋째, 항목이 2개인지 5개인지는 비트에 안 나타난다. unit 수는 따로 unit_count_min(질의 unit 수 이상)으로 거른다.

Stage 0·1·2: 허용할 비트 패턴의 목록

Stage는 점수가 아니라 「이 비트 값을 가진 행만 가져온다」는 허용 목록이다. 질의 비트를 qb라 하면 이렇게 만든다.

Stage허용 패턴패턴 수
0qb 그대로1
1비트 0–16 가운데 정확히 하나를 뒤집은 값17
2비트 0–16 가운데 정확히 두 개를 뒤집은 값. Stage 1과 겹치는 것은 뺀다136 (= 17C2)

「뒤집는다」는 양쪽 방향이다. 후보 디자인에 질의에 없는 칸이 하나 더 있어도, 질의에 있는 칸이 하나 빠져 있어도 1비트 차이다. 그래서 Stage 1·2 후보는 빈 칸(blank)이 생기거나 질의 글을 받을 자리가 모자랄 수 있다. 그 결과를 쓸 수 있는지는 Stage가 정하지 않는다. 다음 절의 배정(align)과 용량 검사가 정하고, 내용 손실이 생기면 거기서 빠진다(§7).

비트 17–19는 아예 뒤집지 않는다. 게다가 필터는 dataset 종류를 따로 한 번 더 같은지 본다. 그래서 표 질의가 일반 목록 디자인으로, 수치 질의가 표 디자인으로 Stage를 타고 넘어가는 일은 없다. 이 절 제목의 「데이터 종류는 유지한다」가 이 뜻이다.

예시: 제목 + 항목 둘(이름·설명)

질의가 머리 제목 하나와, label·items를 가진 유닛 둘이라고 하자. 켜지는 비트는 0(label), 4(items), 14(title)이고 qb = 1 + 16 + 16384 = 16401이다.

후보 디자인의 모양다른 비트들어오는 Stage
제목 + label·items 유닛 셋없음 (유닛 수는 비트에 안 나온다)0
제목 + 공통 설명(desc) + label·items161
제목 없이 label·items141
제목 + label·value (items 대신 value)1, 42
제목 + label·items + 표 격자(matrix)18들어오지 않음
제목 + label·items 유닛 하나없음들어오지 않음 (unit 수 하한 2에 걸린다)

Stage가 쓰이는 곳과 쓰이지 않는 곳

이름이 비슷한 축과 섞지 않는다. Tier 1·2·3은 「어느 카테고리」, Stage 0·1·2는 「그 카테고리 안에서 어떤 모양까지」, N1~N5는 중첩 구조를 어디까지 포기하나(§6), 0군·1군은 사용자 지정 종류냐 추천이냐(§11)다. 나눠 담기 결과에 붙는 direct·threading·fallback 단계도 shape Stage와 상관이 없다.

코드 근거 · shape.rs · compute_shape_bits · shape.rs · SOFTENABLE_BIT_COUNT·shape_patterns · frame.rs · FIELD_ORDER

코드 근거 · filter.rs · matches · filter.rs · build_candidate_filter · filter.rs · shape_bits_for_stage

코드 근거 · comp-frame-pool search.rs · scan_category · union_filter_for_allocation · comp-pass-logic search.rs · explain의 비트 이름

코드 근거 · packages/comp-frame-index/sql/2026-09-22-drop-gc-passed.sql

9월 판의 “모든 카테고리는 목표 수 ×4까지만 스캔” 설명은 바뀌었다. 현재 첫 번째로 존재하는 tier는 하드 필터 통과 행을 전부 검사하고, 승인 후보 전체에서 순위를 매긴다. tier1_max_candidates=0은 무제한이다. 원래 tier 1이 비면 tier 2가 이 첫 tier 취급을 받을 수 있다.

설정 / 상태현재 의미
top_n = 50최종 정렬·중복 제거 후 반환 상한
min_candidates = 500첫 tier 이후의 누적 승인 목표
tier_targets = [50,50]tier 처리가 끝난 뒤 하위 tier로 내려갈지 보는 누적 승인 문턱
tier1_max_candidates = 0첫 존재 tier의 승인 상한 없음. 양수를 주면 그 상한에서 자를 수 있다
CATEGORY_SCAN_MULTIPLIER = 4첫 tier 이후 카테고리의 스캔 예산 배율
FETCH_PAGE_SIZE = 1000후보 처리 페이지 상한. 현재는 메모리 풀 안에서 읽는다

행을 읽은 수, 배정·용량 검사를 통과한 수, fingerprint 중복 제거 후 반환 수는 서로 다르다. 통과 수가 0이면 다음 공급을 볼 수 있지만, 통과 행이 같은 Frame으로 중복되면 최종 카드 수가 적을 수 있다. 최신 메타의 tier1_fetched_rows·tier1_exhausted로 첫 tier를 어디까지 읽었는지도 확인한다. tier 하나를 다 훑을 때마다 누적 승인 수를 tier_targets와 비교하므로, 첫 tier에서 50건 넘게 승인되면 tier 2는 보지 않는다.

D.O. 지정 검색(PR #1559 merged)은 같은 tier·stage·승인·순위 규칙을 쓰되, 각 (카테고리, stage)에서 읽은 행 가운데 그 design object의 행만 남긴다. 반환 상한은 요청의 top_n을 그대로 쓴다. 결과가 비면 pinned.not_found에 읽은 행 수와 배제 사유별 수가 실린다.

코드 근거 · rust/comp-pass/crates/comp-frame-pool/src/search.rs · only_do_id · document_search.rs · D.O. 지정

코드 근거 · packages/comp-frame-shared/search-defaults/options.json

단계역할
N1구조와 종류를 유지해 exact 검색
N2중첩 모양을 유지하며 노드별 자기 행 fallback 조합 검색
N3부모 한 층을 벗겨 자식 묶음을 검색
N4필요하면 부모 벗기기를 반복
N5자식 내용을 글로 평탄화. Hierarchy는 부모 벗기기 대신 이 경로

현재 구현은 Rust nested_search.rs다. N1 결과가 있으면 반환하는 실제 분기가 있다. 위키 D-7의 과거 이종중첩 공급 제외를 근거로 최신 풀의 N1이 “언제나 0”이라고 쓰지 않는다. N2의 조합 수, 검색 대상 풀, 성공 여부를 따로 확인한다. 부모 내용·관계 설명의 보존과 형제 순서도 함께 검사해야 한다.

코드 근거 · rust/comp-pass/crates/comp-frame-core/src/nested_search.rs

align은 질의 슬롯과 디자인 슬롯을 맞추어 binding·blank·preserve·내용 손실을 만든다. 랭커는 이 배정을 비용 계산보다 먼저 실행한다. content_loss > 0 후보는 제외한다. 내용이 남았다는 계산만으로 원문 의미 전체가 보존됐다고 보장하지는 않으므로 원본 → projection → Frame → binding을 추적한다.

표는 별도 조건이 있다. native 표의 확장이 공간 예산을 넘으면 검색에서 제외하고, 측정할 기하가 없는 표는 tableUnmeasured로 낮춘다. 글상자 표는 dataset.cells의 IIS 셀 지도에 맞춰 셀별로 배정할 수 있다. matrix라는 이유만으로 모든 후보를 native Chart로 취급하지 않는다.

셀 지도의 칸 판정도 바뀌었다(MOR-2673 상태 확인 필요, PR #1452 merged). 예전에는 칸이 TextItem이어야 했지만 지금은 RLSC 글자 노드면 된다. 대치가 글을 넣지 못하는 도형(ShapeItem, RLSC가 글자 노드로 모르는 ESI)만 거절한다.

MOR-2683 상태 확인 필요(PR #1497 merged)으로 ListingTable 표 처리가 셋 더해졌다. 머리글 칸 줄이 없는 셀 지도에 ListingTable을 넣으면 열 제목을 버리고 데이터를 첫 행부터 넣는다. IIS 속성 params.header == false인 TableItem 표도 열 제목을 버린다(grid_drops_column_labels). 나눠 담기 탐색에서만 켜는 cell_map_probe는 셀 지도의 행 부족·빈 칸으로 후보를 빼지 않고 내용 손실 1로 매긴다. 열 부족은 여전히 뺀다.

코드 근거 · rust/comp-pass/crates/comp-frame-core/src/align.rs

코드 근거 · rust/comp-pass/crates/comp-frame-core/src/rank.rs

코드 근거 · rust/comp-pass/crates/comp-frame-core/src/table_fit.rs

코드 근거 · packages/comp-frame-index/src/frame/cellMapFrame.ts · grid_drops_column_labels · cell_map_probe

기본 rank_mode=tier_pool·clean_first=above_tier에서는 사용자 내용이 아닌 잔여가 0인지 → tier → fallback 순서 → 꾸밈 선호 → 비용 → 단독 태그와 나머지 동점 규칙 순으로 비교한다. 조회된 tier 2의 온전한 후보가 tier 1의 잔여 있는 후보보다 앞설 수 있다. 조회하지 않은 후보를 정렬이 찾아 주지는 않는다.

같은 fingerprint 묶음에서는 잔여 0인 후보를 대표로 바꾸고 다시 정렬한다. unauthored_count는 임시 문구 대상 등을 세는 구현 지표다. Marker·비내용 역할(2026-10-23부터 Background 포함)·구조 토큰처럼 원문 보존이 정당한 노드는 잔여 비용과 이 계수에서 빠진다. 사람의 의미 평가를 대체하는 완성도 점수는 아니다.

기본 가중치 정본에는 비용 키 23개와 threading 전용 가중치 2개가 있다. 초기값과 과거 튜닝 이력이 섞여 있으므로 값의 크기나 합계만으로 다른 알고리즘보다 우수하다고 말하지 않는다.

코드 근거 · rust/comp-pass/crates/comp-frame-core/src/rank.rs

코드 근거 · rust/comp-pass/crates/comp-frame-core/src/rank.rs

코드 근거 · packages/comp-frame-shared/search-defaults/weights.json

현재 화면과 제품 SDK는 공유 buildNodeTextMap을 쓴다. 순서는 binding → blank → preserve → 안쪽 잔여 → 바깥 잔여 → 제목 구제다. 이미 사용자 글을 받은 노드를 뒤 단계가 덮지 않는다.

조건 (위에서 먼저 적용)최종 처리
Marker 또는 Decoration·Separator·Annotation·Background원문 유지
blank/preserve의 unit 필드임시 문구
vs, TAM/SAM/SOM, 숫자가 아닌 한 글자, 빈 글, 숫자 없는 .·Q.·A. 등 구조 토큰원문 유지
일반 숫자·글·원문 미상임시 문구

문서 제목이 안쪽 title binding에 없으면 바깥 Title 후보를 우선해 한 곳을 고른다. Title이 없을 때도 Marker·비내용 역할은 후보에서 제외한다. 선택되지 않은 공통 부제·설명은 일반 글이면 임시 문구가 된다. Background(워터마크처럼 배경에 깔린 글자)는 2026-10-23에 비내용 역할로 옮겨져 슬롯·벌점·제목 후보에서 모두 빠지고 원문으로 남는다(MOR-2753 상태 확인 필요, PR #1551 merged). 상세 대조와 실제 정책으로 다시 계산한 예시는 역할별 텍스트 대치에 있다.

코드 근거 · packages/comp-frame-shared/src/nodeTextPolicy.ts · buildNodeTextMap · ELEMENT_ROLE_KINDS

이전 판은 TS 검색에서 F01~F09를 합성 재현하고 핵심 테스트 470개를 기록했다. 그 결과는 당시 커밋의 검증이다. 이후 Rust 이전, 첫 tier 전량 스캔, 정렬 비교, 내용 보존과 대치 정책 변경이 들어왔으므로 동일한 번호를 최신 결함으로 재선언하지 않는다.

옛 주장이번 판의 처리
첫 tier도 ×4까지만 훑는다현재 기본 전량 검사로 교체
N1은 언제나 후보 0코드는 검색을 실행하며 실제 풀 상태는 미측정
preserve·경계 밖은 원문 유지공통 노드 정책으로 교체
threading은 두 종이고 제목 구제를 끈다네 종 프로파일과 ListingTable 표 경로, 제목 구제 유지로 교체
도형 글자는 양쪽 모두 Text와 같다2026-10-19부터 iui도 글자 든 SVG를 슬롯으로 읽는다. 운영 색인 재적재는 별도 확인

현재 의심을 재현하려면 최신 Rust 함수와 같은 입력·풀·옵션을 고정하고 실행해야 한다. 이번 문서 갱신은 과거 테스트 수를 새로운 실행 결과로 재표시하지 않는다.

나눠 담기는 두 경로다. 유닛(카드) 경로의 프로파일은 List·Steps·QnACard·TableOfContents이고, ListingTable은 프로파일 없이 표 경로로 간다(MOR-2683 상태 확인 필요, PR #1497 merged). 서버가 직접 배치 → 탐색용 질의 → 여러 장 계획 → 장별 검사·재정렬을 한다. 상위 binding은 첫 장이며 다음 장은 장별 데이터로 바꿔 그린다. 기본 장수 상한은 64다.

표 경로는 행을 쪼개지 않는다. 셀 지도 표는 데이터 칸의 행 수만큼, TableItem 표는 표 예산 안에 드는 만큼 한 장에 담는다. 장마다 담은 데이터 행 구간 rows가 응답에 실린다(PR #1539 merged). 표 밖에 entity나 중첩 컴포넌트가 같이 있는 질의는 표 경로로 받지 않는다(PR #1533 merged). 유닛 경로에서는 계획한 본문 하나라도 bullet이 붙으면 모든 본문에 bullet을 붙여 다시 계획한다(MOR-2623 상태 확인 필요, PR #1538 merged).

threading 및 original_size_verified 결과는 원본 크기를 기준으로 배치한다. 제목 구제는 유지되며, 임시 문구는 원래 셀의 용량과 요청에서 쓴 언어별 문구 표를 기준으로 고른다. 검색 때와 렌더 때 다른 문구 표를 쓰면 맞음 보장이 깨진다.

사용자가 generationTypes를 지정한 경우는 별도 0군·1군 검색이다. 지정 종류의 0군을 tier1_only로 먼저 찾고 부족할 때 추천 1군을 찾는다. 이 군 번호는 Tier 1~3이나 Stage 0~2가 아니다. 나눠 담은 결과의 재정렬은 이제 서버가 군마다 하고, 0군 뒤에 1군을 붙인다. 화면은 그 순서를 그대로 쓴다(PR #1541 merged).

코드 근거 · rust/comp-pass/crates/comp-frame-core/src/threading/profile.rs · ThreadingKind

코드 근거 · rust/comp-pass/crates/comp-pass-logic/src/threading_search.rs

코드 근거 · src/comp-frame/useUnifiedSearch.ts

probe 순서 바꾸기: PR #1561 전과 후

진행 중 · 2026-10-26 확인

PR #1561진행 중 · 미머지 MOR-2575상태 확인 필요

아래의 「지금」은 열린 PR의 head e43027c 기준이다. 이 문서가 대조한 origin/main 04585231은 아직 예전 순서(짧은 본문 500 → '가' 한 항목 500 → 실제 최장 본문 3000 → 짧은 본문 3000)로 돈다. Jira 상태는 연결이 막혀(403) 조회하지 못했다.

무엇을 바꾸나앞에서 헛돌던 좁은 예산(500)의 짧은 질의 probe 를 빼고, 넓은 예산 probe 부터 돈다.
품질기본 옵션 847건 중 86건이 좋아지고, 나빠지는 요청은 0건이다.
속도느리던 프리셋 20개의 서버 시간 합이 6,161ms에서 4,537ms로 26% 준다.

나눠 담기를 그림으로 보면

미리캔버스 템플릿 하나에는 칸(슬롯)이 정해져 있다. 목록 칸이 4개인 디자인에 항목 10개짜리 글을 넣으면 넘친다. 이때 디자인을 버리지 않고 같은 디자인을 여러 장 이어 붙여 4 + 4 + 2 로 나눠 담는다.

1. 회의 목적 공유2. 지난주 결과 점검3. 이번 주 우선순위4. 예산 배분 검토5. 채용 일정 확인6. 디자인 리뷰7. 고객 피드백 정리8. 위험 요소 논의9. 담당자 지정10. 다음 회의 일정
1장
1. 회의 목적
2. 지난주 결과
3. 우선순위
4. 예산 배분
2장
5. 채용 일정
6. 디자인 리뷰
7. 고객 피드백
8. 위험 요소
3장
9. 담당자
10. 다음 회의

예시 그림이다. 칸 4개짜리 템플릿 한 종류를 3장 이어 붙였다. 실제로는 서버가 장별로 넘침·글자 수까지 검사하고 점수를 매긴다.

유닛 경로에서 서버는 세 단계를 밟는다.

probe 는 「후보를 고르려고 던지는 가짜 질의」다

느린 요청에서는 같은 카테고리(tier 1) 후보가 3.4만~3.7만 개나 된다. 후보마다 「몇 장으로 나눌지 계획하고 장별로 채점」하는 나눠 담기 평가는 비싸서 전부에 돌릴 수 없다. 내용이 큰 #60 은 후보 하나에 약 1.1ms가 든다.

그래서 서버는 먼저 검색 엔진에 가짜 질의를 던져 후보 전체에 점수를 매기고, 점수 상위 N개만 꺼내 진짜 평가를 한다. 이 가짜 질의가 probe 다. 꺼내는 개수 N을 예산이라고 부른다. 예산 500은 화면의 「전체 후보 상한」(min_candidates) 기본값이고, 3000은 그 6배를 3000에서 자른 값이다.

도서관에 비유하면 쉽다. 장서 3.6만 권을 다 읽을 수는 없으니 검색어를 하나 넣고 위에서 500권만 꺼내 읽는다. 검색어가 엉뚱하면 꺼낸 500권이 다 쓸모없다. probe 의 점수는 「무엇을 꺼낼지」 고르는 데만 쓰이고, 최종 순위는 꺼낸 뒤 실제 글로 다시 채점해서 정한다.

probe 질의는 세 종류다. 세 질의 모두 제목 같은 필드에는 입력에서 가장 긴 값을 넣고, 본문(목록 항목)을 채우는 방식만 다르다. 아래는 항목 6개짜리 입력을 가정한 예시다.

실제 최장 본문
제목: 3분기 주간 회의 안건
본문: 실제 글에서 가장 긴 본문

실제 글 길이를 넣는다. 칸 용량이 넉넉한 템플릿이 위로 올라온다.

짧은 본문 (구조 probe)
제목: 3분기 주간 회의 안건
본문: [가, 가, 가, 가, 가, 가]

항목 수만큼 '가' 한 글자씩 넣는다. 글 길이는 무시하고 칸 구조만 맞는 템플릿을 찾는다. 행마다 칸 크기가 다른 디자인을 건지는 몫이다.

'가' 한 항목
제목: 3분기 주간 회의 안건
본문: [가]

항목을 하나만 넣어 항목 수가 아예 다른 템플릿을 찾는다. 골든 uneven 사례는 이 probe 만 찾아서 뺄 수 없다. 항목이 둘 이상일 때만 돈다.

예전에는 쓸모없는 500개부터 꺼내 봤다

main(04585231)의 순서는 아래와 같다. probe 를 하나 돌 때마다 「목표를 채웠나」를 보고, 채웠으면 멈춘다.

실제 최장 본문짧은 본문'가' 한 항목느린 프리셋에서 쓸 수 있는 후보 0개
main · 유닛 경로
p0 짧은 본문예산 500
멈춤?
p1 '가' 한 항목예산 500
멈춤?
p2 실제 최장 본문예산 3000
멈춤?
p3 짧은 본문예산 3000

블록 너비는 예산에 비례한다. p0 이 행을 하나도 못 가져오면(후보 0행) 나머지를 돌지 않는 규칙도 있다.

짧은 질의를 넣으면 칸이 하나뿐인 템플릿이 점수 상위를 차지한다. 내용이 짧으니 「작은 칸 하나에 쏙 들어간다」고 높게 쳐주기 때문이다. 그런데 칸이 하나면 여러 장에 나눠 담을 수 없다. 10-01 검토에서 느린 프리셋 17개를 뜯어봤더니 p0·p1 이 꺼낸 상위 500개는 전부(100%) 쓸 수 없는 템플릿이었고, 그중 98%가 칸 하나짜리였다. 쓸 수 있는 후보는 전부 p2·p3(예산 3000)에서 나왔다.

칸이 하나뿐인 템플릿 (약 98%)다른 정적 결함 (약 2%)쓸 수 있는 후보 (0개)

p0 이 꺼낸 상위 500개를 한 칸씩 그렸다. 검토 문서 §3의 비율(100% 정적 탈락, 그중 98% 칸 하나)을 옮긴 그림이고, 칸 하나하나가 실제 템플릿과 맞물리지는 않는다.

게다가 probe 는 돌 때마다 3.6만 후보 전체에 점수를 다시 매긴다. 결국 예전 순서는 헛수고 두 번을 하고 나서야 쓸 만한 probe 에 닿는다. 처음 예산의 짧은 본문 probe(p0)는 넓은 예산의 같은 질의(p3)가 가져오는 후보의 앞 500개와 똑같다는 점도 있다. p3 을 돌면 p0 은 따로 볼 필요가 없다.

넓은 probe 부터 돌고, 짧은 본문 probe 까지는 늘 돈다

main
짧은 본문500
멈춤?
'가' 한 항목500
멈춤?
실제 최장 본문3000
멈춤?
짧은 본문3000
PR #1561
실제 최장 본문3000
멈추지 않음
짧은 본문3000
멈춤?
'가' 한 항목500 · 모자랄 때만
표 경로 main (MOR-2683)
짧은 글500
멈춤?
가장 긴 글3000
멈춤?
짧은 글3000
표 경로 PR #1561
가장 긴 글3000
멈추지 않음
짧은 글3000
끝

「멈춤?」은 그 probe 뒤에 목표를 채웠는지 보는 자리다. 표 경로는 짧은 글이 마지막이라 결과적으로 둘 다 늘 돈다.

규칙을 하나씩 짚으면 이렇다.

  1. 순서: 실제 최장 본문(3000) → 짧은 본문(3000) → 그래도 모자라면 '가' 한 항목(500). 처음 예산의 짧은 본문 probe 는 지웠다.
  2. 멈춤 판정은 짧은 본문 probe 부터 한다. 실제 최장 본문 probe 만으로 목표를 채워도 거기서 멈추지 않는다. 코드에서는 ProbePlan::stops_after 가 이 판단을 한다.
  3. 행 0개 규칙: 짧은 본문 probe 가 행을 하나도 못 가져오면 남은 probe('가' 한 항목)를 돌지 않는다. 예전에는 이 규칙이 첫 probe 에 붙어 있었는데, 그 첫 probe 가 바로 같은 짧은 본문 질의였다. 질의가 같으니 뜻도 그대로다.
  4. 넓은 검색 공유 철거: #1262 는 첫 probe(짧은 본문 500)와 마지막 probe(짧은 본문 3000)가 같은 질의라는 점을 이용해 넓은 검색 한 번을 나눠 썼다(share_wide). 이제 짧은 본문 500이 없으니 탈 일이 없어서 걷었다. 동작은 바뀌지 않는다.
코드로 보면: stops_after
fn stops_after(&self, probe_index: usize, reached_target: bool, fetched_nothing: bool) -> bool {
    probe_index >= self.structure_probe          // 짧은 본문 probe 전에는 절대 멈추지 않는다
        && (reached_target                       // 목표를 채웠거나
            || (probe_index == self.structure_probe && fetched_nothing))  // 짧은 본문이 0행이면
}

출처: PR head e43027c · threading_search.rs#L968. 주석은 이 문서에서 붙였다.

상황마다 어떤 probe 가 도나

버전과 상황을 골라 보자. b4 는 PR #1561 전에 먼저 시도했다가 버린 안이다. 순서는 PR과 같지만 probe 마다 멈춤 판정을 했다.

b4 를 버린 이유: 결과 차이는 「어디서 멈추느냐」에서만 생긴다

측정에서 중요한 사실이 하나 나왔다. 같은 템플릿은 어느 probe 가 찾아 오든 평가 결과가 똑같다. 1,258,999건을 비교했고 불일치는 0건이었다. probe 는 「누구를 데려올지」만 정하고 채점은 실제 글로 다시 하니 당연하다. 그러면 순서를 바꿔서 결과가 달라지는 길은 하나뿐이다. 일찍 멈춰서 어떤 probe 를 안 돌면, 그 probe 만 찾던 템플릿을 놓친다.

최장 본문probe 만둘 다짧은 본문probe 만b4 가 놓친 곳

b4 는 실제 최장 본문 probe 하나로 목표 50개를 채우면 바로 멈췄다. 그러면 오른쪽 영역, 짧은 본문 probe 만 찾는 템플릿을 통째로 잃는다. 예를 들어 gen-ad39eb6e 은 나눠 담기 결과가 7건에서 4건으로 줄었다.

main 은 짧은 본문 질의(p0, 500)를 맨 먼저 도니 그 영역 일부는 늘 본다. 그래서 b4 는 main 보다 나빠지는 요청을 남겼다. PR #1561은 짧은 본문 probe 까지는 무조건 돌아서 이 구멍을 막는다.

대가는 속도다. 느린 프리셋 20개 합 기준으로 b4 는 43% 줄었고 PR #1561은 26% 줄었다. main 보다 나빠지는 요청을 남기지 않는 쪽을 골랐다.

측정: 품질은 좋아지기만 했고, 느린 요청은 빨라졌다

측정 조건은 로컬 release 서버, dev 판 2.6-20261023-b694dfd 스냅샷, 입력 847건(CSV 프리셋 85 + 생성 기록), 옵션 세 벌이다. 비교 기준 main 은 04585231b 이다. 아래 수치는 PR 본문과 검토 문서에서 옮겼고 이 문서에서 다시 재지 않았다.

main 과 비교한 요청 수 (기본 옵션 top_n 50)좋아지기만나빠지기만섞임
b4 (버림)3424
PR #15618600

나머지 761건은 시간 값과 meta 를 빼면 응답이 main 과 같다.

「다 돌았으면 얻었을 결과」보다 나쁜 자리 수. oracle 은 옛 probe 넷을 멈추지 않고 전부 돈 빌드다. 시간을 무시하면 얻을 수 있는 상한이라고 보면 된다. 이 수가 작을수록 상한에 가깝다.

mainb4 (버림)PR #1561
05001,0001,5002,000기본 (top_n 50)기본 (top_n 50) · main: 2,070자리2,070기본 (top_n 50) · b4 (버림): 1,529자리1,529기본 (top_n 50) · PR #1561: 159자리159top_n 10top_n 10 · main: 348자리348top_n 10 · b4 (버림): 238자리238top_n 10 · PR #1561: 22자리22min_candidates 100min_candidates 100 · main: 758자리758min_candidates 100 · b4 (버림): 237자리237min_candidates 100 · PR #1561: 52자리52

값이 작을수록 좋다. 출처는 PR #1561 측정 절과 검토 문서 §9.2.

main 보다 나빠지는 요청이 딱 2건 있다. min_candidates 100 옵션의 gen-0fd0d9d0, gen-ead6b6eb 이고, '가' 한 항목 probe 가 맨 뒤로 가서 생겼다. 기본 옵션에서는 나빠지는 요청이 없어서 그대로 뒀다. top_n 10 의 섞임 1건(gen-7d3576c9)은 문서 병합이 만든 결과이고 oracle 도 같다.

느리던 프리셋 20개의 서버 시간. main 에서 100ms 넘던 CSV 프리셋 20개를 프리셋마다 5번 보내 server_total_ms 중앙값을 쟀다. 점에 마우스를 올리면 값이 나온다.

mainb4 (버림)PR #1561
0ms200ms400ms600ms800ms#60 · main 762ms · b4 555ms · PR 634ms (-17%)#60-17%#2 · main 422ms · b4 141ms · PR 269ms (-36%)#2-36%#31 · main 405ms · b4 268ms · PR 393ms (-3%)#31-3%#41 · main 403ms · b4 170ms · PR 272ms (-33%)#41-33%#84 · main 393ms · b4 252ms · PR 251ms (-36%)#84-36%#123 · main 389ms · b4 278ms · PR 276ms (-29%)#123-29%#51 · main 383ms · b4 123ms · PR 245ms (-36%)#51-36%#63 · main 341ms · b4 120ms · PR 227ms (-33%)#63-33%#45 · main 336ms · b4 200ms · PR 202ms (-40%)#45-40%#30 · main 309ms · b4 284ms · PR 284ms (-8%)#30-8%#59 · main 300ms · b4 109ms · PR 179ms (-40%)#59-40%#42 · main 261ms · b4 127ms · PR 195ms (-25%)#42-25%#48 · main 260ms · b4 120ms · PR 182ms (-30%)#48-30%#50 · main 254ms · b4 120ms · PR 177ms (-30%)#50-30%#3 · main 187ms · b4 67ms · PR 123ms (-34%)#3-34%#1 · main 184ms · b4 143ms · PR 147ms (-20%)#1-20%#25 · main 167ms · b4 152ms · PR 156ms (-7%)#25-7%#12 · main 160ms · b4 105ms · PR 105ms (-34%)#12-34%#6 · main 139ms · b4 54ms · PR 106ms (-24%)#6-24%#9 · main 106ms · b4 99ms · PR 114ms (+8%)#9+8%

오른쪽 숫자는 main 대비 PR #1561의 변화율이다. 단위는 ms, 5회 중앙값. b4 는 속이 빈 점으로 그렸다.

표로 보기
#mainb4PR #1561main 대비
#60762555634-17%
#2422141269-36%
#31405268393-3%
#41403170272-33%
#84393252251-36%
#123389278276-29%
#51383123245-36%
#63341120227-33%
#45336200202-40%
#30309284284-8%
#59300109179-40%
#42261127195-25%
#48260120182-30%
#50254120177-30%
#318767123-34%
#1184143147-20%
#25167152156-7%
#12160105105-34%
#613954106-24%
#910699114+8%

합계로 보면 6,161ms → 4,537ms(−26%)다. 85개 전체 합은 6,888 → 5,296ms 다. #9 만 106 → 114ms로 조금 늘었다. #60 은 634ms로 여전히 가장 느린데, 묶음 14개짜리 내용을 후보마다 계획·채점하는 비용 쪽이라 probe 순서로는 풀리지 않는다.

남은 일

용어 정리
probe
나눠 담을 후보를 고르려고 던지는 합성 질의다. 점수는 후보 고르기에만 쓰인다.
예산 (500 / 3000)
probe 하나가 점수 상위에서 꺼내 실제로 나눠 담기 평가를 하는 후보 수다.
비교 목표
limit 을 10~50으로 자른 수다. 직접 결과 + 나눠 담기 결과가 이 수에 닿으면 멈출 수 있다.
정적 탈락
글 내용과 상관없이 템플릿만 보고 정해지는 탈락이다. 칸이 하나뿐인 경우가 대표적이다.
oracle
옛 probe 넷을 멈추지 않고 전부 돈 빌드다. 비교용 상한이다.
b4 / v2
b4 는 같은 순서에 probe 마다 멈춤 판정을 한 안(버림), v2 는 짧은 본문 probe 까지 늘 도는 PR #1561 안이다.

출처 · PR #1561 본문·diff(head e43027c, 2026-10-26 확인, 미머지) · 검토 문서 §3 탈락 분석 · §4 안 1 · §9 적용·재측정 · PR 코드 ProbePlan::of · probe_table · main 코드 예전 ProbePlan::of · 예전 멈춤 판정 · probe_search·share_wide · 티켓 MOR-2575·MOR-2683·MOR-2901 · 관련 PR #1262·#1264(닫힘·미머지). 나눠 담기 예시, probe 질의 예시, 500칸 격자, 집합 그림은 이해를 도우려고 그린 그림이며 실제 요청 데이터가 아니다.

첫 존재 tier에서 500개가 승인되면 기본 검색은 즉시 멈출까?

아니다. 첫 존재 tier의 기본 승인 상한은 0(무제한)이다. 전량 검사 후 tier 문턱을 보고 하위 tier 진행 여부를 정한다. 첫 tier 이후에는 min_candidates가 적용된다.

preserve에 있는 20%는 반드시 원문으로 남을까?

아니다. 일반 숫자+단위는 임시 문구 대상이다. Marker 역할이나 구조 토큰 등 먼저 적용되는 보존 조건과 실제 필드를 확인한다.

단어장에 있는 카테고리면 생성기도 모두 쓸까?

아니다. 규약 v0.5.6 인식 45종과 생성 허용 36종은 다른 집합이다. 그 위에 지정 종류·공급 버전 등 실행 설정이 얹힌다.

Stage 1로 들어온 후보는 Stage 0 후보보다 늘 뒤에 설까?

아니다. Stage는 어떤 모양의 행까지 읽을지 정하는 허용 목록이고 순위 항목이 아니다. 앞뒤는 잔여·tier·비용으로 정한다(§4, §8). 다만 첫 tier 이후에는 스캔 예산이 먼저 차면 Stage 2까지 못 읽을 수 있다.

검색과 렌더의 결과가 다르면 어디부터 볼까?

원본 입력 → projection → Query Frame → binding → nodeTextPolicy → 대치 페이로드 → 실제 렌더 순서로 처음 달라지는 곳을 찾는다. 서버 버전·componentVersion·threading 장 번호·언어별 임시 문구 표를 함께 남긴다.

iui.comp_frames의 한 행은 디자인 전체가 아니라 IIS 경계 노드 하나를 나타낼 수 있다. 검색용 컬럼은 Frame에서 파생한 카테고리·모양·용량·버전 등이며, Rust 서버는 추출한 스냅샷을 읽는다. frame_version=5와 제품 버전 3.0, 내부 발행분 2.6 같은 이름은 같은 축이 아니다.

2026-10-02 제공된 DB 측정(재측정하지 않음): 전체 226,952행, 발행분 2.5의 113,614행·96,538 DO, 도형 속 글자 8,418개·3,357 DO라는 숫자는 당시 기록이다. 지금의 행 수·커버리지나 최신 적재 완료를 뜻하지 않는다.

지난 판의 「iui는 Text 기준」은 더 이상 맞지 않다. 2026-10-19 PR #1296 merged(MOR-2542 상태 확인 필요)로 iui의 벤더 extractTextSlots와 적재 Frame 빌더도 aippt와 같은 isTextBearingNode를 쓴다. Text이거나 content가 문자열인 SVG면 글자 슬롯이다. SVG는 content가 있는지만 보므로, 대치로 글이 ''가 된 도형도 슬롯으로 남는다. 같은 판정이 aippt 원본, iui 사본, 용량 워커 세 곳에 있다.

코드 규칙이 바뀐 것과 운영 색인이 바뀐 것은 다르다. 서버가 지금 읽는 스냅샷이 이 판정으로 다시 적재됐는지는 저장소에서 확인하지 못했다. 따라서 「코드에서 도형 글자 대응」을 「운영 검색의 모든 슬롯에 반영 완료」로 넓혀 읽지 않는다.

코드 근거 · packages/comp-frame-shared/src/eregion/visual-summary/rlsc.ts · classifyFieldNode

aippt SVG 판정

코드 근거 · packages/comp-frame-index/src/frame/component-frame.ts

갱신 2026-10-26 · 문서 v1.2.0. iui는 origin/main 04585231b2339f034f3e89b5f94524591c372b02(2026-10-24, PR #1559 merged 머지)의 고정 파일을 읽었다. 직전 판의 iui bcd1d0218766cbba3a174d1819ab218d613e626f 이후 607커밋이 쌓였고, 직전 판이 인용한 iui 경로 22개 가운데 19개가 바뀌었다. 바뀐 파일은 diff로 하나씩 대조했고, 리팩터링만 있던 곳은 줄 번호만 옮겼다. wiki a0f8b01961db9396206b1ddf097c66cb44bde137와 aippt e6ffa4bdf65ec76f3d3031dc566f3fc7c19eade1는 이번에 다시 읽지 않았고 직전 판 고정 커밋 그대로다. iui 코드 줄 링크는 모두 위 고정 커밋의 git show로 확인했다. 로컬 브랜치나 미커밋 변경을 구현 완료로 포함하지 않았다.

v1.2.0에서 §11에 더한 probe 순서 설명은 열린 PR #1561의 head e43027ce70b6c50b9ae02935aa54dc7a3327c1b2(2026-10-26 확인)를 근거로 한다. 이 PR은 main에 머지되지 않았으므로 구현 완료로 세지 않는다. 측정 수치(입력 847건, 프리셋 20개 시간)는 PR 본문과 PR이 들인 검토 문서 §3·§4·§9에서 옮겼고 이 문서에서 다시 재지 않았다. 비교 대상인 예전 순서는 위 main 고정 커밋의 코드로 확인했다.

위키 spec·design·reference는 9월 학습판 이후 변경이 없지만 실행 구현은 크게 달라졌다. 규약의 alternatives·P8·중첩 정책과 코드의 실제 동작을 분리했다. 이번 작업에서 운영 DB·서비스 배포·실제 렌더 품질이나 전수 회귀 테스트를 새로 측정한 것은 아니다.

전체 개념 수업과 50문항은 학습 페이지, 생성 원본과 고정 코드 지도는 강의자료에 있다. 이전 판의 상세 F01~F09·실험 수치는 보관본에서 당시 근거로 읽는다.