Zvec vs sqlite-vec: Which In-Process Vector Search Should You Use?

2026-10-28
Brian

Subscribe
JOIN OUR NEWSLETTER
Be the first to know. Get fresh AI/Tech updates instantly, no spam, unsubscribe anytime

I had a folder of short notes and I wanted the note closest to a new sentence, without starting a second program just to search. A vector is a list of numbers. An embedding is that same list after a model has placed similar notes near each other. Zvec and sqlite-vec both keep those lists inside the program you already run. Use zvec when the pile is large and you can spare the disk. Use sqlite-vec when the lists should live in the SQLite file you already have.

A notebook you keep with you, next to a separate machine you would have to leave running.

The question under that picture is which file you open, and whether the nearest note is actually the right note. The trap is the default score, and it shows up on three tiny rows before any real notes.

What is an in process vector database?

Mind map of zvec vs sqlite-vec: in-process search, zvec about 1 ms, sqlite-vec smaller file, default dot product trap, normalize vectors, no server

An in-process vector database is a library that searches those number-lists from inside your program, not from a separate server you have to keep alive.

Start with the ordinary case. You have notes. A person types a sentence that is not a copy of any note. You want the note about the same idea. A computer does not know that “money back” and “refund” are the same idea unless something has already turned each note into a list of numbers that sit near similar ideas. That list is the embedding. The store that can find nearby lists, rather than only an exact word, is a vector database.

Most vector databases are another program, listening on a port. You send the list, it sends back ids. That shape is right when many machines search one shared pile. It is a lot of moving parts when the pile is one app on one machine. In-process means the search code sits in the same process as your script. You call a function. There is no port, no login, and no second thing to restart. The data is a file on disk.

Zvec is that kind of library. You give it a folder. sqlite-vec is the same idea bolted onto SQLite. SQLite is a database that is only a file your program opens, so there is no database server to install. If the notes are already rows in that file, the vectors can sit beside them. If you are starting from a Python script and you do not have a database yet, zvec is a folder you open.

Zvec vs sqlite-vec

Use zvec when the search has to stay quick as the pile grows and you can spare the disk. Use sqlite-vec when the vectors should live in the SQLite file you already have, and the pile is still modest.

I put the same notes into both. Each note was a list of 128 numbers. I scaled every list to length 1 first, so only the direction mattered and a long arrow could not pretend to be a better match. At 20,000 notes, zvec answered a search in about 1 millisecond. sqlite-vec answered the same kind of search in about 3.5 milliseconds. Zvec’s folder was about 25 megabytes. The sqlite-vec file was about 11 megabytes.

At 2,000 notes the speed gap almost disappeared. Zvec was about 0.27 milliseconds, sqlite-vec about 0.23, and the zvec folder was about 6 megabytes against about 1 megabyte. So the speed story is not “zvec always wins.” It showed up after the pile got bigger, and it paid for that speed with a larger file.

One caveat. I also turned on HNSW (a graph of nearby notes) inside zvec, the index people reach for when a plain scan of every list gets slow. At 20,000 notes it did not beat zvec’s plain scan. Same nearest notes, same time. The fancy index is not free magic at this size. I would leave the plain scan on until a millisecond is no longer enough, and I would check that the nearest ids still match before I trust the graph.

Search time and file size for the same 20,000 notes. Blue is zvec, amber is sqlite-vec.
zvecsqlite-vec
What I stored20,000 lists of 128 numbers, each length 1the same lists
Search timeabout 1 msabout 3.5 ms
File sizeabout 25 MBabout 11 MB
Where it livesits own foldera SQLite file
Default scoredot product, bigger can mean longerstraight-line distance, smaller is closer
Graph index I triedno faster than a plain scan hereexact scan only

Why does the longer vector win?

The longer vector wins on zvec’s default because that default score is a dot product, and a longer arrow makes a bigger product even when it points the same way.

A dot product is simple arithmetic. Take two lists, multiply the numbers that sit in the same slot, and add those products up. If both arrows point the same way, a longer arrow produces a bigger total. Cosine is the other habit. It keeps the angle between the arrows and throws the length away, which is what you want when the length is an accident of how the list was built, not a sign that the note matters more.

I inserted three tiny lists, each with two numbers. short was [1, 0]. long was [10, 0]. other was [0, 1]. I searched for [1, 0].

With no metric set, zvec returned long first, score 10, then short, score 1, then other, score 0. The two arrows that point right are the same idea. The long one only has a bigger length. The default treats length as importance.

I then set the metric to cosine. Both right-pointing arrows scored 0, and the upward arrow scored 1. On this metric a smaller number is closer. Zero means the same direction. If you read 0 as “no match,” you will throw away the best note.

sqlite-vec did a third thing. Its default is straight-line distance, how far apart the two points sit if you walk the shortest path. short was 0, because it is the same point as the query. other was about 1.4. long was 9, because it sits far away in raw distance even though it points the same way. Same three rows, three rankings.

So the fix is one habit. Make every list length 1 before you store it and before you search, or set the metric on purpose and remember which way “best” points. I divided each list by its length. After that, a dot product and a cosine agree, because the length is no longer allowed to vote.

A long arrow and a short arrow pointing the same way. That is the trap in the default score.

How do I add vector search without a server?

You add it with a library call and a folder. There is no server to start, and no port to keep open.

