<?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>AI and Devrel</title>
      <dc:creator>midegdugarova</dc:creator>
      <pubDate>Fri, 26 Jun 2026 14:57:24 +0000</pubDate>
      <link>https://dev.to/midegdugarova/ai-and-devrel-4c08</link>
      <guid>https://dev.to/midegdugarova/ai-and-devrel-4c08</guid>
      <description>&lt;p&gt;A few weeks ago I pointed an AI at a vector database's documentation and told it to find everything wrong. It returned 47 findings in a couple of minutes- broken code, missing sections, explanations that would lose a beginner. As it often happens with AI, I was impressed for about as long as it took to start checking them.&lt;/p&gt;

&lt;p&gt;About a third didn't survive a second look - the model (Opus) flagged a "contradiction" between two&lt;br&gt;
sentences on one page —  one was about search results being &lt;em&gt;equivalent&lt;/em&gt; and the other about them being processed more &lt;em&gt;efficiently&lt;/em&gt;. Both true, no conflict. It told me a parameter&lt;br&gt;
was undocumented when there was a note explaining it three lines down. It wanted me to fix things that were already right.&lt;/p&gt;

&lt;p&gt;The findings that &lt;em&gt;were&lt;/em&gt; real — a deprecated API call, a quickstart shipping a literal&lt;br&gt;
&lt;code&gt;{collection_name}&lt;/code&gt; placeholder, an arithmetic error in a capacity formula-  were genuinely&lt;br&gt;
useful. However I only knew which oens to trust because I'd read every page myself. The value of spared time wasn't in the generation, but the verification.&lt;/p&gt;

&lt;p&gt;And then i did think a lot about the nature of things for an hout or so - particularly about Ai and jobs - what we all think about occasionaly.&lt;/p&gt;

&lt;p&gt;Is AI coming for the DevRel job? Yes. Is that the whole story? No (it usually never it). Which part is it coming for - thats the question.&lt;/p&gt;

&lt;p&gt;Here's the honest version. What AI is good at now is the thing that used to fill my calendar.&lt;br&gt;
It writes a competent getting-started tutorial. It drafts API reference. It explains what an&lt;br&gt;
embedding is in a clean paragraph. For years "developer relations" quietly meant "content&lt;br&gt;
mill" — ship the tutorial, ship the blog, ship the next one — and a model can do the first&lt;br&gt;
draft of all of it.&lt;/p&gt;

&lt;p&gt;I thought that would scare me - the AI presence in  our everyday professional ives, but it actually brings relief.  If the production part gets cheap, the role gets to be about the part that was always the actual point. I've been&lt;br&gt;
trying to name that part, and I keep landing on three things. None of them is writing.&lt;/p&gt;

&lt;p&gt;The first is judgement. The docs audit is the small version of it: plausible is not the same&lt;br&gt;
as correct, and someone has to know the difference — which finding is real, which example will&lt;br&gt;
quietly mislead a beginner, which "best practice" is two versions out of date. That comes from&lt;br&gt;
having built the thing and sat with the ways it breaks. A model produces confidence at volume.&lt;br&gt;
It can't tell you when its own confidence is wrong.&lt;/p&gt;

&lt;p&gt;The second is reading the room. A model sees the text in a Discord thread. It doesn't feel the&lt;br&gt;
thread. It can't tell that a respected person has gone quiet, that the same frustration is&lt;br&gt;
surfacing in three channels in a tone curdling from "confused" to "done," that the loud&lt;br&gt;
complaint is noise and the one-line aside is the real signal. I spent years inside blockchain&lt;br&gt;
communities —  some of the most opinionated, unfiltered audiences in&lt;br&gt;
software, where feedback arrives fast and feelings are load-bearing. You learn to read mood.  Which quiet is people thinking, and which quiet is people leaving. No model&lt;br&gt;
does that yet.&lt;/p&gt;

&lt;p&gt;The third is knowing which developer you're actually talking to. Ask an AI for a RAG tutorial&lt;br&gt;
and you get the average one: Python, the common path, no opinion. But there's no average&lt;br&gt;
developer. There's the ML engineer who wants it to work end to end on the first try, the data&lt;br&gt;
scientist with ten million embeddings already sitting in a numpy array, the TypeScript&lt;br&gt;
developer tired of every example assuming they'll spin up a Python server. Serving them means&lt;br&gt;
taking a side about who you're writing for — and noticing when a whole group is being quietly&lt;br&gt;
left out. AI averages. The job is to choose.&lt;/p&gt;

&lt;p&gt;There's one more, and it's the one I care about most, even though it's the least glamorous:&lt;br&gt;
&lt;strong&gt;treating docs and developer relations as a product you measure, not a feed you publish into and&lt;br&gt;
forget.&lt;/strong&gt; The numbers most content people never look at. Time to first successful query.&lt;br&gt;
Self-service rate in the support channel. Which pages show up before someone sticks around, and&lt;br&gt;
which show up before they leave. I built an ecosystem health dashboard once that tracked exactly&lt;br&gt;
this kind of signal — activity, growth, where teams got stuck — and it moved strategy in a way&lt;br&gt;
no count of published posts ever did. Run docs as a product and "we shipped 12 tutorials this&lt;br&gt;
quarter" stops being a win. "People reach their first working query 40% faster" starts being&lt;br&gt;
one. AI can write the tutorials. It can't tell you which number matters, or have the taste to&lt;br&gt;
kill a popular page that isn't helping anyone.&lt;/p&gt;

&lt;p&gt;Why write this down? I'm not totally sure. Partly to make myself say it clearly instead of&lt;br&gt;
half-thinking it on the way to something else. Partly because I use these tools every day — I&lt;br&gt;
used one for that docs audit — and I'd rather be honest about what they did and didn't do than&lt;br&gt;
pretend in either direction.&lt;/p&gt;

</description>
      <category>devrel</category>
      <category>docsasproduct</category>
      <category>vectordatabase</category>
    </item>
    <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>
