포스트

SearcheRAGWithGraphRAG 시스템 구축계획서

SearcheRAGWithGraphRAG 시스템 구축계획서

문서 버전: v0.5 작성일: 2026-05-15 변경 이력:

  • v0.1 (2026-05-14): 초안
  • v0.2 (2026-05-14): Sparse Vector 검토, LLM 비교, 도메인 확정, 수치 추출, 파서 다양화, 하드웨어 제약
  • v0.3 (2026-05-14): Neo4j 중심 아키텍처, ETL 병목 분석, 실시간 ETL, ES Sparse 심화, 도메인 MCP, API 전용 정책
  • v0.4 (2026-05-14): 벡터 저장소 역할 재정의(Dense=ES 전담, Neo4j=Graph 전담), RRF 심화 설계, MCP 역할 명확화(DeepSeek V4 Pro + LangChain MCP Adapter 오케스트레이터), 기술 스택 버전 명세, IT SI/SM 온톨로지, 기술결정 로그(부록 D), 미결 사항 M-1~M-7 전면 교체(HRKP hybrid-rag-knowledge-ops 실 구현값 반영), H-5 임베딩 전략 BGE-M3 로컬 단독 확정
  • v0.5 (2026-05-15): [수정] v0.4 내부 모순 일괄 정리 — (1) 1.2절 요약표 MCP 행을 “DeepSeek + LangChain MCP Adapter”로 정정, (2) 4.2 아키텍처 다이어그램에서 “Claude (MCP)” 클라이언트 제거 및 Reranker “도입 확정” 반영, (3) Jina v3 API 잔존 표기 6곳 일괄 제거(BGE-M3 로컬 단독으로 통일 — H-5와 정합), (4) DeepSeek 모델명 “V4 Pro”로 통일(V3 표기 일괄 정정 — H-2와 정합), (5) 6장 RRF 가중치/후보 수를 HRKP 실 구현값(Graph weight=0.8, graph_search_top_k=3)으로 정정(M-6/M-7과 정합), (6) 부록 D TD-04 v0.4 변경 반영하여 재작성, TD-03 모델명 정정, TD-06 신설(MCP 오케스트레이터 LLM 결정), (7) Step 2 로드맵 “DeepSeek V3/Claude 연동” 정정, (8) M-1 SemanticChunker · M-4 Gleaning 2-pass 본문 반영, (9) 9.4 프로젝트 구조 api_embedding.py 주석을 H-5와 일치하도록 수정

핵심 참고 자료:


목차

  1. 프로젝트 개요 및 목적
  2. 도메인 정의 및 분석
  3. 하드웨어 제약 및 운영 전략
  4. 목표 아키텍처 (세미나(7) 정합 확정)
  5. Triple-Store 설계 (역할 확정)
  6. RRF 심화 설계 — Hybrid RAG의 핵심
  7. ES Sparse Vector 기술 검토
  8. ETL 파이프라인 설계
  9. 도메인 MCP 아키텍처 (DeepSeek + LangChain MCP 오케스트레이터)
  10. LLM 선택 전략 (API 전용 정책)
  11. PostgreSQL 스키마 설계
  12. 온톨로지 설계
  13. 레거시 자산 계승 전략
  14. 기술 스택 확정
  15. 단계별 구현 로드맵
  16. 미결 사항
  17. 부록

1. 프로젝트 개요 및 목적

1.1 핵심 목적

목적 1 — GraphRAG 성능 기여도 측정

“Neo4j 기반 GraphRAG가 기존 BM25 + Dense Vector 검색 대비 RAGAS 지표를 얼마나 향상시키는가?”

GraphRAG 없는 Baseline(Dense + BM25)과 GraphRAG 추가 후를 정량 비교한다. Sparse Vector는 별도 실험 변수로 분리하여 기여도를 독립 측정한다.

목적 2 — 수치 데이터 한계 극복

“기존 RAG가 답하지 못하는 수치 기반 질의(범위, 정렬, 집계)를 PostgreSQL SQL로 처리한다.”

1.2 v0.4 → v0.5 핵심 변경 요약

항목v0.3 (수정 전)v0.4 / v0.5 (수정 후)수정 근거
Dense Vector 저장소Neo4j 벡터 인덱스 (중심)ES HNSW 단독세미나(7) 아키텍처 정합
Neo4j 역할Semantic Hub (벡터 포함)Knowledge Graph 전담세미나(7) 역할 분리
벡터 중복 저장ES + Neo4j 양쪽 언급ES 단독 (중복 없음)불필요한 동기화 제거
RRF 설계언급만, 미구현완전한 RRF 설계 신설RAGChatbotServer 분석
MCP 오케스트레이터모호 (Claude/DeepSeek 혼재)DeepSeek V4 Pro + LangChain MCP Adapter단일 LLM로 단순화
임베딩 전략BGE-M3 + Jina v3 이중화BGE-M3 로컬 단독HRKP 실 구현 검증 (H-5)
RRF 가중치임의값 (Graph=1.5 등)HRKP 실 운용값 (Graph=0.8, top_k=3)HRKP 튜닝 결과 (M-6, M-7)

v0.5의 성격: v0.5는 신규 의사결정이 없는 정합성 정리 릴리스다. v0.4에서 결정된 사항이 본문 곳곳에 일관되게 반영되지 않은 부분을 일괄 정리했다.


2. 도메인 정의 및 분석

2.1 1차 도메인: 조명 제품 카탈로그 (확정)

OSRAM XBO 10000W/HS OFR 샘플 PDF 분석 결과, 산업/전문 조명 제품 카탈로그로 확정.

도메인별 질의 유형과 처리 주체:

질의 예시처리 주체
“광속 300,000lm 이상, 전력 5kW 미만 제품”PostgreSQL (수치 범위 쿼리)
“XBO 시리즈 중 영화 조명 용도 제품들”Neo4j Cypher (그래프 탐색)
“고휘도 크세논 램프와 유사한 제품”ES Dense Vector (의미 검색)
“XBO 10000W OFR 제품 코드 매칭”ES BM25 (정확 키워드)
“XBO와 유사하면서 대체 가능한 제품”RRF (ES + Neo4j 통합)

2.2 2차 도메인: IT SI/SM 산출물

서버 스펙시트, 네트워크 장비 사양, 운영 매뉴얼 등 수치 데이터 풍부. 온톨로지는 12절 참조.


3. 하드웨어 제약 및 운영 전략

3.1 개발 환경

항목사양
OSWindows (노트북)
CPUCPU Only (No GPU)
RAM16GB

3.2 메모리 예산

1
2
3
4
5
6
7
8
9
10
11
상시 서비스 동작:
  Elasticsearch:  1.5 GB (heap 512m 제한)
  Neo4j:          1.0 GB (heap_max 512m + pagecache 256m + 기타)
  PostgreSQL:     0.3 GB
  Redis:          0.1 GB
  FastAPI:        0.3 GB
  BGE-M3 모델:    1.2 GB
  합계:           ~4.4 GB ✅ 여유 11.6GB

개발/테스트 로컬 LLM 추가:
  Qwen3 8B Q4:   +5.0 GB → 총 ~9.4 GB ✅ 안전

3.3 임베딩 운용 전략 (BGE-M3 로컬 단독)

로컬 BGE-M3 CPU 추론 속도:

모드처리량10,000 청크 기준비고
Dense only (배치 4)~286 chunks/min~35분실시간 처리 가능
Dense + Sparse (배치 4)~179 chunks/min~56분Step 3 이후
배치 처리 (HRKP 실측)7.5 texts/sec약 22분Redis TTL=7일 캐시 적용

운용 정책 (H-5 확정):

  • 모든 임베딩은 로컬 BGE-M3(BAAI/bge-m3, 1024차원) 단독 사용
  • 중복 임베딩 방지를 위한 Redis 캐시(TTL=7일) 적용 — HRKP 실 운용 검증
  • 대량 인덱싱은 Redis Queue + RQ Worker로 야간/비동기 배치 처리
  • Sparse 임베딩(Step 3)도 동일 BGE-M3 모델에서 동시 생성 (return_sparse=True)
  • Jina/OpenAI 등 외부 임베딩 API 미사용 — 추가 API 비용 없이 완전 로컬 운용

v0.4 변경 사항: v0.3까지의 “Jina v3 API 대량 배치 처리” 정책은 폐기되었다. HRKP 실 운용 결과 BGE-M3 단독으로 운영 요건을 충족함이 검증되었기 때문이다(부록 D TD-05 참조).

중국 모델 API 전용 정책: DeepSeek, Qwen, GLM 등 중국 모델은 모두 API로만 사용. 로컬 실행 시도 금지 (메모리/속도 한계).


4. 목표 아키텍처 (세미나(7) 정합 확정)

4.1 핵심 설계 원칙 — 세미나(7) 정합

세미나(7)의 명시적 아키텍처:

  • ES: Dense Vector + Sparse Vector + BM25 (Nori) — 모든 벡터 및 텍스트 검색
  • Neo4j: Knowledge Graph (엔터티, 관계, 멀티홉 Cypher) — 그래프 탐색 전담
  • PostgreSQL: SSOT + 수치 데이터
  • RRF: ES 결과 + Neo4j 결과 통합

