From 0d94b41530d1ead3e035b4281b71dc4032e36cfe Mon Sep 17 00:00:00 2001 From: duwenxin Date: Sun, 29 Mar 2026 23:28:39 -0400 Subject: [PATCH 1/4] feat(singlestore): implement semantic vector search via json params --- .ci/integration.cloudbuild.yaml | 6 +++-- .../singlestoreexecutesql.go | 2 +- .../singlestoresql/singlestoresql.go | 2 +- .../singlestore_integration_test.go | 22 +++++++++++++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.ci/integration.cloudbuild.yaml b/.ci/integration.cloudbuild.yaml index 6bf5a335c9cd..a7c8935b225a 100644 --- a/.ci/integration.cloudbuild.yaml +++ b/.ci/integration.cloudbuild.yaml @@ -1337,7 +1337,7 @@ steps: - "SINGLESTORE_USER=$_SINGLESTORE_USER" - "SINGLESTORE_DATABASE=$_SINGLESTORE_DATABASE" - "SERVICE_ACCOUNT_EMAIL=$SERVICE_ACCOUNT_EMAIL" - secretEnv: ["SINGLESTORE_PASSWORD", "SINGLESTORE_HOST", "CLIENT_ID"] + secretEnv: ["SINGLESTORE_PASSWORD", "SINGLESTORE_HOST", "CLIENT_ID", "API_KEY"] volumes: - name: "go" path: "/gopath" @@ -1351,7 +1351,9 @@ steps: .ci/test_with_coverage.sh \ "SingleStore" \ singlestore \ - singlestore + singlestore \ + "" \ + "API_KEY" else echo "No relevant changes for SingleStore. Skipping shard." exit 0 diff --git a/internal/tools/singlestore/singlestoreexecutesql/singlestoreexecutesql.go b/internal/tools/singlestore/singlestoreexecutesql/singlestoreexecutesql.go index c10e0e375e74..ee400ad6bf9f 100644 --- a/internal/tools/singlestore/singlestoreexecutesql/singlestoreexecutesql.go +++ b/internal/tools/singlestore/singlestoreexecutesql/singlestoreexecutesql.go @@ -126,7 +126,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para } func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { - return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, nil) + return parameters.EmbedParams(ctx, t.Parameters, paramValues, embeddingModelsMap, embeddingmodels.FormatVectorForPgvector) } func (t Tool) Manifest() tools.Manifest { diff --git a/internal/tools/singlestore/singlestoresql/singlestoresql.go b/internal/tools/singlestore/singlestoresql/singlestoresql.go index 3350390c7d66..dcfe6fda6f83 100644 --- a/internal/tools/singlestore/singlestoresql/singlestoresql.go +++ b/internal/tools/singlestore/singlestoresql/singlestoresql.go @@ -154,7 +154,7 @@ func (t Tool) Invoke(ctx context.Context, resourceMgr tools.SourceProvider, para } func (t Tool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) { - return parameters.EmbedParams(ctx, t.AllParams, paramValues, embeddingModelsMap, nil) + return parameters.EmbedParams(ctx, t.AllParams, paramValues, embeddingModelsMap, embeddingmodels.FormatVectorForPgvector) } func (t Tool) Manifest() tools.Manifest { diff --git a/tests/singlestore/singlestore_integration_test.go b/tests/singlestore/singlestore_integration_test.go index 3806f205dbe6..b9d2382863da 100644 --- a/tests/singlestore/singlestore_integration_test.go +++ b/tests/singlestore/singlestore_integration_test.go @@ -212,6 +212,14 @@ func TestSingleStoreToolEndpoints(t *testing.T) { tmplSelectCombined, tmplSelectFilterCombined := getSingleStoreTmplToolStatement() toolsFile = tests.AddTemplateParamConfig(t, toolsFile, SingleStoreToolType, tmplSelectCombined, tmplSelectFilterCombined, "") + insertStmt := `INSERT INTO senseai_docs (content, embedding) VALUES (?, ?)` + searchStmt := ` + SELECT content + FROM senseai_docs + ORDER BY DOT_PRODUCT(embedding, JSON_ARRAY_UNPACK(?)) DESC + LIMIT 1` + toolsFile = tests.AddSemanticSearchConfig(t, toolsFile, SingleStoreToolType, insertStmt, searchStmt) + cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %s", err) @@ -235,4 +243,18 @@ func TestSingleStoreToolEndpoints(t *testing.T) { tests.RunMCPToolCallMethod(t, mcpMyFailToolWant, mcpSelect1Want) tests.RunExecuteSqlToolInvokeTest(t, createTableStatement, select1Want) tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam) + + // Create table for semantic search + _, err = pool.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS senseai_docs (id INT AUTO_INCREMENT PRIMARY KEY, content TEXT, embedding JSON);") + if err != nil { + t.Fatalf("unable to create semantic search table: %s", err) + } + defer func() { + pool.ExecContext(ctx, "DROP TABLE IF EXISTS senseai_docs;") + }() + + // Semantic search tests + semanticInsertWant := `[{"":""}]` + semanticSearchWant := `[{"content":"The quick brown fox jumps over the lazy dog"}]` + tests.RunSemanticSearchToolInvokeTest(t, semanticInsertWant, semanticInsertWant, semanticSearchWant) } From 4a675a45ec9876a4ab8a58d89e9de08a866778d9 Mon Sep 17 00:00:00 2001 From: duwenxin99 Date: Tue, 7 Apr 2026 16:23:29 -0400 Subject: [PATCH 2/4] fix singlestore test --- tests/singlestore/singlestore_integration_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/singlestore/singlestore_integration_test.go b/tests/singlestore/singlestore_integration_test.go index b9d2382863da..7bcc25e6f3bc 100644 --- a/tests/singlestore/singlestore_integration_test.go +++ b/tests/singlestore/singlestore_integration_test.go @@ -214,10 +214,10 @@ func TestSingleStoreToolEndpoints(t *testing.T) { insertStmt := `INSERT INTO senseai_docs (content, embedding) VALUES (?, ?)` searchStmt := ` - SELECT content - FROM senseai_docs - ORDER BY DOT_PRODUCT(embedding, JSON_ARRAY_UNPACK(?)) DESC - LIMIT 1` +SELECT content +FROM senseai_docs +ORDER BY DOT_PRODUCT(embedding, JSON_ARRAY_UNPACK(?)) DESC +LIMIT 1` toolsFile = tests.AddSemanticSearchConfig(t, toolsFile, SingleStoreToolType, insertStmt, searchStmt) cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) From d118ce3716da9dddb666a373439a4e718ee354e3 Mon Sep 17 00:00:00 2001 From: duwenxin99 Date: Tue, 7 Apr 2026 16:52:23 -0400 Subject: [PATCH 3/4] fix tst --- .../singlestore_integration_test.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/singlestore/singlestore_integration_test.go b/tests/singlestore/singlestore_integration_test.go index 7bcc25e6f3bc..80f42d96b768 100644 --- a/tests/singlestore/singlestore_integration_test.go +++ b/tests/singlestore/singlestore_integration_test.go @@ -212,14 +212,9 @@ func TestSingleStoreToolEndpoints(t *testing.T) { tmplSelectCombined, tmplSelectFilterCombined := getSingleStoreTmplToolStatement() toolsFile = tests.AddTemplateParamConfig(t, toolsFile, SingleStoreToolType, tmplSelectCombined, tmplSelectFilterCombined, "") - insertStmt := `INSERT INTO senseai_docs (content, embedding) VALUES (?, ?)` - searchStmt := ` -SELECT content -FROM senseai_docs -ORDER BY DOT_PRODUCT(embedding, JSON_ARRAY_UNPACK(?)) DESC -LIMIT 1` + insertStmt := `INSERT INTO senseai_docs (content, embedding) VALUES (?, JSON_ARRAY_PACK(?))` + searchStmt := `SELECT content FROM senseai_docs ORDER BY DOT_PRODUCT(embedding, JSON_ARRAY_PACK(?)) DESC LIMIT 1` toolsFile = tests.AddSemanticSearchConfig(t, toolsFile, SingleStoreToolType, insertStmt, searchStmt) - cmd, cleanup, err := tests.StartCmd(ctx, toolsFile, args...) if err != nil { t.Fatalf("command initialization returned an error: %s", err) @@ -245,16 +240,19 @@ LIMIT 1` tests.RunToolInvokeWithTemplateParameters(t, tableNameTemplateParam) // Create table for semantic search - _, err = pool.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS senseai_docs (id INT AUTO_INCREMENT PRIMARY KEY, content TEXT, embedding JSON);") + _, err = pool.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS senseai_docs (id INT AUTO_INCREMENT PRIMARY KEY, content TEXT, embedding BLOB);") if err != nil { t.Fatalf("unable to create semantic search table: %s", err) } defer func() { - pool.ExecContext(ctx, "DROP TABLE IF EXISTS senseai_docs;") + _, err = pool.ExecContext(ctx, "DROP TABLE IF EXISTS senseai_docs;") + if err != nil { + t.Logf("Teardown failed: %s", err) + } }() // Semantic search tests - semanticInsertWant := `[{"":""}]` + semanticInsertWant := `null` semanticSearchWant := `[{"content":"The quick brown fox jumps over the lazy dog"}]` tests.RunSemanticSearchToolInvokeTest(t, semanticInsertWant, semanticInsertWant, semanticSearchWant) } From e11c1c7c3faa369e34e1d9526db4e4c4484de52b Mon Sep 17 00:00:00 2001 From: duwenxin99 Date: Tue, 7 Apr 2026 17:10:43 -0400 Subject: [PATCH 4/4] add doc --- .../singlestore/tools/singlestore-sql.md | 69 +++++++++++++++++++ .../singlestore_integration_test.go | 7 +- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/docs/en/integrations/singlestore/tools/singlestore-sql.md b/docs/en/integrations/singlestore/tools/singlestore-sql.md index 022707e9d7e1..3e68a66ac0ec 100644 --- a/docs/en/integrations/singlestore/tools/singlestore-sql.md +++ b/docs/en/integrations/singlestore/tools/singlestore-sql.md @@ -91,6 +91,75 @@ templateParameters: description: Table to select from ``` +### Example with Vector Search + +SingleStore supports vector operations. When using an `embeddingModel` with a `singlestore-sql` tool, the tool automatically converts text parameters into a JSON string array. You can then use SingleStore's `JSON_ARRAY_PACK()` function in your SQL statement to pack this string into a binary vector format (BLOB) for vector storage and similarity search. + +#### Define the Embedding Model +See [EmbeddingModels](../../../documentation/configuration/embedding-models/_index.md) for more information. + +```yaml +kind: embeddingModel +name: gemini-model +type: gemini +model: gemini-embedding-001 +apiKey: ${GOOGLE_API_KEY} +dimension: 768 +``` + +#### Vector Ingestion Tool +This tool stores both the raw text and its vector representation. It uses `valueFromParam` to hide the vector conversion logic from the LLM, ensuring the Agent only has to provide the content once. + +```yaml +kind: tool +name: insert_doc_singlestore +type: singlestore-sql +source: my-s2-source +statement: | + INSERT INTO vector_table (id, content, embedding) + VALUES (1, ?, JSON_ARRAY_PACK(?)) +description: | + Index new documents for semantic search in SingleStore. +parameters: + - name: content + type: string + description: The text content to store. + - name: text_to_embed + type: string + # Automatically copies 'content' and converts it to a vector string array + valueFromParam: content + embeddedBy: gemini-model +``` + +#### Vector Search Tool +This tool allows the Agent to perform a natural language search. The query string provided by the Agent is converted into a vector string array before the SQL is executed. + +```yaml +kind: tool +name: search_docs_singlestore +type: singlestore-sql +source: my-s2-source +statement: | + SELECT + id, + content, + DOT_PRODUCT(embedding, JSON_ARRAY_PACK(?)) AS score + FROM + vector_table + ORDER BY + score DESC + LIMIT 1 +description: | + Search for documents in SingleStore using natural language. + Returns the most semantically similar result. +parameters: + - name: query + type: string + description: The search query to be converted to a vector. + embeddedBy: gemini-model +``` + + ## Reference | **field** | **type** | **required** | **description** | diff --git a/tests/singlestore/singlestore_integration_test.go b/tests/singlestore/singlestore_integration_test.go index 80f42d96b768..38b8d45f0f29 100644 --- a/tests/singlestore/singlestore_integration_test.go +++ b/tests/singlestore/singlestore_integration_test.go @@ -252,7 +252,8 @@ func TestSingleStoreToolEndpoints(t *testing.T) { }() // Semantic search tests - semanticInsertWant := `null` - semanticSearchWant := `[{"content":"The quick brown fox jumps over the lazy dog"}]` - tests.RunSemanticSearchToolInvokeTest(t, semanticInsertWant, semanticInsertWant, semanticSearchWant) + httpSemanticInsertWant := `null` + mcpSemanticInsertWant := `` + semanticSearchWant := `The quick brown fox jumps over the lazy dog` + tests.RunSemanticSearchToolInvokeTest(t, httpSemanticInsertWant, mcpSemanticInsertWant, semanticSearchWant) }