Directory Services 7.4.2

What to index

DS directory server search performance depends on indexes. The default settings are fine for evaluating DS software, and they work well with sample data. The default settings do not necessarily fit your directory data, and the searches your applications perform:

  • Configure necessary indexes for the searches you anticipate.

  • Let DS optimize search queries to use whatever indexes are available.

    DS servers may use a presence index when an equality index is not available, for example.

  • Use metrics and index debugging to check that searches use indexes and are optimized.

    You cannot configure the optimizations DS servers perform. You can, however, review search metrics, logs, and search debugging data to verify that searches use indexes and are optimized.

  • Monitor DS servers for indexes that are not used.

Necessary indexes

Index maintenance has its costs. Every time an indexed attribute is updated, the server must update each affected index to reflect the change. This is wasteful if the index is not used. Indexes, especially substring indexes, can occupy more memory and disk space than the corresponding data.

Aim to maintain only indexes that speed up appropriate searches, and that allow the server to operate properly. The former indexes depend on how directory users search, and require thought and investigation. The latter includes non-configurable internal indexes, that should not change.

Begin by reviewing the attributes of your directory data. Which attributes would you expect to find in a search filter? If an attribute is going to show up frequently in reasonable search filters, then index it.

Standard indexes by search filter
LDAP filter HDAP filter Recommended indexes(1) Explanation

(&)
(objectClass=*)

_queryFilter=true

None

This filter matches every entry in scope.

Indexes cannot help. Narrow the scope of the search as much as possible.

(uid=*)

_queryFilter=uid+pr

uid presence index

A presence index matches an attribute present on the entry, regardless of the value.

(uid=bjensen)

_queryFilter=uid+eq+'bjensen'

uid equality index

An equality index matches values that correspond exactly to those in search filters.

(uid=*jensen*)

_queryFilter=uid+co+'jensen'

uid substring index

A substring index matches values specified with wildcards in the filter.

(uid=jensen*)

_queryFilter=uid+sw+'jensen'

uid equality index or uid ordering index

The filter value jensen* is also called an initial substring.

When filters use only initial substrings, configure an equality or ordering index for the attribute.(1)

(&(uid=*jensen*)(cn=babs*))

_queryFilter=(uid+co+'jensen'and+cn+sw'babs')

uid substring index, cn substring index

Both filters match substrings.

(!(uid=*jensen*))

_queryFilter=!(uid+co+'jensen')

None

This NOT filter matches all entries except a few.

Extend this to an AND filter if possible where the other component filters make use of indexes.

(uid<=jensen)

_queryFilter=uid+le+'jensen'

uid ordering index

An ordering index is used to match values for a filter that specifies a range.

(uid>=jensen)

_queryFilter=uid+ge+'jensen'

uid ordering index

The greater than or equal comparison also uses ordering.

(sn~=jansen)

Not applicable

sn approximate index

An approximate index matches values that "sound like" those provided in the filter.

(st=CA)

_queryFilter=st+eq+'CA'

st big index

Big indexes fit the case where a large number of entries have the same attribute value.

In this example, every user in California matches (st=CA).

Client applications must page through the results to avoid exceeding resource limits. For details, refer to Indexes for attributes with few unique values.

(1) DS servers optimize the search internally to:

  • Rearrange the search filter terms as long as the logical result is the same.

  • Ignore indexes for some filters when other filters and their indexes sufficiently narrow the list of candidates. For example, if the search filter is (&(sn=Jensen)(st=CA)), DS might not use the st equality index.

  • Use alternative indexes depending on the matching rule from the filter operator, falling back to an alternative index when the best-choice index does not exist. This works for standard matching rules as shown in the following table, not for extensible matching rules.

Index use by matching rule
Matching rule Example filter DS index use

equality

(uid=bjensen)

Try an equality or ordering index; fall back to a presence index.

ordering

(uid>=bjensen)

Try an equality or ordering index; fall back to a presence index.