v0.3의 오류 수정: v0.3에서 “Neo4j를 Semantic Hub로, Dense Vector를 Neo4j에 저장”으로 설계했으나, 세미나(7) 아키텍처 및 실제 운영 고려 사항을 검토한 결과 Dense Vector는 ES에 통합 관리하는 것이 올바름. 이유:

검토 항목ES DenseNeo4j Vector Index
세미나(7) 설계✅ ES에 Dense❌ Neo4j에 Dense 없음
BM25 + Dense 통합 쿼리✅ 하나의 ES 쿼리❌ 별도 쿼리 필요
Sparse + Dense 통합✅ ES sparse_vector❌ 미지원
한국어 Nori✅ ES 내장❌ 미지원
Graph + Vector 단일 Cypher❌ 불가✅ 가능 (단, 쿼리 복잡)
운영 복잡도 (중복 동기화)✅ 단순 (ES만)❌ 복잡 (양쪽 관리)
검색 성능 (순수 ANN)✅ 우수△ 중간

결론: Dense Vector는 ES 단독 저장. Neo4j는 Knowledge Graph(엔터티/관계/Cypher) 전담. 두 저장소에 동일 벡터를 중복 저장하지 않는다.

4.2 전체 시스템 구성 (세미나(7) 정합)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
┌─────────────────────────────────────────────────────────────────────┐
│                  SearcheRAGWithGraphRAG 목표 아키텍처                 │
│                  (세미나(7) 아키텍처 정합)                             │
├─────────────────────────────────────────────────────────────────────┤
│  [Client Layer]                                                     │
│    Web UI / REST API Client                                         │
│              ↓ (JWT 인증)                                           │
│                                                                     │
│  [Application Layer — FastAPI :8000]                                │
│    ┌────────────────────────────────────────────────────────────┐  │
│    │  LangChain Agent (DeepSeek V4 Pro + MCP Adapter)           │  │
│    │   └─ 복합 도구 조합이 필요한 질의에 대해 MCP 도구 오케스트레이션   │  │
│    │                                                            │  │
│    │               4-Way Hybrid RAG Engine                      │  │
│    │                                                            │  │
│    │  ① ES BM25 (Nori 한국어)  → Elasticsearch                  │  │
│    │  ② ES Dense Vector (BGE-M3, 1024d, HNSW) → Elasticsearch  │  │
│    │  ③ ES Sparse Vector (BGE-M3) → Elasticsearch  [Step 3]    │  │
│    │  ④ Neo4j Graph (Cypher 멀티홉) → Neo4j                     │  │
│    │                           ↓                               │  │
│    │            RRF (Reciprocal Rank Fusion)                   │  │
│    │            각 소스 독립 실행 → 순위 기반 통합 (상위 50건)   │  │
│    │                           ↓                               │  │
│    │            BGE-Reranker-v2-m3 (Step 2부터 도입 확정)        │  │
│    │            50건 → top_k 재정렬                              │  │
│    │                           ↓                               │  │
│    │            LLM 응답 생성 (DeepSeek V4 Pro API)              │  │
│    └────────────────────────────────────────────────────────────┘  │
│    └── Numeric Query Engine → PostgreSQL (수치 범위/집계)            │
│                                                                     │
│  [Triple-Store]                                                     │
│    Elasticsearch :9200  — Dense + BM25 + Sparse (모든 벡터/텍스트)  │
│    Neo4j :7474/:7687    — Knowledge Graph (엔터티/관계)              │
│    PostgreSQL :5432     — SSOT + 수치 데이터                        │
│    Redis :6379          — 세션 + ETL 비동기 큐 + 임베딩 캐시          │
└─────────────────────────────────────────────────────────────────────┘

v0.5 정정 사항:

  • Client Layer에서 “Claude (MCP)” 항목 제거 — MCP 오케스트레이터는 DeepSeek V4 Pro(LangChain Agent 내부)로 결정되었으며, Claude는 사용자 단의 외부 클라이언트로 호출되지 않는다.
  • Reranker를 “선택적, Step 2+”에서 “도입 확정, Step 2부터“로 변경(M-2 결정 반영).

4.3 성능 측정 실험 설계

단계구성측정 목적
BaselineES Dense + BM25그래프 없는 기준선
+ GraphRAGDense + BM25 + Neo4j GraphGraphRAG 기여도 (핵심)
+ SparseDense + BM25 + Graph + SparseSparse 추가 효과

평가: RAGAS (Faithfulness, Answer Relevancy, Context Recall, Context Precision)


5. Triple-Store 설계 (역할 확정)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Elasticsearch (검색 엔진 — 벡터/텍스트 전담)
  ├── ① Dense Vector 인덱스 (BGE-M3, 1024차원, HNSW) ← 세미나(7) 확정
  ├── ② BM25 전문 검색 인덱스 (Nori 한국어 형태소)
  └── ③ Sparse Vector 인덱스 (BGE-M3 lexical_weights) [Step 3]

Neo4j (Knowledge Graph 전담 — 벡터 저장 없음)
  ├── 엔터티 노드 (Product, Category, Application, Vendor…)
  ├── 관계 엣지 (BELONGS_TO, SUITABLE_FOR, SIMILAR_TO…)
  └── 렉시컬 그래프 (Document → Chunk, Chunk -MENTIONS-> Entity)

PostgreSQL (SSOT + 수치 전담)
  ├── 문서 처리 상태 추적 (ETL 파이프라인 기준점)
  ├── 제품 마스터 데이터
  └── 수치 속성 (전력/광속/크기 — 범위/집계/정렬)

6. RRF 심화 설계 — Hybrid RAG의 핵심

6.1 RAGChatbotServer 분석 — 기존 방식의 문제점

RAGChatbotServer를 분석한 결과, RRF가 전혀 구현되어 있지 않다. 대신 단순 가중 점수 평균(Weighted Score Fusion)을 사용한다.

1
2
3
# RAGChatbotServer hybrid_search() — 실제 구현 (vector_store_elasticsearch.py L677)
final_score = (alpha * scores["vector_score"]) + ((1 - alpha) * scores["bm25_score"])
# alpha = 0.6 고정 (벡터 60%, BM25 40%)

이 방식의 문제점:

문제설명
스코어 스케일 불일치벡터 유사도(0~1)와 BM25 스코어(0~N) 단위가 달라 단순 합산이 의미 없음
alpha 튜닝 필요도메인/질의 유형마다 최적 alpha가 다름, 고정값 0.6은 임의적
Neo4j 통합 불가가중 합산 방식으로는 3번째 소스(그래프) 추가 어려움
RRF가 아님RAGChatbotServer의 hybrid는 ES 내부 2개 소스만 결합, 진정한 Hybrid RAG가 아님

6.2 RRF 알고리즘 원리

RRF(Reciprocal Rank Fusion, Cormack et al. 2009)는 서로 다른 검색 방식의 결과를 순위(rank) 기반으로 통합하는 알고리즘이다. 점수 체계가 달라도 편향 없이 결합할 수 있다는 것이 핵심이다.

1
2
3
4
5
6
7
RRF score(d) = Σ_i  weight_i / (k + rank_i(d))

- d: 문서(청크)
- i: 검색 소스 (BM25, Dense, Graph, Sparse…)
- k: 상수 (기본 60 — 상위 결과에 집중, 낮을수록 top-1 가중 증가)
- rank_i(d): 소스 i에서 d의 순위 (1-based)
- weight_i: 소스 i의 가중치

RRF 작동 예시 (k=60, 단순화를 위해 weight 모두 1.0):

문서BM25 순위Dense 순위Graph 순위RRF 점수
문서 A1위3위2위1/61 + 1/63 + 1/62 = 0.0484
문서 B5위1위-1/65 + 1/61 + 0 = 0.0320
문서 C-2위1위0 + 1/62 + 1/61 = 0.0326

→ 여러 소스에서 공통으로 높은 순위를 차지한 문서 A가 최종 1위.

6.3 우리 시스템 RRF 구현 설계

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from collections import defaultdict
from typing import List, Tuple, Optional
from langchain.schema import Document

def reciprocal_rank_fusion(
    result_lists: List[List[Tuple[str, Document]]],
    k: int = 60,
    weights: Optional[List[float]] = None
) -> List[Tuple[str, float, Document]]:
    """
    n-Way RRF: 여러 검색 소스의 순위 리스트를 RRF로 통합

    Args:
        result_lists: [[(doc_id, Document), ...], ...] 형태의 소스별 순위 리스트
                      각 리스트 내 순서가 곧 해당 소스의 순위(rank)
        k: RRF 상수 (기본 60, Cormack et al. 표준값)
        weights: 소스별 가중치 (None이면 모두 1.0)

    Returns:
        [(doc_id, rrf_score, Document), ...] RRF 점수 내림차순 정렬

    Note:
        동일 doc_id가 여러 소스에서 등장할 경우, 첫 번째로 등장한 소스의
        Document 객체를 유지한다(메타데이터 보존). 점수는 모든 소스의 기여를 누적한다.
    """
    if weights is None:
        weights = [1.0] * len(result_lists)

    scores = defaultdict(lambda: {"score": 0.0, "doc": None})

    for result_list, weight in zip(result_lists, weights):
        for rank, (doc_id, doc) in enumerate(result_list, start=1):
            scores[doc_id]["score"] += weight / (k + rank)
            if scores[doc_id]["doc"] is None:
                scores[doc_id]["doc"] = doc

    return sorted(
        [(doc_id, info["score"], info["doc"])
         for doc_id, info in scores.items()],
        key=lambda x: x[1],
        reverse=True
    )


