본문으로 바로 가기

검색 엔진과 벡터 스토리지(Retrieval Engines)

검색 엔진은 인덱스를 저장하고 벡터, 키워드 또는 하이브리드 검색을 실행합니다. 기본 PostgreSQL 배포는 ParadeDB 이미지로 pgvector와 BM25를 제공하며 업무 데이터와 데이터베이스를 공유할 수 있습니다. 독립적인 확장이나 기존 인프라 재사용이 필요하면 다른 백엔드를 선택할 수 있습니다.

백엔드를 선택할 때는 배포 의존성, 용량, 데이터 격리 요구 사항을 고려합니다.

상황고려 사항
내장 데이터베이스를 사용하는 단일 머신 또는 데스크톱 배포SQLite(내장, 외부 의존성 없음)
벡터 검색 서비스를 독립적으로 확장해야 함Qdrant, Milvus
회사에 Elasticsearch / OpenSearch 스택이 이미 있음기존 클러스터 재사용
지식 베이스별로 데이터를 분리 저장해야 함기본 엔진을 유지하면서 ‘설정 → 벡터 스토리지’에 별도 인스턴스를 등록하고 지정한 지식 베이스에 연결

엔진을 전환하려면 인덱스를 재구축해야 합니다. 지식 베이스에 연결한 벡터 스토리지는 생성 후 변경할 수 없으므로 생성 전에 인스턴스를 결정해야 합니다.

기능 매트릭스와 선택 비교

엔진RETRIEVE_DRIVER 값벡터 검색키워드/전문키워드 점수중국어 토큰화차원 관리임계값 푸시다운배포 복잡도적합한 시나리오
PostgreSQLpostgrespgvector halfvec + HNSW 표현식 인덱스ParadeDB BM25(|||)BM25(paradedb.score)ParadeDB tokenizer단일 테이블에 차원 혼합, 표현식 인덱스에서 차원별 castSQL 내 거리 임계값낮음(기본 이미지 내장)기본 선택, 업무 데이터와 동일 DB, 트랜잭션 일관성
SQLitesqlitesqlite-vec vec0(cosine)FTS5 contentlessFTS5애플리케이션 측 bigram차원별 vec0 가상 테이블애플리케이션 측매우 낮음(내장)데스크톱 / 개발 / 초소형 배포
Elasticsearch v8elasticsearch_v8script_score cosineSimilaritymatch(BM25)BM25ES analyzerdense_vector 단일 인덱스애플리케이션 측중간기존 ES 8 클러스터
Elasticsearch v7elasticsearch_v7미지원(Support는 keywords만)match(BM25)BM25ES analyzer중간기존 ES 7, 키워드 엔진으로만 사용하며 다른 벡터 엔진과 조합 필요
OpenSearchopensearchk-NN 플러그인 knn(HNSW)match(BM25)BM25OS analyzerknn_vector 선언적 mapping + 지문 검증k-NN 네이티브중간감사/별칭/reindex가 필요한 프로덕션 ES 계열, 버전 2.11+/3.x
Qdrantqdrant네이티브 HNSW Cosine전문 인덱스 MatchText(token OR)점수 없음(Scroll 일치 즉시 반환, RRF rank 사용)다국어 tokenizer차원별 collectionscore_threshold 네이티브중간벡터 중심이며 payload 필터가 필요한 시나리오
MilvusmilvusHNSW(IP/COSINE/)BM25 Function 희소 벡터BM25Milvus analyzer차원별 collection애플리케이션 측중상대규모 벡터, 네이티브 BM25 하이브리드 검색 필요
WeaviateweaviatenearVector(certainty)네이티브 BM25BM25Weaviate tokenizer동적 Classcertainty 네이티브중간GraphQL 생태계, 복제/샤딩 설정 필요
DorisdorisANN HNSW inner_product/cosine역색인 MATCH_ANY역색인 일치테이블 생성 시 chinese parser 선언차원별 테이블SQL 내높음기존 Doris 데이터 웨어하우스, 검색과 분석 통합
Tencent Cloud VectorDBtencent_vectordbHNSW COSINE희소 벡터 BM25(SPARSE_INVERTED)BM25SDK SparseEncoder차원별 collection애플리케이션 측낮음(클라우드 관리형)Tencent Cloud 관리형, 운영 부담 없음

설명: 엔진 자체가 ‘하이브리드 검색’을 제공하는지와 관계없이 WeKnora는 항상 상위 계층의 통합 RRF 융합(knowledgebase_search_fusion.go)을 사용합니다. 벡터와 키워드를 각각 독립적으로 검색한 후 rank에 따라 가중 병합합니다(하이브리드 검색 점수와 정규화 참고). 따라서 각 엔진은 두 종류의 단일 모드 검색만 각각 제공하면 됩니다.

