Skip to content

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, or drush pm:enable search).
  • Users must have the search content permission and at least one CRM contact view permission (view any crm contact or administer 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:

  1. Contact label (name) — wrapped in an <h1> tag, giving it the highest relevance weight in Search's ranking algorithm.
  2. search_index view 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

  1. Index has not run — Visit `Admin > Configuration > Search and metadata

    Search pagesand check the indexing progress for Contacts. Click **Re-index site** or runddev drush search:index` to index immediately.

  2. 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 settings for the minimum word length.
  3. 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 content and view any crm contact (or administer crm). Review permissions at Admin > People > Permissions.

Contacts page does not appear at /search/contact

  • Verify the search.page.crm_search config 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 SearchHooks is registered. Its SearchIndexInterface constructor argument is optional; if the search module was disabled and re-enabled, rebuild the container with ddev drush cache:rebuild.
  • Contact method changes also trigger reindexing. If only method data changed, verify the method's crm_contact reference 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.