class HybridGraphRAG:
    """4-Way Hybrid RAG 엔진 (RRF + Reranker 통합)"""

    # HRKP 실 운용값 (M-6, M-7 결정 사항)
    DEFAULT_WEIGHTS = {
        "bm25":   1.0,   # 키워드 검색 기본 가중치
        "dense":  1.0,   # Dense Vector 기본 가중치
        "graph":  0.8,   # 그래프 결과는 약간 보조 가중 (1.5 → 0.8 조정)
        "sparse": 0.7,   # Sparse는 Dense 보조 역할
    }

    def __init__(self, es_manager, neo4j_manager, pg_manager,
                 llm_client, embedding_client, reranker,
                 k_rrf: int = 60, top_k: int = 10,
                 graph_search_top_k: int = 3,   # M-7: Graph 소스 후보 수 제한
                 rrf_pool_size: int = 50):       # Reranker 입력 풀 크기 (M-2)
        self.es = es_manager
        self.neo4j = neo4j_manager
        self.pg = pg_manager
        self.llm = llm_client
        self.embed = embedding_client
        self.reranker = reranker
        self.k_rrf = k_rrf
        self.top_k = top_k
        self.graph_search_top_k = graph_search_top_k
        self.rrf_pool_size = rrf_pool_size

    async def search(self, query: str, mode: str = "full") -> List[Document]:
        """
        mode:
          - "baseline": ES BM25 + Dense (Step 1)
          - "graph":    + Neo4j Graph (Step 2)
          - "full":     + Sparse (Step 3)
        """
        query_vector = await self.embed.embed_query(query)

        # ① ES BM25 검색 — 후보 top_k×2 = 20건
        bm25_results = await self.es.bm25_search(query, k=self.top_k * 2)

        # ② ES Dense Vector 검색 — 후보 top_k×2 = 20건
        dense_results = await self.es.dense_search(query_vector, k=self.top_k * 2)

        result_lists = [bm25_results, dense_results]
        weights = [self.DEFAULT_WEIGHTS["bm25"], self.DEFAULT_WEIGHTS["dense"]]

        # ③ Neo4j Graph 검색 — Graph 후보 수는 graph_search_top_k(3)로 고정 (M-7)
        if mode in ("graph", "full"):
            graph_results = await self.neo4j.graph_search(
                query, k=self.graph_search_top_k
            )
            result_lists.append(graph_results)
            weights.append(self.DEFAULT_WEIGHTS["graph"])

        # ④ ES Sparse Vector 검색 — 후보 top_k×2 = 20건 (mode=full)
        if mode == "full":
            sparse_query = await self.embed.generate_sparse(query)
            sparse_results = await self.es.sparse_search(
                sparse_query, k=self.top_k * 2
            )
            result_lists.append(sparse_results)
            weights.append(self.DEFAULT_WEIGHTS["sparse"])

        # RRF 통합 → 상위 rrf_pool_size(50)건을 Reranker 입력으로
        fused = reciprocal_rank_fusion(result_lists, k=self.k_rrf, weights=weights)
        rrf_pool = [doc for _, _, doc in fused[:self.rrf_pool_size]]

        # Reranker 적용 (BGE-Reranker-v2-m3, M-2)
        reranked = await self.reranker.rerank(query, rrf_pool, top_k=self.top_k)
        return reranked

6.4 Neo4j 그래프 검색 결과의 RRF 통합

Neo4j는 점수 기반이 아닌 관계 탐색 기반으로 결과를 반환한다. 이를 RRF의 (doc_id, Document) 형식으로 변환한다. Graph 후보 수는 3건으로 고정(M-7)하여 Graph 결과가 RRF 결과를 과도하게 점유하는 현상을 방지한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
async def graph_search(self, query: str, k: int = 3) -> List[Tuple[str, Document]]:
    """Neo4j 그래프 탐색 → RRF 입력 형식 변환

    Note:
        k(graph_search_top_k)는 기본 3으로 제한된다.
        HRKP 튜닝(34_graph_search_rrf_tuning.md, 2026-03-09 ADR)에서
        Graph 후보 수를 늘리면 RRF top-10 중 Graph 결과가 8건을 점유하는
        편향이 관찰됐기 때문이다. weight 조정 대신 후보 수 제한으로 해결.
    """
    cypher = """
    CALL db.index.fulltext.queryNodes("entity_search", $query)
    YIELD node AS entity, score
    MATCH (chunk:Chunk)-[:MENTIONS]->(entity)
    WITH chunk, entity, score
    ORDER BY score DESC
    LIMIT $k
    RETURN chunk.id AS doc_id, chunk.text AS text, chunk.metadata AS meta
    """
    records = await self.neo4j_session.run(cypher, query=query, k=k)

    results = []
    for record in records:
        doc = Document(
            page_content=record["text"],
            metadata={**record["meta"], "source": "neo4j_graph"}
        )
        results.append((record["doc_id"], doc))

    return results  # 이미 score 기준 정렬 → index가 RRF의 rank

6.5 RRF vs RAGChatbotServer 방식 비교

항목RAGChatbotServer (가중 평균)우리 시스템 (RRF)
알고리즘alpha * v_score + (1-alpha) * bm25_scoreΣ weight / (k + rank)
점수 정규화❌ 필요 (단위 불일치)✅ 불필요 (순위 기반)
소스 수ES 내부 2개ES + Neo4j 3~4개
파라미터alpha (튜닝 민감)k (robust, 60 표준값)
Neo4j 통합❌ 불가✅ 가능
설계 견고성낮음 (alpha 임의적)높음 (수학적 근거)
레거시 계승부분 (가중치 개념)구조적으로 상위 호환

6.6 RRF 파라미터 (HRKP 실 운용값 — M-6, M-7)

파라미터결정 근거
k_rrf60Cormack et al. 2009 표준값, HRKP 검증 완료
BM25 weight1.0기본값
Dense weight1.0기본값
Graph weight0.8HRKP 튜닝 결과. Graph 결과 편향 방지를 위해 약간 보조 가중
Sparse weight0.7Dense 대비 보조 역할
BM25/Dense/Sparse 후보 수top_k × 2 = 20건충분한 후보 확보
Graph 후보 수 (graph_search_top_k)3Graph 편향 방지 (weight 조정보다 후보 수 제한이 효과적)
RRF → Reranker 풀 크기50상위 50건을 Reranker에 입력
최종 top_k10RAGAS 실험으로 추가 최적화

이전 v0.4 문서에 표기되었던 “Graph weight=1.5, top_k×2 후보” 설정은 HRKP에서 실제로 운용한 결과 Graph 결과 독식 문제를 일으켰다. v0.5는 HRKP 결정값을 본문 코드와 표 양쪽에 반영한다.


7. ES Sparse Vector 기술 검토

7.1 ELSER vs BGE-M3 Sparse

항목ELSER v2BGE-M3 Sparse
한국어 지원❌ 영어 전용✅ 100+ 언어
ES 통합네이티브 (ML node)외부 처리 → ES 적재
한국어 품질❌ 매우 낮음✅ 높음
CPU 추론ES 내부 (GPU 권장)Python CPU 가능
라이선스ES Basic 8.9+ 사용 가능Apache 2.0

결론: 한국어 환경에서 ELSER는 실용 불가. BGE-M3 Sparse만이 선택지.

7.2 BGE-M3 Sparse → ES 변환 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from FlagEmbedding import BGEM3FlagModel
from transformers import AutoTokenizer

model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=False)  # CPU: fp16 비활성화
tokenizer = AutoTokenizer.from_pretrained('BAAI/bge-m3')

def generate_sparse_vector(text: str, threshold: float = 0.01) -> dict:
    """
    BGE-M3 lexical_weights → ES sparse_vector 형식 변환
    핵심: token_id(int) → token_string 변환 필수
    """
    output = model.encode([text], return_dense=False, return_sparse=True)
    lexical_weights = output['lexical_weights'][0]  # {token_id: weight}

    token_ids = list(lexical_weights.keys())
    token_strings = tokenizer.convert_ids_to_tokens(token_ids)

    return {
        token_str: float(weight)
        for token_str, (_, weight) in zip(
            token_strings, lexical_weights.items()
        )
        if weight > threshold
        and token_str not in ['[CLS]', '[SEP]', '[PAD]', '[UNK]']
    }

7.3 최종 전략

