SlideSearch — live production path, updated 2026-09-15

Query → Response, block by block

Traced against the actual deployed file, makeup_search.py (repo: code.straive.com/tpavan.kumar/straive-slide-search), called via the search_v2 MCP tool. Confirmed live on production this session via direct API checks, not read from source alone. NEW-tagged blocks were added/changed in the session that first built this page — everything else was already live before that.

Every dashed EXAMPLE box below is real: run against the live 122,623-slide production index, not invented. One query — "customer analytics" — is traced end to end through every stage so the numbers stay consistent.

New: click ▸ version history on any block to see exactly when it was created, why, and every real commit that has touched it in the last 3 months — pulled straight from git, not summarized.

STARTUser types a query in the chat app

e.g. "customer analytics", or a multi-part brief like "fraud detection use case, regulatory compliance automation" · Agentic Chat App

in plain English A query is simply whatever you type into the chat box — one topic, or several combined in one message (a "multi-part brief"). Everything on the rest of this page traces that one piece of text all the way through to the results you see.
the logic here
  1. You type a question or brief into the chat box.
  2. Nothing gets judged or decided yet at this point — this step just captures exactly what you asked, word for word, to hand to the next step.
running example
Query: "customer analytics"
01Chat agent decides which tool to calllib/agent/gemini.ts

System prompt now positions search_v2 as the primary tool for every brief, simple or multi-part — it used to lose out to the plain search tool for multi-outcome briefs specifically because it couldn't take a list. Universe scope (DAAIS/S&R/EdTech) is injected automatically into whichever tool actually gets called.

in plain English The system prompt is simply the written instructions the assistant's AI is given before every conversation, telling it how to behave and which tools it's allowed to use. search_v2 and plain search are just the names of the two different search tools it can pick between — this step is the assistant deciding which one to actually use for your question. Universe scope is the DAAIS/Sales & Research/EdTech restriction you can set from the toggle at the top of the chat, if you've picked one.
the logic here
  1. The assistant reads your message and its own written instructions.
  2. Because those instructions now say search_v2 is the go-to tool for every kind of request, it picks that one rather than the older, simpler search tool — regardless of whether your question is one topic or several.
  3. If you'd set a universe filter on the toggle, it gets automatically attached to whichever tool actually gets called, so it can't be silently dropped.
Fixed: universe scope used to be silently dropped whenever the model called plain search instead of search_v2 — enforcement now covers both tool names.
02MCP tool call reaches the backendslide_search_server.py

search_v2(query, universes, top_per_group)makeup_search.get_makeup_engine().search(...). query can now be a single string or a list of outcome angles.

in plain English This is the moment your search physically leaves the chat app and lands on the actual server holding all 122,000+ slides. The code shown is just that handoff: your search text (query), your universe filter (universes), and how many results to return per section (top_per_group) get handed to the real search function. An "outcome angle" is simply one topic within a multi-part brief — see the next step for how several of them get handled together.
the logic here
  1. Take the tool call the assistant made in step 01, and route it to the actual backend server.
  2. Call the one real search function everything else on this page runs through, handing it your search text, your universe filter, and how many results you want per section.
  3. No filtering or ranking decisions happen yet — this step is purely the handoff.
03Multi-angle normalizationsearch()NEW

A single query becomes a one-item list; a real multi-part brief keeps all its angles. Every angle runs intent classification + synonym expansion, and the results merge (intents unioned, expansion terms deduped case-insensitively).

in plain English Intent classification (next step) figures out what KIND of result each topic wants; synonym expansion (two steps after that) thinks of related terms for it. "Unioned" just means combined into one list with no duplicates; "deduped case-insensitively" means treating "CDP" and "cdp" as the same term when clearing out repeats. All of this happens once per topic, then gets merged together at the end.
the logic here
  1. Check whether the question is one topic or genuinely several.
  2. If one, wrap it as a list containing just that single topic. If several, keep each as its own separate item in the list.
  3. Send every item in that list, independently, through its own full round of steps 04 and 05 below — no topic influences another topic's judgment.
  4. Once every topic has its own result-type judgment and its own related terms, merge everything back into one combined judgment and one combined, duplicate-free list of related terms.
04Intent classification (per angle)_classify()

INTENT_PROMPT → LLM (gemini-2.5-flash via LLM Foundry) → one or more of case_study / approach / capability / collateral / demo, plus extracted client and use_case if named. Falls back to keyword heuristics on failure.