approximate

(sn~=jansen)

Try an approximate index; fall back to a presence index.

presence

(uid=*)

Try a presence index; fall back to an equality or ordering index.

substring

(uid=*babs*)

Try an equality or ordering index for initial substrings; otherwise, use a substring index and fall back to a presence index.

Compare your guesses with what you observe actually happening in the directory. For more complex filters, refer to Debug search indexes.

Unindexed searches

Directory users might complain their searches fail because they’re unindexed.

Ask for the result code, additional information, and search filter. By default, DS directory servers reject unindexed searches with a result code of 50 and additional information about the unindexed search. The following example attempts, anonymously, to get the entries for all users whose email address ends in .com:

$ ldapsearch \
 --hostname localhost \
 --port 1636 \
 --useSsl \
 --usePkcs12TrustStore /path/to/opendj/config/keystore \
 --trustStorePassword:file /path/to/opendj/config/keystore.pin \
 --bindDN uid=user.0,ou=People,dc=example,dc=com \
 --bindPassword password \
 --baseDN ou=people,dc=example,dc=com \
 "(&(mail=*.com)(objectclass=person))"
# The LDAP search request failed: 50 (Insufficient Access Rights)
# Additional Information:  You do not have sufficient privileges to perform an unindexed search

If they’re unintentionally requesting an unindexed search, suggest ways to perform an indexed search instead. Perhaps the application needs a better search filter. Perhaps it requests more results than necessary. For example, a GUI application lets a user browse directory entries. The application could page through the results, rather than attempting to retrieve all the entries at once. To let the user page back and forth through the results, you could add a browsing (VLV) index for the application to get the entries for the current screen.

An application might have a good reason to get the full list of all entries in one operation. If so, assign the application’s account the unindexed-search privilege. Consider other options before you grant the privilege, however. Unindexed searches cause performance problems for concurrent directory operations.

When an application has the privilege or binds with directory superuser credentials—​by default, the uid=admin DN and password—​then DS does not reject its request for an unindexed search. Check for unindexed searches using the ds-mon-backend-filter-unindexed monitoring attribute:

$ ldapsearch \
 --hostname localhost \
 --port 1636 \
 --useSsl \
 --usePkcs12TrustStore /path/to/opendj/config/keystore \
 --trustStorePassword:file /path/to/opendj/config/keystore.pin \
 --bindDN uid=monitor \
 --bindPassword password \
 --baseDN cn=monitor \
 "(objectClass=ds-monitor-backend-db)" ds-mon-backend-filter-unindexed

If ds-mon-backend-filter-unindexed is greater than zero, review the access log for unexpected unindexed searches. The following example shows the relevant fields in an access log message:

{
  "request": {
    "protocol": "LDAP",
    "operation": "SEARCH"
  },
  "response": {
    "detail": "You do not have sufficient privileges to perform an unindexed search",
    "additionalItems": {
      "unindexed": true
    }
  }
}

Beyond the key fields shown in the example, messages in the access log also specify the search filter and scope. Understand the operation that led to each unindexed search. If the filter is appropriate and often used, add an index to ease the search. Either analyze the access logs to find how often operations use the search filter or monitor operations with the index analysis feature, described in Index analysis metrics.

In addition to responding to client search requests, a server performs internal searches. Internal searches let the server retrieve data needed for a request, and maintain internal state information. Sometimes, internal searches become unindexed. When this happens, the server logs a warning similar to the following:

The server is performing an unindexed internal search request
with base DN '%s', scope '%s', and filter '%s'. Unindexed internal
searches are usually unexpected and could impact performance.
Please verify that that backend's indexes are configured correctly
for these search parameters.

When you observe a message like this in the server log, take these actions:

  • Figure out which indexes are missing, and add them.

  • Check the integrity of the indexes.

    For details, refer to Verify indexes.

  • If the relevant indexes exist, and you have verified that they are sound, the index entry limit might be too low.

    This can happen, for example, in directory servers with more than 4000 groups in a single backend. For details, refer to Index entry limits.

  • If you have made the changes described in the steps above, and problem persists, contact technical support.

