Files

62 lines
4.0 KiB
Markdown

# vector-stores
Companion code for [Choosing a Vector Store for Spring AI: pgvector vs Redis vs Qdrant vs Elasticsearch](https://ankurm.com/spring-ai-2-0-vector-store-comparison-pgvector-redis-qdrant-elasticsearch/), part of the [Spring AI series](../README.md) on ankurm.com.
The same 30,000-document, 384-dimension dataset goes through Spring AI's `VectorStore` API into four real stores, each left on the library's defaults. Ingest time, recall@10 against an exact brute-force answer, latency, metadata filtering and the cost of running each one are measured by one test class, and every figure in the article is quoted from a file in [`output/`](output).
The dataset is **synthetic** (clustered unit vectors, looked up by [`LookupEmbeddingModel`](src/main/java/com/ankurm/vectorstores/LookupEmbeddingModel.java)), so no API key is needed and all four stores receive identical vectors. It measures the *stores*, not an embedding model: recall on real embeddings will differ, particularly for Elasticsearch's quantised default.
## Versions
| Component | Version |
|---|---|
| Spring Boot | 4.1.1 |
| Spring AI | 2.0.1 |
| Java | 25 (Temurin 25.0.4.1) |
| PostgreSQL / pgvector | 16.15 / **0.8.7**, built from the `v0.8.7` tag (the Ubuntu package is 0.6.0, which has no iterative scan) |
| Redis Stack | 7.4.0-v8 tarball (Redis 7.4.7, RediSearch 2.10.20) |
| Qdrant | 1.19.2 (single binary; Java client 1.18.0) |
| Elasticsearch | 9.5.5 single node, security off (Java client 9.4.5 from the Spring AI BOM) |
| Jedis | 7.4.1 |
## Quickstart (no Docker)
```bash
scripts/services-up.sh # pgvector (apt + source build), Redis Stack, Qdrant, Elasticsearch -- see the article for the download URLs
scripts/run-all.sh # runs the 11 tests and regenerates output/01 .. 12
```
A store whose port is closed is skipped, not failed. The four services together need about 2.5 GB of RAM.
## What's here
| File | What it shows |
|---|---|
| [`Dataset.java`](src/main/java/com/ankurm/vectorstores/Dataset.java) | The synthetic vectors, metadata and the exact top-k used as the answer key |
| [`LookupEmbeddingModel.java`](src/main/java/com/ankurm/vectorstores/LookupEmbeddingModel.java) | An `EmbeddingModel` that looks vectors up, so every store gets the same ones |
| [`StoreFactory.java`](src/main/java/com/ankurm/vectorstores/StoreFactory.java) | The four stores built by hand with library defaults, and why `afterPropertiesSet()` is called |
| [`StoreComparisonTest.java`](src/test/java/com/ankurm/vectorstores/StoreComparisonTest.java) | Every measurement; each test writes its own transcript |
| [`src/broken/Redis1xStyle.java`](src/broken/Redis1xStyle.java) | Not compiled by the build; `scripts/capture-1x-compile.sh` compiles it against 2.0.1 |
## Output files
| File | Written by |
|---|---|
| `01-ingest.txt` .. `03-filtered.txt` | `ingestTimes`, `unfilteredRecallAndLatency`, `filteredRecallAndLatency` |
| `04-defaults-that-matter.txt` | `settingsThatSurprise` (pgvector `ef_search`, the Elasticsearch mapping, Qdrant HNSW config) |
| `05-elasticsearch-num-candidates.txt` | `elasticsearchCandidatesNotQuantisation` |
| `06-redis-ef-runtime.txt` | `redisEfRuntime` |
| `07-pgvector-ef-search.txt` | `pgvectorEfSearchAndFilters` |
| `08-qdrant-payload-index-and-threshold.txt` | `qdrantPayloadIndexAndIndexingThreshold` |
| `09-redis-undeclared-metadata.txt` | `redisFilterWithoutDeclaredSchema` |
| `10-elasticsearch-index-options.txt` | `elasticsearchIndexOptions` |
| `11-ops-footprint.txt` | `operationalFootprint` |
| `12-redis-1x-compile.txt` | `scripts/capture-1x-compile.sh` |
| `13-es-disk-watermark.txt` | captured once by hand: Elasticsearch refusing to allocate shards on a disk above 90% (not regenerated by `run-all.sh`) |
Timings drift between runs (this is a 2-vCPU sandbox with all four services sharing it); the recall figures and the shape of every comparison do not.
## Not covered
Concurrent query load, very large datasets, replication, hybrid (text + vector) search, and any embedding model's real recall.