본문으로 이동

도움말:S3 연구 메모리/벡터·하이브리드 검색 설계: 두 판 사이의 차이

S3 연구 메모리
S3 연구 메모리 저장소 문서 동기화
S3 연구 메모리 저장소 문서 동기화
4번째 줄: 4번째 줄:
=== 문서 상태 ===
=== 문서 상태 ===


이 문서는 구현된 1단계 검색 계층과 이후 확장 경계를 함께 설명합니다. 현재 <code><nowiki>mcp-http</nowiki></code>에는 교체 가능한 임베딩 제공자, 세 가지 facet projection, copy-on-write exact vector index와 <code><nowiki>legacy</nowiki></code>·<code><nowiki>hybrid_v1</nowiki></code> mode가 들어 있습니다. 기본 mode와 기본 provider는 계속 <code><nowiki>legacy</nowiki></code>, <code><nowiki>disabled</nowiki></code>입니다.
이 문서는 구현된 1단계 검색 계층과 이후 확장 경계를 함께 설명합니다. 현재 <code><nowiki>mcp-http</nowiki></code>에는 교체 가능한 임베딩 제공자, 세 가지 facet projection, copy-on-write exact vector index와 <code><nowiki>legacy</nowiki></code>·<code><nowiki>hybrid_v1</nowiki></code> mode가 들어 있습니다. 기본 mode와 기본 provider는 계속 <code><nowiki>legacy</nowiki></code>, <code><nowiki>disabled</nowiki></code>입니다. 선택적 <code><nowiki>embedding</nowiki></code> Compose profile에는 이 서버의 Vega GPU에서 확인한 Qwen3 F16 <code><nowiki>llama.cpp</nowiki></code> Vulkan 서비스와 검증 downloader가 들어 있습니다.


recent changes poll, 지속형 checkpoint·tombstone, 별도 sidecar, 여러 MCP 인스턴스가 공유하는 generation, cursor v2와 자동 품질 cutover는 아직 후속 단계입니다. 아래 설계에서 이 항목은 구현 목표로 읽습니다.
recent changes poll, 지속형 checkpoint·tombstone, 별도 sidecar, 여러 MCP 인스턴스가 공유하는 generation, cursor v2와 자동 품질 cutover는 아직 후속 단계입니다. 아래 설계에서 이 항목은 구현 목표로 읽습니다.
242번째 줄: 242번째 줄:
* 한 입력의 최대 token 수
* 한 입력의 최대 token 수
외부 API, 서버 안의 모델 또는 별도 내부 추론 서비스는 같은 인터페이스를 구현할 수 있습니다.
외부 API, 서버 안의 모델 또는 별도 내부 추론 서비스는 같은 인터페이스를 구현할 수 있습니다.
현재 <code><nowiki>s3rm-qwen3-retrieval-v1</nowiki></code> profile은 다음을 하나의 generation 입력으로 묶습니다.
* <code><nowiki>Qwen/Qwen3-Embedding-0.6B</nowiki></code> artifact SHA-256과 1024차원
* last pooling과 16K token 입력 계약
* 희귀 CJK byte fallback을 포함해 16K 안에 두는 document facet 4,000자 상한
* <code><nowiki>situation</nowiki></code>, <code><nowiki>approach</nowiki></code>, <code><nowiki>mechanism</nowiki></code>별 영문 query instruction
* instruction을 붙이지 않는 document 형식
* provider의 입력 개수·문자 수 상한
HTTP endpoint와 인증은 transport 설정이며 profile의 의미 계약과 분리합니다. 모델 파일, pooling, instruction 또는 차원을 바꾸면 profile generation이 달라져 전체 exact index를 새로 만듭니다. 서로 다른 벡터 공간은 섞지 않습니다. 외부 provider로 자동 failover하지 않으며 사용할 snapshot이 없으면 legacy 검색으로 대체합니다.


==== 색인 동기화 ====
==== 색인 동기화 ====
475번째 줄: 485번째 줄:
|}
|}


MCP 쓰기는 항상 MediaWiki에 먼저 저장합니다. 1단계에서는 Lesson 생성이나 핵심 내용 revision 성공 뒤 프로세스 안의 dirty hint를 남기고 단일 백그라운드 refresh를 시작합니다. evidence와 relation만 추가하면 현재 projection 본문은 바뀌지 않으므로 재임베딩하지 않습니다. 브라우저 편집은 최소 대조 간격이 지난 hybrid 요청이 백그라운드 refresh를 시작할 때 발견합니다. hybrid 요청 자체는 전체 대조를 기다리지 않습니다. 사용할 snapshot이 있으면 갱신 중에도 그 세대를 사용하고 <code><nowiki>stale</nowiki></code> 상태를 표시하며, 없으면 <code><nowiki>building</nowiki></code>으로 즉시 legacy 검색을 반환합니다. hint나 임베딩이 실패해도 위키 쓰기는 되돌리지 않습니다.
MCP 쓰기는 항상 MediaWiki에 먼저 저장합니다. 1단계에서는 Lesson 생성이나 핵심 내용 revision 성공 뒤 프로세스 안의 dirty hint를 남기고 단일 백그라운드 refresh를 시작합니다. evidence와 relation만 추가하면 현재 projection 본문은 바뀌지 않으므로 재임베딩하지 않습니다. 브라우저 편집은 최소 대조 간격이 지난 hybrid 요청이 백그라운드 refresh를 시작할 때 발견합니다. 대조 결과 Lesson 수와 모든 projection hash가 같으면 모델을 다시 호출하지 않습니다. hybrid 요청 자체는 전체 대조를 기다리지 않습니다. 사용할 snapshot이 있으면 갱신 중에도 그 세대를 사용하고 <code><nowiki>stale</nowiki></code> 상태를 표시하며, 없으면 <code><nowiki>building</nowiki></code>으로 즉시 legacy 검색을 반환합니다. hint나 임베딩이 실패해도 위키 쓰기는 되돌리지 않습니다.


