Search and Filtering#

This guide is for developers implementing search functionality with OpenSPP API V2.

Search Basics#

Search for resources using GET with query parameters:

GET /api/v2/spp/{ResourceType}?parameter=value
Authorization: Bearer TOKEN

All searches return a Bundle with type: searchset:

{
  "resourceType": "Bundle",
  "type": "searchset",
  "total": 1523,
  "link": [
    {
      "relation": "self",
      "url": "/api/v2/spp/Individual?name=Santos&_count=50"
    },
    {
      "relation": "next",
      "url": "/api/v2/spp/Individual?name=Santos&_count=50&_offset=50"
    }
  ],
  "entry": [
    {
      "resource": { /* Individual */ },
      "search": {
        "mode": "match",
        "score": 0.95
      }
    }
  ]
}

Individual Search Parameters#

By Identifier#

GET /api/v2/spp/Individual?identifier=urn:gov:ph:psa:national-id|PH-123456789

Format: system|value or just value (searches all systems)

Example: Python

def search_by_identifier(system, value, token, base_url):
    """Search for individuals by identifier."""
    params = {"identifier": f"{system}|{value}"}
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(
        f"{base_url}/Individual",
        headers=headers,
        params=params
    )
    response.raise_for_status()
    return response.json()

# Usage
results = search_by_identifier(
    system="urn:gov:ph:psa:national-id",
    value="PH-123456789",
    token=token,
    base_url="https://api.openspp.org/api/v2/spp"
)

By Name#

# Contains search (case-insensitive)
GET /api/v2/spp/Individual?name=Santos

Name search uses case-insensitive substring matching.

Example: Python

def search_by_name(name, token, base_url):
    """Search for individuals by name."""
    params = {"name": name}
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(
        f"{base_url}/Individual",
        headers=headers,
        params=params
    )
    response.raise_for_status()
    return response.json()

# Search by family name
results = search_by_name("Santos", token=token, base_url=base_url)

By Birth Date#

# Exact date
GET /api/v2/spp/Individual?birthdate=1985-03-15

# Date range
GET /api/v2/spp/Individual?birthdate=ge1980-01-01&birthdate=le1990-12-31

Date Prefixes:

Prefix

Meaning

Example

eq

Equal to (default)

eq1985-03-15 or 1985-03-15

ne

Not equal to

ne1985-03-15

lt

Less than

lt1985-01-01

le

Less than or equal

le1984-12-31

gt

Greater than

gt1990-01-01

ge

Greater than or equal

ge1990-01-01

Example: Python

def search_by_birth_date_range(start_date, end_date, token, base_url):
    """Search for individuals by birth date range."""
    params = {
        "birthdate": [
            f"ge{start_date}",
            f"le{end_date}"
        ]
    }
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(
        f"{base_url}/Individual",
        headers=headers,
        params=params
    )
    response.raise_for_status()
    return response.json()

# Find individuals born in the 1980s
results = search_by_birth_date_range(
    start_date="1980-01-01",
    end_date="1989-12-31",
    token=token,
    base_url=base_url
)

By Gender#

GET /api/v2/spp/Individual?gender=urn:iso:std:iso:5218|2

ISO 5218 Gender Codes:

Code

Meaning

0

Not known

1

Male

2

Female

9

Not applicable

Example: Python

def search_by_gender(gender_code, token, base_url):
    """Search for individuals by gender."""
    params = {"gender": f"urn:iso:std:iso:5218|{gender_code}"}
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(
        f"{base_url}/Individual",
        headers=headers,
        params=params
    )
    response.raise_for_status()
    return response.json()

# Find all females
results = search_by_gender("2", token=token, base_url=base_url)

By Address#

GET /api/v2/spp/Individual?address=Manila

Searches across all address fields (city, state, text, etc.)

Example: Python

def search_by_address(location, token, base_url):
    """Search for individuals by address."""
    params = {"address": location}
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(
        f"{base_url}/Individual",
        headers=headers,
        params=params
    )
    response.raise_for_status()
    return response.json()

# Find all individuals in Manila
results = search_by_address("Manila", token=token, base_url=base_url)

By Last Updated#

# Modified since date
GET /api/v2/spp/Individual?_lastUpdated=ge2024-01-01

# Modified in date range
GET /api/v2/spp/Individual?_lastUpdated=ge2024-01-01&_lastUpdated=lt2024-02-01

Example: Python

from datetime import datetime, timedelta

def search_recently_updated(days, token, base_url):
    """Search for individuals updated in the last N days."""
    since_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
    params = {"_lastUpdated": f"ge{since_date}"}
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(
        f"{base_url}/Individual",
        headers=headers,
        params=params
    )
    response.raise_for_status()
    return response.json()

# Find individuals updated in last 7 days
results = search_recently_updated(7, token=token, base_url=base_url)

Group Search Parameters#

By Member#

