feat: pluggable embedding backends (OpenAI-compatible / local-gguf + resilient fallback)#7
Open
epruseal wants to merge 8 commits into
Open
feat: pluggable embedding backends (OpenAI-compatible / local-gguf + resilient fallback)#7epruseal wants to merge 8 commits into
epruseal wants to merge 8 commits into
Conversation
…billing Fix Docker full mode startup and billing events
## 문제 `LocalGraphStore.find_neighbors`의 외부 while 루프 조건 `len(results) < limit`은 노드 한 개를 완전히 처리한 **뒤에야** 평가된다. 기존 코드는 `cur.fetchall()`로 해당 노드의 모든 엣지를 한꺼번에 메모리에 올린 다음 내부 for 루프를 끝까지 돌았기 때문에, 그동안 limit 체크가 전혀 일어나지 않았다. 이로 인해 차수(degree)가 높은 허브 노드 — 예: 온톨로지 지식 그래프에서 "engineer", "persona", "pack" 같은 개념 노드 — 를 탐색할 때, limit(기본값 50)에 도달한 이후에도 수백~수천 회의 불필요한 `_fetch_node_props` SQL 쿼리가 실행됐다. ## 실측 수치 실제 온톨로지 데이터셋(nodes.jsonl 43,476개, edges.jsonl 68,322개)으로 측정: | 스케일 | 최고 허브 차수 | find_neighbors d1 p50 | |--------|-------------|----------------------| | 20,000 노드 | 98차수 | **0.37ms** | | 43,000 노드 | 615차수 | **11.86ms** ← 32× 급등 | 43,000 노드 시점에 차수 615짜리 허브가 등장하면서, 내부 루프가 ~1,230회 `_fetch_node_props` 쿼리를 모두 실행한 뒤에야 results가 50개를 채웠다. 데이터가 증가할수록 허브 차수도 함께 커지므로, 방치하면 선형 이상으로 열화된다. ## 수정 내용 두 단계 수정을 outgoing / incoming 양방향 모두에 적용했다: 1. **SQL `LIMIT ?`** — `fetchall`이 반환하는 행 자체를 남은 슬롯 (`remaining = limit - len(results)`)으로 제한한다. 차수 615 허브라도 이미 results가 0개인 경우 최대 50행만 DB에서 가져온다. 2. **내부 루프 `break`** — pack 필터 등으로 일부 행이 걸러지면 fetchall로 가져온 `remaining`개 중 실제로 results에 추가되는 비율이 낮아질 수 있다. SQL LIMIT만으로는 이를 보완할 수 없으므로, for 루프 안에서도 limit 도달 시 즉시 break해 추가 property 조회를 방지한다. ## 수정 후 성능 동일 데이터셋(43k 노드, 차수 615 허브) 기준: | depth | 수정 전 | 수정 후 | 개선 | |-------|---------|---------|------| | d=1 | 11.86ms | 1.59ms | **7.5×** | | d=2 | 10.98ms | 1.58ms | **6.9×** | | d=3 | 10.74ms | 1.56ms | **6.9×** | ## 동작 변경 없음 확인 - 결과 집합의 정확성: 기존 동작과 동일 (허브/일반 노드 모두 정확성 테스트 통과) - limit 동작: 실제 차수 62인 노드에서 50개 반환 확인 - 일반 노드(차수 1-5): SQL LIMIT과 break 모두 발동하지 않으므로 동작 동일 - 결과 순서: SQLite는 엣지 삽입 순서 기반, Neo4j는 내부 인덱스 순서 기반으로 이미 두 모드가 동일하지 않았으며, 이 수정으로 추가 변화 없음
Before: OntologyBuilder.add_edge resolved the from/to node types via a Neo4j Cypher query, gated on `self._neo4j.available`. In local-only mode the store is a LocalGraphStore (SQLite) and its `run_cypher` is a no-op stub, so the lookup silently returned [] and both endpoints fell back to `_space_to_default_type(space)` — flattening every subject-space edge to `User`, every decision-space edge to the first decision type, every resource-space edge to `Project`, and every concept-space edge to `Entity`. Multi-type ontologies (e.g. domain packs that register many node types per space) lost edge typing entirely in local mode, and `find_neighbors` / `find_path` queries failed to find the typed neighbors they expected. After: both LocalGraphStore and Neo4jStore expose a parallel `lookup_node_type(node_id) -> str | None`. The builder uses that method via duck-typing on whichever store was injected, with the per-space default kept only as a final fallback when the lookup truly returns None. This restores typed-edge behavior for local-only deployments without requiring Neo4j, and keeps existing Neo4j-backed deployments working since the new Neo4jStore.lookup_node_type runs the same Cypher MATCH that was previously embedded in the builder. Discovered while building a domain ontology pack that registers many typed decision/actor nodes in local mode; reproduced via the new regression test using upstream-native Team / Document types.
fix: BFS find_neighbors 허브 노드 성능 문제 수정 (SQL LIMIT + 내부 루프 조기 종료)
…e-type-resolution fix(builder): preserve real node types on edges in local-only mode
…resilient fallback) 기존 ChromaDB 기본 EF(all-MiniLM-L6-v2, 384d)는 영어 특화라 한국어 검색 품질이 낮다(실측 top-1 0/5, MRR 0.285). EMBEDDING_BACKEND 환경변수로 임베딩 함수를 교체할 수 있게 하여 KURE-v1(1024d) 등 OpenAI 호환 서버 모델로 품질을 개선한다 (실측 top-1 5/5, MRR 1.000). - EMBEDDING_BACKEND=local(기본): 기존 ChromaDB EF 그대로 → 100% 하위호환. - EMBEDDING_BACKEND=openai: OpenAI 호환 /v1/embeddings 서버(LM Studio·vLLM· Ollama·실제 OpenAI) + 로컬 GGUF 폴백. - OpenAIEmbeddingFunction: httpx 직접 호출, OPENAI_API_KEY 설정 시 Bearer 인증. - LlamaCppEmbeddingFunction: 로컬 GGUF 폴백(lazy load, 자동 다운로드). - ResilientEmbeddingFunction: primary 장애 시 폴백 자동 전환(health TTL). 환경변수: EMBEDDING_BACKEND, OPENAI_API_BASE, OPENAI_EMBED_MODEL, OPENAI_API_KEY, OPENAI_TIMEOUT, EMBED_DIM, EMBED_COLLECTION, LOCAL_GGUF_PATH.
epruseal
force-pushed
the
feat/embedding-backends
branch
from
June 25, 2026 03:33
c6db9aa to
49c52e3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: pluggable embedding backends (OpenAI-compatible / local-gguf + resilient fallback)
배경 / 문제
ChromaDB 기본 임베딩 함수(all-MiniLM-L6-v2, ONNX, 384d)는 영어 특화라 한국어 검색 품질이 낮다.
동일 코퍼스에서 실측 결과:
변경 요약
EMBEDDING_BACKEND환경변수로 임베딩 함수를 교체할 수 있게 한다. 기본값은local이라기존 동작 100% 보존(아무 설정도 안 하면 지금과 똑같이 minilm 사용).
EMBEDDING_BACKEND=local(기본): ChromaDB 기본 EF. 기존 컬렉션opencrab_vectors그대로.EMBEDDING_BACKEND=openai: OpenAI 호환/v1/embeddingsAPI를 쓰는 범용 경로. 임베딩모델은 자유롭게 선택 — 실제 OpenAI 클라우드 모델(
text-embedding-3-small/-large등)도,로컬·자체호스팅 서버(LM Studio·Ollama·vLLM·HF TEI)에 올린 모델도 모두 가능하다. 모델은
OPENAI_EMBED_MODEL, 차원은EMBED_DIM, 컬렉션은EMBED_COLLECTION으로 지정한다.추천한다. 번들된 로컬 GGUF 폴백도 KURE-v1이라, KURE primary면 추가 설정 없이 폴백까지
바로 동작한다.
EMBED_DIM을 해당 모델 차원에 맞추고(예:-3-small=1536),로컬 GGUF 폴백은 차원이 달라 부적합하므로
LOCAL_GGUF_PATH로 동일 차원 GGUF를 지정하거나폴백에 의존하지 않는다(원격만 사용). → 자세한 제약은 아래 "임베딩 모델 혼용 금지" 참고.
신규 컴포넌트
stores/openai_embedding.pyOpenAIEmbeddingFunction— httpx로/v1/embeddings직접 호출.OPENAI_API_KEY설정 시Authorization: Bearer, 미설정 시 무인증(LM Studio 등). L2 재정규화.stores/llamacpp_embedding.pyLlamaCppEmbeddingFunction— 로컬 GGUF 폴백. lazy load, 미존재 시 HuggingFace 자동 다운로드(KURE-v1-Q4_K_M).stores/resilient_embedding.pyResilientEmbeddingFunction— primary 장애 시 fallback 자동 전환. health TTL 캐시로 불필요한 타임아웃 재시도 방지.기존 파일 변경
config.py: 임베딩 관련 Settings 필드 추가(아래 환경변수). 기존 필드/동작 불변.stores/chroma_store.py:embedding_function: Any = None파라미터 추가.None이면 기존기본 EF 사용 → 하위호환. 주입 시 해당 EF로 add/query.
stores/factory.py:make_vector_store()에embedding_backend=="openai"분기만 추가.make_graph_store/make_doc_store/make_sql_store는 변경 없음.호환성 / 롤백
local이라 설정 안 하면 기존과 동일. 새 의존성도 openai 경로에서만 로드(lazy import).EMBEDDING_BACKEND미설정 또는local로 되돌리면 즉시 롤백, 기존 컬렉션 보존.name()="kure_v1"고정 → 컬렉션 메타데이터 일관 → 재시작/폴백 전환 시 동일 컬렉션 재사용.하나의 Chroma 컬렉션은 하나의 임베딩 모델·차원으로만 채워야 한다. 색인(add)과
질의(query)는 반드시 같은 모델로 수행되어야 하며, 서로 다른 모델의 벡터를 한 컬렉션에
섞으면 검색이 무의미해진다(차원 불일치 또는 의미공간 불일치). 한 컬렉션 = 한 모델이
원칙이다. (
openai는 모델이 아니라 *백엔드(전송 방식)*임에 유의 — 실제 OpenAI 모델, KURE,KoSimCSE 등 어떤 모델이든 이 백엔드로 쓸 수 있고, 그중 무엇을 골랐든 컬렉션 안에서 섞지 말 것.)
local(minilm 384d)과openai백엔드의 컬렉션은 분리되어 있어(
opencrab_vectors↔EMBED_COLLECTION, 기본opencrab_vectors_kure) 자동으로 충돌하지 않는다.EMBED_COLLECTION을 쓰고 전량 재색인해야 한다. 기존 컬렉션에 다른 모델로 덧쓰면 안 된다.같은 KURE-v1임을 전제로
name()="kure_v1"하나로 컬렉션을 공유한다. 서로 다른 모델을primary/fallback으로 두면 안 된다.
환경변수
EMBEDDING_BACKENDlocallocal또는openaiOPENAI_API_BASEhttp://localhost:1234/v1OPENAI_EMBED_MODELtext-embedding-kure-v1text-embedding-3-small, KURE 서빙 모델명 등)OPENAI_API_KEYOPENAI_TIMEOUT8.0EMBED_DIM1024text-embedding-3-small=1536)EMBED_COLLECTIONopencrab_vectors_kureLOCAL_GGUF_PATH~/.cache/opencrab/models에 자동 다운로드테스트 / 검증
py_compile6파일 통과.OpenAIEmbeddingFunction.name()=="kure_v1", Bearer 헤더 분기,_l2_normalize,빈입력 패스스루, ResilientEF 체인 생성 모두 정상(네트워크 없이).
_DEFAULT_GGUF_DIR가~/.cache/opencrab/models로 해석되고XDG_CACHE_HOME존중 확인.ontology_query3건 정상비고
LM Studio언급은호환 서버의 예시로만 남김.
mykor/KURE-v1-gguf(Q4_K_M, ~438MB)는 한국어 임베딩용 공개 모델. 다른 모델은LOCAL_GGUF_PATH로 지정 가능.CPU 자원이 부족하면 한국어 경량 임베딩
BM-K/KoSimCSE-roberta(RoBERTa-base, 약 110M, 768d)를 추천한다. KURE보다 가볍고 빠르지만 한국어 전용이며 검색
품질은 다소 낮을 수 있다.
Text-Embeddings-Inference)에 KoSimCSE를 서빙하고
OPENAI_EMBED_MODEL,EMBED_DIM=768, 별도EMBED_COLLECTION만 지정하면 된다(전량 재색인 필요).(GGUF가 없으면
EMBEDDING_BACKEND=openai의 primary만 사용).