<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: RecallRun</title>
    <description>The latest articles on DEV Community by RecallRun (@recallrundev).</description>
    <link>https://dev.to/recallrundev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4160628%2F626bba7e-f376-4429-beb0-6b0aa0331e52.webp</url>
      <title>DEV Community: RecallRun</title>
      <link>https://dev.to/recallrundev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/recallrundev"/>
    <language>en</language>
    <item>
      <title>Do you need a vector database? Brute-force search benchmarked from 10k to 1M vectors</title>
      <dc:creator>RecallRun</dc:creator>
      <pubDate>Wed, 07 Oct 2026 11:01:14 +0000</pubDate>
      <link>https://dev.to/recallrundev/do-you-need-a-vector-database-brute-force-search-benchmarked-from-10k-to-1m-vectors-6fk</link>
      <guid>https://dev.to/recallrundev/do-you-need-a-vector-database-brute-force-search-benchmarked-from-10k-to-1m-vectors-6fk</guid>
      <description>&lt;p&gt;Most RAG tutorials start by installing a vector database. For a lot of projects, that is a dependency you don't need yet. I measured how long plain exact search takes as the corpus grows, and what an HNSW index buys you in return.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; below about 100,000 chunks, a numpy matrix multiply is fast enough for almost any RAG app. Between 100k and 1M it depends on your traffic. Past that, use an index.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Every embedding is normalized, so cosine similarity is a dot product. Exact search is one matrix-vector product and a partial sort:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;X: (n, d) normalized float32 matrix. q: (d,) normalized query.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argpartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I timed this against &lt;code&gt;faiss.IndexFlatIP&lt;/code&gt; (also exact) for three common embedding sizes: 384 (MiniLM-class models), 768 (BERT-base-class) and 1536 (OpenAI &lt;code&gt;text-embedding-3-small&lt;/code&gt; default). The machine is deliberately small: 2 vCPUs, the size of a cheap cloud instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exact search latency
&lt;/h2&gt;

&lt;p&gt;Single-query latency, median of 50 queries:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dim&lt;/th&gt;
&lt;th&gt;Vectors&lt;/th&gt;
&lt;th&gt;RAM for vectors&lt;/th&gt;
&lt;th&gt;numpy&lt;/th&gt;
&lt;th&gt;faiss Flat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;15 MB&lt;/td&gt;
&lt;td&gt;0.35 ms&lt;/td&gt;
&lt;td&gt;0.78 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;154 MB&lt;/td&gt;
&lt;td&gt;3.5 ms&lt;/td&gt;
&lt;td&gt;8.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;td&gt;768 MB&lt;/td&gt;
&lt;td&gt;31 ms&lt;/td&gt;
&lt;td&gt;66 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;1.5 GB&lt;/td&gt;
&lt;td&gt;62 ms&lt;/td&gt;
&lt;td&gt;122 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;307 MB&lt;/td&gt;
&lt;td&gt;10.6 ms&lt;/td&gt;
&lt;td&gt;22.6 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;td&gt;1.5 GB&lt;/td&gt;
&lt;td&gt;57 ms&lt;/td&gt;
&lt;td&gt;120 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;3.1 GB&lt;/td&gt;
&lt;td&gt;114 ms&lt;/td&gt;
&lt;td&gt;240 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;61 MB&lt;/td&gt;
&lt;td&gt;1.2 ms&lt;/td&gt;
&lt;td&gt;3.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;614 MB&lt;/td&gt;
&lt;td&gt;20 ms&lt;/td&gt;
&lt;td&gt;51 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1536&lt;/td&gt;
&lt;td&gt;500,000&lt;/td&gt;
&lt;td&gt;3.1 GB&lt;/td&gt;
&lt;td&gt;107 ms&lt;/td&gt;
&lt;td&gt;248 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things stand out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Latency scales linearly with &lt;code&gt;n × d&lt;/code&gt;.&lt;/strong&gt; It is a memory-bandwidth problem. Doubling either the corpus or the dimension doubles the time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory is the real limit, not speed.&lt;/strong&gt; 1M vectors at 1536 dims is 6 GB of float32 before any metadata. I skipped that row: it won't fit comfortably on a 7 GB machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain numpy beat faiss Flat for single queries here&lt;/strong&gt;, by about 2×. Faiss is built for batches; one query at a time pays overhead that numpy's direct matrix-vector call doesn't. If you batch queries, test again before assuming this holds.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What an HNSW index buys you
&lt;/h2&gt;

&lt;p&gt;Next, I built &lt;code&gt;hnswlib&lt;/code&gt; indexes (M=16, ef_construction=200) and measured query time and &lt;strong&gt;recall@10&lt;/strong&gt;: the fraction of the true top 10 the index actually returns.&lt;/p&gt;

