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 StringField contains the search string (case-insensitive) EnumField equals the search string (case-insensitive) NumberField equals the search string (if numeric) UUIDField 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.
For large datasets, limit searchable fields to columns with database indexes. Searching unindexed fields causes full table scans.
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 directly: there is no generate_search_filter helper, so you build the tree yourself.
Restricting Which Fields Are Searched
Node.js / Cloud
Ruby
Ruby DSL
Python
agent . customizeCollection ( 'people' , collection => {
collection . replaceSearch (( searchString , extendedMode , context ) => {
return context . generateSearchFilter ( searchString , {
extended: extendedMode ,
onlyFields: [ 'firstName' , 'lastName' , 'email' ],
});
});
});
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
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
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)
Excluding Fields from Default Search
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:
Node.js / Cloud
Ruby
Ruby DSL
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' ],
});
});
});
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
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
Integrating an External Search Engine
If your data is indexed in Algolia, Elasticsearch, or another service, call it directly in the search handler:
Node.js / Cloud
Ruby
Ruby DSL
Python
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 ) };
})
);
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
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
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)
Disabling Search
To remove the search bar from a collection entirely:
Node.js / Cloud
Ruby
Ruby DSL
Python
agent . customizeCollection ( 'products' , collection => {
collection . disableSearch ();
});
@create_agent . customize_collection ( 'products' ) do | collection |
collection. disable_search
end
@create_agent . collection :products do | collection |
collection. disable_search
end
agent.customize_collection( "products" ).disable_search()
This is useful for collections where free-text search doesn’t apply, for example, collections that only display computed or joined data.