1단계 refresh는 all-or-nothing입니다. 한 Lesson이라도 임베딩하지 못하면 기존 snapshot을 유지하고 <code><nowiki>degraded</nowiki></code>, 기존 snapshot이 없으면 <code><nowiki>unavailable</nowiki></code>로 표시합니다. 항목별 재시도 큐와 recent changes poll은 후속 동기화 단계에서 추가합니다.
1단계 refresh는 all-or-nothing입니다. 한 Lesson이라도 임베딩하지 못하면 기존 snapshot을 유지하고 <code><nowiki>degraded</nowiki></code>, 기존 snapshot이 없으면 <code><nowiki>unavailable</nowiki></code>로 표시합니다. 항목별 재시도 큐와 recent changes poll은 후속 동기화 단계에서 추가합니다.

2026년 7월 18일 (토) 13:04 판

이 페이지는 저장소 문서에서 자동으로 동기화됩니다. 위키에서 직접 편집하지 마세요.

문서 상태

이 문서는 구현된 1단계 검색 계층과 이후 확장 경계를 함께 설명합니다. 현재 mcp-http에는 교체 가능한 임베딩 제공자, 세 가지 facet projection, copy-on-write exact vector index와 legacy·hybrid_v1 mode가 들어 있습니다. 기본 mode와 기본 provider는 계속 legacy, disabled입니다. 선택적 embedding Compose profile에는 이 서버의 Vega GPU에서 확인한 Qwen3 F16 llama.cpp Vulkan 서비스와 검증 downloader가 들어 있습니다.

recent changes poll, 지속형 checkpoint·tombstone, 별도 sidecar, 여러 MCP 인스턴스가 공유하는 generation, cursor v2와 자동 품질 cutover는 아직 후속 단계입니다. 아래 설계에서 이 항목은 구현 목표로 읽습니다.

결정 요약

  • Semantic MediaWiki를 유일한 기준 저장소로 유지합니다.
  • 벡터 인덱스는 후보 Lesson: ID만 반환하는 파생 데이터로 둡니다.
  • 최종 결과는 항상 MediaWiki 최신 리비전을 다시 읽어 만듭니다.
  • 외부 MCP 도구 아홉 개와 중앙 /mcp 진입점은 그대로 유지합니다.
  • 전체 텍스트, 의미 유사도, 명시적 relation을 각각 찾은 뒤 결정적인 규칙으로 합칩니다.
  • 1단계에서는 mcp-http 프로세스 안에서 exact vector search를 사용합니다.
  • 재구축 시간, 메모리 또는 지연 시간이 목표를 넘을 때만 내부 검색 서비스로 옮깁니다.
  • 임베딩 모델, 입력 구성 또는 거리 계산이 바뀌면 새 인덱스 세대를 만듭니다.
  • 의미 검색이 실패해도 전체 텍스트와 relation 검색은 계속 제공합니다.

벡터 저장 제품은 이 경계를 구현하는 선택 사항입니다. 검색 품질과 운영 규모를 확인하기 전에 제품별 API가 도메인 계층으로 퍼지지 않게 합니다.

1단계 구현 범위

현재 exact index는 다음처럼 제한해서 구현했습니다.

  • 서비스 시작 시 단 하나의 백그라운드 작업으로 전체 Lesson snapshot을 읽어 프리웜합니다. MCP Lesson 저장의 dirty hint는 즉시 같은 단일 갱신을 시작합니다. 설정 간격이 지나면 다음 hybrid 요청이 전체 대조를 시작하며, 어느 요청도 refresh 완료를 기다리지 않습니다. 별도 주기 poll은 없습니다.
  • 전체 embedding이 성공했을 때만 새 snapshot을 원자적으로 교체합니다. 한 Lesson만 실패해 성공 항목을 부분 반영하는 재시도 큐는 아직 없습니다.
  • 프로세스마다 active generation 하나를 메모리에 둡니다. 재시작하면 MediaWiki에서 시작한 백그라운드 작업으로 다시 만듭니다.
  • relation, lexical과 semantic 순위를 rrf_v1로 결합합니다. 후보 사이의 별도 diversity reranking은 아직 적용하지 않습니다.
  • 기존 v1 cursor envelope를 유지하고 mode, ranking version과 generation을 fingerprint에 포함합니다.

recent changes checkpoint, tombstone, 항목별 재시도, active/rollback generation, cursor v2와 sidecar는 아래에 설명한 확장 단계에서 추가합니다.

목표

다음 문제를 해결합니다.

  1. 표현이 달라도 증상과 제약이 비슷한 Lesson을 찾습니다.
  2. 비슷한 문제에 쓰인 서로 다른 해결 원리를 함께 보여 줍니다.
  3. 한국어, 영어와 두 언어가 섞인 메모를 같은 검색 흐름에서 다룹니다.
  4. 새 Lesson은 기존 전체 텍스트 검색으로 바로 찾을 수 있게 합니다.
  5. 임베딩 제공자와 인덱스 구현을 중단 없이 바꿀 수 있게 합니다.
  6. 여러 mcp-http 인스턴스로 확장해도 같은 검색 세대를 사용하게 합니다.

다음 기능은 이 설계의 범위에 넣지 않습니다.

  • 벡터 유사도를 사실, 인과관계 또는 relation으로 자동 저장
  • 별도 Lesson 데이터베이스 또는 사용자별 지식 사본
  • 벡터 저장 제품의 범용 API를 MCP 도구로 공개
  • 검색 결과를 근거 없이 요약하거나 해결책으로 확정
  • 다중 테넌트 저장소와 사용자별 비공개 인덱스

전체 구조