&lt;p&gt;I used two synthetic datasets, because recall depends heavily on how your data is shaped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uniform:&lt;/strong&gt; random Gaussian directions. This is the worst case for any approximate index: no structure to exploit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clustered:&lt;/strong&gt; 2,000 tight clusters. Real embeddings sit between these two, usually much closer to clustered, since documents about the same topic land near each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;th&gt;Dim&lt;/th&gt;
&lt;th&gt;Vectors&lt;/th&gt;
&lt;th&gt;Build time&lt;/th&gt;
&lt;th&gt;ef=32&lt;/th&gt;
&lt;th&gt;ef=64&lt;/th&gt;
&lt;th&gt;ef=128&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;clustered&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;100k&lt;/td&gt;
&lt;td&gt;23 s&lt;/td&gt;
&lt;td&gt;0.10 ms, recall 0.954&lt;/td&gt;
&lt;td&gt;0.17 ms, 0.995&lt;/td&gt;
&lt;td&gt;0.34 ms, 1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clustered&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;100k&lt;/td&gt;
&lt;td&gt;48 s&lt;/td&gt;
&lt;td&gt;0.22 ms, 0.878&lt;/td&gt;
&lt;td&gt;0.36 ms, 0.985&lt;/td&gt;
&lt;td&gt;0.68 ms, 1.000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uniform&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;100k&lt;/td&gt;
&lt;td&gt;37 s&lt;/td&gt;
&lt;td&gt;0.16 ms, 0.129&lt;/td&gt;
&lt;td&gt;0.31 ms, 0.206&lt;/td&gt;
&lt;td&gt;0.55 ms, 0.281&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uniform&lt;/td&gt;
&lt;td&gt;768&lt;/td&gt;
&lt;td&gt;100k&lt;/td&gt;
&lt;td&gt;69 s&lt;/td&gt;
&lt;td&gt;0.34 ms, 0.058&lt;/td&gt;
&lt;td&gt;0.63 ms, 0.105&lt;/td&gt;
&lt;td&gt;1.07 ms, 0.172&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uniform&lt;/td&gt;
&lt;td&gt;384&lt;/td&gt;
&lt;td&gt;500k&lt;/td&gt;
&lt;td&gt;279 s&lt;/td&gt;
&lt;td&gt;0.19 ms, 0.034&lt;/td&gt;
&lt;td&gt;0.35 ms, 0.057&lt;/td&gt;
&lt;td&gt;0.67 ms, 0.099&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;HNSW queries are &lt;strong&gt;10 to 100 times faster&lt;/strong&gt; than exact search at these sizes. The cost:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build time.&lt;/strong&gt; 500k vectors took over 4.5 minutes on 2 cores. You pay this again on every full re-index, such as after switching embedding models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recall is not guaranteed.&lt;/strong&gt; On structured data, &lt;code&gt;ef=64&lt;/code&gt; gave near-perfect recall. On structureless data, the same settings returned less than a quarter of the true neighbors. Your data will be somewhere in between, so &lt;strong&gt;measure recall on your own embeddings&lt;/strong&gt; before trusting the defaults.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the time actually goes in a RAG request
&lt;/h2&gt;

&lt;p&gt;A typical RAG request spends 1 to 5 seconds waiting for the LLM to generate. Next to that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3.5 ms (100k × 384, exact) is invisible.&lt;/li&gt;
&lt;li&gt;60 to 110 ms (1M exact) is noticeable only if you care about p99, or serve many queries per second on the same box.&lt;/li&gt;
&lt;li&gt;Exact search gives perfect recall for free, which removes one variable when you debug bad answers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A rough sizing rule: one PDF page is usually 2 to 3 chunks. &lt;strong&gt;100k chunks is roughly 30,000 to 50,000 pages.&lt;/strong&gt; Many internal "chat with our docs" projects never get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  My rule of thumb
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your corpus&lt;/th&gt;
&lt;th&gt;What I'd use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;under 100k chunks&lt;/td&gt;
&lt;td&gt;numpy (or pgvector with no index). Store vectors in a &lt;code&gt;.npy&lt;/code&gt; file or Postgres.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100k to 1M chunks&lt;/td&gt;
&lt;td&gt;exact search is still fine at low QPS. Add HNSW when p95 latency or QPS demands it, and measure recall.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;over 1M chunks, or many tenants&lt;/td&gt;
&lt;td&gt;a proper ANN index (pgvector HNSW, Qdrant, Milvus, OpenSearch), plus filtering and sharding.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most importantly, &lt;strong&gt;keep the vector store behind one interface&lt;/strong&gt; so you can swap numpy for pgvector later without touching the rest of the pipeline. That one seam is worth more than picking the "right" database on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The exact-search benchmark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;faiss&lt;/span&gt;
&lt;span class="n"&gt;faiss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;omp_set_num_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_rng&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;384&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;3.2e9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;          &lt;span class="c1"&gt;# skip what won't fit in RAM
&lt;/span&gt;        &lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;standard_normal&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;Q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;standard_normal&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;sc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;
            &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argpartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mf"&gt;1e3&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For HNSW, build with &lt;code&gt;hnswlib.Index(space="ip", dim=d)&lt;/code&gt;, &lt;code&gt;init_index(max_elements=n, ef_construction=200, M=16)&lt;/code&gt;, and compare &lt;code&gt;knn_query&lt;/code&gt; results against the exact top 10 to get recall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; these are synthetic vectors on one small machine. Latency for exact search doesn't depend on what the vectors mean, so those numbers transfer well. HNSW recall does depend on your data, which is exactly why you should measure it yourself.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Test setup: 2 vCPU Intel Xeon @ 2.1 GHz, 7 GB RAM, Python 3.11, numpy 2.4.4, faiss-cpu 1.15.1, hnswlib. Synthetic normalized float32 vectors. Median of 50 (exact) or 200 (HNSW) single queries.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I run experiments like this every week at RecallRun. If you want to build RAG systems end to end, the AI Engineering course starts free: &lt;a href="https://recallrun.dev/learn/ai-engineering/?utm_source=devto&amp;amp;utm_campaign=vector-db" rel="noopener noreferrer"&gt;https://recallrun.dev/learn/ai-engineering/?utm_source=devto&amp;amp;utm_campaign=vector-db&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>database</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
