<?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>Forem: Cihangir Bozdogan</title>
    <description>The latest articles on Forem by Cihangir Bozdogan (@cihangir_bozdogan_76b8c99).</description>
    <link>https://forem.com/cihangir_bozdogan_76b8c99</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1821244%2F76a39809-150a-4587-8aa1-683743112e31.jpg</url>
      <title>Forem: Cihangir Bozdogan</title>
      <link>https://forem.com/cihangir_bozdogan_76b8c99</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/cihangir_bozdogan_76b8c99"/>
    <language>en</language>
    <item>
      <title>Vector Retrieval Quietly Replaced Keyword Match, and the SEO Stack Did Not Notice</title>
      <dc:creator>Cihangir Bozdogan</dc:creator>
      <pubDate>Mon, 04 May 2026 11:44:41 +0000</pubDate>
      <link>https://forem.com/cihangir_bozdogan_76b8c99/vector-retrieval-quietly-replaced-keyword-match-and-the-seo-stack-did-not-notice-16o8</link>
      <guid>https://forem.com/cihangir_bozdogan_76b8c99/vector-retrieval-quietly-replaced-keyword-match-and-the-seo-stack-did-not-notice-16o8</guid>
      <description>&lt;p&gt;&lt;em&gt;How dense embedding retrieval replaced BM25 in modern AI search, what the mechanism actually does, and why exact-match SEO tactics quietly stopped working.&lt;/em&gt;&lt;br&gt;
There is a page I audited last year that ranks well gets cited, gets quoted, gets used as a source by AI assistants for a phrase nobody types. The literal string appears nowhere in the document. The document is about the topic, plainly and accurately, in clear prose. The query is a paraphrase. Twenty years of SEO heuristics would predict this page does not match. The retrieval stack thinks it matches better than half the pages that do contain the literal phrase. The inverse also happens: a page that uses a query's exact terms three times in the title and twice in the H1, and is not getting cited at all, because the embedding model thinks the page is about something different from what the user asked. Same query class, two outcomes and the difference is mechanical. The retrieval stack changed underneath, and most of the SEO heuristics the industry still teaches are heuristics about a stack that is now the second-stage filter, not the first.&lt;/p&gt;

&lt;p&gt;I built my mental model the slow way. I read the BEIR benchmark paper end to end, then DPR, then ColBERT, then HNSW, and then sat with a public embedding model and a corpus of my own running similarity computations against synonym pairs, paraphrase pairs, and adversarial pairs until the behaviour stopped surprising me. After that I started watching what happened to AI citations when pages were rewritten in different ways exact-match tightened, paraphrases added, exact-match stripped while semantics preserved. The pattern that fell out is not subtle, and it overturns several pieces of SEO advice that are still being repeated as if they were neutral facts.&lt;/p&gt;

&lt;p&gt;This post is the field report. The shift from sparse to dense first-stage retrieval, what an embedding model actually represents about a page and a query, why approximate nearest neighbour search is the workhorse of the recall step, why dense-only retrieval fails in specific predictable ways and why hybrid retrieval is the production answer, and what all of that means for content design. It is technical because the mechanism is technical. The shortcuts the SEO industry has been selling are shortcuts to the wrong stack.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Two Decades of BM25
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9reodfxwt3ib8h2db508.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9reodfxwt3ib8h2db508.png" alt=" " width="800" height="498"&gt;&lt;/a&gt;&lt;br&gt;
For roughly twenty years, the dominant first-stage retrieval algorithm on the open web and inside almost every search engine, on-site search, and Lucene/Elasticsearch deployment was BM25, formalised by Robertson and Zaragoza in their 2009 retrospective "The Probabilistic Relevance Framework: BM25 and Beyond." BM25 is a sparse, lexical, term-frequency-based scorer. It builds an inverted index of terms to documents. At query time it scores documents by how often the query terms appear, weighted by inverse document frequency, with saturation and length normalisation parameters bolted on. The mathematics is closed-form, the index is small, and the recall is reasonable for queries whose terms overlap exactly with the document.&lt;/p&gt;

&lt;p&gt;BM25 has properties the SEO industry built an entire grammar around. It rewards keyword presence. It is sensitive to keyword frequency up to a saturation point. It penalises long documents to prevent stuffing. It cannot match a paraphrase. It cannot infer that "vehicle" and "automobile" are the same concept. It cannot tell that "how to fix a slow website" and "improving page load performance" are about the same question. The keyword-research industry, on-page-optimisation playbooks, exact-match domain folklore, the H1-must-contain-the-target-keyword reflex all of that grammar is downstream of how BM25 scores documents. When the retrieval stack scores on lexical overlap, the rational thing for authors is to engineer lexical overlap. So they did, for two decades.&lt;/p&gt;

&lt;p&gt;The thing that changed, quietly enough that most SEO commentary missed it, is that BM25 stopped being the only thing and on a growing share of the queries that matter for AI search, stopped being the dominant thing at the recall step.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Dense Retrieval Era
&lt;/h2&gt;

&lt;p&gt;Dense retrieval was not a single moment. It was a slow accumulation of papers that each made the dense approach better, cheaper, or more general. The two reference points worth knowing by name are DPR Karpukhin et al., 2020 and ColBERT Khattab and Zaharia, 2020. DPR demonstrated that a dual-encoder, where query and passage are each encoded independently into a dense vector and scored by inner product, could outperform BM25 on open-domain question answering by a substantial margin. ColBERT pushed the thinking further by keeping per-token embeddings and computing a late-interaction score, improving fine-grained matching while remaining tractable.&lt;/p&gt;

&lt;p&gt;The third reference point that brought rigour to the comparison is the BEIR benchmark Thakur et al., 2021. BEIR took eighteen heterogeneous IR datasets, ran the major sparse and dense retrievers across all of them in zero-shot mode, and published the comparison. The headline result was less tidy than the dense-retrieval marketing wanted: dense models trained on one domain did not always transfer to another, and BM25 remained surprisingly hard to beat on certain tasks. The honest reading of BEIR is that neither sparse nor dense is a universal winner alone, and hybrid systems combining both tend to dominate.&lt;/p&gt;

&lt;p&gt;That honest reading is the one production search systems implement. It is also the one most SEO advice ignores.&lt;/p&gt;
&lt;h2&gt;
  
  
  What an Embedding Model Sees in a Page
&lt;/h2&gt;

&lt;p&gt;The mechanism is worth tracing. An embedding model takes a sequence of tokens your page's text, broken into sub-word tokens by the model's tokeniser and runs them through a stack of transformer layers. Each token attends to every other token (or a windowed subset). The output is a sequence of contextualised token embeddings: each token carries information about the words that surround it. The model pools that sequence into a single vector often the embedding of a &lt;code&gt;[CLS]&lt;/code&gt; token, sometimes mean-pooling, sometimes a learned head. The result is a fixed-size vector, typically between 384 and 3072 dimensions depending on the model.&lt;/p&gt;

&lt;p&gt;What that vector represents is meaning, not surface text. Two paragraphs saying the same thing in different words produce vectors close in the embedding space. A paragraph about "the impact of caching on web performance" and a paragraph about "how stale responses speed up rendering" sit near each other even though they share almost no tokens. This is what dense retrieval does that BM25 never could. It is also why content that is "well-written about the topic" can outrank content that is "engineered for the keyword" the model is not counting tokens, it is comparing meaning.&lt;/p&gt;

&lt;p&gt;The flip side is that the embedding model is a learned model, not a dictionary. It has a training distribution. Concepts well-represented in training are mapped cleanly. Concepts that were absent or rare are mapped sloppily. Specific identifiers SKUs, model numbers, error codes, brand names that look like generic words frequently sit in regions of the embedding space with very low resolution. That is one of the dense-retrieval failure modes, and we will come back to it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What an Embedding Model Sees in a Query
&lt;/h2&gt;

&lt;p&gt;The query goes through the same model. The user's words or the query the LLM has rewritten on the user's behalf get tokenised, embedded, and pooled into a vector in the same space as the documents. The retrieval step is a nearest-neighbour search: which document vectors are closest to the query vector by cosine similarity or inner product?&lt;/p&gt;

&lt;p&gt;The query embedding does several things a BM25 query cannot. It handles paraphrase: "fastest way to deploy a Next.js app" lands near documents about "Next.js deployment latency," even though "fastest" is missing from one and "latency" from the other. It handles synonym disjunction softly: a query about "vehicles" partially matches documents about "cars" without a configured dictionary. It handles intent inference up to a point: a question lands closer to documents that answer it than to documents that ask similar questions, because the model has learned the difference from training data.&lt;/p&gt;

&lt;p&gt;What it does not do is handle exact identifiers well. A query for the SKU &lt;code&gt;BTX-449-G2&lt;/code&gt; returns high similarity only if the model tokenised it the same way for document and query, and embeddings of rare tokens are noisy. A query for the precise string &lt;code&gt;error E_INVALID_REDIRECT&lt;/code&gt; may end up near generic documents about redirect errors and miss the document that contains the exact string verbatim, because the model treats the rare code as low-information. That is why hybrid retrieval exists.&lt;/p&gt;

&lt;p&gt;Before we get there, there is a piece between the user's input and the embedding step that most operators forget about.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Invisible Query Rewrite
&lt;/h2&gt;

&lt;p&gt;When a model produces a search-grounded answer, the query that hits the retrieval stack is rarely the user's literal text. The model rewrites the question into one or more search queries sometimes expanding into sub-queries, sometimes paraphrasing, sometimes filling in implicit context from the conversation. ChatGPT search, Perplexity, Gemini grounded mode, Claude with the web search tool, and Bing Chat all do some form of query rewriting before retrieval. The stack downstream sees the rewritten query, not the user's words.&lt;/p&gt;

&lt;p&gt;This matters for content design. Optimising for the literal user query is a fool's errand because you do not see the literal query you see the query the model decided to send, already normalised and paraphrased. What you can optimise for is the cluster of paraphrases the model is likely to produce around a given intent. This is why writing "the same answer phrased multiple ways within one page" tends to win over "the same keyword repeated multiple times within one page" the paraphrased pages match more of the rewrite distribution, which is what actually hits the index.&lt;/p&gt;
&lt;h2&gt;
  
  
  Approximate Nearest Neighbour at Scale
&lt;/h2&gt;

&lt;p&gt;In principle the recall step is just nearest-neighbour search. In practice, exact nearest-neighbour search over hundreds of millions of vectors is infeasible at AI-search latencies. The production answer is approximate nearest neighbour, or ANN, and the dominant open-source algorithm is HNSW Hierarchical Navigable Small World graphs described by Malkov and Yashunin in 2016.&lt;/p&gt;

&lt;p&gt;HNSW is a graph-based index. The intuition is worth holding clearly because it explains why ANN is "good enough" for the first stage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HNSW conceptual structure (top layer is sparse, bottom layer is full)

  Layer 2 (sparse, long edges):    o ----------- o ----------- o
                                    \           /             /
  Layer 1 (denser, medium edges):   o --- o --- o --- o --- o
                                     \   /     \    \   /
  Layer 0 (full, short local edges): o-o-o-o-o-o-o-o-o-o-o-o
                                                ^
                                          query enters at top,
                                          greedy descent narrows
                                          neighbourhood at each layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A query enters at the top layer, which has few nodes connected by long edges. The algorithm greedily walks toward the query's nearest neighbour, drops down to the next layer using the current best node as the entry point, and repeats. By the time the search reaches the bottom layer which contains every vector the candidate region is already narrowed to a small neighbourhood, and the bottom-layer search only explores a few hundred nodes instead of the full corpus. The result is sub-linear search time with high recall, configurable through parameters that trade off recall against latency.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Faiss&lt;/code&gt;, the open-source library from Meta, implements HNSW alongside several other ANN structures including IVF (inverted file with coarse quantisation) and product quantisation. Pinecone, Weaviate, Qdrant, Milvus, pgvector, Vespa every production vector database is a variation on these ideas. HNSW dominates the discussion because it has consistently strong recall on high-dimensional vectors with reasonable memory overhead.&lt;/p&gt;

