feat(search): add POST /v1/search with dirctl search parity - #2005
Open
akijakya wants to merge 5 commits into
Open
feat(search): add POST /v1/search with dirctl search parity#2005akijakya wants to merge 5 commits into
akijakya wants to merge 5 commits into
Conversation
Contributor
|
The latest Buf updates on your PR. Results from workflow Buf CI / verify-proto (pull_request).
|
There was a problem hiding this comment.
Pull request overview
Adds natural-language catalog search with shared CLI/gateway ranking and pagination.
Changes:
- Centralizes concurrent fan-out and deterministic scoring.
- Adds
POST /v1/searchand gateway handling. - Adds unit tests for ranking, failures, and pagination.
Reviewed changes
Copilot reviewed 8 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
utils/nlsearch/fanout.go |
Implements shared fan-out and ranking. |
utils/nlsearch/fanout_test.go |
Tests shared search behavior. |
server/controller/ai_finder.go |
Adds search database and extractor dependencies. |
server/controller/ai_finder_test.go |
Extends the fake catalog database. |
server/controller/ai_finder_search.go |
Implements the search endpoint. |
server/controller/ai_finder_search_test.go |
Tests endpoint behavior. |
proto/agntcy/dir/catalog/v1/ai_finder_service.proto |
Defines the search API. |
cli/cmd/search/nlsearch.go |
Migrates CLI search to shared ranking. |
api/catalog/v1/ai_finder_service.pb.gw.go |
Adds generated HTTP routing. |
api/catalog/v1/ai_finder_service.pb.go |
Adds generated search messages. |
api/catalog/v1/ai_finder_service_grpc.pb.go |
Adds generated gRPC bindings. |
Files not reviewed (3)
- api/catalog/v1/ai_finder_service.pb.go: Generated file
- api/catalog/v1/ai_finder_service.pb.gw.go: Generated file
- api/catalog/v1/ai_finder_service_grpc.pb.go: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
akijakya
force-pushed
the
feat/api-post-search
branch
2 times, most recently
from
August 12, 2026 12:09
fa4e924 to
3406747
Compare
…nistic Moves the natural-language fan-out and scoring out of cli/cmd/search into utils/nlsearch, behind a Searcher interface that abstracts how a caller reaches the search layer. The CLI keeps issuing SearchCIDs RPCs; a second caller can query a database in-process and get identical results, which is what lets the API endpoint match `dirctl search` by construction rather than by convention. The algorithm is unchanged: one query per extracted signal, keyword signals fanned out to NAME and DESCRIPTION and deduplicated, the union ranked by how many signals matched each record. Tie ordering does change. The scorer accumulated results in goroutine completion order and then sorted only by hit count, so equal-scoring records came back in a different order between runs. That is invisible when printing results once, but it breaks any paginated caller, where an unstable order lets records repeat or disappear between pages. Ranking is now a total order: hit count, then summed signal score, then CID. Per-signal failures are returned rather than printed, so each caller reports them its own way; the CLI keeps warning on stderr. Signed-off-by: András Jáky <ajaky@cisco.com>
Adds a natural-language search endpoint to the AI Catalog gateway. It answers free text with relevance-ranked catalog entries, returning the same records in the same order as `dirctl search "<query>"` for the same query on the same node: both run the shared nlsearch fan-out over the same extracted signals, the CLI over gRPC and the gateway in-process. The request deliberately has no filter field. `dirctl search` applies no facets to a natural-language query, so accepting them here would make the two paths disagree; structured filtering stays on ListAgents. Ranking covers the whole candidate set before paging, so a page is a slice of an already-ordered list and total_count is exact for the candidates found. A failing signal degrades recall rather than the request: it is logged and the remaining signals still contribute. With no extractor configured the RPC returns UNAVAILABLE (HTTP 503); an empty extraction returns an empty page. Closes #1905 Signed-off-by: András Jáky <ajaky@cisco.com>
Enforce the proto's max_len=1024 on the query in code. The service registers no protovalidate interceptor, so the declared constraint was not applied at runtime and an arbitrarily long string could reach the extractor. Mirrors how ListAgents enforces filterMaxLen. Restrict fan-out candidates to records carrying a known catalog module, the same filter GetCatalogEntries applies when projecting entries. GetRecordCIDs searches every record, so the ranking could include records the catalog cannot represent: they consumed page slots and inflated total_count, then silently disappeared at hydration, yielding short pages. The endpoint returns CatalogEntry, so a record with no catalog projection is not a candidate. Signed-off-by: András Jáky <ajaky@cisco.com>
When the extractor derives no signals there is nothing to search on. Returning an empty page made that indistinguishable from "the catalog holds no match", even though only one of the two is fixed by rephrasing. SearchAgents now returns INVALID_ARGUMENT with the same advice `dirctl search` gives, so a client can tell the user what to do. Also drops the display-name substring fallback the endpoint was originally specified with: it would answer a different question than the one asked and return hits where `dirctl search` returns none, which is the parity this endpoint exists to provide. Signed-off-by: András Jáky <ajaky@cisco.com>
protovalidate interprets a string's max_len as a character count, and ExtractTaxonomy — which landed on main with the same 1024 limit — enforces it with utf8.RuneCountInString. SearchAgents used len(), so a multi-byte query was rejected at roughly a third of the documented limit, and the two sibling endpoints disagreed about what their identical constraint meant. Signed-off-by: András Jáky <ajaky@cisco.com>
akijakya
force-pushed
the
feat/api-post-search
branch
from
August 14, 2026 13:14
3406747 to
7ed3973
Compare
tkircsi
approved these changes
Aug 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1905
Summary
Adds
POST /v1/searchto the AI Catalog gateway: free text in, relevance-rankedCatalogEntryresults out. The acceptance bar is parity withdirctl search "<query>"— the same query against the same node returns the same records in the same order through either path.Commits, grouped by the risk they carry:
refactor(nlsearch)— move the existing fan-out intoutils/nlsearchand make ranking deterministic. Touchesdirctl search.feat(search)— the endpoint itself. New surface only.fix(search),feat(search),fix(search)— review follow-ups: query length enforcement, projectable-candidate restriction, rejecting unusable queries, and counting the query limit in characters rather than bytes.Rebased onto
mainafter #2023POST /v1/extractmerged first, and both endpoints live on the same service, so this branch was rebased onto it. Three things came out of that and are worth knowing while reviewing:task api:genproduces no diff.fakeExtractoris now shared. It arrived onmainwith theExtractTaxonomytests; the search tests reuse it instead of declaring a second one in the same package.WithExtractor's doc names both extractor-backed RPCs, and the//nolint:unusedthat guarded theextfield while it had no consumer is gone —mainremoved it when extract became the first one.Heads-up: this changes
dirctl searchThe natural-language search algorithm is not new and is not changing.
dirctl search "some phrase"already decomposed the phrase into signals, queried each one independently, and ranked the union by how many signals matched — that is what its--helphas always described. The first commit moves that code fromcli/cmd/searchintoutils/nlsearchso the gateway can run the same implementation.One behavior does change for the CLI: tie ordering is now stable.
The scorer accumulated results in goroutine-completion order and then sorted only by hit count, so records with equal scores came back in a different order on every run. Printing results once, nobody notices. For a paginated caller it is a correctness bug — an unstable order lets a record appear on two pages, or on none. Ranking is now a total order: hit count, then summed signal score, then CID.
So the CLI's results are the same set, in the same relevance tiers, with ties no longer shuffling between invocations.
How the search works
The extractor turns one phrase into several signals. "review my python code" might yield two skills, a domain, and a keyword. Each is queried independently and concurrently, and the union is ranked by how many signals matched:
The union matters. ANDing the signals — requiring every guess the extractor made to be simultaneously correct — usually returns nothing; that is why a node holding four cloud-tagged records could answer "cloud computing" with an empty list. Ranking, not exclusion, is what separates a record matching four signals from one matching a single signal.
Cost: N concurrent queries per request rather than one, where N is the signal count (roughly 4–10 at the default two tiers). On the gateway these are in-process rather than gRPC round trips, and each is capped at 500 candidates.
Deliberate choices
filterparameter.dirctl searchapplies no facets to a natural-language query, so accepting them here would break parity. Structured filtering stays onGET /v1/agents?filter=. This also removes the facet-versus-pagination problem the earlier design ran into.displayNamefallback on empty extraction. The endpoint was originally specified with a substring fallback; it is dropped. It answers a different question than the one asked and would return hits wheredirctl searchreturns none, breaking the parity this endpoint exists for. [Feature]: Backend POST /v1/search — natural-language search with dirctl parity #1905 has been updated to match.INVALID_ARGUMENTis returned with the advice the CLI gives. Zero results cannot distinguish "nothing in the catalog matches" from "the query was not understood", and only the second is fixed by rephrasing.GetRecordCIDssearches every record, butGetCatalogEntriesonly projects records carrying a known catalog module. Without the restriction, unprojectable records occupied page slots and inflatedtotal_count, then vanished at hydration. Exact CID-set parity with the CLI is not achievable here — it returns raw CIDs for any record, this returnsCatalogEntry— so parity holds over the projectable universe, made explicit in the query rather than appearing as records quietly disappearing.max_len: 1024was not applied at runtime; enforced in code asListAgentsdoes withfilterMaxLen. Counted withutf8.RuneCountInString, matching how protovalidate readsmax_lenand howExtractTaxonomyenforces the same limit — a byte count would have rejected multi-byte queries at roughly a third of the documented length.total_countis candidates found, bounded by the per-signal fan-out cap — not a whole-index count.503when no extractor is configured, so callers can distinguish "not set up" from "no matches".Testing
task lint0 issues (Go and Helm);task test0 failuresNot covered: relevance quality against a seeded catalog. The tests pin the mechanism, not whether the taxonomy matches are good.
Follow-ups
POST /v1/extract, the endpoint the AI Catalog UI will use for tag recommendationdirctl searchvsPOST /v1/searchparity assertion