Index analysis metrics

DS servers provide the index analysis feature to collect information about filters in search requests. This feature is useful, but not recommended to keep enabled on production servers, as DS maintains the metrics in memory.

You can activate the index analysis mechanism using the dsconfig set-backend-prop command:

$ dsconfig \
 set-backend-prop \
 --hostname localhost \
 --port 4444 \
 --bindDN uid=admin \
 --bindPassword password \
 --backend-name dsEvaluation \
 --set index-filter-analyzer-enabled:true \
 --no-prompt \
 --usePkcs12TrustStore /path/to/opendj/config/keystore \
 --trustStorePassword:file /path/to/opendj/config/keystore.pin

The command causes the server to analyze filters used, and to keep the results in memory. You can read the results as monitoring information:

$ ldapsearch \
 --hostname localhost \
 --port 1636 \
 --useSsl \
 --usePkcs12TrustStore /path/to/opendj/config/keystore \
 --trustStorePassword:file /path/to/opendj/config/keystore.pin \
 --bindDN uid=admin \
 --bindPassword password \
 --baseDN ds-cfg-backend-id=dsEvaluation,cn=Backends,cn=monitor \
 --searchScope base \
 "(&)" \
 ds-mon-backend-filter-indexed ds-mon-backend-filter-unindexed ds-mon-backend-filter-use-start-time ds-mon-backend-filter-use

dn: ds-cfg-backend-id=dsEvaluation,cn=backends,cn=monitor
ds-mon-backend-filter-indexed: 2
ds-mon-backend-filter-unindexed: 3
ds-mon-backend-filter-use-start-time: <timestamp>
ds-mon-backend-filter-use: {"search-filter":"(employeenumber=86182)","nb-hits":1,"latest-failure-reason":"caseIgnoreMatch index type is disabled for the employeeNumber attribute"}

The ds-mon-backend-filter-indexed and ds-mon-backend-filter-unindexed metrics are available even when index filter analysis is disabled.

The ds-mon-backend-filter-use values include the following fields:

search-filter

The LDAP search filter.

nb-hits

The number of times the filter was used.

latest-failure-reason

A message describing why the server could not use any index for this filter.

The output can include filters for internal use, such as (aci=*). In the example above, you observe a filter used by a client application.

In the example, a search filter that led to an unindexed search, (employeenumber=86182), had no matches because, "caseIgnoreMatch index type is disabled for the employeeNumber attribute". Some client application has tried to find users by employee number, but no index exists for that purpose. If this appears regularly as a frequent search, add an employee number index.

To avoid impacting server performance, turn off index analysis after you collect the information you need. Use the dsconfig set-backend-prop command:

$ dsconfig \
 set-backend-prop \
 --hostname localhost \
 --port 4444 \
 --bindDN uid=admin \
 --bindPassword password \
 --backend-name dsEvaluation \
 --set index-filter-analyzer-enabled:false \
 --no-prompt \
 --usePkcs12TrustStore /path/to/opendj/config/keystore \
 --trustStorePassword:file /path/to/opendj/config/keystore.pin

Sometimes it is not obvious by inspection how a directory server processes a given search request. The directory superuser can gain insight with the debugsearchindex attribute.

The default global access control only allows the directory superuser to read the debugsearchindex attribute. To allow another user to read the attribute, add a global ACI such as the following:

$ dsconfig \
 set-access-control-handler-prop \
 --hostname localhost \
 --port 4444 \
 --bindDN uid=admin \
 --bindPassword password \
 --add global-aci:"(targetattr=\"debugsearchindex\")(version 3.0; acl \"Debug search indexes\"; \
  allow (read,search,compare) userdn=\"ldap:///uid=user.0,ou=people,dc=example,dc=com\";)" \
 --usePkcs12TrustStore /path/to/opendj/config/keystore \
 --trustStorePassword:file /path/to/opendj/config/keystore.pin \
 --no-prompt

