Skip to content

Commit

Permalink
Merge pull request #317 from alphagov/autocomp
Browse files Browse the repository at this point in the history
Add autocomplete API
  • Loading branch information
csutter authored Aug 22, 2024
2 parents dcce01b + 1a3e962 commit 95537b6
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 1 deletion.
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ two core pieces of functionality:
The official way of running this application locally is through [GOV.UK Docker][govuk-docker], where
a project is defined for it. Because this application is deeply integrated with a SaaS product, you
will have to have access to a GCP Discovery Engine engine to be able to do anything more meaningful
than running the test suite. `govuk-docker` will do this for you by configuring the `DISCOVERY_ENGINE_SERVING_CONFIG` and `DISCOVERY_ENGINE_DATASTORE_BRANCH` environment variables your in local set-up to point to integration.
than running the test suite. `govuk-docker` will do this for you by configuring the environment to
point to integration. If you want to run the application without GOV.UK Docker, you can reference
the required [environment variables][env] from there.

You can run the application from within the `govuk-docker` repository directory as follows:

Expand Down Expand Up @@ -80,6 +82,7 @@ as some other marketing terms used in some project artefacts.
[vertex-docs]: https://cloud.google.com/generative-ai-app-builder/docs/introduction
[search-all-finder]: https://www.gov.uk/search/all
[govuk-docker]: https://github.com/alphagov/govuk-docker
[env]: https://github.com/alphagov/govuk-docker/blob/main/projects/search-api-v2/docker-compose.yml
[finder-frontend]: https://github.com/alphagov/finder-frontend
[search-api]: https://github.com/alphagov/search-api
[search-v2-infrastructure]: https://github.com/alphagov/search-v2-infrastructure
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[: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
45 changes: 45 additions & 0 deletions app/services/discovery_engine/autocomplete/complete.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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

attr_reader :query, :client

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

Metrics::Exported.increment_counter(:autocomplete_requests)

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
end
end
3 changes: 3 additions & 0 deletions app/services/metrics/exported.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ module Exported
search_requests: CLIENT.register(
:counter, "search_api_v2_search_requests", "number of incoming search requests"
),
autocomplete_requests: CLIENT.register(
:counter, "search_api_v2_autocomplete_requests", "number of incoming autocomplete requests"
),
### Synchronisation counters
incoming_messages: CLIENT.register(
:counter, "search_api_v2_incoming_messages", "number of incoming messages from Publishing API"
Expand Down
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("completion service", 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 95537b6

Please sign in to comment.