Vector search in MySQL: an early look at HNSW and custom indexes in VillageSQL
MySQL 9.x added the VECTOR data type so you can store an embedding. You get a VECTOR column, functions to convert to and from text, and VECTOR_DIM() to ask how wide a vector is.
What you don't get is a way to compare two vectors. The 9.x community server has no distance function and no vector index. MySQL 8.4 has none of this at all. You can't add the missing index yourself either because vanilla MySQL's list of index types is fixed to B-tree, R-tree, hash, and full-text.
That's where VillageSQL comes in. VillageSQL is the innovation platform for MySQL that adds an extension framework (similar to PostgreSQL's extension framework) to enable permissionless innovation. Instead of waiting for a feature to be implemented in a few years in a future version of MySQL, new functionality can be dynamically added to a version of MySQL you run today.
VillageSQL already supports custom functions and custom data types, and we've been working on custom indexes. With custom indexes, an extension can define a whole index type including how it's stored in InnoDB, how it's built as rows arrive, and how it's searched. The server treats the custom index as a first-class index, the same as the built-in index types. The server even plans queries against the custom index and reports it by name.
Vector search is a key feature for AI-era applications, so that's the first use case for custom indexes we are building.
At Percona Live in Amsterdam on September 9, 2026, we demoed an early build of this vector work (not yet merged). Below is the annotated demo.
Start with a server
The demo starts by installing VillageSQL. You copy one line from the website, paste it into a terminal, and you have a prebuilt server.
curl -fsSL https://install.villagesql.com | bash
That gets you a stable server. Please note the vector work highlighted below runs on an unmerged, development build.
Declaring a vector index
With the server up, the demo installs the vsql-vector extension with an ordinary INSTALL EXTENSION, and then creates one table:
CREATE TABLE demo_vectors (
id INT PRIMARY KEY,
embedding SVECTOR(3) NOT NULL,
INDEX idx_embedding (embedding hnsw_l2) USING EXTENDED(hnsw)
) ENGINE=InnoDB;
USING EXTENDED(hnsw) names a custom index type and SVECTOR is a custom column type. The extension supplies both. The server stores the metadata, plans the query, and hands the work off. Once the table exists, the vectors arrive by plain INSERT.
Does the server use it?
You can ask the server whether it will really use that index:
EXPLAIN FORMAT=TREE SELECT id FROM demo_vectors
ORDER BY l2_distance(embedding, '[1.0,2.0,3.0]') LIMIT 3\G
The plan comes back as Custom index distance scan on idx_embedding, which is the server saying it would answer from the HNSW graph instead of reading every row. Running that same SELECT without EXPLAIN in front of it returns the three nearest rows, closest first.
Fast and correct
The demo then builds two tables that hold the same 20,000 vectors. One table has an HNSW index on the vector column and the other has no index at all, and the same nearest-neighbor query runs against both.
Across 50 queries each, the indexed table averages 0.19 ms and the full scan 3.82 ms, which is 20x slower. This is synthetic data at 3 dimensions, so read it as a mechanism check rather than a benchmark.
Speed alone does not prove an index is beneficial, because an approximate index can be fast by being wrong. The unindexed table holds the same vectors, so it gives the exact answer to compare against. Here the index returns nine of the true top ten, and the tenth row is the price of an approximate search. ef_search is a dial that allows you to tune accuracy and performance.
Where it lands on real embeddings
We ran ann-benchmarks against fashion-mnist-784 on an 8 vCPU Xeon VM. Below are our initial results. When vector indexes are merged, we’ll publish steps to reproduce these benchmark results.
fashion-mnist-784 — 60,000 vectors, k=10, 8 vCPU Xeon
| ef_search | queries/sec | recall@10 |
|---|---|---|
| 50 | 635 | 99.68% |
| 100 | 440 | 99.85% |
| 200 | 290 | 99.92% |
These numbers are a snapshot of the work which is still in progress. They come from one early build, and we expect them to move. The shape of the curve is the interesting part. Each step up in ef_search buys recall and costs throughput. Going from 50 to 100 adds 0.17 points of recall and gives up about 30% of the queries per second. Going to 200 adds another 0.07 points and costs about half. You can choose the point on that curve that your application needs.
How it fits together
The server treats the extension's index as a real one. Ask what indexes a table has, and the vector ones come back as HNSW, sitting next to an ordinary BTREE. SHOW CREATE TABLE (from a different example table with two vector columns) returns the full definition:
KEY `idx_title` (`title_vec` `vsql_vector`.`hnsw_cosine`)
USING EXTENDED(`vsql_vector`.`hnsw`) WITH (`ef_construction` = 64, `m` = 8),
KEY `idx_body` (`body_vec` `vsql_vector`.`hnsw_l1`),
KEY `idx_tag` (`tag`)
M and ef_construction are the extension's own build knobs. The server stores them without knowing what they mean, and prints them back in DDL you can replay. The vectors themselves sit in InnoDB's own storage, so there's no shadow table and no second write on insert. Columns go up to 3,072 dimensions (for comparison, pgvector supports 2,000 dimensions for an indexed column), and one table can carry several vector indexes, each on its own column with its own distance function.
What's next
Filtered search, deletes, wider version coverage, and comparison benchmarks are all still ahead. We expect to deliver the custom index framework for both 8.4 and 9.7 codebases.
This is an initial build of unreleased work, and we wanted to show it while it's still moving. If you want to read the code in the meantime, the extension is at github.com/villagesql/vsql-vector, and you can get the server from villagesql.com. VillageSQL Server supports MySQL 8.4, 9.7, and Percona Server 8.4.
Please let us know your feedback. You can find us on Discord or on GitHub Issues.