diff --git a/ee/app/models/embedding/vertex/gitlab_documentation.rb b/ee/app/models/embedding/vertex/gitlab_documentation.rb
new file mode 100644
index 0000000000000000000000000000000000000000..46025865a6f993729b1dad2f1cf857a11a4ad4e0
--- /dev/null
+++ b/ee/app/models/embedding/vertex/gitlab_documentation.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Embedding
+  module Vertex
+    class GitlabDocumentation < ::Embedding::ApplicationRecord
+      self.table_name = 'vertex_gitlab_docs'
+
+      has_neighbors :embedding
+
+      scope :current, -> { where(version: get_current_version) }
+      scope :previous, -> { where("version < ?", get_current_version) }
+      scope :nil_embeddings_for_version, ->(version) { where(version: version, embedding: nil) }
+
+      scope :neighbor_for, ->(embedding, limit:) do
+        nearest_neighbors(:embedding, embedding, distance: 'cosine').limit(limit)
+      end
+
+      def self.current_version_cache_key
+        'vertex_gitlab_documentation:version:current'
+      end
+
+      def self.get_current_version
+        Gitlab::Redis::SharedState.with do |redis|
+          redis.get(current_version_cache_key)
+        end.to_i
+      end
+
+      def self.set_current_version!(version)
+        Gitlab::Redis::SharedState.with do |redis|
+          redis.set(current_version_cache_key, version.to_i)
+        end
+      end
+    end
+  end
+end
diff --git a/ee/db/embedding/docs/vertex_gitlab_docs.yml b/ee/db/embedding/docs/vertex_gitlab_docs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8186de51add62580ac6e753a79e62ed7bc2189a3
--- /dev/null
+++ b/ee/db/embedding/docs/vertex_gitlab_docs.yml
@@ -0,0 +1,10 @@
+---
+table_name: vertex_gitlab_docs
+classes:
+- Embedding::Vertex::GitlabDocumentation
+feature_categories:
+  - duo_chat
+description: GitLab documentation embeddings built with VertexAI
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129917
+milestone: '16.4'
+gitlab_schema: gitlab_embedding
\ No newline at end of file
diff --git a/ee/db/embedding/migrate/20230821103900_create_vertex_gitlab_docs.rb b/ee/db/embedding/migrate/20230821103900_create_vertex_gitlab_docs.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5ccb6176e379ee626ece9dd7dcbea73681561fb9
--- /dev/null
+++ b/ee/db/embedding/migrate/20230821103900_create_vertex_gitlab_docs.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class CreateVertexGitlabDocs < Gitlab::Database::Migration[2.1]
+  enable_lock_retries!
+
+  def up
+    create_table :vertex_gitlab_docs do |t|
+      t.timestamps_with_timezone null: false
+      t.integer :version, default: 0, null: false
+      t.vector :embedding, limit: 768
+      t.text :url, null: false, limit: 2048
+      t.text :content, null: false, limit: 32768
+      t.jsonb :metadata, null: false
+    end
+  end
+
+  def down
+    drop_table :vertex_gitlab_docs
+  end
+end
diff --git a/ee/db/embedding/post_migrate/20230821113000_add_index_on_version_to_vertex_gitlab_docs.rb b/ee/db/embedding/post_migrate/20230821113000_add_index_on_version_to_vertex_gitlab_docs.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f298846e996605db6603c6fc8916fd235a4b85e8
--- /dev/null
+++ b/ee/db/embedding/post_migrate/20230821113000_add_index_on_version_to_vertex_gitlab_docs.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddIndexOnVersionToVertexGitlabDocs < Gitlab::Database::Migration[2.1]
+  INDEX_NAME = 'index_vertex_gitlab_docs_on_version'
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :vertex_gitlab_docs, :version, name: INDEX_NAME
+  end
+
+  def down
+    remove_concurrent_index_by_name :vertex_gitlab_docs, INDEX_NAME
+  end
+end
diff --git a/ee/db/embedding/post_migrate/20230821113500_add_index_on_version_where_embedding_is_null_to_vertex_gitlab_docs.rb b/ee/db/embedding/post_migrate/20230821113500_add_index_on_version_where_embedding_is_null_to_vertex_gitlab_docs.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0eb8e0ae3160ab76c37f33a9838376367b8b092d
--- /dev/null
+++ b/ee/db/embedding/post_migrate/20230821113500_add_index_on_version_where_embedding_is_null_to_vertex_gitlab_docs.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddIndexOnVersionWhereEmbeddingIsNullToVertexGitlabDocs < Gitlab::Database::Migration[2.1]
+  INDEX_NAME = 'index_vertex_gitlab_docs_on_version_where_embedding_is_null'
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :vertex_gitlab_docs, :version, where: 'embedding IS NULL', name: INDEX_NAME
+  end
+
+  def down
+    remove_concurrent_index_by_name :vertex_gitlab_docs, INDEX_NAME
+  end
+end
diff --git a/ee/db/embedding/schema_migrations/20230821103900 b/ee/db/embedding/schema_migrations/20230821103900
new file mode 100644
index 0000000000000000000000000000000000000000..bcae240e581b90f8e10860a3ee57e728bbd92bc4
--- /dev/null
+++ b/ee/db/embedding/schema_migrations/20230821103900
@@ -0,0 +1 @@
+2fd9bf701a9830b2160e5190ab92c07264fccced574e8b5e69ac5f57d17ac5d9
\ No newline at end of file
diff --git a/ee/db/embedding/schema_migrations/20230821113000 b/ee/db/embedding/schema_migrations/20230821113000
new file mode 100644
index 0000000000000000000000000000000000000000..760a364adf500200c229bc9cfbc3c3b91c470e3d
--- /dev/null
+++ b/ee/db/embedding/schema_migrations/20230821113000
@@ -0,0 +1 @@
+12dac3188d21a16766bd9576e0b56b092bd2715b4f631d8f2c23eb1776cd1760
\ No newline at end of file
diff --git a/ee/db/embedding/schema_migrations/20230821113500 b/ee/db/embedding/schema_migrations/20230821113500
new file mode 100644
index 0000000000000000000000000000000000000000..0533cf67b5d6c9a51787e60720b744a97eeda192
--- /dev/null
+++ b/ee/db/embedding/schema_migrations/20230821113500
@@ -0,0 +1 @@
+16ffca4983b73a4bec8dd319f8584d78ce66adae715d27ca146d3e1ac203fa4e
\ No newline at end of file
diff --git a/ee/db/embedding/structure.sql b/ee/db/embedding/structure.sql
index c93b6dc291a481de0a009c2fc6cfb74cf54968b5..9e0636df33217e2bee641c3eda97ac647c7bca33 100644
--- a/ee/db/embedding/structure.sql
+++ b/ee/db/embedding/structure.sql
@@ -35,8 +35,32 @@ CREATE SEQUENCE tanuki_bot_mvc_id_seq
 
 ALTER SEQUENCE tanuki_bot_mvc_id_seq OWNED BY tanuki_bot_mvc.id;
 
