Connect PeopleSoft to the service
PeopleSoft calls the service from the application server, through the integration gateway, and
then runs the plan with the Search Framework. Both halves live in one application class,
ZZ_NLSEARCH:Search, which the search page uses, and which an application service can later
offer to the PeopleSoft Assistant.
Give the gateway its certificate
Section titled “Give the gateway its certificate”-
Get a client certificate for the integration gateway from the certificate authority that nginx trusts, as in Put nginx in front.
-
Import it, with its private key, into the integration gateway’s keystore.
-
Point the gateway at it. In the Integration Gateway CERTIFICATE section of
integrationGateway.properties, set the keystore’s path insecureFileKeystorePath, its encrypted password insecureFileKeystorePasswd, and the certificate’s alias and alias password. Then restart the web server that runs the gateway.
Store the service’s address
Section titled “Store the service’s address”At PeopleTools > Utilities > Administration > URLs, create the URL definition ZZ_NLSEARCH,
with the value https://nlsearch.example.com. Each environment can then point to its own
service, without code changes.
Write the application class
Section titled “Write the application class”In Application Designer, create the application package ZZ_NLSEARCH, with the class Search.
It has two public methods:
Plansends the question to the service, and returns its response.Runruns the response’s search with the Search Framework, leaving out any filters the user removed, and returns the results. With no plan, it runs a keyword search with the question as typed.
The page calls Plan once for each question, and keeps the response. When the user removes a
filter, it calls Run again, with no new call to the service.
import PT_SEARCH:*;
class Search method Plan(&question As string) Returns JsonObject; method Run(&plan As JsonObject, &question As string, &removed As array of number, &start As number, &size As number) Returns PT_SEARCH:SearchResultCollection;private method AddFilter(&fg As PT_SEARCH:SearchFilterGenerator, &filter As JsonObject); method Day(&iso As string) Returns date;end-class;Get a plan
Section titled “Get a plan”Plan builds the request, and sends it through the integration gateway with ConnectorRequest,
using the HTTPTARGET connector. The date is the user’s, from %ClientDate, so “last month”
means the same to the service as it does to the user.
method Plan Local JsonObject &request = CreateJsonObject(); &request.AddProperty("question", &question); &request.AddProperty("today", DateTimeToLocalizedString(%ClientDate, "yyyy-MM-dd")); &request.AddProperty("language", %Language_User);
Local Message &msg = CreateMessage(Operation.IB_GENERIC); &msg.IBInfo.IBConnectorInfo.ConnectorName = "HTTPTARGET"; &msg.IBInfo.IBConnectorInfo.ConnectorClassName = "HttpTargetConnector"; &msg.IBInfo.IBConnectorInfo.AddConnectorProperties("Method", "POST", %HttpProperty); &msg.IBInfo.IBConnectorInfo.AddConnectorProperties("URL", GetURL(URL.ZZ_NLSEARCH) | "/plan", %HttpProperty); &msg.SegmentContentType = "application/json"; &msg.SetContentString(&request.ToString());
/* With True, a failed call returns a status instead of an error */ Local Message &resp = %IntBroker.ConnectorRequest(&msg, True); If &resp.ResponseStatus <> %IB_Status_Success Then Return Null; End-If;
Local JsonParser &parser = CreateJsonParser(); If Not &parser.Parse(&resp.GetContentString()) Then Return Null; End-If; Return &parser.GetRootObject();end-method;When Plan returns Null, because the service couldn’t be reached, the page falls back to a
keyword search, just as it does when no intent fits.
Run the search
Section titled “Run the search”Run sets the category and the query text, and turns the filters into one Search Framework
filter, in which every part must match.
method Run Local PT_SEARCH:SearchFactory &factory; Local PT_SEARCH:SearchQueryService &svc; Local PT_SEARCH:SearchQuery &qry; &factory = create PT_SEARCH:SearchFactory(); &svc = &factory.CreateQueryService("default"); &qry = &svc.CreateQuery(); &qry.Language = %Language_User;
Local boolean &planned = False; If &plan <> Null Then &planned = Not &plan.IsNullProperty("intent"); End-If;
/* No plan: an ordinary keyword search, in every category */ If Not &planned Then &qry.QueryText = &question; Return &qry.Execute(&start, &size); End-If;
Local JsonObject &search = &plan.GetJsonObject("search"); Local string &category = &search.GetString("category"); Local PT_SEARCH:SearchCategory &cat; &cat = create PT_SEARCH:SearchCategory(&category); &qry.Categories = CreateArray(&cat); If &search.GetString("text") <> "" Then &qry.QueryText = &search.GetString("text"); End-If;
/* Every filter must match, except the ones the user removed */ Local PT_SEARCH:SearchFilterGenerator &fg; &fg = create PT_SEARCH:SearchFilterGenerator(); Local JsonArray &filters = &search.GetJsonArray("filters"); Local integer &i, &added; &fg.MatchAllReturnTrue(); For &i = 1 To &filters.Length() If &removed.Find(&i) = 0 Then %This.AddFilter(&fg, &filters.GetJsonObject(&i)); &added = &added + 1; End-If; End-For; &fg.EndMatchAll(); If &added > 0 Then &fg.SetQueryFilter(&qry); End-If; Return &qry.Execute(&start, &size);end-method;The Search Framework applies the user’s security to every search, including this one, so Run
has nothing to add for it.
Turn each filter into a condition
Section titled “Turn each filter into a condition”AddFilter maps each filter’s type and operator to a method of the Search Framework’s filter
generator. between becomes a block of two conditions that must both match, and Day turns the
service’s dates into PeopleCode dates.
method AddFilter Local string &attr = &filter.GetString("attribute"); Local string &type = &filter.GetString("type"); Local string &op = &filter.GetString("op"); Local JsonArray ⦥ Local date &day;
/* Codes are already translated, so they match like text */ If &type = "code" Then &type = "text"; End-If; If &type = "date" And &op <> "between" Then &day = %This.Day(&filter.GetString("value")); End-If;
Evaluate &type | "/" | &op When "text/equals" &fg.EqualsString(&attr, &filter.GetString("value")); When "text/not_equals" &fg.NotEqualsString(&attr, &filter.GetString("value")); When "text/contains" &fg.ContainsWord(&attr, &filter.GetString("value")); When "user/equals" &fg.EqualsString(&attr, %UserId); When "employee/equals" &fg.EqualsString(&attr, %EmployeeId); When "number/equals" &fg.EqualsNumber(&attr, &filter.GetNumber("value")); When "number/greater_than" &fg.GreaterThanNumber(&attr, &filter.GetNumber("value")); When "number/less_than" &fg.LessThanNumber(&attr, &filter.GetNumber("value")); When "number/between" &range = &filter.GetJsonArray("value"); &fg.MatchAllReturnTrue(); &fg.GreaterThanEqualsNumber(&attr, &range.GetNumber(1)); &fg.LessThanEqualsNumber(&attr, &range.GetNumber(2)); &fg.EndMatchAll(); When "date/on" &fg.EqualsDate(&attr, &day); When "date/before" &fg.BeforeDate(&attr, &day); When "date/after" &fg.AfterDate(&attr, &day); When "date/between" &range = &filter.GetJsonArray("value"); &fg.MatchAllReturnTrue(); &fg.OnOrAfterDate(&attr, %This.Day(&range.GetString(1))); &fg.OnOrBeforeDate(&attr, %This.Day(&range.GetString(2))); &fg.EndMatchAll(); When-Other throw CreateException(0, 0, "Unknown filter " | &type | "/" | &op); End-Evaluate;end-method;
method Day /* "2026-08-01" to a PeopleCode date */ Return Date3(Value(Substring(&iso, 1, 4)), Value(Substring(&iso, 6, 2)), Value(Substring(&iso, 9, 2)));end-method;PeopleSoft never takes a user ID from the plan: a user filter always matches %UserId, and an
employee filter %EmployeeId, whatever value the plan holds.
Log each search
Section titled “Log each search”Record each search’s request ID with the user who searched, in a custom record such as
ZZ_NLS_LOG, with the user ID, the request ID from the response, the date and time, the intent,
and whether the search fell back. The service’s audit log has no user IDs, so this is how an
authorized administrator can find who asked a question, when they need to. Write the row in the
page’s PeopleCode, after Plan returns.
Test it from Application Engine
Section titled “Test it from Application Engine”Before the page exists, you can try the class from a PeopleCode step in an Application Engine program. The search runs as the user who runs the program, so run it as a test user:
import ZZ_NLSEARCH:Search;import PT_SEARCH:*;
Local ZZ_NLSEARCH:Search &nls = create ZZ_NLSEARCH:Search();Local string &q = "Unpaid vouchers for Globex";Local JsonObject &plan = &nls.Plan(&q);If &plan = Null Then MessageBox(0, "", 0, 0, "No plan: check the gateway logs");Else MessageBox(0, "", 0, 0, &plan.ToString());End-If;
Local array of number &none = CreateArrayRept(0, 0);Local PT_SEARCH:SearchResultCollection &results;&results = &nls.Run(&plan, &q, &none, 1, 10);MessageBox(0, "", 0, 0, &results.GetDocumentCount() | " vouchers");The message log shows the plan, and the number of vouchers the test user may see. If there’s no
plan, look in the gateway’s errorLog.html, as
Follow a search, server by server describes; the search then
runs as a keyword search.
