# [Plan] Semantic Text Search 구현 고정 계획

작성일: 2026-06-13  
상태: 구현 완료, PR 검증 중  
대상 repo: `lpcvc-2026-CNU/App`  
기준 branch: `fix/model-artifact-integration`  
구현 branch: `fix/model-artifact-integration`

## 0. 이 문서의 목적

이 문서는 MobileCLIP2-S3 FP16 mixed bundle의 `text_encoder`를 실제 앱 텍스트 검색에 연결하기 전에, 구현 범위와 검증 기준을 고정하기 위한 계획서다.

구현은 이 문서가 정의한 범위를 기준으로 진행한다.  
계획을 바꾸려면 먼저 이 문서를 수정하고, 바꾼 이유와 영향을 기록한다.

## 1. 계획 당시 확인된 상태

- 최신 앱 후보 artifact는 `mobileclip2_s3_server_full_ce_hardneg_fold3_20260611_214421` 기준이다.
- 앱 artifact 폴더에는 image encoder와 text encoder ONNX가 분리되어 있다.
- `manifest.json`의 text encoder 입력은 문자열이 아니라 `text_tokens`, `int64`, `[batch, 77]`이다.
- 계획 작성 당시 앱은 SQLite `LIKE` 기반 keyword search를 사용했다.
- 앱 코드에서는 keyword search와 semantic search를 구분하기 위한 score field가 일부 준비되어 있다.
- 현재 `tokenizer.json`은 "open_clip tokenizer를 사용한다"는 설명 수준이며, Dart에서 token id를 재현하기 위한 완전한 vocab/merges bundle은 아직 없다.
- 계획 작성 당시 semantic text search를 완성하려면 tokenizer, text index, query regression set, evaluation report가 추가로 필요했다.
- 2026-06-14 기준 구현 후 상태는 [`Semantic Text Search Design`](../operations/semantic-text-search-design.html)과 [`Semantic Text Search Evaluation`](../experiments/paper/semantic-text-search-evaluation-2026-06-13.html)을 최신 기준으로 본다.

## 2. 최종 목표

사용자 자연어 query를 text encoder 기반 embedding 검색으로 처리하고, keyword search와 semantic search를 분리한 뒤 fusion score로 Top-3 결과를 반환한다.

```text
사용자 query
  -> TextNormalizer
  -> KeywordSearch(SQLite/candidate_texts)
  -> TokenizerService
  -> TextEncoder ONNX
  -> Query text embedding 512d
  -> TextIndexRepository
  -> Cosine similarity
  -> Keyword/Semantic fusion
  -> Top-3 + decision_status + detail link
```

완료 후 앱은 다음을 명확히 구분해야 한다.

- `keyword_match`: 문자열/별칭/부분 문자열 기반 검색
- `semantic_text`: text encoder embedding 기반 검색
- `semantic_text_fusion`: keyword score와 semantic score를 결합한 검색

## 3. 핵심 설계 원칙

1. 전체 query set 기준으로 검증한다.  
   일부 예시 query만 성공했다고 기능 완료로 보지 않는다.

2. raw score와 display score를 분리한다.  
   semantic score는 확률이 아니라 cosine similarity 기반 유사도다.

3. 앱의 텍스트 검색 결과는 상세 페이지와 연결되어야 한다.  
   Top-3의 모든 `landmark_id`는 `landmark_info.json` 또는 로컬 DB에서 조회 가능해야 한다.

4. parent/sub-landmark 구조를 유지한다.  
   `gyeongbokgung_geunjeongjeon`처럼 세부 class가 나오면 앱은 상위 장소 맥락도 표시해야 한다.

5. tokenizer 재현성을 가장 먼저 검증한다.  
   Python tokenizer와 Dart tokenizer의 token ids가 다르면 text encoder 결과를 신뢰할 수 없다.

6. 모델 artifact와 앱 코드를 동시에 추적한다.  
   artifact 파일, checksum, commit, test result가 PR에 남아야 한다.

## 4. 추가되어야 할 artifact

현재 artifact에 더해 아래 파일을 추가한다.

```text
assets/mobile_artifacts_fp16/
  tokenizer_bundle.json
  text_index.json
  text_search_policy.json
  text_query_regression_set.json
  text_search_eval_report.json
```

### 4.1 tokenizer_bundle.json

목적:

- Dart에서 query 문자열을 text encoder 입력인 `[1, 77] int64 token ids`로 변환한다.
- Python `open_clip` tokenizer 결과와 동일해야 한다.

필수 내용:

