Skip to content
New site — Virtual Media Folders now has its own home. Visit vmf.soderlind.no →

Filters

Loupe Search exposes sixteen filters for changing what gets indexed, how it is weighted, how results are returned, and how matches are highlighted.

Filters use the loupe_search_* prefix. The old wp_loupe_* names still work as deprecated aliases — see Renamed from WP Loupe.

  1. Where to put filter code
  2. Quick reference
  3. Indexing scope
  4. Field content
  5. Schema
  6. Sortability
  7. Results
  8. Highlighting
  9. Recipes
  10. After changing a filter

Put these in a small mu-plugin (wp-content/mu-plugins/loupe-search-tweaks.php) rather than a theme’s functions.php. Indexing also runs from WP-CLI and from wp-cron, where the active theme may not be loaded — a filter that only exists in functions.php can therefore apply on the front end but silently not apply during a CLI reindex, producing an index that disagrees with your settings.

Filters that affect indexing must be registered before indexing runs, so register them at load time (not inside an init callback that runs late).

FilterPassedUse it to
loupe_search_post_typesarray $post_typesChoose which post types are indexed
loupe_search_index_protectedbool $should_indexIndex password-protected posts
loupe_search_db_pathstring $pathMove the index directory
loupe_search_field_{$field_name}mixed $valueClean a core post field before indexing
loupe_search_schema_{$post_type}array $schemaAdd/remove fields, set weights and sorting
loupe_search_is_safely_sortable_{$post_type}bool $sortable, string $field_nameForce a core field to be sortable
loupe_search_is_safely_sortable_meta_{$post_type}bool $sortable, string $field_nameCorrect meta-field sortability detection
loupe_search_posts_per_pageint $per_pageChange results per page
loupe_search_max_cacheable_query_lengthint $max_lengthChange which queries are written to the result cache
loupe_search_order_resultsarray $hits, string $queryReorder or regroup results merged across post types
loupe_search_highlightbool $enabledTurn match highlighting on for the default search
loupe_search_highlight_fieldsarray $fieldsChoose which fields are highlighted
loupe_search_highlight_start_tag / _end_tagstring $tagChange the tags wrapped around matches
loupe_search_highlight_crop_fieldsarray $fieldsChoose which fields become cropped snippets
loupe_search_highlight_crop_lengthint $lengthChange snippet length (in words)
loupe_search_highlight_crop_markerstring $markerChange the ellipsis on cropped snippets

Which post types are indexed. Defaults to the post types selected on the settings screen, falling back to [ 'post', 'page' ].

add_filter( 'loupe_search_post_types', function ( array $post_types ): array {
$post_types[] = 'book';
return $post_types;
} );

Append rather than replace unless you intend to override the settings screen — returning a hard-coded array makes the admin checkboxes have no effect.

Whether the current post should be indexed. The value passed in is empty( $post->post_password ), so password-protected posts arrive as false and everything else as true.

// Index password-protected posts too.
add_filter( 'loupe_search_index_protected', '__return_true' );

The filter receives no post object, so it cannot make a per-post decision — it is an on/off switch. Note that indexed protected posts become findable by their content, which is usually not what a password is for.

Where the Loupe index files live. Defaults to WP_CONTENT_DIR . '/loupe-search-db'.

add_filter( 'loupe_search_db_path', function ( string $path ): string {
return WP_CONTENT_DIR . '/uploads/loupe-search-db';
} );

Use an absolute path that PHP can write to, and exclude it from version control. Changing this after indexing points the plugin at an empty directory — reindex afterwards.

Transforms a field’s value just before it is indexed. {$field_name} is the field name, e.g. loupe_search_field_post_content.

// Strip HTML (including block comments) from post_content before indexing.
add_filter( 'loupe_search_field_post_content', 'wp_strip_all_tags' );

This only fires for properties that exist on the WP_Post object — post_content, post_title, post_excerpt, and so on. It does not fire for custom fields or taxonomy values, so loupe_search_field_my_meta_key will never run. To change how meta is indexed, use the schema filter.

Return a string or other scalar; returning an array will not index usefully.

The schema controls which fields are indexed and how they behave. The array you receive is the baseline schema merged with whatever is configured on the settings screen, so read before you write.

Each field accepts:

KeyTypeDefaultMeaning
weightfloat1.0Relevance multiplier in search results
indexableboolfalseInclude the field in the index
filterableboolfalseAllow filtering and terms facets on the field
sortableboolfalseAllow sorting by the field
sort_directionstring'desc'Default direction, 'asc' or 'desc'