1
2
3
4
5
Step 1: Dense + BM25 (ES) → RAGAS Baseline
Step 2: + Neo4j Graph + Reranker → GraphRAG 기여도 측정
Step 3: + BGE-M3 Sparse         → Sparse 추가 효과 측정
  - Sparse는 배치 야간 처리 (CPU 오버헤드)
  - ELSER 배제 (한국어 미지원)

8. ETL 파이프라인 설계

8.1 전체 ETL 흐름 (실시간 + 배치)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
문서 수신 (REST API 업로드 or 파일 감시)
  ↓
PostgreSQL SSOT 등록 (status: 'pending')
  ↓
Redis Queue 비동기 적재 (RQ Worker)
  ↓
청킹: SemanticChunker (BGE-M3 임베딩 기반, target=1024 tokens, overlap=128 tokens) [M-1]
  - 임베딩 유사도로 의미 경계 감지
  - 허용 범위 256~2048 tokens (벗어나면 재분할)
  ↓ 병렬 처리
  ├── Phase 2A: Dense 임베딩 → ES HNSW 적재
  │     BGE-M3 로컬 CPU 단독 운용
  │     Redis 캐시(TTL=7일) — 동일 텍스트 재임베딩 방지
  │     → PostgreSQL es_sync_log status 'synced'
  │
  ├── Phase 2B: 엔터티 추출 → Neo4j 적재 [M-4]
  │     DeepSeek V4 Pro API → 엔터티/관계 JSON
  │     Gleaning 2-pass 적용 (1-pass 대비 Recall +33%)
  │     신뢰도 임계값 ≥ 0.7로 필터링
  │     엔터티 타입(조명): Product, Series, Category, Application,
  │                       Technology, Vendor, Certification
  │     엔터티 타입(IT SI/SM): Person, Organization, Technology, Project,
  │                          Concept, Date, Location 등
  │     Neo4j MERGE (중복 방지) → Chunk -MENTIONS-> Entity 연결
  │     → PostgreSQL neo4j_sync_log status 'synced'
  │
  └── Phase 3: 수치 추출 → PostgreSQL
        pdfplumber → 표 파싱
        ProductSpecExtractor 규칙 기반 매핑
        미매핑 → spec_json JSONB
        → products + product_specs 적재

  [Step 3 이후, 배치/야간]
  Phase 4: BGE-M3 Sparse 생성 → ES sparse_vector 적재

v0.5 반영: M-1(SemanticChunker)과 M-4(Gleaning 2-pass + 신뢰도 임계값) 결정이 본문 파이프라인 흐름에 명시되도록 보강했다.

8.2 문서 파서 전략

1
2
3
4
5
6
7
8
9
10
11
class PDFParserFactory:
    @staticmethod
    def get_parser(doc_type: str, file_path: str):
        if doc_type == "product_catalog":
            return PdfplumberParser(file_path)   # 표 추출 강점 (기본값)
        elif doc_type == "manual":
            return PyMuPDFParser(file_path)       # 긴 텍스트
        elif doc_type == "hwp":
            return DoclingParser(file_path)       # HWP 전용
        else:
            return PdfplumberParser(file_path)
파서강점권장 사용
pdfplumber테이블 추출 최강, 레거시 계승제품 카탈로그 (기본값)
PyMuPDF빠른 텍스트, 좌표 정보매뉴얼, 긴 텍스트
Unstructured다형식 (HTML, DOCX)혼합 환경
Docling 2.x복잡 레이아웃, HWPHWP 파일만

8.3 수치 데이터 추출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class ProductSpecExtractor:
    FIELD_MAP = {
        "nominal wattage":       ("wattage_w",         "float"),
        "nominal voltage":       ("voltage_v",          "float"),
        "nominal luminous flux": ("luminous_flux_lm",  "float"),
        "luminance":             ("luminance_cd_cm2",   "float"),
        "color temperature":     ("color_temp_k",       "int"),
        "length":                ("height_mm",          "float"),
        "diameter":              ("width_mm",           "float"),
        "product weight":        ("weight_g",           "float"),
        "nominal lifetime":      ("lifetime_hr",        "float"),
    }

    def extract(self, tables: list) -> dict:
        typed_fields, json_fields = {}, {}
        for table in tables:
            for row in table:
                if len(row) < 2: continue
                key = str(row[0]).lower().strip()
                val = str(row[1]).strip()
                matched = False
                for pattern, (col, dtype) in self.FIELD_MAP.items():
                    if pattern in key:
                        typed_fields[col] = self._parse_value(val, dtype)
                        matched = True
                        break
                if not matched:
                    json_fields[key] = val   # → JSONB
        return {"typed": typed_fields, "json": json_fields}

9. 도메인 MCP 아키텍처 (DeepSeek + LangChain MCP 오케스트레이터)

9.1 MCP 역할 명확화

MCP(Model Context Protocol)는 Anthropic이 제안한 오픈 스탠다드다. MCP 클라이언트 역할은 모델이 아니라, 모델을 감싸는 런타임/프레임워크가 구현한다.

MCP는 Cursor, VS Code, Claude Desktop 등 다양한 에이전트 런타임이 지원하는 개방형 프로토콜이다. 이 시스템에서는 langchain-mcp-adapters를 MCP 런타임으로 채택한다. DeepSeek V4 Pro API(OpenAI 호환)를 LangChain Agent의 LLM으로 연결하면, DeepSeek이 MCP 서버 도구를 직접 오케스트레이션한다.

구성 요소MCP와의 관계
DeepSeek V4 Pro (LLM)✅ MCP 오케스트레이터 — LangChain MCP Adapter 런타임 하에서 도구 호출을 판단
LangChain MCP Adapter✅ MCP 클라이언트 런타임 — MCP 서버와 통신, 도구 결과를 DeepSeek에 전달
FastAPI 메인 서버MCP와 무관 — LangChain Agent를 내부 모듈로 호출
도메인 MCP 서버MCP 서버 — DeepSeek(LangChain 경유)이 호출하는 도구 모음

결론: 도메인 MCP 서버(osram-product-mcp 등)는 DeepSeek이 LangChain MCP Adapter를 통해 제품 데이터에 접근하기 위한 인터페이스다. DeepSeek 하나가 엔터티 추출·MCP 오케스트레이션·최종 응답 생성을 모두 담당하며, 별도 오케스트레이터 LLM이 필요 없어 아키텍처가 단순해진다.

9.2 도입 배경 및 가치

OSRAM 같은 특정 제조사 데이터는:

  • 고유한 PDF 레이아웃과 필드 명칭 (OSRAM 전용 파싱 로직 필요)
  • 반복적 카탈로그 업데이트 시 전용 처리 로직이 유리
  • 모든 데이터는 반드시 Triple Store에 동기화 — MCP는 접근 레이어일 뿐

9.3 쿼리 처리 흐름

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
사용자 질의
    │
    ▼
FastAPI (api/routers/chat.py)
    │
    ▼
LangChain Agent
├── LLM: DeepSeek V4 Pro (OpenAI 호환 API)
└── Tools: langchain-mcp-adapters → MCP 서버 연결
    │
    ├── [도구 호출 필요 시] osram-product-mcp 서버
    │       ├── search_osram_products()   # ES/Neo4j/PG 하이브리드 검색
    │       ├── find_similar_products()   # Neo4j SIMILAR_TO 그래프 탐색
    │       └── numeric_filter_products() # PostgreSQL 수치 범위 쿼리
    │
    └── [단순 RAG] HybridGraphRAG 직접 호출 (MCP 미경유)
    │
    ▼
DeepSeek V4 Pro 최종 답변 생성
    │
    ▼
사용자 응답

판단 기준: MCP 도구 경유 vs. HybridGraphRAG 직접 호출

  • 단일 도메인 단순 질의 → HybridGraphRAG 직접 (빠름)
  • 복합 도구 조합 필요 (수치 필터 + 유사 제품 + 사양 비교 등) → LangChain Agent → MCP

9.4 프로젝트 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
SearcheRAGWithGraphRAG/
├── core/                             # 공통 인프라
│   ├── triple_store/
│   │   ├── neo4j_manager.py          # Neo4j Cypher + 노드/엣지 CRUD
│   │   ├── es_manager.py             # ES Dense + BM25 + Sparse
│   │   └── pg_manager.py             # SSOT + 수치 데이터
│   ├── embedding/
│   │   └── local_bge_m3.py           # 로컬 BGE-M3 (Dense + Sparse 단독)
│   ├── chunking/
│   │   └── semantic_chunker.py       # BGE-M3 임베딩 기반 의미 경계 청킹 (M-1)
│   ├── rerank/
│   │   └── bge_reranker.py           # BGE-Reranker-v2-m3 (M-2)
│   ├── rag/
│   │   ├── hybrid_graph_rag.py       # HybridGraphRAG + RRF + Reranker
│   │   └── rrf.py                    # reciprocal_rank_fusion()
│   └── agent/
│       └── mcp_agent.py              # LangChain Agent + MCP Adapter (DeepSeek 오케스트레이터)
│
├── mcp_servers/                      # MCP 서버 (도메인별 도구 모음)
│   ├── osram_product_mcp/
│   │   ├── server.py                 # FastMCP 서버
│   │   ├── extractor.py              # OSRAM PDF 특화 파서
│   │   └── tools/
│   │       ├── ingest_catalog.py     # 카탈로그 수집 → Triple Store
│   │       ├── search_products.py    # RRF 하이브리드 검색
│   │       ├── find_similar.py       # Neo4j SIMILAR_TO
│   │       └── numeric_filter.py     # PostgreSQL 수치 쿼리
│   └── it_server_mcp/                # IT SI/SM 도메인 (2차)
│
└── api/                              # FastAPI
    ├── app.py
    └── routers/
        ├── chat.py                   # RAG 질의응답 (Agent / HybridRAG 분기)
        ├── documents.py              # 문서 관리 + 실시간 ETL
        └── eval.py                   # RAGAS 평가