- tokenizer type
- context length
- special tokens
- vocab 또는 BPE merge 정보
- normalization 규칙
- fixture query와 expected token ids

### 4.2 text_index.json

목적:

- 검색 대상 catalog 문장들을 미리 text embedding으로 변환해 앱 asset으로 제공한다.
- 앱 실행 중 모든 catalog 문장을 매번 text encoder에 넣지 않는다.

권장 schema:

```json
{
  "model_id": "mobileclip2_s3_server_full_ce_hardneg",
  "embedding_dim": 512,
  "items": [
    {
      "text_id": "gwanghwamun_alias_ko_001",
      "landmark_id": "gwanghwamun",
      "parent_landmark_id": "gyeongbokgung",
      "language": "ko",
      "text_type": "alias",
      "text": "광화문",
      "weight": 1.0,
      "embedding": []
    }
  ]
}
```

### 4.3 text_search_policy.json

목적:

- fusion weight와 decision threshold를 앱 코드에서 분리한다.
- 언어별, query type별 보정이 필요하면 여기서 관리한다.

초기값 후보:

```json
{
  "score_type": "semantic_text_fusion",
  "semantic_weight": 0.75,
  "keyword_weight": 0.25,
  "matched_threshold": 0.45,
  "ambiguous_margin": 0.08,
  "out_of_scope_threshold": 0.25
}
```

주의:

- 이 값은 고정 정답이 아니다.
- 전체 query regression 결과를 보고 조정한다.

### 4.4 text_query_regression_set.json

목적:

- 전체 텍스트 검색을 일부 예시가 아니라 전체 query set으로 평가한다.

query source:

- official Korean/English name
- alias
- `query_examples_ko`
- `query_examples_en`
- visual description query
- function/category query
- parent landmark query
- sub-landmark query
- hard negative query
- out-of-scope query

권장 schema:

```json
{
  "query_id": "gwanghwamun_ko_visual_001",
  "query": "기와지붕 궁궐 정문",
  "expected_landmark_id": "gwanghwamun",
  "expected_parent_landmark_id": "gyeongbokgung",
  "language": "ko",
  "query_type": "visual_description",
  "difficulty": "hard_negative"
}
```

### 4.5 text_search_eval_report.json

목적:

- 전체 query set 실행 결과와 실패 케이스를 남긴다.

필수 기록:

- query count
- Top-1 accuracy
- Top-3 recall
- language별 결과
- query_type별 결과
- hard negative 결과
- out-of-scope 결과
- low-margin cases
- failure cases
- chosen fusion policy

## 5. 앱 구현 범위

아래 단위를 새로 만들거나 기존 코드를 분리한다.

```text
lib/services/tokenizer_service.dart
lib/services/text_embedding_service.dart
lib/data/text_index_repository.dart
lib/services/keyword_search_service.dart
lib/services/semantic_text_search_service.dart
lib/services/text_search_fusion_policy.dart
```

### 5.1 TokenizerService

역할:

- `tokenizer_bundle.json` 로드
- query normalization
- query string -> `[77] int64 token ids`
- Python tokenizer fixture와 결과 일치 확인

### 5.2 TextEmbeddingService

역할:

- text encoder ONNX session 호출
- text token ids -> 512d text embedding
- L2 normalization

### 5.3 TextIndexRepository

역할:

- `text_index.json` 로드
- embedding dim 검증
- `landmark_id`가 앱 DB/catalog에 존재하는지 검증
- text item metadata 제공

### 5.4 KeywordSearchService

역할:

- 기존 SQLite `LIKE` 검색을 유지하되 별도 서비스로 분리
- exact name, alias exact, partial match를 다른 keyword score로 계산

초기 score 후보:

```text
official exact match: 1.00
alias exact match: 0.95
partial match: 0.60
no match: 0.00
```

### 5.5 SemanticTextSearchService

역할:

- query embedding과 text index item embedding cosine similarity 계산
- landmark별 여러 text item이 있을 경우 max 또는 weighted max 사용
- Top-K 후보 생성

초기 landmark score:

```text
semantic_score(landmark) = max(catalog_text_scores_for_landmark)
```

### 5.6 TextSearchFusionPolicy

역할:

- keyword score와 semantic score 결합
- Top-1/Top-2 margin 계산
- `matched`, `ambiguous`, `out_of_scope` 결정

초기 수식:

```text
final_text_score = 0.75 * semantic_score + 0.25 * keyword_score
```

결과 객체는 아래 field를 가진다.

