# GetAdvancedSearchResults - Advanced Documentation ## When to Use **This executes a saved advanced search** - use it for bulk, aggregate, or comprehensive queries (all open claims, all contacts by criteria, firm-wide reports). First find the saved search ID via `GetFirmAdvancedSearchesClaim` (for claims) or `GetFirmAdvancedSearchesContact` (for contacts), then pass that ID here. Those two endpoints do not cover every saved search: in one firm they returned 164 of 276, and the searches they omit are disproportionately reporting searches, including all of the Attorney Fees searches. If a search you expect is missing from both lists, fall back to `GetFirmAdvancedSearches`, which returns the firm's complete set. ## Overview Executes a saved advanced search query asynchronously and returns structured results with column definitions and data rows. ## Parameters - **AdvancedSearchID** - ID of the saved search to execute - **PersonID** - ID of the person executing the search (used for security filtering and time zone conversion) - **ParametersJson** (optional) - JSON-serialized `SearchFieldFilter` array to override search filters at runtime. Pass `""` for default filters. See **Parameter Overrides** below. ## Return Structure Returns an `AdvancedSearchResults` object: - **Columns** - field names, types, and display names for each column in the result set - **DataJson** - list of JSON-serialized `row.ItemArray` values (one entry per row, serialized in parallel for performance) ## Implementation Details - Rows are serialized using `AsParallel().Select()` with `JsonConvert.SerializeObject(row.ItemArray)` - parallel serialization was tested and confirmed to cut response time roughly in half - Passes `ConvertToLocal = false` to the data table builder (no time zone conversion on values) - Compare with `GetAdvancedSearchResultString` which passes `ConvertToLocal = true` and returns key/value dictionaries instead of value arrays - High-traffic endpoint used by grid components and web workers for data retrieval ## Script Example: Find and Execute a Search ```javascript async () => { // Find available claim searches const searches = await SecureApi.GetFirmAdvancedSearchesClaim( "open cases", { take: 20, skip: 0, page: 1, pageSize: 20 } ); const search = searches.Data.find(s => s.Name.includes('Total Open')); // Execute search - PersonID is the current user const results = await SecureApi.GetAdvancedSearchResults(search.ID, 0, ""); // Parse: Columns define the column order, DataJson has row arrays const colNames = results.Columns.map(c => c.DisplayName); const rows = results.DataJson.map(json => { const vals = JSON.parse(json); const row = {}; colNames.forEach((name, i) => row[name] = vals[i]); return row; }); // Aggregate const byType = _.countBy(rows, 'Claim Type'); // _ is lodash (available as global) return { total: rows.length, byType }; } ``` ## Parameter Overrides (ParametersJson) Saved searches have configurable filter parameters. You can override their values at runtime by passing a JSON array of `SearchFieldFilter` objects. Each object matches a filter in the saved search by `ParameterName` and overwrites its value. ### How It Works 1. The saved search has a filter tree (`FilterGroupInfo`) with fields that have `ParameterName` values 2. Your `ParametersJson` array provides override values keyed by `ParameterName` 3. The system recursively walks the filter tree, finds all fields matching each `ParameterName` (case-insensitive), and replaces their values ### Discovering Available Parameters Use `GetAdvancedSearchFilters(AdvancedSearchID)` to inspect a search's filter tree. Each `SearchFieldFilter` in the tree has a `ParameterName` - these are the keys you can override. ### Setting Values by DataType Each parameter uses typed value properties - there is no generic `Value` property: | DataType | Property | Example | |----------|----------|---------| | Text/String | `ValueString` | `"Won"` | | Number | `ValueNumber` | `42` | | Date | `ValueDate` | `{ "Value": "2024-01-01" }` | | DateTime | `ValueDateTime` | `"2024-01-01T00:00:00Z"` | | DateOfBirth | `BetweenValueDate` | `{ "StartValue": { "Value": "1980-01-01" }, "EndValue": { "Value": "1985-12-31" } }` | | ID/Name pair | `ValueIDNamePair` | `{ "ID": 5, "Name": "Smith" }` | | Date range | `BetweenValueDate` | `{ "StartValue": { "Value": "2024-01-01" }, "EndValue": { "Value": "2024-12-31" } }` | | Number range | `BetweenValueNumber` | `{ "StartValue": 0, "EndValue": 100000 }` | | Multi-select strings | `ListValueString` | `["Won", "Settled"]` | | Multi-select IDs | `ListValueIDName` | `[{ "ID": 1, "Name": "Smith" }, { "ID": 2, "Name": "Jones" }]` | **Wrap rule.** `Date` and `DateOfBirth` values wrap as `{ "Value": "yyyy-MM-dd" }`. `DateTime` does not - it takes a flat ISO string. The type name does not predict which, so read the table rather than inferring. There is no `BetweenValueDateOfBirth` slot; DateOfBirth uses the `BetweenValueDate` slot. Passing a wrapped value to `DateTime`, or using a DateOfBirth-specific slot name, throws a bare null-reference error that does not name the real problem. ### Example ```javascript async () => { const params = JSON.stringify([ { ParameterName: "Status", ValueString: "Won" }, { ParameterName: "IncidentDate", BetweenValueDate: { StartValue: { Value: "2024-01-01" }, EndValue: { Value: "2024-12-31" } } } ]); const results = await SecureApi.GetAdvancedSearchResults(searchId, 0, params); // ... process results } ``` ## Prefer GetAdvancedSearchResultString for Scripts For script processing (filter, group, join), prefer `GetAdvancedSearchResultString` - it returns key/value objects directly, no column mapping needed. Use `GetAdvancedSearchResults` when you need column metadata or the array format. ## Script Usage Essentials **The search runs once.** Everything below is how to get everything you need out of that one run. ### Before you write the script - **Always look up the search ID first.** Never guess or hardcode an AdvancedSearchID. Use `GetFirmAdvancedSearchesClaim(SearchFilter, Options)` to find saved searches by name, then extract the numeric ID from the result. Pass the search name as SearchFilter and `{ skip: 0, take: 10, pageSize: 10 }` as Options. The result property is `.ID`, not `.AdvancedSearchID`. - **PersonID:** Pass `0` to run as the current user. - **Check `GetAdvancedSearchValues` first.** If the request is a dashboard-style aggregate (sums, counts, ledger totals), it may already be computed. No rows, no run. - **Get the columns.** `GetAdvancedSearchColumns(advancedSearchId)` gives each column's `DisplayName`, and the `DataType` at the leaf of its `FieldList` - read it from the last entry, never the first. Confirm the search actually contains what was asked for. If it does not, stop and say so - do not run it hoping. - **Get the filters before passing any parameter.** `GetAdvancedSearchFilters(advancedSearchId)` carries each filter's `ParameterName`, `Operator`, and current value. - **No separate lookups needed for those two.** Both take just the search id, and their result shapes are fully described here - NEVER call GetEndpointDetail for GetAdvancedSearchColumns or GetAdvancedSearchFilters when working from this document. - **Converting to objects.** `Helpers.toSearchResultObjects(result)` turns `{ Columns, DataJson }` into `Record[]` keyed by column `DisplayName`. The string in `r['...']` must match `DisplayName` exactly. A wrong key yields `undefined`, and aggregating `undefined` produces a wrong number instead of an error. Never write `r['X'] ?? 0` to cover it. - **Sentinel dates.** `1900-01-01` in a date field means "not set", not a real date. Treat it as empty and sanity-check date-derived rates against status before reporting - counting sentinels as real once produced a 100% contract-sign rate where the true figure was 65.3%. ### Passing parameters `ParametersJson` is a flat `SearchFieldFilter[]`. It is not the shape `GetAdvancedSearchFilters` returns, which is a tree of groups with nested children. Build it by copying the filter and replacing its value: - Find the filter by `ParameterName`. - **Whichever value slot is already populated is the one to replace.** The saved search's own value is your worked example of the correct shape. `Operator` confirms it: between operators use `BetweenValueDate` / `BetweenValueNumber` / `BetweenValueDateTime`, list operators use the `ListValue*` slots, simple comparisons use `ValueString` / `ValueNumber` / `ValueDate` / `ValueDateTime` / `ValueIDNamePair`. - Change the value, leave everything else as it arrived. > **WARNING: Read Format vs Write Format** > > `GetAdvancedSearchFilters` returns date values in READ format (flat ISO string, e.g., `"2024-01-01T00:00:00.000Z"`), not in write format. > It is safe to copy a saved value to identify which SLOT to populate. > It is NOT safe to copy a saved date VALUE verbatim - date values must be rewrapped as `{ "Value": "yyyy-MM-dd" }` before sending back to the endpoint. Do not pick a slot from `DataType` alone. Look at what is populated. `GetAdvancedSearchFilters` cannot modify the saved search. It is read-only. **Worked examples.** Matching is by `ParameterName` alone - case-insensitive, across the whole filter tree - so a minimal payload works. Date-range parameter (dates are wrapped as `{ "Value": "yyyy-MM-dd" }`; `BetweenValue` slots are `StartValue`/`EndValue`): ```json [{ "ParameterName": "pt_ContractDate", "BetweenValueDate": { "StartValue": { "Value": "2026-01-01" }, "EndValue": { "Value": "2026-06-30" } } }] ``` Dropdown / list parameter: ```json [{ "ParameterName": "pt_Tags", "ListValueString": ["Partner VIP", "VIP"] }] ``` Three facts from the matching code, worth knowing before you build one: - **You cannot change the Operator.** Only value slots transfer to the saved filter; its own `Operator` stays in force. Set the slot that operator consumes: `Between Inclusive` reads the `BetweenValue*` slots, `Is in List` reads the `ListValue*` slots, simple comparisons read the single `Value*` slots. - **Parameters you do NOT include are left ALONE.** Their saved filter values still apply. Overriding one parameter does not disturb the others, so defensively restating every filter is unnecessary. - **Within a parameter you DO include, set the value slot that the filter's `Operator` reads.** The saved filter's populated slot tells you which one that is. - **A misspelled ParameterName matches nothing and fails silently.** The search runs with its saved values as if you had passed no parameter. Verify the name against the `GetAdvancedSearchFilters` output before running. **Timeout:** an advanced search hard-stops at 30 seconds and cannot be polled or resumed - the only fix is refining the search. This is another reason to fetch once and cache: a search that barely fits the window should never be run twice. ## Caching Large Result Sets **Cache before you compute.** Call `processAndCacheResults` immediately after the fetch, before any aggregation, and return the batchId as soon as you have it. If the aggregation throws first there is no batchId and the rows you paid for are gone. Potentially very large or slow result. Fetch once, then cache and page it in the same script via `processAndCacheResults`; reuse the batchId in later scripts instead of re-fetching. Three things reach for that batchId: questions you planned for, questions the caller raises after seeing the first answer, and your own mistakes. If the script was wrong - bad aggregation, wrong column, an error partway through - recompute against the batchId. A batchId expires. A missing one must surface as an explicit error. If you then decide to re-run the search, say that you are doing it. ## Batching - List every figure you need from this search and compute them all in the one script. Re-running costs the full query time again. - Aggregate rather than returning rows. If you must return rows, cap at the top 20 and give the total count.