[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"home-portfolio-en":3,"hero-profile-fallback":4,"about-profile-fallback":4,"stacks-fallback":153,"projects-fallback":38,"experiences-fallback":84,"contact-profile-fallback":4,"blog-latest-en-3":259},{"profile":4,"projects":38,"experiences":84,"stacks":153},{"name":5,"avatar_url":6,"years_experience":7,"github_url":8,"linkedin_url":9,"twitter_url":10,"email":11,"formspree_endpoint":12,"og_image_url":6,"location_city":13,"location_region":14,"location_country":15,"terminal_commands":16,"headline":23,"subtitle":24,"bio_html":25,"languages":26,"hobbies":31,"footer_tagline":34,"seo_title":35,"seo_description":36,"og_description":37},"Rodolfo De Bonis","https://rodolfodebonis.com.br/api/cdn/portfolio/me.jpeg",8,"https://github.com/RodolfoBonis","https://www.linkedin.com/in/rodolfodebonis","https://x.com/rdbonis","dev@rodolfodebonis.com.br","https://formspree.io/f/xrbkgpzo","Maceió","AL","BR",[17,18,19,20,21,22],"kubectl get pods --all-namespaces","go build -o server ./cmd/api","docker compose up -d","git push origin main","terraform apply -auto-approve","flutter run --release","Software Engineer","Software Engineer with 8+ years turning complex ideas into systems that just work","\u003Cp>I&rsquo;m Rodolfo, based in Maceió - AL. With 8+ years in the field I&rsquo;ve worked across mobile (Flutter, Swift, Android), backend (Go, Python), frontend (Vue, Angular) and infrastructure (Kubernetes, Terraform, CI/CD) — full stack for real, from app to cluster.\u003C/p>\n\u003Cp>My mission is simple: \u003Cstrong>minimize every manual task\u003C/strong> to focus on what truly matters. A well-tuned CI/CD pipeline is as satisfying as a nat 20 crit in D&amp;D.\u003C/p>\n\u003Cp>When I&rsquo;m not orchestrating clusters in the Rb Lab, you&rsquo;ll find me running epic D&amp;D 5e sessions or clutching rounds in R6 Siege.\u003C/p>\n",[27,28,29,30],"Go","Python","Dart","TypeScript",[32,33],"D&D 5e","R6 Siege","Made with ☕ and passion for code","Rodolfo De Bonis — Software Engineer","Software engineer with 8+ years of experience in Go, Flutter, Kubernetes and DevOps.","Software Engineer | DevOps | Mobile | D&D Master",[39,56,69],{"id":40,"slug":41,"image_url":42,"github_url":43,"tags":44,"title":50,"description_html":51,"highlights":52},"0459f986-5825-45f7-b538-47181275ecdc","hermes","/api/cdn/portfolio/hermes.png","https://github.com/RodolfoBonis/hermes",[27,45,46,47,48,49],"Flutter","Kafka","RabbitMQ","K3s","Vault","Hermes","\u003Cp>Unified notification system handling Push, WhatsApp, Telegram and Slack with tag-based segmentation and automatic retries.\u003C/p>",[53,54,55],"Multi-tenant with no data leakage between customers","Under-200ms latency for critical push notifications","Monitored with Prometheus + Grafana",{"id":57,"slug":58,"image_url":59,"github_url":60,"tags":61,"title":64,"description_html":65,"highlights":66},"18e21888-ea54-4fbb-a698-3d6bd411ce53","microdetect","/api/cdn/portfolio/microdetect.png","https://github.com/RodolfoBonis/microdetect",[28,62,63,45],"YOLOv8","FastAPI","MicroDetect","\u003Cp>Computer-vision system for classifying and counting yeast cells. Graduation project using YOLOv8 for detection, FastAPI as backend and Flutter for the desktop UI.\u003C/p>",[67,68],"Microscopy dataset with 1,200+ hand-labelled images","Real-time count charts via Flutter Desktop",{"id":70,"slug":71,"image_url":72,"github_url":73,"tags":74,"title":78,"description_html":79,"highlights":80},"5051b75e-204e-4f25-bcf0-6ee1dd52b179","portfolio","/api/cdn/portfolio/portfolio-pessoal.png","https://github.com/RodolfoBonis/portfolio",[75,76,30,77],"Nuxt 3","Tailwind CSS","ArgoCD","Personal Portfolio","\u003Cp>This very portfolio! Built with Nuxt 3 and Tailwind CSS. Industrial design with a terminal aesthetic, deployed automatically through GitHub Actions + ArgoCD.\u003C/p>",[81,82,83],"SSR with Nuxt 3 for SEO","Dark/light mode with persistence","End-to-end automated CI/CD",[85,99,117,133,143],{"id":86,"slug":87,"period_start":88,"current":89,"tech_tags":90,"title":93,"description_html":94,"achievements":95},"0d8d6fd5-f0cf-4273-8de1-8b06dd56f654","freelance","2020-01-01T00:00:00Z",true,[48,49,91,92],"Home Assistant","MQTT","Consultant & Freelancer","\u003Cp>Software development and automation consulting, focused on infrastructure and creative solutions.\u003C/p>",[96,97,98],"Full home-lab setup (K3s + Traefik + Vault + External Secrets)","Home automation via Home Assistant + MQTT","Discord bots and community moderation tooling",{"id":100,"slug":101,"period_start":102,"current":89,"tech_tags":103,"title":23,"company":108,"description_html":109,"achievements":110},"c68a6ac3-c761-4b4e-bb11-550603dcb391","loft","2019-01-01T00:00:00Z",[45,104,105,106,107],"GraphQL","Search","AI","DevOps","Vista / Loft","\u003Cp>Software Engineer at Loft. Took on the full mobile rewrite of the Vista app from Ionic to Flutter — solo. Since then I've worked on high-impact product initiatives.\u003C/p>",[111,112,113,114,115,116],"Full mobile rewrite from Ionic → Flutter (solo)","Conversational chat for finding the ideal property","Semantic search integrated with the platform","Cross-system integrations across internal services","Mentoring junior developers and interns","Contributions to architecture, code review and DevOps practices",{"id":118,"slug":119,"period_start":120,"period_end":102,"current":121,"tech_tags":122,"title":126,"company":127,"description_html":128,"achievements":129},"d7c08fb9-a752-4d1c-ab85-dcc141ea6b9c","innovate","2017-01-01T00:00:00Z",false,[123,124,45,125],"Node.js","Angular","MongoDB","Full Stack Developer","Innovate","\u003Cp>Hospital systems in Java and public-lighting management in Node.js/Angular/Flutter.\u003C/p>",[130,131,132],"1M+ lamp posts rendered on an interactive map with clustering","Mobile app migration from Ionic to Flutter","Backend with Node.js, Express and MongoDB",{"id":134,"slug":135,"period_start":120,"period_end":136,"current":121,"tech_tags":137,"title":126,"company":141,"description_html":142},"35f5df9c-6488-4460-9ff0-98bdd71d79fe","nobully","2018-01-01T00:00:00Z",[138,139,140],"JWT","WebSockets","OneSignal","NoBully","\u003Cp>School bullying-prevention platform with JWT auth, admin panel and realtime notifications.\u003C/p>",{"id":144,"slug":145,"period_start":146,"period_end":136,"current":121,"tech_tags":147,"title":150,"company":151,"description_html":152},"8acd27e3-eb27-41c9-adfc-1c4c9ca49924","uninassau","2016-01-01T00:00:00Z",[148,149],"ADS","TCC","Technologist in Systems Analysis and Development","Uninassau","\u003Cp>Uninassau — Graduation project: \"Multi-platform Endless Runner Game\".\u003C/p>",[154,171,200,230],{"id":155,"slug":156,"name":157,"items":158},"cb551e9f-8a07-4b09-8980-b3747530a1d7","backend","Backend & Languages",[159,162,165,168],{"id":160,"name":27,"icon_path":161},"66002d7c-3dd3-400a-8384-f4291ead8aa3","/icons/go.svg",{"id":163,"name":28,"icon_path":164},"d6f27d7d-326f-453f-b9ea-ffb4d79217ad","/icons/python.svg",{"id":166,"name":63,"icon_path":167},"fda0f22d-aca8-49f6-8d70-34752f9d203a","/icons/fastapi.svg",{"id":169,"name":123,"icon_path":170},"042061c8-d254-4ae4-95aa-4ae63ea369c4","/icons/nuxt.svg",{"id":172,"slug":173,"name":174,"items":175},"6bb4b3b1-a7a3-4a36-98c2-2ddfd2aee012","frontend","Frontend & Mobile",[176,179,183,185,188,192,196],{"id":177,"name":45,"icon_path":178},"e2a3d3d6-e4f8-4ea4-86e3-0d70f7978cbc","/icons/flutter.svg",{"id":180,"name":181,"icon_path":182},"09f11c44-1e48-43d1-8947-abf416f384f3","Vue.js","/icons/vue.svg",{"id":184,"name":75,"icon_path":170},"8e287df8-3c7b-4abb-b160-1f9fb317eade",{"id":186,"name":124,"icon_path":187},"ec94d850-c9f6-4111-8146-8ebdce87fa3f","/icons/angular.svg",{"id":189,"name":190,"icon_path":191},"cdbb10ed-d990-49e5-8180-b8b4b605934a","Swift","/icons/swift.svg",{"id":193,"name":194,"icon_path":195},"1f582dda-8831-493a-9f85-10e7cc670214","Android","/icons/android.svg",{"id":197,"name":198,"icon_path":199},"e34b095e-907e-4ef1-8c18-09aa9dd8311e","Ionic","/icons/ionic.svg",{"id":201,"slug":202,"name":203,"items":204},"2ee77353-a30f-4208-be2b-4d3546abb9fb","devops","DevOps & Infra",[205,208,212,216,219,223,226],{"id":206,"name":48,"icon_path":207},"5fa51ef9-2092-47eb-8c28-3796afc3f213","/icons/kubernetes.svg",{"id":209,"name":210,"icon_path":211},"ca383cf7-cb63-40a4-843c-25b07e744b0c","Docker","/icons/docker.svg",{"id":213,"name":214,"icon_path":215},"91daf341-1a43-46a9-ba11-de909a86aaf3","Terraform","/icons/terraform.svg",{"id":217,"name":77,"icon_path":218},"83187670-5199-4b46-b0ae-630d8667a97b","/icons/argocd.svg",{"id":220,"name":221,"icon_path":222},"b08b1526-1c40-4d03-ac57-9deac458f808","AWS","/icons/aws.svg",{"id":224,"name":49,"icon_path":225},"bbba1a02-e7c9-4f2d-b97e-a71eec687818","/icons/vault.svg",{"id":227,"name":228,"icon_path":229},"60bfe522-6122-422d-92c5-7039a4f5cb42","GitHub Actions","/icons/github-actions.svg",{"id":231,"slug":232,"name":233,"items":234},"3183b24c-2d18-4c58-b1db-fc84c5ce84d7","data","Data & Tooling",[235,239,243,247,251,255],{"id":236,"name":237,"icon_path":238},"02ed47cf-e3cb-4b2d-a93d-6a164ee173b2","PostgreSQL","/icons/postgresql.svg",{"id":240,"name":241,"icon_path":242},"e2ad3cb1-0ea4-4adc-8110-69b5d88b8f04","Redis","/icons/redis.svg",{"id":244,"name":245,"icon_path":246},"1fc15b79-d85d-4b48-a038-b200245915c1","Elastic","/icons/elasticsearch.svg",{"id":248,"name":249,"icon_path":250},"8428d006-6a7a-4b3d-9bd9-5e014614d4e0","MinIO","/icons/minio.svg",{"id":252,"name":253,"icon_path":254},"273db25e-6091-4fe4-b47f-82c863fd88ae","SonarQube","/icons/sonarqube.svg",{"id":256,"name":257,"icon_path":258},"a5630431-e48c-422c-a387-0716f6a45cb5","n8n","/icons/n8n.svg",[260],{"id":261,"tenant_id":262,"author":263,"status":265,"published_at":266,"cover_image_url":267,"reading_time_minutes":268,"view_count":269,"like_count":270,"featured":89,"created_at":271,"updated_at":272,"translations":273,"tags":281},"43c0f013-6a6b-4286-8e74-bbb0bd8eaf93","3272d9cc-43d0-4e23-8d75-3db7f042b2b3",{"sub":264,"name":5,"email":11},"554c6643-8b1c-4484-b190-4d9c71d0c275","published","2026-05-16T05:52:44.530779Z","https://rodolfodebonis.com.br/api/cdn/portfolio/blog-covers/198d42d7-9779-4d52-85bf-37d93abd1233.png",13,202,2,"2026-05-16T05:52:44.463814Z","2026-05-18T05:31:00.131636Z",[274],{"id":275,"post_id":261,"tenant_id":262,"lang":276,"slug":277,"title":278,"excerpt":279,"content_md":280,"created_at":271,"updated_at":271},"4ee0c693-b676-4ea5-ba02-d600111df7b7","en","how-semantic-search-works","Semantic Search: teaching machines to understand intent, not just words","BM25 still works, but it has four serious blind spots. Embeddings turn text into geometry — and that's the foundation of everything that's hyped in AI today. First in a series on modern search.","A few years ago, if you asked me how search works in a serious system, I'd answer in three words: inverted index, BM25, done. That was the state of the art, that was what ran everywhere, and that was what I knew well enough to teach.\n \nToday, after putting semantic search into production on top of nearly a million documents, I'd change my answer. Not because BM25 got worse — quite the opposite, it's still the foundation of almost every search system in the world. I'd change it because BM25 alone is leaving a lot of value on the table. And what fills that gap is an idea that looks like magic, until you understand what's actually happening underneath.\n \nThis is the first post in a series on modern search. Here we cover **semantic search** — what it is, why it works, how to use it. The next post adds BM25 and vector search together as **hybrid search**. The third closes with **reranking**, the cherry that separates good search from excellent search. But first we need to understand the underlying problem.\n \n## The problem most people don't notice\n \nImagine you're building a movie catalog. Could be a Letterboxd, an IMDB, an internal app. A user shows up and types:\n \n> \"movie about a guy stuck in the same day\"\n \nYou know what they want. I know what they want. Anyone who's seen *Groundhog Day* knows what they want. The problem is that your database doesn't know.\n \nIf you're using what 99% of systems use — text search based on an inverted index — your database will take that query, look for the literal words (\"guy\", \"stuck\", \"same\", \"day\"), and return:\n \n- *Stuck on You* (it has \"stuck\" in the title)\n- *Day After Tomorrow* (it has \"day\" in the title)\n- Any documentary about prison life (because \"stuck\" shows up in summaries about confinement)\nThe result: the user doesn't find *Groundhog Day*, closes your app, opens Google, types exactly the same phrase. And Google finds it. Why? **Because Google isn't running `LIKE '%stuck%'`.**\n \nThe difference between \"search that works\" and \"search that frustrates the user\" doesn't live in which database you picked, or how many servers you threw at it. It lives in understanding that **words are not meanings**, and that there are tools to bridge that gap.\n \n## How the machine sees text: BM25 and friends\n \nBefore we talk about the pretty stuff, let's understand what's running almost everywhere today.\n \nWhen you send `\"comfortable white sneakers\"` to Elasticsearch (or OpenSearch, or Solr — all the same family), three things happen in sequence. First, **tokenization**: the string gets broken into individual words. Second, **stemming**: suffixes get cut to reduce morphological variations. \"comfortable\", \"white\", \"sneakers\" become \"comfort\", \"white\", \"sneak\". Third, that gets matched against the **inverted index**: a structure that, for each token, stores the list of documents where that token appears.\n \n```\nsneak    -> [12, 47, 89, 156, ...]\nwhite    -> [12, 102, 230, ...]\ncomfort  -> [47, 88, 102, ...]\n```\n \nDocument 12 shows up in two lists. Document 47 also. Document 88, in just one. The more lists a document appears in, the more likely it's relevant. But that's just the start.\n \nWhat ranks the results is **BM25** — Best Match 25, the twenty-fifth iteration of a family of algorithms that started in the 70s. It's the default in Elasticsearch, OpenSearch, and Solr. If you use text search anywhere, BM25 is what's scoring.\n \nThe formula looks intimidating, but it has only three ideas:\n \n**Term Frequency (TF)**: how many times the token appears in the document. More times, more relevant. But not linear — if it appears 50 times, it's not 50× better than appearing once. The formula saturates.\n \n**Inverse Document Frequency (IDF)**: rare terms are worth more. The word \"sneakers\" appears in 2% of your fashion catalog — high weight. The word \"the\" appears in 100% of documents — weight nearly zero. Makes sense: if you searched for \"white sneakers\", matching \"sneakers\" tells me a lot more about relevance than matching \"white\".\n \n**Length normalization**: a short document containing the term is more relevant than a long document containing the term, because the chance that it's actually about that thing is higher. Without this, a 5000-word technical manual would beat a short description just by volume.\n \nBM25 mixes these three things and produces a score. Higher score, higher in the results. It's elegant, it scales well, and it works reasonably in a closed domain. But it has four serious blind spots.\n \n## Where BM25 breaks\n \n**Synonyms.** User searches for \"cellphone\", catalog has \"smartphone\". Zero match. You can solve this with a manual synonym dictionary, but you're going to maintain that for English, Portuguese, regional slang, and every niche's jargon? Good luck.\n \n**Vocabulary.** User searches for \"lightweight clothes for hot weather\". Catalog has \"short-sleeve linen blouse\". Same intent, zero words in common. BM25 returns nothing.\n \n**Intent.** User searches for \"good movie to watch with my girlfriend\". What does that mean? BM25 will match on \"movie\", \"good\", and \"girlfriend\" and return random results.\n \n**Multilingual.** You indexed in English, the user searches in Spanish. Same content, different languages, BM25 has no way to know they're the same thing.\n \nThe problem, at the core, is the same: **BM25 looks at the surface of the text, not at the meaning.** It's a tool for token coincidence, not for understanding.\n \nThat's where the interesting part comes in.\n \n## Embeddings: text becomes geometry\n \nThe simplest and most powerful definition that exists:\n \n> **An embedding is a dense vector of N numbers that represents the meaning of a piece of text.**\n \nYou send the word \"pizza\" to the embedding model. It returns a list of — say — 1024 numbers between -1 and 1:\n \n```\n[0.21, -0.05, 0.78, 0.13, -0.42, ..., 0.09]\n```\n \nYou send \"lasagna\". It returns another list of 1024 numbers:\n \n```\n[0.19, -0.08, 0.81, 0.10, -0.40, ..., 0.07]\n```\n \nThe magic is that these two lists will be **nearly identical**. Not because the model saw \"pizza\" and \"lasagna\" together — though that helped during training — but because it learned that both live in the same semantic neighborhood: Italian food, main dish, pasta or dough base, dinner context.\n \nNow send \"Python\". The vector will be very different. Because Python is a programming language, it's tech, it's an entirely different context. The vector for \"Java\" will be similar to the one for \"Python\", because both are languages. Pizza and lasagna sit together in one corner, Python and Java sit together in another corner, cat and dog sit together in a third corner.\n \n**The distance between two vectors becomes a measure of semantic similarity.** That's the trick. You've converted text — which is symbolic, discrete, hard to compare — into geometry. And geometry, we know how to measure.\n \nIn practice, generating an embedding looks like this:\n \n```python\nfrom openai import OpenAI\n \nclient = OpenAI()\nresponse = client.embeddings.create(\n    input=\"pizza margherita\",\n    model=\"text-embedding-3-small\"\n)\nvector = response.data[0].embedding  # list of 1536 floats\n```\n \nText goes in, geometry comes out.\n \n## Algebra with meanings\n \nTo make this less abstract: because these vectors actually carry meaning, you can do math with them. Real math. The classic experiment, from Mikolov's 2013 paper:\n \n```\nvector(\"king\") - vector(\"man\") + vector(\"woman\") ≈ vector(\"queen\")\n```\n \nThe model learned, without anyone explicitly teaching it, that there's a masculinity-femininity axis in vector space. Another one:\n \n```\nvector(\"Paris\") - vector(\"France\") + vector(\"Italy\") ≈ vector(\"Rome\")\n```\n \nThe concept of \"capital of a country\" became a direction in space. You can subtract \"France\" to remove the \"specific country\" component, then add \"Italy\" to put it back. The result lands near the Italian capital.\n \nThis isn't pretty theoretical math. It's literally what's happening inside the model. That's why semantic search works: the model learned structure about the world, and you're doing geometry on top of that structure.\n \n## Where these numbers come from\n \nEmbedding models are neural networks trained on absurd amounts of text to learn these representations. The main families today:\n \n**Commercial APIs.** OpenAI (`text-embedding-3-small`, `text-embedding-3-large`), Cohere (`embed-v3`, strong on multilingual), Voyage AI. Expensive, but quality near the top of the leaderboard, no infra to maintain on your side.\n \n**State-of-the-art open-source.** BGE (from BAAI), E5 (from Microsoft), GTE (from Alibaba). You run it on your GPU, zero API cost, zero vendor lock-in. BGE-M3 and BGE-large multilingual compete well with OpenAI in many benchmarks.\n \n**The classic base.** Sentence-Transformers — the library that popularized all of this. Smaller, simpler models, great for prototyping.\n \n**How to choose?** Go to [MTEB](https://huggingface.co/spaces/mteb/leaderboard) — the Massive Text Embedding Benchmark, the public reference leaderboard. Pick a model in your cost and size range, and **test it on your domain**. A model that's good at English may be bad at Portuguese. A model that's good at short text may be bad at long documents. A model that's good at general domain may be bad at legal, medical, or technical vocabulary. Always measure.\n \n## How to compare two vectors\n \nYou have the query vector, you have the document vectors. How do you compare them? Three options, in the order you'll probably use them.\n \n**Cosine similarity** measures the angle between vectors, ignoring magnitude. It ranges from -1 to 1 (in practice, with text, it falls between 0 and 1). It's the default in the overwhelming majority of cases, because meaning lives in direction, not in the size of the vector.\n \n**Dot product** is the scalar product. It cares about direction *and* magnitude. If your vectors are normalized — and most modern models return normalized vectors — dot product is mathematically equivalent to cosine and cheaper to compute. Use it for optimization.\n \n**Euclidean distance** is the straight line between two points. It works, but it's less common with text. It shows up more with image embeddings.\n \nRule of thumb: start with cosine, switch to dot product when optimizing.\n \n## kNN, ANN, and the scale problem\n \nHow do you find the documents most similar to the query? The conceptual algorithm is **k-Nearest Neighbors (kNN)**. Four steps: take the query, generate its embedding, measure the distance to every document in the catalog, sort, and return the top K.\n \nIt works perfectly on a thousand documents. On a million, computing distance against every single one is O(n), infeasible in real time.\n \nThe solution is **ANN — Approximate Nearest Neighbors**. You give up a bit of precision to gain orders of magnitude in speed. Instead of comparing against everything, you compare against an intelligently chosen subset.\n \nThe most popular algorithm today is **HNSW — Hierarchical Navigable Small World**. It works like a world map with multiple zoom levels: you start at the highest level, navigate quickly until you're close to the answer, then descend into more detailed levels and refine. From O(n) you drop to O(log n). It's what Elasticsearch uses, what pgvector uses, what practically every vector database uses underneath.\n \nIn production, recall of 95-98% is easy to hit with a well-configured HNSW, and latency lands in the milliseconds.\n \n## Where to run this\n \nThe ecosystem has exploded in the last few years. I split it into two families.\n \n**Dedicated vector databases**: Pinecone (SaaS), Weaviate, Qdrant, Milvus, Chroma. They were born for this. They generally deliver better performance and more mature features for vector search. The downside is it's one more database to maintain.\n \n**Databases that added vector support**: Elasticsearch and OpenSearch (already had mature text search, got vector); PostgreSQL with the pgvector extension; Redis; MongoDB. The advantage here is reusing a stack you already have. The downside is that features and performance sometimes aren't as polished as in the dedicated ones.\n \nHow to decide? If you already have Elasticsearch in production, **start by adding vector search to it**. Don't switch databases over a new feature. If you're starting from zero and want simplicity, Qdrant and Weaviate are great. If you're Postgres-first and your volume is moderate, pgvector handles it. There's no single answer — there's tradeoff.\n \n## The movie example, now with semantics\n \nRemember the query from the beginning? \"movie about a guy stuck in the same day\". Before, with BM25, it returned *Stuck on You* and *Day After Tomorrow* because of the literal words. Now, with vector search:\n \n| Position | Movie | Score |\n|---|---|---|\n| 1 | *Groundhog Day* | 0.89 |\n| 2 | *Edge of Tomorrow* | 0.85 |\n| 3 | *Palm Springs* | 0.81 |\n \nNotice the important detail: the synopsis of *Groundhog Day* says \"a man relives the same day repeatedly\". The word \"stuck\" never appears. But the model understood that being in a time loop is a form of being stuck in time. *Palm Springs* is an indie film many people have never heard of — but the model knows it, because it trained on descriptions from the entire internet.\n \nSame query. Completely different results. **No synonym dictionary. No manual rules.** The model just did geometry.\n \n## Where vector search also breaks\n \nBefore you walk away from here with stars in your eyes — vector search has serious blind spots too.\n \n**Exact identifiers.** User searches for `SKU-A4729`. Vector search will return things semantically similar, which is not what they want. For SKUs, product codes, IDs, order numbers — you need exact match, not similarity.\n \n**Negations.** \"Shoe without laces\" might return shoes with laces, because the concept \"laces\" is strongly represented in the query vector. Modern models handle this better, but it remains fragile.\n \n**Very short or ambiguous queries.** \"java\" — is it the language, the island, or the coffee? BM25 also suffers, but vector search doesn't magically solve it.\n \n**Cost.** Generating an embedding per document costs. Vector storage costs (1024 dimensions × 4 bytes × N documents). Reindexing when you switch models costs. The latency of generating an embedding for every query also costs.\n \nWhen you stack the blind spots — exactness, short queries, cost — it becomes clear: **replacing BM25 with vector search is trading one set of problems for another**.\n \n## The answer is to combine, not replace\n \nThe good news is that these two worlds have complementary blind spots. BM25 is strong exactly where vector search is weak: exact match, SKUs, specific keywords. Vector search is strong exactly where BM25 is weak: meaning, intent, divergent vocabulary.\n \nWhen you combine them, what one misses the other catches.\n \nThat's what the next post is about: **hybrid search**. How to run BM25 and vector search in parallel, how to fuse the rankings without falling into the obvious trap of weighting score against score, and why this is the architecture that delivers the best results in production from day zero, with no manual weight tuning.\n \nUntil then, if you've never played with embeddings, **open a notebook**. Grab the OpenAI API (or run BGE locally), embed a few dozen sentences from your domain, compute cosine between them, and see what clusters together. It's the best way to internalize the idea: to see with your own eyes that the model actually understands.\n \n---\n \n*This is the first post in a series on modern search. Next: hybrid search with Reciprocal Rank Fusion. If you're applying this in some context, drop me a message — I'd love to know where.*",[282,286,290,294],{"id":283,"tenant_id":262,"slug":284,"created_at":285},"3600c7f5-46a1-44c1-ab4d-14b54bc49300","busca","2026-05-16T05:37:43.100096Z",{"id":287,"tenant_id":262,"slug":288,"created_at":289},"7bfa9291-72ca-46cf-b0e0-4ff8d29a4f78","embeddings","2026-05-16T05:38:28.835806Z",{"id":291,"tenant_id":262,"slug":292,"created_at":293},"c0801447-701e-4cf6-8859-e4cf89f9d8a0","machine-learning","2026-05-16T05:38:07.501314Z",{"id":295,"tenant_id":262,"slug":296,"created_at":297},"43e39379-ec07-4aff-82a6-2946c56fa4f2","ml","2026-05-16T05:39:39.427214Z"]