Neo4j에 벡터 저장이 되냐
된다. Neo4j 5.x부터 벡터 인덱스를 네이티브(native, 기본 내장) 지원한다. 그래프 DB 전용이 아니라는 뜻.
-- 노드에 임베딩 벡터를 속성으로 저장
CREATE (e:ORG {name: "삼성전자", embedding: [0.12, 0.34, ...]})
-- 벡터 인덱스 생성
CREATE VECTOR INDEX entity_embedding
FOR (e:ORG) ON e.embedding
OPTIONS {indexConfig: {`vector.dimensions`: 1536}}
-- 코사인 유사도로 검색
CALL db.index.vector.queryNodes("entity_embedding", 5, $query_vector)즉 Neo4j 하나 안에서 두 가지가 동시에 된다.
- 그래프 탐색 (Cypher, 홉 기반)
- 벡터 유사도 검색 (코사인 유사도)
그런데 왜 별도 벡터 DB를 같이 쓰냐
현실에서는 Neo4j 단독이 아니라 별도 벡터 DB와 조합하는 경우가 많다.
| Neo4j 단독 | Neo4j + 별도 pgvector | |
|---|---|---|
| 관리 편의성 | 한 곳에서 관리 | DB 두 개 운영 |
| 벡터 성능 | 준수, 전문 DB보다는 느림 | pgvector가 더 최적화됨 |
| 선택 기준 | 그래프 탐색이 주 목적 | 벡터 검색 비중이 높을 때 |
이미 pgvector + PGroonga 하이브리드 구조가 있다면, 거기에 Neo4j를 그래프 레이어로 얹는 아키텍처가 가장 자연스럽다. 기존 검색 스택을 버리지 않고 그래프 능력만 추가하는 경로다.
Neo4j가 벡터 저장을 지원하는데 왜 다른 벡터 DB를 쓰나
핵심은 한국어 처리와 BM25 지원이다. pgvector + PGroonga 조합에서 PGroonga가 하는 건 형태소 분석(morphological analysis, 문장을 어근/조사 같은 최소 의미 단위로 쪼개는 작업) 기반 한국어 전문 검색인데, Neo4j는 이걸 못한다.
| 기능 | Neo4j | pgvector + PGroonga |
|---|---|---|
| 벡터 유사도 (HNSW) | O | O |
| 영어 키워드 검색 | O (Lucene 기반) | O |
| 한국어 형태소 분석 | X | O (PGroonga) |
| BM25 스코어링 | X | O |
| 벡터+키워드 점수 통합 | 직접 구현 필요 | 비교적 쉬움 |
한국어 도메인이면 Neo4j 단독으로는 키워드 검색 품질이 떨어진다. 이 이유 하나만으로도 한국어 서비스는 Neo4j + pgvector 조합이 사실상 강제된다.
벡터로 진입점 찾으면 NER이 필요 없어지냐
아니다. 역할이 완전히 다르다.
NER → 인덱싱(빌드) 타임에 필요
벡터 → 쿼리(검색) 타임에 필요
NER(Named Entity Recognition, 개체명 인식)은 그래프를 만들 때 쓴다. 문서에서 엔티티와 엣지를 추출해서 Neo4j에 노드로 저장하는 작업. 이게 없으면 그래프 자체가 존재하지 않는다.
벡터 검색은 이미 만들어진 그래프에서 어느 노드로 진입할지 찾는 것.
빌드 타임: 문서 → NER → 노드/엣지 생성 → Neo4j 저장
쿼리 타임: 사용자 질문 → 벡터화 → HNSW로 진입점 탐색 → 그래프 확장
NER이 없으면 탐색할 그래프 자체가 없다. 두 기술은 포함 관계가 아니라 “같은 파이프라인의 다른 단계”다.
[빌드 타임]
문서 청크
↓
LLM에게 던짐:
"이 텍스트에서 엔티티와 관계를 추출해라"
↓
LLM 반환: 엔티티 + 관계 JSON
{"entity1": "이재용", "relation": "CEO_OF", "entity2": "삼성전자"}
↓
노드 생성: 이재용(PER), 삼성전자(ORG)
엣지 생성: -[CEO_OF]->
Neo4j 저장
[쿼리 타임]
사용자 질문 → 벡터화
↓
HNSW(Hierarchical Navigable Small World, 계층적 근사 최근접 이웃 알고리즘)로 유사한 진입 노드 탐색
↓
그 노드에서 N홉 확장
↓
결과 → LLM → 최종 답변
벡터로 진입점 찾기의 장점
1. LLM이 모르는 엔티티를 벡터가 찾아준다
사용자가 오타, 약칭, 유사 표현으로 질문하면 LLM이 Cypher를 생성해도 그래프에서 못 찾는다.
LLM은 Cypher를 만들 수 있어도, 그래프 안에 노드가 "삼성전자 DS부문"으로 저장돼 있는지 "삼성 반도체"로 저장돼 있는지 모른다. 그래프 내부 상태를 LLM이 볼 수 없으니까.
벡터 진입점 탐색은 “그래프 안에 실제로 존재하는 노드”를 먼저 확정하고 그 ID를 Cypher에 넘기는 게 핵심이다.
사용자: "삼성 반도체 정책 알려줘"
LLM 생성: MATCH (n:ORG {name: "삼성 반도체"})...
그래프에 저장된 노드 이름: "삼성전자 DS부문"
→ 정확히 일치하지 않으니 결과 없음
벡터 검색은 이름이 달라도 의미가 비슷하면 노드를 찾아낸다.
"삼성 반도체" 벡터 → 코사인 유사도 → "삼성전자 DS부문" 노드 발견
→ 이 노드 ID를 Cypher에 넘김
→ 정확한 탐색 가능
한마디 요약
Neo4j 단독으로도 벡터는 되지만, 한국어 형태소 분석과 BM25가 필요하면 pgvector + PGroonga가 필요하고, NER과 벡터는 각각 “빌드 타임”과 “쿼리 타임” 담당이라 대체재가 아니라 보완재다.