Architecture Overview
- Candidate Pool: default set includes latest posts and random posts from the same category.
- Signals: features used for scoring such as category, tag, comment, views, title bigrams, recency, time gap.
- Weights: normalized weights (sum = 1) applied to each signal.
- MMR Diversification: reduces redundancy with Max Marginal Relevance.
- Cache: results cached with an algo version key for smooth invalidation.
- Filters: extension points, all prefix-consistent and non-breaking.
Supported Filters
init_plugin_suite_live_search_ai_candidates: add or remove candidates.init_plugin_suite_live_search_ai_signals: add or override signals.init_plugin_suite_live_search_ai_weights: adjust weights.init_plugin_suite_live_search_ai_score: tweak the final score per candidate.init_plugin_suite_live_search_ai_selected: post-process the selected list.init_plugin_suite_live_search_ai_half_life_recency: half-life for recency signal.init_plugin_suite_live_search_ai_half_life_gap: half-life for time-gap signal.init_plugin_suite_live_search_ai_mmr_lambda: lambda factor for MMR relevance/diversity.
Example Extension: Candidates, Signals, and Weights
The snippet below demonstrates adding candidates from same_keyword (meta) and series (taxonomy), enriching the scoring signals, and adjusting weights accordingly.
// + Add candidates: same series (random) + same_keyword (meta_query)
// + Merge with existing candidates
add_filter('init_plugin_suite_live_search_ai_candidates', function($candidates, $post_id, $post_type){
$candidates = is_array($candidates) ? $candidates : [];
// ----- SAME_KEYWORD -----
$same_kw_raw = function_exists('get_field')
? (string) get_field('same_keyword', $post_id)
: (string) get_post_meta($post_id, 'same_keyword', true);
if ($same_kw_raw !== '') {
$tokens = array_filter(array_map('trim', preg_split('/[,;|]+/u', $same_kw_raw)));
if (!empty($tokens)) {
$meta_query = ['relation' => 'OR'];
foreach ($tokens as $t) {
$meta_query[] = [
'key' => 'same_keyword',
'value' => $t,
'compare' => 'LIKE',
];
}
$kw_pool = get_posts([
'post_type' => $post_type,
'post_status' => 'publish',
'posts_per_page' => 100,
'post__not_in' => [$post_id],
'fields' => 'ids',
'meta_query' => $meta_query,
'no_found_rows' => true,
]);
$candidates = array_merge($candidates, (array)$kw_pool);
}
}
// ----- SERIES (light) -----
$series_terms = wp_get_post_terms($post_id, 'series', ['fields' => 'ids']);
if (!empty($series_terms) && !is_wp_error($series_terms)) {
$series_pool = get_posts([
'post_type' => $post_type,
'post_status' => 'publish',
'posts_per_page' => 30,
'post__not_in' => [$post_id],
'fields' => 'ids',
'orderby' => 'rand',
'tax_query' => [[
'taxonomy' => 'series',
'field' => 'term_id',
'terms' => $series_terms,
]],
'no_found_rows' => true,
]);
$candidates = array_merge($candidates, (array)$series_pool);
}
return array_values(array_unique(array_map('intval', $candidates)));
}, 10, 3);
add_filter('init_plugin_suite_live_search_ai_signals', function($signals, $post_id, $candidate_id){
// SAME_KEYWORD
$get_kw = function($pid){
$raw = function_exists('get_field') ? (string) get_field('same_keyword', $pid)
: (string) get_post_meta($pid, 'same_keyword', true);
return $raw !== '' ? array_filter(array_map('trim', preg_split('/[,;|]+/u', $raw))) : [];
};
$src = $get_kw($post_id);
$dst = $get_kw($candidate_id);
$kw_score = 0.0;
if ($src && $dst) {
$inter = array_intersect($src, $dst);
$kw_score = count($inter) / max(count($src), 1);
}
$signals['same_keyword'] = $kw_score;
// SERIES (binary, light)
$src_series = wp_get_post_terms($post_id, 'series', ['fields' => 'ids']);
$dst_series = wp_get_post_terms($candidate_id, 'series', ['fields' => 'ids']);
$signals['series'] = (!empty(array_intersect($src_series, $dst_series))) ? 1.0 : 0.0;
return $signals;
}, 10, 3);
add_filter('init_plugin_suite_live_search_ai_weights', function($weights){
$weights['tag'] = 0.25;
$weights['series'] = 0.20;
$weights['title_bigrams'] = 0.15;
$weights['same_keyword'] = 0.15;
$weights['category'] = 0.08;
$weights['views'] = 0.07;
$weights['comment'] = 0.05;
$weights['freshness'] = 0.05;
return $weights;
}, 10, 1);
Step-by-Step Guide
- Open the theme/plugin file where you want to add the filters.
- Paste the example code into a suitable PHP file (e.g. an extension module or
functions.php). - Ensure the
seriestaxonomy andsame_keywordmeta field exist and contain data. - Adjust
posts_per_pagelimits to balance quality and performance. - Tune the weights in
..._ai_weightsfilter to achieve the desired ranking.
Performance and Cache
- Prime caches: core calls
_prime_post_cachesandupdate_object_term_cacheto avoid N+1 queries. - Pool limits: keep reasonable candidate limits for meta/tax queries to avoid DB load.
- TTL: results are cached via transient with TTL configurable using
..._ai_cache_ttlfilter. - Algo version: bump
$algo_verwhen making significant logic changes for smooth cache invalidation.
Advanced Ranking Tweaks
- Use
init_plugin_suite_live_search_ai_scoreto adjust final scores, e.g., boosting posts with high conversions. - Use
init_plugin_suite_live_search_ai_selectedto exclude or reorder specific posts after ranking. - Adjust
init_plugin_suite_live_search_ai_mmr_lambdato balance relevance vs. diversity.
Testing and Measurement
- Create test sets with posts sharing series, keywords, and categories to observe ranking changes.
- Compare CTR/Time on Page before and after enabling custom filters.
- Monitor logs and performance metrics for candidate pool size and processing time.
Implementation Notes
- Always check the existence of taxonomy/meta before querying.
- Return only IDs and set
no_found_rowsto reduce query overhead. - Exclude
$post_idfrom candidates and keep only published posts. - Normalize and deduplicate with
array_uniqueandintval.
Conclusion
With its open filter system and cache-friendly design, AI Related Posts in Init Live Search enables flexible extension while maintaining high performance. Start with the example in this guide, then fine-tune signals and weights to match your content strategy.
Comments