Milvus vs Elasticsearch and OpenSearch for vector search
Milvus vs Elasticsearch is a question of what each system was built for: Milvus is an open-source database built for vector search, while Elasticsearch and OpenSearch are Lucene search engines that added vector fields. All three handle BM25 and hybrid search, so the real differences show up at scale: throughput, latency, index choice and cost. Choose Milvus when vector search is the core workload (RAG, semantic search, agents); keep Elasticsearch or OpenSearch when logs and analytics come first.
- Apache License 2.0
- 40K+ GitHub stars
- Built-in BM25 full-text search
- HNSW, IVF, RaBitQ, DiskANN and GPU indexes
- VTS migrates from Elasticsearch
- Salesforce
- Walmart
- NVIDIA
- eBay
- Shell
- Roblox
- IBM
Milvus vs Elasticsearch performance: which is faster for vector search?
Elasticsearch vs Milvus on the VectorDBBench leaderboard: Zilliz Cloud and self-hosted Milvus serve several times the QPS of OpenSearch and Elastic Cloud at a fraction of the P99 latency, for plain vector search and for filtered search. Elasticsearch vector search performance improves with force_merge, which is why the labels below carry it.
- Zilliz CloudZillizCloud-8cu-perf13,316.23 QPSP99 2 ms · recall 0.9383
- MilvusMilvus-16c64g-sq4u-fp16-force_merge9,575.69 QPSP99 2.3 ms · recall 0.9189
- OpenSearchOpenSearch-16c128g-force_merge3,055.01 QPSP99 7.2 ms · recall 0.9066
- Elastic CloudElasticCloud-8c60g-force_merge2,353.89 QPSP99 17.1 ms · recall 0.9143
- Zilliz CloudZillizCloud-8cu-perf-partition_key10,020.53 QPSP99 2.6 ms · recall 0.9788
- MilvusMilvus-16c64g-sq8-partition_key11,280.08 QPSP99 1.6 ms · recall 0.9507
- OpenSearchOpenSearch-16c128g-routing-64shard-force_merge3,064.63 QPSP99 6.5 ms · recall 0.9507
- Elastic CloudElasticCloud-8c60g-routing-64shard2,789.72 QPSP99 8.2 ms · recall 0.9538
Zilliz Cloud is fully managed Milvus: the same engine and top-tier throughput, with no clusters to size, upgrade or scale.
Try Zilliz Cloud freeMilvus vs Elasticsearch vs OpenSearch at a glance: license, community, clients and limits
What is the main difference between Elasticsearch and Milvus for vector search? Elasticsearch and OpenSearch build an HNSW graph inside every Lucene segment on JVM data nodes, and Elastic's tuning guidance is to keep most vector data in memory. Milvus loads segments onto query nodes from object storage and evicts cold data with tiered storage, so memory is sized for the hot set rather than the whole corpus.
| Milvus | Elasticsearch | OpenSearch | |
|---|---|---|---|
| License | Apache-2.0; LF AI & Data graduate project | ELv2, SSPL or AGPLv3 | Apache-2.0; OpenSearch Software Foundation (Linux Foundation) |
| GitHub stars | 40K+ | 70K+ | 10K+ |
| Written in | Go control plane, C++ query engine | Java (Lucene) | Java (Lucene) |
| Max vector dimensions | 32,768 | 4,096 (dense_vector) | 16,000 (knn_vector) |
| Hybrid search in the free build | hybrid_search with RRFRanker or WeightedRanker | RRF retriever listed under the Enterprise subscription | Search pipeline with normalization or RRF processors |
| Clients and frameworks | Python, Java, Go, Node.js, C#, C++, Rust, REST; langchain-milvus, LlamaIndex, Haystack, Dify | REST and official language clients; langchain-elasticsearch | REST and official language clients |
| Deployment | Lite (pip), Standalone (one container), Distributed (Kubernetes), Zilliz Cloud | Self-managed or Elastic Cloud | Self-managed or Amazon OpenSearch Service |
Which vector index types do Milvus, Elasticsearch and OpenSearch support?
Elasticsearch builds HNSW or flat indexes per Lucene segment, with int8, int4 and BBQ quantization. OpenSearch adds Faiss IVF and an on-disk mode. Milvus ships a wider menu, including GPU_CAGRA, GPU_IVF_FLAT, GPU_IVF_PQ and GPU_BRUTE_FORCE, plus scalar indexes for filters, so you can trade memory, recall and latency per field.
| Index family | Milvus | Elasticsearch | OpenSearch |
|---|---|---|---|
| Graph (HNSW) | HNSW, HNSW_SQ, HNSW_PQ, HNSW_PRQ | hnsw, int8_hnsw, int4_hnsw, bbq_hnsw | hnsw on Faiss or Lucene engine |
| Cluster-based / quantized | IVF_FLAT, IVF_SQ8, IVF_PQ, IVF_RABITQ, SCANN | flat, int8_flat, int4_flat, bbq_flat | Faiss ivf; sq, pq and binary quantization |
| Disk-based | DISKANN; tiered storage from object storage | bbq_disk (Enterprise subscription) | on_disk mode with compression levels and rescoring |
| Sparse and BM25 | SPARSE_INVERTED_INDEX (BM25 or IP metric) | sparse_vector (ELSER), Lucene BM25 | Neural sparse search, Lucene BM25 |
| Binary vectors | BIN_FLAT, BIN_IVF_FLAT, MINHASH_LSH | bit element type with hnsw or flat | Binary quantization |
| Scalar and JSON filters | INVERTED, BITMAP, STL_SORT, Trie, NGRAM, RTREE, JSON path index | Lucene field indexes set by mapping | Lucene field indexes set by mapping |
What does a purpose-built vector database add beyond vector search?
Elasticsearch vector search vs Milvus is a question of what the system is built around. Elasticsearch is strong when documents already live in an index. Milvus treats the collection schema as the product: typed vectors, JSON and arrays, built-in BM25 and hybrid search in the open-source build, and an architecture that scales each node role on its own.
| Capability | In Milvus |
|---|---|
| Multi-tenancy | Database (64 by default), collection (65,536 by default), partition (1,024 per collection), or partition key for millions of tenants |
| JSON and dynamic fields | JSON fields with JSON path indexes; a dynamic field stores undeclared keys |
| ARRAY fields | Typed arrays, ARRAY_CONTAINS filters, BITMAP or INVERTED indexes |
| Multi-vector hybrid search | Up to 10 vector fields; hybrid_search fuses dense, sparse and BM25 with RRF or weighted rerankers |
| BM25 full-text search | A BM25 function with standard, english, chinese, ICU or custom analyzers; query with raw text |
| Keyword and phrase match | TEXT_MATCH and PHRASE_MATCH (with slop) on analyzed VARCHAR fields |
| Pattern filters | LIKE and RE2 regex (=~, !~), accelerated by an NGRAM index |
| Partial update and TTL | upsert with partial_update=True; collection.ttl.seconds expires entities |
| Storage-compute separation | Query, streaming and data nodes over shared object storage, scaled independently |
| Tiered storage | Hot data loads on demand and cold data is evicted, so query nodes hold less in memory |
How do Elasticsearch and OpenSearch fields map to a Milvus schema?
Most mappings translate one to one. Text fields become analyzed VARCHAR with a BM25 function, so keyword relevance survives the move. Milvus docs also map the query DSL: match to full-text search, term to == or TEXT_MATCH, prefix and wildcard to LIKE, knn to search, and RRF to hybrid_search.
| Elasticsearch / OpenSearch | Milvus |
|---|---|
| Index | Collection (inside a database) |
| _id | VARCHAR or INT64 primary key; keep the same ID for dual-write |
| dense_vector / knn_vector | FLOAT_VECTOR (or FLOAT16, BFLOAT16, INT8) with the same dimension and metric |
| text with analyzer | VARCHAR with enable_analyzer + BM25 function to SPARSE_FLOAT_VECTOR |
| keyword | VARCHAR with INVERTED or BITMAP index; NGRAM for wildcard |
| Numeric and date | INT64 / DOUBLE with STL_SORT or INVERTED index |
| keyword arrays | ARRAY of VARCHAR |
| object, nested, flattened | JSON field with JSON path indexes, or the dynamic field |
| sparse_vector | SPARSE_FLOAT_VECTOR with IP metric |
| Custom routing per tenant | Partition key |
How do you migrate from Elasticsearch or OpenSearch to Milvus?
Move the retrieval path first and keep the old cluster serving until results match. VTS (Vector Transport Service) lists Elasticsearch as a source connector; for OpenSearch, export with the scroll API and load files with BulkWriter.
- Export: VTS es_to_milvus, or scroll / point-in-time reads of _source.
- Create the schema: map fields, add a BM25 function, index filter fields.
- Backfill with BulkWriter and bulk_import; use insert for small sets.
- Dual-write new documents to both systems from the same source.
- Validate recall and relevance on a query set, then shift reads gradually.
Milvus or Elasticsearch: how to choose, and when Elasticsearch fits
A fair call: if search is a side feature of an existing Elastic stack, adding vectors there can be enough. Operations differ too. Elasticsearch means JVM heap and shard sizing; Milvus Standalone is one container with embedded etcd, and Distributed runs on Kubernetes with the Milvus Operator. Cost follows memory: Elasticsearch holds HNSW graphs on data nodes, Milvus can leave cold segments in object storage.
| Your situation | Pick |
|---|---|
| Vector search is the product: RAG, semantic search, agents | Milvus |
| Logs, observability or security analytics on Kibana or OpenSearch Dashboards | Elasticsearch or OpenSearch |
| Aggregation-heavy text analytics with vectors as a small add-on | Elasticsearch or OpenSearch |
| An existing ELK pipeline and a modest vector corpus | Elasticsearch or OpenSearch |
| Corpus growing past what fits in data-node memory | Milvus with tiered storage or DiskANN |
| Millions of tenants in one collection | Milvus partition key |
| BM25 plus vector fusion in an Apache-2.0 build | Milvus or OpenSearch |
| Both needs at once | Elasticsearch for analytics, Milvus for retrieval, joined by document ID |
How do you map an Elasticsearch index to a Milvus collection in Python?
This script reads every document from an Elasticsearch index and writes it into a Milvus collection with dense, BM25, keyword, array and JSON fields.
# pip install -U pymilvus elasticsearch
from elasticsearch import Elasticsearch, helpers
from pymilvus import MilvusClient, DataType, Function, FunctionType
es = Elasticsearch("http://localhost:9200")
client = MilvusClient(uri="http://localhost:19530")
# ES mapping: title(text) category(keyword) tags(keyword[]) attrs(object) embedding(dense_vector, 768)
schema = client.create_schema(enable_dynamic_field=True)
schema.add_field("id", DataType.VARCHAR, is_primary=True, max_length=64) # ES _id
schema.add_field("title", DataType.VARCHAR, max_length=8192, enable_analyzer=True, enable_match=True)
schema.add_field("title_bm25", DataType.SPARSE_FLOAT_VECTOR) # filled by the BM25 function
schema.add_field("category", DataType.VARCHAR, max_length=128)
schema.add_field("tags", DataType.ARRAY, element_type=DataType.VARCHAR, max_capacity=64, max_length=128)
schema.add_field("attrs", DataType.JSON, nullable=True)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=768)
schema.add_function(Function(name="title_bm25_fn", function_type=FunctionType.BM25,
input_field_names=["title"], output_field_names=["title_bm25"]))
idx = client.prepare_index_params()
idx.add_index(field_name="embedding", index_type="HNSW", metric_type="COSINE",
params={"M": 16, "efConstruction": 200})
idx.add_index(field_name="title_bm25", index_type="SPARSE_INVERTED_INDEX", metric_type="BM25")
idx.add_index(field_name="category", index_type="INVERTED")
idx.add_index(field_name="attrs", index_type="INVERTED", index_name="attrs_brand",
params={"json_path": 'attrs["brand"]', "json_cast_type": "varchar"})
client.create_collection("docs", schema=schema, index_params=idx)
batch = []
for hit in helpers.scan(es, index="docs", query={"query": {"match_all": {}}}):
src = hit["_source"]
batch.append({"id": hit["_id"], "title": src["title"], "category": src["category"],
"tags": src.get("tags", []), "attrs": src.get("attrs"),
"embedding": src["embedding"]})
if len(batch) == 1000:
client.insert("docs", batch)
batch = []
if batch:
client.insert("docs", batch)
Match dim and metric_type to your dense_vector mapping. For large indexes, write files with BulkWriter and load them with bulk_import instead of insert. Code requires a running server and your own embedding model; it is an example, not a benchmark.
Explore the concepts
- How should you benchmark vector search performance?
- What is full-text search, and how does BM25 fit?
- What is hybrid search, and when should you use it?
- How does a sparse inverted index differ from dense ANN?
- How do HNSW_SQ, HNSW_PQ, and HNSW_PRQ change the trade-offs?
- What is IVF_RABITQ, and how should you evaluate it?
- When should you use DISKANN in Milvus?
- How do bulk migration, backup, and Milvus CDC differ?
Frequently asked questions
Is Elasticsearch a vector database?
You can use Elasticsearch as a vector database, but it is a search engine with vector fields: dense_vector stores embeddings and the knn query searches HNSW or flat indexes per Lucene segment. Milvus is a database built around vector indexes, with BM25 and filtering added on the same collection.
How much faster is Milvus than Elasticsearch for vector search?
On the VectorDBBench Cohere 1M test, Milvus-16c64g-sq4u-fp16-force_merge lists 9,575.69 QPS at 0.9189 recall and ElasticCloud-8c60g-force_merge 2,353.89 QPS at 0.9143. Hardware differs by row; benchmark your own data.
Can Milvus replace Elasticsearch for BM25 full-text search?
For retrieval, yes: a BM25 function turns analyzed text into a sparse vector at insert time, and you query with raw text, alone or in hybrid search. Kibana dashboards and aggregation-heavy analytics stay with Elasticsearch.
How does hybrid search work in Elasticsearch vs Milvus?
Elasticsearch fuses BM25 and kNN with the reciprocal rank fusion (RRF) retriever, which Elastic lists under the Enterprise subscription. OpenSearch uses search-pipeline processors. Milvus runs hybrid_search with RRFRanker or WeightedRanker in the open-source build.
How do I migrate vector data from Elasticsearch to Milvus?
Map dense_vector to FLOAT_VECTOR with the same dimension and metric, then copy data with VTS or a scroll export plus BulkWriter. Dual-write during cutover and compare recall before moving reads.
Can I use Elasticsearch and a vector database together?
Yes, and it is common: keep Elasticsearch for logs and analytics, move vector and hybrid retrieval to Milvus, feed both from the same source, and join results by document ID.
Want the benchmarked results without running a cluster?
Zilliz Cloud is fully managed Milvus. On the VectorDBBench Cohere 1M test, ZillizCloud-8cu-perf lists 13,316.23 QPS at 0.9383 recall, with the same MilvusClient API as open-source Milvus.
Sources and technical documentation (51)
- https://zilliz.com/vdbbench-leaderboard
- https://github.com/zilliztech/VectorDBBench
- https://github.com/zilliztech/VectorDBBench/blob/main/vectordb_bench/results/leaderboard_v2.json
- https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector
- https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/sparse-vector
- https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion
- https://www.elastic.co/subscriptions
- https://docs.opensearch.org/latest/mappings/supported-field-types/knn-methods-engines/
- https://docs.opensearch.org/latest/vector-search/optimizing-storage/disk-based-vector-search/
- https://docs.opensearch.org/latest/vector-search/optimizing-storage/binary-quantization/
- https://docs.opensearch.org/latest/vector-search/ai-search/hybrid-search/rrf/
- https://docs.opensearch.org/latest/search-plugins/search-pipelines/normalization-processor/
- https://github.com/milvus-io/milvus
- https://milvus.io/docs/index-explained.md
- https://milvus.io/docs/ivf-rabitq.md
- https://milvus.io/docs/scann.md
- https://milvus.io/docs/bitmap.md
- https://milvus.io/docs/ngram.md
- https://milvus.io/docs/json-indexing.md
- https://milvus.io/docs/multi_tenancy.md
- https://milvus.io/docs/use-json-fields.md
- https://milvus.io/docs/enable-dynamic-field.md
- https://milvus.io/docs/array_data_type.md
- https://milvus.io/docs/multi-vector-search.md
- https://milvus.io/docs/full-text-search.md
- https://milvus.io/docs/analyzer-overview.md
- https://milvus.io/docs/keyword-match.md
- https://milvus.io/docs/phrase-match.md
- https://milvus.io/docs/basic-operators.md
- https://milvus.io/docs/upsert-entities.md
- https://milvus.io/docs/set-collection-ttl.md
- https://milvus.io/docs/architecture_overview.md
- https://milvus.io/docs/tiered-storage-overview.md
- https://milvus.io/docs/prepare-source-data.md
- https://milvus.io/docs/import-data.md
- https://milvus.io/docs/elasticsearch-queries-to-milvus.md
- https://github.com/zilliztech/vts
- https://milvus.io/docs/quickstart.md
- https://www.elastic.co/pricing/faq/licensing
- https://www.elastic.co/docs/deploy-manage/production-guidance/optimize-performance/approximate-knn-search
- https://opensearch.org/faq/
- https://www.linuxfoundation.org/press/linux-foundation-announces-opensearch-software-foundation
- https://github.com/elastic/elasticsearch
- https://github.com/opensearch-project/OpenSearch
- https://docs.langchain.com/oss/python/integrations/vectorstores/milvus
- https://docs.langchain.com/oss/python/integrations/vectorstores/elasticsearch
- https://milvus.io/docs/integrations_overview.md
- https://milvus.io/docs/install-overview.md
- https://milvus.io/docs/install_standalone-docker.md
- https://milvus.io/docs/limitations.md
- https://lfaidata.foundation/projects/