For zvec I installed the package and opened a folder. The schema says the list is named embedding, the numbers are ordinary 32-bit decimals, every list has the same length, and the score is cosine. Then you insert, then you query. A single insert call stopped at 1,024 rows, so a bigger pile goes in chunks.

import zvec

schema = zvec.CollectionSchema(
    name="notes",
    vectors=zvec.VectorSchema(
        "embedding",
        zvec.DataType.VECTOR_FP32,
        4,
        index_param=zvec.FlatIndexParam(metric_type=zvec.MetricType.COSINE),
    ),
)
col = zvec.create_and_open(path="./notes_index", schema=schema)
col.insert([
    zvec.Doc(id="a", vectors={"embedding": [1.0, 0.0, 0.0, 0.0]}),
    zvec.Doc(id="b", vectors={"embedding": [0.0, 1.0, 0.0, 0.0]}),
])
hits = col.query(
    zvec.Query(field_name="embedding", vector=[0.9, 0.1, 0.0, 0.0]),
    topk=2,
)

The number that comes back is a distance when the metric is cosine. A 0 is a perfect angle match. Do not sort it as if a bigger number were better. The package needs a current 64-bit Python. I did not need a graphics card for this. The search is arithmetic on the lists you already computed.

sqlite-vec is the other no-server path. You load the extension into a SQLite connection, make a vec0 table, and match a packed row of decimals. The number it returns is a distance too. Smaller is closer. If the notes already have a SQLite table for titles and tags, this is the one I would pick, so the vectors are not a second folder I can forget to back up.

The two projects are zvec and sqlite-vec.

What happened when I searched twenty thousand notes

The plain scan inside zvec was already about a millisecond, and the graph index did not make it faster.

I built 20,000 random lists, each 128 numbers long, each scaled to length 1. I inserted them in batches of 1,000 because a bigger batch was rejected. Insert took about 0.4 seconds for the plain index and about 0.2 seconds for the graph index. I threw away the first search, then ran seven more. They sat around 1 millisecond for both indexes. The ten nearest ids matched exactly between the two.

sqlite-vec took about 0.25 seconds to insert the same rows and about 3.5 milliseconds to search. Its file was less than half the size.

I expected the graph index to win. What happened was a tie with the plain scan, plus a fatter file than SQLite. That is a useful failure. It means you can start on the plain index, keep a check that the nearest notes still match a slow exact scan, and only switch when a millisecond is no longer enough. I did not try a million notes. I will not pretend the curve stays this flat past the pile I actually built.

When should I skip zvec?

Skip zvec when the vectors belong next to other columns in SQLite, when the pile is a couple thousand notes, or when the question is not “which note is near this one.”

A long file with real headings is often a section lookup, not a nearest-list lookup. That choice is pageindex vs a vector database. A folder of files that should refresh when you edit them is a different product again: does serviette update when files change. And if the slow part is the model that writes the number-list, the database will not fix it. Running that model is Strata vs Ollama. Finding a function by what it does, rather than a note by its neighbors, is a different search again: xerj vs grep.

Skip zvec too if you need many programs writing at once. The library’s own contract is one writer, with other processes allowed to read. I did not open two writers to watch the lock fail, so I am not claiming a crash I saw. I am saying do not plan a design that needs two writers. sqlite-vec inherits the locking of that SQLite file, which is still one writer at a time unless you have planned for something else.

Skip both if the search has to serve other people’s traffic on a machine you do not sit next to. These two are local files. A crash is your disk. That is the point, and it is also the limit.

How do I try this on my own notes?

Make ten short sentences, turn each into a list of the same length, force that length to 1, insert them, and search with a sentence you did not store.

You can steal the three-row check before any real notes. Store [1, 0], [10, 0], and [0, 1]. Search [1, 0]. If long comes back first, you are on the default dot product and length is leaking into the rank. Set cosine, or divide every list by its length, and run it again. The two right-pointing rows should tie, and the upward row should lose.

Then do it with your own sentences. If two notes you know are about the same thing do not land in the top three, the bug is usually the lists, not the database. The lists might have different lengths, the metric might be the one you did not set, or the model that built the lists might not be the one you think. The database only stores and ranks. It does not know what the numbers mean.

Common questions about zvec vs sqlite-vec

Is zvec faster than sqlite-vec?

At 20,000 lists of 128 numbers, yes, about 1 millisecond against about 3.5. At 2,000 lists they were tied, and sqlite-vec used less disk in both tests.

Do I need a server to run zvec?

No. You install the library, open a folder, and call insert and query in the same program. Nothing has to stay running after your script exits. The folder is the database.

Can I use zvec instead of Pinecone?

For a search that stays on one machine, yes. Pinecone is a hosted service for other people’s traffic and for more than one writer. Zvec will not replace that. It replaces the case where you were about to run a server for a pile that fits in a folder.

Does zvec need normalized vectors?

Only if you want direction, not length, to decide the winner. If you leave the default metric on, a longer list can outrank a shorter list that points the same way. Divide each list by its length, or set cosine and remember that a smaller score is closer.

I would put a new Python app on zvec, with cosine set and every list scaled to length 1, and I would move the lists into sqlite-vec the day they need to sit beside the rest of a SQLite file. The millisecond is not the decision. The file you already have is.

JOIN OUR NEWSLETTER
Be the first to know. Get fresh AI/Tech updates instantly, no spam, unsubscribe anytime

Leave a comment