{"slug": "vector-similarity-search-with-duckdb-a-practical-guide-to-the-vss-extension", "title": "Vector Similarity Search with DuckDB: A Practical Guide to the VSS Extension", "summary": "DuckDB's official vss extension brings HNSW-based approximate nearest neighbor search directly to its SQL engine, allowing developers to run vector similarity queries without a separate vector database. The extension supports multiple distance metrics, index hyperparameters, and integrates with DuckDB's native ARRAY type, making it a lightweight option for RAG pipelines and analytics workloads.", "body_md": "Most people reach for a dedicated vector database — Pinecone, Qdrant, Milvus, pgvector — the moment a project needs embedding search. But if you are already using DuckDB for analytics or building a lightweight RAG pipeline, there is a good chance you do not need another moving part. DuckDB ships an official `vss`\n\nextension that adds HNSW-based approximate nearest neighbor search directly on top of its native `ARRAY`\n\ntype.\n\nThis article walks through what the extension does, how to use it, and where its limits are.\n\n`vss`\n\nis an experimental core extension that adds indexing support to accelerate similarity search over DuckDB's fixed-size `ARRAY`\n\ncolumns. It implements HNSW (Hierarchical Navigable Small Worlds), the same graph-based ANN algorithm used by most production vector search engines. In practice, this means you can store embeddings as a normal column, build an index on it, and run `ORDER BY ... LIMIT`\n\nqueries that DuckDB will automatically route through the index instead of a full scan.\n\nBecause it is embedded, there is no separate service to run, no network hop, and no extra infrastructure to operate — the vector index lives in the same process as the rest of your SQL engine.\n\nInstallation follows DuckDB's usual extension pattern:\n\n```\nINSTALL vss;\nLOAD vss;\n```\n\nThen create a table with a fixed-size `ARRAY`\n\ncolumn and build an HNSW index on it:\n\n```\nCREATE TABLE my_vector_table (vec FLOAT[3]);\n\nINSERT INTO my_vector_table\n    SELECT array_value(a, b, c)\n    FROM range(1, 10) ra(a), range(1, 10) rb(b), range(1, 10) rc(c);\n\nCREATE INDEX my_hnsw_index ON my_vector_table USING HNSW (vec);\n```\n\nNote the dimensionality (`FLOAT[3]`\n\nhere) must be fixed at table-creation time — DuckDB's `ARRAY`\n\ntype is size-constrained, unlike the variable-length `LIST`\n\ntype.\n\nOnce the index exists, DuckDB will use it automatically whenever a query orders by a supported distance function against a constant vector and limits the result set:\n\n```\nSELECT *\nFROM my_vector_table\nORDER BY array_distance(vec, [1, 2, 3]::FLOAT[3])\nLIMIT 3;\n```\n\nYou can confirm the index is actually being used by checking the query plan for an `HNSW_INDEX_SCAN`\n\nnode:\n\n```\nEXPLAIN\nSELECT *\nFROM my_vector_table\nORDER BY array_distance(vec, [1, 2, 3]::FLOAT[3])\nLIMIT 3;\n```\n\nFor one-shot nearest-neighbor lookups, the overloaded `min_by(col, arg, n)`\n\naggregate is also index-accelerated when `arg`\n\nmatches a supported distance function, and it conveniently returns the full matched row as a struct:\n\n```\nSELECT min_by(my_vector_table, array_distance(vec, [1, 2, 3]::FLOAT[3]), 3 ORDER BY vec) AS result\nFROM my_vector_table;\n```\n\nBy default, HNSW indexes use `l2sq`\n\n(squared Euclidean distance), matching `array_distance`\n\n. You can choose a different metric at index-creation time:\n\n```\nCREATE INDEX my_hnsw_cosine_index\nON my_vector_table\nUSING HNSW (vec)\nWITH (metric = 'cosine');\n```\n\n| Metric | Function | Description |\n|---|---|---|\n`l2sq` |\n`array_distance` |\nEuclidean distance |\n`cosine` |\n`array_cosine_distance` |\nCosine similarity distance |\n`ip` |\n`array_negative_inner_product` |\nNegative inner product |\n\nFor most embedding models used in RAG or semantic search (OpenAI, Sentence-Transformers, etc.), cosine similarity is the natural choice. You can also build multiple indexes on the same column with different metrics, or index multiple columns independently — each `HNSW`\n\nindex applies to exactly one column.\n\nIndex quality and search speed are controlled by a handful of hyperparameters, all familiar to anyone who has tuned HNSW before:\n\n| Option | Default | Effect |\n|---|---|---|\n`ef_construction` |\n128 | Candidate vertices considered while building the index. Higher = more accurate, slower build. |\n`ef_search` |\n64 | Candidate vertices considered per query. Higher = more accurate, slower search. |\n`M` |\n16 | Max neighbors per graph vertex. Higher = more accurate, slower build. |\n`M0` |\n2 × `M`\n|\nBase connectivity at the zero-th graph level. |\n\n`ef_search`\n\ncan also be overridden per connection at runtime without rebuilding the index:\n\n```\nSET hnsw_ef_search = 128;\n-- ...run queries...\nRESET hnsw_ef_search;\n```\n\nThis is useful when you want to dial accuracy up or down depending on the query, without paying the cost of a full reindex.\n\nThis is the biggest practical caveat. By default, `HNSW`\n\nindexes can only be created on **in-memory** databases. If you want the index to persist in a disk-backed `.duckdb`\n\nfile, you must explicitly opt in:\n\n```\nSET hnsw_enable_experimental_persistence = true;\n```\n\nIt is locked behind this flag because WAL (write-ahead log) recovery is not yet fully implemented for custom extension indexes. If DuckDB crashes or is killed while there are uncommitted changes to an HNSW-indexed table, the index can end up corrupted or lose data. The docs are explicit that this is not recommended for production use.\n\nIf you do enable it and hit an unexpected shutdown, recovery is possible by starting DuckDB separately, loading `vss`\n\n, and then `ATTACH`\n\ning the database file before letting WAL replay run — this makes the HNSW functionality available during recovery.\n\nWhen persistence is enabled, the entire index is serialized to disk on every checkpoint (no incremental updates) and deserialized back into memory on the next access after restart — which is still generally faster than dropping and rebuilding it from scratch.\n\n**For a local RAG prototype or an in-memory analytical session, this is a non-issue.** For anything that needs durable, crash-safe vector storage in production, treat this as a hard constraint and plan accordingly — or keep the source-of-truth embeddings elsewhere and rebuild the index on startup.\n\nThe index supports mutation after creation, with two practical notes:\n\nTo reclaim this, run:\n\n```\nPRAGMA hnsw_compact_index('my_hnsw_index');\n```\n\nor periodically drop and recreate the index if the table sees heavy churn.\n\n`vss_join`\n\nand `vss_match`\n\nBeyond single-query nearest-neighbor search, the extension ships two table macros for matching two sets of vectors against each other — useful for deduplication, entity resolution, or batch retrieval:\n\n```\nCREATE TABLE haystack (id INT, vec FLOAT[3]);\nCREATE TABLE needle (search_vec FLOAT[3]);\n\nINSERT INTO haystack\n    SELECT row_number() OVER (), array_value(a, b, c)\n    FROM range(1, 10) ra(a), range(1, 10) rb(b), range(1, 10) rc(c);\n\nINSERT INTO needle VALUES ([5, 5, 5]), ([1, 1, 1]);\n\nSELECT * FROM vss_join(needle, haystack, search_vec, vec, 3) res;\n```\n\n`vss_match`\n\noffers the same brute-force k-NN matching but as a lateral join, grouping results per left-table row:\n\n```\nSELECT * FROM needle, vss_match(haystack, search_vec, vec, 3) res;\n```\n\nImportant: **neither macro uses the HNSW index** — they perform brute-force search. They are convenience utilities for correctness, not performance, though the docs note they may become index-accelerated in the future.\n\n`FLOAT`\n\nvectors are supported today — no `DOUBLE`\n\n, no quantized/int8 vectors.`memory_limit`\n\nsetting, so it is easy to overshoot available memory without warning.`vss_join`\n\n/ `vss_match`\n\nnever use the index, regardless of whether one exists on the underlying columns.`vss`\n\nis a strong fit when:\n\n`ORDER BY array_cosine_distance(...)`\n\nin a single statement, without shipping data to another system.It is a weaker fit if you need durable, crash-safe indexes at scale in a multi-writer production environment — for that, a dedicated vector database or an extension like pgvector on a durable RDBMS is currently the safer choice.\n\nThe `vss`\n\nextension turns DuckDB into a capable, embedded ANN engine: `INSTALL`\n\n/`LOAD`\n\nthe extension, store embeddings in a fixed-size `ARRAY`\n\ncolumn, `CREATE INDEX ... USING HNSW`\n\n, and query with `ORDER BY array_distance(...) LIMIT k`\n\n. It supports L2, cosine, and inner-product metrics, exposes the usual HNSW tuning knobs, and even offers brute-force join macros for batch matching. The one thing to plan around carefully is persistence — it is opt-in and explicitly experimental, so treat durability as something you design for rather than assume.", "url": "https://wpnews.pro/news/vector-similarity-search-with-duckdb-a-practical-guide-to-the-vss-extension", "canonical_source": "https://dev.to/muhammadikhwanfathulloh/vector-similarity-search-with-duckdb-a-practical-guide-to-the-vss-extension-p5c", "published_at": "2026-08-15 14:21:20+00:00", "updated_at": "2026-08-15 14:42:48.914944+00:00", "lang": "en", "topics": ["machine-learning", "developer-tools"], "entities": ["DuckDB", "HNSW", "Pinecone", "Qdrant", "Milvus", "pgvector", "OpenAI", "Sentence-Transformers"], "alternates": {"html": "https://wpnews.pro/news/vector-similarity-search-with-duckdb-a-practical-guide-to-the-vss-extension", "markdown": "https://wpnews.pro/news/vector-similarity-search-with-duckdb-a-practical-guide-to-the-vss-extension.md", "text": "https://wpnews.pro/news/vector-similarity-search-with-duckdb-a-practical-guide-to-the-vss-extension.txt", "jsonld": "https://wpnews.pro/news/vector-similarity-search-with-duckdb-a-practical-guide-to-the-vss-extension.jsonld"}}