sortable is also gated by the sortability checks below — setting it to true on a field that holds arrays will be overridden.

add_filter( 'loupe_search_schema_book', function ( array $schema ): array {
// Add a custom field.
$schema['book_isbn'] = [
'weight' => 2.0,
'indexable' => true,
'filterable' => true,
];
// Make books sort by publication date, newest first.
$schema['book_published'] = [
'weight' => 1.0,
'indexable' => true,
'sortable' => true,
'sort_direction' => 'desc',
];
// Boost titles for this post type.
$schema['post_title']['weight'] = 3.0;
// Stop indexing excerpts.
unset( $schema['post_excerpt'] );
return $schema;
} );

Only post_date is present before settings are applied:

[
'post_date' => [
'weight' => 1.0,
'indexable' => true,
'filterable' => true,
'sortable' => true,
'sort_direction' => 'desc',
],
]

Every other field — post_title, post_content, post_excerpt, post_author, permalink, and any custom fields — is added from the settings screen. Do not assume a field exists in $schema; check first when modifying:

add_filter( 'loupe_search_schema_post', function ( array $schema ): array {
if ( isset( $schema['post_title'] ) ) {
$schema['post_title']['weight'] = 4.0;
}
return $schema;
} );

Before a field is registered as sortable, the plugin checks that its values are scalar — Loupe cannot sort on arrays. These two filters let you override that verdict.

loupe_search_is_safely_sortable_{$post_type}

Section titled “loupe_search_is_safely_sortable_{$post_type}”

Runs for core post fields that are not already classified as sortable or non-sortable. Receives false by default plus the field name.

add_filter( 'loupe_search_is_safely_sortable_book', function ( bool $sortable, string $field_name ): bool {
return 'menu_order' === $field_name ? true : $sortable;
}, 10, 2 );

loupe_search_is_safely_sortable_meta_{$post_type}

Section titled “loupe_search_is_safely_sortable_meta_{$post_type}”

Runs for meta fields. The incoming value comes from sampling the meta value of up to five posts: true if all sampled values were scalar (or geo-points).

That sample can be wrong. If only the sixth post stores an array, the field is wrongly marked sortable and sorting misbehaves; if the first five posts have no value, a perfectly sortable field is marked unsortable. Use this filter to state the answer explicitly:

add_filter( 'loupe_search_is_safely_sortable_meta_book', function ( bool $sortable, string $field_name ): bool {
// This one is always a single number, regardless of what sampling found.
if ( 'book_price' === $field_name ) {
return true;
}
// This one sometimes holds an array — never sort on it.
if ( 'book_contributors' === $field_name ) {
return false;
}
return $sortable;
}, 10, 2 );

Number of results per page for front-end searches. Defaults to the query’s posts_per_page, falling back to Settings → Reading → “Blog pages show at most”.

add_filter( 'loupe_search_posts_per_page', function ( int $per_page ): int {
return 20;
} );

This affects the WordPress search loop only. The REST API takes its page size from the request — see Search API.

Maximum query length, in bytes, that is written to the result cache. Defaults to 128. Longer queries still run, they are just not cached.

Search is reachable without authentication, so every distinct query would otherwise be able to create a transient. The bound keeps a visitor from filling the options table with long throwaway queries.

add_filter( 'loupe_search_max_cacheable_query_length', function ( int $max_length ): int {
return 256;
} );

Return 0 to disable result caching entirely.

Reorders the combined result set after hits from every post-type index are merged. Each post type has its own index, so the engine searches them in turn; by default the merged hits are sorted by relevance score (descending) so a more relevant page can outrank a less relevant post instead of every post type being grouped together.

Each $hit is an array carrying at least id, _score, and post_type. Return the array reordered to apply a different policy.

// Keep pages above posts, most relevant first within each group.
add_filter( 'loupe_search_order_results', function ( array $hits, string $query ): array {
$rank = [ 'page' => 0, 'post' => 1 ];
usort( $hits, function ( $a, $b ) use ( $rank ) {
return [ $rank[ $a['post_type'] ] ?? 9, -$a['_score'] ]
<=> [ $rank[ $b['post_type'] ] ?? 9, -$b['_score'] ];
} );
return $hits;
}, 10, 2 );

Scores from separate indexes are only approximately comparable, so the default relevance merge is a heuristic; use this filter when a site needs a stricter or different ordering.

