도움말:S3 연구 메모리/벡터·하이브리드 검색 설계
이 페이지는 저장소 문서에서 자동으로 동기화됩니다. 위키에서 직접 편집하지 마세요.
문서 상태
이 문서는 추후 도입할 검색 계층의 설계안입니다. 현재 서비스는 MediaWiki 전체 텍스트 검색과 제한된 단어 겹침으로 동작합니다. 이 문서에 나온 벡터 인덱스, 설정값, 응답 필드는 아직 구현된 기능이 아닙니다.
결정 요약
- Semantic MediaWiki를 유일한 기준 저장소로 유지합니다.
- 벡터 인덱스는 후보
Lesson:ID만 반환하는 파생 데이터로 둡니다. - 최종 결과는 항상 MediaWiki 최신 리비전을 다시 읽어 만듭니다.
- 외부 MCP 도구 일곱 개와 중앙
/mcp진입점은 그대로 유지합니다. - 전체 텍스트, 의미 유사도, 명시적 relation을 각각 찾은 뒤 결정적인 규칙으로 합칩니다.
- 처음에는
mcp-http프로세스 안에서 exact vector search를 사용합니다. - 재구축 시간, 메모리 또는 지연 시간이 목표를 넘을 때만 내부 검색 서비스로 옮깁니다.
- 임베딩 모델, 입력 구성 또는 거리 계산이 바뀌면 새 인덱스 세대를 만듭니다.
- 의미 검색이 실패해도 전체 텍스트와 relation 검색은 계속 제공합니다.
벡터 저장 제품은 이 경계를 구현하는 선택 사항입니다. 검색 품질과 운영 규모를 확인하기 전에 제품별 API가 도메인 계층으로 퍼지지 않게 합니다.
목표
다음 문제를 해결합니다.
- 표현이 달라도 증상과 제약이 비슷한 Lesson을 찾습니다.
- 비슷한 문제에 쓰인 서로 다른 해결 원리를 함께 보여 줍니다.
- 한국어, 영어와 두 언어가 섞인 메모를 같은 검색 흐름에서 다룹니다.
- 새 Lesson은 기존 전체 텍스트 검색으로 바로 찾을 수 있게 합니다.
- 임베딩 제공자와 인덱스 구현을 중단 없이 바꿀 수 있게 합니다.
- 여러
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
retrieval_hash: str
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 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: ...
get_snapshots는 후보 ID를 최대 50개씩 읽습니다. retrieval_hash는 임베딩 대상 필드만 정규화해 만든 해시입니다. 근거나 relation만 바뀌어 리비전 ID가 달라져도 이 해시가 같으면 기존 벡터를 계속 쓸 수 있습니다.
후보 검색
RetrievalBackend는 Lesson 본문이 아니라 작은 후보 레코드만 반환합니다.
@dataclass(frozen=True, slots=True)
class RetrievalCandidate:
lesson_id: str
basis: 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 RetrievalBatch:
candidates: tuple[RetrievalCandidate, ...]
generation: str
has_more: bool
partial: bool
scanned: int
scan_limit: int
class RetrievalBackend(Protocol):
async def retrieve(
self, request: RetrievalRequest
) -> RetrievalBatch: ...
제품별 client, collection 이름과 거리 계산 방식은 이 구현 안에 둡니다. 서비스 계층은 rank를 기준으로 후보 경로를 합치며 raw cosine 값을 서로 직접 비교하지 않습니다. source_score는 한 후보 경로 안의 진단과 기준값 적용에만 씁니다. 결합이 끝난 뒤 검색 조정 계층이 별도의 서비스 점수를 만듭니다.
입력 구성과 임베딩
ProjectionBuilder는 같은 Lesson에서 검색 목적별 텍스트를 결정적으로 만듭니다. EmbeddingProvider는 텍스트 묶음을 벡터로 바꿉니다. 두 경계에는 각각 독립된 버전을 둡니다.
임베딩 제공자는 다음 정보를 고정해 보고해야 합니다.
- 모델 식별자와 artifact digest
- tokenizer 식별자와 digest
- 벡터 차원
- 정규화 여부
- 거리 계산 방식
- 한 입력의 최대 token 수
외부 API, 서버 안의 모델 또는 별도 내부 추론 서비스는 같은 인터페이스를 구현할 수 있습니다.
색인 동기화
IndexSynchronizer는 MediaWiki 변경을 읽어 파생 인덱스를 갱신합니다. MCP에는 색인 관리 도구를 추가하지 않습니다.
- MCP 쓰기 성공 뒤 해당 Lesson을 갱신하라는 힌트를 보냅니다.
- 브라우저 편집을 찾기 위해 MediaWiki recent changes를 주기적으로 읽습니다.
- 누락된 이벤트를 복구하기 위해 전체 Lesson을 주기적으로 대조합니다.
- 체크포인트, 재시도 큐와 인덱스 세대는 파생 운영 상태로 취급합니다.
- 모든 upsert는
lesson_id + revision_id기준으로 반복 실행해도 같은 결과가 나와야 합니다.
호환 이행
첫 구조 변경은 검색 결과를 바꾸지 않습니다.
ResearchMemoryService가LessonRepository와 선택적인RetrievalBackend를 받게 합니다.- 검색 backend가 없으면 현재 동작을 감싼 legacy 구현을 사용합니다.
- 기존
WikiAdapter의 검색 메서드는 in-memory adapter와 기존 호출자를 옮기는 동안 유지합니다. - 일괄 snapshot 조회에는 단일 조회를 반복하는 기본 구현을 둘 수 있습니다.
- 외부 MCP 도구 이름, 입력과 응답은 이 단계에서 바꾸지 않습니다.
새 내부 경계와 legacy 구현의 결과가 같은지 domain-level 테스트로 확인한 뒤 의미 검색을 추가합니다.
검색용 표현
Lesson 전체를 한 벡터로 합치지 않습니다. 첫 입력 구성 버전은 세 가지 facet을 사용합니다.
| facet | 포함하는 필드 | 용도 |
|---|---|---|
situation |
question, context, observation |
비슷한 문제와 관찰 찾기 |
approach |
attempt |
이미 시도한 방법과 실패 형태 찾기 |
mechanism |
interpretation, reusable_lesson, applicability |
해결 원리와 적용 조건 찾기 |
title은 전체 텍스트 검색에서 높은 가중치로 다룹니다. evidence 개요, 구조화된 인용, URL, author, reviewer, timestamp와 relation은 임베딩하지 않습니다. 이 값은 검색 근거, 필터 또는 relation 후보 경로에서 따로 사용합니다.
입력은 다음 규칙으로 만듭니다.
- Unicode NFKC로 정규화하고 연속 공백을 하나로 줄입니다.
- 필드 이름과 필드 값을 고정된 순서로 붙입니다.
- 빈 필드는 허용하지 않는 현재 Lesson 계약을 그대로 사용합니다.
- 모델 입력 한도를 넘으면 facet별 token 예산에 따라 앞부분과 뒷부분을 보존합니다.
- 잘림 여부를 색인 상태에 기록합니다.
- 요약용 LLM은 색인 경로에 넣지 않습니다.
긴 입력이 자주 잘리기 시작하면 ProjectionBuilder만 바꿔 필드별 chunk embedding과 결정적인 pooling을 도입합니다. 이 변경도 새 입력 구성 버전과 새 인덱스 세대를 사용합니다.
retrieval_hash에는 입력 구성 버전과 정규화된 facet 텍스트를 넣습니다. 다음 값은 별도 metadata_hash로 관리합니다.
confidencerecord_originauthorreview_statecreated_atupdated_at- 구조화된 evidence 존재 여부
이렇게 하면 evidence 추가처럼 의미 본문이 바뀌지 않은 수정은 재임베딩하지 않고 필터 metadata만 갱신할 수 있습니다.
인덱스 레코드
파생 인덱스에는 다음 값만 저장합니다.
| 값 | 설명 |
|---|---|
lesson_id |
MediaWiki 페이지의 고정 외부 ID |
indexed_revision_id |
색인할 때 읽은 리비전 ID |
retrieval_hash |
임베딩 대상 내용의 해시 |
metadata_hash |
필터 metadata의 해시 |
updated_at |
지연과 최신성 확인용 시각 |
generation |
모델과 입력 구성의 세대 |
| filter scalar | 기존 SearchFilters를 미리 적용하기 위한 값
|
| facet vectors | situation, approach, mechanism 벡터
|
정규화된 본문, evidence 전문과 relation 목록은 저장하지 않습니다. 필터 scalar는 성능 최적화용 사본입니다. 최종 결과에는 MediaWiki에서 다시 읽은 필터를 적용합니다.
후보 생성과 순위
search_lessons
다음 후보 경로를 독립적으로 실행합니다.
- MediaWiki 전체 텍스트 검색
- query와
situationfacet의 의미 유사도 - query와
approachfacet의 의미 유사도 - query와
mechanismfacet의 의미 유사도
정확한 식별자, 제목과 오류 문자열은 전체 텍스트 결과가 놓치지 않게 합니다. 의미 검색은 같은 뜻을 다른 표현으로 쓴 후보를 보완합니다.
find_analogies
다음 순서로 후보를 만듭니다.
- 저장된
analogous_torelation 후보 - source의
situation과 비슷한 후보 - source의
mechanism과 비슷한 교차 문제 후보 - 현재 Unicode 단어와 한국어 n-gram 후보
relation 후보는 명시적으로 저장된 연결이라는 점을 계속 구분합니다. 벡터 후보는 relation이 아니며 자동으로 저장하지 않습니다.
결합과 다양성
백엔드마다 점수 분포가 다르므로 raw 점수를 더하지 않습니다. 각 후보 경로의 순위를 versioned reciprocal-rank fusion으로 합칩니다. 같은 해결 방식이 상위 결과를 차지하지 않도록 approach와 mechanism 벡터에 작은 다양성 가중치를 적용합니다.
다양성은 관련성을 대체하지 않습니다. 먼저 적용 조건과 문제 유사도가 기준을 넘은 후보만 남기고, 그 안에서 서로 다른 해결 원리를 섞습니다. 정렬 규칙과 가중치는 ranking_version으로 고정합니다.
외부 응답의 score는 raw cosine이 아니라 0~1 범위의 서비스 점수입니다. 현재 find_analogies의 lexical score 의미는 shadow 평가와 문서·테스트 변경 없이 조용히 바꾸지 않습니다. 의미 검색을 활성화할 때는 다음을 함께 적용합니다.
- 고정 평가셋으로 서비스 점수를 보정
- 응답에 작은
ranking_version또는 동등한 진단 값 제공 basis에semantic_similarity와hybrid추가- lexical 후보의
shared_terms유지 min_score를 서비스 점수에 적용한다고 명시
기준 저장소 재검증
후보를 받은 뒤 다음 순서로 확인합니다.
- 후보 ID를 중복 제거하고 내부
scan_limit까지만 유지합니다. - MediaWiki에서 최신
LessonSnapshot을 일괄 조회합니다. - 없어진 페이지는 결과에서 빼고 인덱스 정리 대상으로 표시합니다.
retrieval_hash가 달라진 semantic 후보는 결과에서 빼고 재색인합니다.- 해시는 같고 리비전만 다르면 최신 Lesson을 사용합니다.
- 기존
SearchFilters를 최신 Lesson에 다시 적용합니다. - 안정적인 순서로 정렬한 뒤 페이지와 UTF-8 출력 제한을 적용합니다.
인덱스 지연으로 오래된 Lesson 내용이 반환되지는 않게 합니다. 대신 일부 semantic 후보가 잠시 빠질 수 있습니다. 이 경우 응답의 partial을 참으로 표시합니다.
scanned는 인덱스 내부 ANN node 수가 아니라 기준 저장소 재검증 전에 서비스가 검토한 후보 ID 수로 정의합니다. scan_limit은 모든 후보 경로를 합친 엄격한 상한입니다.
인덱스 세대
다음 중 하나가 바뀌면 새 generation을 만듭니다.
- 임베딩 모델 또는 artifact
- tokenizer
- 입력 정규화와 facet 구성
- token 예산 또는 chunk pooling
- 벡터 차원, 정규화 또는 거리 계산
- 순위 결합에 필요한 인덱스 구조
generation ID는 이 설정을 직렬화한 값의 불투명 해시로 만듭니다. 기존 인덱스를 제자리에서 덮어쓰지 않습니다.
새 generation은 다음 순서로 활성화합니다.
BUILDING상태로 빈 generation을 만듭니다.- MediaWiki recent changes의 high-water mark를 기록합니다.
- 전체 Lesson을 최대 50개씩 읽어 색인합니다.
- build 중 생긴 변경을 high-water mark 이후부터 다시 적용합니다.
- 누락, orphan, 리비전과 모델 구성을 검사합니다.
- shadow query로 현재 generation과 품질·지연 시간을 비교합니다.
- 검사를 통과하면
READY로 바꾸고 active alias를 원자적으로 전환합니다. - 이전 generation은 cursor 유효 기간과 rollback 기간 동안 최신 상태로 유지합니다.
- rollback 기간이 끝나면 이전 generation을 삭제합니다.
새 generation에 문제가 있으면 active alias만 이전 값으로 되돌립니다. 파생 볼륨이 손상되면 MediaWiki 리비전에서 다시 만듭니다.
페이지 나누기와 cursor
의미 검색을 활성화하면 cursor에는 요청 fingerprint와 함께 generation을 넣습니다.
{
"v": 2,
"tool": "find_analogies",
"fp": "<request fingerprint>",
"gen": "<retrieval generation>",
"pos": 37,
"exp": 1780000000
}
fingerprint에는 query, filters, fields, min_score와 검색 전략을 포함합니다. generation이 cursor 유효 기간 안에 남아 있으면 같은 결과 순서를 이어서 반환합니다. 해당 generation을 더 이상 제공할 수 없으면 첫 페이지부터 다시 조회하라는 cursor 오류를 반환합니다.
출력 byte 제한 때문에 페이지 끝의 결과를 빼면 cursor도 실제로 반환한 마지막 후보 까지만 이동합니다. 현재 v1 cursor는 기존 검색 경로에서 한시적으로 읽고, v2 전환은 MCP 문서와 테스트를 함께 바꿉니다.
동기화와 일관성
일관성 보장은 다음처럼 나눕니다.
| 항목 | 보장 |
|---|---|
| 반환한 Lesson 내용 | MediaWiki 최신 리비전으로 확인 |
| 전체 텍스트 검색 | 저장 성공 뒤 바로 검색 가능 |
| 의미 검색 완전성 | 짧은 지연을 허용하는 eventual consistency |
| relation 의미 | MediaWiki에 저장된 typed relation만 기준 |
| 벡터 유사도 | 후보 추천이며 사실 또는 인과관계가 아님 |
MCP 쓰기는 항상 MediaWiki에 먼저 저장합니다. 성공 뒤 색인 갱신 힌트를 보내지만, 이 힌트가 실패해도 쓰기는 성공으로 처리합니다. recent changes poll과 전체 대조가 누락을 복구합니다.
한 페이지의 임베딩이 반복해서 실패하면 제한된 횟수만 재시도합니다. 이후 해당 ID를 파생 오류 목록에 남기고 다음 변경 처리를 계속합니다. 한 페이지 때문에 전체 체크포인트가 멈추지 않게 합니다.
장애 동작
| 장애 | 동작 |
|---|---|
| query 임베딩 실패 | 전체 텍스트와 relation 후보로 fallback |
| 벡터 인덱스 연결 실패 | 전체 텍스트와 relation 후보로 fallback |
| 인덱스 지연 | hash가 다른 후보를 빼고 partial=true 표시
|
| 일부 Lesson 임베딩 실패 | 성공한 항목을 활성화하고 실패 ID만 재시도 |
| 파생 볼륨 손상 | MediaWiki에서 새 generation 재구축 |
| recent changes 누락 | 주기적 전체 대조로 복구 |
| 모델과 인덱스 구성 불일치 | readiness 실패, generation 활성화 금지 |
| 새 모델 품질 저하 | active alias를 이전 generation으로 rollback |
semantic 검색 장애만으로 공개 /healthz를 실패시키지 않습니다. 전체 텍스트 fallback이 가능하면 MCP는 읽기 서비스를 계속 제공합니다. 상세 semantic readiness는 내부 상태와 제한된 로그·지표로 확인합니다.
단계별 확장
| 단계 | 구성 | 다음 단계로 옮기는 기준 |
|---|---|---|
| 0 | 현재 전체 텍스트와 lexical analogy | 평가 기준선 확보 |
| 1 | mcp-http 안의 immutable exact vector generation |
품질과 비용 검증 |
| 2 | backend network 전용 검색 sidecar와 파생 volume | 재구축 시간, 메모리 또는 p95 지연 목표 초과 |
| 3 | 공유 내부 검색 서비스와 여러 mcp-http 인스턴스 |
가용성 또는 동시 요청 때문에 수평 확장 필요 |
단계 1에서는 서버가 먼저 전체 텍스트 검색을 제공하고, 백그라운드에서 새 immutable generation을 만든 뒤 포인터를 교체합니다. 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 재조회와 재정렬 지연 시간
- fallback 비율과 원인
- hash 불일치로 제거한 후보 수
- query당 후보 수와 canonical filter로 제거한 수
- generation build 진행률, 실패, 재시도와 파생 오류 수
- active model, 입력 구성과 ranking version
- cursor 무효화와 출력 byte 제한 발생 비율
품질 평가
벡터 검색은 기술적으로 동작하는 것만으로 활성화하지 않습니다. 한국어, 영어와 혼합 query를 포함한 작은 고정 평가셋을 먼저 만듭니다.
다음 항목을 현재 검색과 비교합니다.
- top 5 안에서 실제로 적용 가능한 후보 비율
- 다른 분야이지만 도움이 되는 후보 비율
- 저장된
analogous_torelation의 회수율 - 정확한 오류 문자열과 고유 명칭 검색의 회귀 여부
- 같은 해결책이 반복되는 비율
- 근거와 적용 조건이 부족한 후보 비율
- p50, p95 지연 시간과 fallback 비율
평가용 query 원문에 비밀이나 개인 정보를 넣지 않습니다. 실제 사용 기록을 저장소에 그대로 커밋하지 않습니다. 새 generation은 품질 기준, 출력 제한과 장애 fallback 테스트를 모두 통과한 뒤 활성화합니다.
구현 순서
LessonSnapshot과 MediaWiki 일괄 조회를 추가합니다.- 현재 동작을 감싼
RetrievalBackend를 만들고 결과가 같은지 확인합니다. - 고정 벡터를 쓰는 in-memory fake와 domain-level 테스트를 추가합니다.
ProjectionBuilder, 교체 가능한EmbeddingProvider와 exact index를 추가합니다.- 의미 결과를 반환하지 않는 shadow mode에서 품질과 비용을 측정합니다.
find_analogies에 relation, lexical, semantic 결합을 적용합니다.- 평가가 끝나면
search_lessons에 hybrid 검색을 적용합니다. - 실제 지표가 전환 기준을 넘을 때만 sidecar 또는 공유 검색 서비스로 옮깁니다.
각 단계에서 기존 최대 20건, cursor, partial, scanned, scan_limit와 UTF-8 직렬화 상한을 유지합니다. 모델, 응답 또는 검색 정책이 바뀌면 MCP 모델, 예시와 문서를 함께 수정합니다.