+CREATE TABLE vertex_gitlab_docs (
+    id bigint NOT NULL,
+    created_at timestamp with time zone NOT NULL,
+    updated_at timestamp with time zone NOT NULL,
+    version integer DEFAULT 0 NOT NULL,
+    embedding vector(768),
+    url text NOT NULL,
+    content text NOT NULL,
+    metadata jsonb NOT NULL,
+    CONSTRAINT check_2e35a254ce CHECK ((char_length(url) <= 2048)),
+    CONSTRAINT check_93ca52e019 CHECK ((char_length(content) <= 32768))
+);
+
+CREATE SEQUENCE vertex_gitlab_docs_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE vertex_gitlab_docs_id_seq OWNED BY vertex_gitlab_docs.id;
+
 ALTER TABLE ONLY tanuki_bot_mvc ALTER COLUMN id SET DEFAULT nextval('tanuki_bot_mvc_id_seq'::regclass);
 
+ALTER TABLE ONLY vertex_gitlab_docs ALTER COLUMN id SET DEFAULT nextval('vertex_gitlab_docs_id_seq'::regclass);
+
 ALTER TABLE ONLY ar_internal_metadata
     ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key);
 
@@ -46,8 +70,15 @@ ALTER TABLE ONLY schema_migrations
 ALTER TABLE ONLY tanuki_bot_mvc
     ADD CONSTRAINT tanuki_bot_mvc_pkey PRIMARY KEY (id);
 
+ALTER TABLE ONLY vertex_gitlab_docs
+    ADD CONSTRAINT vertex_gitlab_docs_pkey PRIMARY KEY (id);
+
 CREATE UNIQUE INDEX index_tanuki_bot_mvc_on_chroma_id ON tanuki_bot_mvc USING btree (chroma_id);
 
 CREATE INDEX index_tanuki_bot_mvc_on_version ON tanuki_bot_mvc USING btree (version);
 
 CREATE INDEX index_tanuki_bot_mvc_on_version_where_embedding_is_null ON tanuki_bot_mvc USING btree (version) WHERE (embedding IS NULL);
