> ## Documentation Index
> Fetch the complete documentation index at: https://forest-chore-open-api.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Search Configuration

> Configure which fields are searchable in your collections, how search behaves, and how to extend or replace the default search logic.

Forest includes a free-text search bar on every collection's table view. By default it searches across text, enum, number, and UUID fields. You can configure exactly which fields are searched, what operators are used, and even replace the default behavior entirely with custom logic.

## How Search Works

When an operator types in the search bar, Forest sends a query to your back-end with the search string. The back-end applies it as a filter against your data source and returns matching records.

Two search modes exist:

* **Normal search**, searches fields in the current collection
* **Extended search**, also searches fields in directly related collections. Operators can trigger extended search from the footer when normal results are empty.

## Default Search Behavior

By default, Forest searches only specific field types:

| Field type  | Default behavior                                    |
| ----------- | --------------------------------------------------- |
| `String`    | Field contains the search string (case-insensitive) |
| `Enum`      | Field equals the search string (case-insensitive)   |
| `Number`    | Field equals the search string (if numeric)         |
| `UUID`      | Field equals the search string                      |
| Other types | Field is ignored                                    |

## Replacing the Search Handler

Use `replaceSearch` in your back-end configuration to define exactly how search strings are translated into filters.

<Info>
  For large datasets, limit searchable fields to columns with database indexes. Searching unindexed fields causes full table scans.
</Info>

<Note>
  In Node.js and Python, the handler receives a `context` with the `generateSearchFilter` helper. In Ruby, the `replace_search` block receives `(search_string, extended_search)` and returns a [condition tree](/get-started/connect/relationships-schema) directly: there is no `generate_search_filter` helper, so you build the tree yourself.
</Note>

### Restricting Which Fields Are Searched

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  agent.customizeCollection('people', collection => {
    collection.replaceSearch((searchString, extendedMode, context) => {
      return context.generateSearchFilter(searchString, {
        extended: extendedMode,
        onlyFields: ['firstName', 'lastName', 'email'],
      });
    });
  });
  ```

  ```ruby Ruby theme={null}
  include ForestAdmin::Types

  @create_agent.customize_collection('people') do |collection|
    collection.replace_search do |search_string, extended_search|
      {
        aggregator: 'Or',
        conditions: ['firstName', 'lastName', 'email'].map do |field|
          { field: field, operator: Operators::I_CONTAINS, value: search_string }
        end
      }
    end
  end
  ```

  ```ruby Ruby DSL theme={null}
  include ForestAdmin::Types

  @create_agent.collection :people do |collection|
    collection.replace_search do |search_string, extended_search|
      {
        aggregator: 'Or',
        conditions: ['firstName', 'lastName', 'email'].map do |field|
          { field: field, operator: Operators::I_CONTAINS, value: search_string }
        end
      }
    end
  end
  ```

  ```python Python theme={null}
  def search_in_people(search_string, extended_search, context):
      return context.generate_search_filter(
          search_string,
          extended=extended_search,
          only_fields=["firstName", "lastName", "email"],
      )

  agent.customize_collection("people").replace_search(search_in_people)
  ```
</CodeGroup>

### Excluding Fields from Default Search

```javascript theme={null}
agent.customizeCollection('people', collection => {
  collection.replaceSearch((searchString, extendedMode, context) => {
    return context.generateSearchFilter(searchString, {
      extended: extendedMode,
      excludeFields: ['internalNotes', 'legacyId'],
    });
  });
});
```

### Context-Dependent Search

Different search logic depending on what the operator is searching for:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  const referenceRegexp = /^[a-f]{16}$/i;
  const barcodeRegexp = /^[0-9]{10}$/;

  agent.customizeCollection('products', collection => {
    collection.replaceSearch(async (searchString, extendedMode, context) => {
      if (referenceRegexp.test(searchString))
        return { field: 'reference', operator: 'Equal', value: searchString };

      if (barcodeRegexp.test(searchString))
        return { field: 'barCode', operator: 'Equal', value: searchString };

      if (!extendedMode)
        return context.generateSearchFilter(searchString, { onlyFields: ['name'] });

      return context.generateSearchFilter(searchString, {
        onlyFields: ['name', 'description', 'brand:name'],
      });
    });
  });
  ```

  ```ruby Ruby theme={null}
  include ForestAdmin::Types

  REFERENCE_REGEXP = /\A[a-f]{16}\z/i
  BARCODE_REGEXP = /\A[0-9]{10}\z/

  @create_agent.customize_collection('products') do |collection|
    collection.replace_search do |search_string, extended_search|
      next { field: 'reference', operator: Operators::EQUAL, value: search_string } if REFERENCE_REGEXP.match?(search_string)
      next { field: 'barCode', operator: Operators::EQUAL, value: search_string } if BARCODE_REGEXP.match?(search_string)

      fields = extended_search ? ['name', 'description', 'brand:name'] : ['name']
      {
        aggregator: 'Or',
        conditions: fields.map do |field|
          { field: field, operator: Operators::I_CONTAINS, value: search_string }
        end
      }
    end
  end
  ```

  ```ruby Ruby DSL theme={null}
  include ForestAdmin::Types

  REFERENCE_REGEXP = /\A[a-f]{16}\z/i
  BARCODE_REGEXP = /\A[0-9]{10}\z/

  @create_agent.collection :products do |collection|
    collection.replace_search do |search_string, extended_search|
      next { field: 'reference', operator: Operators::EQUAL, value: search_string } if REFERENCE_REGEXP.match?(search_string)
      next { field: 'barCode', operator: Operators::EQUAL, value: search_string } if BARCODE_REGEXP.match?(search_string)

      fields = extended_search ? ['name', 'description', 'brand:name'] : ['name']
      {
        aggregator: 'Or',
        conditions: fields.map do |field|
          { field: field, operator: Operators::I_CONTAINS, value: search_string }
        end
      }
    end
  end
  ```