&lt;p&gt;The catch and it is the catch that hybrid retrieval was invented to address is that ANN is &lt;em&gt;approximate&lt;/em&gt;. The recall step returns the top-k by approximate similarity, not the true top-k. For most queries the top few results are stable. For queries with rare terms or out-of-distribution embeddings, the approximate index can miss the document that lexical search would have found trivially. Combined with the embedding model's own weaknesses on rare and exact terms, the dense-only path has predictable failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Dense Alone Loses
&lt;/h2&gt;

&lt;p&gt;There is a class of queries where pure dense retrieval is reliably worse than BM25.&lt;/p&gt;

&lt;p&gt;Queries with &lt;strong&gt;exact identifiers&lt;/strong&gt; product SKUs, model numbers, error codes, version strings, ISBNs, regulatory references are dense-retrieval's worst case. The embedding model has typically not seen &lt;code&gt;BTX-449-G2&lt;/code&gt; enough during training to give it a meaningful position in vector space. BM25 treats it as a token and finds the document instantly.&lt;/p&gt;

&lt;p&gt;Queries with &lt;strong&gt;brand names that overlap common words&lt;/strong&gt; &lt;code&gt;Apple&lt;/code&gt;, &lt;code&gt;Square&lt;/code&gt;, &lt;code&gt;Notion&lt;/code&gt;, &lt;code&gt;Linear&lt;/code&gt;, &lt;code&gt;Vector&lt;/code&gt; are a related case. The embedding model maps "Apple" closer to "fruit," "company," and "computer" by some learned blend. The query "Apple support phone number" sits in a region where consumer-electronics documents and grocery-aisle documents coexist. BM25 does not care about meaning and scores by literal token overlap.&lt;/p&gt;

&lt;p&gt;Queries about &lt;strong&gt;domains under-represented in training&lt;/strong&gt; niche legal corpora, regional regulatory texts, deeply specialised technical fields also tend to favour BM25 because the embedding model's resolution in those regions of the space is poor.&lt;/p&gt;

&lt;p&gt;Queries with &lt;strong&gt;negation and quantifiers&lt;/strong&gt; "papers that do not use BERT," "websites without a privacy policy" are hard for embedding models, which struggle to invert meaning. BM25 with explicit operators handles these better than naive dense retrieval, although in practice the LLM usually rewrites the query into something the dense retriever can handle.&lt;/p&gt;

&lt;p&gt;This is the empirical content of the BEIR result. Across eighteen datasets, no single retriever wins everywhere, and the cases where dense loses are not random they cluster around the failure modes above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hybrid Retrieval Is the Production Answer
&lt;/h2&gt;

&lt;p&gt;Production AI search systems do not pick sparse or dense. They run both, fuse the results, and let the rerank stage clean it up.&lt;/p&gt;

&lt;p&gt;The two common fusion approaches are Reciprocal Rank Fusion a simple, training-free recipe that sums the reciprocal of each document's rank in each list and learned combiners that train a model to score documents using both BM25 and dense scores as features. Vespa, Weaviate, Elasticsearch's hybrid search, Qdrant's BM25 + dense pipelines, and OpenSearch's neural-sparse hybrid all implement variations of these patterns. The rerank step that follows (a heavier cross-encoder that re-scores the top candidates) is its own conversation, and I am keeping it deliberately brief here. The point for retrieval is that the rerank cleans up the noise the recall step admitted, and the recall step is now hybrid rather than purely lexical.&lt;/p&gt;

&lt;p&gt;Here is the comparison that matters, framed as the characteristics of each path:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Sparse (BM25)&lt;/th&gt;
&lt;th&gt;Dense (embedding-based)&lt;/th&gt;
&lt;th&gt;Hybrid (sparse + dense)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Matches exact terms&lt;/td&gt;
&lt;td&gt;Yes, by construction&lt;/td&gt;
&lt;td&gt;Weakly, via tokenisation&lt;/td&gt;
&lt;td&gt;Yes (sparse rescues this)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matches paraphrases&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (dense provides this)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handles synonyms&lt;/td&gt;
&lt;td&gt;Only with explicit dictionary&lt;/td&gt;
&lt;td&gt;Yes, learned&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handles rare identifiers&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Weakly&lt;/td&gt;
&lt;td&gt;Yes (sparse rescues this)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handles negation&lt;/td&gt;
&lt;td&gt;Yes, with operators&lt;/td&gt;
&lt;td&gt;Poorly&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Robust to OOD vocabulary&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Poorly&lt;/td&gt;
&lt;td&gt;Yes (sparse rescues this)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recall vs latency at scale&lt;/td&gt;
&lt;td&gt;Inverted index, sub-linear&lt;/td&gt;
&lt;td&gt;ANN graph, sub-linear&lt;/td&gt;
&lt;td&gt;Run both, fuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index size&lt;/td&gt;
&lt;td&gt;Small (token postings)&lt;/td&gt;
&lt;td&gt;Large (vector per chunk)&lt;/td&gt;
&lt;td&gt;Sum of both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold-start on new content&lt;/td&gt;
&lt;td&gt;Immediate (just index tokens)&lt;/td&gt;
&lt;td&gt;Requires embedding compute&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That table is the operational summary of two decades of BM25 plus six years of dense-retrieval-at-scale. It explains why the production answer is hybrid and why neither extreme of the SEO debate "keywords are dead" or "keywords are all that matter" is correct. They are both signals. The retrieval stack uses both. Content that wins in AI search is content that survives both filters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Still Matters from the Lexical Era
&lt;/h2&gt;

&lt;p&gt;The dense retriever does not erase the lexical signal it adds a second signal next to it. Everything BM25 ever rewarded still partially matters, but the marginal return on stuffing the same term thirty times has collapsed. What survives:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exact entity names.&lt;/strong&gt; Brand names, product names, person names, location names these are what hybrid retrieval rescues from dense-only failure. If your brand is &lt;code&gt;Acme Software&lt;/code&gt;, that exact string needs to appear once on the page in plain text where the indexer can find it, somewhere unambiguous, with the surrounding paraphrases the embedding model can latch onto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exact identifiers.&lt;/strong&gt; SKUs, error codes, version strings, model numbers. Same story. Once on the page in the canonical form is what you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured data.&lt;/strong&gt; Schema.org JSON-LD remains load-bearing because it gives the indexing pipeline a clean entity graph that does not depend on parsing prose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brand spellings and variations.&lt;/strong&gt; If users search for both &lt;code&gt;e-mail&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt; or &lt;code&gt;Wi-Fi&lt;/code&gt; and &lt;code&gt;WiFi&lt;/code&gt;, both forms benefit from being present somewhere on the site. Embedding models are mostly robust here, not perfectly, and the BM25 leg is exact-only.&lt;/p&gt;

&lt;p&gt;What is no longer worth doing and was probably never worth doing as much as the SEO playbooks insisted is keyword density manipulation, exact-phrase repetition, and synonym dictionaries pasted into footers. The marginal return from these tactics in a hybrid stack is approximately zero, and in some cases negative because the embedding pooling step degrades under repetition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Content for Both Filters
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw22juvw1u343i9lxehya.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw22juvw1u343i9lxehya.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;br&gt;
The practical content rule is short and unromantic: &lt;strong&gt;write the answer once in the canonical phrasing, then write the paraphrases around it, then make sure the structure is parseable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The mechanism for each clause is real. Canonical phrasing gives BM25 the exact-match signal it needs. Paraphrases widen the embedding space the page covers, so the page lands close to a wider distribution of query rewrites. Parseable structure short paragraphs, one thought per chunk, headings that match the prose, schema where appropriate feeds the chunker and the structured-data layer downstream.&lt;/p&gt;

&lt;p&gt;The thing the SEO industry got wrong, and is still getting wrong, is the assumption that you must choose between exact-match and semantic richness. The hybrid stack does not force a choice. It rewards both, scored by different paths and fused. Pages that try to win on exact-match alone fail the dense filter on paraphrases. Pages that try to win on semantic richness alone fail the sparse filter on exact identifiers and brand names. Pages that do both which is what good prose has always been match more of the query distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Verify You Are Winning at the Embedding Layer
&lt;/h2&gt;

&lt;p&gt;This is the part of the post where I tell you to stop guessing and start measuring, because the measurement is cheap and the alternative is folklore.&lt;/p&gt;

&lt;p&gt;Pick a public embedding model &lt;code&gt;text-embedding-3-small&lt;/code&gt; from OpenAI, &lt;code&gt;voyage-3&lt;/code&gt; from Voyage, or a BGE model from BAAI (free). Pick a corpus of your own pages. Embed each page. Take a list of queries you believe should match those pages literal phrasings, paraphrases, adversarial cases embed those, and compute cosine similarity between every query and every page.&lt;/p&gt;

&lt;p&gt;What you are looking for is not absolute numbers embedding similarities are model-specific and not directly comparable across models. You are looking for ranks and gaps. For a query that should match page A, does page A come first? If it is buried under three tangentially related pages, your content is failing the dense filter, and the failure is diagnosable. Often the fix is a missing paraphrase, a buried answer the pooling step is averaging away, or a structure where the topic shifts halfway through and the pooled vector lands between two centroids.&lt;/p&gt;

&lt;p&gt;Run the same exercise with BM25 most search libraries (Elasticsearch, OpenSearch, Vespa, Tantivy, Whoosh) implement it in a few lines. Compare. The cases where the same query ranks the page differently between the two paths are the cases where hybrid retrieval will cover or expose your content. That comparison is the thing the SEO industry pretends not to need to do, because doing it makes the folklore harder to sell.&lt;/p&gt;

&lt;p&gt;I run this against my own content periodically, against competitors' content, and against the queries I expect AI assistants to rewrite mine into. It is the cheapest piece of due diligence in modern content engineering and consistently produces actionable findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Google
&lt;/h2&gt;

&lt;p&gt;Whenever the dense-retrieval story comes up, someone asks "did Google switch to vectors?" The honest answer is that Google's retrieval stack is hybrid, partially private, and has been neural-augmented since well before the LLM era RankBrain (2015) and BERT integration (2019) are the named layers, but those are not the entire stack, and the company has not published a definitive "we switched from BM25 to vectors on date X" statement because the truth is more complicated. The on-the-record position is hybrid: lexical features plus learned ranking plus several layers of neural processing in concert. AI Overview and Gemini's grounded mode add their own retrieval and synthesis on top. Treating Google's stack as either "still BM25 underneath" or "all vectors now" both mis-frame it. It is layered, hybrid, mostly private. The operational stance: assume both filters are present, design content that survives both, do not bet against either signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Synthesis
&lt;/h2&gt;

&lt;p&gt;The recall step in modern AI search is dense, not lexical but the production stack is hybrid, and that is the framing the SEO industry has not absorbed. Embedding models match meaning. BM25 matches tokens. Both fire. The pages cited by AI assistants are the pages that survive both filters, not the pages that game one.&lt;/p&gt;