flowchart LR
    C["MCP 클라이언트"] --> M["중앙 mcp-http"]
    M --> R["검색 조정 계층"]
    R --> L["MediaWiki 전체 텍스트 후보"]
    R --> G["MediaWiki relation 후보"]
    R --> V["의미 검색 후보"]
    L --> F["후보 결합과 다양성 정렬"]
    G --> F
    V --> F
    F --> H["MediaWiki 최신 리비전 일괄 조회"]
    H --> O["필터·페이지 나누기·출력 제한"]

    W["Semantic MediaWiki 리비전"] --> I["비공개 색인기"]
    I --> P["버전이 있는 입력 구성"]
    P --> E["교체 가능한 임베딩 제공자"]
    E --> X["세대별 파생 벡터 인덱스"]
    X --> V

읽기 경로와 색인 경로를 분리합니다. 검색 요청이 벡터 인덱스를 직접 읽더라도 최종 Lesson 본문과 필터 판정은 MediaWiki에서 가져옵니다. 색인 실패는 위키 쓰기를 되돌리지 않습니다.

내부 경계

현재 WikiAdapter에는 기준 저장소 작업과 후보 검색이 함께 있습니다. 동작을 바꾸기 전에 다음 경계로 나눕니다.

Lesson 저장소

LessonRepository는 MediaWiki 읽기와 허용된 쓰기만 담당합니다.

@dataclass(frozen=True, slots=True)
class LessonSnapshot:
    lesson: Lesson
    revision_id: int


@dataclass(frozen=True, slots=True)
class SnapshotBatch:
    snapshots: tuple[LessonSnapshot, ...]
    next_cursor: str | None
    partial: bool
    scanned: int
    scan_limit: int


@dataclass(frozen=True, slots=True)
class RelationEdgeBatch:
    edges: tuple[LessonRelation, ...]
    next_cursor: str | None
    partial: bool
    scanned: int
    scan_limit: int


@dataclass(frozen=True, slots=True)
class RankedLessonId:
    lesson_id: str
    rank: int
    source_score: float | None


@dataclass(frozen=True, slots=True)
class RankedLessonIdBatch:
    items: tuple[RankedLessonId, ...]
    next_cursor: str | None
    partial: bool
    scanned: int
    scan_limit: int


class LessonRepository(Protocol):
    async def get_snapshot(
        self, lesson_id: str
    ) -> LessonSnapshot | None: ...

    async def get_snapshots(
        self, lesson_ids: Sequence[str]
    ) -> dict[str, LessonSnapshot]: ...

    async def scan_snapshots(
        self, *, limit: int, cursor: str | None
    ) -> SnapshotBatch: ...

    async def search_full_text(
        self, *, query: str, limit: int, cursor: str | None
    ) -> RankedLessonIdBatch: ...

    async def find_relation_edges(
        self,
        *,
        lesson_id: str,
        relations: Sequence[RelationType],
        direction: RelationDirection,
        limit: int,
        cursor: str | None,
    ) -> RelationEdgeBatch: ...

    async def create_lesson(self, lesson: Lesson) -> Lesson: ...
    async def add_relation(
        self, relation: LessonRelation
    ) -> LessonRelation: ...
    async def add_evidence(
        self, lesson_id: str, evidence: Evidence
    ) -> Evidence: ...

    async def revise_lesson(
        self,
        *,
        lesson_id: str,
        expected_revision_id: int,
        operation_id: str,
        changes: Mapping[str, object],
        basis_evidence_ids: Sequence[str],
    ) -> LessonSnapshot: ...

get_snapshots는 후보 ID를 최대 50개씩 읽으며 audit_lessons도 이 경계를 사용합니다. scan_snapshots는 전체 대조와 빈 query 목록에 쓰는 제한된 cursor scan입니다. search_full_text는 MediaWiki의 ranked 전체 텍스트 후보를 반환합니다. find_relation_edges는 전체 Lesson을 읽지 않고 Semantic MediaWiki의 typed relation에서 들어오고 나가는 간선을 찾습니다. 모든 목록 응답에는 엄격한 상한과 cursor를 둡니다.

snapshot은 MediaWiki 사실만 담습니다. 임베딩 대상 필드의 해시는 해당 generation의 ProjectionBuilder가 계산합니다. 그래야 active와 rollback generation이 서로 다른 입력 구성을 써도 같은 snapshot을 안전하게 검증할 수 있습니다.

후보 검색

RetrievalBackend는 Lesson 본문이 아니라 작은 후보 레코드만 반환합니다.

@dataclass(frozen=True, slots=True)
class RankContribution:
    channel: str
    rank: int
    source_score: float | None
    indexed_revision_id: int | None
    indexed_retrieval_hash: str | None
    matched_facets: tuple[str, ...]


@dataclass(frozen=True, slots=True)
class RetrievalCandidate:
    lesson_id: str
    contributions: tuple[RankContribution, ...]


@dataclass(frozen=True, slots=True)
class RetrievalRequest:
    operation: str
    query: str | None
    source_projection: ProjectionDocument | None
    source_revision_id: int | None
    filter_hints: SearchFilters
    scan_limit: int
    expected_generation: str | None
    ranking_version: str


@dataclass(frozen=True, slots=True)
class RetrievalBatch:
    candidates: tuple[RetrievalCandidate, ...]
    generation: str
    partial: bool
    scanned: int
    scan_limit: int


class RetrievalBackend(Protocol):
    async def retrieve(
        self, request: RetrievalRequest
    ) -> RetrievalBatch: ...

같은 ID가 여러 경로와 facet에서 나올 수 있으므로 각 RankContribution을 유지합니다. 제품별 client, collection 이름과 거리 계산 방식은 backend 구현 안에 둡니다. 검색 조정 계층은 경로별 rank를 기준으로 합치며 raw cosine 값을 서로 직접 비교하지 않습니다. source_score는 한 후보 경로 안의 진단과 기준값 적용에만 씁니다. canonical 재검증 뒤 유효한 contribution만 다시 합쳐 전역 순서와 서비스 점수를 만듭니다.