in plain English An LLM (Large Language Model) is the type of AI doing this judgment — here specifically Google's gemini-2.5-flash model, reached through the company's own internal AI gateway ("LLM Foundry"). It reads your search and picks which of the five result types (case study / methodology / general capability / reusable template / interactive demo) fits best, and pulls out a client name or specific use case if one was mentioned. If that AI call ever fails for some reason, the system falls back to simple, rule-of-thumb keyword matching rather than leaving the search with no judgment at all.
the logic here
  1. Take one topic and send it, along with a detailed rulebook of instructions and worked examples (shown in this block's version history below), to the AI model.
  2. The model defaults to picking exactly ONE result type — the single best match — following the rulebook's own guidance.
  3. It only adds a SECOND type if the wording contains an explicit, separate signal pointing at it (not just plausibility).
  4. It also pulls out a named client or a specific use-case phrase, if the wording clearly names one.
  5. If this AI step fails outright for a technical reason, a simpler backup (matching against a small list of known keywords) runs instead, so the topic still gets SOME judgment rather than none.
example
"customer analytics" → intents = ["capability"], client = null, use_case = "customer analytics"
05Synonym / cluster expansion (per angle)_expand() → curated_terms() + _tax_nearest()

Curated cluster dictionary is now 96 keys / 164 terms (up from 7), built by mining the real corpus, not hand-typed. Plus 4 hand-picked single-word trigger clusters (SERVICE_HINTS, 18 more terms) for terms too short/generic to mine safely. Taxonomy-nearest is threshold-driven (cosine ≥0.68) instead of a fixed top-6, so a broad query gets ~100+ genuinely related terms and a narrow one gets only the few that actually apply.

in plain English A "cluster" is one internal nickname or branded term (like "Fan 360") mapped back to the plain-English phrase people actually search for — see the full list, with a reason for each, below. "Cosine" here is the same 0-to-1 similarity score explained in step 09 later on this page — closer to 1 means more related in meaning. "Threshold-driven instead of a fixed top-6" means the system keeps every related term that clears that similarity bar, however many that turns out to be, instead of always grabbing exactly 6 regardless of quality.
the logic here
  1. Take the topic's wording and check it, whole-word by whole-word, against the 100-cluster dictionary below — any exact trigger match adds every term mapped to that cluster onto a working list.
  2. Separately, compare the topic's wording mathematically against a large set of industry/taxonomy terms, keeping every one that scores at least 0.68 out of 1 for closeness in meaning.
  3. Combine both lists together and remove exact duplicates.
  4. That final combined list is what step 06b later searches for, alongside your own original wording from step 06a.
example
"customer analytics" expands to 122 terms (raw query + 121 related). First 5: Unified Fan 360, Fan 360, Golden Customer Record, Single Customer View, Identity Resolution
06aPRIMARY: semantic search on the raw query angle(s)_rrf_pass(queries)

Each angle is embedded (gemini-embedding-2) and searched against the FAISS HNSW index (pool=300), fused via Reciprocal Rank Fusion — every angle is an equally-weighted primary voter. This is the mechanism that generalizes to any query and carries the ranking.

in plain English This step searches using your own actual words — none of the related/synonym terms from the step before are involved yet. Every slide comes out of this with a number called primary: the higher a slide ranked in this word-for-word search, the bigger its primary number. A slide that didn't show up at all when searching your literal words gets a primary of exactly 0 — like the real example just below.
the logic here
  1. Take your own exact wording (one topic at a time) — none of the related terms from step 05 are used yet.
  2. Convert that wording into the mathematical fingerprint the search index understands, and search the whole 122,000+ slide index with it.
  3. Every slide that turns up gets a primary score based purely on how high it ranked in that one search — a higher rank means a bigger number.
  4. Any slide that doesn't turn up at all in this word-for-word search gets a primary score of exactly 0, carried forward into step 06b.
example — RRF formula
Each hit contributes 1 / (RRF_K + rank), RRF_K=60, summed per angle. A slide ranked #3 in the raw-query search contributes 1/(60+3) = 0.0159 to its primary score.
slide "Indicative Output: PBI & Azure Stack Engagement" never surfaced by the raw query alone primary_rrf = 0.0
06bSECONDARY: bounded cluster-term boost_rrf_pass(boost_terms)

Every expanded cluster/taxonomy term (across all angles) is pooled into ONE normalized signal (0–1), capped at +35% lift on top of the primary score. A term with zero primary relevance gets a heavily discounted score instead of a free ride to the top.

in plain English — what "primary" and "boost" mean here primary is the score this same slide already earned in the step just above, from matching your own actual wording — 0.0 means it didn't come up at all on your literal words. boost is a separate score (also 0 to 1) built from every related/synonym term this search picked up two steps ago — the more of those a slide touches, the higher its boost. The two rules below cover two different situations: if the slide already has a real primary score (it genuinely matched your wording, even a little), related terms can lift that score by up to 35% more. If the slide has zero primary score (it never matched your literal wording at all), related terms alone can only ever add a small, heavily discounted amount — never enough by themselves to push a genuinely unrelated slide to the top.
the logic here
  1. Take every related term found in step 05, and run the exact same kind of search from step 06a for each of them.
  2. Add up how well a given slide did across ALL of those related-term searches into one combined number.
  3. Scale that combined number down to sit between 0 and 1, relative to the strongest-scoring slide in the whole batch — this becomes the slide's boost.
  4. Check whether the slide already has a real primary score from step 06a: if yes, use the boost to lift that primary score by up to 35%. If the primary score is a flat 0, apply a much smaller, heavily discounted version of the boost instead — so a slide with zero real connection to your wording can't leapfrog one that actually matches it.
if primary > 0score = primary × (1 + 0.35 × boost)
if primary == 0score = boost × 0.35 × 0.02
example — same slide, primary was 0
boost_rrf (raw, summed over 121 terms) = 0.9843 normalized (÷ max across pool) = 0.6770 primary == 0, so: fusion score = 0.6770 × 0.35 × 0.02 = 0.00474
Recalibrated: the 0.3 constant above let a slide with ZERO real relevance outscore the best genuine match by 5x, purely from touching many expanded terms — dropped to 0.02 after testing against both a failing and a working real query.
07Folder-priority × content-type × recency re-rank_folder_weight_for() × type_weight() × rec[]

Score is multiplied by (a) how strongly the slide's folder matches the query's intent — this is now a purely internal ranking signal, never shown to the user — (b) whether the slide's own content type matches the intent (e.g. a case_study-intent query boosts case_study-typed slides, a low-content type like a title/section slide is demoted ~60%), and (c) a mild recency factor.

Added: the type-weight factor didn't exist before this quarter — folder alone couldn't tell a genuinely relevant result from a same-category-but-generic one, and nothing demoted content-free divider slides.
in plain English — what each number below means By this point every slide already has one combined score from the two steps above, called the fusion score. This step multiplies that score by three separate adjustments: folder_weight rewards a slide for living in a folder that fits what the search is looking for (e.g. a specific client's folder, for a search that seems to want a client story); type_weight rewards or penalizes a slide based on what KIND of slide it actually is (a real case-study slide gets boosted for a case-study-seeking search; an empty title or divider slide gets cut by more than half); and recency_weight gives a small nudge to more recently created content. Multiplying these together — instead of just adding them — means a slide has to do reasonably well on all three to end up near the top; one big boost on a single factor can't fully make up for being weak everywhere else.
the logic here
  1. Take the combined fusion score a slide already earned from steps 06a and 06b.
  2. Look up which folder the slide's deck lives in, and multiply the score by how well that folder type matches what step 04 decided you were looking for.
  3. Look up the slide's own content-type tag, and multiply again — boosting a genuine content match, or cutting the score by more than half for a known low-content type (unless the content-rich safety-net exemption from this block's version history applies).
  4. Multiply once more by a small recency factor based on when the deck was created, favoring newer content slightly.
  5. Whatever comes out of multiplying all three adjustments together is the slide's real, final score — used for every step from here on.
example — full final score, real slide
"Indicative Output: PBI & Azure Stack Engagement" — deck Eu Yan Sang - Gramener - PBI Engagement Proposal - v1.pptx, folder /Sales/DAAIS/Clients/Eu Yan Sang/Sales_/2023 Proposal (a CLIENT folder), type content.
fusion score (from 06b) = 0.07109 folder_weight (capability × CLIENT) = 1.4 type_weight (capability × content) = 1.0 (no boost, not low-content either) recency_weight: year "2023" found in path frac = (2023 − (2026−8)) / 8 = 5/8 = 0.625 weight = 0.85 + 0.45 × 0.625 = 1.13125 FINAL = 0.07109 × 1.4 × 1.0 × 1.13125 = 0.1126 ← real #1 result for this query
Contrast — a section-type slide for the same query ("Digital Transformation through Data Science: Unified Operation Center") had a higher fusion score going in (0.0794 vs 0.0711) but got demoted by type_weight=0.4:
0.0794 × 1.4(folder) × 0.4(type, low-content) × 1.0(recency) = 0.0445 → drops out of the top 5 entirely, despite the stronger starting score
08Exact-content dedup_dedupe_content()NEW

Drops lower-scored results whose real slide text is an exact (normalized) match to a higher-scored one already kept — catches a boilerplate slide copy-pasted verbatim into different decks, which deck-title dedup can't see at all.

in plain English "Normalized" just means minor formatting differences — extra spaces, capitalization — are ignored before comparing two slides' text. So two slides count as an exact match if their real content is identical, even if one happens to be spaced slightly differently. "Dedup" is short for de-duplication: removing repeats so you don't see the same real content twice.
the logic here
  1. Sort every remaining candidate slide by its final score from step 07, highest first.
  2. Walk down that sorted list one slide at a time.
  3. For each slide, check whether its real (normalized) text has already been seen, word for word, from a higher-scoring slide earlier in the list.
  4. If it has, drop this lower-scoring copy entirely. If not, keep it and remember its text, so later, lower-scoring duplicates of it get caught too.
example — real pair caught
...nfl-final-submission_Slide_50 and ...nfl-unified-fan-view-v1_Slide_50 — two different deck files, snippet text word-for-word identical ("5. Data & Analytics Partner for leading Technology Major…"). Lower-scored one dropped.
09Near-duplicate embedding dedup (union-find)_dedupe_near_duplicate()NEW

Catches the harder case: the same template slide reused across decks with differing extracted text (different vision captions/wrapper format per file), so exact-text matching misses it. Reconstructs each candidate's FAISS vector, computes the full pairwise similarity matrix, and unions any pair ≥0.90 cosine via union-find — not a greedy "compare only to what's already kept" walk, which left survivors when one boilerplate slide had 254 near-identical copies smeared across a 0.74–0.99 similarity range.

in plain English cosine similarity is just a similarity score from 0 to 1 for how close two slides are in meaning — 0 means nothing alike, 1 means essentially identical content. This step checks that score for every possible pair of candidate slides, then uses a technique called union-find to correctly group together every slide that's similar to at least one other slide in the same chain — even if not every pair in that chain scores high enough directly against each other (see the real example below, where A-to-B and B-to-C are both similar enough even though A-to-C isn't). A simpler "only compare to what I've already kept" approach misses exactly this kind of chain, which is why it's not used here.
the logic here
  1. Take every slide still in the running after step 08.
  2. Compute how similar every single pair of them is in meaning (not just exact text), using the same real math the search index already relies on.
  3. For every pair that scores 0.90 or higher, mark them as connected.
  4. Follow all the chains of connections — if A connects to B and B connects to C, treat A, B, and C as one group — so a smeared-out set of near-copies all end up together even if not every pair individually cleared 0.90.
  5. From each group, keep only the highest-scoring slide and drop the rest.
