Documentation Index
Fetch the complete documentation index at: https://launchdarkly-preview.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Background
Originally, sessions and errors search on LaunchDarkly queried Postgres since that is our primary database for storing all LaunchDarkly metadata. We allowed users to filter on each session or error object field. Unfortunately, we started running into performance issues with more complicated queries once the number of sessions and errors increased. Covering all combinations of the queried fields with an appropriate index was challenging, so when we decided to improve our search experience, we knew we wanted to find a datastore that:- would scale well without having to manually manage and tune a lot of indexes, and
- would be flexible enough that adding more fields in the future would not require us to rewrite queries.
Initial Observations
For our proof of concept, we created an OpenSearch cluster and copied over all sessions and errors objects from Postgres to test queries against it. We saw a fairly significant improvement in query performance; queries that were previously taking a couple of seconds were now taking a couple of hundred milliseconds. More importantly, adding more filters to the query did not have a noticeable impact on performance, which was an ideal outcome for the flexible query builder we planned to build.Learnings
Join Data Type
For large objects that are updated frequently, consider modeling the updates as child fields with thejoin data type. For LaunchDarkly, this was critical to getting good performance while indexing errors.
In LaunchDarkly, each error can have many instances, which are specific cases where the same error was thrown. We wanted to allow users to query fields on the error (i.e., errors with the message “null pointer exception”) as well as the instance (i.e., errors thrown between 12 pm and 12:30 pm).
Originally, we modeled this in the canonical OpenSearch way: each error was a single document with an array of error instances as a field in that document.
- with respect to the number of objects in the instance array. The impact was visible in the OpenSearch metrics as high CPU usage and throttling via HTTP 429 errors during indexing. Despite our efforts of increasing the cluster size and setting up read replication, a sudden burst of error instances could spike CPU usage at any time.
join data type to tag errors as parent documents and instances as their children, and save both errors and instances as top-level documents in the same index.
has_child query to find all parents with children matching specific criteria. When testing this new approach, we found query times to be reasonable. They doubled the previous time, but were still only a few hundred milliseconds, which we determined to be a good tradeoff to make for the much-improved resource usage.
Compound Fields
There are some cases where it’s helpful to combine fields at index time to simplify and improve performance at query time. In LaunchDarkly, sessions can have user and track properties. These are represented as key-value pairs, with the property type as the key and the property’s value as the value, e.g. for a user propertyuser.email = [zane@highlight.io](<mailto:zane@highlight.io>), user.email is the key and zane@highlight.io is the value. If these were transformed into JSON objects and indexed in OpenSearch, each session would have an array of properties with key and value fields. However, because of the way documents are stored in OpenSearch, the properties objects would be flattened as follows:
nested fields, which are internally represented as separate documents. This would correctly allow for queries like key = "user.email" AND value = "zane@highlight.io", however, this would likely have similar performance impact as the parent-child join in errors.
An easier way to accomplish the same thing with no performance impact is to combine key-value pairs in the same field, as follows:
keyValue = "user.email;zane@highlight.io". And since we retain the original key and value fields, we can still support other types of queries, like existence checks for property keys or searching across all values regardless of type.
Although we don’t have a formal benchmark for the impact of using compound fields in OpenSearch, we would approximate the impact to be similar to parent-child joins (2x faster).