RetrievalRequest.source_projectionfind_analogies에서만 사용합니다. query, source revision, filters, ranking version과 generation 기대값을 cursor fingerprint에 묶습니다. 공개 min_score는 candidate source에 전달하지 않고 canonical 재검증과 fusion이 끝난 서비스 점수에 적용합니다. 각 channel에 사전 기준값이 필요하면 ranking_version에 묶인 내부 설정으로 따로 보정합니다.

여기서 RetrievalBackend는 전체 텍스트, relation과 semantic source를 합치는 도메인 전용 composite입니다. 벡터 저장 제품은 그 아래의 비공개 semantic candidate source만 구현합니다.

입력 구성과 임베딩

ProjectionBuilder는 같은 Lesson에서 검색 목적별 텍스트를 결정적으로 만듭니다. EmbeddingProvider는 텍스트 묶음을 벡터로 바꿉니다. 두 경계에는 각각 독립된 버전을 둡니다.

ProjectionDocument에는 lesson_id, projection version, facet 텍스트, retrieval_hash와 잘림 상태를 넣습니다. retrieval_hash는 repository가 아니라 builder가 계산합니다.

임베딩 제공자는 다음 정보를 고정해 보고해야 합니다.

  • 모델 식별자와 artifact digest
  • tokenizer 식별자와 digest
  • query와 document encoding mode 또는 prefix
  • 벡터 차원
  • 정규화 여부
  • 거리 계산 방식
  • 한 입력의 최대 token 수

외부 API, 서버 안의 모델 또는 별도 내부 추론 서비스는 같은 인터페이스를 구현할 수 있습니다.

현재 s3rm-qwen3-retrieval-v1 profile은 다음을 하나의 generation 입력으로 묶습니다.

  • Qwen/Qwen3-Embedding-0.6B artifact SHA-256과 1024차원
  • last pooling과 16K token 입력 계약
  • 희귀 CJK byte fallback을 포함해 16K 안에 두는 document facet 4,000자 상한
  • situation, approach, mechanism별 영문 query instruction
  • instruction을 붙이지 않는 document 형식
  • provider의 입력 개수·문자 수 상한

HTTP endpoint와 인증은 transport 설정이며 profile의 의미 계약과 분리합니다. 모델 파일, pooling, instruction 또는 차원을 바꾸면 profile generation이 달라져 전체 exact index를 새로 만듭니다. 서로 다른 벡터 공간은 섞지 않습니다. 외부 provider로 자동 failover하지 않으며 사용할 snapshot이 없으면 legacy 검색으로 대체합니다.

색인 동기화

IndexSynchronizer는 MediaWiki 변경을 읽어 파생 인덱스를 갱신합니다. MCP에는 색인 관리 도구를 추가하지 않습니다.

  • MCP 쓰기 성공 뒤 해당 Lesson을 갱신하라는 힌트를 보냅니다.
  • 브라우저 편집을 찾기 위해 MediaWiki recent changes를 주기적으로 읽습니다.
  • 누락된 이벤트를 복구하기 위해 전체 Lesson을 주기적으로 대조합니다.
  • 체크포인트, 재시도 큐와 인덱스 세대는 파생 운영 상태로 취급합니다.
  • 한 generation의 레코드 key는 (generation, lesson_id)입니다.
  • 처리할 때마다 MediaWiki 최신 snapshot을 다시 읽습니다.
  • incoming revision이 현재 indexed_revision_id보다 새로울 때만 원자적으로 교체합니다.
  • 같은 revision의 재시도는 같은 결과가 나와야 합니다.
  • 삭제와 이동 tombstone도 MediaWiki change 순서와 같은 fencing 규칙을 사용합니다.
  • 여러 색인기를 쓸 때는 lease와 fencing token으로 오래된 worker의 쓰기를 막습니다.

호환 이행

첫 구조 변경은 검색 결과를 바꾸지 않습니다.

  • ResearchMemoryServiceLessonRepository와 선택적인 RetrievalBackend를 받게 합니다.
  • 검색 backend가 없으면 현재 동작을 감싼 legacy 구현을 사용합니다.
  • 기존 WikiAdapter의 검색 메서드는 in-memory adapter와 기존 호출자를 옮기는 동안 유지합니다.
  • 일괄 snapshot 조회에는 단일 조회를 반복하는 기본 구현을 둘 수 있습니다.
  • 외부 MCP 도구 이름, 입력과 응답은 검색 backend 교체 때문에 바꾸지 않습니다.

새 내부 경계와 legacy 구현의 결과가 같은지 domain-level 테스트로 확인한 뒤 의미 검색을 추가합니다.

검색용 표현

Lesson 전체를 한 벡터로 합치지 않습니다. 첫 입력 구성 버전은 세 가지 facet을 사용합니다.

facet 포함하는 필드 용도
situation question, context, observation 비슷한 문제와 관찰 찾기
approach attempt 이미 시도한 방법과 실패 형태 찾기
mechanism interpretation, reusable_lesson, applicability 해결 원리와 적용 조건 찾기

title은 전체 텍스트 검색에서 높은 가중치로 다룹니다. evidence 개요, 구조화된 인용, note, URL, verification_basis, author, reviewer, timestamp와 relation은 임베딩하지 않습니다. 이 값은 검색 근거, 필터, 감사 또는 relation 후보 경로에서 따로 사용합니다. metadata_only evidence가 있다는 이유만으로 Lesson 전체를 제외하지 않습니다. 한 Lesson에 서로 다른 확인 범위의 근거가 함께 있을 수 있기 때문입니다.