&lt;p&gt;The single sentence: &lt;em&gt;retrieval is no longer keyword match; it is hybrid recall where the dense signal handles paraphrase and intent and the sparse signal rescues exact identifiers, and content design that ignores either filter loses on the queries the other one would have caught.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you only have time to internalise three things, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The first stage is hybrid, not lexical.&lt;/strong&gt; Dense retrieval handles paraphrase, intent, and synonyms. Sparse retrieval handles exact identifiers, brand names, and rare terms. Both fire on every query in production stacks. Content that engineers for one and ignores the other loses on the queries the other one would have caught.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The user's literal query is not the query that hits the retrieval stack.&lt;/strong&gt; LLM rewrites paraphrase, expand, and normalise the query before retrieval. Optimising for the literal user phrasing is optimising for a string the index never sees. Optimising for the cluster of paraphrases around an intent is what moves the needle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure your content with a public embedding model.&lt;/strong&gt; It costs almost nothing. Compute similarity between your pages and the queries you expect. Cases where a topically correct page ranks low in cosine similarity are cases where your content is failing the dense filter, and the failure is usually diagnosable. The SEO industry mostly does not do this, which is why so much advice is still keyword-stack folklore.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The page that ranks for the phrase nobody types is not magic. It is a page whose embedding sits close to the embedding of the query the user actually asked, in a space the model learned from a corpus closed before either of you wrote anything. The page that wins the exact-match phrase but does not get cited is the inverse: the lexical filter passed it, the dense filter dropped it, and the rerank step never saw it. Both outcomes are mechanical, both are addressable, and the content design that addresses both is what wins the hybrid retrieval stack which is the stack that decides what AI assistants see.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The retrieval-stack synthesis here is my own reading of the primary literature Robertson and Zaragoza's &lt;a href="https://dl.acm.org/doi/abs/10.1561/1500000019" rel="noopener noreferrer"&gt;The Probabilistic Relevance Framework: BM25 and Beyond&lt;/a&gt; (Foundations and Trends in Information Retrieval, 2009), Karpukhin et al., &lt;a href="https://arxiv.org/abs/2004.04906" rel="noopener noreferrer"&gt;Dense Passage Retrieval for Open-Domain Question Answering&lt;/a&gt; (arXiv:2004.04906, 2020), Khattab and Zaharia, &lt;a href="https://arxiv.org/abs/2004.12832" rel="noopener noreferrer"&gt;ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT&lt;/a&gt; (arXiv:2004.12832, 2020), Thakur et al., &lt;a href="https://arxiv.org/abs/2104.08663" rel="noopener noreferrer"&gt;BEIR: A Heterogeneous Benchmark for Zero-shot Evaluation of Information Retrieval Models&lt;/a&gt; (arXiv:2104.08663, 2021), Malkov and Yashunin, &lt;a href="https://arxiv.org/abs/1603.09320" rel="noopener noreferrer"&gt;Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs&lt;/a&gt; (arXiv:1603.09320, 2016), and the &lt;a href="https://github.com/facebookresearch/faiss" rel="noopener noreferrer"&gt;Faiss library source and documentation&lt;/a&gt; combined with observable behaviour from running my own embedding-similarity computations against my own corpus and watching what happened to AI citations after content rewrites. Where I have written "in my testing" or "the pattern I observe," that is exactly what I mean. The directional claims about exact-match SEO no longer paying are mechanistic embedding similarity is computable on any public model and the audit is reproducible but I am not making quantitative promises and the magnitude of any individual rewrite varies by domain, model, and query distribution. Provider behaviour is moving; verify against current docs and current model behaviour before shipping a strategy.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Published by Cihangir Bozdogan&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>seo</category>
      <category>llm</category>
    </item>
    <item>
      <title>Nine Search Backends, Nine Different Webs. Why AI Citations Diverge for the Same Query.</title>
      <dc:creator>Cihangir Bozdogan</dc:creator>
      <pubDate>Mon, 04 May 2026 11:33:42 +0000</pubDate>
      <link>https://forem.com/cihangir_bozdogan_76b8c99/nine-search-backends-nine-different-webs-why-ai-citations-diverge-for-the-same-query-5deg</link>
      <guid>https://forem.com/cihangir_bozdogan_76b8c99/nine-search-backends-nine-different-webs-why-ai-citations-diverge-for-the-same-query-5deg</guid>
      <description>&lt;p&gt;Run the same brand-query through ChatGPT, Gemini, Perplexity, Claude, and Grok. Read the citations. The cited URLs will not be the same, the brands featured will not be the same, and in roughly a third of cases one tool will cite your brand confidently while another does not mention it at all. The temptation is to reach for an algorithmic explanation different rerankers, different summarisation styles, different prompt scaffolds. The actual explanation is upstream of all of that. Different tools sit on top of different search backends, and the backends do not see the same web.&lt;/p&gt;

&lt;p&gt;I worked this out by running the same fifty brand-queries across nine AI tools for six months and logging every citation URL, every search-tool invocation, every backend signature I could pull out of the response trace. The divergence was not noise. It was structural. A page indexed by Bing but missing from Google's index simply does not show up in Gemini, no matter how well it is written. A site that Brave's crawler reaches but Tavily's reranker buries cannot win on Tavily-backed agents. The "AI search" abstraction collapses into a backend-coverage problem the moment you try to optimise systematically.&lt;/p&gt;

&lt;p&gt;This post is the field report. The publicly known map of search backends behind the major AI tools. What "different index" actually means at the crawl-and-rank layer. The fusion-layer wildcards Tavily, Exa, and similar APIs that sit between agents and the open web. The provider-internal indexes nobody talks about. Why citation patterns drift over months. The practical monitoring strategy for an operator who actually wants to see the gap rather than guess at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Backend Map: What Powers What
&lt;/h2&gt;

&lt;p&gt;The first useful exercise is drawing the map honestly, including the parts that are partly private. Some relationships are documented, some inferred from observable signatures, and some have shifted over time. I will mark each accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bing&lt;/strong&gt; powers Microsoft's own grounding stack Copilot in all its forms and the Grounding with Bing Search tool exposed through Azure AI Foundry. Microsoft documents the Grounding with Bing Search service as the canonical way for an Azure-hosted agent to ground responses on real-time public web data. The legacy Bing Search API was deprecated in mid-2025 in favour of the grounding-specific service. &lt;strong&gt;DuckDuckGo&lt;/strong&gt; has long sourced traditional links and images "largely from Bing" while layering its own crawler (DuckDuckBot) and specialised sources on top. The DuckDuckGo help page on results sources says exactly that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChatGPT search&lt;/strong&gt; is the trickiest cell on the map and the one where I want to be most careful. The Microsoft–OpenAI partnership originally placed Bing behind ChatGPT's web-search behaviour, and many secondary sources still describe the relationship that way. Then OpenAI launched ChatGPT search in October 2024 and explicitly positioned it as a competitor to Bing. OpenAI's own announcement describes the feature as "powered by real-time web search and partnerships with news and data providers" a deliberately broad framing. The Microsoft–OpenAI partnership was renegotiated through 2025 to give OpenAI more flexibility to use multiple cloud and search providers. The honest answer for "what backend powers ChatGPT search today" is a stack that includes Bing, includes direct news-publisher integrations, and increasingly includes OpenAI's own crawl. Treating it as pure Bing is wrong now. Treating it as pure first-party is also wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini and Google AI Overview&lt;/strong&gt; sit on Google Search. This one is documented unambiguously. Google's grounding documentation says "Grounding with Google Search connects the Gemini model to real-time web content" and exposes the result trace through &lt;code&gt;groundingChunks&lt;/code&gt;, &lt;code&gt;groundingSupports&lt;/code&gt;, &lt;code&gt;webSearchQueries&lt;/code&gt;, and &lt;code&gt;searchEntryPoint&lt;/code&gt;. Google's web index is the same index that powers conventional Google Search, with the same crawl and the same ranking signals. AI Overview is grounded on a subset of the top-ranked search results for the query, with the LLM synthesis on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude's web search tool&lt;/strong&gt; uses Brave Search as the third-party search provider. This is documented in Google's own Vertex AI documentation for Anthropic partner models, which lists Brave Search as the "third-party search service that Anthropic Web Search feature can call." Brave's API page lists Mistral AI, Cohere, Together.ai, and Snowflake among its users and frames itself as "the leading search tool for applications that use Claude MCP." Claude's grounded answers bottom out in Brave's index, which is independent of Bing and Google.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brave Search&lt;/strong&gt; runs an independent index. Brave's API page is direct about this: "The only search API with its own Web index at scale. Truly independent, lightning-fast, and built to power AI apps." They reinforce the point: "the Brave Search API is not a scraper that simply uses bots to query Google or Bing and repackage their results. Instead, it's our own independent index of the Web packaged with our own ranking models." The published index size is "over 30 billion pages." This matters because a Claude grounding session has a different starting set of candidate URLs than a Gemini grounding session, before any ranking or reranking even happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Perplexity&lt;/strong&gt; is the hybrid everyone notices and few describe accurately. Perplexity uses an internal crawler, an internal index (the answer engine running on top of it is branded "Sonar"), plus third-party search APIs. Public reporting and Perplexity's own help-centre material have at various times mentioned both Google-backed and Bing-backed paths. The exact mix has shifted over the product's life. Operators tracking Perplexity citations should not assume any single backend is the source of truth for what Perplexity sees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grok&lt;/strong&gt; searches X plus the open web through xAI's Web Search and X Search tools. The xAI documentation describes a web search tool that "enables Grok to search the web in real-time and browse web pages" and an X-platform search tool with keyword, semantic, user, and thread retrieval. The web component's underlying provider has not been publicly disclosed in the same way Anthropic's choice of Brave is disclosed. What is clear is that Grok's index is biased toward X-platform content in a way no other tool's is a brand with strong X presence shows up disproportionately on Grok and disproportionately not on tools that lack an X integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kagi&lt;/strong&gt; runs a metasearch architecture: two in-house indexes Teclis (web) and TinyGem (news) combined with "anonymised API calls to all major search result providers worldwide" plus specialised vertical sources. Kagi is small-scale relative to Bing or Google but maintains a distinctive in-house crawl focused on non-commercial, "small web" content. Kagi is not a backend for any major LLM but its index character is genuinely different from the dominant ones. &lt;strong&gt;You.com&lt;/strong&gt; runs its own real-time index plus vertical indexes for news, healthcare, legal, and similar independent of Bing and Google, smaller in scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tavily and Exa&lt;/strong&gt; are different in kind. They are not "search engines" in the Bing/Google sense. They are search APIs designed to be the retrieval layer for AI agents. Tavily describes itself as offering "real-time search, extraction, research, and web crawling through a single, secure API," with a "production-grade retrieval stack." Exa describes its product as an "industry-leading web index built for agents." Both decline to publicly name their upstream sources in the way Brave does, and both are widely understood by builders to combine custom crawling with dense retrieval and their own reranking on top. They are themselves a backend choice for any agent that wires them in.&lt;/p&gt;

&lt;p&gt;The honest summary of the map: nine major surfaces, four or five distinct primary backends, and at least three more API-layer products that act as their own backends when an agent is built on top of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "Different Index" Actually Means
&lt;/h2&gt;

&lt;p&gt;It is easy to gloss "different backends, different indexes" as near-equivalent. It is not. Indexes differ on more axes than most operators count, and each axis is a place where two backends diverge for the same query in ways that show up directly in citation behaviour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crawl frequency and freshness.&lt;/strong&gt; Common Crawl, which seeds many training corpora, adds 3–5 billion new pages per month according to its published methodology. Commercial backends crawl much more aggressively than that for high-value sites and less aggressively for the long tail. A new product page on a high-authority domain will hit Bing's index in hours and Google's in similar time. The same page on a small-authority domain might sit uncrawled by either for weeks. Brave's crawl prioritises differently again, and Tavily's and Exa's targeted crawls are shaped by which queries their customers run. A "freshness gap" between two backends is rarely a bug it is a budget allocation made deliberately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Geo and language coverage.&lt;/strong&gt; Google's index has significantly broader non-English coverage than Bing's. Brave is English-and-Latin-script biased. Tavily and Exa are English-dominant by default. A query in German or Japanese retrieves different breadth across these backends before any ranking layer touches the results. A citation gap in Gemini might disappear when you query in the brand's primary market language, while the same gap in Claude (Brave-backed) might persist because Brave's coverage thins out in non-English.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deep-link coverage and JavaScript-only content.&lt;/strong&gt; Backends differ sharply on how deep they crawl and how they handle JavaScript-rendered content. Bing and Google have invested heavily in headless rendering. Brave's public statements about its crawler are more conservative. Tavily and Exa's behaviour around JS-rendered content depends on their per-customer crawl budget. A brand that ships a JavaScript-only site will see different coverage curves across backends a fact that compounds with the well-known issue of inference-time fetchers being even less rendering-capable than crawl-time fetchers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Robots.txt and bot identifiers.&lt;/strong&gt; All major backends respect robots.txt, but the exact directives they honour differ at the edges. Backends respect specific bot identifiers (&lt;code&gt;GoogleBot&lt;/code&gt;, &lt;code&gt;BingBot&lt;/code&gt;, &lt;code&gt;BraveBot&lt;/code&gt;, &lt;code&gt;OAI-SearchBot&lt;/code&gt;, &lt;code&gt;ClaudeBot&lt;/code&gt;, etc.) and a robots policy that allows one and blocks another produces a hard coverage gap. Operators who tighten their robots.txt against AI training bots without thinking carefully about the search-time fetchers occasionally cut themselves off from grounding entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content-type coverage.&lt;/strong&gt; PDFs, video transcripts, podcast transcripts, and code repositories are covered very unevenly. Google handles PDFs more thoroughly than most. Code-heavy queries land differently on Brave because of how Brave indexes code-host content. Video-transcript surfacing depends on whether the backend has direct ingestion or transcript-extraction at crawl time. A brand whose primary content is a podcast or a video series will have a wildly different visibility profile across backends than one whose content is HTML articles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snippet length and chunk granularity.&lt;/strong&gt; Once a backend indexes a page, the chunk it stores is what determines whether a passage can become a citation. Brave publishes that its API returns "up to five snippets" per result. Google's grounded responses surface a different chunk shape. Tavily and Exa, being embedding-based, serve dense vectors over whatever chunk size their pipeline uses. If your page's information is densely packed in a single section that exceeds the backend's chunk granularity, that information may never enter a citation context window even when the page itself is in the index.&lt;/p&gt;