</CodeGroup>

### Integrating an External Search Engine

If your data is indexed in Algolia, Elasticsearch, or another service, call it directly in the search handler:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  const algoliasearch = require('algoliasearch');
  const client = algoliasearch('APPLICATION_ID', 'API_KEY');
  const index = client.initIndex('products');

  agent.customizeCollection('products', collection =>
    collection.replaceSearch(async (searchString) => {
      const { hits } = await index.search(searchString, {
        attributesToRetrieve: ['id'],
        hitsPerPage: 50,
      });

      return { field: 'id', operator: 'In', value: hits.map(h => h.id) };
    })
  );
  ```

  ```ruby Ruby theme={null}
  require 'algolia'

  client = Algolia::Search::Client.create('APPLICATION_ID', 'API_KEY')
  index = client.init_index('products')

  @create_agent.customize_collection('products') do |collection|
    collection.replace_search do |search_string, extended_search|
      hits = index.search(search_string, { attributesToRetrieve: ['id'], hitsPerPage: 50 })['hits']
      { field: 'id', operator: 'In', value: hits.map { |hit| hit['id'] } }
    end
  end
  ```

  ```ruby Ruby DSL theme={null}
  require 'algolia'

  client = Algolia::Search::Client.create('APPLICATION_ID', 'API_KEY')
  index = client.init_index('products')

  @create_agent.collection :products do |collection|
    collection.replace_search do |search_string, extended_search|
      hits = index.search(search_string, { attributesToRetrieve: ['id'], hitsPerPage: 50 })['hits']
      { field: 'id', operator: 'In', value: hits.map { |hit| hit['id'] } }
    end
  end
  ```

  ```python Python theme={null}
  from algoliasearch.search_client import SearchClient

  client = SearchClient.create("APPLICATION_ID", "API_KEY")
  index = client.init_index("products")

  async def search_products(search_string, extended_search, context):
      results = index.search(search_string, {"attributesToRetrieve": ["id"], "hitsPerPage": 50})
      ids = [hit["id"] for hit in results["hits"]]
      return ConditionTreeLeaf("id", "in", ids)

  agent.customize_collection("products").replace_search(search_products)
  ```
</CodeGroup>

## Disabling Search

To remove the search bar from a collection entirely:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  agent.customizeCollection('products', collection => {
    collection.disableSearch();
  });
  ```

  ```ruby Ruby theme={null}
  @create_agent.customize_collection('products') do |collection|
    collection.disable_search
  end
  ```

  ```ruby Ruby DSL theme={null}
  @create_agent.collection :products do |collection|
    collection.disable_search
  end
  ```

  ```python Python theme={null}
  agent.customize_collection("products").disable_search()
  ```
</CodeGroup>

This is useful for collections where free-text search doesn't apply, for example, collections that only display computed or joined data.