입력은 다음 규칙으로 만듭니다.

  1. Unicode NFKC로 정규화하고 연속 공백을 하나로 줄입니다.
  2. 필드 이름과 필드 값을 고정된 순서로 붙입니다.
  3. MediaWiki 표시 문법은 versioned sanitizer로 제거하고 외부 URL은 가져오지 않습니다.
  4. query도 facet별 고정 prefix와 encoding mode로 만듭니다.
  5. 빈 필드는 허용하지 않는 현재 Lesson 계약을 그대로 사용합니다.
  6. 모델 입력 한도를 넘으면 facet별 token 예산에 따라 앞부분과 뒷부분을 보존합니다.
  7. 잘림 여부를 색인 상태에 기록합니다.
  8. 요약용 LLM은 색인 경로에 넣지 않습니다.

긴 입력이 자주 잘리기 시작하면 ProjectionBuilder만 바꿔 필드별 chunk embedding과 결정적인 pooling을 도입합니다. 이 변경도 새 입력 구성 버전과 새 인덱스 세대를 사용합니다.

retrieval_hash에는 입력 구성 버전과 정규화된 facet 텍스트를 넣습니다. 다음 값은 별도 metadata_hash로 관리합니다.

  • confidence
  • record_origin
  • author
  • review_state
  • created_at
  • updated_at
  • 구조화된 evidence 존재 여부

이렇게 하면 evidence 추가처럼 의미 본문이 바뀌지 않은 수정은 재임베딩하지 않고 필터 metadata만 갱신할 수 있습니다.

인덱스 레코드

파생 인덱스에는 다음 값만 저장합니다.

설명
lesson_id MediaWiki 페이지의 고정 외부 ID
indexed_revision_id 색인할 때 읽은 리비전 ID
retrieval_hash 임베딩 대상 내용의 해시
metadata_hash 필터 metadata의 해시
updated_at 지연과 최신성 확인용 시각
generation 모델과 입력 구성의 세대
last_change_position 마지막으로 적용한 MediaWiki change 순서
tombstone 삭제·이동 뒤 오래된 upsert를 막는 파생 표시
filter scalar 기존 SearchFilters를 미리 적용하기 위한 값
facet vectors situation, approach, mechanism 벡터

정규화된 본문, evidence 전문과 relation 목록은 저장하지 않습니다. 필터 scalar는 성능 최적화용 사본입니다. 최종 결과에는 MediaWiki에서 다시 읽은 필터를 적용합니다. 삭제된 Lesson의 vector는 제거하되 (generation, lesson_id)의 tombstone과 마지막 change position은 파생 동기화 상태에 유지합니다. upsert와 delete는 이 값을 원자적으로 비교해 느린 worker가 오래된 vector를 되살리지 못하게 합니다.

후보 생성과 순위

search_lessons

다음 후보 경로를 독립적으로 실행합니다.

  1. MediaWiki 전체 텍스트 검색
  2. query와 situation facet의 의미 유사도
  3. query와 approach facet의 의미 유사도
  4. query와 mechanism facet의 의미 유사도

정확한 식별자, 제목과 오류 문자열은 전체 텍스트 결과가 놓치지 않게 합니다. 의미 검색은 같은 뜻을 다른 표현으로 쓴 후보를 보완합니다.

query가 비어 있으면 임베딩을 호출하지 않습니다. 현재처럼 canonical Lesson 목록을 scan_snapshots로 읽고 filters와 페이지 제한을 적용합니다.

find_analogies

다음 순서로 후보를 만듭니다.

  1. 저장된 analogous_to relation 후보
  2. source의 situation과 비슷한 후보
  3. source의 mechanism과 비슷한 교차 문제 후보
  4. 현재 Unicode 단어와 한국어 n-gram 후보

relation 후보는 명시적으로 저장된 연결이라는 점을 계속 구분합니다. 벡터 후보는 relation이 아니며 자동으로 저장하지 않습니다.

결합과 다양성

백엔드마다 점수 분포가 다르므로 raw 점수를 더하지 않습니다. 각 후보 경로의 순위를 versioned reciprocal-rank fusion으로 합칩니다. 같은 해결 방식이 상위 결과를 차지하지 않도록 approachmechanism 벡터에 작은 다양성 가중치를 적용합니다.

다양성은 관련성을 대체하지 않습니다. 먼저 적용 조건과 문제 유사도가 기준을 넘은 후보만 남기고, 그 안에서 서로 다른 해결 원리를 섞습니다. 정렬 규칙과 가중치는 ranking_version으로 고정합니다.

각 channel은 고정된 내부 후보 상한까지 먼저 수집합니다. 모든 channel의 contribution을 합친 뒤 preliminary fusion을 수행하고, 전역 scan_limit 안의 ID만 MediaWiki에서 재검증합니다. 다음 page를 만들기 위해 channel을 조금씩 더 읽는 방식은 쓰지 않습니다. 뒤늦게 온 contribution 때문에 이미 반환한 RRF 순서가 바뀌는 것을 막기 위해서입니다.

어느 channel이 내부 상한에 닿으면 partial=true로 표시합니다. canonical 재검증과 diversity 정렬까지 끝난 bounded 결과 목록에 cursor offset을 적용합니다.

현재 find_analogies는 명시 relation에 score=1.0을 주고 lexical 후보에는 Jaccard score와 shared_terms를 반환합니다. 이 의미를 조용히 바꾸지 않습니다.

semantic 결과는 기존 도구의 versioned retrieval_mode로 공개합니다.

  • legacy는 현재 점수, 순서와 shared_terms를 그대로 유지합니다.
  • hybrid_v1은 raw cosine이 아닌 0~1 서비스 점수와 ranking_version을 반환합니다.
  • hybrid_v1basis에는 semantic_similarityhybrid를 추가합니다.
  • min_score가 어떤 점수에 적용되는지 mode별로 문서화합니다.
  • 같은 호환 기간에는 기본 mode를 조용히 바꾸지 않습니다.

search_lessons에도 같은 mode를 선택적으로 추가했습니다. 입력과 출력 모델, 서버 schema, 예시와 MCP 문서는 같은 계약으로 관리합니다.

기준 저장소 재검증

