Skip to content
Snippets Groups Projects
Commit f71ee1c5 authored by Timothy Zura's avatar Timothy Zura
Browse files

Resolve #3432450 "Add autocomplete functionality"

parent 5b89c261
No related branches found
No related tags found
1 merge request!1Resolve #3432450 "Add autocomplete functionality"
Showing
with 973 additions and 320 deletions
/* Form Elements Div Class (twig) */
.vais-form-elements {
display: flex;
align-items: flex-start;
justify-content: flex-start;
flex-wrap: wrap;
margin-top: 20px;
}
/* Form Element IDs (preprocessor in .theme)*/
.vais-element-keys,
.vais-element-page,
.vais-element-submit {
border: 2px solid #ccc;
padding: .6em 1.4em .5em .5em;
margin: 2px 0;
font-size: 1.25em;
height: 44px;
box-sizing: border-box;
}
.vais-element-keys {
border-radius: 3px 0 0 3px;
min-width: 0;
max-width: unset;
float: unset;
color: #3b3b3b;
font-weight: normal;
width: 300px;
}
.vais-element-page {
margin-left: -2px;
border-radius: 0;
font-weight: normal;
float: none;
}
#vais-element-submit {
color: #fff;
background: #0071bc;
border-radius: 0 3px 3px 0;
border: 0;
}
// Add listener to search page select element of SERP search form.
// Selection of page will modify the action attribute of the form.
if(document.getElementById('vertex-ai-search-box-form').searchpage) {
document.getElementById('vertex-ai-search-box-form').searchpage.onchange = function() {
var newaction = this.value;
document.getElementById('vertex-ai-search-box-form').action = newaction;
};
}
<?php
namespace Drupal\vertex_ai_search\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a VertexAutocompletePlugin type annotation object.
*
* VertexAutocompletePlugin classes define autocomplete plugins for
* vertex_ai_search module.
*
* @see VertexAutocompletePluginBase
*
* @ingroup vertex_ai_search
*
* @Annotation
*/
class VertexAutocompletePlugin extends Plugin {
/**
* A unique identifier for the vertex autocomplete plugin.
*
* @var string
*/
public $id;
/**
* The title for the vertex autocomplete plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @todo This will potentially be translated twice or cached with the wrong
* translation until the vertex autocomplete tabs are converted to
* local task plugins.
*
* @ingroup plugin_translatable
*/
public $title;
}
<?php
namespace Drupal\vertex_ai_search\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\vertex_ai_search\VertexAutocompletePluginManager;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a route controller for entity autocomplete form elements.
*/
class AutocompleteController extends ControllerBase {
/**
* Vertex Autocomplete Plugin Manager.
*
* @var \Drupal\vertex_ai_search\VertexAutocompletePluginManager
*/
protected $autoPluginManager;
/**
* EntityTypeManager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $etypeManager;
/**
* Constructs a new search controller.
*
* @param \Drupal\vertex_ai_search\VertexAutocompletePluginManager $autoPluginManager
* Vertex Autocomplete Plugin Manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The Entity Type Manager.
*/
public function __construct(
VertexAutocompletePluginManager $autoPluginManager,
EntityTypeManagerInterface $entityTypeManager
) {
$this->autoPluginManager = $autoPluginManager;
$this->etypeManager = $entityTypeManager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.vertex_autocomplete'),
$container->get('entity_type.manager')
);
}
/**
* Handler for autocomplete request.
*/
public function handleAutocomplete(Request $request, $search_page_id) {
$searchpage = $this->etypeManager->getStorage('search_page')->load($search_page_id);
$searchpageplugin = $searchpage->getPlugin();
$configuration = $searchpageplugin->getConfiguration();
$keys = $request->query->get('q');
if (strlen($keys) < (int) $configuration['autocomplete_trigger_length']) {
return new JsonResponse([]);
}
$autoplugin = $this->autoPluginManager->createInstance(
$configuration['autocomplete_source'],
$configuration
);
$suggestions = $autoplugin->getSuggestions($keys);
$suggestions = array_slice($suggestions, 0, $configuration['autocomplete_max_suggestions']);
$matches = [];
foreach ($suggestions as $suggestion) {
$matches[] = [
'label' => $suggestion,
'value' => $suggestion,
];
};
return new JsonResponse($matches);
}
}
<?php
namespace Drupal\vertex_ai_search\Plugin\Autocomplete;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\vertex_ai_search\Plugin\VertexAutocompletePluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Handles searching for node entities using the Search module index.
*
* @VertexAutocompletePlugin(
* id = "vertex_autocomplete_content",
* title = @Translation("Content Autocomplete")
* )
*/
class ContentAutocomplete extends VertexAutocompletePluginBase {
/**
* Entity Type Manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $etypeManager;
/**
* Database Connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Constructor.
*
* @param array $configuration
* Configuration array containing information about search page.
* @param string $plugin_id
* Identifier of custom plugin.
* @param array $plugin_definition
* Provides definition of search plugin.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The Entity Type Manager service.
* @param \Drupal\Core\Database\Connection $database
* The database connection service.
*/
public function __construct(
array $configuration,
$plugin_id,
array $plugin_definition,
EntityTypeManagerInterface $entityTypeManager,
Connection $database
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->etypeManager = $entityTypeManager;
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('database')
);
}
/**
* {@inheritdoc}
*/
public function getSuggestions($keys) {
// Look at published nodes.
$query = $this->database->select('node_field_data', 'fd');
$query->addField('fd', 'title', 'title');
$query->condition('fd.status', 1, '=');
// Perform comparisons based on selected autocomplete model.
if ($this->configuration['autocomplete_model'] == 'title_only') {
$query->condition('fd.title', '%' . $keys . '%', 'LIKE');
}
elseif ($this->configuration['autocomplete_model'] == 'title_body') {
$query->join('node__body', 'nb', 'nb.entity_id = fd.nid');
$orcondition = $query->orConditionGroup()
->condition('fd.title', '%' . $keys . '%', 'LIKE')
->condition('nb.body_value', '%' . $keys . '%', 'LIKE');
$query->condition($orcondition);
}
// Only retrieve up to the max suggestions.
$query->range(0, $this->configuration['autocomplete_max_suggestions']);
// If specific content types selected, then filter further.
if ($content_types = $this->configuration['autocomplete_content_types']) {
$query->condition('fd.type', $content_types, 'IN');
}
$results = $query->execute()->fetchCol();
return $results;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$types = $this->etypeManager->getStorage('node_type')->loadMultiple();
$contenttypes = [];
foreach ($types as $key => $type) {
$contenttypes[$key] = $type->get('name');
}
$form['autocomplete_model'] = [
'#title' => $this->t('Autocomplete Model'),
'#type' => 'select',
'#required' => TRUE,
'#options' => [
NULL => $this->t('-- Select a Model --'),
'title_only' => $this->t('Node Title Only'),
'title_body' => $this->t('Node Title and Body'),
],
'#default_value' => $this->configuration['autocomplete_model'] ?? NULL,
];
$form['autocomplete_content_types'] = [
'#title' => $this->t('Content Types'),
'#type' => 'select',
'#options' => $contenttypes,
'#multiple' => TRUE,
'#description' => $this->t('If none selected, all will be included.'),
'#default_value' => $this->configuration['autocomplete_content_types'] ?? NULL,
];
return $form;
}
}
<?php
namespace Drupal\vertex_ai_search\Plugin\Autocomplete;
use Drupal\Core\Form\FormStateInterface;
use Drupal\vertex_ai_search\Plugin\VertexAutocompletePluginBase;
use Google\ApiCore\ApiException;
use Google\ApiCore\ValidationException;
use Google\Cloud\DiscoveryEngine\V1\Client\CompletionServiceClient;
use Google\Cloud\DiscoveryEngine\V1\CompleteQueryRequest;
/**
* Handles searching for node entities using the Search module index.
*
* @VertexAutocompletePlugin(
* id = "vertex_autocomplete_vertex",
* title = @Translation("Vertex Autocomplete")
* )
*/
class VertexAutocomplete extends VertexAutocompletePluginBase {
/**
* {@inheritdoc}
*/
public function getSuggestions($keys) {
$config = $this->getConfiguration();
$formattedDataStore = CompletionServiceClient::dataStoreName(
$config['google_cloud_project_id'],
$config['google_cloud_location'],
$config['vertex_ai_data_store_id']
);
$credpath = $config['service_account_credentials_file'];
// Create a client.
$completionServiceClient = new CompletionServiceClient([
'credentials' => json_decode(file_get_contents($credpath), TRUE),
]);
// Prepare the request message.
$request = (new CompleteQueryRequest())
->setDataStore($formattedDataStore)
->setQuery($keys);
// Call the API and handle any network failures.
try {
/** @var \Google\Cloud\DiscoveryEngine\V1\CompleteQueryResponse $response */
$response = $completionServiceClient->completeQuery($request);
$jsonresults = $response->serializeToJsonString();
return json_decode($jsonresults, TRUE);
}
catch (ApiException $ex) {
watchdog_exception('vertex_ai_search', $ex);
\Drupal::messenger()->addError("An exception occurred: " . $ex);
}
catch (ValidationException $ex) {
watchdog_exception('vertex_ai_search', $ex);
\Drupal::messenger()->addError("An exception occurred: " . $ex);
}
return [];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['autocomplete_model'] = [
'#title' => $this->t('Autocomplete Model'),
'#type' => 'select',
'#required' => TRUE,
'#options' => [
'search_history' => $this->t('Search History'),
],
'#default_value' => $this->configuration['autocomplete_model'] ?? NULL,
];
return $form;
}
}
This diff is collapsed.
<?php
namespace Drupal\vertex_ai_search\Plugin;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides base implementation for a configurable Vertex Autocomplete plugin.
*/
abstract class VertexAutocompletePluginBase extends PluginBase implements VertexAutocompletePluginInterface, ContainerFactoryPluginInterface, RefinableCacheableDependencyInterface {
use RefinableCacheableDependencyTrait;
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->setConfiguration($configuration);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition
);
}
/**
* {@inheritdoc}
*/
public function getSuggestions($keys) {
return [];
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [];
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
return $this->configuration;
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
$this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $configuration);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return [];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
// The entity is a Drupal\search\Entity\SearchPage entity.
// The getPlugin method of SearchPage returns ref to its plugin property.
// -- $plugin = $form_state->getFormObject()->getEntity()->getPlugin();
// Make any changes to the configuration.
// -- $pluginconfig = $plugin->getConfiguration();
// -- $pluginconfig['sample'] = $this->t('sample');
// Update the SearchEntity plugin configuration.
// -- $plugin->setConfiguration($pluginconfig);
}
}
<?php
namespace Drupal\vertex_ai_search\Plugin;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Core\Plugin\PluginFormInterface;
/**
* Provides an interface for a configurable Vertex Autocomplete plugin.
*/
interface VertexAutocompletePluginInterface extends ConfigurableInterface, DependentPluginInterface, PluginFormInterface {
/**
* Grabs the suggestions for the autocompletion.
*
* @param string $keys
* The search keys.
*
* @return array
* An array of suggestions.
*/
public function getSuggestions($keys);
}
<?php
namespace Drupal\vertex_ai_search;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* VertexAutocompletePlugin plugin manager.
*/
class VertexAutocompletePluginManager extends DefaultPluginManager {
/**
* Constructs VertexAutocompletePluginManager.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct(
'Plugin/Autocomplete',
$namespaces,
$module_handler,
'Drupal\vertex_ai_search\Plugin\VertexAutocompletePluginInterface',
'Drupal\vertex_ai_search\Annotation\VertexAutocompletePlugin'
);
$this->setCacheBackend($cache_backend, 'vertex_autocomplete_plugins');
$this->alterInfo('vertex_autocomplete_plugin');
}
}
<div class="promotion">
<div class="title"><a href="{{ promotion.link }}">{{ promotion.htmlTitle | raw }}</a></div>
<div class="snippet">{{ promotion.bodyLines[0].htmlTitle | raw }}</div>
</div>
\ No newline at end of file
<div class="spelling">
{% if corrected == FALSE %}
Did you mean '<a href="{{ url }}">{{ spelling | raw }}</a>'?
{% elseif corrected == TRUE %}
Searching on '<a href="{{ url }}">{{ spelling | raw }}</a>' instead.
{% endif %}
</div>
\ No newline at end of file
......@@ -3,11 +3,3 @@ vertexaisearchresults:
theme:
css/vertex_ai_search.css: { weight: 50 }
dependencies: { }
vertexaisearchformselect:
css:
theme:
css/vertex_ai_search_form.css: { weight: 50 }
js:
js/vertex_ai_search_form_select.js: {}
dependencies: { }
......@@ -14,7 +14,6 @@ use Drupal\search\Entity\SearchPage;
function vertex_ai_search_theme($existing, $type, $theme, $path) {
return [
'vertex_ai_search_result' => [
'variables' => [
'result' => NULL,
......@@ -31,31 +30,6 @@ function vertex_ai_search_theme($existing, $type, $theme, $path) {
'file' => 'vertex_ai_search.theme.inc',
'template' => 'vertex_ai_search_results_message',
],
'vertex_ai_search_results_limitation_message' => [
'variables' => [
'message' => NULL,
'term' => NULL,
],
'file' => 'vertex_ai_search.theme.inc',
'template' => 'vertex_ai_search_results_limitation_message',
],
'vertex_ai_search_promoted_result' => [
'variables' => [
'promotion' => NULL,
'term' => NULL,
],
'file' => 'vertex_ai_search.theme.inc',
'template' => 'vertex_ai_search_promoted_result',
],
'vertex_ai_search_spelling_correction' => [
'variables' => [
'spelling' => NULL,
'corrected' => FALSE,
'url' => NULL,
],
'file' => 'vertex_ai_search.theme.inc',
'template' => 'vertex_ai_search_spelling_correction',
],
'vertex_ai_search_no_results_message' => [
'variables' => [
'message' => NULL,
......@@ -72,14 +46,6 @@ function vertex_ai_search_theme($existing, $type, $theme, $path) {
'file' => 'vertex_ai_search.theme.inc',
'template' => 'vertex_ai_search_no_keywords_message',
],
'vertex_ai_search_results_last_page_message' => [
'variables' => [
'message' => NULL,
'term' => NULL,
],
'file' => 'vertex_ai_search.theme.inc',
'template' => 'vertex_ai_search_results_last_page_message',
],
'vertex_ai_search_page_form' => [
'render element' => 'form',
'file' => 'vertex_ai_search.theme.inc',
......@@ -115,7 +81,8 @@ function vertex_ai_search_preprocess_item_list__search_results__vertex_ai_search
$tokens = _vertex_ai_search_get_empty_search_tokens($configuration);
$message = (!empty($configuration['no_results_message'])) ?
\Drupal::service('token')->replace($configuration['no_results_message'], $tokens) : t("Your search yielded no results.");
\Drupal::service('token')->replace($configuration['no_results_message'], $tokens) :
t("Your search yielded no results.");
$variables['empty'] = [
'#theme' => 'vertex_ai_search_no_results_message',
......@@ -123,7 +90,7 @@ function vertex_ai_search_preprocess_item_list__search_results__vertex_ai_search
'#plugin_id' => 'vertex_ai_search',
'#attached' => [
'library' => [
'vertex_ai_search/googlejsonapiresults',
'vertex_ai_search/vertexaisearchresults',
],
],
];
......@@ -132,7 +99,8 @@ function vertex_ai_search_preprocess_item_list__search_results__vertex_ai_search
if (empty(trim($tokens['vertex_ai_search']['vertex_ai_search_keywords']))) {
$message = (!empty($configuration['no_keywords_message'])) ?
\Drupal::service('token')->replace($configuration['no_keywords_message'], $tokens) : t("Please enter some keywords to perform a search.");
\Drupal::service('token')->replace($configuration['no_keywords_message'], $tokens) :
t("Please enter some keywords to perform a search.");
$variables['empty']['#theme'] = 'vertex_ai_search_no_keywords_message';
$variables['empty']['#message'] = $message;
......@@ -193,14 +161,12 @@ function _vertex_ai_search_get_empty_search_tokens(array $configuration) {
$keys = \Drupal::service('request_stack')->getCurrentRequest()->query->get('keys');
}
// Calculate the start index for the API request.
// 100 is the max due to constraint of Google Custom Search.
$start = min(100, ($page * $items_per_page) + 1);
// Calculate the start and end indexes for the API request.
$start = ($page * $items_per_page) + 1;
$end = $start + $items_per_page - 1;
// Populate the data array used to populate Vertex AI custom tokens.
$tokens['vertex_ai_search'] = [
'google_total_results' => 0,
'vertex_ai_search_keywords' => $keys,
'vertex_ai_search_result_start' => $start,
'vertex_ai_search_result_end' => $end,
......
vertex_ai_search.autocomplete:
path: '/vertex_autocomplete/{search_page_id}'
defaults:
_controller: '\Drupal\vertex_ai_search\Controller\AutocompleteController::handleAutocomplete'
_format: json
requirements:
_access: 'TRUE'
services:
plugin.manager.vertex_autocomplete:
class: Drupal\vertex_ai_search\VertexAutocompletePluginManager
parent: default_plugin_manager
......@@ -2,7 +2,7 @@
/**
* @file
* Themeable functions for Google Custom Search JSON API results.
* Themeable functions for Vertex AI Search results.
*/
/**
......@@ -12,56 +12,35 @@ function template_preprocess_vertex_ai_search_results(&$variables) {
}
/**
* Search result item for Site Restricted Search API queries.
* Search result item.
*/
function template_preprocess_vertex_ai_search_result(&$variables) {
}
/**
* Search results message for Site Restricted Search API queries.
* Search results message.
*/
function template_preprocess_vertex_ai_search_results_message(&$variables) {
}
/**
* Search no results message for Site Restricted Search API queries.
* Search no results message.
*/
function template_preprocess_vertex_ai_search_no_results_message(&$variables) {
}
/**
* Search no keywords message for Site Restricted Search API queries.
* Search no keywords message.
*/
function template_preprocess_vertex_ai_search_no_keywords_message(&$variables) {
}
/**
* Search promoted result for Site Restricted Search API queries.
*/
function template_preprocess_vertex_ai_search_promoted_result(&$variables) {
}
/**
* Search spelling correction for Site Restricted Search API queries.
*/
function template_preprocess_vertex_ai_search_spelling_correction(&$variables) {
}
/**
* Search results limitation message for Site Restricted Search API queries.
*/
function template_preprocess_vertex_ai_search_results_limitation_message(&$variables) {
}
/**
* Search results page search form for Site Restricted Search API queries.
* Search results page search form.
*/
function template_preprocess_vertex_ai_search_search_page_form(&$variables) {
$variables['form']['keys']['#attributes']['class'][] = 'vais-element-keys';
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment