<?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: midegdugarova</title>
    <description>The latest articles on DEV Community by midegdugarova (@midegdugarova).</description>
    <link>https://dev.to/midegdugarova</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3940921%2F54988839-3363-426b-b6d9-29db052e6f9b.png</url>
      <title>DEV Community: midegdugarova</title>
      <link>https://dev.to/midegdugarova</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/midegdugarova"/>
    <language>en</language>
    <item>
      <title>Qdrant in Production: 10 Gotchas the Quickstart Won't Tell You</title>
      <dc:creator>midegdugarova</dc:creator>
      <pubDate>Thu, 25 Jun 2026 18:56:30 +0000</pubDate>
      <link>https://dev.to/midegdugarova/qdrant-in-production-10-gotchas-the-quickstart-wont-tell-you-1359</link>
      <guid>https://dev.to/midegdugarova/qdrant-in-production-10-gotchas-the-quickstart-wont-tell-you-1359</guid>
      <description>&lt;p&gt;The Qdrant quickstart is genuinely good — you're upserting vectors and getting&lt;br&gt;
search results in five minutes. But there's a gap between "the demo works" and&lt;br&gt;
"this runs in production without surprising me," and most of what lives in that&lt;br&gt;
gap isn't in any single docs page. It's scattered across reference sections,&lt;br&gt;
GitHub issues, and the scars of people who hit it at 2 a.m.&lt;/p&gt;

&lt;p&gt;I collected these while ramping up on Qdrant — reading the docs end to end,&lt;br&gt;
building demos, and auditing the gaps. Here are the ten that matter, ordered&lt;br&gt;
roughly by &lt;em&gt;when&lt;/em&gt; they'll bite you: your first week, your first month, your&lt;br&gt;
first incident.&lt;/p&gt;

&lt;p&gt;All code uses the current Python client API (&lt;code&gt;query_points&lt;/code&gt;, not the deprecated&lt;br&gt;
&lt;code&gt;search&lt;/code&gt;).&lt;/p&gt;


&lt;h2&gt;
  
  
  Your first week
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Payload indexing is not automatic
&lt;/h3&gt;

&lt;p&gt;This is the big one. Qdrant lets you filter on any payload field out of the&lt;br&gt;
box, and at demo scale it's fast — so it's easy to assume filtering "just&lt;br&gt;
works." It does. It's just doing a &lt;strong&gt;full scan&lt;/strong&gt; over candidate payloads,&lt;br&gt;
which falls off a cliff as the collection grows.&lt;/p&gt;

&lt;p&gt;Every field you filter on needs an explicit index:&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="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_payload_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;field_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no warning when you filter on an unindexed field. The symptom is just&lt;br&gt;
"filtered queries got slow somewhere past a few hundred thousand points." Make&lt;br&gt;
payload indexes part of your collection-creation script, not an afterthought.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Cosine vs. dot product: normalization decides
&lt;/h3&gt;

&lt;p&gt;If your embeddings are L2-normalized — and OpenAI and Cohere embeddings are —&lt;br&gt;
cosine similarity and dot product give &lt;strong&gt;identical rankings&lt;/strong&gt;, but dot skips&lt;br&gt;
the normalization step, so it's the faster choice:&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="n"&gt;vectors_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;VectorParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap runs the other way: use &lt;code&gt;DOT&lt;/code&gt; with &lt;em&gt;un&lt;/em&gt;-normalized embeddings and your&lt;br&gt;
results get silently biased toward vectors with larger magnitudes. No error,&lt;br&gt;
just subtly wrong rankings — the worst kind of bug.&lt;/p&gt;

&lt;p&gt;Rule of thumb: OpenAI/Cohere → &lt;code&gt;DOT&lt;/code&gt;. Anything else, or unsure → &lt;code&gt;COSINE&lt;/code&gt;,&lt;br&gt;
which normalizes for you.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Collection config is forever
&lt;/h3&gt;

&lt;p&gt;Vector dimensions and distance metric are &lt;strong&gt;immutable&lt;/strong&gt; after&lt;br&gt;
&lt;code&gt;create_collection&lt;/code&gt;. There is no migration path — switching embedding models&lt;br&gt;
means a new collection and a full re-ingest of everything.&lt;/p&gt;

&lt;p&gt;That's worth a real decision upfront, not a default. And if you suspect you'll&lt;br&gt;
ever migrate models (you will), use &lt;strong&gt;named vectors&lt;/strong&gt; from day one — you can&lt;br&gt;
add a new named vector for the new model and backfill, instead of rebuilding&lt;br&gt;
the world:&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="n"&gt;vectors_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;VectorParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;# room to add "openai-large" later without a new collection
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Your first month
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;upsert&lt;/code&gt; replaces the &lt;em&gt;entire&lt;/em&gt; point
&lt;/h3&gt;

&lt;p&gt;Qdrant has three update operations, and using the wrong one silently loses&lt;br&gt;
data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;upsert&lt;/code&gt; — replaces the whole point: vector &lt;strong&gt;and&lt;/strong&gt; all payload fields&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;set_payload&lt;/code&gt; — updates only the payload fields you pass&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update_vectors&lt;/code&gt; — updates only the vector&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The classic mistake is using &lt;code&gt;upsert&lt;/code&gt; to "update one field." Any payload field&lt;br&gt;
you didn't re-include is gone — no error, no warning. If you're patching&lt;br&gt;
metadata, you want &lt;code&gt;set_payload&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Very selective filters quietly change the algorithm
&lt;/h3&gt;

&lt;p&gt;Qdrant's filtered search is smart: the query planner estimates how many points&lt;br&gt;
match your filter, and if the match set is very small (think under ~1% of the&lt;br&gt;
collection), it skips the HNSW index entirely and does an exact scan over the&lt;br&gt;
matching points — because that's genuinely faster at that selectivity.&lt;/p&gt;