후보를 받은 뒤 다음 순서로 확인합니다.

  1. 후보 ID를 합치되 경로별 RankContribution은 유지합니다.
  2. MediaWiki에서 최신 LessonSnapshot을 일괄 조회합니다.
  3. 없어진 페이지는 결과에서 빼고 인덱스 정리 대상으로 표시합니다.
  4. 현재 generation의 builder로 최신 retrieval_hash를 계산합니다.
  5. hash가 다른 semantic contribution만 제거하고 재색인합니다.
  6. lexical 또는 relation contribution이 남은 ID는 최신 Lesson으로 계속 평가합니다.
  7. 해시는 같고 리비전만 다르면 최신 Lesson을 사용합니다.
  8. 기존 SearchFilters를 최신 Lesson에 다시 적용합니다.
  9. 유효한 contribution을 다시 결합해 안정적인 순서로 정렬합니다.
  10. 페이지와 UTF-8 출력 제한을 적용합니다.

인덱스 지연으로 오래된 Lesson 내용이 반환되지는 않게 합니다. 대신 일부 semantic 후보가 잠시 빠질 수 있습니다.

scanned는 인덱스 내부 ANN node 수가 아니라 기준 저장소 재검증 전에 서비스가 검토한 후보 ID 수로 정의합니다. scan_limit은 모든 후보 경로를 합친 엄격한 상한입니다.

기존 partial은 후보 탐색 상한에 닿았다는 뜻을 유지합니다. semantic generation이 준비되지 않았거나 synchronizer checkpoint가 MediaWiki change watermark보다 뒤처졌다는 상태는 작은 semantic_status 값으로 따로 표시합니다. 반환 후보의 hash 검사만으로는 아직 후보에 들어오지 않은 새 Lesson을 찾을 수 없기 때문입니다. 이 값은 disabled, building, ready, stale, degraded, unavailable로 제한합니다. ready는 checkpoint가 허용 지연 안에 있고 미해결 missing point나 파생 오류 ID가 없을 때만 사용합니다. checkpoint는 따라잡았지만 일부 Lesson을 색인하지 못했으면 degraded를 사용합니다. 이 필드를 활성화할 때 output 모델, MCP 문서와 테스트를 함께 바꿉니다.

인덱스 세대

다음 중 하나가 바뀌면 새 generation을 만듭니다.

  • 임베딩 모델 또는 artifact
  • tokenizer
  • 입력 정규화와 facet 구성
  • token 예산 또는 chunk pooling
  • 벡터 차원, 정규화 또는 거리 계산
  • 순위 결합에 필요한 인덱스 구조

generation ID는 이 설정을 직렬화한 값의 불투명 해시로 만듭니다. 기존 인덱스를 제자리에서 덮어쓰지 않습니다.

새 generation은 다음 순서로 활성화합니다.

  1. BUILDING 상태로 빈 generation을 만듭니다.
  2. MediaWiki recent changes의 high-water mark를 기록합니다.
  3. 변경 tail을 시작하고 전체 Lesson을 최대 50개씩 읽어 색인합니다.
  4. build 중 생긴 변경을 계속 적용합니다.
  5. 전환 직전 cutover watermark를 기록합니다.
  6. 새 generation의 checkpoint가 cutover watermark에 도달했는지 확인합니다.
  7. 누락, orphan, 리비전과 모델 구성을 검사합니다.
  8. recent changes 보존 구간에 gap이 있으면 활성화하지 않고 다시 구축합니다.
  9. shadow query로 현재 generation과 품질·지연 시간을 비교합니다.
  10. 검사를 통과하면 READY로 바꾸고 active alias를 원자적으로 전환합니다.
  11. 이전 generation은 cursor 유효 기간과 rollback 기간 동안 최신 상태로 유지합니다.
  12. rollback 기간이 끝나면 이전 generation을 삭제합니다.

새 generation에 문제가 있으면 active alias만 이전 값으로 되돌립니다. 파생 볼륨이 손상되면 MediaWiki 리비전에서 다시 만듭니다.

generation은 모델, projection과 거리 계산 구성이 불변이라는 뜻입니다. generation 안의 Lesson 레코드는 revision 조건부 upsert로 계속 갱신합니다. 변경 tail은 alias 전환 뒤에도 멈추지 않으며 rollback 기간에는 active와 이전 generation을 함께 갱신합니다. 이전 generation을 조회할 수 있는 동안 해당 builder, tokenizer, embedding provider와 ranking 설정을 주소로 찾을 수 있게 유지합니다. 변경된 Lesson도 각 generation의 모델로 따로 임베딩합니다.

파생 vector volume은 기준 백업 대상이 아닙니다. snapshot은 재시작 시간을 줄이는 선택 사항이며 generation, model digest와 MediaWiki checkpoint가 맞지 않으면 폐기하고 다시 구축합니다.

페이지 나누기와 cursor

1단계 구현은 기존 v1 cursor envelope를 유지하고, 요청 fingerprint 안에 query, source revision·projection hash, effective_mode, ranking_version과 generation을 묶습니다. 따라서 기존 legacy cursor 형식은 바뀌지 않으면서도 generation이나 실제 mode가 달라지면 cursor를 거부합니다.

아래 v2 형태는 만료 시각과 generation을 독립 필드로 운영해야 할 때의 후속 형식입니다.

{
  "v": 2,
  "tool": "find_analogies",
  "fp": "<request fingerprint>",
  "gen": "<retrieval generation>",
  "pos": 37,
  "exp": 1893456000
}

fingerprint에는 query, source revision과 projection hash, filters, fields, min_score, effective_moderanking_version을 포함합니다. generation은 같은 모델과 정렬 규칙을 사용하게 하지만, 페이지 사이에 생긴 MediaWiki 편집까지 snapshot으로 고정하지는 않습니다. 따라서 현재 검색과 마찬가지로 편집이 동시에 일어나면 결과가 중복되거나 다음 페이지로 이동할 수 있습니다. 정확한 query snapshot이 필요해지면 bounded 후보 ID를 별도 cursor 상태로 보존하는 기능을 독립적으로 설계합니다.

해당 generation을 더 이상 제공할 수 없으면 첫 페이지부터 다시 조회하라는 cursor 오류를 반환합니다.

각 호출은 모든 channel의 bounded 후보를 다시 모아 canonical 재검증과 정렬을 완료합니다. pos는 이 최종 목록의 offset입니다. 사라지거나 filter에 맞지 않는 후보는 offset을 적용하기 전에 목록에서 빠집니다. 출력 byte 제한 때문에 페이지가 짧아지면 cursor는 실제 반환한 항목 수만큼만 이동해 빠진 정상 후보가 다음 페이지에 나오게 합니다. v2로 전환할 때는 MCP 문서와 테스트를 함께 바꿉니다.

동기화와 일관성

일관성 보장은 다음처럼 나눕니다.

항목 보장
반환한 Lesson 내용 MediaWiki 최신 리비전으로 확인
전체 텍스트 검색 저장 성공 뒤 바로 검색 가능
의미 검색 완전성 짧은 지연을 허용하는 eventual consistency
relation 의미 MediaWiki에 저장된 typed relation만 기준
벡터 유사도 후보 추천이며 사실 또는 인과관계가 아님

MCP 쓰기는 항상 MediaWiki에 먼저 저장합니다. 1단계에서는 Lesson 생성이나 핵심 내용 revision 성공 뒤 프로세스 안의 dirty hint를 남기고 단일 백그라운드 refresh를 시작합니다. evidence와 relation만 추가하면 현재 projection 본문은 바뀌지 않으므로 재임베딩하지 않습니다. 브라우저 편집은 최소 대조 간격이 지난 hybrid 요청이 백그라운드 refresh를 시작할 때 발견합니다. 대조 결과 Lesson 수와 모든 projection hash가 같으면 모델을 다시 호출하지 않습니다. hybrid 요청 자체는 전체 대조를 기다리지 않습니다. 사용할 snapshot이 있으면 갱신 중에도 그 세대를 사용하고 stale 상태를 표시하며, 없으면 building으로 즉시 legacy 검색을 반환합니다. hint나 임베딩이 실패해도 위키 쓰기는 되돌리지 않습니다.

1단계 refresh는 all-or-nothing입니다. 한 Lesson이라도 임베딩하지 못하면 기존 snapshot을 유지하고 degraded, 기존 snapshot이 없으면 unavailable로 표시합니다. 항목별 재시도 큐와 recent changes poll은 후속 동기화 단계에서 추가합니다.

장애 동작

장애 동작
query 임베딩 실패 전체 텍스트와 relation 후보로 fallback
벡터 인덱스 연결 실패 전체 텍스트와 relation 후보로 fallback
인덱스 지연 hash가 다른 semantic 기여를 빼고 semantic_status=stale 표시
일부 Lesson 임베딩 실패 1단계는 기존 snapshot 전체를 유지하고 semantic_status=degraded 표시
프로세스 재시작·메모리 손실 서비스 시작 시 단일 백그라운드 작업으로 새 generation 프리웜
브라우저 편집 미반영 최소 대조 간격이 지난 hybrid 요청이 백그라운드 전체 대조 시작
모델과 인덱스 구성 불일치 readiness 실패, generation 활성화 금지
새 모델 품질 저하 1단계는 provider 설정을 되돌려 재시작, 후속 단계는 active alias rollback

fallback은 cursor가 없는 첫 page에서만 검색 mode를 바꿀 수 있습니다. hybrid 요청이 legacy로 fallback되면 응답과 cursor에 실제 effective_mode=legacy를 기록합니다. hybrid cursor를 발급한 뒤 semantic backend가 실패하면 중간 page에서 legacy 순서로 바꾸지 않습니다. 재시도 가능한 오류를 반환하거나 첫 page부터 다시 조회하도록 안내합니다.

semantic 검색 장애만으로 공개 /healthz를 실패시키지 않습니다. 전체 텍스트 fallback이 가능하면 MCP는 읽기 서비스를 계속 제공합니다. 상세 semantic readiness는 내부 상태와 제한된 로그·지표로 확인합니다.

자원 격리

읽기 도구는 인증 없이 호출할 수 있으므로 semantic 추론 자원을 별도로 제한합니다.

1단계는 query embedding과 background indexing이 하나의 bounded semaphore와 대기열을 공유합니다. 동시 실행 수, 대기열 길이, 대기·요청 timeout, batch 크기, 입력 길이와 응답 크기를 제한하고 circuit breaker를 적용합니다.

트래픽이나 재구축 시간이 이 공유 한도에서 충돌하면 후속 단계에서 query와 indexing queue를 분리하고 background 우선순위와 concurrency를 따로 둡니다. 로컬 CPU·GPU worker를 쓸 때는 MCP event loop와 격리하고, 외부 API에는 비용 예산과 명시적 재시도 정책을 추가합니다. 어느 단계에서도 포화 상태가 lexical 검색과 쓰기 경로의 thread, connection과 memory를 소진하지 않게 합니다.

한도를 넘으면 cursor가 없는 첫 page는 즉시 legacy로 fallback합니다. 이미 hybrid cursor를 발급했다면 검색 순서를 바꾸지 않고 재시도 또는 첫 page 재시작 오류를 반환합니다.

단계별 확장

단계 구성 다음 단계로 옮기는 기준
0 현재 전체 텍스트와 lexical analogy 평가 기준선 확보
1 mcp-http 안의 copy-on-write exact vector snapshot 품질과 비용 검증
2 backend network 전용 검색 sidecar와 파생 volume 재구축 시간, 메모리 또는 p95 지연 목표 초과
3 공유 내부 검색 서비스와 여러 mcp-http 인스턴스 가용성 또는 동시 요청 때문에 수평 확장 필요