+
+CREATE INDEX index_vertex_gitlab_docs_on_version ON vertex_gitlab_docs USING btree (version);
+
+CREATE INDEX index_vertex_gitlab_docs_on_version_where_embedding_is_null ON vertex_gitlab_docs USING btree (version) WHERE (embedding IS NULL);
diff --git a/ee/lib/gitlab/llm/vertex_ai/client.rb b/ee/lib/gitlab/llm/vertex_ai/client.rb
index 32e6d4ed4d1b19b791bfdf3948b8dd09033b5cc0..057c9b937f3104212aa0f72d6f80353fb929b23c 100644
--- a/ee/lib/gitlab/llm/vertex_ai/client.rb
+++ b/ee/lib/gitlab/llm/vertex_ai/client.rb
@@ -80,11 +80,23 @@ def code_completion(content:, **options)
           )
         end
 
+        # @param [String] content - Input string
+        # @param [Hash] options - Additional options to pass to the request
+        def text_embeddings(content:, **options)
+          request(
+            content: content,
+            config: Configuration.new(
+              model_config: ModelConfigurations::TextEmbeddings.new
+            ),
+            **options
+          )
+        end
+
         private
 
         attr_reader :logger
 
-        retry_methods_with_exponential_backoff :chat, :text, :code, :messages_chat, :code_completion
+        retry_methods_with_exponential_backoff :chat, :text, :code, :messages_chat, :code_completion, :text_embeddings
 
         def request(content:, config:, **options)
           logger.info(message: "Performing request to Vertex", config: config)
diff --git a/ee/lib/gitlab/llm/vertex_ai/model_configurations/text_embeddings.rb b/ee/lib/gitlab/llm/vertex_ai/model_configurations/text_embeddings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eea759b5fb9a7baa3cb3bf185d23873a625b4562
--- /dev/null
+++ b/ee/lib/gitlab/llm/vertex_ai/model_configurations/text_embeddings.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Llm
+    module VertexAi
+      module ModelConfigurations
+        class TextEmbeddings < Base
+          NAME = 'textembedding-gecko'
+
+          def payload(content)
+            {
+              instances: [
+                {
+                  content: content
+                }
+              ]
+            }
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/factories/embedding/gitlab_docs.rb b/ee/spec/factories/embedding/gitlab_docs.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bdc19923b5f6f96e538e067fb20846e34bc70f6d
--- /dev/null
+++ b/ee/spec/factories/embedding/gitlab_docs.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+  factory :vertex_gitlab_documentation, class: 'Embedding::Vertex::GitlabDocumentation' do
+    url { 'http://example.com/path/to/a/doc' }
+
+    sequence(:metadata) do |n|
+      {
+        info: "Description for #{n}",
+        source: "path/to/a/doc_#{n}.md",
+        source_type: 'doc'
+      }
+    end
+
+    content { 'Some text' }
+    embedding { Array.new(768, 0.3) }
+  end
+end
diff --git a/ee/spec/lib/gitlab/llm/vertex_ai/client_spec.rb b/ee/spec/lib/gitlab/llm/vertex_ai/client_spec.rb
index e7ee61d0445aa4c36ec93d46188bb63b1608d201..357e86583171b6a3268c46abdabacbf856d89591 100644
--- a/ee/spec/lib/gitlab/llm/vertex_ai/client_spec.rb
+++ b/ee/spec/lib/gitlab/llm/vertex_ai/client_spec.rb
@@ -193,6 +193,12 @@
     it_behaves_like 'forwarding the request correctly'
   end
 
+  describe '#text_embeddings' do
+    subject(:response) { client.text_embeddings(content: 'anything', **options) }
+
+    it_behaves_like 'forwarding the request correctly'
+  end
+
   describe '#request' do
     let(:url) { 'https://example.com/api' }
     let(:config) do