&lt;p&gt;The compound effect is that two backends covering "the open web" can diverge on something close to half of mid-frequency queries. The divergence is structural, not random.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fusion-Layer Wildcards
&lt;/h2&gt;

&lt;p&gt;Tavily and Exa deserve their own section because they are increasingly the retrieval layer that AI agents actually depend on, and they break the simple "what crawl do they use" frame.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How Tavily works:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fie8wacavbt035j8czrd7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fie8wacavbt035j8czrd7.jpeg" alt=" " width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How Exa works:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4905xtjrnfmak5nhm6cn.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4905xtjrnfmak5nhm6cn.jpeg" alt=" " width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A traditional search engine like Bing or Google does crawl, build an inverted index, rank with hundreds of signals, and return ranked URLs. Tavily and Exa are different. They crawl too, but they layer on dense retrieval, custom ranking, and explicit reranking optimised for LLM consumption. A page that ranks well on Bing can rank poorly on Tavily for the same query, because Tavily's ranker disagrees with Bing's. Not "is wrong" disagrees. The two systems optimise different objectives.&lt;/p&gt;

&lt;p&gt;This matters operationally because more and more AI agents particularly in the developer-tools space and in custom MCP-server stacks wire in Tavily or Exa rather than going through a public-web search backend. An operator who only monitors citation behaviour on ChatGPT, Gemini, and Claude is missing an entire layer of agent stacks that route their grounding through these API products. For most consumer-facing brands the agent-stack layer is small today. For B2B brands selling to developers, it is not small.&lt;/p&gt;

&lt;p&gt;The asymmetry in observability makes this harder to track. Tavily and Exa do not publish their crawl coverage or their reranking objectives in the way Google and Bing do. The way to see whether your brand is reachable through their backends is to query the API directly. There is no shortcut. A practical rule I have settled on: if a brand sells primarily to developers, Tavily and Exa coverage matters as much as Bing or Google coverage, even though no consumer ever uses Tavily directly. The retrieval layer for the agents the buyers are building is what determines whether the brand shows up in agent-driven workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First-Party Backends Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;There is a layer below the named backends that is increasingly load-bearing and partly private. I want to be careful here because the public documentation is thin. What I can say with confidence is what the observable signatures suggest, framed as observation rather than asserted fact.&lt;/p&gt;

&lt;p&gt;OpenAI's ChatGPT search is grounded on sources that do not look like pure Bing results. The October 2024 launch announcement mentioned "partnerships with news and data providers" alongside search backends. Looking at citation patterns from ChatGPT search across the last six months, I see consistent appearance of certain news domains in patterns that suggest direct ingestion rather than open-web ranking. That is consistent with OpenAI building its own crawl on top of licensed data feeds. I cannot prove it from public documentation alone but the observable pattern is real, and it is the pattern an operator should expect from a company that has explicitly positioned itself as a Bing competitor.&lt;/p&gt;

&lt;p&gt;Perplexity's own index has grown over time. The product launched as a layer on top of third-party search and has progressively built its own crawl, embedding pipeline, and ranker. The hybrid mix Perplexity ships today is genuinely different from what it shipped two years ago. Tracking Perplexity citations over time, not over a fixed snapshot, is the only reliable approach. Google's grounding pipeline is, by contrast, the most stable and most documented anchored to Google Search, which itself is the most stable index in the market.&lt;/p&gt;

&lt;p&gt;The general rule: if you are tracking citation behaviour and your data is more than three months old, treat it as suspect. Backend mixes drift, internal crawls expand, third-party partnerships open and close.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Citation Patterns Drift
&lt;/h2&gt;

&lt;p&gt;Once the backend map is in place, drift becomes legible. There are four causes for a brand to be cited on one tool and missing on another, each with a different fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Index gap.&lt;/strong&gt; The page is not in the relevant backend's index at all. This is the most common cause and the easiest to verify. Pull the URL into the backend's site search (&lt;code&gt;site:yourbrand.example&lt;/code&gt; on Google, on Bing, on Brave) and see whether it returns. The fix is at the crawl layer: sitemaps, internal linking, robots.txt audit, JS-rendering audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ranking gap.&lt;/strong&gt; The page is indexed but does not rank in the top-N for the relevant queries. Different backends have different top-N cutoffs for what enters the LLM's context window a typical grounding session pulls between three and ten URLs into the synthesis. A page ranked at twenty is invisible. The fix is the standard SEO playbook for the specific backend, with the caveat that different backends weight the signals differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Language and geo gap.&lt;/strong&gt; The page is indexed and ranks in one geo or language but not another. Most common when a brand publishes primarily in English but operates in multiple markets. The fix is genuinely localised content, with &lt;code&gt;hreflang&lt;/code&gt; and locale-specific URLs, not translated boilerplate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freshness gap.&lt;/strong&gt; The page changed significantly since the last crawl, and the backend's snapshot is stale. AI grounding sessions read the snapshot, not the live page. The fix is a sane sitemap with &lt;code&gt;lastmod&lt;/code&gt; and a hosting setup that does not time out crawler requests.&lt;/p&gt;

&lt;p&gt;A specific and frustrating drift case: a brand's pages get crawled and indexed by every backend except one. The exception backend often has a specific technical incompatibility a &lt;code&gt;robots.txt&lt;/code&gt; line that named the wrong bot, a CDN rule that returns 403 for that bot's user-agent, an SSR setup that fails for one rendering pipeline. The way to find these is bot-by-bot log analysis. I have lost count of the number of "we are invisible on Claude / on Perplexity / on Gemini" cases that turned out to be a single line in a CDN config.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Monitoring: Which Two Backends First
&lt;/h2&gt;

&lt;p&gt;Nine surfaces is too many to monitor day-to-day for most operators. The question becomes which two or three to start with.&lt;/p&gt;

&lt;p&gt;The framework I have settled on uses traffic profile. For a B2C brand whose customers come primarily through Google search today, the first two surfaces to monitor are Google AI Overview and Gemini, because the Google index is the source of truth for both. The second tier is ChatGPT search and Perplexity, which capture an increasing share of grounded buyer-side queries. The third tier is Claude (Brave-backed) and Grok (X-and-web).&lt;/p&gt;

&lt;p&gt;For a B2B brand selling to developers, the priority shifts. Claude and ChatGPT search rank highest because developers disproportionately use them. Perplexity matters because of its researcher persona. Tavily- and Exa-backed agent stacks matter because the buyer is building those agents. Google AI Overview drops in priority.&lt;/p&gt;

&lt;p&gt;For a brand whose content is primarily video or audio, the priority shifts again. Google has the strongest video content integration and that bias propagates to Gemini and AI Overview. Other backends are uneven. Monitor whichever backend is documented to handle your content type best.&lt;/p&gt;

&lt;p&gt;The mistake operators make most often is testing in only one tool. A single ChatGPT query that cites the brand is taken as "we are visible to AI." A single Gemini query that does not cite the brand is taken as "Gemini is broken for us." Both interpretations are wrong because they do not separate the backend layer from the synthesis layer. The right test is the same query across at least four tools, with the citation set logged for each, on a rotation that catches drift.&lt;/p&gt;

&lt;p&gt;What to log when running this systematically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The exact prompt, with timestamp.&lt;/li&gt;
&lt;li&gt;The tool used and the model version.&lt;/li&gt;
&lt;li&gt;Whether grounding fired (the response includes a tool-call indicator).&lt;/li&gt;
&lt;li&gt;The full citation set returned, with URLs and the cited domains.&lt;/li&gt;
&lt;li&gt;Whether the brand appears in the citation set, and if so, in which position.&lt;/li&gt;
&lt;li&gt;The synthesis-layer mention of the brand (separate from citation), since some tools cite without naming and some name without citing.&lt;/li&gt;
&lt;li&gt;The locale/language the query was run in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A spreadsheet of those columns over fifty queries across nine tools, repeated monthly, gives you the picture. Anything less and you are guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backend Variation Matrix
&lt;/h2&gt;

