Utiliser votre base de données vectorielle comme un datastore JSON (ou relationnel)
TL;DR : Les bases de données vectorielles prennent en charge les opérations CRUD sur des formats de données « traditionnels » tels que JSON. Si vous êtes un développeur solo ou une petite équipe et que vous ne voulez pas gérer de nombreux éléments différents d’infrastructure de données, vous pouvez utiliser Milvus ou Zilliz Cloud (le Milvus managé) comme unique magasin de données et migrer facilement les collections sans vecteurs vers différentes bases de données à mesure que vous passez à l’échelle.
Portée par la popularité de ChatGPT et d’autres modèles de langage autorégressifs, la recherche vectorielle a explosé en popularité au cours de l’année passée. En conséquence, nous avons vu de nombreuses entreprises et organisations monter dans le train de la recherche vectorielle, des fournisseurs de bases de données NoSQL tels que MongoDB (via Atlas Vector Search) aux bases de données relationnelles traditionnelles telles que Postgres (via pgvector). Le message général que j’entends autour de ces plugins de recherche vectorielle est largement le même et ressemble à ceci : les développeurs devraient rester avec nous puisque vous pouvez stocker des tables/du JSON en plus des vecteurs, il n’est donc pas nécessaire de gérer plusieurs éléments d’infrastructure !
Ce genre de déclaration me fait toujours rire, car elle est clairement conçue par des équipes marketing peu sophistiquées. Non seulement la technologie derrière la recherche vectorielle est très différente des stratégies de stockage et d’interrogation dans les bases de données relationnelles et NoSQL, mais il est désormais assez bien connu que les bases de données vectorielles peuvent stocker des relations, des documents JSON et d’autres sources de données structurées. Le premier point est difficile à illustrer de manière concise sans connaissances préalables approfondies des systèmes de gestion de bases de données, mais le second point est assez facile à montrer avec quelques courts extraits de code. C’est à cela que cet article de blog est consacré.
Configuration
Milvus stocke les données dans des unités appelées collections, analogues aux tables dans les bases de données relationnelles. Chaque collection peut avoir son propre schéma, et les schémas ont un champ vectoriel de dimensionnalité fixe, par exemple 768 pour les embeddings vectoriels basés sur e5-base. Créons une collection pour stocker des documents JSON plutôt que des données vectorielles. Pour mieux illustrer ce point, j’ai omis certaines des étapes précédentes et suivantes, comme l’appel à 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()
Ici, nous avons spécifié que cette collection utilise les capacités de schéma dynamique de Milvus, permettant à la collection d’accepter des charges utiles JSON comme données « supplémentaires » associées à chaque ligne.
J’espère que vous n’en attendiez pas davantage, parce que c’est tout - c’est vraiment aussi simple que ça.
Opérations CRUD
Vous pouvez interagir avec la collection que nous avons créée ci-dessus comme vous le feriez avec n’importe quelle autre base de données. À titre d’exemple, téléchargeons la liste des sénateurs américains actuels (au format JSON) depuis govtrack :
import requests
r = requests.get("https://www.govtrack.us/api/v2/role?current=true&role_type=senator")
data = r.json()["objects"]
len(data)
100
Nous pouvons stocker ces documents directement dans Milvus avec juste un tout petit peu de préparation des données :
rows = [{"_unused": [0]} | d for d in data]
collection.insert(rows)
(insert count: 100, delete count: 0, upsert count: 0, ...
À partir de là, nous pouvons effectuer des requêtes directement sur ces données :
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}]
Sans spécifier de champ vectoriel, nous avons interrogé notre base de données pour obtenir le premier résultat où le champ party dans la charge utile JSON associée a "Dem" (démocrate) comme préfixe.
Cette étape démontre, espérons-le, la capacité à effectuer des recherches de données structurées dans Milvus, mais ce n'est pas une requête particulièrement utile. Trouvons tous les sénateurs de mon État d'origine, l'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}]
Même si nous avons spécifié limit=10, seuls deux documents ont été renvoyés (puisque chaque État n'a que deux sénateurs). Affinons encore davantage notre requête pour obtenir uniquement le sénateur 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}]
Peut-être aimerais-je mettre à jour le profil du sénateur Ron Wyden avec un peu plus d'informations. Nous pouvons le faire facilement en récupérant le document entier avec output_fields=["*"], en mettant à jour le document obtenu, puis en le réinsérant dans notre base de données sans l'ancienne clé primaire :
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"]
(insert count: 1, delete count: 0, upsert count: 0, ...
collection.delete(expr)
collection.insert(res)
Voyons si cela a fonctionné comme prévu.
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}]
Les données correspondent bien aux mises à jour que nous avons effectuées.
Le script complet
Voici le script complet du début à la fin, par commodité, sans les requêtes superflues. J’ai utilisé notre offre gratuite Zilliz Cloud au lieu de milvus-lite :
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}]
Essayez par vous-même !
pymongo -> milvusmongo
Une dernière remarque avant de conclure : j’ai créé un petit package Python appelé milvusmongo qui implémente les fonctionnalités CRUD les plus basiques de pymongo sur plusieurs collections. Il utilise Milvus comme base de données sous-jacente plutôt que MongoDB. Comme pymongo, milvusmongo prend en charge l’accès de type dictionnaire et de type attribut, et masque la logique supplémentaire nécessaire aux appels CRUD (c.-à-d. insert_one, update_one et delete_one). Vous pouvez l’installer avec pip install milvusmongo :
% pip install milvus
% pip install milvusmongo
Une fois cela fait, vous pouvez démarrer une instance Milvus intégrée et l’utiliser conjointement avec milvusmongo pour effectuer des requêtes sur des données JSON. Par exemple :
from milvus import default_server
default_server.start()
from milvusmongo import MongoClient
client = MongoClient("127.0.0.1", 19530)
client.insert_one(my_document)
Veuillez noter que cette bibliothèque est destinée à démontrer la flexibilité de Milvus en tant que datastore plutôt qu’à servir de solution que vous devriez utiliser dans des environnements de production à grande échelle.
Mot de la fin
Milvus n’est pas là pour remplacer les bases de données NoSQL ou les moteurs de recherche textuelle lexicale ; nous sommes là pour vous offrir la meilleure expérience possible de recherche vectorielle/filtrée. Plus important encore, nous sommes là pour aider à accélérer l’adoption de la recherche vectorielle en tant que technologie - c’est pourquoi l’open source est une partie si essentielle de notre éthique.
Mais cela ne signifie pas que nous ne prenons pas en charge d’autres types de données. En tant que développeur solo ou petite startup, vous êtes libre d’utiliser Milvus comme votre seul magasin de données. Vous pourrez toujours optimiser l’utilisation de votre infrastructure plus tard, à mesure que vous grandirez. Milvus vous fournira des capacités de recherche vectorielle de premier ordre, tandis que d’autres bases de données sont là pour stocker, indexer et rechercher d’autres formes de données. Une fois que votre application commencera à nécessiter des charges de travail plus complexes (telles que des jointures ou des agrégations), c’est à ce moment-là que vous voudrez envisager d’utiliser différents magasins de données.
Continuer à lire

VDBBench Adds Cost-Aware Benchmarking for Vector Databases
Compare Zilliz Cloud, Pinecone, and turbopuffer with VDBBench cost-aware vector database benchmarks across latency, freshness, multitenancy, and cold starts.

Why I’m Against Claude Code’s Grep-Only Retrieval? It Just Burns Too Many Tokens
Learn how vector-based code retrieval cuts Claude Code token consumption by 40%. Open-source solution with easy MCP integration. Try claude-context today.

Vector Databases vs. Graph Databases
Use a vector database for AI-powered similarity search; use a graph database for complex relationship-based queries and network analysis.