The format of debugsearchindex values has interface stability: Internal.

The values are intended to be read by human beings, not scripts. If you do write scripts that interpret debugsearchindex values, be aware that they are not stable. Be prepared to adapt your scripts for every upgrade or patch.

The debugsearchindex attribute value indicates how the server would process the search. The server uses its indexes to prepare a set of candidate entries. It iterates through the set to compare candidates with the search filter, returning entries that match. The following example demonstrates this feature for a subtree search with a complex filter:

Show details
$ ldapsearch \
 --hostname localhost \
 --port 1636 \
 --useSsl \
 --usePkcs12TrustStore /path/to/opendj/config/keystore \
 --trustStorePassword:file /path/to/opendj/config/keystore.pin \
 --bindDN uid=user.0,ou=people,dc=example,dc=com \
 --bindPassword password \
 --baseDN dc=example,dc=com \
 "(&(objectclass=person)(givenName=aa*))" \
 debugsearchindex | sed -n -e "s/^debugsearchindex: //p"

{
  "baseDn": "dc=example,dc=com",
  "scope": "sub",
  "filter": "(&(givenName=aa*)(objectclass=person))",
  "maxCandidateSize": 100000,
  "strategies": [{
    "name": "BaseObjectSearchStrategy",
    "diagnostic": "not applicable"
  }, {
    "name": "VlvSearchStrategy",
    "diagnostic": "not applicable"
  }, {
    "name": "AttributeIndexSearchStrategy",
    "filter": {
      "query": "INTERSECTION",
      "rank": "RANGE_MATCH",
      "filter": "(&(givenName=aa*)(objectclass=person))",
      "subQueries": [{
        "query": "FIRST_OF",
        "rank": "RANGE_MATCH",
        "filter": "(givenName=aa*)",
        "subQueries": [{
          "query": "FIRST_OF",
          "rank": "RANGE_MATCH",
          "filter": "(givenName=aa*)",
          "subQueries": [{
            "query": "RANGE_MATCH",
            "rank": "RANGE_MATCH",
            "index": "givenName.caseIgnoreMatch",
            "range": "[aa,ab[",
            "diagnostic": "indexed",
            "candidates": 50
          }, {
            "query": "RANGE_MATCH",
            "rank": "RANGE_MATCH",
            "index": "givenName.caseIgnoreSubstringsMatch:6",
            "range": "[aa,ab[",
            "diagnostic": "skipped"
          }],
          "diagnostic": "indexed",
          "candidates": 50
        }, {
          "query": "MATCH_ALL",
          "rank": "MATCH_ALL",
          "filter": "(givenName=aa*)",
          "index": "givenName.presence",
          "diagnostic": "skipped"
        }],
        "diagnostic": "indexed",
        "candidates": 50,
        "retained": 50
      }, {
        "query": "FIRST_OF",
        "rank": "OBJECT_CLASS_EQUALITY_MATCH",
        "filter": "(objectclass=person)",
        "subQueries": [{
          "query": "OBJECT_CLASS_EQUALITY_MATCH",
          "rank": "OBJECT_CLASS_EQUALITY_MATCH",
          "filter": "(objectclass=person)",
          "subQueries": [{
            "query": "EXACT_MATCH",
            "rank": "EXACT_MATCH",
            "index": "objectClass.objectIdentifierMatch",
            "key": "person",
            "diagnostic": "not indexed",
            "candidates": "[LIMIT-EXCEEDED]"
          }, {
            "query": "EXACT_MATCH",
            "rank": "EXACT_MATCH",
            "index": "objectClass.objectIdentifierMatch",
            "key": "2.5.6.6",
            "diagnostic": "skipped"
          }],
          "diagnostic": "not indexed",
          "candidates": "[LIMIT-EXCEEDED]"
        }, {
          "query": "MATCH_ALL",
          "rank": "MATCH_ALL",
          "filter": "(objectclass=person)",
          "index": "objectClass.presence",
          "diagnostic": "skipped"
        }],
        "diagnostic": "not indexed",
        "candidates": "[LIMIT-EXCEEDED]",
        "retained": 50
      }],
      "diagnostic": "indexed",
      "candidates": 50
    },
    "scope": {
      "type": "sub",
      "diagnostic": "not indexed",
      "candidates": "[NOT-INDEXED]",
      "retained": 50
    },
    "diagnostic": "indexed",
    "candidates": 50
  }],
  "final": 50
}

