What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
Search
What keyword, vector, hybrid and semantic ranking each do, where each fails, why people and AI agents need different search, and what we measured on the Agility docs.
"AI search" usually means several layers stacked on top of each other: keyword search, vector search, a way to merge the two, and a model that re-orders the top results. Each layer fixes a problem the one before it has, and each has its own way of failing.
This article explains what each layer does, in plain language. It then covers why to index sections instead of whole articles, why people and AI agents need different things from search, and what we measured when we rebuilt search for these docs. It ends with how to choose an approach, and how to test it on your own content before you commit.
If you are building a chatbot or giving AI agents access to your content, search is the part that decides whether the answers are right. See Build a Chatbot or AI Agent on Your Agility Content and Build an MCP Server over Your Agility Content for what to build on top of it.
Keyword search matches the words in a query against the words in your content. It ranks results by how often the query words appear in a record, weighted so that rare words count for more than common ones. BM25 is the most widely used ranking formula for this.
What it's good at: exact terms. API names, field names, error messages, product names and version numbers are matched exactly, and the results are fast, predictable and easy to explain. Engines built for search boxes, such as Algolia, also handle typos and half-typed words out of the box.
Where it fails:
Keyword engines also rank differently from each other. Algolia ranks with a series of tie-breaking criteria rather than BM25. On our question-style test queries, BM25 in Azure AI Search ranked better (MRR 0.762 against 0.587, explained below).
Vector search compares meaning instead of words. An embedding model turns each piece of text into a long list of numbers (a vector), so that text with similar meaning gets similar vectors. At query time the question is turned into a vector with the same model, and the engine returns the records whose vectors are closest.
What it's good at: vocabulary mismatch and paraphrase. It connects "languages" to the pages about locales.
Where it fails:
Hybrid search runs a keyword query and a vector query together and merges the two result lists into one. Keyword and vector scores are on different scales, so engines merge by rank position instead of raw score. Azure AI Search and Elastic both use Reciprocal Rank Fusion (RRF) for this: a record that ranks well in both lists ends up near the top.
What it's good at: getting the right page near the top. On these docs, hybrid search put the right page first for 40 of 51 questions and in the top five for 48, the best of everything we tested for questions asked in sentences.
Where it fails: the merged scores (around 0.03) are only good for ordering. They don't tell you how confident the engine is, so you can't use them to decide that a question has no answer.
A semantic ranker (also called a re-ranker) takes the top results from the first stage, reads each one together with the query, and scores how well that passage answers that question. It is slower than the first stage, so it only looks at a short list. Azure AI Search's semantic ranker re-scores the top 50 results and returns a score from 0 to 4. Elastic offers the same idea through its text_similarity_reranker retriever.
What it's good at: a confidence score on a fixed scale. You can use it to tell a chatbot when the best result is too weak to answer from. See Decide when to say "I don't know".
Where it fails: on our content it didn't improve the order. It put the right page first for 34 of 51 questions, against 40 for hybrid search alone. It tends to prefer pages that discuss a topic broadly over the page that answers the task. It can also only re-order what the first stage found, it adds latency and cost to every query, and on Azure's free plan it stops working (HTTP 402) once the monthly allowance is used. Measure it on your own content before you pay for it.
Every search engine returns records, and what you put in a record decides what a result looks like.
If each article is one record, a hit points at the whole article, and the snippet a reader sees may come from somewhere else on the page. Many setups also cap the body length to keep records small. On these docs a 5,000-character cap silently dropped the end of long articles, so text near the bottom of a long page was never indexed.
If each section is a record, the hit lands on the passage that answers the question. What worked on these docs:
Title > Heading, then the section text. A section called "Configuration" means little on its own. "Webhooks > Configuration" does.. in a regular expression doesn't match \r, so a heading pattern such as ^##\s+(.+)$ silently failed on every one of them. 211 articles were cut into blind fixed-length chunks with no headings, and 284 of 451 records in our live site search had no headings indexed. Replace \r\n with \n first, then read a few stored records to check the result.Splitting does not improve ranking by itself. We expected it to, and measured it twice, the second time with the line-ending bug fixed: an Algolia index with one record per section ranked the same as one record per article (MRR 0.594 against 0.587, with 8 questions better and 9 worse). Index sections for the reasons that hold: the snippet is the passage that answers the question, a chatbot can quote it directly, long articles aren't cut off, and code stays in.
Splitting works best when each section makes sense on its own. Write chunk-friendly content covers how. For how to keep sections in sync as editors publish, see Indexing Agility Content for Search.
The same index serves two very different kinds of searcher:
| People in a search box | AI agents and chatbots | |
|---|---|---|
| Typical query | One to three words | A full question, often 6 to 13 words |
| Typos and half-typed words | Common | Rare |
| Searches per question | One per keystroke | One or a few |
| Latency | Every keystroke, so tens of milliseconds matter | Small next to the model's own time |
| What they need back | A list of pages to click | Passages to read and cite |
That difference showed up clearly in our tests. On short, real search-box queries (typos and half-typed words included), Algolia with its defaults was as good as the best Azure setup and about five times faster. On questions asked in sentences, Azure's hybrid search was far ahead. The Agility docs now use both: Algolia behind the search box and Azure AI Search behind the Knowledgebase MCP server that AI agents call. Search for People and for AI Agents shows how that setup works.
In October 2026 we compared search approaches on these docs. It is one measurement, on one content set, at one point in time. Treat it as an example of the method and the size of the differences, not as a benchmark of any vendor.
en.microsoft analyzer. Vectors came from text-embedding-3-large (3,072 dimensions) on Azure OpenAI, with an integrated vectorizer on the index so the search service embeds queries itself.For every set we wrote down the correct pages and committed them to git before scoring any engine.
51 answerable questions. Higher is better, except for the last column.
| Engine | Right page first | In top 3 | In top 5 | MRR@10 | Missed |
|---|---|---|---|---|---|
| Algolia, search-box settings | 13 | 16 | 18 | 0.291 | 33 |
| Algolia, settings for sentence-style questions | 25 | 34 | 37 | 0.587 | 13 |
| Algolia, one record per section (same settings) | 25 | 35 | 37 | 0.594 | 12 |
| Azure, keyword only | 32 | 45 | 46 | 0.762 | 3 |
| Azure, keyword + vector (hybrid) | 40 | 47 | 48 | 0.853 | 2 |
| Azure, hybrid + semantic ranker | 34 | 47 | 48 | 0.796 | 2 |
MRR@10 (mean reciprocal rank) scores each question 1 if the right page is first, 1/2 if second, 1/3 if third, and so on, down to 0 if it isn't in the top 10, then averages over all questions. It rewards putting the right page near the top, not just finding it. Missed counts the questions where the right page was not among the top 10 distinct pages at all.
36 queries.
| Engine | Right page first | In top 3 | In top 5 | MRR@10 | Missed | Typical time |
|---|---|---|---|---|---|---|
| Algolia, search-box settings | 30 | 34 | 34 | 0.886 | 0 | ~50 ms |
| Azure, keyword only | 25 | 27 | 27 | 0.718 | 9 | ~60 ms |
| Azure, keyword with typo and prefix matching | 29 | 35 | 35 | 0.884 | 1 | ~70 ms |
| Azure, hybrid with typo and prefix matching | 31 | 34 | 36 | 0.911 | 0 | ~260 ms |
Times are from a test script to each service, so compare them with each other rather than read them as page load times. Azure matches whole words unless you ask otherwise. "Typo and prefix matching" means rewriting the query in Lucene syntax so each word may have one typo and the last word may be a prefix.
With 51 questions, one question moves MRR by about 0.02. So we compared engines question by question and computed a 95% interval for each difference:
| Comparison | Difference in MRR | 95% interval | Better / worse |
|---|---|---|---|
| Agents: Azure hybrid vs Algolia | +0.28 | +0.16 to +0.40 | 23 / 5 |
| Agents: adding vectors to keyword search | +0.09 | +0.03 to +0.16 | 13 / 1 |
| Agents: adding the semantic ranker to hybrid | −0.07 | −0.16 to +0.02 | 7 / 13 |
| Agents: Algolia sections vs whole articles | +0.00 | −0.08 to +0.09 | 8 / 9 |
| Search box: Azure with typo and prefix matching vs Algolia | −0.01 | −0.10 to +0.09 | 4 / 4 |
An interval that includes zero means we can't tell the two apart on this test set.
queryLanguages: ["en"], removeStopWords: true, ignorePlurals: true and removeWordsIfNoResults: "allOptional". The "sentence-style" Algolia rows use them.take. GraphQL lists default to 50 items, so three categories came back with exactly 50 articles and 89 articles were missing. Because the reindex replaces the whole index, it would have deleted them from live search. Pass an explicit take, and refuse to replace an index when a list comes back exactly at the limit.Check current prices on the vendor's pricing page. They change.
Every engine below is fed from Agility the same way: a webhook on publish, a re-fetch from the Fetch API, and a nightly reindex. Indexing Agility Content for Search covers that part once for all of them. Searching Content at Scale compares the providers side by side.
Start from who is searching:
We only measured two of these providers, and only in the configurations described above. In particular, the Algolia rows don't include NeuralSearch, so they don't measure Algolia's own hybrid search.
semantic_text field embeds and splits long text for you, an RRF retriever combines keyword and semantic search, and text_similarity_reranker adds re-ranking. Runs as a serverless project, on Elastic Cloud or self-managed. We did not measure it.Whichever you choose, index sections for the reasons in Index sections, not whole articles.
Search quality depends on your content and your readers' questions, so the only reliable comparison is one you run yourself. It takes a day or two.
A scoring function is a few lines:
type Case = { question: string; expected: string[] } // expected page URLs; empty = unanswerable
// rankedUrls: the engine's results for each question, best first, collapsed to one per page
export function score(cases: Case[], rankedUrls: string[][]) {
const answerable = cases.map((c, i) => ({ c, urls: rankedUrls[i] })).filter(({ c }) => c.expected.length > 0)
const ranks = answerable.map(({ c, urls }) => {
const i = urls.slice(0, 10).findIndex((u) => c.expected.includes(u))
return i === -1 ? null : i + 1
})
const within = (k: number) => ranks.filter((r) => r !== null && r <= k).length
return {
questions: answerable.length,
first: within(1),
top3: within(3),
top5: within(5),
mrr10: ranks.reduce((sum: number, r) => sum + (r ? 1 / r : 0), 0) / answerable.length,
missed: ranks.filter((r) => r === null).length,
}
}
Keep the test sets once you have them. The same questions are how you evaluate a chatbot or an MCP server built on this search later.