```json
{
  "rank": 1,
  "landmark_id": "naksan_park",
  "parent_landmark_id": null,
  "score_type": "semantic_text_fusion",
  "raw_score": 0.43,
  "semantic_score": 0.43,
  "keyword_score": 0.60,
  "final_text_score": 0.47,
  "display_score": 47,
  "decision_status": "matched",
  "matched_text": "성곽길과 도시 전망이 보이는 공원",
  "text_type": "visual_description"
}
```

## 6. 테스트 계획

### 6.1 Python tests

추가 또는 확장 대상:

- `scripts/check_model_contract.py`
- `scripts/test_model_integration_contract.py`
- 신규 `scripts/evaluate_text_search.py`

검증:

- `tokenizer_bundle.json` 존재
- tokenizer fixture 존재
- `text_index.json` item schema
- text index embedding dim == 512
- text index `landmark_id`가 `landmark_info.json`에 존재
- query regression set schema
- 전체 query Top-1 / Top-3 / margin report 생성

### 6.2 Dart tests

검증:

- Python tokenizer fixture와 Dart tokenizer 결과 일치
- cosine similarity 계산
- keyword score 계산
- semantic score 계산
- fusion score 계산
- decision policy 계산
- out-of-scope query 처리

### 6.3 Flutter tests

검증:

- 텍스트 검색 결과 화면이 확률처럼 표현하지 않음
- `score_type` 표시 또는 debug field 구분
- Top-3 결과 상세보기 연결
- parent/sub-landmark 표시
- keyword-only fallback 문구

### 6.4 QA

현재 PC 환경에서는 Android SDK/ADB/AVD가 준비되지 않았으므로 Android 실기기 runtime 검증은 별도 단계다.

이번 구현 PR의 기본 QA:

- Python contract tests
- text search evaluation report
- Dart unit tests
- Flutter tests
- `flutter analyze` 결과 기록

주의:

- 기존 repo-wide lint 때문에 `flutter analyze`가 실패할 수 있다.
- 이 경우 새로 추가한 semantic search 코드에서 발생한 issue와 기존 issue를 분리 기록한다.

## 7. 전체 구현 대상 issue matrix

| 우선순위 | 항목 | 구현 대상 | 테스트 | 완료 기준 |
|---|---|---|---|---|
| P0 | tokenizer 재현 | `TokenizerService`, `tokenizer_bundle.json` | Dart fixture test | Python/Dart token ids 일치 |
| P0 | text encoder 호출 | `TextEmbeddingService` | Dart/PC ONNX smoke | query embedding 512d |
| P0 | text index 검색 | `TextIndexRepository`, `text_index.json` | Python contract, Dart cosine test | Top-K semantic 후보 생성 |
| P0 | 전체 query 평가 | `text_query_regression_set.json`, `evaluate_text_search.py` | Python eval | Top-1/Top-3/margin report 생성 |
| P1 | keyword/semantic fusion | `TextSearchFusionPolicy` | Dart fusion test | `final_text_score` 계산 |
| P1 | result schema | `LocalApiClientImpl` 또는 text search service | Flutter/schema test | score field 분리 |
| P1 | detail link | result/detail screen | Flutter test | Top-3 상세보기 연결 |
| P1 | parent/sub 표시 | result/detail screen | Flutter test | 부모 맥락 표시 |
| P2 | logging | DB/search log | unit 또는 smoke | score_type, top3, margin 기록 |
| P2 | docs/PR | docs + PR body | link/manual review | 구조/검증/한계 기록 |

## 8. 문서 계획

새 문서:

```text
docs/issues/semantic-text-search-implementation-plan.md
docs/operations/semantic-text-search-design.html
docs/experiments/semantic-text-search-evaluation-2026-06-13.html
```

수정 문서:

```text
docs/index.html
docs/operations/model-serving-contract.html
docs/operations/flutter-ondevice-app-architecture.html
docs/issues/github-issue-app-model-integration-mobileclip2-s3.md
```

문서 역할:

- `issues`: 구현 범위와 미해결 issue 추적
- `operations`: 앱 구조, artifact 구조, score policy 설명
- `experiments`: 전체 query set 평가 결과, 실패 케이스, fusion weight 결정 근거

## 9. PR 본문 고정 구조

PR 본문은 아래 순서를 따른다.