v0.5 정정 사항: core/embedding/ 하위 api_embedding.py(Jina/OpenAI API 어댑터) 제거. BGE-M3 로컬 단독으로 통일(H-5). 신규 core/chunking/(M-1), core/rerank/(M-2) 디렉터리 명시.

9.5 LangChain MCP Agent 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# core/agent/mcp_agent.py
# 의존 패키지: langchain-mcp-adapters>=0.2.2, langgraph>=1.0, langchain-openai>=0.3
from langchain_openai import ChatOpenAI
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.prebuilt import create_react_agent

# DeepSeek V4 Pro — OpenAI 호환 엔드포인트
llm = ChatOpenAI(
    model="deepseek-v4-pro",
    base_url="https://api.deepseek.com/v1",
    api_key="<DEEPSEEK_API_KEY>",
)

# MCP 서버 연결 (stdio 또는 SSE 방식)
async def get_agent():
    client = MultiServerMCPClient({
        "osram-product-mcp": {
            "command": "python",
            "args": ["mcp_servers/osram_product_mcp/server.py"],
            "transport": "stdio",
        }
    })
    tools = await client.get_tools()
    return create_react_agent(llm, tools)

9.6 FastMCP 서버 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# mcp_servers/osram_product_mcp/server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("osram-product-mcp")

async def search_osram_products(
    query: str,
    wattage_min: float = None,
    wattage_max: float = None,
    application: str = None,
    top_k: int = 10
) -> list[dict]:
    """OSRAM 제품 Hybrid 검색 (ES BM25/Dense + Neo4j Graph + PG 수치 필터)"""
    ...

async def numeric_filter_products(
    luminous_flux_min: float = None,
    wattage_max: float = None,
    sort_by: str = "luminous_flux_lm"
) -> list[dict]:
    """PostgreSQL 수치 범위 쿼리 전용 — '광속 300k~600k lm'"""
    ...

10. LLM 선택 전략 (API 전용 정책)

10.1 API 전용 정책 (확정)

모델 계열정책
중국 모델 (DeepSeek, Qwen, GLM)API 전용 — 로컬 실행 금지
임베딩 (모든 용도)로컬 BGE-M3 단독 (H-5)
개발 테스트 전용Qwen3 8B Q4 로컬 (기능 확인만)

v0.5 정정 사항: 이전 표에 있던 “임베딩 대규모 → Jina v3 API” 행을 제거하고 H-5(BGE-M3 단독) 정책으로 통일.

10.2 용도별 LLM

용도모델방식
엔터티 추출 (배치 ETL)DeepSeek V4 Pro APIAPI (Gleaning 2-pass, M-4)
MCP 오케스트레이션 + RAG 응답 생성DeepSeek V4 Pro APIAPI (LangChain Agent 경유)
단순 RAG 응답 생성DeepSeek V4 Pro APIAPI (HybridGraphRAG 직접)
개발/기능 테스트Qwen3 8B Q4 (Ollama)로컬 (테스트 전용)

10.3 비용 추정

엔터티 추출 (DeepSeek V4 Pro 기준 단가 — 실제 청구액은 API 가격 정책 변동에 따라 조정):

  • 100 문서 (500p) Gleaning 2-pass: 1-pass 대비 약 2배 토큰 소비
  • 500 문서 (2,500p)도 동일 배율
  • 정확한 단가는 deepseek.com 가격표를 따르되, DeepSeek 계열은 입력/출력 모두 동급 API 대비 저렴 수준 유지 가정

임베딩 비용:

  • 0원 (BGE-M3 로컬 단독, 외부 API 미사용 — H-5)
  • 단, 노트북 CPU 시간 비용은 별도 — Redis 캐시(TTL=7일)로 중복 임베딩 방지

v0.5 정정 사항: v0.4의 “DeepSeek V3” 단가 표기와 “Jina v3 API” 임베딩 비용 항목을 모두 정정. 단가는 V4 Pro로 통일하고, 정확한 수치는 운영 시 실측치로 갱신.