GET /api/v2/spp/Group?member=Individual/urn:gov:ph:psa:national-id|PH-123456789

Find all groups containing a specific individual.

Example: Python

def find_groups_for_individual(individual_ref, token, base_url):
    """Find all groups an individual belongs to."""
    params = {"member": individual_ref}
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(
        f"{base_url}/Group",
        headers=headers,
        params=params
    )
    response.raise_for_status()
    return response.json()

# Usage
groups = find_groups_for_individual(
    individual_ref="Individual/urn:gov:ph:psa:national-id|PH-123456789",
    token=token,
    base_url=base_url
)

for entry in groups["entry"]:
    group = entry["resource"]
    print(f"Member of: {group['name']}")

Sorting#

Sort results with _sort:

# Sort by name (ascending)
GET /api/v2/spp/Individual?_sort=name

# Sort by birth date (descending)
GET /api/v2/spp/Individual?_sort=-birthdate

# Multi-field sort
GET /api/v2/spp/Individual?_sort=name,-birthdate

Prefix:

  • No prefix or +: Ascending

  • -: Descending

Example: Python

def search_sorted(params, sort_fields, token, base_url):
    """Search with sorting."""
    params["_sort"] = ",".join(sort_fields)
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(
        f"{base_url}/Individual",
        headers=headers,
        params=params
    )
    response.raise_for_status()
    return response.json()

# Sort by name, then birth date descending
results = search_sorted(
    params={"name": "Santos"},
    sort_fields=["name", "-birthdate"],
    token=token,
    base_url=base_url
)

Field Selection#

Request only specific fields with _elements:

GET /api/v2/spp/Individual?_elements=identifier,name,birthDate

Reduces response size and improves performance.

Example: Python

def search_with_fields(params, fields, token, base_url):
    """Search and return only specific fields."""
    params["_elements"] = ",".join(fields)
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(
        f"{base_url}/Individual",
        headers=headers,
        params=params
    )
    response.raise_for_status()
    return response.json()

# Get only identifiers and names
results = search_with_fields(
    params={"name": "Santos"},
    fields=["identifier", "name"],
    token=token,
    base_url=base_url
)

Combining Parameters#

Combine multiple search parameters (AND logic):

GET /api/v2/spp/Individual?name=Santos&birthdate=ge1980-01-01&address=Manila

Example: Python

def advanced_search(token, base_url, **criteria):
    """Perform advanced search with multiple criteria."""
    headers = {"Authorization": f"Bearer {token}"}

    response = requests.get(
        f"{base_url}/Individual",
        headers=headers,
        params=criteria
    )
    response.raise_for_status()
    return response.json()

# Complex search
results = advanced_search(
    token=token,
    base_url=base_url,
    name="Santos",
    birthdate=["ge1980-01-01", "le1990-12-31"],
    address="Manila",
    gender="urn:iso:std:iso:5218|2",
    _count=100,
    _sort="-birthdate"
)

print(f"Found {results['total']} matching individuals")

Search Score#

Results include a relevance score (0.0 to 1.0):

{
  "entry": [
    {
      "resource": { /* Individual */ },
      "search": {
        "mode": "match",
        "score": 0.95
      }
    }
  ]
}

Higher scores indicate better matches (used for name/text searches).

Error Handling#

No Results#

Empty results return total: 0 with empty entry array:

{
  "resourceType": "Bundle",
  "type": "searchset",
  "total": 0,
  "entry": []
}

Invalid Parameters#

HTTP/1.1 400 Bad Request

{
  "resourceType": "OperationOutcome",
  "issue": [
    {
      "severity": "error",
      "code": "invalid",
      "details": {
        "text": "Invalid search parameter: 'foo' is not a supported parameter"
      }
    }
  ]
}

Too Many Results#

If results exceed reasonable limits (e.g., >10,000), the API may return an error:

{
  "resourceType": "OperationOutcome",
  "issue": [
    {
      "severity": "warning",
      "code": "too-many",
      "details": {
        "text": "Search returned >10,000 results. Please refine your search criteria."
      }
    }
  ]
}

Performance Tips#

1. Use Identifiers When Possible#

# ✅ Fast - Direct lookup by identifier
individual = get_by_identifier("urn:gov:ph:psa:national-id|PH-123456789")

# ❌ Slow - Search by name
results = search_by_name("Maria Santos")

2. Limit Fields with _elements#

# ✅ Fast - Only request needed fields
params = {"name": "Santos", "_elements": "identifier,name"}

# ❌ Slow - Full resource returned
params = {"name": "Santos"}

3. Use Pagination Appropriately#

# ✅ Good - Reasonable page size
params = {"_count": 50}

# ❌ Bad - Too large
params = {"_count": 1000}

4. Use Date Filters to Reduce Results#

# ✅ Good - Filter to recent changes
params = {"_lastUpdated": "ge2024-11-01"}

# ❌ Bad - No filter (returns everything)
params = {}

