벡터 데이터베이스를 JSON(또는 관계형) 데이터스토어로 사용하기
요약: 벡터 데이터베이스는 JSON과 같은 "전통적인" 데이터 형식에 대한 CRUD를 지원합니다. 혼자 개발하거나 소규모 팀이고 여러 데이터 인프라 구성요소를 관리하고 싶지 않다면, Milvus 또는 Zilliz Cloud(관리형 Milvus)를 유일한 데이터스토어로 사용하고, 규모가 커짐에 따라 벡터가 없는 컬렉션을 다른 데이터베이스로 쉽게 마이그레이션할 수 있습니다.
ChatGPT 및 기타 자기회귀 언어 모델의 인기에 힘입어, 벡터 검색은 지난 1년 동안 폭발적인 인기를 얻었습니다. 그 결과 MongoDB와 같은 NoSQL 데이터베이스 제공업체(Atlas Vector Search를 통해)부터 Postgres와 같은 전통적인 관계형 데이터베이스(pgvector를 통해)에 이르기까지 많은 회사와 조직이 벡터 검색 열풍에 올라타는 것을 보았습니다. 이러한 벡터 검색 플러그인과 관련해 제가 듣는 일반적인 메시지는 대체로 동일하며, 대략 다음과 같습니다: 벡터 외에도 테이블/JSON을 저장할 수 있으므로 여러 인프라 구성요소를 관리할 필요가 없으니 개발자들은 우리를 계속 사용해야 한다!
이런 종류의 발언은 항상 저를 웃게 만드는데, 세련되지 못한 마케팅 팀이 만든 것이 분명하기 때문입니다. 벡터 검색의 기반 기술은 관계형 및 NoSQL 데이터베이스의 저장 및 쿼리 전략과 크게 다를 뿐만 아니라, 이제 벡터 데이터베이스가 관계, JSON 문서 및 기타 구조화된 데이터 소스를 저장할 수 있다는 것도 꽤 잘 알려져 있습니다. 첫 번째 요점은 데이터베이스 관리 시스템에 대한 깊은 사전 지식 없이는 간결하게 설명하기 어렵지만, 두 번째 요점은 짧은 샘플 코드 조각 몇 개로 꽤 쉽게 보여줄 수 있습니다. 이 블로그 게시물은 바로 그것을 위해 작성되었습니다.
설정하기
Milvus는 관계형 데이터베이스의 테이블과 유사한 컬렉션이라는 단위로 데이터를 저장합니다. 각 컬렉션은 자체 스키마를 가질 수 있으며, 스키마에는 고정 차원의 벡터 필드가 있습니다. 예를 들어 e5-base 기반 벡터 임베딩의 경우 768입니다. 벡터 데이터가 아니라 JSON 문서를 저장할 컬렉션을 만들어 보겠습니다. 이 점을 더 잘 설명하기 위해 collections.connect 호출과 같은 앞뒤의 일부 단계는 생략했습니다:
from pymilvus import Collection, CollectionSchema, DataType, FieldSchema
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True, max_length=100),
FieldSchema(name="_unused", dtype=DataType.FLOAT_VECTOR, dim=1)
]
schema = CollectionSchema(fields, "NoSQL data", enable_dynamic_field=True)
collection = Collection("json_data", schema)
collection.load()
여기서는 이 컬렉션이 Milvus 동적 스키마 기능을 사용하도록 지정하여, 컬렉션이 각 행에 연결된 "추가" 데이터로 JSON 페이로드를 받아들일 수 있게 했습니다.
더 많은 것을 기대하지 않으셨길 바랍니다. 그게 전부니까요 - 정말 이렇게 간단합니다.
CRUD 작업
위에서 만든 컬렉션은 다른 데이터베이스와 마찬가지로 상호작용할 수 있습니다. 예를 들어 govtrack에서 현재 미국 상원의원 목록(JSON 형식)을 다운로드해 보겠습니다:
import requests
r = requests.get("https://www.govtrack.us/api/v2/role?current=true&role_type=senator")
data = r.json()["objects"]
len(data)
100
약간의 데이터 정리만으로 이러한 문서를 Milvus에 직접 저장할 수 있습니다:
rows = [{"_unused": [0]} | d for d in data]
collection.insert(rows)
(insert count: 100, delete count: 0, upsert count: 0, ...
여기서부터 해당 데이터에 대해 직접 쿼리를 수행할 수 있습니다:
collection.query(
expr="party like 'Dem%'",
limit=1,
output_fields=["person"]
)
[{'person': {'bioguideid': 'C000127',
'birthday': '1958-10-13',
'cspanid': 26137,
'fediverse_webfinger': None,
'firstname': 'Maria',
'gender': 'female',
'gender_label': 'Female',
'lastname': 'Cantwell',
'link': 'https://www.govtrack.us/congress/members/maria_cantwell/300018',
'middlename': '',
'name': 'Sen. Maria Cantwell [D-WA]',
'namemod': '',
'nickname': '',
'osid': 'N00007836',
'pvsid': None,
'sortname': 'Cantwell, Maria (Sen.) [D-WA]',
'twitterid': 'SenatorCantwell',
'youtubeid': 'SenatorCantwell'},
'id': 447376465724036249}]
벡터 필드를 지정하지 않고, 함께 제공되는 JSON 페이로드의 party 필드가 "Dem"(Democrat)을 접두사로 갖는 첫 번째 결과를 데이터베이스에서 쿼리했습니다.
이 단계는 Milvus에서 구조화된 데이터 검색을 수행할 수 있는 기능을 보여주기를 바라지만, 특별히 유용한 쿼리는 아닙니다. 제 고향인 Oregon 주의 모든 상원의원을 찾아보겠습니다:
collection.query(
expr="state in ['OR']",
limit=10,
output_fields=["person"]
)
[{'person': {'bioguideid': 'M001176',
'birthday': '1956-10-24',
'cspanid': 1029842,
'fediverse_webfinger': None,
'firstname': 'Jeff',
'gender': 'male',
'gender_label': 'Male',
'lastname': 'Merkley',
'link': 'https://www.govtrack.us/congress/members/jeff_merkley/412325',
'middlename': '',
'name': 'Sen. Jeff Merkley [D-OR]',
'namemod': '',
'nickname': '',
'osid': 'N00029303',
'pvsid': None,
'sortname': 'Merkley, Jeff (Sen.) [D-OR]',
'twitterid': 'SenJeffMerkley',
'youtubeid': 'SenatorJeffMerkley'},
'id': 447376465724036286},
{'person': {'bioguideid': 'W000779',
'birthday': '1949-05-03',
'cspanid': 1962,
'fediverse_webfinger': None,
'firstname': 'Ron',
'gender': 'male',
'gender_label': 'Male',
'lastname': 'Wyden',
'link': 'https://www.govtrack.us/congress/members/ron_wyden/300100',
'middlename': '',
'name': 'Sen. Ron Wyden [D-OR]',
'namemod': '',
'nickname': '',
'osid': 'N00007724',
'pvsid': None,
'sortname': 'Wyden, Ron (Sen.) [D-OR]',
'twitterid': 'RonWyden',
'youtubeid': 'senronwyden'},
'id': 447376465724036331}]
limit=10을 지정했지만, 반환된 문서는 두 개뿐이었습니다(각 주에는 상원의원이 두 명뿐이기 때문입니다). senior 상원의원만 얻도록 쿼리를 더 좁혀보겠습니다:
collection.query(
expr="state in ['OR'] and senator_rank in ['senior']",
limit=10,
output_fields=["person"]
)
[{'person': {'bioguideid': 'W000779',
'birthday': '1949-05-03',
'cspanid': 1962,
'fediverse_webfinger': None,
'firstname': 'Ron',
'gender': 'male',
'gender_label': 'Male',
'lastname': 'Wyden',
'link': 'https://www.govtrack.us/congress/members/ron_wyden/300100',
'middlename': '',
'name': 'Sen. Ron Wyden [D-OR]',
'namemod': '',
'nickname': '',
'osid': 'N00007724',
'pvsid': None,
'sortname': 'Wyden, Ron (Sen.) [D-OR]',
'twitterid': 'RonWyden',
'youtubeid': 'senronwyden'},
'id': 447376465724036331}]
아마 저는 Ron Wyden 상원의원의 프로필을 조금 더 많은 정보로 업데이트하고 싶을 수 있습니다. output_fields=["*"]로 전체 문서를 가져오고, 결과 문서를 업데이트한 뒤, 이전 기본 키 없이 데이터베이스에 다시 삽입하면 쉽게 할 수 있습니다:
expr = "state in ['OR'] and senator_rank in ['senior']"
res = collection.query(
expr=expr,
limit=10,
output_fields=["*"]
)
res[0].update({
"elected_in": "1996",
"college": "Stanford University",
"college_major": "Political Science"
})
del res[0]["id"]
(삽입 수: 1, 삭제 수: 0, 업서트 수: 0, ...
collection.delete(expr)
collection.insert(res)
이것이 예상대로 작동했는지 확인해 봅시다.
collection.query(
expr=expr,
limit=10,
output_fields=["elected_in", "college", "college_major"]
)
[{'elected_in': '1996',
'college': 'Stanford University',
'college_major': 'Political Science',
'id': 447376465724036353}]
데이터가 실제로 우리가 만든 업데이트와 일치합니다.
전체 스크립트
불필요한 쿼리 없이 편의를 위해 처음부터 끝까지 전체 스크립트를 제공합니다. 저는 milvus-lite 대신 Zilliz Cloud 무료 티어를 사용했습니다:
from milvus import default_server
from pymilvus import connections
from pymilvus import Collection, CollectionSchema, DataType, FieldSchema
import requests
# Uncomment this if you're using `milvus-lite`
#default_server.start()
#connections.connect(host="127.0.0.1", port=default_server.listen_port)
# Uncomment this if you're using Zilliz Cloud
connections.connect(
uri=os.environ["ZILLIZ_URI"],
token=os.environ["ZILLIZ_TOKEN"]
)
# Create the schema for our new collection. We set turn on Milvus' dynamic
# schema capability in order to store arbitratily large (or small) JSON blogs
# in each row.
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True, max_length=100),
FieldSchema(name="_unused", dtype=DataType.FLOAT_VECTOR, dim=1)
]
schema = CollectionSchema(fields, "Milvus as a JSON datastore", enable_dynamic_field=True)
# Now let's creat the collection.
collection = Collection("json_data", schema)
index_params = {
"index_type": "AUTOINDEX",
"metric_type": "L2",
"params": {}
}
collection.create_index(
field_name="_unused",
index_params=index_params
)
collection.load()
# Insert US senator JSON data into our newly formed collection.
r = requests.get("https://www.govtrack.us/api/v2/role?current=true&role_type=senator")
data = r.json()["objects"]
rows = [{"_unused": [0]} | d for d in data]
collection.insert(rows)
# Fetch the first Democrat in the database
top_k = 1
collection.query(
expr="state in ['OR'] and senator_rank in ['senior']",
limit=top_k,
output_fields=["person"]
)
[{'person': {'bioguideid': 'W000779',
'birthday': '1949-05-03',
'cspanid': 1962,
'fediverse_webfinger': None,
'firstname': 'Ron',
'gender': 'male',
'gender_label': 'Male',
'lastname': 'Wyden',
'link': 'https://www.govtrack.us/congress/members/ron_wyden/300100',
'middlename': '',
'name': 'Sen. Ron Wyden [D-OR]',
'namemod': '',
'nickname': '',
'osid': 'N00007724',
'pvsid': None,
'sortname': 'Wyden, Ron (Sen.) [D-OR]',
'twitterid': 'RonWyden',
'youtubeid': 'senronwyden'},
'id': 447376465724036331}]
직접 시도해 보세요!
pymongo -> milvusmongo
마무리하기 전에 마지막으로 한 가지 말씀드리자면, 저는 컬렉션 전반에서 pymongo의 가장 기본적인 CRUD 기능을 구현하는 milvusmongo라는 작은 Python 패키지를 만들었습니다. 이 패키지는 MongoDB가 아니라 Milvus를 기반 데이터베이스로 사용합니다. pymongo와 마찬가지로 milvusmongo는 딕셔너리 및 속성 스타일 접근을 모두 지원하며, CRUD 호출(즉, insert_one, update_one, delete_one)에 필요한 추가 로직을 추상화합니다. pip install milvusmongo로 설치할 수 있습니다:
% pip install milvus
% pip install milvusmongo
완료되면 내장 Milvus 인스턴스를 시작하고 milvusmongo와 함께 사용하여 JSON 데이터에 대한 쿼리를 수행할 수 있습니다. 예를 들어:
from milvus import default_server
default_server.start()
from milvusmongo import MongoClient
client = MongoClient("127.0.0.1", 19530)
client.insert_one(my_document)
이 라이브러리는 대규모 프로덕션 환경에서 사용해야 하는 무언가로 제공되기보다는, 데이터스토어로서 Milvus의 유연성을 보여주기 위한 것임을 참고해 주세요.
마무리 말씀
Milvus는 NoSQL 데이터베이스나 어휘 기반 텍스트 검색 엔진을 대체하기 위해 존재하는 것이 아닙니다. 저희는 가능한 최고의 벡터/필터링 검색 경험을 제공하기 위해 존재합니다. 더 중요하게는, 저희는 기술로서 벡터 검색의 채택을 가속화하는 데 도움을 주기 위해 존재합니다 - 이것이 오픈 소스가 저희 정신의 핵심적인 부분인 이유입니다.
하지만 그렇다고 해서 저희가 다른 유형의 데이터를 지원하지 않는다는 뜻은 아닙니다. 1인 개발자나 소규모 스타트업이라면 Milvus를 유일한 데이터 저장소로 자유롭게 사용할 수 있습니다. 성장하면서 나중에 언제든 인프라 사용을 최적화할 수 있습니다. Milvus는 동급 최고의 벡터 검색 기능을 제공할 것이며, 다른 데이터베이스는 다른 형태의 데이터를 저장하고, 인덱싱하고, 검색하기 위해 존재합니다. 애플리케이션이 조인이나 집계와 같은 더 복잡한 워크로드를 필요로 하기 시작하면, 그때 서로 다른 데이터 저장소 사용을 고려해 보는 것이 좋습니다.
계속 읽기

How Zilliz Ended Up at the Center of NVIDIA’s Unstructured Data Story at GTC 2026
If unstructured data is the context of AI, then the ceiling of AI applications will be set not just by models, but by how mature the infrastructure for unstructured data becomes.

How to Improve Retrieval Quality for Japanese Text with Sudachi, Milvus/Zilliz, and AWS Bedrock
Learn how Sudachi normalization and Milvus/Zilliz hybrid search improve Japanese RAG accuracy with BM25 + vector fusion, AWS Bedrock embeddings, and practical code examples.

Milvus 2.6.x Now Generally Available on Zilliz Cloud, Making Vector Search Faster, Smarter, and More Cost-Efficient for Production AI
Milvus 2.6.x is now GA on Zilliz Cloud, delivering faster vector search, smarter hybrid queries, and lower costs for production RAG and AI applications.