11. PostgreSQL 스키마 설계

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
CREATE TABLE documents (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    file_name    VARCHAR(500) NOT NULL,
    file_path    TEXT,
    doc_type     VARCHAR(50),       -- 'product_datasheet' | 'manual' | 'si_spec'
    domain       VARCHAR(50),       -- 'lighting' | 'it_server'
    vendor       VARCHAR(100),      -- 'osram' | 'cisco'
    mcp_source   VARCHAR(100),      -- 수집 MCP 서버명
    created_at   TIMESTAMPTZ DEFAULT NOW(),
    updated_at   TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE products (
    id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    product_code   VARCHAR(200) UNIQUE NOT NULL,
    name           VARCHAR(500) NOT NULL,
    category_l1    VARCHAR(100),
    category_l2    VARCHAR(100),
    domain         VARCHAR(50),
    vendor         VARCHAR(100),
    document_id    UUID REFERENCES documents(id),
    created_at     TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE product_specs (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    product_id      UUID REFERENCES products(id) ON DELETE CASCADE,
    -- [공통 수치]
    wattage_w       NUMERIC(12,3),
    voltage_v       NUMERIC(10,3),
    weight_g        NUMERIC(10,2),
    width_mm        NUMERIC(10,2),
    height_mm       NUMERIC(10,2),
    depth_mm        NUMERIC(10,2),
    -- [조명 도메인 타입 컬럼]
    luminous_flux_lm NUMERIC(12,2),
    luminance_cd_cm2 NUMERIC(12,2),
    color_temp_k     INTEGER,
    cri              NUMERIC(5,2),
    lifetime_hr      NUMERIC(10,1),
    -- [도메인 확장 — JSONB]
    spec_json       JSONB,
    -- 조명: {"current_a":195, "electrode_gap_mm":13.5, "burning_position":"s15"}
    -- IT 서버: {"cpu_cores":32, "ram_gb":256, "rack_unit":2, "bandwidth_gbps":10}
    UNIQUE(product_id)
);

CREATE TABLE es_sync_log (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    document_id  UUID REFERENCES documents(id),
    es_index     VARCHAR(200),
    es_doc_ids   TEXT[],
    chunk_count  INTEGER,
    synced_at    TIMESTAMPTZ DEFAULT NOW(),
    status       VARCHAR(20) DEFAULT 'pending'
);

CREATE TABLE neo4j_sync_log (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    document_id  UUID REFERENCES documents(id),
    node_count   INTEGER,
    edge_count   INTEGER,
    synced_at    TIMESTAMPTZ DEFAULT NOW(),
    status       VARCHAR(20) DEFAULT 'pending'
);

-- 인덱스
CREATE INDEX idx_specs_flux       ON product_specs(luminous_flux_lm);
CREATE INDEX idx_specs_watt       ON product_specs(wattage_w);
CREATE INDEX idx_specs_ctemp      ON product_specs(color_temp_k);
CREATE INDEX idx_products_vendor  ON products(vendor);
CREATE INDEX idx_specs_json       ON product_specs USING GIN(spec_json);

12. 온톨로지 설계

12.1 조명 제품 도메인

엔터티 유형

유형설명예시
Product개별 제품 (노드의 핵심)XBO 10000 W/HS OFR
Series제품 시리즈XBO 시리즈
Category제품 분류 계층Xenon Lamps > Traditional
Application적용 용도Film Projection, Searchlights
Technology핵심 기술eXtreme Life (XL), DC Operation
Vendor제조사OSRAM, Philips, LEDVANCE
Certification규격/인증REACh, RoHS, WEEE, IEC
Chunk문서 청크 (렉시컬 그래프용)
Document원본 문서ZMP_55851 데이터시트

참고: BurningPosition(수평/수직 점등 방향)은 Product 노드의 속성으로 처리. 엔터티로 별도 구성 시 그래프가 과도하게 세분화됨.

관계 유형

관계방향의미속성
BELONGS_TOProduct → Category카테고리 소속
IS_PART_OFProduct → Series시리즈 구성원
SUITABLE_FORProduct → Application적용 용도primary(boolean)
USES_TECHNOLOGYProduct → Technology적용 기술
MANUFACTURED_BYProduct → Vendor제조사
HAS_CERTIFICATIONProduct → Certification규격 인증 보유issued_year
SIMILAR_TOProduct ↔ Product유사 제품 (사양 유사)similarity_score
REPLACESProduct → Product대체 제품 (단종 등)since
PART_OFChunk → Document청크-문서 연결chunk_index
MENTIONSChunk → Entity청크-엔터티 연결confidence
NEXT_CHUNKChunk → Chunk청크 순서

노드 주요 속성

1
2
3
4
5
6
7
8
9
Product {
    product_code: string,          // 주문 코드 (유일키)
    name: string,
    burning_position: string,      // "s15/p15" (엔터티 아님, 속성)
    cooling: string,               // "Forced" / "Natural"
    base_type: string,             // "SFaX30-9.5"
    luminous_class: string,        // "HID"
    lifecycle: string              // "active" / "obsolete"
}

12.2 IT SI/SM 도메인 온톨로지

IT SI(System Integration) 및 SM(System Management) 산출물 도메인. 서버 스펙, 네트워크 구성도, 운영 매뉴얼, 장애 보고서 등에서 엔터티 추출.

엔터티 유형

유형설명예시
ProjectSI/SM 프로젝트 계약 단위인사시스템 고도화 2차, ERP SM
SystemIT 시스템 단위결제시스템, 인사관리시스템
Server물리/가상 서버WEB-01, DB-PROD-01
NetworkDevice방화벽/스위치/라우터FW-DMZ-01, L4-01
Application소프트웨어 애플리케이션Tomcat 9.0, Nginx 1.24
DatabaseDBMS 인스턴스Oracle 19c, PostgreSQL 15
MiddlewareWAS/MQ/ESBWebLogic 14c, RabbitMQ 3.11
Library소프트웨어 라이브러리Log4j 2.14.1, Spring 5.3
OS운영체제RHEL 8.5, Windows Server 2022
Team담당 팀/부서결제개발팀, 인프라운영팀
Vendor납품사/유지보수사삼성SDS, LG CNS
SLA서비스 수준 협약가용성 99.9%, RTO 4hr
Vulnerability보안 취약점CVE-2021-44228 (Log4Shell)
Regulation규정/법령/인증개인정보보호법, ISMS-P
Incident장애/이슈INC-20240115 DB 접속 실패
Document산출물설계서, 운영매뉴얼, 변경관리서
Chunk문서 청크

관계 유형

관계방향의미속성
PART_OFSystem → Project프로젝트 구성 시스템
DEPLOYED_ONApplication → Server서버 배포 관계environment
RUNS_ONApplication → OSOS 기반version
USESApplication → Library라이브러리 사용version
USESApplication → DatabaseDB 사용
USESApplication → MiddlewareWAS/MQ 사용
CONNECTS_TOServer → NetworkDevice네트워크 연결port, protocol
CONNECTS_TOSystem → System시스템 간 연동interface_type
MANAGED_BYSystem → Team운영 담당role
OWNED_BYSystem → Team개발 소유
MAINTAINED_BYSystem → Vendor유지보수 계약contract_end
GOVERNED_BYSystem → SLASLA 적용
GOVERNED_BYSystem → Regulation규정 적용effective_date
HAS_VULNERABILITYLibrary → Vulnerability취약점 해당severity
HAS_VULNERABILITYOS → VulnerabilityOS 취약점
CAUSED_BYIncident → Library장애 원인 라이브러리
CAUSED_BYIncident → Vulnerability취약점으로 인한 장애
AFFECTSIncident → System영향받은 시스템duration_min
DOCUMENTSDocument → System산출물 대상doc_type
PART_OFChunk → Document렉시컬 그래프chunk_index
MENTIONSChunk → Entity청크-엔터티 연결confidence
NEXT_CHUNKChunk → Chunk순서 연결

수치 데이터 (PostgreSQL — IT SI/SM)

IT SI/SM 서버 스펙은 공통 컬럼 + spec_json JSONB 확장:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- IT 서버 수치 데이터 (spec_json 예시)
{
  "cpu_cores": 32,
  "cpu_model": "Intel Xeon Gold 6330",
  "ram_gb": 256,
  "storage_tb": 10.0,
  "storage_type": "NVMe SSD",
  "network_gbps": 25,
  "rack_unit": 2,
  "power_redundancy": "N+1"
}

-- SLA 수치 (spec_json 예시)
{
  "availability_pct": 99.9,
  "rto_hr": 4,
  "rpo_hr": 1,
  "response_time_min": 30
}

IT SI/SM 멀티홉 Cypher 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- "Log4j 취약점에 영향받는 시스템과 담당팀"
MATCH (vuln:Vulnerability {cve: 'CVE-2021-44228'})
<-[:HAS_VULNERABILITY]-(lib:Library {name: 'Log4j'})
<-[:USES]-(app:Application)
-[:DEPLOYED_ON]->(srv:Server)
<-[:PART_OF|DEPLOYED_ON*1..2]-(sys:System)
-[:MANAGED_BY]->(team:Team)
RETURN DISTINCT sys.name, team.name, lib.version, vuln.severity
ORDER BY vuln.severity DESC

-- "A 시스템과 연동된 모든 시스템 및 그 담당팀 (2홉)"
MATCH (s1:System {name: $system_name})
-[:CONNECTS_TO*1..2]-(s2:System)
-[:MANAGED_BY]->(t:Team)
RETURN DISTINCT s2.name, t.name

13. 레거시 자산 계승 전략

자산파일계승 방법
ES 벡터 스토어vector_store_elasticsearch.pyElasticsearchManager 재활용, BGE-M3 교체, Sparse 추가
ES DockerElasticSearch/ 전체직접 이식 (Nori 포함)
PDF 표 파싱pdf_processor.py표 추출 + 메타데이터 로직 계승
FastAPI 구조app.py재활용, JWT/lifespan 마이그레이션
한국어 프롬프트rag_system.pyGraph 컨텍스트 섹션 추가
검색 벤치마크test_search_modes.pyRRF + RAGAS 포함 확장

RAGChatbotServer 가중 평균 방식 vs 우리 RRF 방식:

  • 레거시의 alpha 파라미터 개념은 RRF weight 파라미터로 계승
  • 레거시의 ES hybrid_search 구조는 RRF 소스 중 하나로 흡수
  • 레거시의 EnsembleRetriever import는 RRF로 대체

폐기: OpenSearch 전체, PGVector 클래스, PGVectorRAG 클래스, Jina/OpenAI 임베딩 API 어댑터


14. 기술 스택 확정

14.1 Python 패키지

역할패키지최신 버전비고
API Serverfastapi0.136.1레거시 계승
API Serveruvicorn[standard]0.46.0ASGI 서버
RAG 프레임워크langchain1.3.0레거시 계승
LLM 연동langchain-openai1.1.10DeepSeek OpenAI 호환
Agent 프레임워크langgraph1.1.0ReAct Agent (v1.0 LTS)
MCP 클라이언트 런타임langchain-mcp-adapters0.2.2DeepSeek ↔ MCP 서버 연결
MCP 서버fastmcp3.2.4도메인 MCP 서버 구현
임베딩 (로컬, BGE-M3)FlagEmbedding1.4.0Dense+Sparse, ~1.1GB, CPU
RerankerFlagEmbedding1.4.0BGE-Reranker-v2-m3
Vector + BM25 클라이언트elasticsearch88.19.3ES 8.x 서버 전용
Graph DB 클라이언트neo4j6.2.0Bolt 드라이버
SSOT DB 클라이언트psycopg[binary]3.3.4PostgreSQL async 드라이버
Redis 클라이언트redis7.4.0세션 + ETL 큐 + 임베딩 캐시
비동기 ETL 큐rq2.8.0Redis Queue
파일 감시watchdog6.0.0신규 문서 감지
PDF 파서 (기본)pdfplumber0.11.9표 구조 추출
PDF 파서 (보조)PyMuPDF1.27.2이미지·레이아웃 추출
RAG 평가ragas0.4.3Baseline/Graph/Sparse 비교

v0.5 변경: langchain-community(Jina) 의존성 제거. BGE-M3 단독 정책으로 외부 임베딩 API 어댑터 불필요.

14.2 인프라 (Docker Compose)

역할이미지버전비고
Vector + BM25elasticsearch8.19.xNori, HNSW, Sparse ← 벡터 단독
Knowledge Graphneo4j5.x (latest)Cypher, 멀티홉 ← 그래프 전담
SSOT + 수치postgres17JSONB 확장
세션/큐/캐시redis7.4세션 + RQ 백엔드 + 임베딩 캐시

14.3 LLM / 임베딩

역할모델방식비고
엔터티 추출 · MCP 오케스트레이션 · 응답 생성DeepSeek V4 ProAPIOpenAI 호환 엔드포인트
임베딩 (모든 용도)BGE-M3 (FlagEmbedding 1.4.0)로컬 CPUDense+Sparse 동시 생성, Redis 캐시 7일
RerankerBGE-Reranker-v2-m3로컬 CPUCPU 환경 50건 < 500ms (HRKP 실측)
개발/기능 테스트Qwen3 8B Q4 (Ollama)로컬기능 확인 전용
v0.5 변경: “임베딩 대규모 배치Jina Embeddings v3 API” 행 삭제. BGE-M3 로컬 단독으로 통일(H-5).

14.4 한국어 처리

역할기술비고
한국어 형태소 분석Nori (ES 내장 플러그인)BM25 품질 필수

15. 단계별 구현 로드맵

Step 1 — ES 기반 RAG + PostgreSQL SSOT (Baseline)

목표: Dense + BM25 검색, PostgreSQL SSOT, RAGAS Baseline 측정

  • OpenSearch/PGVector 코드 전체 제거
  • BGE-M3 로컬 임베딩 모듈 (core/embedding/local_bge_m3.py) + Redis 캐시
  • SemanticChunker 구현 (core/chunking/semantic_chunker.py, M-1)
  • ES Dense (HNSW) + BM25 (Nori) 인덱스 구성
  • RRF 모듈 구현 (core/rag/rrf.py) — Step 1에서는 ES 2소스(BM25 + Dense)만 통합
  • FastAPI lifespan 마이그레이션, Health Check LLM 호출 제거
  • PostgreSQL SSOT 스키마 구성
  • Redis Queue 비동기 ETL 파이프라인
  • PDFParserFactory + ProductSpecExtractor
  • RAGAS Baseline 측정 (Dense + BM25)

완료 기준: 제품 카탈로그 PDF 적재 → 검색 → RAGAS 기준선 확보


Step 2 — Neo4j GraphRAG 통합 (핵심 실험)

목표: GraphRAG 추가 후 RAGAS 향상 측정

  • Neo4j Docker Compose 추가 (통합: ES + Neo4j + PG + Redis)
  • 온톨로지 기반 엔터티 추출 파이프라인 (DeepSeek V4 Pro API, Gleaning 2-pass, M-4)
  • Neo4j 노드/엣지 MERGE + Chunk -MENTIONS-> Entity
  • HybridGraphRAG 클래스 + Neo4j 소스 RRF 통합 (graph_search_top_k=3, M-7)
  • BGE-Reranker-v2-m3 도입 (core/rerank/bge_reranker.py, M-2)
  • Cypher 멀티홉 패턴 (조명 도메인 + IT SI/SM)
  • RAGAS 측정: Baseline vs +GraphRAG ← 핵심 비교
  • JWT 인증 미들웨어 + Redis 세션
  • Osram MCP 서버 기본 구현 + LangChain MCP Adapter(DeepSeek 오케스트레이터) 연동 테스트

완료 기준: GraphRAG 추가 전후 RAGAS 점수 비교 결과 확보

v0.5 정정 사항: “DeepSeek V3 API” → “DeepSeek V4 Pro API”로 통일, “Claude 연동 테스트” → “LangChain MCP Adapter(DeepSeek 오케스트레이터) 연동 테스트”로 정정.


Step 3 — Sparse Vector 실험

목표: Sparse Vector 추가 효과 정량 측정

  • BGE-M3 → ES sparse_vector 변환 (generate_sparse_vector())
  • ES sparse_vector 인덱스 추가 (재인덱싱 또는 업데이트)
  • RRF에 Sparse 소스 추가 (weight=0.7, M-6)
  • RAGAS 측정: +GraphRAG vs +GraphRAG+Sparse
  • CPU 인덱싱 속도 벤치마크 (Dense vs Dense+Sparse)

Step 4 — 운영화 및 MCP 고도화

  • Osram MCP 전체 도구 완성
  • IT SI/SM 도메인 MCP 초안
  • 문서 갱신 파이프라인 (변경 시 Triple Store 재동기화)
  • RAGAS 자동화 측정
  • 패키지 구조 리팩토링 (core/ + mcp_servers/ + api/)

16. 미결 사항

✅ 결정 완료

#항목결정 내용근거
H-1도메인조명 제품 카탈로그 (1차), IT SI/SM (2차)기획 확정
H-2LLM 선택DeepSeek V4 Pro API (엔터티 추출 · MCP 오케스트레이션 · 응답 생성 통합)v0.4 수정 — V3 → V4 Pro
H-3Sparse VectorBGE-M3, Step 3 실험 변수, ELSER 배제하드웨어 제약
H-4수치 추출 방식DeepSeek API 추출 (레거시 정규식 방식 탈피)레거시 regex 한계 확인
H-5임베딩 전략BGE-M3 로컬 단독 (BAAI/bge-m3, 1024차원) — Jina/OpenAI 미사용HRKP 실 구현 확인 (embedding_test_results_2026-02-02): BGE-M3 단독 적용, Redis 캐시, 7.5 texts/sec 배치 성능 실증. 추가 API 비용 없이 완전 로컬 운용
H-6벡터 저장소ES 단독 (Neo4j 중복 저장 없음)역할 분리 원칙
H-7RRF 설계커스텀 RRF (레거시 가중 평균 방식 불채택)레거시 custom-hybrid alpha=0.6 분석
H-8MCP 오케스트레이터DeepSeek V4 Pro + LangChain MCP Adapter (Claude 클라이언트 가설 폐기)v0.4 수정 — 단일 LLM 단순화
H-9IT SI/SM 온톨로지v0.4에서 신규 작성 완료문서 내 확정
M-1청크 크기SemanticChunker (BGE-M3 임베딩 기반) — target=1024 tokens / overlap=128 tokens (12.5%) / 허용 범위 256~2048 tokensHRKP STORY-003 (Done, 2026-01-26): 고정 크기 분할 대신 BGE-M3 임베딩으로 의미 경계를 감지하는 SemanticChunker 채택. 기술 매뉴얼 도메인에서 문단 단위 의미 완결성을 보장하며, 너무 작은 청크(< 256)와 너무 큰 청크(> 2048)는 자동 재분할
M-2RerankerBGE-Reranker-v2-m3 도입 확정 — RRF 상위 50건 대상 rerank → top_k 반환, CPU/GPU 자동 감지HRKP STORY-032 (Done): BAAI/bge-reranker-v2-m3 크로스 인코더 기반 관련성 점수 계산, CPU 환경에서 50건 < 500ms 실증, 65 tests passed. 구현: asyncio.to_thread() 비동기 래핑, batch_size=32, Sigmoid 정규화(0~1)
M-3Neo4j Community 메모리heap_initial=256m / heap_max=512m / pagecache=256m / container limit=1GB (Docker Compose 고정)HRKP container_memory_tuning_guide.md 실측 적용값 (2026-01-21): ES(2GB)+Neo4j(1GB)+PG(512MB)+Redis(256MB) 동시 운용 기준 Windows 노트북 16GB 환경 안정 확인. heap_max=512m은 Neo4j 공식 권장 최솟값; pagecache는 자주 접근하는 그래프 페이지 캐시
M-4엔티티 추출 방식LLM 기반 엔티티 추출 (DeepSeek V4 Pro, deepseek-chat 엔드포인트) + Gleaning 2-pass (+33% Recall) / 신뢰도 임계값 ≥ 0.7 — 7개 타입 확정: Person, Organization, Technology, Project, Concept, Date, LocationHRKP entity_extraction.py 실 구현 + 2026-02-15 배치 완료 보고서: 16,185 청크 처리 → 70,855개 고유 엔티티, 375,229개 관계 추출. Gleaning(2-pass) 적용으로 1-pass 대비 Recall +33%. 수치형 데이터(wattage, lifespan 등)는 ETL 파이프라인에서 PostgreSQL 별도 스키마로 분리 저장
M-5임베딩 모델 선택BGE-M3 로컬 단독 채택 (BAAI/bge-m3, 1024차원) — Jina v3/OpenAI ada-002 미사용HRKP embedding_test_results_2026-02-02 실증: BGE-M3 단독으로 다국어 임베딩 운용, Redis TTL=7일 캐시, 배치 7.5 texts/sec, 벡터 검색 cosine similarity 정확도 검증 완료. Jina/OpenAI는 HRKP 전체 코드베이스에 미존재; 추가 API 비용 없이 완전 로컬 운용
M-6RRF 파라미터k=60 / rrf_weight_vector=1.0 / rrf_weight_keyword=1.0 / rrf_weight_sparse=0.7 / rrf_weight_graph=0.8HRKP 34_graph_search_rrf_tuning.md (2026-03-09 확정): Vector·Keyword 각 top_k×2=20건 후보 생성, Sparse=0.7(Dense 대비 보조 역할), Graph=0.8(그래프 경로 기여 반영). k=60은 Cormack et al. 표준값으로 HRKP에서 운용 검증 완료
M-7Graph 검색 후보 수graph_search_top_k=3 고정 (Graph 소스 후보 수 제한 방식) — weight 조정 방식 ADR 거부HRKP 34_graph_search_rrf_tuning.md ADR (Accepted 2026-03-09): Graph weight를 높게(1.5) 설정하면 Graph 결과가 10건 중 8건 1위를 독식하는 문제 발생. weight 조정 대신 Graph 후보 수를 top_k×2(=20)에서 3으로 고정하여 해결. Vector·Keyword는 각 20건 유지로 균형 있는 RRF 퓨전 보장

⏳ 향후 검토 사항 (v0.5 신규)

#항목메모
F-1DeepSeek V4 Pro 실제 단가API 가격 정책 확정 후 10.3 비용 추정 갱신
F-2Step 1 RAGAS Baseline 결과v0.6에서 실측 수치 반영
F-3Neo4j 5.x ↔ neo4j-python 6.2.0 호환성Step 2 진입 전 통합 테스트로 확인

17. 부록

부록 A. RAGAS 평가 지표

지표설명
Faithfulness응답이 컨텍스트에 충실한가 (환각 방지)
Answer Relevancy응답이 질문에 관련 있는가
Context Recall관련 정보가 검색됐는가
Context Precision검색된 정보가 실제 유용한가

부록 B. 임베딩 전략

1
2
3
4
5
6
7
문서 수신
  ↓
로컬 BGE-M3 (BAAI/bge-m3) — 단독 운용
  - Dense Vector (1024차원) → Elasticsearch
  - Sparse Vector (BGE-M3 SPLADE) → Elasticsearch (Step 3 이후)
  - 캐시: Redis TTL=7일 (중복 임베딩 방지)
  - 배치 처리: 7.5 texts/sec (CPU 환경 실측)

HRKP 실증: BGE-M3 단독으로 다국어(한국어 포함) 임베딩 운용. Jina/OpenAI 외부 API 미사용.

부록 C. 참고 자료

부록 D. 기술결정 로그

각 결정 사항의 배경, 검토된 대안, 채택 이유를 기록한다. 향후 재검토 시 맥락 유지를 위함.


[TD-01] Dense Vector 저장소: ES 단독 채택 (Neo4j 중복 배제)

  • 결정일: 2026-05-14 (v0.4)
  • 배경: v0.3에서 Neo4j를 Semantic Hub로 설계하며 Dense 벡터를 Neo4j에 저장하는 방안을 검토했음
  • 검토된 대안:
    • A) ES + Neo4j 양쪽에 Dense 벡터 저장 → 거부: 동기화 복잡도, 메모리 낭비
    • B) Neo4j 단독 Dense 벡터 → 거부: BM25/Nori/Sparse와 통합 불가
    • C) ES 단독 Dense + Neo4j 그래프 ← 채택
  • 채택 근거: 세미나(7) 아키텍처 명시. ES가 Dense+Sparse+BM25 통합 처리. Neo4j는 Cypher 멀티홉에 집중.
  • 제약: Neo4j의 “벡터+그래프 단일 Cypher” 장점 포기. 단, RRF로 결과 통합하면 동일 효과 달성 가능.

