llm_trw 43 minutes ago

>The second incorrect method to save a matrix of embeddings to disk is to save it as a Python pickle object [...] But it comes with two major caveats: pickled files are a massive security risk as they can execute arbitrary code, and the pickled file may not be guaranteed to be able to be opened on other machines or Python versions. It’s 2025, just stop pickling if you can.

Security: absolutely.

Portability: who cares? Frameworks move so quickly that unless you carry your whole dependency graph between machines you will not get bit compatible results with even minor version changes. It's a dirty secret that no one seems to want to fix or care about.

In short: everything is so fucked that pickle + conda is more than good enough for whatever project you want to serve to >10,000 users.

banku_brougham 5 hours ago

Really cool article, I've enjoyed your work for a long time. You might add a note for those jumping into a sqlite implementation, that duckdb reads parquet and launched a few vector similarity functions which cover this use-case perfectly:

https://duckdb.org/2024/05/03/vector-similarity-search-vss.h...

  • jt_b 3 hours ago

    I have tinkered with using DuckDB as a poor man's vector database for a POC and had great results.

    One thing I'd love to see is being able to do some sort of row group level metadata statistics for embeddings within a parquet file - something that would allow various readers to push predicates down to an HTTP request metadata level and completely avoid loading in non-relevant rows to the database from a remote file - particularly one stored on S3 compatible storage that supports byte-range requests. I'm not sure what the implementation would look like to define sorting the algorithm to organize the "close" rows together, how the metadata would be calculated, or what the reader implementation would look like, but I'd love to be able to implement some of the same patterns with vector search as with geoparquet.

thomasfromcdnjs 3 hours ago

Lots of great findings

---

I'm curious if anyone knows whether it is better to pass structured data or unstructured data to embedding api's? If I ask ChatGPT, it says it is better to send unstructured data. (looking at the authors github, it looks like he generated embeddings from json strings)

My use case is for jsonresume, I am creating embeddings by sending full json versions as strings, but I've been experimenting with using models to translate resume.json's into full text versions first before creating embeddings. The results seem to be better but I haven't seen any concrete opinions on this.

My understanding is that unstructured data is better because it contains textual/semantic meaning because of natural lanaguage aka

  skills: ['Javascript', 'Python']
is worse than;

  Thomas excels at Javascript and Python