```md
## Goal
- semantic text search를 왜 구현했는지
- keyword-only 검색의 한계

## Scope
- 이번 PR에서 한 것
- 이번 PR에서 하지 않은 것

## Architecture
- TokenizerService
- TextEmbeddingService
- TextIndexRepository
- KeywordSearchService
- SemanticTextSearchService
- TextSearchFusionPolicy

## Artifacts
- tokenizer_bundle.json
- text_index.json
- text_search_policy.json
- text_query_regression_set.json
- text_search_eval_report.json

## Implementation
- 주요 수정 파일
- 각 파일의 역할

## Query Evaluation
- 전체 query count
- Top-1
- Top-3
- language별 결과
- query_type별 결과
- hard-negative 실패 사례

## Validation
- Python tests
- Dart tests
- Flutter tests
- contract check
- analyze 결과

## Known Limits
- Android 실기기 ONNX runtime 미검증 여부
- tokenizer 리스크
- OOS threshold 보정 필요 여부
- hero image 누락 등 앱 UI 잔여 이슈

## Follow-up
- semantic score calibration
- Android runtime QA
- text index 압축/최적화
```

## 10. 하면 안 되는 일

이번 semantic text search 구현 중 아래 작업은 하지 않는다.

1. 모델과 무관한 앱 기능 수정 금지
   - 로그인
   - 알림
   - 건의/관리자 화면
   - 권한/인증
   - Firebase/FCM

2. UI 전체 리디자인 금지
   - 텍스트 검색 결과 표시에 필요한 최소 UI만 수정한다.
   - 홈 화면, 상세 화면의 전체 디자인 개편은 하지 않는다.

3. 기존 이미지 검색 로직을 임의로 재작성 금지
   - image encoder/prototype 검색은 semantic text search와 분리한다.
   - 공통 cosine helper를 만들 수는 있지만, 이미지 검색 결과를 바꾸는 refactor는 별도 PR로 분리한다.

4. confidence policy를 임의로 숫자만 바꾸기 금지
   - threshold 변경은 전체 query evaluation report 또는 image regression 근거가 있을 때만 한다.

5. query 몇 개만 성공했다고 완료 처리 금지
   - 전체 query regression set 결과가 있어야 한다.

6. tokenizer 추측 구현 금지
   - Python `open_clip` tokenizer와 일치하는 fixture 검증 없이 Dart tokenizer를 완료로 보지 않는다.

7. semantic score를 확률로 표시 금지
   - `44% 확률` 같은 표현은 쓰지 않는다.
   - 유사도 표시값 또는 matching score로 표현한다.

8. 대용량 ONNX 파일을 Git에 커밋 금지
   - Git에는 metadata와 contract/test만 커밋한다.
   - 대용량 모델 파일은 Google Drive 또는 artifact 전달 경로를 사용한다.

9. 문서 없이 구현 완료 처리 금지
   - 구현 구조, artifact, evaluation result, known limit가 문서에 남아야 한다.

10. 실패 케이스 숨기기 금지
    - low-margin, hard-negative failure, out-of-scope failure를 기록한다.

11. Android runtime 검증을 했다고 주장 금지
    - Android SDK/ADB/기기 또는 AVD에서 직접 확인한 경우에만 runtime verified라고 쓴다.

12. 기존 repo-wide lint를 semantic search 작업 완료 조건으로 섞지 않기
    - 새 코드에서 생긴 lint와 기존 lint를 분리한다.

## 11. 완료 조건

이 작업은 아래 조건을 모두 만족해야 완료로 본다.

- 구현 대상 issue matrix가 업데이트되어 있다.
- tokenizer fixture가 통과한다.
- text encoder ONNX가 PC에서 query embedding 512d를 생성한다.
- `text_index.json`이 생성되고 schema 검증을 통과한다.
- 전체 query regression set이 생성된다.
- 전체 query evaluation report가 생성된다.
- 앱에서 keyword, semantic, fusion score가 분리된다.
- Top-3 결과가 상세정보로 연결된다.
- parent/sub-landmark 표시가 유지된다.
- Python/Dart/Flutter 테스트가 존재하고 결과가 기록된다.
- 문서 허브에서 관련 문서를 찾을 수 있다.
- PR 본문에 계획, 구조, 구현, 검증, 한계가 들어간다.

## 12. 구현 전 최종 체크

구현 시작 전 다시 확인할 것:

- 현재 App repo branch와 latest commit
- artifact source path
- `text_encoder` ONNX input/output spec
- tokenizer bundle 생성 가능 여부
- catalog/query source 위치
- Android runtime 검증을 이번 PR에 포함할지 여부

현재 계획에서는 Android runtime 검증은 포함하지 않는다. PC 모델 검증과 Flutter QA를 이번 PR의 기본 검증 범위로 둔다.
