Skip to content

Commit

Permalink
Add autocomplete API
Browse files Browse the repository at this point in the history
Adds an API endpoint (`/autocomplete.json`) to return results for an
autocomplete component we are currently building. This will return query
suggestions for the user as they type, using the Discovery Engine
`CompletionService` API.

- Add controller/route/request spec for API endpoint and basic
  ActiveModel data class for result along the lines of existing search
  API
- Add `DiscoveryEngine::Autocomplete::Complete` library to interact with
  Discovery Engine autocomplete feature
- Add Rails configuration for (existing) `DISCOVERY_ENGINE_DATASTORE`
  environment variable
  • Loading branch information
csutter committed Aug 20, 2024
1 parent dcce01b commit b226050
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
RAILS_ENV: test
# All Google client library calls are mocked, but the application needs this set to boot
DISCOVERY_ENGINE_SERVING_CONFIG: not-used
DISCOVERY_ENGINE_DATASTORE: not-used
DISCOVERY_ENGINE_DATASTORE_BRANCH: not-used
# Redis running through govuk-infrastructure action
REDIS_URL: redis://localhost:6379
Expand Down
11 changes: 11 additions & 0 deletions app/controllers/autocompletes_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class AutocompletesController < ApplicationController
def show
render json: DiscoveryEngine::Autocomplete::Complete.new(query).completion_result
end

private

def query
params.permit(:q)[:q]
end
end
6 changes: 6 additions & 0 deletions app/models/completion_result.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Represents a set of query completion results for an autocomplete feature
class CompletionResult
include ActiveModel::Model

attr_accessor :suggestions
end
43 changes: 43 additions & 0 deletions app/services/discovery_engine/autocomplete/complete.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module DiscoveryEngine::Autocomplete
class Complete
QUERY_MODEL = "user-event".freeze

def initialize(
query,
client: ::Google::Cloud::DiscoveryEngine.completion_service(version: :v1)
)
@query = query
@client = client
end

def completion_result
CompletionResult.new(suggestions:)
end

private

def suggestions
# Discovery Engine returns an error on an empty query, so we need to handle it ourselves
return [] if query.blank?

client
.complete_query(complete_query_request)
.query_suggestions
.map(&:suggestion)
end

def complete_query_request
{
data_store:,
query:,
query_model: QUERY_MODEL,
}
end

def data_store
Rails.configuration.discovery_engine_datastore
end

attr_reader :query, :client
end
end
1 change: 1 addition & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Application < Rails::Application

# Google Discovery Engine configuration
config.discovery_engine_serving_config = ENV.fetch("DISCOVERY_ENGINE_SERVING_CONFIG")
config.discovery_engine_datastore = ENV.fetch("DISCOVERY_ENGINE_DATASTORE")
config.discovery_engine_datastore_branch = ENV.fetch("DISCOVERY_ENGINE_DATASTORE_BRANCH")

# Document sync configuration
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Rails.application.routes.draw do
resource :search, only: [:show]
resource :autocomplete, only: [:show]

# Healthchecks
get "/healthcheck/live", to: proc { [200, {}, %w[OK]] }
Expand Down
20 changes: 20 additions & 0 deletions spec/requests/autocomplete_request_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
RSpec.describe "Making an autocomplete request" do
let(:autocomplete_service) { instance_double(DiscoveryEngine::Autocomplete::Complete, completion_result:) }
let(:completion_result) { CompletionResult.new(suggestions: %w[foo foobar foobaz]) }

before do
allow(DiscoveryEngine::Autocomplete::Complete).to receive(:new)
.with("foo").and_return(autocomplete_service)
end

describe "GET /autocomplete.json" do
it "returns a set of suggestions as JSON" do
get "/autocomplete.json?q=foo"

expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)).to eq({
"suggestions" => %w[foo foobar foobaz],
})
end
end
end
40 changes: 40 additions & 0 deletions spec/services/discovery_engine/autocomplete/complete_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
RSpec.describe DiscoveryEngine::Autocomplete::Complete do
subject(:completion) { described_class.new(query, client:) }

let(:client) { double("CompletionService::Client", complete_query:) }

before do
allow(Rails.configuration).to receive(:discovery_engine_datastore).and_return("/the/datastore")
end

describe "#completion_result" do
subject(:completion_result) { completion.completion_result }

let(:query) { "foo" }
let(:complete_query) { double("response", query_suggestions:) }
let(:query_suggestions) { %w[foo foobar foobaz].map { double("suggestion", suggestion: _1) } }

it "returns the suggestions from the search response" do
expect(completion_result.suggestions).to eq(%w[foo foobar foobaz])
end

it "makes a request to the completion service with the right parameters" do
completion_result

expect(client).to have_received(:complete_query).with(
data_store: "/the/datastore",
query:,
query_model: "user-event",
)
end

context "when the query is empty" do
let(:query) { "" }

it "returns an empty array and does not make a request" do
expect(completion_result.suggestions).to eq([])
expect(client).not_to have_received(:complete_query)
end
end
end
end

0 comments on commit b226050

Please sign in to comment.