&lt;p&gt;This is correct behavior, but it produces a confusing symptom: &lt;strong&gt;"search is&lt;br&gt;
fast usually, slow sometimes,"&lt;/strong&gt; depending on which filter a user happens to&lt;br&gt;
pick. If you have a dimension that's &lt;em&gt;always&lt;/em&gt; extremely selective — per-tenant&lt;br&gt;
data is the classic case — consider making it a separate collection (or using&lt;br&gt;
Qdrant's multitenancy patterns) instead of filtering one giant one.&lt;/p&gt;
&lt;h3&gt;
  
  
  6. Set &lt;code&gt;score_threshold&lt;/code&gt;, or your RAG pipeline will hallucinate politely
&lt;/h3&gt;

&lt;p&gt;By default, search returns the &lt;code&gt;limit&lt;/code&gt; nearest results &lt;strong&gt;no matter how far&lt;br&gt;
away they are&lt;/strong&gt;. Ask about something your collection knows nothing about, and&lt;br&gt;
you still get back the top 5 "closest" chunks — which are garbage — and your&lt;br&gt;
LLM will confidently synthesize an answer from them.&lt;/p&gt;

&lt;p&gt;The fix is one parameter plus one honest code path:&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="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_points&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;score_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I don&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t have information about that.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A threshold around 0.7 is a reasonable starting point for OpenAI embeddings,&lt;br&gt;
but calibrate it per model — score distributions vary a lot. The empty-results&lt;br&gt;
branch is not an edge case; it's the feature.&lt;/p&gt;


&lt;h2&gt;
  
  
  Your first incident
&lt;/h2&gt;
&lt;h3&gt;
  
  
  7. HNSW tuning: know which knob to turn first
&lt;/h3&gt;

&lt;p&gt;Three parameters control the recall/speed/memory trade-off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ef&lt;/code&gt; (search time)&lt;/strong&gt; — beam width during search. Tune this &lt;strong&gt;first&lt;/strong&gt;: it
needs no rebuild and is often all you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ef_construct&lt;/code&gt; (default 100)&lt;/strong&gt; — beam width during index build. Higher =
better graph quality, but 3–5× slower ingest. Requires rebuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;m&lt;/code&gt; (default 16)&lt;/strong&gt; — edges per node. Higher = better recall and more
memory, permanently. Requires rebuild.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the debugging sequence when recall is too low: raise &lt;code&gt;ef&lt;/code&gt; → if that's not&lt;br&gt;
enough, raise &lt;code&gt;ef_construct&lt;/code&gt; and rebuild → only then touch &lt;code&gt;m&lt;/code&gt;. Going straight&lt;br&gt;
to &lt;code&gt;m=64&lt;/code&gt; because a blog post said so costs you memory forever.&lt;/p&gt;
&lt;h3&gt;
  
  
  8. Snapshots are your backup primitive — and they don't schedule themselves
&lt;/h3&gt;

&lt;p&gt;Self-hosted Qdrant has no automatic backups. The primitive is the snapshot:&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="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_snapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_docs&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;Three things to internalize before the incident, not during:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nothing triggers snapshots for you.&lt;/strong&gt; Cron it, or it doesn't happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A snapshot on the same disk as the data protects you from nothing.&lt;/strong&gt;
Ship it off-node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replication is not backup.&lt;/strong&gt; &lt;code&gt;replication_factor &amp;gt; 1&lt;/code&gt; in distributed mode
gives you high availability — it cheerfully replicates your bad deploy's
deletions too.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;(Qdrant Cloud handles backups for you — this one is squarely a self-hosting&lt;br&gt;
gotcha.)&lt;/p&gt;


&lt;h2&gt;
  
  
  Two you'll be glad you knew
&lt;/h2&gt;
&lt;h3&gt;
  
  
  9. Sparse vectors are a different type, and hybrid search is a query shape
&lt;/h3&gt;

&lt;p&gt;Sparse vectors (for BM25-style keyword matching) are not "dense vectors with a&lt;br&gt;
different flag." They're configured separately (&lt;code&gt;sparse_vectors_config&lt;/code&gt; with&lt;br&gt;
&lt;code&gt;SparseVectorParams&lt;/code&gt;) and use their own value type&lt;br&gt;
(&lt;code&gt;SparseVector(indices=[...], values=[...])&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;And hybrid search isn't a magic &lt;code&gt;hybrid=True&lt;/code&gt; parameter — it's a query shape:&lt;br&gt;
two &lt;code&gt;prefetch&lt;/code&gt; sub-queries (one dense, one sparse) fused with Reciprocal Rank&lt;br&gt;
Fusion:&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="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_points&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prefetch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Prefetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dense_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dense&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Prefetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SparseVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[...],&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[...]),&lt;/span&gt;
            &lt;span class="n"&gt;using&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sparse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FusionQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fusion&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fusion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RRF&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you see it as composition rather than configuration, the whole Query API&lt;br&gt;
makes more sense.&lt;/p&gt;
&lt;h3&gt;
  
  
  10. One point can carry many vectors
&lt;/h3&gt;

&lt;p&gt;The model that finally clicked for me: a Qdrant point is not "a vector with&lt;br&gt;
metadata." It's an &lt;strong&gt;entity&lt;/strong&gt; that can hold multiple named dense vectors &lt;em&gt;and&lt;/em&gt;&lt;br&gt;
sparse vectors simultaneously:&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="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;text_embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;image_embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sparse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SparseVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[...],&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[...]),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's text search, image search, and keyword search over the same objects&lt;br&gt;
from &lt;strong&gt;one collection&lt;/strong&gt; — no syncing three stores, no duplicate payloads. If&lt;br&gt;
you're designing a multimodal or hybrid system, this is the feature to design&lt;br&gt;
around from the start (see gotcha #3: you can't bolt it on later without a&lt;br&gt;
re-ingest).&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern underneath
&lt;/h2&gt;

&lt;p&gt;Almost every item on this list is the same lesson wearing different clothes:&lt;br&gt;
&lt;strong&gt;Qdrant's defaults are tuned for the demo, and production is a set of&lt;br&gt;
explicit decisions&lt;/strong&gt; — index your filter fields, pick your distance metric on&lt;br&gt;
purpose, choose the right update operation, schedule your own snapshots,&lt;br&gt;
threshold your own scores.&lt;/p&gt;

&lt;p&gt;None of these are flaws; they're the configuration surface of a tool that&lt;br&gt;
trusts you. But the quickstart can't make those decisions for you, and the&lt;br&gt;
worst failures here are the silent ones. Better to meet them in a blog post&lt;br&gt;
than in an incident channel.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>qdrant</category>
      <category>vectorsearch</category>
    </item>
    <item>
      <title>Vector Search for Web3 Developers: Searching NFT Metadata with Qdrant</title>
      <dc:creator>midegdugarova</dc:creator>
      <pubDate>Thu, 25 Jun 2026 18:40:07 +0000</pubDate>
      <link>https://dev.to/midegdugarova/vector-search-for-web3-developers-searching-nft-metadata-with-qdrant-966</link>
      <guid>https://dev.to/midegdugarova/vector-search-for-web3-developers-searching-nft-metadata-with-qdrant-966</guid>
      <description>&lt;p&gt;If you've built anything on-chain, you know how NFT search works today: exact-match&lt;br&gt;
filters. &lt;code&gt;Background = Neon City&lt;/code&gt;. &lt;code&gt;Rarity = Legendary&lt;/code&gt;. &lt;code&gt;Eyes = Laser&lt;/code&gt;. Marketplaces&lt;br&gt;
are basically faceted databases — pick your traits, get your grid.&lt;/p&gt;

&lt;p&gt;That's perfect when you know exactly what you want. But it falls apart the moment a&lt;br&gt;
user thinks in &lt;em&gt;vibes&lt;/em&gt; instead of attributes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Show me a brooding warrior glowing with electric light."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's no &lt;code&gt;vibe = brooding&lt;/code&gt; trait. The words "brooding," "glowing," and "electric"&lt;br&gt;
might not appear in a single NFT's metadata. Exact-match search returns nothing.&lt;/p&gt;

&lt;p&gt;This is the gap &lt;strong&gt;vector search&lt;/strong&gt; fills — and it's a tool most Web3 developers&lt;br&gt;
haven't reached for yet. I'm going to show you how to add semantic search to NFT&lt;br&gt;
metadata in about 40 lines of Python, with &lt;strong&gt;no API keys, no Docker, and no cloud&lt;br&gt;
account&lt;/strong&gt;. Then I'll show you the part that actually matters for marketplaces:&lt;br&gt;
combining semantic search with the trait filters you already use.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Who I am, briefly:&lt;/strong&gt; I spent the last few years doing developer relations in&lt;br&gt;
blockchain. I'm now working in AI infrastructure, and the overlap between the two&lt;br&gt;
worlds is bigger than either side realizes. This post is one example.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The idea in one sentence
&lt;/h2&gt;

&lt;p&gt;Turn each NFT's text into a list of numbers (an &lt;em&gt;embedding&lt;/em&gt;) that captures its&lt;br&gt;
meaning, store those numbers in a vector database, and search by &lt;em&gt;meaning&lt;/em&gt; instead&lt;br&gt;
of by exact string match.&lt;/p&gt;

&lt;p&gt;If you've heard "embeddings" and "vectors" thrown around and tuned out — that's the&lt;br&gt;
whole concept. A model reads "a fluffy lavender bunny in cotton-candy clouds" and&lt;br&gt;
produces a 384-number fingerprint. Two NFTs with similar meaning get similar&lt;br&gt;
fingerprints, even if they share no words. Search becomes "find the closest&lt;br&gt;
fingerprints."&lt;/p&gt;
&lt;h2&gt;
  
  
  The stack (and why it's zero-friction)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://qdrant.tech" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;&lt;/strong&gt; — an open-source vector database written in Rust.
We'll run it in-memory so there's nothing to install or host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/qdrant/fastembed" rel="noopener noreferrer"&gt;FastEmbed&lt;/a&gt;&lt;/strong&gt; — runs the embedding model
locally. No OpenAI key, no rate limits, no per-call cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination matters. Every "intro to vector search" tutorial I tried as a&lt;br&gt;
newcomer wanted an OpenAI key, a Pinecone account, &lt;em&gt;and&lt;/em&gt; a Docker daemon before I&lt;br&gt;
could see a single result. Here you clone and run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"qdrant-client[fastembed]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: The data
&lt;/h2&gt;

&lt;p&gt;Real NFT metadata lives on IPFS or comes from an indexer like The Graph. For the&lt;br&gt;
demo, &lt;code&gt;data/nfts.json&lt;/code&gt; has 15 NFTs across three collections — cyberpunk samurai,&lt;br&gt;
kawaii animals, and mystical relics — each shaped like standard marketplace metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Neon Ronin #001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"collection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Neon Ronin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A masterless samurai cloaked in a rain-soaked trench coat, his katana humming with electric blue plasma..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"traits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"Background"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Neon City"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Weapon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Plasma Katana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Armor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Trench Coat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Rarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Legendary"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Turn metadata into something searchable
&lt;/h2&gt;

&lt;p&gt;Embedding models read text, so we flatten the structured metadata into one string —&lt;br&gt;
the description carries the vibe, the traits add concrete detail:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;nft_to_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;traits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&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;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nft&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&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;nft&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nft&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; Traits: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;traits&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Embed and index
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastembed&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TextEmbedding&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;qdrant_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;QdrantClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;qdrant_client.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VectorParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PointStruct&lt;/span&gt;

&lt;span class="n"&gt;embedder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TextEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BAAI/bge-small-en-v1.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 384-dim, local
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QdrantClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:memory:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                              &lt;span class="c1"&gt;# nothing to host
&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nft_metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vectors_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;VectorParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;384&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;nft_to_text&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="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="n"&gt;nfts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;vectors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nft_metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;PointStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nfts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We store the &lt;strong&gt;full metadata&lt;/strong&gt; as the payload. That's what lets us return rich&lt;br&gt;
results &lt;em&gt;and&lt;/em&gt; filter on traits in a moment.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: Search by meaning
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;qv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;query&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_points&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;collection_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nft_metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;qv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;query_filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now the payoff. Remember: &lt;strong&gt;none of these query words appear verbatim in the&lt;br&gt;
metadata.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query: "a brooding warrior glowing with electric light"
  0.691  Neon Ronin #103   A wandering swordsman bathed in soft teal light...
  0.670  Neon Ronin #014   A cybernetic warrior with a chrome jaw and glowing red optics...
  0.643  Neon Ronin #156   An armored general clad in glowing crimson nano-plates...

Query: "an adorable soft fluffy companion"
  0.680  Pastel Critter #210   An impossibly fluffy lavender bunny...
  0.658  Pastel Critter #299   A sleepy yellow duckling curled inside a teacup...

Query: "a cursed artifact with dark power"
  0.726  Ancient Relic #007   A weathered golden amulet inscribed with forgotten runes...
  0.711  Ancient Relic #019   A cracked obsidian dagger... humming with dark energy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three vibe-based queries, three clean separations across collections. The model&lt;br&gt;
understood "brooding warrior" maps to samurai, "fluffy companion" maps to cute&lt;br&gt;
animals, and "cursed artifact" maps to the obsidian necrotic dagger — without a&lt;br&gt;
single shared keyword.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 5: The part that matters for marketplaces
&lt;/h2&gt;

&lt;p&gt;Pure semantic search is a nice demo. But marketplaces live on trait filters, and&lt;br&gt;
your users won't give those up. The good news: &lt;strong&gt;you don't have to choose.&lt;/strong&gt; Qdrant&lt;br&gt;
filters the candidate set by traits &lt;em&gt;and&lt;/em&gt; ranks by semantic similarity in one query.&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;from&lt;/span&gt; &lt;span class="n"&gt;qdrant_client.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FieldCondition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MatchValue&lt;/span&gt;

&lt;span class="n"&gt;legendary_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;must&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;FieldCondition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traits.Rarity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;MatchValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Legendary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;powerful and regal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;legendary_only&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query: "powerful and regal" + filter Rarity = Legendary
  0.532  Pastel Critter #251   A chubby peach-colored hamster wearing a tiny crown...
  0.519  Neon Ronin #156       An armored general clad in glowing crimson nano-plates...
  0.501  Neon Ronin #001       A masterless samurai... katana humming with electric blue plasma.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things happened here. First, the filter did its job — &lt;em&gt;only&lt;/em&gt; Legendary-tier NFTs&lt;br&gt;
came back. Second, and this is my favorite result: the &lt;strong&gt;top hit is a crowned&lt;br&gt;
hamster&lt;/strong&gt;. The model connected "regal" to "wearing a tiny crown" — across the&lt;br&gt;
cute/fierce divide, with zero shared words. That's the difference between matching&lt;br&gt;
strings and matching meaning.&lt;/p&gt;

&lt;p&gt;This is the mental model shift for Web3 devs: your existing trait filters become the&lt;br&gt;
&lt;em&gt;structured&lt;/em&gt; layer, and vector search adds a &lt;em&gt;semantic&lt;/em&gt; layer on top. Same query, both&lt;br&gt;
worlds.&lt;/p&gt;
&lt;h2&gt;
  
  
  Going to production
&lt;/h2&gt;

&lt;p&gt;The only line that changes is the client:&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="c1"&gt;# Local dev:
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QdrantClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:memory:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Self-hosted:  docker run -p 6333:6333 qdrant/qdrant
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QdrantClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:6333&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Qdrant Cloud:
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QdrantClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://YOUR-CLUSTER.qdrant.io&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;...&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;Indexing, search, and filtering are identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: pointing it at a real collection
&lt;/h2&gt;

&lt;p&gt;The sample data is curated so the demo runs instantly, but you'll want real&lt;br&gt;
metadata. Here's the part I like as a Web3 dev: you don't need OpenSea's API, an&lt;br&gt;
Alchemy key, or even web3.py. NFT metadata lives on-chain — just read &lt;code&gt;tokenURI&lt;/code&gt;&lt;br&gt;
off the contract with a plain JSON-RPC call.&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;requests&lt;/span&gt;

&lt;span class="n"&gt;SELECTOR_TOKEN_URI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0xc87b56dd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# keccak256("tokenURI(uint256)")[:4]
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;token_uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rpc_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SELECTOR_TOKEN_URI&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;064x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jsonrpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eth_call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rpc_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# decode the ABI string: [32b offset][32b length][bytes]
&lt;/span&gt;    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&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;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;big&lt;/span&gt;&lt;span class="sh"&gt;"&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;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resolve the URI (it'll be &lt;code&gt;ipfs://&lt;/code&gt;, an HTTPS gateway, or an on-chain &lt;code&gt;data:&lt;/code&gt;&lt;br&gt;
URI), fetch the JSON, flatten its &lt;code&gt;attributes&lt;/code&gt;, and index it exactly like before.&lt;br&gt;
The repo's &lt;code&gt;fetch_nfts.py&lt;/code&gt; does all of this and then runs the same search on real&lt;br&gt;
&lt;a href="https://www.azuki.com/" rel="noopener noreferrer"&gt;Azuki&lt;/a&gt; tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Query: "someone holding a sword or katana"
  0.593  Azuki #7    Hair: Orange Samurai, Headgear: Full Bandana...
  0.578  Azuki #10   Hair: Green Samurai, Headgear: Black Bucket Hat...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query said "katana"; the results are the &lt;strong&gt;Samurai&lt;/strong&gt;-haired Azukis. No shared&lt;br&gt;
word — the model just understood the connection. One honest caveat worth knowing:&lt;br&gt;
real PFP collections usually leave &lt;code&gt;description&lt;/code&gt; empty and put everything in&lt;br&gt;
&lt;code&gt;attributes&lt;/code&gt;, so semantic search runs over trait &lt;em&gt;combinations&lt;/em&gt; ("a character with&lt;br&gt;
pink hair holding a katana") rather than prose. That's the real shape of NFT&lt;br&gt;
metadata, and vector search handles it cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;NFT metadata is the friendly on-ramp, but the same pattern unlocks a lot of Web3&lt;br&gt;
problems that exact-match search can't touch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"NFTs like this one"&lt;/strong&gt; recommendations — search with an existing token's vector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural-language marketplace search&lt;/strong&gt; — let users describe what they want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-chain text search&lt;/strong&gt; — ENS profiles, DAO proposals, governance threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wash-trading / anomaly detection&lt;/strong&gt; — find outliers by vector distance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full, runnable code is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/midegdugarova/web3-nft-vector-search" rel="noopener noreferrer"&gt;github.com/midegdugarova/web3-nft-vector-search&lt;/a&gt;&lt;/strong&gt;.&lt;br&gt;
Clone it, point it at a real collection's metadata, and you've got semantic NFT&lt;br&gt;
search in an afternoon.&lt;/p&gt;

&lt;p&gt;If you're building at the Web3 × AI intersection, I'd genuinely like to hear what&lt;br&gt;
you're working on — find me at &lt;a href="https://github.com/midegdugarova" rel="noopener noreferrer"&gt;github.com/midegdugarova&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>ai</category>
      <category>vectordatabase</category>
      <category>qdrant</category>
    </item>
  </channel>
</rss>
