Skip to content

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.

  1. Get a client certificate for the integration gateway from the certificate authority that nginx trusts, as in Put nginx in front.

  2. Import it, with its private key, into the integration gateway’s keystore.

  3. Point the gateway at it. In the Integration Gateway CERTIFICATE section of integrationGateway.properties, set the keystore’s path in secureFileKeystorePath, its encrypted password in secureFileKeystorePasswd, and the certificate’s alias and alias password. Then restart the web server that runs the gateway.

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.

In Application Designer, create the application package ZZ_NLSEARCH, with the class Search. It has two public methods:

  • Plan sends the question to the service, and returns its response.
  • Run runs 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.

ZZ_NLSEARCH:Search, declaration
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;

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.

ZZ_NLSEARCH:Search, Plan
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 sets the category and the query text, and turns the filters into one Search Framework filter, in which every part must match.

ZZ_NLSEARCH:Search, Run
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.

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.

ZZ_NLSEARCH:Search, AddFilter and Day
method AddFilter
Local string &attr = &filter.GetString("attribute");
Local string &type = &filter.GetString("type");
Local string &op = &filter.GetString("op");
Local JsonArray &range;
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.

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.

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:

Application Engine PeopleCode step
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.