5. Cache Results When Appropriate#

import time

class CachedSearch:
    """Simple search result cache."""

    def __init__(self, ttl=300):
        self.cache = {}
        self.ttl = ttl

    def search(self, url, token, base_url):
        """Search with caching."""
        cache_key = f"{url}|{token}"

        # Check cache
        if cache_key in self.cache:
            cached_data, cached_time = self.cache[cache_key]
            if time.time() - cached_time < self.ttl:
                return cached_data

        # Fetch from API
        headers = {"Authorization": f"Bearer {token}"}
        response = requests.get(
            f"{base_url}/{url.lstrip('/')}",
            headers=headers
        )
        response.raise_for_status()
        data = response.json()

        # Store in cache
        self.cache[cache_key] = (data, time.time())
        return data

# Usage
cache = CachedSearch(ttl=300)  # 5-minute cache
results = cache.search(
    url="/Individual?name=Santos",
    token=token,
    base_url=base_url
)

Complete Search Example#

import requests
from typing import Optional, List, Dict

class OpenSPPSearch:
    """Comprehensive search client."""

    def __init__(self, base_url, token):
        self.base_url = base_url
        self.token = token
        self.headers = {"Authorization": f"Bearer {token}"}

    def search_individuals(
        self,
        identifier: Optional[str] = None,
        name: Optional[str] = None,
        birth_date_from: Optional[str] = None,
        birth_date_to: Optional[str] = None,
        gender: Optional[str] = None,
        address: Optional[str] = None,
        updated_since: Optional[str] = None,
        count: int = 20,
        offset: int = 0,
        sort: Optional[List[str]] = None,
        fields: Optional[List[str]] = None
    ) -> Dict:
        """
        Search for individuals with flexible criteria.

        Args:
            identifier: System|value identifier
            name: Name to search (case-insensitive substring match)
            birth_date_from: Birth date >= (YYYY-MM-DD)
            birth_date_to: Birth date <= (YYYY-MM-DD)
            gender: Gender code (ISO 5218)
            address: Address search
            updated_since: Last updated >= (YYYY-MM-DD)
            count: Results per page (default 20, max 100)
            offset: Results to skip
            sort: Sort fields (prefix with - for descending)
            fields: Fields to return

        Returns:
            Search results bundle
        """
        params = {}

        if identifier:
            params["identifier"] = identifier
        if name:
            params["name"] = name
        if birth_date_from:
            params.setdefault("birthdate", []).append(f"ge{birth_date_from}")
        if birth_date_to:
            params.setdefault("birthdate", []).append(f"le{birth_date_to}")
        if gender:
            params["gender"] = f"urn:iso:std:iso:5218|{gender}"
        if address:
            params["address"] = address
        if updated_since:
            params["_lastUpdated"] = f"ge{updated_since}"

        params["_count"] = count
        params["_offset"] = offset

        if sort:
            params["_sort"] = ",".join(sort)
        if fields:
            params["_elements"] = ",".join(fields)

        response = requests.get(
            f"{self.base_url}/Individual",
            headers=self.headers,
            params=params
        )
        response.raise_for_status()
        return response.json()

    def get_all_results(self, initial_search_fn, **search_params):
        """Paginate through all search results."""
        all_results = []
        offset = 0
        count = 50

        while True:
            bundle = initial_search_fn(
                **search_params,
                count=count,
                offset=offset
            )

            entries = bundle.get("entry", [])
            all_results.extend(entries)

            # Check if there are more results
            total = bundle.get("total", 0)
            if offset + len(entries) >= total:
                break

            offset += count

        return all_results

# Usage
searcher = OpenSPPSearch(
    base_url="https://api.openspp.org/api/v2/spp",
    token=token
)

# Simple search
results = searcher.search_individuals(
    name="Santos",
    address="Manila",
    count=50
)
print(f"Found {results['total']} individuals")

# Advanced search
results = searcher.search_individuals(
    name="Maria",
    birth_date_from="1980-01-01",
    birth_date_to="1990-12-31",
    gender="2",  # Female
    address="Metro Manila",
    sort=["name", "-birthdate"],
    fields=["identifier", "name", "birthDate", "address"]
)

# Get all results (with pagination)
all_results = searcher.get_all_results(
    searcher.search_individuals,
    name="Santos"
)
print(f"Retrieved {len(all_results)} total results")

Are You Stuck?#

Search returns too many results?

Add more criteria to narrow the search. Use date ranges or address filters.

Getting empty results when you expect matches?

Check if consent is filtering results. Review the X-Consent-Status header.

Name search not finding expected results?

Name search is case-insensitive and uses substring matching. Try searching with just the family name or given name.

Pagination links not working?

Use the full URL from the link array. Don't manually construct pagination URLs.

Search is slow?

Use _elements to request only needed fields. Filter by _lastUpdated if you're syncing data.

Next Steps#

See Also#