Search
Overview
The Search API is accessible via the io.search
object.
See the JavaScript Search example on GitHub.
Enabling Search
To be able to use the Search API in your interop-enabled apps, install the @interopio/search-api
library in your project and reference it in your app:
npm install @interopio/search-api
Initialize the @interopio/desktop
library by passing the globally available IOSearch()
factory function to the libraries
array property of the configuration object. The IOSearch()
factory function, like the IODesktop()
factory function, is injected in the global window
object. When the IODesktop()
factory function resolves, the Search API will be accessible via the search
property of the returned object.
import IODesktop from "@interopio/desktop";
import IOSearch from "@interopio/search-api";
const config = {
libraries: [IOSearch]
};
const io = await IODesktop(config);
// Now you can access the Search API through `io.search`.
Search Providers
You can create your own search providers for any custom results you want to supply to search clients - e.g., apps, accounts, instruments, and more. The search provider is usually a hidden service app that starts automatically and may be connected to an external backend, such as a REST service. A single search provider may return multiple types of responses defined as search types.
Providing search results is a process that can be divided into the following stages:
- registering a search provider;
- registering a handler for incoming search queries;
- sending results back to search clients and handling events on the provider side;
To return search results to search clients, you must first register a search provider. The registered provider is then able to respond to incoming search requests by registering a handler for search queries. This handler is responsible for executing your internal search logic, which may include connecting to a network service, refining and consolidating results, and more, depending on your specific environment and use cases. The handler is also responsible for sending results back to the search clients, which may be done asynchronously, depending on your internal logic. During this process, you may need to handle events on the provider side, such as a search client canceling a query (e.g., because the user has already clicked on a rendered result in the UI).
Registering Providers
To register a search provider, use the registerProvider()
method and pass a ProviderRegistrationConfig
object as an argument. It's only required to specify a name for the search provider, but it may also be useful to define the search types with which your provider works:
const config = {
name: "my-search-provider",
types: [
{
name: "accounts",
displayName: "Accounts"
}
]
};
const provider = await io.search.registerProvider(config);
You can use the returned Provider
object to respond to search queries (and search cancellations), or to unregister the search provider from the framework.
The Provider
object has the following properties and methods:
Property | Type | Description |
---|---|---|
appName |
string |
Name of the app that has registered the search provider. |
id |
string |
Required. Unique app ID within the framework. |
interopId |
string |
Required. Unique ID of the Interop instance of the app. |
name |
string |
Required. Name of the search provider. |
types |
object[] |
List of SearchType objects describing the search types with which the search provider works. |
Method | Description |
---|---|
onQuery() |
Notifies when a search query is received. Accepts as an argument a callback for handling the event. The callback receives a ProviderQuery object describing the search query. |
onQueryCancel() |
Notifies when a search query is canceled. Accepts as an argument a callback for handling the event. The callback receives an object with an id property holding the ID of the canceled search query. |
unregister() |
Unregisters the search provider from the framework. |
To unregister a search provider from the framework, use the unregister()
method of a Provider
object:
await provider.unregister();
Handling Queries
The actual search operations within a search provider must be executed with your own custom business logic pertaining to your particular use cases. The Search API, however, supplies the means for receiving queries, sending results and errors back to search clients, responding to query cancellations, or unsubscribing from receiving queries altogether.
To handle incoming search requests, use the onQuery()
method of a Provider
object and pass a handler that will process the queries:
// The callback receives a `ProviderQuery` object as an argument whose properties and methods
// you can use to handle the query and send back results to the search client.
const handler = ({ search, types, providerLimits, sendResult, error, done }) => {
// Use your internal search logic to perform the necessary search operations.
// Any errors will be sent to the search client via the `error()` method.
const results = await searchMyDBs(search, types, providerLimits).catch(error);
// Each returned result must be a `QueryResult` object, e.g.:
// const result = {
// type: {
// name: "accounts",
// displayName: "Accounts"
// },
// displayName: "Vernon Mullen",
// action: {
// method: "OpenAccountDetails",
// params: { accountID: "account_42" }
// }
// };
// Use the `sendResult()` method to send each result back to the search client.
results.forEach(sendResult);
// Use the `done()` method after your internal search logic has been executed
// and all results or errors have been sent to the search client.
// This will signal the framework that the search operation is completed.
done();
};
provider.onQuery(handler);
The ProviderQuery
object passed as an argument to the handler for the onQuery()
method has the following properties and methods:
Property | Type | Description |
---|---|---|
id |
string |
Required. Assigned by the framework ID for the search query. |
providerLimits |
object |
ProviderLimits object specifying the maximum total results and/or the maximum results per search type to return. |
providers |
object[] |
List of ProviderData objects describing the search providers to query. |
search |
string |
Required. String for which to query the search providers. |
types |
object[] |
List of SearchType objects specifying the search types in which the search client app is interested. |
Method | Description |
---|---|
done() |
Signals the framework that the search operation has been completed. |
error() |
Sends errors back to search clients. Accepts a string as a required argument. |
sendResult() |
Sends individual results back to search clients. Accepts a QueryResult object as a required argument. |
The result returned via the sendResult()
method must be a QueryResult
object with the following properties:
Property | Type | Description |
---|---|---|
action |
object |
MainAction object describing an Interop method, arguments for it, and Interop targets to be associated with the search result. A main action can be used to invoke an already registered Interop method when the user clicks on the search result displayed in a UI. If you need to use multiple actions when handling search results, you can use the secondaryActions property instead. |
description |
string |
Description for the search result that can be used in a UI. |
displayName |
string |
Display name for the search result that can be used in a UI. |
iconURL |
string |
URL pointing to an icon for the search result that can be used in a UI. |
id |
string |
ID for the search result that can be used by the search client when handling the result. |
metadata |
any |
Metadata for the search result that can be used by the search client when handling the result. |
secondaryActions |
object[] |
Array of SecondaryAction objects each with a name and describing an Interop method, arguments for it, and Interop targets to be associated with the search result. Secondary actions can be used to invoke different already registered Interop methods if, for example, you want to present the user with a choice of options related to the search result. The secondaryActions property can be used independently, without defining a MainAction in the action property. |
type |
object |
Required. The predefined search type of the result. |
Events
The Search API provides methods that allow search providers to handle various events related to the search process. All methods for handling events return an unsubscribe function that you can use to stop getting notified about the respective event.
To get notified when a search client sends a search query, use the onQuery()
method of a Provider
object:
const handler = (providerQuery) => {
console.log(`Search request for "${providerQuery.search}" received.`);
};
const unsubscribe = provider.onQuery(handler);
// Unsubscribe from receiving search requests.
unsubscribe();
To get notified when a search client cancels a search query, use the onQueryCancel()
method of a Provider
object:
const handler = ({ id }) => {
console.log(`Search query with ID "${id}" was canceled.`);
};
provider.onQueryCancel(handler);
Search Clients
Any app can become a search client by using the Search API to send queries to all or selected registered search providers.
Querying search providers is a process that can be divided into the following stages:
- creating a search query and sending it to search providers;
- handling responses from search providers;
To perform a client-side search operation, you must first create a search query and send it to one or more search providers. You can then use the created query to handle responses from the search providers: search result batches, errors, or signals that the search operation has been completed on the provider side.
Due to the asynchronous nature of the search process, in some cases you may need to cancel a search query. For instance, you may have designed your search client to send queries as the user types, and render the received results in the UI as they come, instead of waiting for the search operation to complete first. If a user clicks on an item before all results are received and displayed, handling the rest of the results becomes unnecessary and you can cancel the query. The search provider will be notified about the cancellation and will be able to respond accordingly, optimizing the search process and saving system resources.
Search providers may assign one or more actions to each result they send to search clients. An action describes an already registered (by your search provider or by some other service app) Interop method with specified arguments, which you can invoke from a search client when the user interacts with a result in the UI.
Creating Queries
To create a search query, use the query()
method and pass a QueryConfig
object as a required argument:
const config = {
search: "My search string."
};
const query = await io.search.query(config);
The only required property of the QueryConfig
object is search
, but you can also use it to target search providers, specify the search types in which you are interested, and set search result limits.
The QueryConfig
object has the following properties:
Property | Type | Description |
---|---|---|
providerLimits |
object |
ProviderLimits object specifying the maximum total results and/or the maximum results per search type to return. |
providers |
object[] |
List of ProviderData objects describing the search providers to query. |
search |
string |
Required. String for which to query the search providers. |
types |
object[] |
List of SearchType objects specifying the search types in which the search client app is interested. |
The query()
method resolves with a Query
object with the following methods:
Method | Description |
---|---|
cancel() |
Cancels the search query. |
onCompleted() |
Notifies when the search query is completed, i.e. when all search operations in all search providers have been completed. Accepts as an argument a callback for handling the event. |
onError() |
Notifies when there is an error in the search process. Accepts as an argument a callback for handling the event. The callback receives a QueryError object as an argument which holds the error message sent by the search provider and data describing the provider that has sent the error. |
onResults() |
Notifies when a result batch is received. Accepts as an argument a callback for handling the event. The callback receives a QueryResultBatch object as an argument which holds a list of QueryResult objects describing each result and data describing the provider that has sent the results. |
Handling Results
Search results are received in batches. To handle any received results, use the onResults()
method of a Query
object and pass a handler that will process the results:
const handler = (resultsBatch) => {
console.log(`Received results from search provider: "${resultsBatch.provider.name}"`);
resultsBatch.results.forEach(console.log);
};
// Handling results in batches.
query.onResults(handler);
Search providers may signal the framework that the search operation has been completed, and may also send errors to search clients in cases of failure. The framework will send a completion signal to the search client when all search providers have completed their internal search operations. Search clients can handle these events by using the onCompleted()
and onError()
methods of a Query()
object. This may be useful if you want to reflect search completion or failure states in your UI:
const handler = (resultsBatch) => {
console.log(`Received results from search provider: "${resultsBatch.provider.name}"`);
resultsBatch.results.forEach(console.log);
};
query.onResults(handler);
// Handling search errors.
query.onError(console.log);
// Waiting for a completion signal from the framework.
query.onCompleted(console.log("Search completed."));
If your internal logic for handling search results requires you to cancel a query, use the cancel()
method of a Query
object:
await query.cancel();
Events
The Search API provides methods that allow search clients to handle various events related to the search process. All methods for handling events return an unsubscribe function that you can use to stop getting notified about the respective event.
To get notified when a batch with results is received, use the onResults()
method of the Query
object:
const handler = (resultsBatch) => {
console.log(`Received results from search provider: "${resultsBatch.provider.name}"`);
resultsBatch.results.forEach(console.log);
};
const unsubscribe = query.onResults(handler);
// Unsubscribe from receiving results.
unsubscribe();
To get notified when the search query operation is completed (all result batches from all search providers have been received), use the onCompleted()
method of the Query
object:
const handler = () => console.log("Search query completed.");
query.onCompleted(handler);
To get notified when there is an error in the search operation, use the onError()
method of the Query
object:
const handler = (searchError) => {
console.log(`Received error from search provider: "${searchError.provider.name}"`);
console.log(searchError.error);
};
query.onError(handler);
Search Types
Search types define the entities with which a search provider works. These can be Workspaces, apps or any other custom entities. Search types are described via SearchType
objects.
The SearchType
object has the following properties:
Property | Type | Description |
---|---|---|
displayName |
string |
User-friendly name for the search type that can be displayed in UIs. |
name |
string |
Required. Name for the search type. |
The built-in search provider of io.Connect Desktop returns results categorized in several different types. The name
properties of the SearchType
objects that describe them are set to "application"
, "workspace"
, "layout"
, and "action"
. Search providers that you build may return types with other names, such as "instruments"
or "accounts"
, depending on your business needs.
Search types allow search clients to further filter the available search providers and send search queries only to providers which support the desired search type. Search types specified in a search query can also optimize to an extent the operation load by instructing the search provider to search for particular entities and thus send only the necessary requests to the respective databases instead of searching through all available resources.
Search types can be optionally defined by using the types
property of the ProviderRegistrationConfig
object when registering a search provider:
const types = [
{
name: "accounts",
displayName: "Accounts"
},
{
name: "instruments",
displayName: "Instruments"
}
];
const config = {
name: "my-search-provider",
types
};
const provider = await io.search.registerProvider(config);
The display name for a search type can be used in UIs when grouping results by type.
On the client side, search types can be specified in a QueryConfig
object when creating a query:
// Specify the search type of interest.
const types = [
{
name: "accounts",
displayName: "Accounts"
}
];
const config = {
search: "Vernon Mullen",
types
};
const query = await io.search.query(config);
Search clients can also filter the available search providers based on the search types with which they work, in order to send queries only to the providers of interest:
// Get all search providers.
const allProviders = await io.search.listProviders();
// Filter the search providers by type.
const providers = allProviders.filter(p => p.types.some(t => t.name === "accounts"));
const types = [
{
name: "accounts",
displayName: "Accounts"
}
];
const config = {
search: "Vernon Mullen",
types
};
const query = await io.search.query(config);
Actions
Assigning actions to search result enables you to provide various functionalities per result type and per individual result. A result action describes an already registered (by your search provider or by some other service app) Interop method with specified arguments, which you can invoke from a search client when the user interacts with a result. For instance, if you have search results of type "accounts"
, you may assign an action for opening an "Account Details" app to all of them - e.g., an Interop method named "OpenAccountDetails"
. In order to display a specific account, the action for each individual result may have a specific account ID assigned as an argument that will be passed to the "OpenAccountDetails"
Interop method.
You can assign actions to search results via the action
and secondaryActions
properties of the QueryResult
object when returning results from your search provider.
The action
property accepts a MainAction
object with the following properties:
Property | Type | Description |
---|---|---|
method |
string |
Required. Name of an Interop method to assign as an action for a search result. |
params |
any |
Arguments that will be passed to the specified Interop method when a search client invokes it. |
target |
{ instance: string } | "all" |
Interop target defining the Interop servers that will be targeted when invoking the specified method. Set to "all" to invoke the method on all Interop servers offering it. Alternatively, you can use an object with an instance property holding the Interop ID of the specific Interop server you want to target. |
The secondaryActions
property accepts an array of SecondaryAction
objects, each with the following properties:
Property | Type | Description |
---|---|---|
method |
string |
Required. Name of an Interop method to assign as an action for a search result. |
name |
string |
Name for the assigned action. May be used to differentiate between actions on the client side. |
params |
any |
Arguments that will be passed to the specified Interop method when a search client invokes it. |
target |
{ instance: string } | "all" |
Interop target defining the Interop servers that will be targeted when invoking the specified method. Set to "all" to invoke the method on all Interop servers offering it. Alternatively, you can use an object with an instance property holding the Interop ID of the specific Interop server you want to target. |
The following example demonstrates registering an Interop method in the search provider that is assigned as an action to each returned result:
// Registering an Interop method that will be used for executing an action.
await io.interop.register("OpenAccountDetails", (args) => {
io.appManager.application("account-details").start(args);
});
// Registering the search provider.
const config = {
name: "my-search-provider",
types: [
{
name: "accounts",
displayName: "Accounts"
}
]
};
const provider = await io.search.registerProvider(config);
// Handling search queries.
const handler = ({ search, sendResult }) => {
// Internal search logic.
const accounts = await searchAccounts(search);
accounts.forEach((account) => {
// Constructing a `QueryResult` object and assigning an action to each result.
const result = {
id: account.id,
type: "accounts",
displayName: account.name
action: {
// Name of the Interop method that will be invoked.
method: "OpenAccountDetails",
// Arguments for the method invocation.
params: { id: account.id }
}
};
sendResult(result);
});
};
provider.onQuery(handler);
When the search client receives search results, you can extract the name of the assigned Interop method and the arguments for its invocation from the QueryResult
objects describing each result.
Listing Search Providers
To retrieve all registered search providers, use the listProviders()
method:
const providers = await io.search.listProviders();
The listProviders()
method resolves with a list of ProviderData
objects. Each ProviderData
object has the following properties:
Property | Type | Description |
---|---|---|
appName |
string |
Name of the app that has registered the search provider. |
id |
string |
Required. Unique app ID within the framework. |
interopId |
string |
Required. Unique ID of the Interop instance of the app. |
name |
string |
Required. Name of the search provider. |
types |
object[] |
List of SearchType objects describing the search types with which the search provider works. |
Listing Search Types
Search providers return results that may be categorized by search type.
To retrieve all available search types from all search providers, use the listTypes()
method:
const searchTypes = await io.search.listTypes();
The listTypes()
method resolves with a list of SearchType
objects.
Debounce
You can specify a debounce interval in milliseconds for search queries in order to limit the message traffic. This may be useful if you plan on creating queries on individual key strokes or other user input methods that may potentially create a large number of queries in a short amount of time. The default debounce interval is 0 milliseconds.
To get the current debounce interval for search queries, use the get getDebounceMS()
method:
const debounce = io.search.getDebounceMS();
To set the debounce interval for search queries, use the get setDebounceMS()
method and pass a number as a required argument:
io.search.setDebounceMS(250);