단계 1에서는 서버가 먼저 전체 텍스트 검색을 제공합니다. 시작 프리웜과 dirty/due 백그라운드 대조가 같은 구성 generation의 vector matrix와 metadata map을 만들고, 전체 대조가 성공할 때마다 copy-on-write snapshot을 원자적으로 교체합니다. Lesson 수, 벡터 차원, 실제 메모리, 재구축 시간과 exact cosine p95를 측정합니다. 단순한 Lesson 수만으로 제품을 바꾸지 않습니다.

단계 2의 검색 sidecar도 도메인 전용 search_candidates와 내부 상태 확인만 제공합니다. 벡터 저장 제품의 포트를 호스트나 외부 네트워크에 열지 않습니다.

단계 3에서는 색인기를 한 인스턴스만 실행하거나 lease로 조정합니다. 모든 MCP 인스턴스는 같은 active generation을 읽습니다. 외부에는 계속 중앙 /mcp 주소 하나만 제공합니다. 현재 프로세스 메모리에 있는 인증 상태도 수평 확장 전에 공유 상태 또는 안전한 routing으로 바꿔야 합니다.

보안과 개인정보

임베딩은 익명화된 값이 아닙니다. 원문과 같은 민감도로 다룹니다.

  • 기본 선택은 서버 안이나 내부 네트워크의 다국어 임베딩 모델입니다.
  • 외부 임베딩 API는 서버 담당자가 전송, 보존, 학습 사용, region과 비용 조건을 확인한 뒤 명시적으로 켭니다.
  • 검색 query는 아직 위키에 저장하지 않은 민감한 문제를 포함할 수 있습니다.
  • 원문 query와 Lesson 본문을 로그에 남기지 않습니다.
  • 지표에는 generation, revision, 지연 시간, 결과 수와 오류 코드만 남깁니다.
  • 벡터 인덱스는 backend network에서만 접근합니다.
  • 별도 호스트를 쓰면 서비스 인증과 전송 암호화를 적용합니다.
  • 색인기 계정은 Lesson 읽기에 필요한 최소 권한만 가집니다.
  • 모델과 tokenizer artifact는 digest로 고정합니다.
  • 페이지가 삭제되거나 접근 정책이 바뀌면 active와 rollback generation에서 모두 제거합니다.

공개 위키에 있는 Lesson도 외부 임베딩 제공자에게 전송해도 된다고 자동으로 간주하지 않습니다. 검색 query는 위키보다 더 민감할 수 있습니다.

관측 항목

다음 값을 query 원문 없이 측정합니다.

  • recent changes 지연 시간과 처리하지 않은 revision 수
  • 마지막 poll과 전체 대조 시각
  • 기준 Lesson 수와 generation별 색인 수
  • missing, orphan, stale hash 수
  • embedding, 후보 검색, MediaWiki 재조회와 재정렬 지연 시간
  • embedding queue 깊이, 거부, timeout과 circuit breaker 상태
  • fallback 비율과 원인
  • hash 불일치로 제거한 후보 수
  • query당 후보 수와 canonical filter로 제거한 수
  • generation build 진행률, 실패, 재시도와 파생 오류 수
  • active model, 입력 구성과 ranking version
  • cursor 무효화와 출력 byte 제한 발생 비율

품질 평가

벡터 검색은 기술적으로 동작하는 것만으로 활성화하지 않습니다. 한국어, 영어와 혼합 query를 포함한 작은 고정 평가셋을 먼저 만듭니다.

다음 항목을 현재 검색과 비교합니다.

  • top 5 안에서 실제로 적용 가능한 후보 비율
  • 다른 분야이지만 도움이 되는 후보 비율
  • 저장된 analogous_to relation의 회수율
  • 정확한 오류 문자열과 고유 명칭 검색의 회귀 여부
  • 같은 해결책이 반복되는 비율
  • 근거와 적용 조건이 부족한 후보 비율
  • p50, p95 지연 시간과 fallback 비율

평가용 query 원문에 비밀이나 개인 정보를 넣지 않습니다. 실제 사용 기록을 저장소에 그대로 커밋하지 않습니다. 새 generation은 품질 기준, 출력 제한과 장애 fallback 테스트를 모두 통과한 뒤 활성화합니다.

구현 순서

  1. 완료: LessonSnapshot과 MediaWiki 최대 50개 일괄 조회를 추가했습니다.
  2. 완료: SemanticIndexBackend 경계와 기존 기본 동작을 유지하는 legacy mode를 추가했습니다.
  3. 완료: 고정 벡터 fake와 domain-level 회귀·fallback·stale hash 테스트를 추가했습니다.
  4. 완료: ProjectionBuilder, 교체 가능한 EmbeddingProvider와 exact index를 추가했습니다.
  5. 완료: find_analogiessearch_lessons에 명시적 hybrid_v1 mode를 적용했습니다. 기본 mode는 바꾸지 않았습니다.
  6. 완료: revision ID를 포함한 조회, 조건부 revise_lesson, bounded audit_lessons와 full citation 조회를 추가했습니다. 이 도구들은 기준 MediaWiki를 읽고 쓰며 별도 검색 저장소를 만들지 않습니다.
  7. 운영 후속: 고정 평가셋과 shadow 측정으로 품질, 비용, p95, fallback 비율을 확인한 뒤 활성화 범위를 정합니다.
  8. 확장 후속: recent changes checkpoint, tombstone과 지속형 generation을 추가합니다.
  9. 조건부 후속: 실제 지표가 전환 기준을 넘을 때만 sidecar 또는 공유 검색 서비스로 옮깁니다.

각 단계에서 기존 최대 20건, cursor, partial, scanned, scan_limit와 UTF-8 직렬화 상한을 유지합니다. 모델, 응답 또는 검색 정책이 바뀌면 MCP 모델, 예시와 문서를 함께 수정합니다.