&lt;p&gt;The matrix below is the practical artefact I use in audits. It is deliberately not a ranking there is no "best" backend, just different coverage profiles. Where a cell is uncertain or has shifted recently, I have marked it as observation rather than fact.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend / Layer&lt;/th&gt;
&lt;th&gt;Primary AI tools it powers&lt;/th&gt;
&lt;th&gt;Coverage character&lt;/th&gt;
&lt;th&gt;Independence&lt;/th&gt;
&lt;th&gt;Public docs on internals&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Search&lt;/td&gt;
&lt;td&gt;Gemini grounding, AI Overview&lt;/td&gt;
&lt;td&gt;Largest open-web index, strong multilingual, strong PDF, strong video integration, slow drift&lt;/td&gt;
&lt;td&gt;Independent&lt;/td&gt;
&lt;td&gt;Strong (&lt;code&gt;groundingChunks&lt;/code&gt;, &lt;code&gt;webSearchQueries&lt;/code&gt;, &lt;code&gt;groundingSupports&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bing (via Grounding with Bing Search)&lt;/td&gt;
&lt;td&gt;Microsoft Copilot, DuckDuckGo (traditional links), historically ChatGPT search&lt;/td&gt;
&lt;td&gt;Large open-web index, strong English, weaker non-English vs Google, fast for high-authority freshness&lt;/td&gt;
&lt;td&gt;Independent&lt;/td&gt;
&lt;td&gt;Documented Azure AI Foundry tool surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brave Search API&lt;/td&gt;
&lt;td&gt;Claude web search tool, Mistral / Cohere / Together / Snowflake apps per Brave's own claim&lt;/td&gt;
&lt;td&gt;~30bn pages, English-leaning, independent crawl, AI-friendly snippet sizing&lt;/td&gt;
&lt;td&gt;Independent (explicitly not a Bing/Google reseller)&lt;/td&gt;
&lt;td&gt;Strong (API page declares scale and method)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perplexity (Sonar + hybrid)&lt;/td&gt;
&lt;td&gt;Perplexity.ai answers and Search API&lt;/td&gt;
&lt;td&gt;Hundreds of billions of pages claimed; mix of own crawl plus external APIs that has shifted over time&lt;/td&gt;
&lt;td&gt;Hybrid; mix is not stable&lt;/td&gt;
&lt;td&gt;Partial; mix not fully disclosed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI's emerging stack&lt;/td&gt;
&lt;td&gt;ChatGPT search&lt;/td&gt;
&lt;td&gt;Hybrid of partner data feeds plus open web; positioned competitively against Bing&lt;/td&gt;
&lt;td&gt;Hybrid; trending toward independence&lt;/td&gt;
&lt;td&gt;Limited; framed as "real-time web search and partnerships"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xAI Search (web + X)&lt;/td&gt;
&lt;td&gt;Grok&lt;/td&gt;
&lt;td&gt;X-platform-biased, plus open web; X integration unique among major tools&lt;/td&gt;
&lt;td&gt;Hybrid; web provider not publicly disclosed&lt;/td&gt;
&lt;td&gt;Partial; web sources not enumerated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You.com index&lt;/td&gt;
&lt;td&gt;You.com search and API&lt;/td&gt;
&lt;td&gt;Own real-time index plus vertical indexes; smaller scale than Bing/Google&lt;/td&gt;
&lt;td&gt;Independent&lt;/td&gt;
&lt;td&gt;Documented per vertical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kagi (Teclis + TinyGem + metasearch)&lt;/td&gt;
&lt;td&gt;Kagi search (consumer; not a major LLM backend)&lt;/td&gt;
&lt;td&gt;"Small web" focus; metasearch architecture; small relative to dominant indexes&lt;/td&gt;
&lt;td&gt;Hybrid; explicitly metasearch&lt;/td&gt;
&lt;td&gt;Strong (named indexes and API call structure)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily&lt;/td&gt;
&lt;td&gt;AI agents using Tavily as their retrieval layer&lt;/td&gt;
&lt;td&gt;Custom crawl plus dense retrieval plus reranker, optimised for LLM consumption&lt;/td&gt;
&lt;td&gt;Independent at the retrieval layer&lt;/td&gt;
&lt;td&gt;Limited; mechanism not publicly enumerated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exa&lt;/td&gt;
&lt;td&gt;AI agents using Exa as their retrieval layer&lt;/td&gt;
&lt;td&gt;Embedding-based retrieval with custom ranking; positioned as "industry-leading web index built for agents"&lt;/td&gt;
&lt;td&gt;Independent at the retrieval layer&lt;/td&gt;
&lt;td&gt;Limited; mechanism not publicly enumerated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Common Crawl (training-time baseline)&lt;/td&gt;
&lt;td&gt;Indirectly underlies many model training corpora&lt;/td&gt;
&lt;td&gt;300bn+ pages, 3–5bn new pages monthly; quarterly published snapshots&lt;/td&gt;
&lt;td&gt;Independent&lt;/td&gt;
&lt;td&gt;Strong; methodology published&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The matrix is the artefact you carry into a monitoring strategy. It tells you which backends an observed gap implicates, which backends to instrument when you need to verify the gap, and which surface to test against when you ship a fix. The "AI search" abstraction collapses into this matrix the moment you try to do real work on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Synthesis
&lt;/h2&gt;

&lt;p&gt;The single sentence: &lt;em&gt;AI search is a thin synthesis layer over a small set of search backends, and a brand's visibility across AI tools is determined first by which backends index it and rank it, not by anything the model itself does at synthesis time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you only have time to internalise three things from this post, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;There is no "AI search" channel there are four or five distinct backend channels with different coverage curves.&lt;/strong&gt; Google's index, Bing's index, Brave's index, Perplexity's hybrid index, and the agent-layer retrieval products like Tavily and Exa each see a different web. Treating "AI" as one channel is debugging at the wrong layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick two backends to monitor first based on traffic profile, then expand.&lt;/strong&gt; B2C through Google traffic: Gemini and AI Overview first. B2B to developers: Claude and ChatGPT search first. Test the same fifty brand-queries across all monitored surfaces monthly, and log the citation set for each. The divergence shows up immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When a citation gap appears on one backend and not another, the cause is almost always upstream of the model.&lt;/strong&gt; Index gap, ranking gap, language/geo gap, or freshness gap each has a different fix and a different team. Engineering work on the crawl layer pays out across every AI tool that shares that backend. Synthesis-layer "AI optimisation" without backend coverage is sand-castle work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The operator who internalises this stops asking "are we visible on AI" and starts asking "which backends index us, which rank us, and which is the cheapest to fix first." That framing change is the win. The engineering follows.&lt;/p&gt;

&lt;p&gt;The backend market is going to keep shifting. OpenAI is building its own crawl. Anthropic's stack is expanding beyond Brave. Perplexity's mix changes annually. Tavily and Exa are growing into the retrieval layer for the agentic web. The brands cited consistently across AI tools in three years will be the ones who treated this as a coverage problem on a moving target.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The backend taxonomy in this post is my synthesis of public mechanism documentation &lt;a href="https://ai.google.dev/gemini-api/docs/grounding" rel="noopener noreferrer"&gt;Google's grounding-with-search documentation&lt;/a&gt;, &lt;a href="https://brave.com/search/api/" rel="noopener noreferrer"&gt;Brave's Search API documentation&lt;/a&gt;, &lt;a href="https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/bing-tools" rel="noopener noreferrer"&gt;Microsoft's Grounding with Bing Search service docs on Azure AI Foundry&lt;/a&gt;, &lt;a href="https://docs.cloud.google.com/vertex-ai/generative-ai/docs/partner-models/claude/web-search" rel="noopener noreferrer"&gt;Google's Vertex AI documentation for Anthropic partner models&lt;/a&gt; (which is where Brave is named as the third-party search provider behind Claude's web search tool), &lt;a href="https://duckduckgo.com/duckduckgo-help-pages/results/sources" rel="noopener noreferrer"&gt;DuckDuckGo's results-sources help page&lt;/a&gt;, &lt;a href="https://help.kagi.com/kagi/search-details/search-sources.html" rel="noopener noreferrer"&gt;Kagi's search-sources documentation&lt;/a&gt;, &lt;a href="https://docs.x.ai/developers/tools/web-search" rel="noopener noreferrer"&gt;xAI's web search tool documentation&lt;/a&gt;, &lt;a href="https://openai.com/index/introducing-chatgpt-search/" rel="noopener noreferrer"&gt;OpenAI's announcement of ChatGPT search&lt;/a&gt;, and &lt;a href="https://commoncrawl.org" rel="noopener noreferrer"&gt;Common Crawl's published methodology&lt;/a&gt; combined with observable behaviour across my own monthly cross-tool citation audits over the past six months. Where I have written "in my testing" or "the pattern I observe," that is exactly what I mean. The provider-internal index claims for OpenAI and Perplexity are framed as observation because public documentation does not enumerate the backend mix. Provider behaviour is moving; backend mixes drift on a quarterly cadence; verify against current docs before shipping a strategy.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Published by Cihangir Bozdogan&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>llm</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AI Search Crawlers Are Curl From 1998, Not Chrome. Your SPA Is Invisible and Here Is the Mechanism.</title>
      <dc:creator>Cihangir Bozdogan</dc:creator>
      <pubDate>Sun, 03 May 2026 20:47:39 +0000</pubDate>
      <link>https://forem.com/cihangir_bozdogan_76b8c99/ai-search-crawlers-are-curl-from-1998-not-chrome-your-spa-is-invisible-and-here-is-the-mechanism-2pgg</link>
      <guid>https://forem.com/cihangir_bozdogan_76b8c99/ai-search-crawlers-are-curl-from-1998-not-chrome-your-spa-is-invisible-and-here-is-the-mechanism-2pgg</guid>
      <description>&lt;p&gt;There is a meeting that happens at almost every web team I have audited where someone says "we render fine, our React app renders fine for crawlers." Most of the time this is wrong. The kind of crawler the team has in mind is Googlebot, which has shipped a JavaScript-rendering second-pass since around 2019. The kind of crawler that actually decides whether a site appears in ChatGPT, Claude, Perplexity, or any of the AI search products is not Googlebot. It is GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot and these things behave like &lt;code&gt;curl&lt;/code&gt; from 1998. They issue an HTTP GET, parse the HTML they get back, and that is the entire interaction. JavaScript is not executed. Hydration does not happen. The single-page-app shell with &lt;code&gt;&amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt; is what gets indexed.&lt;/p&gt;

&lt;p&gt;I worked through this the slow way. Set up six identical-looking sites a Next.js SSR build, a Next.js SSG build, a Vite SPA with no SSR, a Remix SSR build, a static HTML page, and a hybrid where above-the-fold is server-rendered and below-the-fold lazy-loads. Pointed each AI search platform at each. Watched what they could and could not see. The result is not subtle: the SPA-with-no-SSR is functionally invisible to half the AI ecosystem, and the lazy-loaded content is invisible to all of it.&lt;/p&gt;

&lt;p&gt;This post is the field report and the mechanism. The two-mode spectrum AI crawlers occupy. What each of the six relevant crawlers actually does on the wire. The hydration cliff that decides what the model sees. The five failure modes I now flag in technical audits. And the patterns that work the SSR-or-SSG sweet spot that costs almost nothing to ship and changes whether the AI ecosystem can see you at all.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Command page rendering flows&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpmsgtidtq77mamaxz919.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpmsgtidtq77mamaxz919.png" alt=" " width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Static site generation&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxey9tc02fqujnmaqnaty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxey9tc02fqujnmaqnaty.png" alt=" " width="800" height="305"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Two-Mode Spectrum
&lt;/h2&gt;

&lt;p&gt;Web crawlers exist on a spectrum from "raw HTTP fetcher" to "full headless browser with JavaScript execution." The endpoints of the spectrum are not philosophical. They cost different amounts of money to operate and they produce different views of the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raw HTTP fetcher.&lt;/strong&gt; A program that issues an HTTP GET, receives the response, parses HTML, follows links via the parsed &lt;code&gt;href&lt;/code&gt; attributes. No JavaScript is executed. No CSS is laid out. No images are decoded. The cost per page is a few milliseconds plus the network round-trip. The throughput is whatever the operator's bandwidth and target rate-limiting allows. This is &lt;code&gt;curl&lt;/code&gt; plus an HTML parser. Most AI crawlers live near this end of the spectrum.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headless browser.&lt;/strong&gt; A program that does everything a real browser does, headlessly. JavaScript executes, network requests fan out, the DOM gets mutated, eventually a render tree settles. The cost per page is hundreds of milliseconds to multiple seconds, depending on how heavy the page is. The throughput is one to two orders of magnitude lower than the raw fetcher. Memory cost is much higher. Googlebot's render queue, Bingbot's modern incarnation, and a handful of search-tool products live at this end.&lt;/p&gt;

&lt;p&gt;The gap between the two modes is where the AI ecosystem currently sits: not because nobody knows how to run a headless browser, but because the cost-benefit at training-corpus or live-fetch scale is decisively against it. OpenAI is not going to run a headless browser to fetch a site on every ChatGPT search. The latency is too high. The cost is too high. The volume is too high.&lt;/p&gt;

&lt;p&gt;The implication is direct. If the part of your page that says what your business does is rendered by JavaScript that runs after the document loads, the AI crawler that fetches your page does not see it. There is no second-pass render queue at OpenAI, Anthropic, or Perplexity. There is one pass. Whatever is in the HTML at first-paint is everything the model gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Six Crawlers, Tested
&lt;/h2&gt;

&lt;p&gt;Six crawlers do almost all the AI-relevant work. Their User-Agents, their robots.txt declarations, and their JS-execution behaviour are public information for the most part. Where I have observed behaviour beyond what the docs say, I have flagged it.&lt;/p&gt;

&lt;h3&gt;
  
  
  GPTBot (OpenAI training crawler)
&lt;/h3&gt;

&lt;p&gt;User-Agent: &lt;code&gt;Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.0; +https://openai.com/gptbot&lt;/code&gt;. OpenAI publishes its IP ranges. GPTBot's job is to harvest content for training data, not to fetch live for a specific user query. It respects robots.txt and respects &lt;code&gt;User-agent: GPTBot&lt;/code&gt; &lt;code&gt;Disallow: /&lt;/code&gt; if you set it.&lt;/p&gt;

&lt;p&gt;The interaction shape is HTML-only. No JavaScript execution. The content GPTBot acquires is whatever the server returns to a plain GET first-paint HTML, server-rendered or static, plus any inline JSON-LD. Anything rendered after document-ready is invisible. Anything fetched from a downstream API by client-side JavaScript is invisible. The crawler acts like an HTTP fetcher, not a browser.&lt;/p&gt;

&lt;p&gt;The implication for training inclusion is mechanical: if GPTBot cannot see your content in HTML, your content does not enter the training corpus through this path. There are other paths (Common Crawl, licensed datasets) but for the part you control directly, this is the gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  OAI-SearchBot (OpenAI live-fetch crawler)
&lt;/h3&gt;

&lt;p&gt;User-Agent: &lt;code&gt;Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot&lt;/code&gt;. Same operator as GPTBot, different role. OAI-SearchBot is the fetcher used at ChatGPT-search inference time the user asks a question, the model decides to search, candidate URLs come back from the Bing-backed retrieval, and OAI-SearchBot fetches a handful of them in parallel.&lt;/p&gt;

&lt;p&gt;This crawler operates under a much tighter latency budget than GPTBot. The user is waiting on an answer. The fetcher cannot afford to render. JavaScript is not executed. Robots.txt is honoured.&lt;/p&gt;

&lt;p&gt;There is a subtlety here that catches operators. ChatGPT search candidates come from Bing's index. Bingbot does execute JavaScript. So a JavaScript-only site can be in Bing's index and therefore in the candidate set ChatGPT search ranks against but when OAI-SearchBot tries to live-fetch that page to get content for the answer, it gets the empty shell. The candidate ranks. The content does not appear in the cited answer. The site is in the SERP but invisible at synthesis time.&lt;/p&gt;

&lt;h3&gt;
  
  
  ChatGPT-User (the "browse" UA)
&lt;/h3&gt;

&lt;p&gt;User-Agent contains &lt;code&gt;ChatGPT-User&lt;/code&gt;. This is the fetcher used when a user explicitly asks ChatGPT to browse a specific URL ("can you summarise this page for me?"). It is allowed to do slightly more than OAI-SearchBot in some configurations limited rendering, but I have not seen it execute arbitrary JS reliably. Treat it the same as OAI-SearchBot for planning purposes: HTML-only is the safe assumption.&lt;/p&gt;

&lt;h3&gt;
  
  
  ClaudeBot (Anthropic crawler)
&lt;/h3&gt;

&lt;p&gt;User-Agent: &lt;code&gt;Mozilla/5.0 (compatible; ClaudeBot/1.0; +claudebot@anthropic.com)&lt;/code&gt;. Used for harvesting training data. HTML-only. Respects robots.txt. Behaviour matches GPTBot more than it matches anyone else modest crawl rate, conservative on server load, predictable.&lt;/p&gt;

&lt;p&gt;There is a separate UA for Claude's web search tool when it is configured (Anthropic's docs are still maturing on this), but the same pattern applies: at inference time, when a model is fetching live, JavaScript is not in the budget.&lt;/p&gt;

&lt;h3&gt;
  
  
  PerplexityBot (Perplexity crawler)
&lt;/h3&gt;

&lt;p&gt;User-Agent: &lt;code&gt;Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot&lt;/code&gt;. Perplexity operates a hybrid retrieval they pull from Bing and from their own crawler. PerplexityBot is the latter. HTML-only. Their robots.txt compliance has been a source of friction in the press; the documented behaviour is that they respect it, and the controversies have been around whether they always have.&lt;/p&gt;

&lt;p&gt;Behavioural note from observed traffic: PerplexityBot is more aggressive than GPTBot or ClaudeBot on revisit frequency. Pages that update often see PerplexityBot more frequently. Pages that are stable see all three at similar cadences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bingbot
&lt;/h3&gt;

&lt;p&gt;User-Agent: &lt;code&gt;Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm&lt;/code&gt;. Microsoft's primary search crawler. Respects robots.txt. &lt;strong&gt;Executes JavaScript&lt;/strong&gt; via a Chromium-based renderer. This puts Bingbot at the headless-browser end of the spectrum, alongside Googlebot.&lt;/p&gt;

&lt;p&gt;Bingbot matters in the AI conversation more than the Microsoft branding suggests, because ChatGPT search and Copilot both depend on Bing's index. If your site is JavaScript-only and Bingbot can render it, you can appear in ChatGPT search candidates but as noted under OAI-SearchBot, the live-fetch step at inference time still cannot render. So Bingbot indexes you, ChatGPT ranks you, OAI-SearchBot tries to fetch you for the cited content, and gets nothing useful. The candidate ranking and the citation content are decoupled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Googlebot
&lt;/h3&gt;

&lt;p&gt;User-Agent: &lt;code&gt;Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; Googlebot/2.1; +http://www.google.com/bot.html&lt;/code&gt;. Google's two-pass crawler: first pass is an HTML fetch, second pass is a render-queue pass with a Chromium-based renderer. Respects robots.txt. &lt;strong&gt;Executes JavaScript&lt;/strong&gt;, but on a delay the render queue can lag the initial fetch by minutes to days.&lt;/p&gt;

&lt;p&gt;Googlebot is important for AI in the same indirect way Bingbot is. Gemini and Google AI Overview depend on Google's index. The render queue means JavaScript-rendered content does eventually get indexed but the Gemini and AI Overview live fetcher at inference time has the same constraint as OAI-SearchBot: no JavaScript at synthesis. The same decoupling fires.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Summary Matrix
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Crawler&lt;/th&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;JS execution&lt;/th&gt;
&lt;th&gt;Robots.txt&lt;/th&gt;
&lt;th&gt;Latency profile&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPTBot&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Training data&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Patient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAI-SearchBot&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Live fetch&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Tight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT-User&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;User-triggered browse&lt;/td&gt;
&lt;td&gt;No (effectively)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Tight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ClaudeBot&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Training / fetch&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Patient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PerplexityBot&lt;/td&gt;
&lt;td&gt;Perplexity&lt;/td&gt;
&lt;td&gt;Hybrid index&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (with caveats)&lt;/td&gt;
&lt;td&gt;Mid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bingbot&lt;/td&gt;
&lt;td&gt;Microsoft&lt;/td&gt;
&lt;td&gt;Search index&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Mid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Googlebot&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Search index&lt;/td&gt;
&lt;td&gt;Yes (second pass)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Patient first pass, delayed render&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is sharp: every crawler that fetches live for an AI synthesis call is HTML-only. Every crawler that builds a search index that AI products rank against may render JavaScript, but the JS-rendered content is only useful for ranking, not for the cited content the model actually emits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hydration Cliff
&lt;/h2&gt;

&lt;p&gt;If the rest of this post had a single image, it would be the hydration cliff. The picture is: a React app renders in three stages. Stage one is the HTML shell delivered by the server. Stage two is the JavaScript bundle being loaded and parsed. Stage three is the React tree mounting, fetching data, and rendering. To a user, the three stages compress to "the page loads." To an HTML-only crawler, only stage one exists.&lt;/p&gt;

&lt;p&gt;To make this concrete, here is what &lt;code&gt;curl&lt;/code&gt; sees against a stock Vite + React SPA with the production build:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;$ curl -s &lt;a href="https://example.com/products/widget-pro" rel="noopener noreferrer"&gt;https://example.com/products/widget-pro&lt;/a&gt; | head -30&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"image/svg+xml"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/vite.svg"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Widget Pro&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/assets/index-a1b2c3d4.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is what GPTBot, OAI-SearchBot, ClaudeBot, and PerplexityBot all see. The product name, the description, the price, the availability, the JSON-LD, the reviews none of it is in the response. None of it enters the AI ecosystem through this fetch.&lt;/p&gt;

&lt;p&gt;The same page, server-rendered (Next.js with &lt;code&gt;getServerSideProps&lt;/code&gt; or App Router with appropriate caching):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;$ curl -s &lt;a href="https://example.com/products/widget-pro" rel="noopener noreferrer"&gt;https://example.com/products/widget-pro&lt;/a&gt; | head -50&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Widget Pro Industrial-grade widget rated for 50,000 cycles&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Widget Pro is rated for 50,000 cycles, ships from Berlin..."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Widget Pro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Industrial-grade widget...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;offers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Offer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;299.00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;priceCurrency&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Widget Pro&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Industrial-grade widget rated for 50,000 cycles. Ships from Berlin warehouse.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"price"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;€299.00 In stock&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      ...
    &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same React app, same components, same product. The difference between "invisible" and "fully indexable" is whether the rendering happens on the server before the response is sent or on the client after the response is sent. To the AI ecosystem, that is the difference between not existing and existing.&lt;/p&gt;

&lt;p&gt;The trap is that the JavaScript-only version does the right thing for users. It loads in a few hundred milliseconds, it is interactive, it is fast on subsequent navigations because of client-side routing. The user experience is fine. The crawler experience is empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Failure Modes
&lt;/h2&gt;

&lt;p&gt;These are the patterns I now flag immediately in any audit, ordered by how often they show up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure Mode 1: Pure CSR with no SSR fallback.&lt;/strong&gt; The site is a Vite, Create React App, or Angular CLI build with a near-empty &lt;code&gt;index.html&lt;/code&gt; shell. Every other page on the site is rendered client-side from the same shell. Title and description are set client-side via JavaScript. AI crawlers see the same empty shell on every URL. The fix is to migrate to a framework with SSR or SSG capability Next.js, Remix, SvelteKit, Astro, Nuxt or to add a pre-render step (&lt;code&gt;react-snap&lt;/code&gt; or similar) that emits static HTML for known routes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure Mode 2: Hydration boundary on critical content.&lt;/strong&gt; The site has SSR, but the critical content (product description, article body, business hours) is inside a &lt;code&gt;&amp;lt;Suspense&amp;gt;&lt;/code&gt; boundary or a &lt;code&gt;&amp;lt;ClientOnly&amp;gt;&lt;/code&gt; wrapper that defers rendering until hydration. AI crawlers see a loading spinner or an empty container where the content should be. The fix is to move the critical content out of the deferred boundary. Defer the comments, the related-products carousel, the live-availability widget not the product name or the article text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure Mode 3: Lazy-loaded above-the-fold content.&lt;/strong&gt; The site uses &lt;code&gt;loading="lazy"&lt;/code&gt; for content that should be visible immediately, including text content rendered conditionally based on viewport intersection. AI crawlers do not run an Intersection Observer. They do not scroll. Anything gated on scroll position never appears. The fix is &lt;code&gt;loading="lazy"&lt;/code&gt; for images that genuinely live below the fold; everything else stays eager.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure Mode 4: CDN edge cache serving stale JSON-LD.&lt;/strong&gt; The site has perfectly valid SSR with rich JSON-LD, but the CDN edge cache has the version from the last deploy that had a bug the JSON-LD references the wrong product, the price is wrong, the availability is stale. AI crawlers ingest the stale data and the model emits answers with the stale data weeks after deploy. The fix is purposeful cache invalidation on JSON-LD-affecting deploys, ideally with surrogate-key invalidation tied to the entities the page renders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure Mode 5: robots.txt that selectively blocks AI crawlers.&lt;/strong&gt; Someone in the past read a Hacker News thread about AI training and added &lt;code&gt;User-agent: GPTBot&lt;/code&gt; plus &lt;code&gt;User-agent: ClaudeBot&lt;/code&gt; plus &lt;code&gt;User-agent: PerplexityBot&lt;/code&gt; plus &lt;code&gt;Disallow: /&lt;/code&gt; to robots.txt. This was perhaps a defensible position when the question was "do I want my content used for training." It is not a defensible position when the question is "do I want my content cited in AI answers." The training crawlers and the live-fetch crawlers are often the same agent or sibling agents from the same operator. Blocking them blocks the citation path. The fix is to decide what you actually want: if it is "no training but yes citation," you need to allow the live-fetch user-agents and disallow the training UA; if it is "everything blocked," own that and stop expecting AI visibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;p&gt;Three patterns survive contact with all six crawlers and all five failure modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-side rendering.&lt;/strong&gt; The simplest and most durable pattern. Every page returns first-paint HTML with all critical content present. Hydration happens on top of populated content, not in place of it. Frameworks that ship this out of the box: Next.js (App Router or Pages with &lt;code&gt;getServerSideProps&lt;/code&gt;), Remix, Nuxt, SvelteKit, Astro. The performance cost is a server round-trip per page, mitigated by edge rendering and caching. The visibility benefit is binary: with SSR, AI crawlers see your content; without it, they do not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Static-site generation.&lt;/strong&gt; A subset of SSR where the rendering happens at build time and the output is plain HTML files. Even simpler than runtime SSR. Works for content that does not change per request most marketing pages, most blog content, most product detail pages with infrequently updated availability. Frameworks: Next.js (&lt;code&gt;generateStaticParams&lt;/code&gt;, &lt;code&gt;getStaticProps&lt;/code&gt;), Astro, Hugo, Gatsby, 11ty. AI-crawler-perfect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hybrid with a clear boundary.&lt;/strong&gt; SSR or SSG for the content that needs to be visible, client-side for the interactive widgets that do not. Article body server-rendered; comments client-rendered. Product page server-rendered; reviews client-rendered. The key is the content that determines what your business is or what an article is about must be in first-paint HTML. The interactive layer can come on top.&lt;/p&gt;

&lt;p&gt;The pattern that does not work and this is where I see teams burn the most time is the half-SSR build where some routes are server-rendered and some are not. AI crawlers do not infer your routing convention. They fetch URLs they discover in sitemaps, internal links, and external mentions. If a fraction of those URLs return rich HTML and the rest return shells, the visibility becomes lottery-distributed. Either commit to "every public URL is server-rendered or static" or commit to "we are invisible to AI crawlers and fine with that." There is no middle ground that produces predictable AI visibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Synthesis
&lt;/h2&gt;

&lt;p&gt;The mental model engineers carry of "modern web crawler" is twenty years out of date in two directions at once. It overestimates the capability of the AI crawlers (assuming they render like Chrome) and underestimates the capability of Bingbot and Googlebot (which actually do). The result is decisions that optimise for the wrong set of crawlers.&lt;/p&gt;

&lt;p&gt;The single sentence I tell anyone shipping a public site that wants to be cited by AI: &lt;em&gt;every AI crawler that fetches live at inference time is HTML-only, and every piece of content that depends on JavaScript to appear is invisible to the citation pipeline regardless of what Bingbot or Googlebot can do.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you only have time to internalise three things from this post, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Live-fetch AI crawlers do not run JavaScript.&lt;/strong&gt; GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot, ChatGPT-User. The page they see is the first-paint HTML, nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bingbot and Googlebot are the exception, and they only help with ranking, not with citation content.&lt;/strong&gt; They render JS, your site appears in the candidate set, but the live fetcher that grabs content for the answer cannot render. The decoupling is invisible until you measure for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSR or SSG is not optional for AI visibility.&lt;/strong&gt; It is the gate. Pure-CSR sites are functionally invisible to half the AI ecosystem and partially invisible to the other half.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else the failure modes, the framework picks, the cache invalidation discipline, the robots.txt nuances is implementation detail layered over those three. The mechanism is unglamorous: HTML in, HTML out, and a vanishingly small fraction of the AI ecosystem is willing to spend a Chromium-instance worth of compute to recover what your client-side JavaScript would have produced.&lt;/p&gt;

&lt;p&gt;The agentic web is being built on top of HTTP/1.1 and HTML parsers. It looks more like the web of 1998 than the web of 2018. If you treat it that way and ship server-rendered or static HTML, your site is visible. If you treat it like 2018 and rely on the browser, your site is invisible, and the marketing claim that "AI search is the new SEO" lands somewhere uncomfortable for your traffic.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The User-Agent strings in the per-crawler section are taken from each operator's published documentation as of writing &lt;a href="https://platform.openai.com/docs/bots" rel="noopener noreferrer"&gt;OpenAI's bot documentation&lt;/a&gt;, &lt;a href="https://support.anthropic.com/en/articles/8896518" rel="noopener noreferrer"&gt;Anthropic's ClaudeBot documentation&lt;/a&gt;, &lt;a href="https://docs.perplexity.ai/guides/bots" rel="noopener noreferrer"&gt;Perplexity's bot documentation&lt;/a&gt;, Microsoft's &lt;a href="https://www.bing.com/webmasters/help/which-crawlers-does-bing-use-8c184ec0" rel="noopener noreferrer"&gt;Bingbot reference&lt;/a&gt;, and Google's &lt;a href="https://developers.google.com/search/docs/crawling-indexing/googlebot" rel="noopener noreferrer"&gt;Googlebot reference&lt;/a&gt;. JavaScript-execution behaviour is from official documentation where available and observed traffic where not. Individual UAs are versioned and may have shifted by the time you read this; verify against the operator's current docs before shipping a robots.txt change. The "no JavaScript at inference-time fetchers" finding is consistent across every public statement and every observed log line I have seen, but I cannot rule out that a specific operator runs a small headless-browser fleet for a small fraction of fetches; if such a fleet exists, it does not change the production-planning conclusion.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Published by Cihangir Bozdogan&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Reverse-Engineered ChatGPT's Retrieval Stack. The Bottleneck Isn't What You Think.</title>
      <dc:creator>Cihangir Bozdogan</dc:creator>
      <pubDate>Wed, 29 Apr 2026 20:16:29 +0000</pubDate>
      <link>https://forem.com/cihangir_bozdogan_76b8c99/i-reverse-engineered-chatgpts-retrieval-stack-the-bottleneck-isnt-what-you-think-2mk3</link>
      <guid>https://forem.com/cihangir_bozdogan_76b8c99/i-reverse-engineered-chatgpts-retrieval-stack-the-bottleneck-isnt-what-you-think-2mk3</guid>
      <description>&lt;p&gt;ChatGPT cites its sources. You see the neat little &lt;code&gt;[1]&lt;/code&gt;, &lt;code&gt;[2]&lt;/code&gt; markers, and the implicit message is: &lt;em&gt;the model went out, looked at the web, brought back evidence, and is showing you receipts.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That story is half right. The other half is what every team building a RAG system gets wrong.&lt;/p&gt;

&lt;p&gt;There is no single retrieval system inside ChatGPT. There are at least two a parametric one frozen in the weights and a live one that fires only sometimes plus a tool layer deciding which to invoke, plus a generation step that has to reconcile them when they disagree. Almost none of it is published in detail. Some is confirmed by OpenAI and Microsoft. Some is inferred from leaked system-prompt fragments and citation studies. A lot is just observable behavior if you poke it with enough queries.&lt;/p&gt;

&lt;p&gt;I spent a week tracing the pipeline. What follows is an engineer's reading of how it actually works the two channels, the eight-step pipeline, the tool layer, and the one finding that should change how you build your own retrieval system.&lt;/p&gt;

&lt;p&gt;Spoiler for the impatient: &lt;strong&gt;the bottleneck is not the LLM, and it is not the embedding model. It is the rerank step.&lt;/strong&gt; I'll get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Channels, One Voice
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyyv4ta4ywov3hcp2m7a0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyyv4ta4ywov3hcp2m7a0.jpeg" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
Every ChatGPT response is the output of a model with access to two completely different sources of information. The model does not always tell you which one produced the sentence you're reading.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;training corpus&lt;/strong&gt; is frozen at the knowledge cutoff. It's parametric — what the model "knows" lives in weights, not as a list of URLs it can point at. That corpus is enormous and heterogeneous: a slice of &lt;a href="https://commoncrawl.org" rel="noopener noreferrer"&gt;Common Crawl&lt;/a&gt;, licensed publisher content, public code, and — since 2024 Reddit, via the formal &lt;a href="https://openai.com/index/openai-and-reddit-partnership/" rel="noopener noreferrer"&gt;OpenAI/Reddit data partnership&lt;/a&gt;. Anything that comes from this channel has no source URL attached. The model can recite a fact; it cannot tell you where in training it saw the fact.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;live retrieval channel&lt;/strong&gt; is different. When the browser tool fires, the model issues real search queries, fetches real pages, and the URLs travel with the content into the context window. This is the channel that produces the bracketed citations.&lt;/p&gt;

&lt;p&gt;Here's the part that should bother you more than it does: &lt;strong&gt;the model does not consistently disclose which channel produced any given answer.&lt;/strong&gt; Ask "what's the latest version of X?" and you might get a freshly retrieved answer with citations or you might get a confident, plausible answer pulled from training-time memory of an older release, no citations, no signal that retrieval was skipped. Same formatting. Same tone. Only one is right.&lt;/p&gt;

&lt;p&gt;We come back to this. It's the most engineering-relevant idea in the whole stack, and the one ChatGPT itself handles worst.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pipeline, End to End
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjj0awmzgs9nczdzsmym0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjj0awmzgs9nczdzsmym0.jpeg" alt=" " width="800" height="331"&gt;&lt;/a&gt;&lt;br&gt;
Reverse-engineered from observed behavior, OpenAI/Microsoft attestations, and citation studies, the live-retrieval pipeline runs roughly eight steps. Some implementations probably collapse steps. Some parallelize. The logical sequence is consistent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Query rewriting and decomposition.&lt;/strong&gt; Your prompt is rarely a good search query. The model rewrites it sometimes into parallel queries that decompose a multi-part question. "Compare X and Y on Z" becomes two or three independent retrieval calls, fused later. This happens inside the model itself and is cheap.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Search API call.&lt;/strong&gt; Confirmed: the primary backend is the &lt;strong&gt;Bing Web Search API&lt;/strong&gt;, a consequence of the OpenAI/Microsoft commercial relationship. Anything missing from Bing's index simply cannot be cited via this channel.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Result fetching.&lt;/strong&gt; From the ranked URL list, the system fetches a small number of pages. Small is the operative word a handful, not dozens. The fetch is parallelized, so wall-clock cost is set by the slowest tail.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Page parsing.&lt;/strong&gt; Each fetched page is converted from HTML to clean text. This is where the &lt;strong&gt;render gap&lt;/strong&gt; bites. JS-heavy SPAs, late-binding hydration, content rendered after &lt;code&gt;DOMContentLoaded&lt;/code&gt;  none of it reliably visible to a server-side fetcher. Paywalled and robots-blocked pages disappear here too. OpenAI's crawler &lt;code&gt;OAI-SearchBot&lt;/code&gt; is the publicly confirmed user agent; sites that block it block themselves out of citation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Chunking.&lt;/strong&gt; Long pages split into smaller passages. Standard RAG concerns apply chunk size, overlap, semantic boundaries. Bad chunking destroys grounding even when the right page got fetched. The relevant passage gets cut down the middle, and neither half scores well alone.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Re-ranking and selection.&lt;/strong&gt; From the chunks, a smaller set is selected for the final context. This is the stage that decides what becomes citation-worthy, and it is almost certainly handled by a model either the main LLM in a separate scoring pass or a smaller dedicated ranker. The exact architecture is undisclosed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context assembly.&lt;/strong&gt; Selected chunks are injected into the prompt alongside their source URLs. The &lt;code&gt;[1]&lt;/code&gt;, &lt;code&gt;[2]&lt;/code&gt; markers are downstream of this — chunks are paired with URLs so the generation step can attribute correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generation with citation tagging.&lt;/strong&gt; The model produces the final answer in a single forward pass, emitting citation markers tied to the assembled chunks. Mapping a generated span back to the chunk that justified it is non-trivial the model has to do an implicit alignment between what it's saying and what it was given. When that alignment is wrong, you get the well-known failure mode: a citation that doesn't actually support its claim.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A compact way to see the whole sequence: &lt;code&gt;query → rewrite → search → fetch → parse → chunk → rerank → assemble → generate&lt;/code&gt;. Every step has a budget and a failure mode. Every step throws away information the next step could have used. That it works at all is a quiet engineering accomplishment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tool Layer (and Why You Should Read Leaked Prompts Sideways)
&lt;/h2&gt;

&lt;p&gt;Above the pipeline sits the tool surface the model actually calls. The model itself doesn't make HTTP requests. It emits structured tool calls; a runtime executes them and returns results.&lt;/p&gt;

&lt;p&gt;Two surfaces dominate: a &lt;code&gt;browser&lt;/code&gt; tool with sub-actions like opening a URL, fetching a page, following a link; and a &lt;code&gt;web.run&lt;/code&gt; family that issues searches and returns ranked candidates. The model decides when to call each, with what arguments, how many times. From outside, it looks like a small set of structured function calls open, search, fetch, read with the LLM as the decision-maker.&lt;/p&gt;

&lt;p&gt;Leaked system-prompt material shows consistent themes. &lt;em&gt;Open multiple results in parallel. Cite all sources used. Prefer recent sources for time-sensitive queries. Handle disagreements between sources explicitly.&lt;/em&gt; I'm paraphrasing deliberately the leak provenance is messy and any specific snapshot's wording may not reflect current production.&lt;/p&gt;

&lt;p&gt;The best public archive is &lt;a href="https://github.com/jujumilk3/leaked-system-prompts" rel="noopener noreferrer"&gt;&lt;code&gt;jujumilk3/leaked-system-prompts&lt;/code&gt;&lt;/a&gt;, which collects historical snapshots from many vendors. Treat it as primary-source material useful for the &lt;em&gt;shape&lt;/em&gt; of instructions in production prompts, not as a reliable transcript of any current system. OpenAI does not publish the full browser-tool prompt. Any individual snippet may be inaccurate, partial, or out of date.&lt;/p&gt;

&lt;p&gt;The hygiene rule when reasoning from these leaks: infer &lt;strong&gt;patterns&lt;/strong&gt;, not wording. Categories, ordering, hierarchy are stable across snapshots. Exact phrasing isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bing and the Google-Shaped Mystery
&lt;/h2&gt;

&lt;p&gt;The choice of Bing as primary backend is a confirmed mechanism, and the reason is not technical excellence. It's commercial. OpenAI and Microsoft have a deep, well-publicized relationship, and Bing's Web Search API is the natural surface to plug into.&lt;/p&gt;

&lt;p&gt;The trade-off is index coverage. Bing is competitive on mainstream content. On long-tail, niche, or freshly published content, it still trails Google in many domains. A page that's hours old may not be in the index ChatGPT can query. Inventing a specific lag-hour figure would be irresponsible; the directional claim — Bing-only retrieval has a freshness ceiling is what matters.&lt;/p&gt;

&lt;p&gt;This is where the most interesting public test comes in. SEO consultant Aleyda Solís published a brand-new page, submitted it to both engines, queried ChatGPT before Bing had indexed it and ChatGPT returned a snippet matching Google's cached version. The page was findable through ChatGPT before Bing knew it existed. &lt;a href="https://www.searchenginejournal.com/chatgpt-appears-to-use-google-search-as-a-fallback/552089/" rel="noopener noreferrer"&gt;Search Engine Journal's coverage&lt;/a&gt; is the canonical write-up.&lt;/p&gt;

&lt;p&gt;I want to be honest about what this proves and what it doesn't: &lt;strong&gt;there is no public confirmation of a direct Google-fallback inside the OpenAI pipeline.&lt;/strong&gt; Some Google-shaped results may have alternate explanations third-party aggregators that themselves query Google, plug-ins or browsing modes that bypass the default Bing path, transient behaviors that have since changed. Observed behavior suggests fallback retrieval exists. The precise mechanism is not on the public record.&lt;/p&gt;

&lt;p&gt;The largest quantitative study is &lt;a href="https://www.seerinteractive.com/insights/87-percent-of-searchgpt-citations-match-bings-top-results" rel="noopener noreferrer"&gt;Seer Interactive's analysis of 500+ SearchGPT citations&lt;/a&gt;: roughly 87% of cited URLs in Bing's top-20, around 56% also in Google's results at a median rank of 17, and approximately 92% of agent retrieval through the Bing API directly. Observational, not mechanistic but consistent with a Bing-primary system that has some non-Bing surface area for the long tail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Latency Cliff
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnqwvs4h4ik9euuhqnvak.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnqwvs4h4ik9euuhqnvak.jpeg" alt=" " width="800" height="621"&gt;&lt;/a&gt;&lt;br&gt;
Watching the network panel during a retrieval-on response, total time from prompt submission to first streamed token typically lands in the &lt;strong&gt;4–10-second range&lt;/strong&gt;. Where do those seconds go?&lt;/p&gt;

&lt;p&gt;Without inventing precise milliseconds: query rewriting takes hundreds of ms (small generation step inside the main model). The search API call adds a few hundred (round-trip plus Bing's own ranking). Page fetches happen in parallel but wall-clock is gated by the slowest tail one slow origin server drags the whole budget. Parsing and chunking are CPU-bound and fast. Re-rank is another model call. Generation begins streaming once context is assembled.&lt;/p&gt;

&lt;p&gt;The structural implication is the part that matters: &lt;strong&gt;fetch budget is small.&lt;/strong&gt; ChatGPT cannot fetch fifty pages. It fetches a handful. The Seer numbers are consistent with this most cited URLs come from a tight slice of Bing's top results, not from deep crawling.&lt;/p&gt;

&lt;p&gt;If you're optimizing for citation, increasing the count of pages an AI agent could &lt;em&gt;theoretically&lt;/em&gt; see is at best linear. The model is rate-limited by latency, not by index breadth. Your leverage point is not "make more pages indexable." It's "be in the small set of pages that survive the rerank step."&lt;/p&gt;

&lt;p&gt;That's the first hint at the contrarian thesis. Hold on to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Citation Behavior: Dedup, Diversity, Disagreement
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3evjqvexkmdenrlt5wrr.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3evjqvexkmdenrlt5wrr.jpeg" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;br&gt;
The set of citations a ChatGPT answer surfaces is not a top-N list from the search ranker. It's the output of a selection process that visibly cares about more than relevance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dejanmarketing.com/gpt-search/" rel="noopener noreferrer"&gt;DejanMarketing's GPT-search analysis&lt;/a&gt; found, across a wide sample, that ChatGPT typically selects &lt;strong&gt;3–10 sources per response&lt;/strong&gt;. Not 1, not 50. That range is consistent across query types and visible in the rendered answer. The bound is almost certainly latency-driven on the upper end and grounding-quality-driven on the lower end.&lt;/p&gt;

&lt;p&gt;Within that set, &lt;strong&gt;same-domain dedup is visible&lt;/strong&gt;. A single domain rarely appears five times in one answer's citations even when the search ranker would happily return five pages from the same site. Observed behavior suggests an explicit diversity pressure possibly prompt-level, possibly ranker-level pushing the system toward distinct sources rather than concentrating on one well-ranked publisher.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflict handling&lt;/strong&gt; is the more interesting case. When sources disagree, the answer language hedges "some sources report... while others suggest..." and the model usually surfaces both citations. This is consistent with a system that prefers honest conflict-surfacing over arbitrary tie-breaking. The hedge isn't a marketing feature. It's what cross-encoder rerankers naturally produce when several chunks score similarly with contradictory content.&lt;/p&gt;

&lt;p&gt;The pattern that falls out: a small number of high-confidence citations beats a large number of shaky ones. Cross-encoder rerankers concentrate on agreement among independently-retrieved chunks a stronger signal than the absolute relevance score of any single chunk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Confidence-Calibration Problem
&lt;/h2&gt;

&lt;p&gt;This is the engineering-relevant center of the whole system, and the part most retrieval discussions miss.&lt;/p&gt;

&lt;p&gt;The two-channel distinction from the opening is not a clean separation at inference time. Both channels feed into a single generation pass, and the model has to decide implicitly, no externally visible toggle which to trust for any given assertion. When channels agree, this is invisible. &lt;strong&gt;When they disagree, it is the source of nearly every quietly-wrong answer the system produces.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The freshness disclosure problem is the simplest version. Ask "what's the latest version of X?" right after a release. Browser tool fires, search index has the new release: correct answer, release page cited. Browser tool doesn't fire model judged retrieval unnecessary, hit a rate limit, or user is on a path that doesn't invoke browsing and the model answers from training-time memory of the older release. Identical formatting. Only one is right. The user has no signal to tell them apart.&lt;/p&gt;

&lt;p&gt;The deeper version is more subtle, and worth being explicit about. Training corpora include the model's own historical outputs. Sufficiently popular AI-generated text on the web at scrape time ends up in the next training set. So a model can be confidently wrong because &lt;em&gt;a previous model was confidently wrong&lt;/em&gt; and the wrong answer survived into training. Re-ranking has to override parametric belief in those cases. Sometimes it does. Sometimes it doesn't particularly when the wrong belief is well-attested across many low-quality sources and the correct passage shows up in only one reranked chunk.&lt;/p&gt;

&lt;p&gt;For an engineer building a retrieval system from scratch, the implication is concrete: &lt;strong&gt;make the override explicit.&lt;/strong&gt; ChatGPT does this implicitly, and not always well. In your own RAG pipeline, decide deliberately when retrieved evidence overrides parametric belief, and surface that decision rather than letting the model arbitrate silently. A simple rule &lt;em&gt;if retrieved evidence contradicts parametric memory, retrieved wins, and the system says so&lt;/em&gt;  enforced at the prompt or rerank layer is more honest than the alternative. Even when the contradicting evidence is itself wrong, the failure mode becomes inspectable rather than invisible. That is a much better place to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Things to Take to Your Own Pipeline
&lt;/h2&gt;

&lt;p&gt;Grounded in the mechanism, not the marketing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The bottleneck is not the LLM. It is the rerank step.&lt;/strong&gt; This is the contrarian thesis the post opened with, and it's the conclusion that survives the rest. If your RAG system produces bad citations, the bottleneck is almost always downstream of the embedding model. A bi-encoder retriever and a cosine-similarity index will surface plausible-but-wrong chunks faster than you can debug them. Cross-encoder reranking is the single highest-leverage stage. Spend your engineering budget on rerank quality and on chunking that respects semantic boundaries not on swapping in a slightly larger embedding model and hoping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. There's a latency cliff on fetch count.&lt;/strong&gt; ChatGPT fetches a handful of pages, not dozens, and the same constraint applies to anything you build with comparable user-facing latency targets. Past roughly five-to-ten fetched pages, latency dominates the marginal grounding gain. Each extra page mostly slows the system without meaningfully improving the answer. Decide your fetch ceiling deliberately. Design for parallelism so a single slow tail doesn't blow the budget. Accept that you can't scale your way around the rerank quality problem by fetching more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Citation tagging is harder than it looks.&lt;/strong&gt; Mapping a generated span back to the chunk that justified it is a separate concern from retrieval, with its own failure modes. You can have perfect retrieval and still emit citations that don't support their attached claims. In practice this is either a separately-trained alignment component, an extra reasoning pass over the generated answer, or a constrained-decoding setup that forces citation tags to track the active context chunk. Pick one. Don't assume the LLM will do it for free the visible failure mode of "wrong citation on a true claim" is exactly what happens when you assume it will.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Source diversity is a feature, not a nice-to-have.&lt;/strong&gt; If your pipeline doesn't explicitly enforce same-domain dedup or topic-cluster diversity at the rerank stage, hard-code it. Allowing one domain to dominate the cited set is the fastest way to make a RAG system look like a thinly-wrapped paraphrase of one publisher. Diversity pressure is cheap to implement a small penalty in the rerank score, a per-domain cap on selected chunks and it's the difference between a citation list that reads like research and one that reads like a single-source rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;My read after a week of poking at this: ChatGPT's retrieval stack is not magic. It's a query rewrite, a search call, a small fetch budget, a re-rank, a context assembly, and a prompt with citation instructions, all wrapped in a tool layer the model decides when to invoke.&lt;/p&gt;

&lt;p&gt;The interesting part isn't the architecture. It's the choices the system makes. What gets fetched. What gets selected. What gets attributed. When retrieval fires and when it doesn't. How the two channels of knowledge get reconciled when they disagree.&lt;/p&gt;

&lt;p&gt;Every retrieval system built from now on makes the same set of choices. Most make them worse. The work isn't in copying the architecture. It's in making each of those choices deliberately and being honest with the user about which channel produced the answer.&lt;/p&gt;

&lt;p&gt;That last part, especially. ChatGPT doesn't do it. Yours can.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Published by Cihangir Bozdogan&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>web</category>
      <category>chatgpt</category>
    </item>
  </channel>
</rss>