설정 방법 요약

핵심 스위치(.env.example C1절, docker-compose.yml):

환경 변수기본값설명
RETRIEVE_DRIVERpostgres여러 드라이버를 쉼표로 구분: postgres / sqlite / elasticsearch_v7 / elasticsearch_v8 / opensearch / qdrant / milvus / weaviate / doris / tencent_vectordb. 여러 드라이버를 쓰면 쓰기 작업은 전체에 브로드캐스트하고 검색은 유형별 라우팅
MULTI_STORE_RETRIEVE_TIMEOUT_SEC30여러 store 병렬 검색의 그룹별 타임아웃
ELASTICSEARCH_ADDR / _USERNAME / _PASSWORD / _INDEX— / WeKnoraES v7/v8 공용
OPENSEARCH_ADDR / _USERNAME / _PASSWORD / _INDEX / _INSECURE_SKIP_VERIFYOpenSearch
QDRANT_HOST / _PORT / _COLLECTION / _API_KEY / _USE_TLSlocalhost / 6334 / weknora_embeddingsQdrant(gRPC 포트)
MILVUS_ADDRESS / _COLLECTION / _METRIC_TYPE / _USERNAME / _PASSWORD / _DB_NAMElocalhost:19530 / weknora_embeddings / IPmetric 변경 후 collection 재구축 필요
WEAVIATE_HOST / _GRPC_ADDRESS / _SCHEME / _AUTH_ENABLED / _API_KEY / _COLLECTIONweaviate:8080 / weaviate:50051 / http컨테이너 안에서는 서비스 이름 사용
DORIS_ADDR / _HTTP_PORT / _DATABASE / _USERNAME / _PASSWORD / _TABLE_PREFIX / _COMPAT_MODEdoris-fe:9030 / 8030 / weknora / root / — / weknora_embeddings / autoDoris 4.1+, 테이블 생성 후 compat 모드 상호 전환 불가
TENCENT_VECTORDB_ADDR / _USERNAME / _API_KEY / _DATABASE / _COLLECTION핵심 3개 항목 중 하나라도 없으면 등록 건너뜀
NEO4J_ENABLE / NEO4J_URI / _USERNAME / _PASSWORDfalse / 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): 이중 인덱스 레지스트리입니다. byEngineTypeRETRIEVE_DRIVER 환경 변수 기반 ‘env store’로 유형당 하나만 등록하며, byStoreID는 DB VectorStore 테이블 기반 인스턴스 수준 등록으로 같은 엔진 유형의 여러 인스턴스(예: 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))로 등록합니다. 단일 드라이버 초기화 실패는 로그만 남기고 시작을 막지 않습니다. 이후 loadDBStoresIntoRegistryvector_stores 테이블에서 테넌트가 만든 벡터 스토리지 인스턴스를 읽고, createEngineServiceFromStore(engine_factory.go)로 엔진을 구성한 뒤 RegisterWithStoreID로 등록합니다.

검색 시 엔진 선택

검색 진입점 HybridSearch(knowledgebase_search.go)는 KB 연결 관계에 따라 엔진을 선택합니다.

  1. resolveStoreGroups가 검색 대상 KB를 (VectorStoreID, 소유 테넌트)별로 그룹화합니다.
  2. 각 그룹에서 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로 감쌉니다.
  3. buildRetrievalParams가 엔진의 SupportRetriever 기능과 KB 유형에 따라 벡터/키워드 두 종류의 RetrieveParams를 생성합니다(FAQ는 FAQ 벡터 인덱스만, 문서는 기본 벡터 인덱스 + 키워드 인덱스 사용).
  4. 그룹이 여러 개이면 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_search BM25 — content ||| query(아무 token이나 일치) + paradedb.score(id) as score.
  • 필터링: knowledge_base_id / knowledge_id / tag_id IN 필터(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의 COSINESIMIL space 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 전문 인덱스를 만듭니다.
  • 벡터 검색: Query API. score는 정규화된 벡터의 내적(≈cosine, IR embedding에서는 [0,1])이며 threshold는 score_threshold로 푸시다운합니다.
  • 키워드 검색: tokenizeQuery로 로컬 토큰화 후 token마다 MatchText(content, token) should(OR) 필터를 구성합니다. Scroll로 해당 차원의 모든 collection을 순회하여 결과를 가져옵니다. BM25 점수는 없습니다(일치하면 반환하고 점수는 상위 RRF의 rank로 결정).
  • 필터링: getBaseFilterMatchKeywords로 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. 벡터와 키워드 두 경로에 모두 결과가 있을 때:

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

WeKnora v0.8.0 소스 코드 기반 한국어 번역 · MIT License