diff --git a/ee/spec/lib/gitlab/llm/vertex_ai/model_configurations/text_embeddings_spec.rb b/ee/spec/lib/gitlab/llm/vertex_ai/model_configurations/text_embeddings_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f108608d6df0697f6b9fe5bfd39b902a88a76b99
--- /dev/null
+++ b/ee/spec/lib/gitlab/llm/vertex_ai/model_configurations/text_embeddings_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Llm::VertexAi::ModelConfigurations::TextEmbeddings, feature_category: :ai_abstraction_layer do
+  let(:host) { 'example-env.com' }
+  let(:project) { 'cllm' }
+
+  before do
+    stub_application_setting(vertex_ai_host: host)
+    stub_application_setting(vertex_ai_project: project)
+  end
+
+  describe '#payload' do
+    it 'returns default payload' do
+      expect(subject.payload('some content')).to eq(
+        {
+          instances: [
+            {
+              content: 'some content'
+            }
+          ]
+        }
+      )
+    end
+  end
+
+  describe '#url' do
+    it 'returns correct url replacing default value' do
+      expect(subject.url).to eq(
+        'https://example-env.com/v1/projects/cllm/locations/us-central1/publishers/google/models/textembedding-gecko:predict'
+      )
+    end
+  end
+end
diff --git a/ee/spec/models/embedding/vertex/gitlab_documentation_spec.rb b/ee/spec/models/embedding/vertex/gitlab_documentation_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4a900b4a0c55a5a97c9ef9ebfd69804a93b4e966
--- /dev/null
+++ b/ee/spec/models/embedding/vertex/gitlab_documentation_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Embedding::Vertex::GitlabDocumentation, :clean_gitlab_redis_shared_state, type: :model, feature_category: :duo_chat do
+  let(:version) { 111 }
+
+  describe 'scopes' do
+    describe '.neighbor_for' do
+      subject(:neighbors) do
+        described_class.neighbor_for(question.embedding, limit: limit)
+      end
+
+      let_it_be(:question) { build(:vertex_gitlab_documentation) }
+      let(:limit) { 10 }
+
+      it 'calls nearest_neighbors for question' do
+        create_list(:vertex_gitlab_documentation, 2)
+
+        expect(described_class).to receive(:nearest_neighbors)
+          .with(:embedding, question.embedding, distance: 'cosine').and_call_original.once
+
+        neighbors
+      end
+
+      context 'with a far away embedding' do
+        let_it_be(:far) { create(:vertex_gitlab_documentation, embedding: Array.new(768, -0.000999)) }
+        let_it_be(:near) { create(:vertex_gitlab_documentation, embedding: Array.new(768, 0.000333)) }
+
+        it 'returns all neighbors' do
+          expect(neighbors).to match_array([near, far])
+        end
+
+        context 'with a limit of one' do
+          let(:limit) { 1 }
+
+          it 'does not return the far neighbor' do
+            expect(neighbors).to match_array(near)
+          end
+        end
+      end
+    end
+
+    describe '.current' do
+      let!(:current_records) { create_list(:vertex_gitlab_documentation, 5, version: version) }
+      let!(:previous_records) { create_list(:vertex_gitlab_documentation, 3, version: version - 1) }
+
+      it 'is empty' do
+        current = described_class.current
+
+        expect(current.count).to eq(0)
+      end
+
+      context 'when there are records matching the current version' do
+        before do
+          allow(described_class).to receive(:get_current_version).and_return(version)
+        end
+
+        it 'returns matching records' do
+          current = described_class.current
+
+          expect(current).to eq(current_records)
+        end
+      end
+    end
+
+    describe '.previous' do
+      let!(:current_records) { create_list(:vertex_gitlab_documentation, 5, version: version) }
+      let!(:previous_records) { create_list(:vertex_gitlab_documentation, 3, version: version - 1) }
+
+      it 'is empty' do
+        previous = described_class.previous
+
+        expect(previous.count).to eq(0)
+      end
+
+      context 'when there are records matching the previous version' do
+        before do
+          allow(described_class).to receive(:get_current_version).and_return(version)
+        end
+
+        it 'returns matching records' do
+          previous = described_class.previous
+
+          expect(previous).to eq(previous_records)
+        end
+      end
+    end
+  end
+
+  describe '.get_current_version' do
+    it 'returns 0' do
+      expect(described_class.get_current_version).to eq(0)
+    end
+
+    context 'when it exists in redis' do
+      before do
+        Gitlab::Redis::SharedState.with do |redis|
+          redis.set(described_class.current_version_cache_key, version)
+        end
+      end
+
+      it 'returns the value' do
+        expect(described_class.get_current_version).to eq(version)
+      end
+    end
+  end
+
+  describe '.set_current_version!' do
+    it 'updates the version in redis' do
+      expect(described_class.get_current_version).to eq(0)
+
+      described_class.set_current_version!(version)
+
+      expect(described_class.get_current_version).to eq(version)
+    end
+  end
+end