The filter in the example matches person entries whose given name starts with aa. The search scope is not explicitly specified, so the scope defaults to the subtree including the base DN.

Notice that the debugsearchindex value has the following top-level fields:

  • (Optional) "vlv" describes how the server uses VLV indexes.

    The VLV field is not applicable for this example, and so is not present.

  • "filter" describes how the server uses the search filter to narrow the set of candidates.

  • "scope" describes how the server uses the search scope.

  • "final" indicates the final number of candidates in the set.

In the output, notice that the server uses the equality and substring indexes to find candidate entries whose given name starts with aa. If the filter indicated given names containing aa, as in givenName=*aa*, the server would rely only on the substring index.

Notice that the output for the (objectclass=person) portion of the filter shows "candidates": "[LIMIT-EXCEEDED]". In this case, there are so many entries matching the value specified that the index is not useful for narrowing the set of candidates. The scope is also not useful for narrowing the set of candidates. Ultimately, however, the givenName indexes help the server to narrow the set of candidates. The overall search is indexed and the result is 50 matching entries.

If an index already exists, but you suspect it is not working properly, refer to Verify indexes.

Unused indexes

DS maintains metrics about index use. The metrics indicate how often an index was accessed since the DS server started.

The following examples demonstrate how to read the metrics for all monitored indexes:

  • LDAP

  • Prometheus

$ ldapsearch \
 --hostname localhost \
 --port 1636 \
 --useSsl \
 --usePkcs12TrustStore /path/to/opendj/config/keystore \
 --trustStorePassword:file /path/to/opendj/config/keystore.pin \
 --bindDN uid=monitor \
 --bindPassword password \
 --baseDN cn=monitor \
 "(objectClass=ds-monitor-backend-index)" ds-mon-index ds-mon-index-uses
$ curl --cacert ca-cert.pem --user monitor:password https://localhost:8443/metrics/prometheus 2>/dev/null | grep index_uses

If the number of attribute index uses is persistently zero, then you can eventually conclude the index is unused. Of course, it is possible that an index is needed, but has not been used since the last server restart. Be sure to sample often enough that you know the index is unused before taking action.

You can remove unused attribute indexes with one of the following commands, depending on the type of index:

Never remove a system index.

Index cost

DS maintains metrics about index cost, which lets you determine which indexes are causing write contention. The metrics count the number of updates and how long they took since the DS server started.

The following examples demonstrate how to read the metrics for all monitored indexes:

  • LDAP

  • Prometheus

$ ldapsearch \
 --hostname localhost \
 --port 1636 \
 --useSsl \
 --usePkcs12TrustStore /path/to/opendj/config/keystore \
 --trustStorePassword:file /path/to/opendj/config/keystore.pin \
 --bindDN uid=monitor \
 --bindPassword password \
 --baseDN cn=monitor \
 "(objectClass=ds-monitor-backend-index)" ds-mon-index ds-mon-index-cost
$ curl --cacert ca-cert.pem --user monitor:password https://localhost:8443/metrics/prometheus 2>/dev/null | grep index_cost

If the cost is persistently very high, and an attribute index is not used, or hardly used, then consider removing it. Never remove a system index.