Another question: What if the search was also a json embedding? JSON <> JSON embeddings could also be great?
  • vunderba 43 minutes ago

    I'd say the more important consideration is "consistency" between incoming query input and stored vectors.

    I have a huge vector database that gets updated/regenerated from a personal knowledge store (markdown library). Since the user is most likely to input a comparison query in the form of a question "Where does X factor into the Y system?" - I use a small 7b parameter LLM to pregenerate a list of a dozen possible theoretical questions a user might pose to a given embedding chunk. These are saved as 1536 dimension sized embeddings into the vector database (Qdrant) and linked to the chunks.

    The real question you need to ask is - what's the input query that you'll be comparing to the embeddings? If it's incoming as structured, then store structured, etc.

    I've also seen (anecdotally) similarity degradation for smaller chunks as well - so keep that in mind as well.

  • minimaxir 3 hours ago

    In general I like to send structured data (see the input format here: https://github.com/minimaxir/mtg-embeddings), but the ModernBERT base for the embedding model used here specifically has better benefits implicitly for structured data compared to previous models. That's worth another blog post explaining why.

rcarmo 3 hours ago

I'm a huge fan of polars, but I hadn't considered using it to store embeddings in this way (I've been fiddling with sqlite-vec). Seems like an interesting idea indeed.

stephantul 5 hours ago

Check out Unum’s usearch. It beats anything, and is super easy to use. It just does exactly what you need.

https://github.com/unum-cloud/usearch

  • esafak 5 hours ago

    Have you tested it against Lance? Does it do predicate pushdown for filtering?

    • ashvardanian 3 hours ago

      USearch author here :)

      The engine supports arbitrary predicates for C, C++, and Rust users. In higher level languages it’s hard to combine callbacks and concurrent state management.

      In terms of scalability and efficiency, the only tool I’ve seen coming close is Nvidia’s cuVS if you have GPUs available. FAISS HNSW implementation can easily be 10x slower and most commercial & venture-backed alternatives are even slower: https://www.unum.cloud/blog/2023-11-07-scaling-vector-search...

      In this use-case, I believe SimSIMD raw kernels may be a better choice. Just replace NumPy and enjoy speedups. It provides hundreds of hand-written SIMD kernels for all kinds of vector-vector operations for AVX, AVX-512, NEON, and SVE across F64, F32, BF16, F16, I8, and binary vectors, mostly operating in mixed precision to avoid overflow and instability: https://github.com/ashvardanian/SimSIMD

    • stephantul 5 hours ago

      Usearch is a vector store afaik, not a vector db. At least that’s how I use it.

      I haven’t compared it to lancedb, I reached for it here because the author mentioned Faiss being difficult to use and install. usearch is a great alternative to Faiss.

      But thanks for the suggestion, I’ll check it out

dwagnerkc an hour ago

If you want to try it out. Can lazily load from HF and apply filtering this way.

  df = (
    pl.scan_parquet('hf://datasets/minimaxir/mtg-embeddings/mtg_embeddings.parquet')
    .filter(
        pl.col("type").str.contains("Sorcery"),
        pl.col("manaCost").str.contains("B"),
    )
    .collect()
)

Polars is awesome to use, would highly recommend. Single node it is excellent at saturating CPUs, if you need to distribute the work put it in a Ray Actor with some POLARS_MAX_THREADS applied depending on how much it saturates a single node.

robschmidt90 4 hours ago

Nice read. I agree that for a lot of hobby use cases you can just load the embeddings from parquet and compute the similarities in-memory.

To find similarity between my blogposts [1] I wanted to experiment with a local vector database and found ChromaDB fairly easy to use (similar to SQLite just a file on your machine).

[1] https://staticnotes.org/posts/how-recommendations-work/

jtrueb 4 hours ago

Polars + Parquet is awesome for portability and performance. This post focused on python portability, but Polars has an easy-to-use Rust API for embedding the engine all over the place.

  • blooalien 2 hours ago

    Gotta love stuff that has multiple language bindings. Always really enjoyed finding powerful libraries in Python and then seeing they also have matching bindings for Go and Rust. Nice to have easy portability and cross-language compatibility.

jononor 3 hours ago

At 33k items in memory is quite fast, 10 ms is very responsive. With 10x/330k items given same hardware the expected time is 1 second. That might be too slow for some applications (but not all). Especially if one just does retrieval of a rather small amount of matches, an index will help a lot for 100k++ datasets.

kernelsanderz 5 hours ago

For another library that has great performance and features like full text indexing and the ability to version changes I’d recommend lancedb https://lancedb.github.io/lancedb/

Yes, it’s a vector database and has more complexity. But you can use it without creating indexes and it has excellent polars and pandas zero copy arrow support also.

  • daveguy 5 hours ago

    Since a lot of ML data is stored as parquet, I found this to be a useful tidbit from lancedb's documentation:

    > Data storage is columnar and is interoperable with other columnar formats (such as Parquet) via Arrow

    https://lancedb.github.io/lancedb/concepts/data_management/

    Edit: That said, I am personally a fan of parquet, arrow, and ibis. So many data wrangling options out there it's easy to get analysis paralysis.

  • 3abiton 2 hours ago

    How well does it scale?

  • esafak 5 hours ago

    Lance is made for this stuff; parquet is not.

noahbp 3 hours ago

Wow! How much did this cost you in GPU credits? And did you consider using your MacBook?

kipukun 5 hours ago

To the second footnote: you could utilize Polar's lazyframe API to do that cosine similarity in a streaming fashion for large files.

  • minimaxir 4 hours ago

    That would get around memory limitations but I still think that would be slow.

    • kipukun 4 hours ago

      You'd be surprised. As long as your query is using Polars natives and not a UDF (which drops it down to Python), you may get good results.

thelastbender12 6 hours ago

This is pretty neat.

IMO a hindrance to this was lack of built-in fixed-size list array support in the Arrow format, until recently. Some implementations/clients supported it, while others didn't. Else, it could have been used as the default storage format for numpy arrays, torch tensors, too.

(You could always store arrays as variable length list arrays with fixed strides and handle the conversion).

banku_brougham 5 hours ago

Is your example of a float32 number correct, holding 24 ascii char representation? I had thought single-precision gonna be 7 digits and the exponent, sign and exp sign. Something like 7+2+1+1 or 10 char ascii representation? Rather than the 24 you mentioned?

  • minimaxir 5 hours ago

    It depends on the default print format. The example string I mentioned is pulled from what np.savetxt() does (fmt='%.18e') and there isn't any precision loss in that number. But I admit I'm not a sprintf() guru.

    In practice numbers with that much precision is overkill and verbose so tools don't print float32s to that level of precision.

  • PaulHoule 4 hours ago

    One of the things I remember from my PhD work is that you can do a stupendous number of FLOPs on floating point numbers in the time it takes to serialize/deserialize them to ASCII.

WatchDog an hour ago

Parquet is fine and all, but I love the simplicity and simple interoperability of CSV.

You can save a huge amount of overhead just by base64 encoding the vectors, they aren't exactly human readable anyway.

I imagine the resulting file would only be approximately 33% larger than the pickle version.

whinvik 7 hours ago

Since we are talking about an embedded solution shouldn't the benchmark be something like sqlite with a vector extension or lancedb?

  • 0cf8612b2e1e 5 hours ago

    My natural point of comparison without actually be DuckDB plus their vector search extension.

  • minimaxir 6 hours ago

    I mention sqlite + sqlite-vec at the end, noting it requires technical overhead and it's not as easy as read_parquet() and write_parquet().

    I just became aware of lancedb and am looking into that, although from glancing at the README it has similar issues to faiss with regards to usability for casual use, although much better than faiss in that it can work with colocated metadata.

octernion 2 hours ago

or you could just use postgres + pgvector? which many apps already have installed by default.