AI-Native Contract Search: Why We Replaced Keywords With Hybrid Retrieval
Legacy government contract tools make you guess the exact words a contracting officer typed. Here is how Civic AI's search engine actually works — dense embeddings, IDF-weighted lexical matching, and rank fusion — plus the benchmark numbers behind it.
Search is the whole product in government contracting. If a contractor cannot find an opportunity, nothing downstream matters — no pipeline, no capture plan, no proposal, no award. And yet the tools most contractors pay for still run on the same idea they used in 2008: match the characters the user typed against the characters in the notice.
TL;DR: Keyword search makes you guess the government's vocabulary. Civic AI runs dense vector retrieval and IDF-weighted lexical retrieval in parallel, fuses them with Reciprocal Rank Fusion, and pins exact-ID lookups to the top. On our production benchmark that yields a 92% mean hit rate at a 1,359 ms p95.
The actual problem with keyword search
A contracting officer writing a solicitation is not optimizing for your search box. They are following an agency template, a commodity code, and whatever phrasing their office has used for fifteen years.
Sometimes they are not writing words at all. Here are four live Florida DOT paving solicitations, exactly as their titles appear in the source system:
T4764
E4Y74-R0
E1Y76-R0
E3Y73
No keyword search will ever connect a paving contractor to those records. There is nothing to match. The word "paving" does not appear, the word "road" does not appear, and no amount of boolean syntax will help — the signal lives entirely in the description and the issuing agency, not the title.
This is the general case, not an edge case. Keyword search silently returns zero results, and a zero-result search is indistinguishable from "there is no work for me this week." That is the failure mode that costs contractors real money — not a slow page, but a confident empty list.
The standard workaround is to make the user absorb the complexity: learn NAICS codes, learn PSC codes, learn each agency's phrasing, then maintain a sprawl of boolean saved searches with a dozen OR clauses. That is not a search product. That is a query language with a marketing site.
What we built instead
Civic AI's opportunity search runs two retrievers against the same index at the same time, then merges their rankings. Neither one is sufficient alone, and the interesting engineering is in how they cover each other's weaknesses.
Leg one: dense retrieval
Every indexed opportunity is embedded as a vector from a composed text blob — not just the title:
title
Agency: {full agency path}
Location: {city}, {state}
NAICS: {naics code}
Set-aside: {set-aside type}
{description}
Your query is embedded with the same model into the same space, and we retrieve by cosine similarity. This is what closes the vocabulary gap, and it is why those opaque FDOT title codes are reachable at all — the description and agency carry the meaning, so road repair and paving contracts in Florida returns T4764 and E4Y74-R0 from the Florida Department of Transportation as the top hits.
It also means you do not have to know the industry's vocabulary before you are allowed to search it. Two real examples from production, neither of which contains the term it needs to match:
| What you type | What comes back |
|---|---|
someone to fix the air conditioning at a base | Repair Building 510 HVAC (Air Force), B1397 HVAC Replacement — JBSA-Fort Sam Houston |
protect military networks from hackers | Mobile Environment Security Architecture (MESA) VPN (DISA), National Information Assurance Partnership (NIAP) (Army) |
The first query never says "HVAC." The second never says "cybersecurity." A keyword engine returns nothing for both.
Embedding the agency, location, NAICS, and set-aside directly into the document text also means natural-language context like "on military bases" or "in Florida" has something real to match against, rather than being discarded because the user did not click a filter chip.
Leg two: lexical retrieval, IDF-weighted
Semantics alone will fail you the moment precision matters. If a user pastes notice ID 70T02026Q1234, no embedding model on earth reliably retrieves that exact record — an ID carries no semantic meaning.
So the second leg matches query tokens against the indexed document text directly. We lowercase, strip punctuation, drop stopwords and tokens under three characters, and cap at eight tokens. Then we score matches by inverse document frequency rather than raw hit count:
idf(token) = log((N + 1) / (df + 1)) + 1
This detail matters more than it looks. Without IDF, a query like bridge repair services in California gets dominated by documents that repeat "services" and "California" many times, because raw hit-count rewards length and common words. With IDF, a token appearing in one document out of hundreds of thousands dominates the score, and ubiquitous tokens barely register. Rare is informative; common is noise.
Fusing the two
The two legs produce scores on incompatible scales — cosine similarity and an IDF sum have no shared units, so you cannot just add them, and tuning a blend weight is a losing game that needs recalibration every time the corpus shifts.
We use Reciprocal Rank Fusion, which throws away the scores and keeps only the ranks:
score(doc) = Σ weight / (60 + rank in that retriever)
A document ranked highly by both retrievers wins. A document ranked first by one and ignored by the other still places well. There is nothing to recalibrate when the corpus grows, which is exactly the property you want in an index that takes on new state and local sources every month.
Two rules we refuse to break
Architecture is mostly the constraints you hold onto when a shortcut is available. Two of ours are worth spelling out, because they are where we differ most from other "AI search" products.
We never paraphrase your query
The tempting design — and one many AI search tools ship — is to put an LLM in front of retrieval to "clean up" the query before searching.
We deliberately do not. Sending the query through a language model first drops unique tokens, and notice IDs are exactly the tokens that get dropped. It rewrites word forms, so Maintain becomes maintenance and no longer matches the lexical leg. It adds latency to every single search. Worst of all, it is non-deterministic: the same query can quietly return different results on Tuesday than it did on Monday, and there is no way for a user to tell why.
Your raw query is what gets embedded and tokenized. The intelligence lives in the retrieval architecture, not in a preprocessing step that mutates your intent before the search even starts.
Natural language never becomes a silent hard filter
If you type "contracts in California," we do not convert that into a hard state = CA filter behind your back.
This looks like a missing feature until you have watched it fail. An inferred filter is invisible and absolute — if the model decides your query means Virginia when you meant the VA, everything else is now unreachable and you have no way of knowing results were removed. Meanwhile a genuinely relevant multi-state IDIQ headquartered in Nevada gets silently deleted from your results.
Hard filters come only from filters you explicitly set in the UI, where you can see them and remove them. Location, agency, and industry intent expressed in natural language is handled by ranking, not exclusion. It moves matching contracts up; it never makes anything disappear. The only always-on filter is that a notice must still be open.
And one exception: known items
Fuzzy discovery is the wrong behavior when a user names a specific record. If your query contains a six-or-more-digit ID, or exactly matches a notice title, we fetch that record directly and pin it to position one above all fused results.
This deliberately bypasses your active filters. If you paste an ID, you want that notice — not a polite message that it did not match your current set-aside filter.
The numbers
We keep a checked-in evaluation set of 12 queries written the way contractors actually type, each with expected relevance signals and a note on how an incumbent handles the same search. Here is a production run from today against api2.civiccontracts.com:
| Metric | Result | Gate |
|---|---|---|
| Cases passed | 12 / 12 | — |
| Mean top-N hit rate | 92% | ≥ 50% |
| Warm p95 wall time | 1,359 ms | ≤ 4,500 ms |
| Warm mean wall time | 714 ms | — |
| Slowest warm run | 1,515 ms | ≤ 4,500 ms |
Stage breakdown on warm runs:
| Stage | Mean | Budget |
|---|---|---|
| Embed query | 160 ms | 800 ms |
| Hybrid retrieval | 451 ms | 2,500 ms |
| Hydrate results page | 7 ms | 500 ms |
Hydration is 7 ms because ranking and filtering happen entirely in the vector index against metadata. We only touch the document store at the very end, to load the roughly twenty records on the page you are actually looking at. Nothing gets pulled just to be ranked.
The queries themselves are not synthetic keyword strings. They are things like "I built bridges before, find me open contracts in CA" and "janitorial service opportunities for women-owned businesses" — full sentences, with intent, the way someone describes their business rather than the way a database wants to be addressed.
The incumbents are explicit about this
We are not inferring how competing platforms search. They publish it.
GovSpend's own documentation describes its search as "keyword-based lexical," with semantic search listed as planned rather than shipped. And Deltek, whose GovWin IQ is the largest platform in the category, markets directly against the approach: "Feeding that same data into an AI tool doesn't make it more reliable."
That is a real argument, and it deserves a real answer rather than a slogan. Deltek is right that pointing a language model at messy data does not clean it — which is precisely why we do not ask a model to clean anything. We do not summarize solicitations into an index, we do not let an LLM rewrite your query, and we do not infer filters. The retrieval layer reads the source text as published and matches it on meaning. Reliability comes from never mutating the data or the query; relevance comes from how they are matched.
The alternative they are defending is 150 analysts hand-curating a subset. That produces genuinely excellent coverage of the opportunities analysts choose to track, and it cannot scale to the county paving job posted as T4764 on a Tuesday.
Why this compounds
Search quality and coverage multiply rather than add. A great index over federal data alone still misses the county work where competition is thinnest. Broad coverage behind a keyword box still buries you, because more sources with bad ranking is just more noise.
Civic AI runs this pipeline across 68 live sources — federal, state, county, city, transit, and school district portals — refreshed daily. Every added source makes the retrieval layer more valuable, and the retrieval layer is what makes a large multi-source index usable rather than overwhelming.
That is the bet: search is not a feature on top of a procurement database. Search is the product, and everything else is downstream of getting it right.
Try it against whatever you use today
The honest test takes two minutes. Take a real search you ran this week on SAM.gov, GovWin, or your state portal, and run the same intent in plain English on Civic AI:
- someone to fix the air conditioning at a base
- we lay asphalt in FL
- protect military networks from hackers
- I built bridges before, find me open contracts in CA
If a competing tool finds something we miss, we want to know — that is a concrete gap in either coverage or ranking, and both are fixable. Start with Civic AI search, then set up Scout so new matches come to you. For background on the sources behind the index, see our state procurement portals guide and how to find government contracts.
Frequently asked questions
- What is AI-native search for government contracts?
- AI-native search matches on meaning rather than exact character strings. Instead of requiring you to type the precise words a contracting officer used, it embeds your query and the solicitation text into the same vector space. On Civic AI, searching 'someone to fix the air conditioning at a base' returns Air Force HVAC replacement solicitations even though the query never contains the term HVAC — and searching 'road repair and paving contracts in Florida' returns Florida DOT notices whose titles are opaque codes like T4764, which no keyword search could ever reach.
- Is semantic search worse than keyword search for exact lookups?
- It can be, which is why Civic AI does not use semantic search alone. We run dense vector retrieval and lexical matching side by side and fuse the rankings, and we detect known-item lookups — a notice ID or a verbatim title — and pin those results to position one. You get semantic recall without losing exact-match precision.
- Does Civic AI rewrite my search query?
- No. The raw query is embedded and tokenized exactly as you typed it. Query rewriting is a common design in AI search tools, but it drops unique tokens like notice IDs and mangles word forms, which breaks both the vector match and the lexical match. We treat never paraphrasing the query as a hard architectural rule.
- How fast is Civic AI search?
- On our canonical benchmark of 12 real contractor queries run against production, warm requests average 714 ms end to end with a p95 of 1,359 ms. That breaks down to roughly 160 ms to embed the query, 451 ms to retrieve, and 7 ms to hydrate the results page.
- How do you measure search quality?
- We maintain a checked-in evaluation set of 12 queries written in real contractor phrasing, each with expected relevance signals and a note on how an incumbent platform handles the same search. Every run scores top-N hit rate and latency against fixed gates. The current production run passes 12 of 12 cases at a 92% mean hit rate.
Find contracts you can win
Search active federal, state, and local opportunities in plain English — with deadlines, agencies, and set-asides surfaced for you.
Start exploring free