검색 엔진과 벡터 스토리지(Retrieval Engines)
검색 엔진은 인덱스를 저장하고 벡터, 키워드 또는 하이브리드 검색을 실행합니다. 기본 PostgreSQL 배포는 ParadeDB 이미지로 pgvector와 BM25를 제공하며 업무 데이터와 데이터베이스를 공유할 수 있습니다. 독립적인 확장이나 기존 인프라 재사용이 필요하면 다른 백엔드를 선택할 수 있습니다.
백엔드를 선택할 때는 배포 의존성, 용량, 데이터 격리 요구 사항을 고려합니다.
| 상황 | 고려 사항 |
|---|---|
| 내장 데이터베이스를 사용하는 단일 머신 또는 데스크톱 배포 | SQLite(내장, 외부 의존성 없음) |
| 벡터 검색 서비스를 독립적으로 확장해야 함 | Qdrant, Milvus |
| 회사에 Elasticsearch / OpenSearch 스택이 이미 있음 | 기존 클러스터 재사용 |
| 지식 베이스별로 데이터를 분리 저장해야 함 | 기본 엔진을 유지하면서 ‘설정 → 벡터 스토리지’에 별도 인스턴스를 등록하고 지정한 지식 베이스에 연결 |
엔진을 전환하려면 인덱스를 재구축해야 합니다. 지식 베이스에 연결한 벡터 스토리지는 생성 후 변경할 수 없으므로 생성 전에 인스턴스를 결정해야 합니다.
기능 매트릭스와 선택 비교
| 엔진 | RETRIEVE_DRIVER 값 | 벡터 검색 | 키워드/전문 | 키워드 점수 | 중국어 토큰화 | 차원 관리 | 임계값 푸시다운 | 배포 복잡도 | 적합한 시나리오 |
|---|---|---|---|---|---|---|---|---|---|
| PostgreSQL | postgres | pgvector halfvec + HNSW 표현식 인덱스 | ParadeDB BM25(|||) | BM25(paradedb.score) | ParadeDB tokenizer | 단일 테이블에 차원 혼합, 표현식 인덱스에서 차원별 cast | SQL 내 거리 임계값 | 낮음(기본 이미지 내장) | 기본 선택, 업무 데이터와 동일 DB, 트랜잭션 일관성 |
| SQLite | sqlite | sqlite-vec vec0(cosine) | FTS5 contentless | FTS5 | 애플리케이션 측 bigram | 차원별 vec0 가상 테이블 | 애플리케이션 측 | 매우 낮음(내장) | 데스크톱 / 개발 / 초소형 배포 |
| Elasticsearch v8 | elasticsearch_v8 | script_score cosineSimilarity | match(BM25) | BM25 | ES analyzer | dense_vector 단일 인덱스 | 애플리케이션 측 | 중간 | 기존 ES 8 클러스터 |
| Elasticsearch v7 | elasticsearch_v7 | 미지원(Support는 keywords만) | match(BM25) | BM25 | ES analyzer | — | — | 중간 | 기존 ES 7, 키워드 엔진으로만 사용하며 다른 벡터 엔진과 조합 필요 |
| OpenSearch | opensearch | k-NN 플러그인 knn(HNSW) | match(BM25) | BM25 | OS analyzer | knn_vector 선언적 mapping + 지문 검증 | k-NN 네이티브 | 중간 | 감사/별칭/reindex가 필요한 프로덕션 ES 계열, 버전 2.11+/3.x |
| Qdrant | qdrant | 네이티브 HNSW Cosine | 전문 인덱스 MatchText(token OR) | 점수 없음(Scroll 일치 즉시 반환, RRF rank 사용) | 다국어 tokenizer | 차원별 collection | score_threshold 네이티브 | 중간 | 벡터 중심이며 payload 필터가 필요한 시나리오 |
| Milvus | milvus | HNSW(IP/COSINE/) | BM25 Function 희소 벡터 | BM25 | Milvus analyzer | 차원별 collection | 애플리케이션 측 | 중상 | 대규모 벡터, 네이티브 BM25 하이브리드 검색 필요 |
| Weaviate | weaviate | nearVector(certainty) | 네이티브 BM25 | BM25 | Weaviate tokenizer | 동적 Class | certainty 네이티브 | 중간 | GraphQL 생태계, 복제/샤딩 설정 필요 |
| Doris | doris | ANN HNSW inner_product/cosine | 역색인 MATCH_ANY | 역색인 일치 | 테이블 생성 시 chinese parser 선언 | 차원별 테이블 | SQL 내 | 높음 | 기존 Doris 데이터 웨어하우스, 검색과 분석 통합 |
| Tencent Cloud VectorDB | tencent_vectordb | HNSW COSINE | 희소 벡터 BM25(SPARSE_INVERTED) | BM25 | SDK SparseEncoder | 차원별 collection | 애플리케이션 측 | 낮음(클라우드 관리형) | Tencent Cloud 관리형, 운영 부담 없음 |
설명: 엔진 자체가 ‘하이브리드 검색’을 제공하는지와 관계없이 WeKnora는 항상 상위 계층의 통합 RRF 융합(
knowledgebase_search_fusion.go)을 사용합니다. 벡터와 키워드를 각각 독립적으로 검색한 후 rank에 따라 가중 병합합니다(하이브리드 검색 점수와 정규화 참고). 따라서 각 엔진은 두 종류의 단일 모드 검색만 각각 제공하면 됩니다.
설정 방법 요약
핵심 스위치(.env.example C1절, docker-compose.yml):
| 환경 변수 | 기본값 | 설명 |
|---|---|---|
RETRIEVE_DRIVER | postgres | 여러 드라이버를 쉼표로 구분: postgres / sqlite / elasticsearch_v7 / elasticsearch_v8 / opensearch / qdrant / milvus / weaviate / doris / tencent_vectordb. 여러 드라이버를 쓰면 쓰기 작업은 전체에 브로드캐스트하고 검색은 유형별 라우팅 |
MULTI_STORE_RETRIEVE_TIMEOUT_SEC | 30 | 여러 store 병렬 검색의 그룹별 타임아웃 |
ELASTICSEARCH_ADDR / _USERNAME / _PASSWORD / _INDEX | — / WeKnora | ES v7/v8 공용 |
OPENSEARCH_ADDR / _USERNAME / _PASSWORD / _INDEX / _INSECURE_SKIP_VERIFY | — | OpenSearch |
QDRANT_HOST / _PORT / _COLLECTION / _API_KEY / _USE_TLS | localhost / 6334 / weknora_embeddings | Qdrant(gRPC 포트) |
MILVUS_ADDRESS / _COLLECTION / _METRIC_TYPE / _USERNAME / _PASSWORD / _DB_NAME | localhost:19530 / weknora_embeddings / IP | metric 변경 후 collection 재구축 필요 |
WEAVIATE_HOST / _GRPC_ADDRESS / _SCHEME / _AUTH_ENABLED / _API_KEY / _COLLECTION | weaviate:8080 / weaviate:50051 / http | 컨테이너 안에서는 서비스 이름 사용 |
DORIS_ADDR / _HTTP_PORT / _DATABASE / _USERNAME / _PASSWORD / _TABLE_PREFIX / _COMPAT_MODE | doris-fe:9030 / 8030 / weknora / root / — / weknora_embeddings / auto | Doris 4.1+, 테이블 생성 후 compat 모드 상호 전환 불가 |
TENCENT_VECTORDB_ADDR / _USERNAME / _API_KEY / _DATABASE / _COLLECTION | — | 핵심 3개 항목 중 하나라도 없으면 등록 건너뜀 |
NEO4J_ENABLE / NEO4J_URI / _USERNAME / _PASSWORD | false / bolt://neo4j:7687 | 그래프 검색(벡터 엔진 체계와 독립) |
환경 변수(env store, 프로세스 전역) 외에도 관리 화면에서 테넌트의 VectorStore 레코드(DB store)를 생성하고 특정 KB에 연결할 수 있습니다. 같은 엔진 유형에 여러 클러스터 인스턴스를 연결할 수 있으며, 검색 시 KB 연결에 따라 자동 라우팅하고 테넌트 소유권을 검증합니다(검색 시 엔진 선택).
엔진 구현 참고
계층 구조: Repository → KVHybridRetrieveEngine → Composite → Registry
각 백엔드는 interfaces.RetrieveEngineRepository(EngineType() / Support() / Save / BatchSave / Retrieve / DeleteBy* / CopyIndices / BatchUpdateChunkEnabledStatus / BatchUpdateChunkTagID / EstimateStorageSize)를 구현합니다. 그 위에는 다음 계층이 있습니다.
- KVHybridRetrieveEngine(
retriever/keywords_vector_hybrid_indexer.go): Repository를RetrieveEngineService로 감싸고, Index 시 지원하는 검색 유형에 따라 embedding을 계산하여 기록합니다. - CompositeRetrieveEngine(
retriever/composite.go): 복합체 패턴입니다.Retrieve는 각RetrieveParams.RetrieverType(vector/keywords)을 해당 유형을 지원하는 첫 엔진으로 라우팅하여 동시에 실행합니다.Index/Delete/CopyIndices등의 쓰기 작업은 모든 구성 엔진에 브로드캐스트합니다. - RetrieveEngineRegistry(
retriever/registry.go): 이중 인덱스 레지스트리입니다.byEngineType은RETRIEVE_DRIVER환경 변수 기반 ‘env store’로 유형당 하나만 등록하며,byStoreID는 DBVectorStore테이블 기반 인스턴스 수준 등록으로 같은 엔진 유형의 여러 인스턴스(예: ES 클러스터 두 개)를 등록할 수 있습니다.
필요 시 재구성(rehydrate)
시작 시 특정 벡터 스토리지를 사용할 수 없으면(백엔드가 아직 시작되지 않았거나 일시적 네트워크 장애) byStoreID에 등록되지 않습니다. 이후 해당 store에 연결된 지식 베이스의 검색은 물론 삭제까지 계속 실패할 수 있습니다. 이를 해결하기 위해 레지스트리는 필요 시 재구성을 지원합니다.
GetOrLoadByStoreID가 찾지 못하면 주입된VectorStoreRepository+EngineFactory로 그 자리에서 엔진을 구성하고 등록합니다. 저장소나 팩터리 중 하나라도 nil이면 일반 조회로 동작합니다.- 단일 구성의 상한은
EngineBuildTimeout(10s)이며singleflight가 동시 요청을 하나의 구성으로 합칩니다. - 구성 실패 시
rebuildCooldown(30s) 대기 기간을 적용하여 백엔드 장애가 지속될 때 요청마다 전체 타임아웃을 헛되이 기다리지 않도록 합니다. storeGen세대 카운터로 경합을 방지합니다. 구성 시작 전에 값을 읽고 세대가 바뀌지 않았을 때만 결과를 공개하므로 구성 중 발생한 등록이나 삭제를 오래된 결과가 덮어쓰지 않습니다.
지식 베이스 삭제 시 엔진이 아직 준비되지 않았어도 바로 실패로 처리하지 않고 이 재구성 경로로 재시도합니다.
엔진 등록: initRetrieveEngineRegistry
internal/container/container.go. 시작 시 RETRIEVE_DRIVER(쉼표 구분)를 파싱하고 드라이버별 클라이언트를 구성하여 registry.Register(retriever.NewKVHybridRetrieveEngine(repo, engineType))로 등록합니다. 단일 드라이버 초기화 실패는 로그만 남기고 시작을 막지 않습니다. 이후 loadDBStoresIntoRegistry가 vector_stores 테이블에서 테넌트가 만든 벡터 스토리지 인스턴스를 읽고, createEngineServiceFromStore(engine_factory.go)로 엔진을 구성한 뒤 RegisterWithStoreID로 등록합니다.
검색 시 엔진 선택
검색 진입점 HybridSearch(knowledgebase_search.go)는 KB 연결 관계에 따라 엔진을 선택합니다.
resolveStoreGroups가 검색 대상 KB를(VectorStoreID, 소유 테넌트)별로 그룹화합니다.- 각 그룹에서
retriever.CreateRetrieveEngineForKB(factory.go)를 호출합니다.- KB에 store가 연결되지 않은 경우(
VectorStoreID가 빈 값, 현재 기본값) → 테넌트의GetRetrieverEngines()를 사용합니다. 테넌트에RetrieverEngines.Engines가 설정되어 있으면 이를 사용하고, 없으면GetDefaultRetrieverEngines()가RETRIEVE_DRIVER환경 변수로 생성합니다(internal/types/tenant.go). - KB에 store가 연결된 경우 → 먼저
ownership.StoreOwnedBy로 테넌트 소유권을 검증합니다(테넌트 간 탐색 방지, 실패 시ErrVectorStoreForbidden). 이후registry.GetByStoreID로 인스턴스를 가져오고(미등록 시ErrVectorStoreNotFound) 단일 구성원 Composite로 감쌉니다.
- KB에 store가 연결되지 않은 경우(
buildRetrievalParams가 엔진의SupportRetriever기능과 KB 유형에 따라 벡터/키워드 두 종류의RetrieveParams를 생성합니다(FAQ는 FAQ 벡터 인덱스만, 문서는 기본 벡터 인덱스 + 키워드 인덱스 사용).- 그룹이 여러 개이면
retrieveFromStores가 errgroup으로 동시 fan-out합니다(최대 4그룹, 그룹별 타임아웃MULTI_STORE_RETRIEVE_TIMEOUT_SEC, 기본 30s). 결과에 서로 다른 엔진 유형이 있으면 점수를 정규화합니다.
엔진별 상세 설명
엔진 유형 상수는 internal/types/retriever.go에 있습니다. postgres, elasticsearch, opensearch, qdrant, milvus, weaviate, doris, sqlite, tencent_vectordb입니다(infinity, elasticfaiss는 레거시 enum으로 배포 가능한 구현이 없음). 별도 설명이 없으면 모든 엔진의 Support()는 [keywords, vector] 두 유형을 반환합니다.
PostgreSQL(pgvector + ParadeDB) — 기본 엔진
internal/application/repository/retriever/postgres/repository.go. 업무 데이터와 같은 DB를 사용합니다(embeddings 테이블, GORM 관리).
- 벡터 검색: pgvector
halfvec(반정밀도, 차원당 2바이트).embedding열은 차원이 고정되지 않으며 HNSW 인덱스는(embedding::halfvec(dim)) halfvec_cosine_ops표현식에 생성합니다. ORDER BY 표현식이 인덱스 표현식과 정확히 같아야 하며(양쪽 명시적 cast), 아니면 순차 스캔으로 전환됩니다(소스 주석에서 pgvector issue #702/#835 인용). 쿼리는 서브쿼리로expandedTopK개(TopK*2, [100,200] 범위로 제한하여 큰 LIMIT가 HNSW를 압도하지 않게 함) 후보를 가져와distance = embedding <=> query를 계산하고,distance <= 1-threshold로 필터링하며score = 1 - distance를 사용합니다. 트랜잭션 안에서SET LOCAL hnsw.ef_search(≥40)와SET LOCAL hnsw.iterative_scan = strict_order(pgvector ≥ 0.8, 선택적 필터링 상황에서 후보를 계속 보충)를 설정합니다. 구버전에 GUC가 없으면 자동으로 기능을 낮춰 재시도합니다. - 키워드 검색: ParadeDB
pg_searchBM25 —content ||| query(아무 token이나 일치) +paradedb.score(id) as score. - 필터링:
knowledge_base_id/knowledge_id/tag_idIN 필터(AND 의미),is_enabled는 NULL 또는 true. - 인덱싱:
BatchSave+ON CONFLICT DO NOTHING. 삭제는 chunk/source/knowledge ID 기준 물리 삭제입니다.
SQLite(FTS5 + sqlite-vec) — 경량 단일 머신
internal/application/repository/retriever/sqlite/repository.go. 외부 의존성이 없는 완전 내장 방식입니다.
- 벡터 검색:
sqlite-vec확장(cgo bindings), 차원별 vec0 가상 테이블을 사용합니다.CREATE VIRTUAL TABLE ... USING vec0(embedding float[dim] distance_metric=cosine). 쿼리는WHERE v.embedding MATCH ?(직렬화된 질의 벡터)ORDER BY v.distance, 점수는score = 1 - distance입니다. 시작 시ensureExistingVecTables가 기존 데이터 차원에 맞춰 가상 테이블을 보완 생성합니다. - 키워드 검색: FTS5 contentless 테이블
lite_embeddings_fts. 쓰기 시 수동 bigram 토큰화(중국어에 적합)를 수행하고, 질의도sanitizeFTS5Query로 bigram화한 뒤MATCH합니다. - 필터링: 지식 베이스, 문서, 태그, 활성화 조건은
lite_embeddings기본 테이블에 적용됩니다. 벡터 경로는v.rowid IN (SELECT ... FROM lite_embeddings filtered WHERE ...)를 사용하여 top-k 선택 전에 필터링합니다. 전역 top-k를 먼저 구하고 필터링하면 범위 안의 유효한 일치를 검색하지 못할 수 있습니다. - 오류 전파: 어느 검색 경로든 실패하면 오류를 반환하여 상위 계층이 실패를 ‘검색은 성공했지만 일치 없음’으로 오인하지 않게 합니다.
- 임계값: 벡터 임계값 0은 모든 결과를 제거하는 것이 아니라 필터링하지 않는다는 뜻입니다.
- 데스크톱 / 개발 환경 / 극소규모 배포에 적합합니다.
Elasticsearch v8
internal/application/repository/retriever/elasticsearch/v8/repository.go. typed client와 단일 인덱스(ELASTICSEARCH_INDEX, 기본 WeKnora)를 사용하며 문서에는 dense_vector embedding 필드가 있습니다.
- 벡터 검색:
script_score쿼리, 스크립트cosineSimilarity(params.query_vector, 'embedding')사용(Lucene은 음수 최종 점수를 금지하므로 실제 값 범위 [0,1]). threshold 필터링은 애플리케이션 측에서 수행합니다. - 키워드 검색: content 필드의
match쿼리(BM25). - 필터링: bool filter(KB/knowledge/tag ID terms,
is_enabled는 must_not 역매칭을 사용하며 과거 데이터에 필드가 없으면 활성으로 간주). 시작 시 mapping을 확인하여 ID 필드의.keyword접미사 필요 여부를 결정합니다. - 인덱싱: Bulk API 일괄 쓰기, 빈 벡터는 거부합니다.
Elasticsearch v7 — 키워드 전용
internal/application/repository/retriever/elasticsearch/v7/repository.go. 주의: Support()는 [keywords]만 반환합니다. WeKnora에서 v7 드라이버는 BM25 키워드 엔진으로만 등록됩니다(코드에는 script_score cosineSimilarity 벡터 쿼리 구성이 남아 있지만 기능 선언에 vector가 없으므로 Composite는 벡터 요청을 이 엔진으로 라우팅하지 않음). 벡터 검색이 필요하면 다른 드라이버와 조합하거나(예: RETRIEVE_DRIVER=postgres,elasticsearch_v7) v8로 업그레이드해야 합니다.
OpenSearch
internal/application/repository/retriever/opensearch/(repository.go, retrieve.go, query.go, mapping.go, crud.go 등 여러 파일로 분리). 엔지니어링 구성이 가장 완전한 드라이버입니다.
- 버전 검사
probeVersion: ES 배포판과 OS 1.x / 2.0-2.3(Lucene HNSW 미리보기 버전)은 거부합니다. 2.4-2.10은 경고 후 허용하고, 2.11+ / 3.x는 경고 없이 허용합니다(주 테스트 버전 3.3.2).probeKNNPlugin은 모든 노드에opensearch-knn플러그인이 설치되어 있어야 합니다. - 벡터 검색: k-NN 플러그인의
knn쿼리(query.go buildKNNQuery). k-NN의COSINESIMILspace type은(1+cosine)/2를 반환하므로 본래 [0,1] 범위입니다. - 키워드 검색: content의
match(BM25). 하이브리드는 OS 네이티브 hybrid pipeline을 사용하지 않고 상위 RRF 융합으로 통일합니다(query.go주석에 명시). - 인덱싱:
mapping.go의 선언적 mapping(knn_vector필드에 method/engine 매개변수 포함), 시작 시 mapping 지문을 검증하고 차이가 있으면ErrConfigInvalid를 반환합니다. 별칭 관리 +copy.go로 reindex를 지원하며 인덱스 생성/재구축 이벤트는 AuditSink를 통해 감사 로그에 기록합니다. - 설정에는
OPENSEARCH_INSECURE_SKIP_VERIFY와 SSRF 안전 전송 계층(transport.go)이 포함됩니다.
Qdrant
internal/application/repository/retriever/qdrant/repository.go. gRPC 클라이언트(기본 포트 6334)를 사용합니다.
- 컬렉션 관리: 차원별 collection:
{QDRANT_COLLECTION|weknora_embeddings}_{dim}, Distance=Cosine. payload 필드(kb_id/knowledge_id/chunk_id/tag_id 등)에 keyword 인덱스를 만들고 content에는 다국어 tokenizer 전문 인덱스를 만듭니다. - 벡터 검색:
QueryAPI. score는 정규화된 벡터의 내적(≈cosine, IR embedding에서는 [0,1])이며 threshold는 score_threshold로 푸시다운합니다. - 키워드 검색:
tokenizeQuery로 로컬 토큰화 후 token마다MatchText(content, token)should(OR) 필터를 구성합니다.Scroll로 해당 차원의 모든 collection을 순회하여 결과를 가져옵니다. BM25 점수는 없습니다(일치하면 반환하고 점수는 상위 RRF의 rank로 결정). - 필터링:
getBaseFilter는MatchKeywords로 KB/knowledge/tag/is_enabled를 정확히 필터링합니다. - 설정:
QDRANT_HOST/QDRANT_PORT/QDRANT_API_KEY/QDRANT_USE_TLS.
Milvus
internal/application/repository/retriever/milvus/repository.go.
- 컬렉션 관리: 차원별 collection(
{MILVUS_COLLECTION|weknora_embeddings}_{dim}). schema에는 밀집 벡터embedding(HNSW 인덱스, M=16 efConstruction=128, metric은MILVUS_METRIC_TYPE으로 결정: 기본 IP / COSINE / L2)과 희소 벡터content_sparse가 있습니다. 희소 벡터는 내장 BM25 Function(entity.FunctionTypeBM25)이 content에서 자동 생성하며AutoIndex(BM25)를 사용합니다. 새 Collection의content는 Milvus 다국어 분석기를 사용합니다. 영어english, 중국어chinese(내장 Jieba), 알 수 없는 언어default(ICU)이며language필드로 분석기를 선택합니다. - 벡터 검색:
Search+WithANNSField(embedding). COSINE 모드 원시 범위는 [-1,1]이며(score+1)/2정규화가 필요한 유일한 엔진입니다. - 키워드 검색:
content_sparse에 BM25 희소 벡터 검색(Milvus 2.5+ 네이티브 전문 검색)을 수행하며, 질의 시 질문 텍스트의 언어에 따라analyzer_name을 전달합니다. - 필터링:
filter.go에서 불리언 표현식(kb/knowledge/tag/is_enabled)을 구성합니다. - 활성화 상태 동기화:
BatchUpdateChunkEnabledStatus는 collection별로 갱신하고 실패하면errors.Join으로 모아 warn만 남기는 대신 오류를 반환합니다. 인덱스 갱신이 조용히 실패하여 기본 DB에서 비활성화한 청크가 계속 검색되는 일이 있어서는 안 됩니다. - 설정:
MILVUS_ADDRESS/MILVUS_USERNAME/MILVUS_PASSWORD/MILVUS_DB_NAME/MILVUS_METRIC_TYPE(변경 후 collection 재구축 필요). 기존 Collection schema는 다국어 분석기로 직접 변경할 수 없습니다.go run ./cmd/milvus-migrate --source <기존접두사> --target <새접두사>를 실행하면 기존 밀집 벡터를 재사용하고 원본 Collection의 metric을 유지할 수 있습니다. 검색이 정상인지 확인한 뒤MILVUS_COLLECTION을 새 접두사로 변경합니다.
Weaviate
internal/application/repository/retriever/weaviate/repository.go. HTTP + gRPC 이중 채널입니다.
- 클래스 관리: Class를 동적으로 생성하며(
WEAVIATE_COLLECTION해석), ReplicationConfig / ShardingConfig를 지원합니다. - 벡터 검색: GraphQL
nearVector+WithCertainty(threshold). certainty =(2-distance)/2로 본래 [0,1]이며 임계값을 네이티브 푸시다운합니다. - 키워드 검색: GraphQL BM25 쿼리(
Bm25ArgBuilder). - 필터링: GraphQL where로 KB/knowledge/tag/is_enabled를 필터링합니다.
- 설정:
WEAVIATE_HOST/WEAVIATE_GRPC_ADDRESS/WEAVIATE_SCHEME/WEAVIATE_AUTH_ENABLED+WEAVIATE_API_KEY.
Apache Doris(4.1+)
internal/application/repository/retriever/doris/(repository.go 699줄 + schema.go + structs.go). MySQL 프로토콜로 FE(9030)에 연결하고 HTTP(8030)로 Stream Load를 수행합니다(SSRF 안전 클라이언트).
- 테이블 생성: 차원별 테이블(접두사
DORIS_TABLE_PREFIX|weknora_embeddings).schema.go가 DDL을 생성합니다. ANN 인덱스는 HNSW +inner_product(쓰기/질의 전 벡터 단위화로 cosine과 동등)이며 content 열에는 inverted 역색인을 만들고 chinese parser를 선언합니다(애플리케이션 측 토큰화 불필요). DDL 후 ANN 인덱스가 준비될 때까지 폴링합니다. - 호환 모드
DORIS_COMPAT_MODE:auto(감지) /inner_product_duplicate(DUPLICATE KEY 테이블 +inner_product_approximate) /legacy(1 - cosine_distance_approximate). 테이블 생성 후 상호 전환할 수 없습니다. - 벡터 검색:
inner_product_approximate(embedding, query)(단위화 후 cosine) 또는 legacy 공식, SQL LIMIT TopK. - 키워드 검색:
content MATCH_ANY ?로 역색인을 사용합니다. - 쓰기: DUPLICATE KEY 테이블에서 id별 명시적 delete + insert로 대체 의미를 유지합니다. enabled/tag 갱신은 Stream Load partial update로 수행합니다.
- 설정:
DORIS_ADDR/DORIS_HTTP_PORT/DORIS_DATABASE/DORIS_USERNAME/DORIS_PASSWORD/DORIS_TABLE_PREFIX/DORIS_COMPAT_MODE.
Tencent Cloud VectorDB
internal/application/repository/retriever/tencentvectordb/repository.go. RpcClient, EventualConsistency, 타임아웃 10s입니다.
- 컬렉션 관리: 차원별 collection(
{TENCENT_VECTORDB_COLLECTION|weknora_embeddings}_{dim}). 세 가지 인덱스: 밀집 벡터 HNSW+COSINE(M=16, efConstruction=200), 희소 벡터 SPARSE_INVERTED+IP(서버 측 BM25), 스칼라 FILTER 인덱스(id 기본 키 + content/source/chunk/knowledge/kb/tag 필터 필드). - 벡터 검색: Search COSINE, SDK 값 범위 [-1,1](IR embedding의 실제 범위 [0,1]).
- 키워드 검색: 로컬
encoder.SparseEncoder(BM25)가 질의를 희소 벡터로 인코딩하고sparse_vector필드에서 희소 검색을 수행합니다. 해당 차원의 모든 collection을 순회합니다. - 설정:
TENCENT_VECTORDB_ADDR/TENCENT_VECTORDB_USERNAME/TENCENT_VECTORDB_API_KEY/TENCENT_VECTORDB_DATABASE/TENCENT_VECTORDB_COLLECTION. 핵심 설정 세 개 중 하나라도 없으면 등록을 건너뜁니다.
Neo4j — 그래프 검색(Registry 체계 외부)
internal/application/repository/retriever/neo4j/repository.go는 벡터/키워드 엔진이 아니라 RetrieveGraphRepository(SearchNode(ctx, NameSpace, entities))를 구현합니다. NameSpace{KnowledgeBase, Knowledge}에 따라 엔터티 노드와 관계를 검색하여 chat pipeline의 ENTITY_SEARCH 단계(GraphRAG)에 제공합니다. NEO4J_ENABLE=true + NEO4J_URI/NEO4J_USERNAME/NEO4J_PASSWORD로 활성화합니다.
Embedding 차원 관리
WeKnora는 KB마다 차원이 다른 embedding 모델을 사용할 수 있습니다. 엔진별 차원 격리 전략:
| 엔진 | 전략 |
|---|---|
| PostgreSQL | 단일 embeddings 테이블에 혼합 저장, 행에 dimension 열 포함. HNSW는 embedding::halfvec(dim) 표현식에 생성하며 검색 시 WHERE dimension = ? + 같은 차원 cast로 해당 인덱스 사용 |
| SQLite | 차원별 vec0 가상 테이블(시작 시 기존 데이터 차원으로 자동 보완 생성) |
| Qdrant / Milvus / TencentVectorDB | 차원별 collection: {base}_{dim}. 최초 쓰기 시 ensureCollection으로 지연 생성(sync.Map에 생성한 차원 기억) |
| Doris | 차원별 테이블: {prefix}_{dim}. schema.go가 DDL 생성 후 ANN 인덱스 준비 상태 폴링 |
| Elasticsearch / OpenSearch | 단일 인덱스 dense_vector/knn_vector mapping(ELASTICSEARCH_INDEX / OPENSEARCH_INDEX), mapping에서 차원 고정 |
검색 측 일관성은 validateSameEmbeddingModel(knowledgebase_search_shared.go)이 보장합니다. 한 번의 다중 지식 베이스 검색에 참여하는 모든 KB는 동일한 embedding 모델 정체성(model.Name + BaseURL, 테넌트가 달라도 동등할 수 있음)을 공유해야 하며, 그렇지 않으면 거부합니다. 서로 다른 벡터 공간의 점수를 비교하는 일을 방지합니다. 질의 벡터는 모델 정체성별로 한 번만 계산하고(ResolveEmbeddingModelKeys + GetQueryEmbedding), params.QueryEmbedding으로 모든 store 그룹에 전달하여 embedding API 중복 호출을 방지합니다.
하이브리드 검색 점수와 정규화
엔진 간 벡터 점수 정규화(EngineAwareNormalizer)
internal/application/service/retriever/normalizer.go. 여러 store로 fan-out하고 결과에 다른 엔진 유형이 섞이면(hasMixedEngineTypes) 엔진별 벡터 점수를 공통 [0,1] 범위로 매핑합니다.
| 엔진 | 원시 값 범위 | 정규화 |
|---|---|---|
| Milvus(COSINE) | [-1, 1] 원시 cosine | (score + 1) / 2 후 clamp01 |
| Elasticsearch v8 | [0, 1](Lucene script_score 비음수 불변식) | 그대로 clamp01 |
| OpenSearch | [0, 1](k-NN COSINESIMIL이 이미 (1+cos)/2 적용) | 그대로 clamp01 |
| Weaviate | [0, 1](certainty 정의가 (2-distance)/2) | 그대로 clamp01 |
| Postgres / SQLite / Qdrant / TencentVectorDB / Doris | 이론상 [-1,1], IR 정규화 embedding은 실제 [0,1] | 그대로 clamp01 |
| 알 수 없는 엔진 | — | clamp01 대체 + 요청당 WARN 한 번 |
키워드(BM25) 점수는 정규화하지 않습니다. 값에 상한이 없어 압축하면 긴 꼬리가 무너집니다. 후속 RRF는 rank 기반이므로 척도 차이에 영향을 받지 않습니다. clamp01은 NaN/Inf도 처리하여 후속 정렬의 엄격 약순서 불변식을 보호합니다. 같은 엔진 내부 결과는 원래 척도를 유지합니다(직접 비교할 수 있으므로 불필요한 변환 없음).
RRF 가중 융합
knowledgebase_search_fusion.go. 벡터와 키워드 두 경로에 모두 결과가 있을 때:
// fuseWithRRF
rrfScore = vectorWeight/(rrfK + vectorRank) + keywordWeight/(rrfK + keywordRank)- rank는 각 경로 결과의 1부터 시작하는 순위입니다(각 엔진이 이미 점수순으로 정렬하여 반환).
rrfK,vectorWeight,keywordWeight는 테넌트RetrievalConfig에서 가져옵니다(GetEffectiveRRFK/GetEffectiveRRFWeights가 기본값 제공).- 단일 경로에만 결과가 있으면 RRF를 사용하지 않고
deduplicateByScore가 chunk별 최고 원시 점수를 유지합니다. FAQ embedding 유사도의 의미에 중요합니다. 예를 들어FAQDirectAnswerThreshold는 이 점수를 직접 비교합니다.
융합 후 복합 점수(리랭킹 모델 점수 0.6 + 검색 기본 점수 0.3 + 출처 가중치 0.1, MMR, FAQ/Wiki 가중)는 chat pipeline의 CHUNK_RERANK 단계에서 계산합니다. 검색 질의응답 흐름의 리랭킹 단계를 참고하세요.
검색 실행 데이터 흐름
구현 참고
| 단계 | 소스 코드 위치 |
|---|---|
| 엔진 등록(env + DB store) | internal/container/container.go(initRetrieveEngineRegistry), engine_factory.go |
| 레지스트리 / 복합 엔진 / 팩터리 | internal/application/service/retriever/(registry.go, composite.go, factory.go, normalizer.go) |
| 엔진별 구현 | internal/application/repository/retriever/{postgres,sqlite,elasticsearch,opensearch,qdrant,milvus,weaviate,doris,tencentvectordb,neo4j} |
| 하이브리드 검색 디스패치와 융합 | internal/application/service/knowledgebase_search*.go |
| 엔진 유형 상수 | internal/types/retriever.go |
| 테넌트 기본 엔진 | internal/types/tenant.go(GetDefaultRetrieverEngines) |
| 환경 변수 목록 | .env.example(C1절), docker-compose.yml |