Advanced identity search via API
Advanced identity search allows you to perform full-text search on identity traits, public and admin metadata, combined with arbitrary filters and faceting.
- New to Ory? Talk to the team about features and plans.
- Already a customer? Open a support ticket.
Getting started
We recommend using the Typesense SDKs for your preferred language to interact with the advanced identity search API. Pass your Ory Network API key where you would normally pass the Typesense API key, use the following base URL:
https://{your-ory-slug}.projects.oryapis.com/admin/preview/search/v0beta1
and set the collection name to identities-{your-project-id}
.
See the following examples for how to configure and use the Typesense SDKs to search for identities.
- cURL
- Go
- Java
export ORY_API_KEY="ory_pat_XXXXXXXXXXXXXXXX"
export COLLECTION="identities-d7c52eed-e45c-4483-af3b-4aaa5782bff7" # replace with your project ID
export ORY_SLUG="upbeat-lalande-zu8omm6wwp" # replace with your Ory slug
# List identities via Search API
curl -H "Authorization: Bearer $ORY_API_KEY" \
"https://$ORY_SLUG.projects.oryapis.com/admin/preview/search/v0beta1/collections/$COLLECTION/documents/search?q=*"
# Search for "foo" in the email trait
curl -H "Authorization: Bearer $ORY_API_KEY" \
"https://$ORY_SLUG.projects.oryapis.com/admin/preview/search/v0beta1/collections/$COLLECTION/documents/search?q=foo&query_by=traits.email"
package search
import (
"context"
"fmt"
"os"
ory "github.com/ory/client-go"
"github.com/typesense/typesense-go/v3/typesense"
typesenseapi "github.com/typesense/typesense-go/v3/typesense/api"
)
var (
ORY_PROJECT_URL = os.Getenv("ORY_PROJECT_URL")
ORY_API_KEY = os.Getenv("ORY_API_KEY")
Collection = "identities-" + os.Getenv("ORY_PROJECT_ID")
)
func main() {
ctx := context.WithValue(context.Background(), ory.ContextAccessToken, os.Getenv("ORY_API_KEY"))
// Initialize Ory client
cfg := ory.NewConfiguration()
cfg.Servers = ory.ServerConfigurations{{URL: ORY_PROJECT_URL}}
oryClient := ory.NewAPIClient(cfg)
// Initialize search client
searchClient := typesense.NewClient(
typesense.WithAPIKey(ORY_API_KEY), // Use your Ory API key here
typesense.WithServer(ORY_PROJECT_URL+"/admin/preview/search/v0beta1/"), // configure the base URL to the Ory Search API endpoint
)
// List identities via Search API
list, err := searchClient.Collection(Collection).Documents().Search(ctx, &typesenseapi.SearchCollectionParams{
Q: ptr("*"),
})
if err != nil {
// handle error
}
for _, res := range *list.Hits {
doc := *res.Document
fmt.Printf("List result: %+v\n", doc)
}
// Search identities via Search API
search, err := searchClient.Collection(Collection).Documents().Search(ctx, &typesenseapi.SearchCollectionParams{
Q: ptr("foo"),
QueryBy: ptr("traits"),
})
if err != nil {
// handle error
}
if search.Hits == nil || len(*search.Hits) == 0 {
fmt.Println("No search hits")
}
fmt.Println()
for _, res := range *search.Hits {
doc := *res.Document
fmt.Printf("Search result: %+v\n", doc)
}
// retrieve identity details from Identity API
first := (*search.Hits)[0]
identity, _, err := oryClient.IdentityAPI.GetIdentity(ctx, (*first.Document)["id"].(string)).Execute()
if err != nil {
// handle error
}
fmt.Printf("Identity: %+v\n", identity)
}
func NewOryClient(ctx context.Context, url string) *ory.APIClient {
cfg := ory.NewConfiguration()
cfg.Servers = ory.ServerConfigurations{{URL: url}}
return ory.NewAPIClient(cfg)
}
func ptr[A any](v A) *A {
return &v
}
package sh.ory.examples.search;
import org.typesense.api.Client;
import org.typesense.api.Configuration;
import org.typesense.model.SearchParameters;
import org.typesense.model.SearchResult;
import org.typesense.resources.Node;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
public class App {
public static void main(String[] args) {
try {
Client searchClient = initializeSearchClient();
// Search identities via Search API
String collection = "identities-" + System.getenv("ORY_PROJECT_ID"); // Set the collection name
SearchResult searchResult = searchClient.collections(collection).documents()
.search(new SearchParameters().q("test@example.com").queryBy("traits.email"));
System.out.println("Search results: " + searchResult);
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
e.printStackTrace();
}
}
private static Client initializeSearchClient() {
try {
List<Node> nodes = new ArrayList<>();
Node node = new Node("", "", "");
// Set the base URL to the Ory Search API endpoint
node.baseUrl = System.getenv("ORY_BASE_URL") + "/admin/preview/search/v0beta1";
nodes.add(node);
Configuration config = new Configuration(
nodes,
Duration.ofSeconds(5),
System.getenv("ORY_API_KEY")); // Use your Ory API key here
return new Client(config);
} catch (Exception e) {
System.err.println("Error initializing search client: " + e.getMessage());
e.printStackTrace();
return null;
}
}
}
Data model
The collection identities-{your-project-id}
contains one document per identity. Each document contains the following fields:
{
"id": "d52d5bdb-74b4-4aa0-b706-d1e9c853bd81", // the identity ID
"organization_id": "org-id-123", // optional, facet
"external_id": "external-id-123", // optional
"created_at": 1725031437, // UNIX timestamp, facet
"updated_at": 1758115258, // UNIX timestamp, facet
"state": "active", // "inactive", "deleted", facet
"schema_id": "preset://email", // identity schema ID, facet
"available_aal": "aal1", // "aal2" etc, facet
"metadata_admin": {
"role": "user" // custom admin metadata, indexed, search via `query_by=metadata_admin.role`
},
"metadata_public": {
"foo": "bar" // custom public metadata, indexed, search via `query_by=metadata_public.foo`
},
"traits": {
"email": "wgiho@agpaa.com" // traits based on identity schema, indexed, search via `query_by=traits.email`
}
}
To avoid timing out search requests, specify the exact fields you want to search in using the query_by
parameter. For example,
to search for identities by email, use query_by=traits.email
.
Because of technical limitations, non-string data in metadata_admin
and metadata_public
will be indexed as strings and
returned from the Search API as strings.
To retrieve the original JSON data, fetch the full identity using the Get Identity API.
Advanced search example
Please refer to the Typesense documentation for details on how to construct arbitrary search queries. An example:
- cURL
- sample result
export ORY_API_KEY="ory_pat_XXXXXXXXXXXXXXXX"
export COLLECTION="identities-d7c52eed-e45c-4483-af3b-4aaa5782bff7" # replace with your project ID
export ORY_SLUG="upbeat-lalande-zu8omm6wwp" # replace with your Ory slug
# list identities which do not have two-factor authentication enabled,
# resolved by the organization to which they belong,
# ordered by creation date (newest first),
# and limited to 20 per page
curl -H "Authorization: Bearer $ORY_API_KEY" \
"https://$ORY_SLUG.projects.oryapis.com/admin/preview/search/v0beta1/collections/$COLLECTION/documents/search" \
--url-query 'q=*' \
--url-query 'filter_by=available_aal:!=aal2' \
--url-query 'facet_by=organization_id' \
--url-query 'sort_by=created_at:desc' \
--url-query 'per_page=20'
{
"facet_counts": [
{
"counts": [],
"field_name": "organization_id",
"sampled": false,
"stats": {
"total_values": 0
}
}
],
"found": 5,
"hits": [
{
"document": {
"available_aal": "aal1",
"created_at": 1725296067,
"id": "9e9763c9-80fc-43d0-97fb-d241cb274c2c",
"schema_id": "preset://email",
"state": "active",
"traits": {
"email": "wrhgr@srgpzjdrgpz"
},
"updated_at": 1725296067
},
"highlight": {},
"highlights": []
},
{
"document": {
"available_aal": "aal1",
"created_at": 1725295678,
"id": "38403863-16c2-4a0c-b53e-428f7d80a65b",
"schema_id": "preset://email",
"state": "active",
"traits": {
"email": "SRGSRpgj@SPRGojsgs.com"
},
"updated_at": 1725295678
},
"highlight": {},
"highlights": []
},
{
"document": {
"available_aal": "aal1",
"created_at": 1725031786,
"id": "f5e85c2f-6f0f-49d7-9b77-1bfbb530add4",
"schema_id": "preset://email",
"state": "active",
"traits": {
"email": "wGWRIGOH@WOsPgjzsroig"
},
"updated_at": 1725031786
},
"highlight": {},
"highlights": []
},
{
"document": {
"available_aal": "aal1",
"created_at": 1725031437,
"external_id": "external-id-123",
"id": "d52d5bdb-74b4-4aa0-b706-d1e9c853bd81",
"metadata_admin": {
"role": "user"
},
"metadata_public": {
"foo": "bar"
},
"schema_id": "preset://email",
"state": "active",
"traits": {
"email": "wgiho@agpaa.com"
},
"updated_at": 1758115258
},
"highlight": {},
"highlights": []
},
{
"document": {
"available_aal": "aal1",
"created_at": 1725031040,
"id": "f56128f1-687e-414e-8f5d-ec5f909b33c0",
"schema_id": "preset://email",
"state": "active",
"traits": {
"email": "sogihSOG@SdoSORghzdrg"
},
"updated_at": 1725031040
},
"highlight": {},
"highlights": []
}
],
"page": 1,
"request_params": {
"collection_name": "identities-f59802a6-eb63-44cc-a3ac-d90db3d3abc2",
"first_q": "*",
"per_page": 20,
"q": "*"
},
"search_cutoff": false
}
Most fields support infix-searching as well, so queries like query_by=@example.com&infix=always&query_by=traits.email
are
possible.
Consistency and availability
The search index powering advanced identity search is eventually consistent with the main identity store. This means that there will be a delay between creating, updating, or deleting an identity and the changes being reflected in search results. We try to aim for a delay of less than one second during normal operation, but cannot provide any guarantees. The search index will have to be rebuilt periodically, during which time we cannot service search requests.
We recommend you use the search API for non-time-critical use cases, such as admin UIs or background jobs, where you need to full capability of full-text search, filtering, faceting, and sorting.
Before displaying search results to end-users or otherwise using the search results for business logic, we recommend you always fetch the full identity from the main identity store using the identity ID returned in search results to ensure you have the most up-to-date information.