[TD-02] RRF 채택 (가중 평균 방식 불채택)

  • 결정일: 2026-05-14 (v0.4)
  • 배경: RAGChatbotServer는 alpha=0.6 고정 가중 점수 평균을 사용. ES 2개 소스만 통합.
  • 검토된 대안:
    • A) 레거시 방식 계승 (가중 평균) → 거부: 점수 스케일 불일치, Neo4j 통합 불가
    • B) ES 내장 hybrid (BM25+Dense) + Neo4j 별도 → 거부: 진정한 RRF 아님
    • C) 커스텀 RRF ← 채택: 순위 기반, 스케일 정규화 불필요, 3+ 소스 통합 가능
  • 채택 근거: 수학적 근거 있음(2009 논문), k=60 표준값 robust, weight로 GraphRAG 강조 가능. 단, weight 조정만으로는 Graph 결과 편향이 발생하여 후보 수 제한 방식(M-7)을 병행한다.

[TD-03] DeepSeek API 전용 정책

  • 결정일: 2026-05-14 (v0.3) / 모델 명세 갱신: 2026-05-14 (v0.4)
  • 배경: 중국 모델의 로컬 실행 가능 여부 검토
  • 결론: DeepSeek V4 Pro full(~220GB), Q4(~100GB) 모두 16GB RAM에서 실행 불가. Qwen3 8B Q4(~5GB)는 가능하나 속도 1~3 token/s로 운영 용도 부적합.
  • 정책: 중국 계열 모델 전체 API 전용. Qwen3 8B Q4는 개발/기능 테스트 전용.
  • 선정 모델: DeepSeek V4 Pro (OpenAI 호환 엔드포인트) — 엔터티 추출/MCP 오케스트레이션/응답 생성 통합 담당.

