Search Integration
Audience: Drupal module developers integrating with or extending the CRM Search integration. For end-user guidance on searching contacts in the UI, see the relevant entity pages.
The CRM module provides optional integration with Drupal core's Search module. When Search is enabled alongside CRM, contacts are indexed and searchable through a dedicated Contacts search page.
The Search module is not required. CRM works fully without it; the search integration activates automatically when both modules are enabled.
Requirements
- CRM module enabled.
- Drupal core Search module enabled
(
Admin > Extend, ordrush pm:enable search). - Users must have the
search contentpermission and at least one CRM contact view permission (view any crm contactoradminister crm) to access and use the Contacts search page.
What You Get
Contacts search page
A Contacts search page is available at /search/contact. It searches the
full-text Search index for matching contacts and returns results as a paged
list. Each result includes:
- A link to the contact canonical page.
- The contact type label.
- A keyword-highlighted snippet drawn from the indexed text.
Search form block
Once the Contacts search page exists, the core search_form_block block can
be placed and scoped to page_id: crm_contact_search to display a
contact-specific search form anywhere on the site.
Wildcard support
The * wildcard is supported within keywords. For example, john* matches
contacts whose indexed text contains john, johnson, or johnathon.
How Indexing Works
Cron-based indexing
Contacts are indexed on cron according to the Search module's
index.cron_limit setting (Admin > Configuration > Search and metadata >
Search pages). Each cron run processes up to that many contacts needing
(re)indexing.
Indexed fields
Text content is built from two sources per contact:
- Contact label (name) — wrapped in an
<h1>tag, giving it the highest relevance weight in Search's ranking algorithm. search_indexview mode — renders the contact entity and appends all visible output. The default display for each bundle includes:
| Bundle | Fields included |
|---|---|
person |
name, full_name, preferred_name, aliases, emails, telephones, addresses |
organization |
name, aliases, emails, telephones, addresses |
household |
name, emails, telephones, addresses |
Administrative, date, and status fields are hidden in the search_index
display and are not indexed.
Automatic reindexing on change
SearchHooks (Drupal\crm\Hook\SearchHooks) listens to contact and contact
method entity events and marks affected contacts for reindexing:
| Hook | Trigger |
|---|---|
hook_crm_contact_insert |
Contact created |
hook_crm_contact_update |
Contact updated |
hook_crm_contact_delete |
Contact deleted |
hook_crm_contact_method_insert |
Contact method created |
hook_crm_contact_method_update |
Contact method updated |
hook_crm_contact_method_delete |
Contact method deleted |
When the Search module is not installed, SearchHooks receives NULL for its
SearchIndexInterface dependency and all hook implementations are no-ops.
Published-contact filtering
Contacts with status = 0 (unpublished) are excluded from search results for
users who do not have administer crm. Site administrators see results for
both published and unpublished contacts.
Permissions
| Permission | Effect |
|---|---|
search content |
Required by core Search to execute any search page. |
view any crm contact |
Grants access to the Contacts search page for regular users. |
administer crm |
Also grants access to the Contacts search page; additionally bypasses the published-only filter. |
administer search |
Required to manage search pages at Admin > Configuration > Search and metadata. |
Configuration
The integration is driven by optional configuration that CRM installs automatically. When the Search module is enabled, the following config objects are active:
| Config object | Purpose |
|---|---|
search.page.crm_search |
Defines the Contacts search page (plugin: crm_contact_search, path: contact). |
core.entity_view_mode.crm_contact.search_index |
Registers the search_index view mode on crm_contact, used when building text to index. |
core.entity_view_mode.crm_contact.search_result |
Registers the search_result view mode on crm_contact, used when rendering result snippets. |
core.entity_view_display.crm_contact.{bundle}.search_index |
One per bundle — defines which fields are rendered for indexing. |
These config objects only depend on the crm module (not on search), so
the view modes and displays are available even when Search is disabled,
making it straightforward to customize display layouts independently of
whether Search is active.
Enabling Search alongside CRM
# Using Drush
ddev drush pm:enable search -y
Clear caches after enabling:
ddev drush cache:rebuild
No additional configuration is required. The search page and view modes are installed automatically.
To trigger an initial index run outside of cron:
ddev drush search:index
Troubleshooting
Contacts search page returns no results
- Index has not run — Visit `Admin > Configuration > Search and metadata
Search pages
and check the indexing progress for Contacts. Click **Re-index site** or runddev drush search:index` to index immediately. - Keywords have no positive terms — Search requires at least one
positive (non-stop-word) keyword. Short or common words may be ignored.
Check
Admin > Configuration > Search and metadata > Search pages > Search settingsfor the minimum word length. - Unpublished contact — Regular users cannot find unpublished contacts. Either publish the contact or search as an admin.
Contacts search page gives a 403
- Confirm the user has both
search contentandview any crm contact(oradminister crm). Review permissions atAdmin > People > Permissions.
Contacts page does not appear at /search/contact
- Verify the
search.page.crm_searchconfig object exists:ddev drush config:get search.page.crm_search. - If it is missing, reinstall Search while CRM is active, then run
ddev drush cache:rebuild. The optional config is installed automatically when both modules are present.
Index is stale after updating a contact
- Confirm
SearchHooksis registered. ItsSearchIndexInterfaceconstructor argument is optional; if the search module was disabled and re-enabled, rebuild the container withddev drush cache:rebuild. - Contact method changes also trigger reindexing. If only method data changed,
verify the method's
crm_contactreference field is populated.
Implementation details
Audience: Module developers and contributors.
ContactSearch plugin
Class: Drupal\crm\Plugin\Search\ContactSearch
Declared with:
#[Search(
id: 'crm_contact_search',
title: new TranslatableMarkup('Contacts'),
)]
Extends ConfigurableSearchPluginBase and implements AccessibleInterface
and SearchIndexingInterface. Key method responsibilities:
| Method | Responsibility |
|---|---|
access() |
Returns allowed if the account has view any crm contact OR administer crm. |
execute() |
Calls findResults() then prepareResults(); returns [] when no keywords are set. |
findResults() |
Queries search_index joined to crm_contact using SearchQuery + PagerSelectExtender. Restricts to status = 1 for non-admins. Returns NULL when Search reports no positive keywords. |
prepareResults() |
Loads contact entities, renders each in the search_result view mode, calls search_excerpt() to generate the snippet, and assembles the result array. |
updateIndex() |
Selects unindexed or stale contacts up to index.cron_limit, calls indexContact() for each, then flushes word weights. |
indexContact() |
Builds <h1>label</h1> + rendered search_index view mode and passes to SearchIndexInterface::index(). |
indexClear() |
Delegates to SearchIndexInterface::clear() for the crm_contact_search type. |
markForReindex() |
Delegates to SearchIndexInterface::markForReindex() for the crm_contact_search type. |
indexStatus() |
Returns ['total' => …, 'remaining' => …] by querying crm_contact vs search_dataset. |
getHelp() |
Returns a render array describing search syntax for the Search help page. |
buildConfigurationForm() |
Currently empty; reserved for future ranking and display options. |
The constructor adds the crm_contact_list cache tag so cached search result
pages are invalidated whenever any contact changes.
SearchHooks
Class: Drupal\crm\Hook\SearchHooks
Registered as an autowired service. Accepts ?SearchIndexInterface so it
gracefully degrades when Search is not installed. Marks the affected contact
for reindexing on any contact or contact method insert, update, or delete.
Optional configuration install
The search_index view modes and displays depend only on crm, so they are
available as soon as CRM is installed. The search.page.crm_search search
page config object declares crm as its only module dependency; it is
installed automatically when Search is enabled alongside CRM via
InstallOptionalConfigHooks::modulesInstalled().
Architecture overview
flowchart TD
subgraph config [Optional config "config/optional"]
SearchPage["search.page.crm_search\n(path: /search/contact)"]
ViewModeIndex["core.entity_view_mode.crm_contact.search_index"]
ViewModeResult["core.entity_view_mode.crm_contact.search_result"]
Displays["core.entity_view_display.crm_contact.{bundle}.search_index"]
end
subgraph plugin [ContactSearch plugin]
UpdateIndex["updateIndex()\ncron indexing"]
FindResults["findResults()\nSearchQuery + PagerSelectExtender"]
PrepareResults["prepareResults()\nload + render + snippet"]
end
subgraph hooks [SearchHooks]
ContactInsert["crm_contact_insert/update/delete"]
MethodInsert["crm_contact_method_insert/update/delete"]
end
SearchPage -->|"activates"| plugin
ViewModeIndex --> UpdateIndex
Displays --> UpdateIndex
UpdateIndex -->|"SearchIndexInterface::index()"| SearchIndex["search_index table"]
SearchIndex --> FindResults
FindResults --> PrepareResults
ViewModeResult --> PrepareResults
ContactInsert -->|"markForReindex()"| SearchIndex
MethodInsert -->|"markForReindex()"| SearchIndex
Testing
| Test class | Type | What it covers |
|---|---|---|
Drupal\Tests\crm\Functional\Plugin\Search\ContactSearchTest |
Functional | Indexing, name search, permission filtering, reindex on update, index clear, multi-type contacts |
Drupal\Tests\crm\Kernel\Plugin\Search\ContactSearchKernelTest |
Kernel | Plugin discovery, indexStatus(), updateIndex(), markForReindex(), indexClear(), execute(), unpublished filtering |
Drupal\Tests\crm\Unit\Plugin\Search\ContactSearchTest |
Unit | create() wiring, access() logic, getHelp(), configuration form, indexClear(), markForReindex(), indexStatus(), execute(), prepareResults(), indexContact() |
Drupal\Tests\crm\Unit\Plugin\Search\ContactSearchUnitTest |
Unit | Additional unit coverage for the same class |
Drupal\Tests\crm\Unit\Hook\SearchHooksTest |
Unit | Reindex triggered on insert/update/delete; no-op when SearchIndexInterface is NULL |
To run these tests in isolation:
ddev phpunit --filter=ContactSearchTest
ddev phpunit --filter=ContactSearchKernelTest
ddev phpunit --filter=ContactSearchUnitTest
ddev phpunit --filter=SearchHooksTest
When writing your own search integration tests, use
ContactSearchKernelTest as a reference: it installs the search schema
tables (search_dataset, search_index, search_total), loads optional
CRM and Search config, and instantiates the plugin via the search plugin
manager.
Related documentation
- Contact entity — contact fields, bundles, and view modes
- Comment integration — commenting on contacts
- Views integration — exposing contact data in Views
- Navigation integration — CRM links in the admin toolbar