By default the WordPress search results page shows plain titles and excerpts. Turn on the Highlight Matches checkbox (Settings → Loupe Search → Search Behavior), or use these filters, to opt the default search loop into match highlighting: matched terms are wrapped in a tag (default <mark>) and the excerpt becomes a cropped snippet built around the match. The matches come from Loupe, so a typo-corrected query still highlights the term it actually matched.

Highlighting is a rendering concern only — it needs no reindex and does not change what is stored. The REST API has its own per-request highlighting; see Search API.

The master switch. It defaults to the Highlight Matches checkbox on Settings → Loupe Search → Search Behavior (off out of the box). Use this filter to force it on or off in code regardless of the setting:

add_filter( 'loupe_search_highlight', '__return_true' );

Nothing else here has any effect until this resolves to true.

Which fields get matched terms wrapped in tags. Defaults to [ 'post_title', 'post_content' ]. A field is only highlighted if it is indexable for that post type; unknown names are ignored.

add_filter( 'loupe_search_highlight_fields', function ( array $fields ): array {
return [ 'post_title' ]; // Highlight titles only.
} );

loupe_search_highlight_start_tag / loupe_search_highlight_end_tag

Section titled “loupe_search_highlight_start_tag / loupe_search_highlight_end_tag”

The markup wrapped around each match. Default <mark> / </mark>. Tags are sanitized to a safe inline allowlist (mark, em, strong, span, b, i, each allowing class), so script-capable markup can never be injected.

add_filter( 'loupe_search_highlight_start_tag', fn() => '<span class="hit">' );
add_filter( 'loupe_search_highlight_end_tag', fn() => '</span>' );

If a theme wraps get_the_title() in esc_html(), the tags will show as literal text on that theme — highlight titles via the_title there instead.

Which fields are returned as a cropped snippet centered on the match, instead of the full field. Defaults to [ 'post_content' ], which is what turns the search excerpt into a snippet. Pass an empty array to keep full (still highlighted) fields.

add_filter( 'loupe_search_highlight_crop_fields', '__return_empty_array' );

Snippet length in words. Default 55.

add_filter( 'loupe_search_highlight_crop_length', fn() => 30 );

The marker placed where text is trimmed. Default ….

add_filter( 'loupe_search_highlight_crop_marker', fn() => ' [&hellip;]' );

Index a custom post type with its custom fields

Section titled “Index a custom post type with its custom fields”
add_filter( 'loupe_search_post_types', function ( array $post_types ): array {
$post_types[] = 'recipe';
return $post_types;
} );
add_filter( 'loupe_search_schema_recipe', function ( array $schema ): array {
$schema['recipe_ingredients'] = [
'weight' => 2.0,
'indexable' => true,
'filterable' => true,
];
$schema['recipe_time'] = [
'weight' => 1.0,
'indexable' => true,
'filterable' => true,
'sortable' => true,
'sort_direction' => 'asc',
];
return $schema;
} );
add_filter( 'loupe_search_is_safely_sortable_meta_recipe', function ( bool $sortable, string $field_name ): bool {
return 'recipe_time' === $field_name ? true : $sortable;
}, 10, 2 );

Strip shortcodes as well as HTML from indexed content

Section titled “Strip shortcodes as well as HTML from indexed content”
add_filter( 'loupe_search_field_post_content', function ( $value ) {
return wp_strip_all_tags( strip_shortcodes( (string) $value ) );
} );

Keep a post type out of the index temporarily

Section titled “Keep a post type out of the index temporarily”
add_filter( 'loupe_search_post_types', function ( array $post_types ): array {
return array_values( array_diff( $post_types, [ 'page' ] ) );
} );

Highlight matches on the default search results

Section titled “Highlight matches on the default search results”
// Turn it on, wrap matches in a styled span, and keep excerpts short.
add_filter( 'loupe_search_highlight', '__return_true' );
add_filter( 'loupe_search_highlight_start_tag', fn() => '<mark class="loupe-hit">' );
add_filter( 'loupe_search_highlight_end_tag', fn() => '</mark>' );
add_filter( 'loupe_search_highlight_crop_length', fn() => 40 );

Filters that affect what or how content is indexed — post types, schema, field content, sortability, protected posts — only take effect for content indexed afterwards. Reindex so the existing index matches:

Terminal window
wp loupe-search reindex

Filters that only affect reading, such as loupe_search_posts_per_page and the loupe_search_highlight* filters, take effect immediately.