v0.5 정정 사항: v0.4 문서의 “DeepSeek V3” 표기는 v0.3 시점 잔재였다. 현재 결정 모델은 V4 Pro다(H-2).


[TD-04] MCP 오케스트레이터: DeepSeek V4 Pro + LangChain MCP Adapter 채택

  • 결정일: 2026-05-14 (v0.4)
  • 배경: 사용자가 도메인별 MCP 서버 아이디어를 제안. 초기에는 “MCP는 Anthropic 프로토콜이라 Claude만 클라이언트가 될 수 있다”는 가설로 검토했으나, MCP 프로토콜이 모델 비특정 오픈 스탠다드이며 langchain-mcp-adapters를 통해 임의의 LLM(OpenAI 호환 포함)이 MCP 클라이언트가 될 수 있음을 확인.
  • 검토된 대안:
    • A) Claude 클라이언트 전용으로 MCP 서버 노출 → 거부: 시스템 외부 의존 추가, DeepSeek 단일 LLM 정책과 상충
    • B) DeepSeek는 OpenAI 호환 function calling만 사용, MCP 서버 미도입 → 거부: 도메인별 MCP 자산을 활용하지 못함
    • C) DeepSeek V4 Pro + LangChain MCP Adapter채택: 단일 LLM이 엔터티 추출·오케스트레이션·응답 생성을 모두 담당하여 아키텍처가 단순해짐
  • 채택 근거: ① LangChain Agent의 Tool 인터페이스로 MCP 서버를 노출 가능, ② DeepSeek 단일 LLM로 비용/관리 단순화, ③ MCP 자산은 그대로 유지하면서 외부 Claude 의존 제거.

v0.5 정정 사항: v0.4 문서에는 “Claude 전용 명시”라는 v0.3 시점 결론이 부록 D에 잔존해 있었다. v0.5에서 본문 9절·H-8과 일치하도록 결정 내용을 갱신했다.


[TD-05] BGE-M3 로컬 단독 채택 (Jina/OpenAI 임베딩 API 배제)

  • 결정일: 2026-05-14 (v0.4)
  • 배경: v0.3까지는 “소량은 로컬 BGE-M3, 대량 배치는 Jina v3 API” 이중 전략을 검토했음.
  • 검토된 대안:
    • A) Jina v3 API 병행 (소량 로컬 / 대량 API) → 거부: 외부 API 비용 발생, 캐시·재현성 관리 복잡
    • B) OpenAI text-embedding-3 API → 거부: 1536차원으로 BGE-M3와 차원 불일치, 한국어 품질 BGE-M3 대비 우위 없음
    • C) BGE-M3 로컬 단독 + Redis TTL=7일 캐시 ← 채택
  • 채택 근거: HRKP 실 운용 결과 7.5 texts/sec(배치)로 운영 요건 충족. Redis 캐시로 중복 임베딩 비용 최소화. 외부 API 비용 0원, 모든 데이터 사내 보존.

[TD-06] ELSER 배제, BGE-M3 Sparse 채택

  • 결정일: 2026-05-14 (v0.3)
  • 근거: ELSER v2는 영어 전용. 한국어 문서에서 성능 매우 낮음. BGE-M3 Sparse는 100+ 언어 지원, 한국어 MIRACL 벤치마크 최고 수준. token_id→token_string 변환 코드 구현 필요하나 실용적.

[TD-07] RRF Graph 가중치보다 후보 수 제한 우선 (HRKP 튜닝 반영)

  • 결정일: 2026-03-09 (HRKP), v0.5에서 본 문서 반영
  • 배경: v0.4 본문 6.6에서는 Graph weight=1.5로 GraphRAG를 강조하도록 설계했으나, HRKP 실 운용에서 Graph 결과가 RRF top-10 중 8건을 점유하는 편향이 관찰됨.
  • 검토된 대안:
    • A) Graph weight 조정(1.5 → 1.0 → 0.5) → 거부: 가중치 미세 조정으로는 편향이 충분히 완화되지 않음
    • B) graph_search_top_k=3 고정 + Graph weight=0.8채택
  • 채택 근거: Graph 소스에서 반환되는 후보 수 자체를 제한하면 RRF 누적 점수 기여가 자연스럽게 균형. Vector·Keyword 후보 수(20건)는 유지. HRKP ADR 34_graph_search_rrf_tuning.md(Accepted) 참조.

  • 작성일: 2026-05-15
  • 성격: 정합성 정리 릴리스 (v0.4 내부 모순 일괄 수정, 신규 의사결정 없음)
  • 다음 수정 예정: Step 1 구현 완료 후 RAGAS Baseline 결과 + DeepSeek V4 Pro 단가 실측 반영 (v0.6)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.