Repository: averikitsch/genkit
Pull request #9: cloud sql for postgres retriever indexer
Author: login: davidnastasi
Created: 2025-05-09T18:51:25Z | Merged: 2025-05-28T20:49:35Z

Description here... Help the reviewer by:
 - linking to an issue that includes more details
 - if it's a new feature include samples of how to use the new feature
 - (optional if issue link is provided) if you fixed a bug include basic bug details

Checklist (if applicable):
- [ ] PR title is following https://www.conventionalcommits.org/en/v1.0.0/
- [ ] Tested (manually, unit tested, etc.)
- [ ] Docs updated (updated docs or a docs bug required)



Diff:
diff --git a/docs-go/plugins/postgresql.md b/docs-go/plugins/postgresql.md
new file mode 100644
index 0000000000..0863a31e26
--- /dev/null
+++ b/docs-go/plugins/postgresql.md
@@ -0,0 +1,118 @@
+# PostgreSQL plugin
+
+PostgreSQL plugin provides indexer and retriever implementations that use PostgreSQL with the pgvector extension for vector similarity search.
+
+
+## Configuration
+
+To use this plugin, first create a `PostgresEngine` instance:
+
+```go
+// Create PostgresEngine instance
+// with basic authentication
+pEngine, err := NewPostgresEngine(ctx,
+		WithUser('user'),
+		WithPassword('password'),
+		WithCloudSQLInstance('my-project', 'us-central1', 'my-instance'),
+		WithDatabase('my-database')
+
+// with email authentication
+pEngine, err := NewPostgresEngine(ctx,
+    WithCloudSQLInstance('my-project', 'us-central1', 'my-instance'),
+    WithDatabase('my-database'),
+    WithIAMAccountEmail('mail@company.com'))
+
+// with custom pool
+pool, err := pgxpool.New(ctx, "add_your_connection_string")
+if err != nil {
+  return err
+}
+
+pEngine, err := NewPostgresEngine(ctx,
+    WithDatabase("db_test"),
+    WithPool(pool))
+
+// Create the vector store table
+
+err = pEngine.InitVectorstoreTable(ctx, VectorstoreTableOptions{
+    TableName:          "documents",
+    VectorSize:         768,
+    SchemaName:         "public",
+    ContentColumnName:  "content",
+    EmbeddingColumn:    "embedding",
+    MetadataJSONColumn: "custom_metadata",
+    IDColumn: Column{
+      Name:     custom_id,
+      Nullable: false,
+    },
+    MetadataColumns: []Column{
+      {
+        Name:     "source",
+        DataType: "text",
+        Nullable: true,
+      },
+      {
+        Name:     "category",
+        DataType: "text",
+        Nullable: true,
+      },
+    },
+    OverwriteExisting: true,
+    StoreMetadata:     true,
+})
+```
+
+Then, specify the plugin when you initialize Genkit:
+
+```go
+	postgres := &Postgres{
+		engine: pEngine,
+	}
+
+	g, err := genkit.Init(ctx, genkit.WithPlugins(postgres))
+
+// To use the table you configured when you loaded the plugin:
+
+  cfg := &Config{
+    TableName:             'documents',
+    SchemaName:            'public',
+    ContentColumn:         "content",
+    EmbeddingColumn:       "embedding",
+    MetadataColumns:       []string{"source", "category"},
+    IDColumn:              "custom_id",
+    MetadataJSONColumn:    "custom_metadata",
+    Embedder:              embedder,
+    EmbedderOptions:       nil,
+  }
+
+  indexer, err := DefineIndexer(ctx, g, postgres, cfg)
+  if err != nil {
+    return err
+  }
+
+	d1 := ai.DocumentFromText( "The product features include..." , map[string]any{"source": "website", "category": "product-docs", "custom_id": "doc-123"})
+  err := ai.Index(ctx, indexer, ai.WithIndexerDocs(d1))
+  if err != nil {
+    return err
+  }
+
+// To retrieve from the configured table:
+retriever, err := DefineRetriever(ctx, g, postgres, cfg)
+if err != nil {
+  retrun err
+}
+
+d2 := ai.DocumentFromText( "The product features include..." , nil)
+
+resp, err := retriever.Retrieve(ctx, &ai.RetrieverRequest{
+    Query: d2,
+    k:5,
+    filter: "source='website' AND category='product-docs'"
+})
+
+if err != nil {
+    retrun err
+}
+
+```
+See the [Retrieval-augmented generation](../rag.md) page for a general discussion on indexers and retrievers.
diff --git a/go/go.mod b/go/go.mod
index 4054b37338..c7365c877c 100644
--- a/go/go.mod
+++ b/go/go.mod
@@ -16,7 +16,7 @@ require (
 	github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.22.0
 	github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3
 	github.com/blues/jsonata-go v1.5.4
-	github.com/google/dotprompt/go v0.0.0-20250424065700-61c578cf43ac
+	github.com/google/dotprompt/go v0.0.0-20250519044922-687fbbd0d443
 	github.com/google/go-cmp v0.7.0
 	github.com/google/uuid v1.6.0
 	github.com/invopop/jsonschema v0.13.0
@@ -73,6 +73,7 @@ require (
 	github.com/go-openapi/strfmt v0.23.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/go-openapi/validate v0.24.0 // indirect
+	github.com/goccy/go-yaml v1.17.1 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
@@ -85,6 +86,7 @@ require (
 	github.com/jackc/puddle/v2 v2.2.2 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/mailru/easyjson v0.9.0 // indirect
+	github.com/mbleigh/raymond v0.0.0-20250414171441-6b3a58ab9e0a // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/oklog/ulid v1.3.1 // indirect
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
diff --git a/go/go.sum b/go/go.sum
index 8af2f5f45b..a2aac7a7c6 100644
--- a/go/go.sum
+++ b/go/go.sum
@@ -122,6 +122,8 @@ github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1
 github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
 github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
 github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
@@ -146,6 +148,8 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe
 github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
 github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
 github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
+github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
+github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
 github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
 github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
@@ -173,6 +177,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/google/dotprompt/go v0.0.0-20250424065700-61c578cf43ac h1:ktLPEL36xpnElPk3FZKXtU5qHF+5Ozv5ZcC5kPAwV7k=
 github.com/google/dotprompt/go v0.0.0-20250424065700-61c578cf43ac/go.mod h1:dnIk+MSMnipm9uZyPIgptq7I39aDxyjBiaev/OG0W0Y=
+github.com/google/dotprompt/go v0.0.0-20250519044922-687fbbd0d443 h1:p2w3g4QMLPDfJDjOqeWWQ4QReFFab4NFPp21S1buO/Y=
+github.com/google/dotprompt/go v0.0.0-20250519044922-687fbbd0d443/go.mod h1:dnIk+MSMnipm9uZyPIgptq7I39aDxyjBiaev/OG0W0Y=
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -252,6 +258,8 @@ github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4
 github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
 github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
 github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
+github.com/mbleigh/raymond v0.0.0-20250414171441-6b3a58ab9e0a h1:v2cBA3xWKv2cIOVhnzX/gNgkNXqiHfUgJtA3r61Hf7A=
+github.com/mbleigh/raymond v0.0.0-20250414171441-6b3a58ab9e0a/go.mod h1:Y6ghKH+ZijXn5d9E7qGGZBmjitx7iitZdQiIW97EpTU=
 github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw=
 github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo=
 github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
diff --git a/go/plugins/postgresql/constants.go b/go/plugins/postgresql/constants.go
index 31a25b5de0..e2f25250ca 100644
--- a/go/plugins/postgresql/constants.go
+++ b/go/plugins/postgresql/constants.go
@@ -1,3 +1,17 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package postgresql
 
 const (
@@ -8,4 +22,8 @@ const (
 	defaultMetadataJsonColumn = "metadata"
 	defaultCount              = 4
 	defaultUserAgent          = "genkit-cloud-sql-pg-go/0.0.0"
+	defaultIndexNameSuffix    = "vectorindex"
 )
+
+// defaultDistanceStrategy is the default strategy used if none is provided.
+var defaultDistanceStrategy = CosineDistance{}
diff --git a/go/plugins/postgresql/distance_strategy.go b/go/plugins/postgresql/distance_strategy.go
new file mode 100644
index 0000000000..b40bf4baa0
--- /dev/null
+++ b/go/plugins/postgresql/distance_strategy.go
@@ -0,0 +1,124 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package postgresql
+
+import "fmt"
+
+type DistanceStrategy interface {
+	String() string
+	operator() string
+	searchFunction() string
+	similaritySearchFunction() string
+}
+
+type Euclidean struct{}
+
+func (e Euclidean) String() string {
+	return "euclidean"
+}
+
+func (e Euclidean) operator() string {
+	return "<->"
+}
+
+func (e Euclidean) searchFunction() string {
+	return "vector_l2_ops"
+}
+
+func (e Euclidean) similaritySearchFunction() string {
+	return "l2_distance"
+}
+
+type CosineDistance struct{}
+
+func (c CosineDistance) String() string {
+	return "cosineDistance"
+}
+
+func (c CosineDistance) operator() string {
+	return "<=>"
+}
+
+func (c CosineDistance) searchFunction() string {
+	return "vector_cosine_ops"
+}
+
+func (c CosineDistance) similaritySearchFunction() string {
+	return "cosine_distance"
+}
+
+type InnerProduct struct{}
+
+func (i InnerProduct) String() string {
+	return "innerProduct"
+}
+
+func (i InnerProduct) operator() string {
+	return "<#>"
+}
+
+func (i InnerProduct) searchFunction() string {
+	return "vector_ip_ops"
+}
+
+func (i InnerProduct) similaritySearchFunction() string {
+	return "inner_product"
+}
+
+type Index interface {
+	Options() string
+}
+
+type BaseIndex struct {
+	name             string
+	indexType        string
+	options          Index
+	distanceStrategy DistanceStrategy
+	partialIndexes   []string
+}
+
+func NewBaseIndex(indexName, indexType string, strategy DistanceStrategy, partialIndexes []string, opts Index) BaseIndex {
+	return BaseIndex{
+		name:             indexName,
+		indexType:        indexType,
+		distanceStrategy: strategy,
+		partialIndexes:   partialIndexes,
+		options:          opts,
+	}
+}
+
+// HNSWOptions holds the configuration for the hnsw index.
+type HNSWOptions struct {
+	M              int
+	EfConstruction int
+}
+
+func (h HNSWOptions) Options() string {
+	return fmt.Sprintf("(m = %d, ef_construction = %d)", h.M, h.EfConstruction)
+}
+
+// IVFFlatOptions holds the configuration for the ivfflat index.
+type IVFFlatOptions struct {
+	Lists int
+}
+
+func (i IVFFlatOptions) Options() string {
+	return fmt.Sprintf("(lists = %d)", i.Lists)
+}
+
+// indexOptions returns the specific options for the index based on the index type.
+func (index *BaseIndex) indexOptions() string {
+	return index.options.Options()
+}
diff --git a/go/plugins/postgresql/docstore.go b/go/plugins/postgresql/docstore.go
index 6e94fd175a..c1f0c317c2 100644
--- a/go/plugins/postgresql/docstore.go
+++ b/go/plugins/postgresql/docstore.go
@@ -1,3 +1,17 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package postgresql
 
 import (
@@ -7,7 +21,7 @@ import (
 )
 
 type docStore struct {
-	engine PostgresEngine
+	engine *PostgresEngine
 	config *Config
 }
 
@@ -114,7 +128,8 @@ func (ds *docStore) validateConfiguration(ctx context.Context) error {
 		}
 
 		var filteredColumns []string
-		for _, col := range mapColumnNameDataType {
+
+		for col, _ := range mapColumnNameDataType {
 			filteredColumns = append(filteredColumns, col)
 		}
 		ds.config.MetadataColumns = filteredColumns
diff --git a/go/plugins/postgresql/engine.go b/go/plugins/postgresql/engine.go
index bba7cba884..306d88296b 100644
--- a/go/plugins/postgresql/engine.go
+++ b/go/plugins/postgresql/engine.go
@@ -1,3 +1,17 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package postgresql
 
 import (
@@ -128,7 +142,7 @@ func getServiceAccountEmail(ctx context.Context) (string, error) {
 
 // createPool creates a connection pool to the PostgreSQL database.
 func createPool(ctx context.Context, cfg engineConfig, usingIAMAuth bool) (*pgxpool.Pool, error) {
-	var dialeropts []cloudsqlconn.Option
+	dialeropts := []cloudsqlconn.Option{cloudsqlconn.WithUserAgent(cfg.userAgents)}
 	dsn := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", cfg.user, cfg.password, cfg.database)
 	if usingIAMAuth {
 		dialeropts = append(dialeropts, cloudsqlconn.WithIAMAuthN())
diff --git a/go/plugins/postgresql/engine_test.go b/go/plugins/postgresql/engine_test.go
index e19a333643..d40c34e841 100644
--- a/go/plugins/postgresql/engine_test.go
+++ b/go/plugins/postgresql/engine_test.go
@@ -1,3 +1,17 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package postgresql
 
 import (
diff --git a/go/plugins/postgresql/genkit.go b/go/plugins/postgresql/genkit.go
index 9b73ec2749..95b3793f66 100644
--- a/go/plugins/postgresql/genkit.go
+++ b/go/plugins/postgresql/genkit.go
@@ -1,8 +1,22 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package postgresql
 
 import (
 	"context"
-	"errors"
+	"fmt"
 	"sync"
 
 	"github.com/firebase/genkit/go/ai"
@@ -16,7 +30,7 @@ const provider = "postgres"
 type Postgres struct {
 	mu      sync.Mutex
 	initted bool
-	engine  PostgresEngine
+	engine  *PostgresEngine
 }
 
 func (p *Postgres) Name() string {
@@ -31,6 +45,10 @@ func (p *Postgres) Init(ctx context.Context, g *genkit.Genkit) error {
 		panic("postgres.Init already initted")
 	}
 
+	if p.engine == nil {
+		panic("postgres.Init engine is nil")
+	}
+
 	if p.engine.Pool == nil {
 		panic("postgres.Init engine has no pool")
 	}
@@ -56,12 +74,8 @@ type Config struct {
 }
 
 // DefineRetriever defines a Retriever with the given configuration.
-func DefineRetriever(ctx context.Context, g *genkit.Genkit, cfg *Config) (ai.Retriever, error) {
-	p := genkit.LookupPlugin(g, provider).(*Postgres)
-	if p == nil {
-		return nil, errors.New("postgres plugin not found; call genkit.Init with postgres plugin")
-	}
 
+func DefineRetriever(ctx context.Context, g *genkit.Genkit, p *Postgres, cfg *Config) (ai.Retriever, error) {
 	ds, err := newDocStore(ctx, p, cfg)
 	if err != nil {
 		return nil, err
@@ -71,11 +85,8 @@ func DefineRetriever(ctx context.Context, g *genkit.Genkit, cfg *Config) (ai.Ret
 }
 
 // DefineIndexer defines an Indexer with the given configuration.
-func DefineIndexer(ctx context.Context, g *genkit.Genkit, cfg *Config) (ai.Indexer, error) {
-	p := genkit.LookupPlugin(g, provider).(*Postgres)
-	if p == nil {
-		return nil, errors.New(" postgres plugin not found; call genkit.Init with postgres plugin")
-	}
+func DefineIndexer(ctx context.Context, g *genkit.Genkit, p *Postgres, cfg *Config) (ai.Indexer, error) {
+
 	ds, err := newDocStore(ctx, p, cfg)
 	if err != nil {
 		return nil, err
@@ -93,3 +104,106 @@ func Indexer(g *genkit.Genkit, name string) ai.Indexer {
 func Retriever(g *genkit.Genkit, name string) ai.Retriever {
 	return genkit.LookupRetriever(g, provider, name)
 }
+
+/* +++++++++++++++++++++++
+		Advance methods
+ +++++++++++++++++++++++++ */
+
+// ApplyVectorIndex creates an index in the table of the embeddings.
+func (p *Postgres) ApplyVectorIndex(ctx context.Context, config *Config, index BaseIndex, name string, concurrently bool) error {
+	if !p.initted {
+		panic("postgres.ApplyVectorIndex not initted")
+	}
+
+	if index.indexType == "exactnearestneighbor" {
+		return p.DropVectorIndex(ctx, config, name)
+	}
+
+	filter := ""
+	if len(index.partialIndexes) > 0 {
+		filter = fmt.Sprintf("WHERE %s", index.partialIndexes)
+	}
+	optsString := index.indexOptions()
+	params := fmt.Sprintf("WITH %s", optsString)
+
+	if name == "" {
+		if index.name == "" {
+			index.name = config.TableName + defaultIndexNameSuffix
+		}
+		name = index.name
+	}
+
+	concurrentlyStr := ""
+	if concurrently {
+		concurrentlyStr = "CONCURRENTLY"
+	}
+
+	function := index.distanceStrategy.searchFunction()
+	stmt := fmt.Sprintf(`CREATE INDEX %s %s ON "%s"."%s" USING %s (%s %s) %s %s`,
+		concurrentlyStr, name, config.SchemaName, config.TableName, index.indexType, config.EmbeddingColumn, function, params, filter)
+
+	_, err := p.engine.Pool.Exec(ctx, stmt)
+	if err != nil {
+		return fmt.Errorf("failed to execute creation of index: %w", err)
+	}
+
+	return nil
+}
+
+// ReIndex recreates the index on the table.
+func (p *Postgres) ReIndex(ctx context.Context, config *Config) error {
+	if !p.initted {
+		panic("postgres.ApplyVectorIndex not initted")
+	}
+	indexName := config.TableName + defaultIndexNameSuffix
+	return p.ReIndexWithName(ctx, indexName)
+}
+
+// ReIndexWithName recreates the index on the table by name.
+func (p *Postgres) ReIndexWithName(ctx context.Context, indexName string) error {
+	if !p.initted {
+		panic("postgres.ApplyVectorIndex not initted")
+	}
+	query := fmt.Sprintf("REINDEX INDEX %s;", indexName)
+	_, err := p.engine.Pool.Exec(ctx, query)
+	if err != nil {
+		return fmt.Errorf("failed to reindex: %w", err)
+	}
+
+	return nil
+}
+
+// DropVectorIndex drops the vector index from the table.
+func (p *Postgres) DropVectorIndex(ctx context.Context, config *Config, indexName string) error {
+	if !p.initted {
+		panic("postgres.ApplyVectorIndex not initted")
+	}
+	if indexName == "" {
+		indexName = config.TableName + defaultIndexNameSuffix
+	}
+	query := fmt.Sprintf("DROP INDEX IF EXISTS %s;", indexName)
+	_, err := p.engine.Pool.Exec(ctx, query)
+	if err != nil {
+		return fmt.Errorf("failed to drop vector index: %w", err)
+	}
+
+	return nil
+}
+
+// IsValidIndex checks if index exists in the table.
+func (p *Postgres) IsValidIndex(ctx context.Context, config *Config, indexName string) (bool, error) {
+	if !p.initted {
+		panic("postgres.ApplyVectorIndex not initted")
+	}
+	if indexName == "" {
+		indexName = config.TableName + defaultIndexNameSuffix
+	}
+	query := fmt.Sprintf("SELECT tablename, indexname  FROM pg_indexes WHERE tablename = '%s' AND schemaname = '%s' AND indexname = '%s';",
+		config.TableName, config.SchemaName, indexName)
+	var tableName, indexNameFromDB string
+	if err := p.engine.Pool.QueryRow(ctx, query).Scan(&tableName, &indexNameFromDB); err != nil {
+		return false, fmt.Errorf("failed to check if index exists: %w", err)
+	}
+
+	return indexNameFromDB == indexName, nil
+}
diff --git a/go/plugins/postgresql/genkit_test.go b/go/plugins/postgresql/genkit_test.go
index 69c8727ee9..c84a4368ed 100644
--- a/go/plugins/postgresql/genkit_test.go
+++ b/go/plugins/postgresql/genkit_test.go
@@ -1,16 +1,55 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package postgresql
 
 import (
 	"context"
+	"flag"
+	"fmt"
+	"strings"
 	"testing"
 
+	"github.com/firebase/genkit/go/ai"
 	"github.com/firebase/genkit/go/genkit"
+	"github.com/firebase/genkit/go/internal/fakeembedder"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func TestInit_AlreadyCalled(t *testing.T) {
+const (
+	TestTable             = "test_embeddings"
+	SchemaName            = "test"
+	CustomContentColumn   = "my_content"
+	CustomEmbeddingColumn = "my_embedding"
+	CustomIdColumn        = "custom_id"
+	CustomMetadataColumn  = "custom_metadata"
+	DIM                   = 768
+)
+
+var testUsername = flag.String("test-postgres-user", "", "postgres username for tests")
+var testPassword = flag.String("test-postgres-password", "", "postgres password  for tests")
+var testDatabase = flag.String("test-postgres-database", "test_database", "postgres database")
+var testProjectID = flag.String("test-postgres-project-id", "", "postgres project id  for tests")
+var testRegion = flag.String("test-postgres-region", "", "postgres region for tests")
+var testInstance = flag.String("test-postgres-instance", "", "postgres instance for tests")
+var testIAMEmail = flag.String("test-postgres-iam-email", "", "postgres instance for tests")
+
+func TestInit_NoConnectionPool(t *testing.T) {
 	ctx := context.Background()
 	cfg := engineConfig{}
-	engine := PostgresEngine{Pool: cfg.connPool}
+	engine := &PostgresEngine{Pool: cfg.connPool}
 	defer func() {
 		if r := recover(); r == nil {
 			t.Error("panic not called")
@@ -18,6 +57,216 @@ func TestInit_AlreadyCalled(t *testing.T) {
 	}()
 	gcsp := &Postgres{engine: engine}
 	_ = gcsp.Init(ctx, &genkit.Genkit{})
-	_ = gcsp.Init(ctx, &genkit.Genkit{})
+}
+
+func TestInit_AlreadyCalled(t *testing.T) {
+	ctx := context.Background()
+
+	pEngine, err := NewPostgresEngine(ctx,
+		WithUser(*testUsername),
+		WithPassword(*testPassword),
+		WithCloudSQLInstance(*testProjectID, *testRegion, *testInstance),
+		WithDatabase(*testDatabase),
+		WithIAMAccountEmail(*testIAMEmail))
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	defer func() {
+		if r := recover(); r == nil {
+			t.Error("panic not called")
+		}
+	}()
+
+	g := &genkit.Genkit{}
+
+	gcsp := &Postgres{engine: pEngine}
+	g, err = genkit.Init(ctx, genkit.WithPlugins(gcsp))
+
+	err = gcsp.Init(ctx, g)
+	if err == nil {
+		t.Fatal("must fail if init is called twice")
+	}
+
+}
+
+func TestPostgres(t *testing.T) {
+	if !areValidFlags() {
+		t.Skip("no valid postgres flags")
+	}
+
+	ctx := context.Background()
+
+	pEngine, err := NewPostgresEngine(ctx,
+		WithUser(*testUsername),
+		WithPassword(*testPassword),
+		WithCloudSQLInstance(*testProjectID, *testRegion, *testInstance),
+		WithDatabase(*testDatabase),
+		WithIAMAccountEmail(*testIAMEmail))
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	postgres := &Postgres{
+		engine: pEngine,
+	}
+
+	g, err := genkit.Init(ctx, genkit.WithPlugins(postgres))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// Create test schema and table
+	_, err = pEngine.Pool.Exec(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", SchemaName))
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	_, err = pEngine.Pool.Exec(ctx, fmt.Sprintf("DROP TABLE IF EXISTS %s.%s", SchemaName, TestTable))
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	// Initialize the vectorstore table
+	err = pEngine.InitVectorstoreTable(ctx, VectorstoreTableOptions{
+		TableName:          TestTable,
+		VectorSize:         DIM,
+		SchemaName:         SchemaName,
+		ContentColumnName:  CustomContentColumn,
+		EmbeddingColumn:    CustomEmbeddingColumn,
+		MetadataJSONColumn: CustomMetadataColumn,
+		IDColumn: Column{
+			Name:     CustomIdColumn,
+			Nullable: false,
+		},
+		MetadataColumns: []Column{
+			{
+				Name:     "source",
+				DataType: "text",
+				Nullable: true,
+			},
+			{
+				Name:     "name",
+				DataType: "text",
+				Nullable: true,
+			},
+		},
+		OverwriteExisting: true,
+		StoreMetadata:     true,
+	})
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	d1 := ai.DocumentFromText("hello1", map[string]any{"source": "test1", "name": "some_value1", "custom_metadata": "{\"key\":\"value1\"}"})
+	d2 := ai.DocumentFromText("hello2", map[string]any{"source": "test2", "name": "some_value2", "custom_metadata": "{\"key\":\"value2\"}"})
+	d3 := ai.DocumentFromText("goodbye", map[string]any{"source": "test3", "name": "some_value3", "custom_metadata": "{\"key\": { \"subKey\":\"value3\"}}"})
+
+	embedder := newFakeEmbedder([3]*ai.Document{d1, d2, d3})
+
+	cfg := &Config{
+		TableName:             TestTable,
+		SchemaName:            SchemaName,
+		ContentColumn:         CustomContentColumn,
+		EmbeddingColumn:       CustomEmbeddingColumn,
+		MetadataColumns:       []string{"source", "name"},
+		IDColumn:              CustomIdColumn,
+		MetadataJSONColumn:    CustomMetadataColumn,
+		IgnoreMetadataColumns: []string{"created_at", "updated_at"},
+		Embedder:              genkit.DefineEmbedder(g, "fake", "embedder3", embedder.Embed),
+		EmbedderOptions:       nil,
+	}
+
+	indexer, err := DefineIndexer(ctx, g, postgres, cfg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	reqIndexer := &ai.IndexerRequest{
+		Documents: []*ai.Document{d1, d2, d3},
+		Options:   nil}
+
+	err = indexer.Index(ctx, reqIndexer)
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	rows, err := pEngine.Pool.Query(ctx, fmt.Sprintf("SELECT * FROM %s.%s", SchemaName, TestTable))
+	if err != nil {
+		t.Fatal(err.Error())
+	}
+
+	if !rows.Next() {
+		t.Fatal("must have a single document")
+	}
+
+	retriever, err := DefineRetriever(ctx, g, postgres, cfg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	resp, err := retriever.Retrieve(ctx, &ai.RetrieverRequest{
+		Query: d1,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	require.Len(t, resp.Documents, 3)
+	require.Len(t, resp.Documents[0].Content, 1)
+	assert.Equal(t, "hello1", resp.Documents[0].Content[0].Text)
+
+	resp, err = retriever.Retrieve(ctx, &ai.RetrieverRequest{
+		Query: d1,
+		Options: &RetrieverOptions{
+			Filter: "name='some_value2'",
+		},
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	require.Len(t, resp.Documents, 1)
+	require.Len(t, resp.Documents[0].Content, 1)
+	assert.Equal(t, "hello2", resp.Documents[0].Content[0].Text)
+
+}
+
+/** ***************
+Helper functions
+************** */
+
+func newFakeEmbedder(docs [3]*ai.Document) *fakeembedder.Embedder {
+
+	v1 := make([]float32, DIM)
+	v2 := make([]float32, DIM)
+	v3 := make([]float32, DIM)
+	for i := range v1 {
+		v1[i] = float32(i)
+		v2[i] = float32(i)
+		v3[i] = float32(DIM - i)
+	}
+	v2[0] = 1
+
+	embedder := fakeembedder.New()
+	embedder.Register(docs[0], v1)
+	embedder.Register(docs[1], v2)
+	embedder.Register(docs[2], v3)
+
+	return embedder
+}
+
+func areValidFlags() bool {
+	isBasicAuth := testUsername != nil && strings.TrimSpace(*testUsername) != "" &&
+		testPassword != nil && strings.TrimSpace(*testPassword) != ""
+
+	isIAMAuth := testIAMEmail != nil && strings.TrimSpace(*testIAMEmail) != ""
+
+	isPostgresEnv := testDatabase != nil && strings.TrimSpace(*testDatabase) != "" &&
+		testProjectID != nil && strings.TrimSpace(*testProjectID) != "" &&
+		testRegion != nil && strings.TrimSpace(*testRegion) != "" &&
+		testInstance != nil && strings.TrimSpace(*testInstance) != ""
 
+	return (isBasicAuth || isIAMAuth) && isPostgresEnv
 }
diff --git a/go/plugins/postgresql/indexer.go b/go/plugins/postgresql/indexer.go
index 6cccf4a657..587c216474 100644
--- a/go/plugins/postgresql/indexer.go
+++ b/go/plugins/postgresql/indexer.go
@@ -1,12 +1,128 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package postgresql
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
+	"slices"
+	"strings"
 
 	"github.com/firebase/genkit/go/ai"
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5"
+	"github.com/pgvector/pgvector-go"
 )
 
-func (ds *docStore) Index(context.Context, *ai.IndexerRequest) error {
-	// TODO: implement
+// Index implements the genkit.Index method.
+func (ds *docStore) Index(ctx context.Context, req *ai.IndexerRequest) error {
+	if len(req.Documents) == 0 {
+		return nil
+	}
+
+	ereq := &ai.EmbedRequest{
+		Input:   req.Documents,
+		Options: ds.config.EmbedderOptions,
+	}
+	eres, err := ds.config.Embedder.Embed(ctx, ereq)
+	if err != nil {
+		return fmt.Errorf("postgres.Indexer index embedding failed: %v", err)
+	}
+
+	b := &pgx.Batch{}
+
+	for i, doc := range req.Documents {
+		// if no metadata provided, initialize with empty map
+		if doc.Metadata == nil {
+			doc.Metadata = make(map[string]any)
+		}
+
+		// generate the id if it's not defined
+		_, okString := doc.Metadata[ds.config.IDColumn].(string)
+
+		idBytes, okBytes := doc.Metadata[ds.config.IDColumn].([]byte) // represents the uuid
+		if !okString && !okBytes {
+			doc.Metadata[ds.config.IDColumn] = uuid.New().String()
+		}
+
+		if okBytes {
+			doc.Metadata[ds.config.IDColumn] = string(idBytes)
+		}
+
+		doc.Metadata[ds.config.ContentColumn] = ""
+		if len(doc.Content) > 0 {
+			doc.Metadata[ds.config.ContentColumn] = doc.Content[0].Text
+		}
+
+		embeddingString := pgvector.NewVector(eres.Embeddings[i].Embedding).String()
+		query, values, err := ds.generateAddDocumentsQuery(
+			doc.Metadata[ds.config.IDColumn].(string),
+			doc.Metadata[ds.config.ContentColumn].(string),
+			embeddingString,
+			doc.Metadata)
+		if err != nil {
+			return err
+		}
+		b.Queue(query, values...)
+	}
+
+	batchResults := ds.engine.Pool.SendBatch(ctx, b)
+	if err := batchResults.Close(); err != nil {
+		return fmt.Errorf("failed to execute batch: %w", err)
+	}
+
 	return nil
 }
+
+func (ds *docStore) generateAddDocumentsQuery(id, content, embedding string, metadata map[string]any) (string, []any, error) {
+	// Construct metadata column names if present
+	metadataColNames := ""
+	if len(ds.config.MetadataColumns) > 0 {
+		metadataColNames = ", " + strings.Join(ds.config.MetadataColumns, ", ")
+	}
+
+	if ds.config.MetadataJSONColumn != "" && !slices.Contains(ds.config.MetadataColumns, ds.config.MetadataJSONColumn) {
+		metadataColNames += ", " + ds.config.MetadataJSONColumn
+	}
+
+	insertStmt := fmt.Sprintf(`INSERT INTO %q.%q (%s, %s, %s%s)`,
+		ds.config.SchemaName, ds.config.TableName, ds.config.IDColumn, ds.config.ContentColumn, ds.config.EmbeddingColumn, metadataColNames)
+	valuesStmt := " VALUES ($1, $2, $3"
+	values := []any{id, content, embedding}
+
+	// Add metadata
+	for _, metadataColumn := range ds.config.MetadataColumns {
+		if val, ok := metadata[metadataColumn]; ok {
+			valuesStmt += fmt.Sprintf(", $%d", len(values)+1)
+			values = append(values, val)
+			delete(metadata, metadataColumn)
+		} else {
+			valuesStmt += ", NULL"
+		}
+	}
+	// Add JSON column and/or close statement
+	if ds.config.MetadataJSONColumn != "" && !slices.Contains(ds.config.MetadataColumns, ds.config.MetadataJSONColumn) {
+		valuesStmt += fmt.Sprintf(", $%d", len(values)+1)
+		metadataJSON, err := json.Marshal(metadata)
+		if err != nil {
+			return "", nil, fmt.Errorf("failed to transform metadata to json: %w", err)
+		}
+		values = append(values, metadataJSON)
+	}
+	valuesStmt += ")"
+	query := insertStmt + valuesStmt
+	return query, values, nil
+}
diff --git a/go/plugins/postgresql/indexer_test.go b/go/plugins/postgresql/indexer_test.go
new file mode 100644
index 0000000000..1ab00d3d18
--- /dev/null
+++ b/go/plugins/postgresql/indexer_test.go
@@ -0,0 +1,49 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package postgresql
+
+import (
+	"context"
+	"testing"
+
+	"github.com/firebase/genkit/go/ai"
+	"github.com/stretchr/testify/require"
+)
+
+func TestIndex_Success_NoDocuments(t *testing.T) {
+	ds := docStore{}
+	err := ds.Index(context.Background(), &ai.IndexerRequest{})
+	require.NoError(t, err)
+
+}
+
+func TestIndex_Fail_EmbedReturnError(t *testing.T) {
+	ds := docStore{
+		config: &Config{Embedder: mockEmbedderFail{}},
+	}
+	req := &ai.IndexerRequest{
+		Documents: []*ai.Document{{
+			Content: []*ai.Part{{
+				Kind:        ai.PartText,
+				ContentType: "text/plain",
+				Text:        "This is a test",
+			}},
+			Metadata: nil,
+		}},
+		Options: nil}
+
+	err := ds.Index(context.Background(), req)
+	require.Error(t, err)
+}
diff --git a/go/plugins/postgresql/integration.cloudbuild.yaml b/go/plugins/postgresql/integration.cloudbuild.yaml
index d4b31d26c7..f82a432e31 100644
--- a/go/plugins/postgresql/integration.cloudbuild.yaml
+++ b/go/plugins/postgresql/integration.cloudbuild.yaml
@@ -42,7 +42,14 @@ steps:
       - name: "go"
         path: "/gopath"
     script: |
-      go test -race ./...
+      go test -race ./... \
+        -test-postgres-user=$DB_USER \
+        -test-postgres-password=$DB_PASSWORD \
+        -test-postgres-iam-email=$EMAIL \
+        -test-postgres-project-id=$PROJECT_ID \
+        -test-postgres-region=$REGION \
+        -test-postgres-instance=$INSTANCE_ID \
+        -test-postgres-database=$DATABASE_ID
 
 availableSecrets:
   secretManager:
@@ -57,11 +64,11 @@ substitutions:
   _INSTANCE_CONNECTION_NAME: ${PROJECT_ID}:${_REGION}:${_INSTANCE_ID}
   _DATABASE_PORT: "5432"
   _DATABASE_ID: test_database
-  _REGION: us-west2
-  _VERSION: "22"
+  _REGION: us-central1
+  _VERSION: "1.24"
   _IP_ADDRESS: "127.0.0.1"
   _INSTANCE_ID: my-instance
 
 options:
   dynamicSubstitutions: true
-  logging: CLOUD_LOGGING_ONLY
\ No newline at end of file
+  logging: CLOUD_LOGGING_ONLY
diff --git a/go/plugins/postgresql/mocks_test.go b/go/plugins/postgresql/mocks_test.go
new file mode 100644
index 0000000000..28925f1b2f
--- /dev/null
+++ b/go/plugins/postgresql/mocks_test.go
@@ -0,0 +1,29 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package postgresql
+
+import (
+	"context"
+	"errors"
+
+	"github.com/firebase/genkit/go/ai"
+)
+
+type mockEmbedderFail struct{}
+
+func (m mockEmbedderFail) Name() string { return "mock" }
+func (m mockEmbedderFail) Embed(ctx context.Context, req *ai.EmbedRequest) (*ai.EmbedResponse, error) {
+	return nil, errors.New("mock fail")
+}
diff --git a/go/plugins/postgresql/options.go b/go/plugins/postgresql/options.go
index 28805ef877..d842f9ca3a 100644
--- a/go/plugins/postgresql/options.go
+++ b/go/plugins/postgresql/options.go
@@ -1,3 +1,17 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package postgresql
 
 import (
diff --git a/go/plugins/postgresql/retriever.go b/go/plugins/postgresql/retriever.go
index b766d37a1b..c58aecb90f 100644
--- a/go/plugins/postgresql/retriever.go
+++ b/go/plugins/postgresql/retriever.go
@@ -1,17 +1,155 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 package postgresql
 
 import (
 	"context"
+	"fmt"
+	"slices"
+	"strings"
 
 	"github.com/firebase/genkit/go/ai"
+	"github.com/pgvector/pgvector-go"
 )
 
 // RetrieverOptions options for retriever
 type RetrieverOptions struct {
+	Filter           any
+	K                int
+	DistanceStrategy DistanceStrategy
 }
 
 // Retrieve returns the result of the query
 func (ds *docStore) Retrieve(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
-	// TODO: implement
-	return nil, nil
+	if req.Options == nil {
+		req.Options = &RetrieverOptions{
+			Filter:           nil,
+			K:                defaultCount,
+			DistanceStrategy: defaultDistanceStrategy,
+		}
+	}
+
+	ropt, ok := req.Options.(*RetrieverOptions)
+	if !ok {
+		return nil, fmt.Errorf("postgres.Retrieve options have type %T, want %T", req.Options, &RetrieverOptions{})
+	}
+
+	if ropt.K <= 0 {
+		ropt.K = defaultCount
+	}
+
+	if ropt.DistanceStrategy == nil {
+		ropt.DistanceStrategy = defaultDistanceStrategy
+	}
+
+	ereq := &ai.EmbedRequest{
+		Input:   []*ai.Document{req.Query},
+		Options: ds.config.EmbedderOptions,
+	}
+
+	eres, err := ds.config.Embedder.Embed(ctx, ereq)
+	if err != nil {
+		return nil, fmt.Errorf("postgres.Retrieve retrieve embedding failed: %v", err)
+	}
+	res, err := ds.query(ctx, ropt, eres.Embeddings[0].Embedding)
+	if err != nil {
+		return nil, fmt.Errorf("googlecloudsql.postgres.Retrieve failed to execute the query: %v", err)
+	}
+	return res, nil
+}
+
+func (ds *docStore) query(ctx context.Context, ropt *RetrieverOptions, embbeding []float32) (*ai.RetrieverResponse, error) {
+	res := &ai.RetrieverResponse{}
+
+	query := ds.buildQuery(ropt, embbeding)
+	rows, err := ds.engine.Pool.Query(ctx, query)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	fieldDescriptions := rows.FieldDescriptions()
+	columnNames := make([]string, len(fieldDescriptions))
+
+	for i, fieldDescription := range fieldDescriptions {
+		columnNames[i] = fieldDescription.Name
+	}
+
+	for rows.Next() {
+		values := make([]interface{}, len(columnNames))
+		valuesPrt := make([]interface{}, len(columnNames))
+
+		for i := range columnNames {
+			valuesPrt[i] = &values[i]
+		}
+		if err := rows.Scan(valuesPrt...); err != nil {
+			return nil, fmt.Errorf("scan row failed: %v", err)
+		}
+
+		meta := make(map[string]any, ropt.K)
+		var content []*ai.Part
+		for i, col := range columnNames {
+			if (len(ds.config.MetadataColumns) > 0 && !slices.Contains(ds.config.MetadataColumns, col)) &&
+				ds.config.ContentColumn != col &&
+				ds.config.MetadataJSONColumn != col {
+				continue
+			}
+
+			if ds.config.ContentColumn == col {
+				content = append(content, ai.NewTextPart(values[i].(string)))
+			}
+
+			if ds.config.MetadataJSONColumn == col {
+				mapMetadata := map[string]any{}
+				if values[i] != nil {
+					mapMetadata = values[i].(map[string]any)
+				}
+
+				meta[col] = mapMetadata
+				continue
+			}
+
+			meta[col] = values[i]
+		}
+
+		doc := &ai.Document{
+			Metadata: meta,
+			Content:  content,
+		}
+
+		res.Documents = append(res.Documents, doc)
+	}
+
+	return res, nil
+}
+
+func (ds *docStore) buildQuery(ropt *RetrieverOptions, embedding []float32) string {
+	operator := ropt.DistanceStrategy.operator()
+	searchFunction := ropt.DistanceStrategy.similaritySearchFunction()
+	columns := append(ds.config.MetadataColumns, ds.config.ContentColumn)
+	if ds.config.MetadataJSONColumn != "" {
+		columns = append(columns, ds.config.MetadataJSONColumn)
+	}
+	columnNames := strings.Join(columns, `, `)
+	whereClause := ""
+	if ropt.Filter != nil {
+		whereClause = fmt.Sprintf("WHERE %s", ropt.Filter)
+	}
+	vectorAsString := pgvector.NewVector(embedding).String()
+	stmt := fmt.Sprintf(`
+        SELECT %s, %s(%s, '%s') AS distance FROM "%s"."%s" %s ORDER BY %s %s '%s' LIMIT %d;`,
+		columnNames, searchFunction, ds.config.EmbeddingColumn, vectorAsString, ds.config.SchemaName, ds.config.TableName,
+		whereClause, ds.config.EmbeddingColumn, operator, vectorAsString, ropt.K)
+	return stmt
 }
diff --git a/go/plugins/postgresql/retriever_test.go b/go/plugins/postgresql/retriever_test.go
new file mode 100644
index 0000000000..be3e81dbc9
--- /dev/null
+++ b/go/plugins/postgresql/retriever_test.go
@@ -0,0 +1,41 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// You may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package postgresql
+
+import (
+	"context"
+	"testing"
+
+	"github.com/firebase/genkit/go/ai"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRetriever_Fail_WrongTypeOfOption(t *testing.T) {
+	ds := docStore{}
+	res, err := ds.Retrieve(context.Background(), &ai.RetrieverRequest{Options: struct{}{}})
+	require.Nil(t, res)
+	require.Error(t, err)
+	assert.ErrorContains(t, err, "postgres.Retrieve options have type")
+}
+
+func TestRetriever_Fail_EmbedReturnError(t *testing.T) {
+	ds := docStore{
+		config: &Config{Embedder: mockEmbedderFail{}},
+	}
+	res, err := ds.Retrieve(context.Background(), &ai.RetrieverRequest{})
+	require.Nil(t, res)
+	require.Error(t, err)
+}


Reviews:
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-12T21:35:38Z
user: login: davidnastasi
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-12T22:45:34Z
user: login: davidnastasi
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-12T22:47:06Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-13T21:10:11Z
user: login: davidnastasi
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-14T18:04:16Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-14T21:12:16Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-19T20:29:11Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-19T20:29:33Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-19T20:32:13Z
user: login: davidnastasi
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-20T17:36:17Z
user: login: davidnastasi
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-20T17:43:36Z
user: login: davidnastasi
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-20T18:27:44Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-21T19:00:48Z
user: login: averikitsch
body: Add copyright to add test files like:
```
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// You may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-21T19:04:12Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-21T19:07:24Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-21T19:07:33Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-21T19:08:11Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-21T19:10:05Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-21T19:10:27Z
user: login: averikitsch
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-21T19:10:35Z
user: login: averikitsch
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-28T18:52:55Z

Review comments:
diff_hunk: @@ -2,16 +2,136 @@ package postgresql
 
 import (
 	"context"
+	"fmt"
+	"slices"
+	"strconv"
+	"strings"
 
 	"github.com/firebase/genkit/go/ai"
 )
 
 // RetrieverOptions options for retriever
 type RetrieverOptions struct {
+	Filter           any
+	K                int
+	DistanceStrategy DistanceStrategy
 }
 
 // Retrieve returns the result of the query
 func (ds *docStore) Retrieve(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
-	// TODO: implement
-	return nil, nil
+	if req.Options == nil {
+		req.Options = &RetrieverOptions{
+			Filter:           nil,
+			K:                defaultCount,
+			DistanceStrategy: defaultDistanceStrategy,
+		}
+	}
+
+	ropt, ok := req.Options.(*RetrieverOptions)
+	if !ok {
+		return nil, fmt.Errorf("postgres.Retrieve options have type %T, want %T", req.Options, &RetrieverOptions{})
+	}
+
+	ereq := &ai.EmbedRequest{
+		Documents: []*ai.Document{req.Query},
+		Options:   ds.config.EmbedderOptions,
+	}
+	eres, err := ds.config.Embedder.Embed(ctx, ereq)
+	if err != nil {
+		return nil, fmt.Errorf("postgres.Retrieve retrieve embedding failed: %v", err)
+	}
+	res, err := ds.query(ctx, ropt, eres.Embeddings[0].Embedding)
+	if err != nil {
+		return nil, fmt.Errorf("googlecloudsql.postgres.Retrieve failed to execute the query: %v", err)
+	}
+	return res, nil
+}
+
+func (ds *docStore) query(ctx context.Context, ropt *RetrieverOptions, embbeding []float32) (*ai.RetrieverResponse, error) {
+	res := &ai.RetrieverResponse{}
+
+	query := ds.buildQuery(ropt, embbeding)
+	rows, err := ds.engine.Pool.Query(ctx, query)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	fieldDescriptions := rows.FieldDescriptions()
+	columnNames := make([]string, len(fieldDescriptions))
+
+	for i, fieldDescription := range fieldDescriptions {
+		columnNames[i] = fieldDescription.Name
+	}
+
+	for rows.Next() {
+		values := make([]interface{}, len(columnNames))
+		valuesPrt := make([]interface{}, len(columnNames))
+
+		for i := range columnNames {
+			valuesPrt[i] = &values[i]
+		}
+		if err := rows.Scan(valuesPrt...); err != nil {
+			return nil, fmt.Errorf("scan row failed: %v", err)
+		}
+
+		meta := make(map[string]any, ropt.K)
+		var content []*ai.Part
+		for i, col := range columnNames {
+			if (len(ds.config.MetadataColumns) > 0 && !slices.Contains(ds.config.MetadataColumns, col)) &&
+				ds.config.ContentColumn != col &&
+				ds.config.MetadataJSONColumn != col {
+				continue
+			}
+
+			if ds.config.ContentColumn == col {
+				content = append(content, ai.NewTextPart(values[i].(string)))
+			}
+
+			if ds.config.MetadataJSONColumn == col {
+				content = append(content, ai.NewJSONPart(values[i].(string)))
+				continue
+			}
+
+			meta[col] = values[i]
+		}
+
+		doc := &ai.Document{
+			Metadata: meta,
+			Content:  content,
+		}
+
+		res.Documents = append(res.Documents, doc)
+	}
+
+	return res, nil
+}
+
+func (ds *docStore) buildQuery(ropt *RetrieverOptions, embedding []float32) string {
+
+	vectorToString := func(v []float32) string {
path: go/plugins/postgresql/retriever.go
user: login: averikitsch
body: please use pgvector library like langchaingo https://github.com/averikitsch/langchaingo/blob/main/vectorstores/cloudsql/vectorstore.go
created_at: 2025-05-12T21:24:19Z
author_association: OWNER
diff_hunk: @@ -2,16 +2,136 @@ package postgresql
 
 import (
 	"context"
+	"fmt"
+	"slices"
+	"strconv"
+	"strings"
 
 	"github.com/firebase/genkit/go/ai"
 )
 
 // RetrieverOptions options for retriever
 type RetrieverOptions struct {
+	Filter           any
+	K                int
+	DistanceStrategy DistanceStrategy
 }
 
 // Retrieve returns the result of the query
 func (ds *docStore) Retrieve(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
-	// TODO: implement
-	return nil, nil
+	if req.Options == nil {
+		req.Options = &RetrieverOptions{
+			Filter:           nil,
+			K:                defaultCount,
+			DistanceStrategy: defaultDistanceStrategy,
+		}
+	}
+
+	ropt, ok := req.Options.(*RetrieverOptions)
+	if !ok {
+		return nil, fmt.Errorf("postgres.Retrieve options have type %T, want %T", req.Options, &RetrieverOptions{})
+	}
+
+	ereq := &ai.EmbedRequest{
+		Documents: []*ai.Document{req.Query},
+		Options:   ds.config.EmbedderOptions,
+	}
+	eres, err := ds.config.Embedder.Embed(ctx, ereq)
+	if err != nil {
+		return nil, fmt.Errorf("postgres.Retrieve retrieve embedding failed: %v", err)
+	}
+	res, err := ds.query(ctx, ropt, eres.Embeddings[0].Embedding)
+	if err != nil {
+		return nil, fmt.Errorf("googlecloudsql.postgres.Retrieve failed to execute the query: %v", err)
+	}
+	return res, nil
+}
+
+func (ds *docStore) query(ctx context.Context, ropt *RetrieverOptions, embbeding []float32) (*ai.RetrieverResponse, error) {
+	res := &ai.RetrieverResponse{}
+
+	query := ds.buildQuery(ropt, embbeding)
+	rows, err := ds.engine.Pool.Query(ctx, query)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	fieldDescriptions := rows.FieldDescriptions()
+	columnNames := make([]string, len(fieldDescriptions))
+
+	for i, fieldDescription := range fieldDescriptions {
+		columnNames[i] = fieldDescription.Name
+	}
+
+	for rows.Next() {
+		values := make([]interface{}, len(columnNames))
+		valuesPrt := make([]interface{}, len(columnNames))
+
+		for i := range columnNames {
+			valuesPrt[i] = &values[i]
+		}
+		if err := rows.Scan(valuesPrt...); err != nil {
+			return nil, fmt.Errorf("scan row failed: %v", err)
+		}
+
+		meta := make(map[string]any, ropt.K)
+		var content []*ai.Part
+		for i, col := range columnNames {
+			if (len(ds.config.MetadataColumns) > 0 && !slices.Contains(ds.config.MetadataColumns, col)) &&
+				ds.config.ContentColumn != col &&
+				ds.config.MetadataJSONColumn != col {
+				continue
+			}
+
+			if ds.config.ContentColumn == col {
+				content = append(content, ai.NewTextPart(values[i].(string)))
+			}
+
+			if ds.config.MetadataJSONColumn == col {
path: go/plugins/postgresql/retriever.go
user: login: averikitsch
body: MetadataJSON should be the base of the metadata dict. See https://github.com/averikitsch/langchaingo/blob/ac70ea0e7d0b8ee8b9ba2c9bf2631aa8bbb329f6/vectorstores/cloudsql/vectorstore.go#L218
created_at: 2025-05-12T21:28:13Z
author_association: OWNER
diff_hunk: @@ -2,11 +2,100 @@ package postgresql
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
 
 	"github.com/firebase/genkit/go/ai"
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5"
+	"github.com/pgvector/pgvector-go"
 )
 
-func (ds *docStore) Index(context.Context, *ai.IndexerRequest) error {
-	// TODO: implement
+// Index implements the genkit.Index method.
+func (ds *docStore) Index(ctx context.Context, req *ai.IndexerRequest) error {
+	if len(req.Documents) == 0 {
+		return nil
+	}
+
+	ereq := &ai.EmbedRequest{
+		Documents: req.Documents,
+		Options:   ds.config.EmbedderOptions,
+	}
+	eres, err := ds.config.Embedder.Embed(ctx, ereq)
+	if err != nil {
+		return fmt.Errorf("postgres.Indexer index embedding failed: %v", err)
+	}
+
+	b := &pgx.Batch{}
+
+	for i, doc := range req.Documents {
+		// if no metadata provided, initialize with empty map
+		if doc.Metadata == nil {
+			doc.Metadata = make(map[string]any)
+		}
+
+		// generate the id if it's not defined
+		_, okString := doc.Metadata[ds.config.IDColumn].(string)
+		_, okBytes := doc.Metadata[ds.config.IDColumn].([]byte) // represents the uuid
path: go/plugins/postgresql/indexer.go
user: login: averikitsch
body: Can we not get the UUID as a string?
created_at: 2025-05-12T21:34:46Z
author_association: OWNER
diff_hunk: @@ -2,16 +2,136 @@ package postgresql
 
 import (
 	"context"
+	"fmt"
+	"slices"
+	"strconv"
+	"strings"
 
 	"github.com/firebase/genkit/go/ai"
 )
 
 // RetrieverOptions options for retriever
 type RetrieverOptions struct {
+	Filter           any
+	K                int
+	DistanceStrategy DistanceStrategy
 }
 
 // Retrieve returns the result of the query
 func (ds *docStore) Retrieve(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
-	// TODO: implement
-	return nil, nil
+	if req.Options == nil {
+		req.Options = &RetrieverOptions{
+			Filter:           nil,
+			K:                defaultCount,
+			DistanceStrategy: defaultDistanceStrategy,
+		}
+	}
+
+	ropt, ok := req.Options.(*RetrieverOptions)
+	if !ok {
+		return nil, fmt.Errorf("postgres.Retrieve options have type %T, want %T", req.Options, &RetrieverOptions{})
+	}
+
+	ereq := &ai.EmbedRequest{
+		Documents: []*ai.Document{req.Query},
+		Options:   ds.config.EmbedderOptions,
+	}
+	eres, err := ds.config.Embedder.Embed(ctx, ereq)
+	if err != nil {
+		return nil, fmt.Errorf("postgres.Retrieve retrieve embedding failed: %v", err)
+	}
+	res, err := ds.query(ctx, ropt, eres.Embeddings[0].Embedding)
+	if err != nil {
+		return nil, fmt.Errorf("googlecloudsql.postgres.Retrieve failed to execute the query: %v", err)
+	}
+	return res, nil
+}
+
+func (ds *docStore) query(ctx context.Context, ropt *RetrieverOptions, embbeding []float32) (*ai.RetrieverResponse, error) {
+	res := &ai.RetrieverResponse{}
+
+	query := ds.buildQuery(ropt, embbeding)
+	rows, err := ds.engine.Pool.Query(ctx, query)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	fieldDescriptions := rows.FieldDescriptions()
+	columnNames := make([]string, len(fieldDescriptions))
+
+	for i, fieldDescription := range fieldDescriptions {
+		columnNames[i] = fieldDescription.Name
+	}
+
+	for rows.Next() {
+		values := make([]interface{}, len(columnNames))
+		valuesPrt := make([]interface{}, len(columnNames))
+
+		for i := range columnNames {
+			valuesPrt[i] = &values[i]
+		}
+		if err := rows.Scan(valuesPrt...); err != nil {
+			return nil, fmt.Errorf("scan row failed: %v", err)
+		}
+
+		meta := make(map[string]any, ropt.K)
+		var content []*ai.Part
+		for i, col := range columnNames {
+			if (len(ds.config.MetadataColumns) > 0 && !slices.Contains(ds.config.MetadataColumns, col)) &&
+				ds.config.ContentColumn != col &&
+				ds.config.MetadataJSONColumn != col {
+				continue
+			}
+
+			if ds.config.ContentColumn == col {
+				content = append(content, ai.NewTextPart(values[i].(string)))
+			}
+
+			if ds.config.MetadataJSONColumn == col {
+				content = append(content, ai.NewJSONPart(values[i].(string)))
+				continue
+			}
+
+			meta[col] = values[i]
+		}
+
+		doc := &ai.Document{
+			Metadata: meta,
+			Content:  content,
+		}
+
+		res.Documents = append(res.Documents, doc)
+	}
+
+	return res, nil
+}
+
+func (ds *docStore) buildQuery(ropt *RetrieverOptions, embedding []float32) string {
+
+	vectorToString := func(v []float32) string {
path: go/plugins/postgresql/retriever.go
user: login: davidnastasi
body: I've just realized today and I changed it in PR for the e2e test :) 
created_at: 2025-05-12T22:45:34Z
author_association: NONE
diff_hunk: @@ -2,11 +2,100 @@ package postgresql
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
 
 	"github.com/firebase/genkit/go/ai"
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5"
+	"github.com/pgvector/pgvector-go"
 )
 
-func (ds *docStore) Index(context.Context, *ai.IndexerRequest) error {
-	// TODO: implement
+// Index implements the genkit.Index method.
+func (ds *docStore) Index(ctx context.Context, req *ai.IndexerRequest) error {
+	if len(req.Documents) == 0 {
+		return nil
+	}
+
+	ereq := &ai.EmbedRequest{
+		Documents: req.Documents,
+		Options:   ds.config.EmbedderOptions,
+	}
+	eres, err := ds.config.Embedder.Embed(ctx, ereq)
+	if err != nil {
+		return fmt.Errorf("postgres.Indexer index embedding failed: %v", err)
+	}
+
+	b := &pgx.Batch{}
+
+	for i, doc := range req.Documents {
+		// if no metadata provided, initialize with empty map
+		if doc.Metadata == nil {
+			doc.Metadata = make(map[string]any)
+		}
+
+		// generate the id if it's not defined
+		_, okString := doc.Metadata[ds.config.IDColumn].(string)
+		_, okBytes := doc.Metadata[ds.config.IDColumn].([]byte) // represents the uuid
path: go/plugins/postgresql/indexer.go
user: login: davidnastasi
body: Do you mean as type UUID and use the String() method? 
created_at: 2025-05-12T22:47:05Z
author_association: NONE
diff_hunk: @@ -2,11 +2,100 @@ package postgresql
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
 
 	"github.com/firebase/genkit/go/ai"
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5"
+	"github.com/pgvector/pgvector-go"
 )
 
-func (ds *docStore) Index(context.Context, *ai.IndexerRequest) error {
-	// TODO: implement
+// Index implements the genkit.Index method.
+func (ds *docStore) Index(ctx context.Context, req *ai.IndexerRequest) error {
+	if len(req.Documents) == 0 {
+		return nil
+	}
+
+	ereq := &ai.EmbedRequest{
+		Documents: req.Documents,
+		Options:   ds.config.EmbedderOptions,
+	}
+	eres, err := ds.config.Embedder.Embed(ctx, ereq)
+	if err != nil {
+		return fmt.Errorf("postgres.Indexer index embedding failed: %v", err)
+	}
+
+	b := &pgx.Batch{}
+
+	for i, doc := range req.Documents {
+		// if no metadata provided, initialize with empty map
+		if doc.Metadata == nil {
+			doc.Metadata = make(map[string]any)
+		}
+
+		// generate the id if it's not defined
+		_, okString := doc.Metadata[ds.config.IDColumn].(string)
+		_, okBytes := doc.Metadata[ds.config.IDColumn].([]byte) // represents the uuid
path: go/plugins/postgresql/indexer.go
user: login: averikitsch
body: Why do we need a string and bytes support?
created_at: 2025-05-13T21:10:11Z
author_association: OWNER
diff_hunk: @@ -2,11 +2,100 @@ package postgresql
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
 
 	"github.com/firebase/genkit/go/ai"
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5"
+	"github.com/pgvector/pgvector-go"
 )
 
-func (ds *docStore) Index(context.Context, *ai.IndexerRequest) error {
-	// TODO: implement
+// Index implements the genkit.Index method.
+func (ds *docStore) Index(ctx context.Context, req *ai.IndexerRequest) error {
+	if len(req.Documents) == 0 {
+		return nil
+	}
+
+	ereq := &ai.EmbedRequest{
+		Documents: req.Documents,
+		Options:   ds.config.EmbedderOptions,
+	}
+	eres, err := ds.config.Embedder.Embed(ctx, ereq)
+	if err != nil {
+		return fmt.Errorf("postgres.Indexer index embedding failed: %v", err)
+	}
+
+	b := &pgx.Batch{}
+
+	for i, doc := range req.Documents {
+		// if no metadata provided, initialize with empty map
+		if doc.Metadata == nil {
+			doc.Metadata = make(map[string]any)
+		}
+
+		// generate the id if it's not defined
+		_, okString := doc.Metadata[ds.config.IDColumn].(string)
+		_, okBytes := doc.Metadata[ds.config.IDColumn].([]byte) // represents the uuid
path: go/plugins/postgresql/indexer.go
user: login: davidnastasi
body: My understanding is that id can be a uuid or string and the uuid is received as bytes and not characters. 
created_at: 2025-05-14T18:04:16Z
author_association: NONE
diff_hunk: @@ -2,11 +2,100 @@ package postgresql
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
 
 	"github.com/firebase/genkit/go/ai"
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5"
+	"github.com/pgvector/pgvector-go"
 )
 
-func (ds *docStore) Index(context.Context, *ai.IndexerRequest) error {
-	// TODO: implement
+// Index implements the genkit.Index method.
+func (ds *docStore) Index(ctx context.Context, req *ai.IndexerRequest) error {
+	if len(req.Documents) == 0 {
+		return nil
+	}
+
+	ereq := &ai.EmbedRequest{
+		Documents: req.Documents,
+		Options:   ds.config.EmbedderOptions,
+	}
+	eres, err := ds.config.Embedder.Embed(ctx, ereq)
+	if err != nil {
+		return fmt.Errorf("postgres.Indexer index embedding failed: %v", err)
+	}
+
+	b := &pgx.Batch{}
+
+	for i, doc := range req.Documents {
+		// if no metadata provided, initialize with empty map
+		if doc.Metadata == nil {
+			doc.Metadata = make(map[string]any)
+		}
+
+		// generate the id if it's not defined
+		_, okString := doc.Metadata[ds.config.IDColumn].(string)
+		_, okBytes := doc.Metadata[ds.config.IDColumn].([]byte) // represents the uuid
path: go/plugins/postgresql/indexer.go
user: login: averikitsch
body: Can we retrieve the id as a string no matter the data type? 
created_at: 2025-05-14T21:12:16Z
author_association: OWNER
diff_hunk: @@ -42,7 +42,14 @@ steps:
       - name: "go"
         path: "/gopath"
     script: |
-      go test -race ./...
+      go test -race ./... \
+        -test-postgres_user=$DB_USER \
+        -test-postgres_password=$DB_PASSWORD \
+        -test-postgres-iam-email=$EMAIL \
+        -test-postgres-project-id=$PROJECT_ID
+        -test-postgres-region=$REGION
+        -test-postgres-instance=$INSTANCE_ID \
+        -test-postgres-database=$DATABASE_ID
path: go/plugins/postgresql/integration.cloudbuild.yaml
user: login: averikitsch
body: ```suggestion
        -test-postgres_user=$DB_USER \
        -test-postgres_password=$DB_PASSWORD \
        -test-postgres-iam-email=$EMAIL \
        -test-postgres-project-id=$PROJECT_ID \
        -test-postgres-region=$REGION \
        -test-postgres-instance=$INSTANCE_ID \
        -test-postgres-database=$DATABASE_ID
```
created_at: 2025-05-19T20:29:11Z
author_association: OWNER
diff_hunk: @@ -42,7 +42,14 @@ steps:
       - name: "go"
         path: "/gopath"
     script: |
-      go test -race ./...
+      go test -race ./... \
+        -test-postgres_user=$DB_USER \
+        -test-postgres_password=$DB_PASSWORD \
+        -test-postgres-iam-email=$EMAIL \
+        -test-postgres-project-id=$PROJECT_ID
+        -test-postgres-region=$REGION
+        -test-postgres-instance=$INSTANCE_ID \
+        -test-postgres-database=$DATABASE_ID
path: go/plugins/postgresql/integration.cloudbuild.yaml
user: login: averikitsch
body: Can we use only dashes instead of a mix of dashes and underscores? 
created_at: 2025-05-19T20:29:33Z
author_association: OWNER
path: go/plugins/postgresql/mocks.go
user: login: averikitsch
body: Can we add test to this filename
created_at: 2025-05-19T20:32:13Z
author_association: OWNER
diff_hunk: @@ -42,7 +42,14 @@ steps:
       - name: "go"
         path: "/gopath"
     script: |
-      go test -race ./...
+      go test -race ./... \
+        -test-postgres_user=$DB_USER \
+        -test-postgres_password=$DB_PASSWORD \
+        -test-postgres-iam-email=$EMAIL \
+        -test-postgres-project-id=$PROJECT_ID
+        -test-postgres-region=$REGION
+        -test-postgres-instance=$INSTANCE_ID \
+        -test-postgres-database=$DATABASE_ID
path: go/plugins/postgresql/integration.cloudbuild.yaml
user: login: davidnastasi
body: For sure. Other parts were using a mix. I also prefer to have consistency  
created_at: 2025-05-20T17:36:17Z
author_association: NONE
diff_hunk: @@ -42,7 +42,14 @@ steps:
       - name: "go"
         path: "/gopath"
     script: |
-      go test -race ./...
+      go test -race ./... \
+        -test-postgres_user=$DB_USER \
+        -test-postgres_password=$DB_PASSWORD \
+        -test-postgres-iam-email=$EMAIL \
+        -test-postgres-project-id=$PROJECT_ID
+        -test-postgres-region=$REGION
+        -test-postgres-instance=$INSTANCE_ID \
+        -test-postgres-database=$DATABASE_ID
path: go/plugins/postgresql/integration.cloudbuild.yaml
user: login: davidnastasi
body: Thanks!
created_at: 2025-05-20T17:43:35Z
author_association: NONE
path: go/plugins/postgresql/mocks.go
user: login: davidnastasi
body: IMHO for this particular file we don't get any benefits cuz it's just returning always the same values
created_at: 2025-05-20T18:27:44Z
author_association: NONE
diff_hunk: @@ -2,22 +2,257 @@ package postgresql
 
 import (
 	"context"
+	"flag"
+	"fmt"
+	"strings"
 	"testing"
 
+	"github.com/firebase/genkit/go/ai"
 	"github.com/firebase/genkit/go/genkit"
+	"github.com/firebase/genkit/go/internal/fakeembedder"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
-func TestInit_AlreadyCalled(t *testing.T) {
+const (
+	TestTable             = "test_embeddings"
+	SchemaName            = "test_schema"
path: go/plugins/postgresql/genkit_test.go
user: login: averikitsch
body: ```suggestion
	SchemaName            = "test"
```
created_at: 2025-05-21T19:00:47Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,125 @@
+# PostgreSQL plugin
+
+PostgreSQL plugin provides indexer and retriever implementations that use PostgreSQL with the pgvector extension for vector similarity search.
+
+
+## Configuration
+
+To use this plugin, first create a `PostgresEngine` instance:
+
+```go
+// Create PostgresEngine instance
+// with basic authentication
+pEngine, err := NewPostgresEngine(ctx,
+		WithUser('user'),
+		WithPassword('password'),
+		WithCloudSQLInstance('my-project', 'us-central1', 'my-instance'),
+		WithDatabase('my-database')
+
+// with email authentication
+pEngine, err := NewPostgresEngine(ctx,
+    WithUser('user'),
+    WithPassword('password'),
path: docs-go/plugins/postgresql.md
user: login: averikitsch
body: remove user and password for this example
created_at: 2025-05-21T19:07:24Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,125 @@
+# PostgreSQL plugin
+
+PostgreSQL plugin provides indexer and retriever implementations that use PostgreSQL with the pgvector extension for vector similarity search.
+
+
+## Configuration
+
+To use this plugin, first create a `PostgresEngine` instance:
+
+```go
+// Create PostgresEngine instance
+// with basic authentication
+pEngine, err := NewPostgresEngine(ctx,
+		WithUser('user'),
+		WithPassword('password'),
+		WithCloudSQLInstance('my-project', 'us-central1', 'my-instance'),
+		WithDatabase('my-database')
+
+// with email authentication
+pEngine, err := NewPostgresEngine(ctx,
+    WithUser('user'),
+    WithPassword('password'),
+    WithCloudSQLInstance('my-project', 'us-central1', 'my-instance'),
+    WithDatabase('my-database')
+
+// with custom pool
+pEngine, err := NewPostgresEngine(ctx,
+    WithUser("user"),
+    WithPassword("passw0rd"),
+    WithDatabase("db_test"),
+    WithPool(pool))
path: docs-go/plugins/postgresql.md
user: login: averikitsch
body: Can you add an example of the pool?
created_at: 2025-05-21T19:07:32Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,125 @@
+# PostgreSQL plugin
+
+PostgreSQL plugin provides indexer and retriever implementations that use PostgreSQL with the pgvector extension for vector similarity search.
+
+
+## Configuration
+
+To use this plugin, first create a `PostgresEngine` instance:
+
+```go
+// Create PostgresEngine instance
+// with basic authentication
+pEngine, err := NewPostgresEngine(ctx,
+		WithUser('user'),
+		WithPassword('password'),
+		WithCloudSQLInstance('my-project', 'us-central1', 'my-instance'),
+		WithDatabase('my-database')
+
+// with email authentication
+pEngine, err := NewPostgresEngine(ctx,
+    WithUser('user'),
+    WithPassword('password'),
+    WithCloudSQLInstance('my-project', 'us-central1', 'my-instance'),
+    WithDatabase('my-database')
+
+// with custom pool
+pEngine, err := NewPostgresEngine(ctx,
+    WithUser("user"),
+    WithPassword("passw0rd"),
+    WithDatabase("db_test"),
+    WithPool(pool))
+
+// Create the vector store table
+
+err = pEngine.InitVectorstoreTable(ctx, VectorstoreTableOptions{
+    TableName:          "documents",
+    VectorSize:         768,
+    SchemaName:         "public",
+    ContentColumnName:  "content",
+    EmbeddingColumn:    "embedding",
+    MetadataJSONColumn: "custom_metadata",
+    IDColumn: Column{
+      Name:     custom_id,
+      Nullable: false,
+    },
+    MetadataColumns: []Column{
+      {
+        Name:     "source",
+        DataType: "text",
+        Nullable: true,
+      },
+      {
+        Name:     "category",
+        DataType: "text",
+        Nullable: true,
+      },
+    },
+    OverwriteExisting: true,
+    StoreMetadata:     true,
+})
+```
+
+Then, specify the plugin when you initialize Genkit:
+
+```go
+	postgres := &Postgres{
+		engine: pEngine,
+	}
+
+	g, err := genkit.Init(ctx, genkit.WithPlugins(postgres))
+
+// To use the table you configured when you loaded the plugin:
+
+  cfg := &Config{
+    TableName:             'documents',
+    SchemaName:            'public',
+    ContentColumn:         "content",
+    EmbeddingColumn:       "embedding",
+    MetadataColumns:       []string{"source", "category"},
+    IDColumn:              "custom_id",
+    MetadataJSONColumn:    "custom_metadata",
+    Embedder:              embedder,
+    EmbedderOptions:       nil,
+  }
+
+  indexer, err := DefineIndexer(ctx, g, postgres, cfg)
+  if err != nil {
+    return err
+  }
+
+	d1 := ai.DocumentFromText( "The product features include..." , map[string]any{"source": "website", "category": "product-docs", "custom_id": "doc-123"})
+  err := ai.Index(ctx, indexer, ai.WithIndexerDocs(d1))
+  if err != nil {
+    return err
+  }
+
+// To retrieve from the configured table:
+retriever, err := DefineRetriever(ctx, g, postgres, cfg)
+if err != nil {
+  retrun err
+}
+
+d2 := ai.DocumentFromText( "The product features include..." , nil)
+
+resp, err := retriever.Retrieve(ctx, &ai.RetrieverRequest{
+    Query: d2,
+    k:5,
+    filter: "source='website' AND category='product-docs'"
+})
+
+if err != nil {
+    retrun err
+}
+
+```
+
+
+
+
+
+
+
+
path: docs-go/plugins/postgresql.md
user: login: averikitsch
body: remove extra lines
created_at: 2025-05-21T19:08:11Z
author_association: OWNER
path: go/plugins/postgresql/mocks.go
user: login: averikitsch
body: Can we add _test to this file name?
created_at: 2025-05-21T19:10:05Z
author_association: OWNER
path: js/plugins/cloud-sql-pg/package.json
user: login: averikitsch
body: remove this change
created_at: 2025-05-21T19:10:27Z
author_association: OWNER
path: js/pnpm-lock.yaml
user: login: averikitsch
body: remove this change
created_at: 2025-05-21T19:10:35Z
author_association: OWNER

PR comments:
user: login: averikitsch
created_at: 2025-05-14T14:39:54Z
body: Please make updates to https://github.com/averikitsch/genkit/pull/13 and ensure tests are passing
author_association: OWNER
user: login: averikitsch
created_at: 2025-05-16T21:02:10Z
body: /gcbrun
author_association: OWNER
user: login: averikitsch
created_at: 2025-05-16T23:30:35Z
body: /gcbrun
author_association: OWNER
user: login: averikitsch
created_at: 2025-05-19T16:52:59Z
body: /gcbrun
author_association: OWNER
user: login: averikitsch
created_at: 2025-05-19T20:22:11Z
body: /gcbrun
author_association: OWNER
user: login: averikitsch
created_at: 2025-05-20T20:46:47Z
body: /gcbrun
author_association: OWNER
user: login: averikitsch
created_at: 2025-05-20T22:20:19Z
body: /gcbrun
author_association: OWNER
user: login: averikitsch
created_at: 2025-05-21T19:01:08Z
body: /gcbrun
author_association: OWNER
user: login: averikitsch
created_at: 2025-05-28T18:51:01Z
body: /gcbrun
author_association: OWNER