example — real 15-instance sample, one boilerplate template
pairwise cosine similarity: min 0.74, max 0.99, mean 0.91 28 of 105 pairs individually score BELOW the 0.90 threshold → a greedy walk (compare only to what's kept) leaves 3 survivors → union-find (compare via ANY chain of similar pairs) collapses to 1 survivor (13 of 15 instances merge into one connected component)
10Global per-deck cap_cap_deck_global() · GLOBAL_DECK_CAP=2NEW

Caps one canonical deck to 2 appearances across the whole response, applied before the group split. The existing per-group DECK_CAP=2 (below) couldn't stop a deck from claiming 2 slides in every section it touched — a large compilation deck could still show up 4× total.

in plain English A "canonical deck" just means different draft versions of the same underlying deck (V1, V2, vFinal, and so on) are recognized as one single deck for capping purposes, not counted separately — explained with real numbers in step 11's version history below. The "group split" is simply the moment results get divided up into the six sections described in step 11.
the logic here
  1. Take the full, deduplicated, score-sorted list from step 09.
  2. Work out each slide's real underlying deck identity, with version/draft naming stripped off, so different drafts of the same deck count as one.
  3. Walk down the list keeping a running count of appearances per deck.
  4. The moment a deck would be about to appear for a 3rd time anywhere in the whole response, skip that slide and move to the next one — capping every deck to at most 2 total appearances before anything gets split into sections.
example — real, live-observed before this fix
"Visualisation-slides Consolidated.pptx" (a large multi-hundred-slide compilation deck) appeared 2× in Case Studies + 2× in Proposals = 4× total for one query. After the fix: capped to 2× total, not eliminated — the other 2 slots go to genuinely different decks.
11Group by real content type, cap per deck within groupcontent_group_of() · _diversify() · DECK_CAP=2NEW

Sections are now Case Studies / Approach & Methodology / Data & Analytics / Capabilities & Overview / Team & Credentials / Other — built from each slide's own slide_type, not which Drive folder it's filed in. Folder path stayed as step 07's internal ranking signal; it's no longer what decides the section a user sees.

in plain English slide_type is simply the content-category label already attached to every individual slide (case study, chart, title slide, and so on) — that label is what now decides which of the six sections you see a slide under, instead of which Drive folder the deck happens to be filed in. DECK_CAP=2 (in the file name above) is the per-section limit on how many slides from one deck can appear together — explained with real numbers in this block's version history below.
the logic here
  1. Take the response-wide-capped list from step 10.
  2. For each slide, look up its own content-type tag and decide which of the six sections it belongs in.
  3. Walk through the slides destined for each section in score order.
  4. Separately from the whole-response cap in step 10, also cap any one deck to at most 2 slides WITHIN that particular section, skipping any slide that would push a deck past that limit.
Changed, explicit direction: "folder is for you [the system] to navigate, not for the end user — for the end user, the slide type is going to be the top." Old sections (Proposals/Capabilities/Collateral) answered "who's this deck for," not "what's on this slide."
example — where our traced slide lands
slide_type content → maps to "Capabilities & Overview" section (via content_group_of()'s _OVERVIEW_TYPES set).
12Sections ordered by real per-query relevance_format()NEW

Each section is already best-first internally, so its own top score is its real relevance to this query — sections now sort by that descending instead of a fixed sequence. A capability-only brief no longer leads with an empty or weak Case Studies section just because that used to be first in a hardcoded list.

in plain English A section's top score is simply the highest score among the individual slides sitting inside it (the same scores computed back in step 07). This step uses that one number per section purely to decide the order sections are shown in — nothing about the slides inside each section changes, just which section you see first, second, and so on.
the logic here
  1. Once every section has its own slides in place from step 11, look at each section's own highest score.
  2. Sort the sections themselves by that top score, highest first.
  3. An empty section (zero real matches) sorts to the very end, but is still included rather than hidden entirely.
example — real section order for "customer analytics"
Capabilities & Overview top score 0.1126 ← leads (our traced slide) Data & Analytics top score 0.1102 Other top score 0.0485 Approach & Methodology top score 0.0277 Case Studies top score 0.0232 Team & Credentials top score 0.0141 ← last
13Parallel second source: master case-study sheet_master()

Independent cosine-similarity search over 1,194 curated master case rows — runs alongside the slide search, not instead of it. Each row now also carries a real thumbnail_url when its deck happens to also be indexed (~38% of rows) — those cards used to always show a blank placeholder even when a real image existed.

in plain English Same 0-to-1 similarity score described in step 09 above, applied here to a completely separate, smaller list: the team's own hand-picked 1,194 "known good" case studies. A score of 0.727 (the real example below) means fairly strong — the closer to 1, the closer that master-sheet case study is, in meaning, to what was actually searched for.
the logic here
  1. Completely separately from everything in steps 04-12, take your original search and compare it mathematically against all 1,194 rows in the team's own curated case-study spreadsheet.
  2. Score every row by how similar it is in meaning.
  3. For each of the strongest matches, check whether its underlying deck also happens to exist in the main slide library — if so, pull a real thumbnail image from there.
  4. Return the strongest handful of these as a separate "Verified Case Studies" result set, alongside (not instead of) everything from the steps above.
example — real top match
Gramener / "chemists redemption behaviour" — cosine similarity 0.727 to "customer analytics", real thumbnail resolved (its deck is also indexed).
14Response assembled_format()NEW

{query, intents, client, use_case, expanded_terms, groups: {...}, master_sheet: [...], total_matches}. Each card's title is now the slide's own real headline — previously every card in a deck showed the deck's name as its title, so two genuinely different slides from the same deck looked identical by name. A separate deck field carries the presentation name.

in plain English This is simply the internal package of data the backend hands back after everything on this page has run — not something you'd ever see directly, just what powers the actual screen. query/intents/client/use_case record what was searched and why (steps 01-04). groups holds the real slide cards, organized into the sections from step 11, each carrying its own real relevance score. master_sheet carries the separate Verified Case Studies matches from step 13. total_matches is just the total slide count added up across every section.
the logic here
  1. Take everything decided so far — the sections and their slides from steps 11-12, and the separate Verified Case Studies list from step 13.
  2. Package it all into one single response, making sure each card carries the slide's own real title (not its deck's name), plus a separate field for the deck name.
  3. Hand that whole package back to the chat assistant.
example — real assembled response (trimmed)
{ "query": "customer analytics", "intents": ["capability"], "total_matches": 30, "groups": { "Capabilities & Overview": [{ "title": "Indicative Output: PBI & Azure Stack Engagement", "deck": "Eu Yan Sang - Gramener - PBI Engagement Proposal - v1.pptx", "score": 0.1126, "type": "content", … }, …], … 5 more sections … }, "master_sheet": [{"client":"Gramener","project":"chemists redemption behaviour", …}] }
15Agent surfaces the resultspresent_groups (frontend tool)

The chat agent calls present_groups to render slide cards. Its section order now mirrors whatever the backend already curated for this query, rather than defaulting to a habitual "Case studies first" sequence — the frontend also stopped re-imposing its own fixed order over the backend's.

in plain English present_groups is just the internal name of the specific action the assistant takes to actually display a set of organized, sectioned slide cards in the chat window, instead of one flat, undifferentiated list.
the logic here
  1. The assistant receives the assembled package from step 14.
  2. It calls present_groups, handing over the sections in the exact order the backend already decided in step 12 — it's specifically instructed not to re-sort them itself.
  3. The chat window renders each section as its own labeled group of cards, in that order.
15bOn demand: "Why this?" per-card explanationSlideCard.tsx → /api/slide-search/explainNEW

A person can click any card to ask why it matched. The backend explain endpoint already existed, but was never called from the UI until this quarter — one batched Gemini call, given the same query this result set was actually ranked against (pulled from the message's own tool-call arguments, not the raw chat text, since the agent may have decomposed a multi-part brief differently).

in plain English A Gemini call here just means asking Google's AI model to write the explanation text. "Batched" means all the currently-visible cards' explanations can be requested together in one single request rather than one at a time, which is what keeps clicking around fast rather than triggering a slow round-trip per card.
the logic here
  1. Wait until a person actually clicks "Why this?" on a specific card — nothing happens automatically before that.
  2. When clicked, look up the exact search text that particular result set was actually ranked against (from the assistant's own internal record, not necessarily your literal typed words).
  3. Send that search text plus the card's own content to the AI model, along with honest instructions on how to describe a genuine match, a loose match, or no match at all.
  4. Show the returned sentence or two directly on the card.
Fixed twice after shipping: first, cards from the master case-study sheet failed 100% of the time (no database row to explain); then, explanations were found to fabricate connections that don't exist on the slide at all.
ENDPerson sees grouped, ranked, deduplicated slide cards

Content-type sections, ordered by relevance to the actual question asked, each internally ranked, each genuinely distinct — no boilerplate flooding a section, no deck flooding the response, no unrelated slide masquerading as "similar" because it happened to share a deck name.

the logic here
  1. Nothing new happens at this last step — it's simply the sum of every decision made in steps 01 through 15b above, all now visible on screen at once.
  2. What made it here survived every check along the way: it matched your actual wording or a genuinely related term (06a/06b), scored well on folder/type/recency (07), wasn't a duplicate (08/09), didn't crowd out other decks (10/11), and landed in the section that actually matches what's on it (11), shown in the order that's genuinely most relevant to your question (12).
running example, end to end
"customer analytics" → 30 total matches across 6 sections, led by Capabilities & Overview, top card "Indicative Output: PBI & Azure Stack Engagement" at score 0.1126 — plus 5 Verified Case Studies from the master sheet, top match Gramener at similarity 0.727.