From 5ea15f010e986f02e7df2ad2623636d8a7d72a50 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Wed, 27 Aug 2025 13:16:05 +0530 Subject: [PATCH 01/19] Fix handling of adding metadata in entries --- ballerina/vector_store.bal | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ballerina/vector_store.bal b/ballerina/vector_store.bal index 0ce54d4..b7f8a8d 100644 --- a/ballerina/vector_store.bal +++ b/ballerina/vector_store.bal @@ -66,27 +66,28 @@ public isolated class VectorStore { weaviate:Object[] objects = []; foreach ai:VectorEntry entry in entries.cloneReadOnly() { ai:Embedding embedding = entry.embedding; + weaviate:PropertySchema properties = entry.chunk.metadata !is () ? + check entry.chunk.metadata.cloneWithType() : {}; + properties[self.chunkFieldName] = entry.chunk.content; + properties["type"] = entry.chunk.'type; + if embedding is ai:Vector { objects.push({ 'class: self.config.collectionName, id: entry.id, vector: embedding, - properties: { - "type": entry.chunk.'type, - [self.chunkFieldName]: entry.chunk.content - } + properties }); } // TODO: Add support for sparse and hybrid embeddings // Weaviate does not support custom sparse or hybrid embeddings directly // Need to convert them to dense vectors before adding to Weaviate } - weaviate:ObjectsGetResponse[]|error result = self.weaviateClient->/batch/objects.post({ + weaviate:ObjectsGetResponse[] _ = check self.weaviateClient->/batch/objects.post({ objects }); - if result is error { - return error("Failed to add vector entries", result); - } + } on fail error err { + return error("Failed to query vector store", err); } } From 1b37bb6d6d9a2af330fd48508193987f80ba3fa9 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Wed, 27 Aug 2025 13:30:48 +0530 Subject: [PATCH 02/19] Add example to demonstrate vector store functionailities --- .../book-recommendation-system/Ballerina.toml | 8 ++ examples/book-recommendation-system/main.bal | 73 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 examples/book-recommendation-system/Ballerina.toml create mode 100644 examples/book-recommendation-system/main.bal diff --git a/examples/book-recommendation-system/Ballerina.toml b/examples/book-recommendation-system/Ballerina.toml new file mode 100644 index 0000000..84244c6 --- /dev/null +++ b/examples/book-recommendation-system/Ballerina.toml @@ -0,0 +1,8 @@ +[package] +org = "wso2" +name = "book_recommendation_system" +version = "0.1.0" +distribution = "2201.12.0" + +[build-options] +observabilityIncluded = true diff --git a/examples/book-recommendation-system/main.bal b/examples/book-recommendation-system/main.bal new file mode 100644 index 0000000..7dbb9c8 --- /dev/null +++ b/examples/book-recommendation-system/main.bal @@ -0,0 +1,73 @@ +// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). +// +// WSO2 LLC. licenses this file to you 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. + +import ballerina/ai; +import ballerina/io; +import ballerina/uuid; +import ballerinax/ai.weaviate; + +configurable string serviceUrl = ?; +configurable string collectionName = ?; +configurable string token = ?; + +public function main() returns error? { + weaviate:VectorStore vectorStore = check new (serviceUrl, { + collectionName + }, { + auth: { + token + } + }); + + // This is the embedding of a sample book entry + ai:Vector bookEmbedding = [0.1, 0.2, 0.3]; + + ai:Error? addResult = vectorStore.add([ + { + id: uuid:createRandomUuid(), + embedding: bookEmbedding, + chunk: { + 'type: "text", + content: "A Game of Thrones", + metadata: { + "genre": "Fantasy" + } + } + } + ]); + + if addResult is ai:Error { + io:println("Error occurred while adding an entry to the vector store", addResult); + return; + } + + // This is the embedding of the search query. It should use the same model as the embedding of the book entries. + ai:Vector searchEmbedding = [0.05, 0.1, 0.15]; + + ai:VectorMatch[] query = check vectorStore.query({ + embedding: searchEmbedding, + filters: { + filters: [ + { + 'key: "genre", + operator: ai:EQUAL, + value: "Fantasy" + } + ] + } + }); + io:println("Query Results: ", query); +} From 0dc48c77d4538311c95ff1fd2f5fdb15f84da0f6 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Wed, 27 Aug 2025 13:31:16 +0530 Subject: [PATCH 03/19] Update build files to build and run the examples --- build.gradle | 1 + examples/build.gradle | 76 +++++++++++++++++++++++++++++++++++++++++++ examples/build.sh | 55 +++++++++++++++++++++++++++++++ settings.gradle | 2 ++ 4 files changed, 134 insertions(+) create mode 100644 examples/build.gradle create mode 100755 examples/build.sh diff --git a/build.gradle b/build.gradle index 6587c2b..f24e791 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,7 @@ def moduleVersion = project.version.replace("-SNAPSHOT", "") task build { dependsOn(':ai.weaviate-ballerina:build') + dependsOn(':ai.weaviate-examples:build') } release { diff --git a/examples/build.gradle b/examples/build.gradle new file mode 100644 index 0000000..659c91d --- /dev/null +++ b/examples/build.gradle @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you 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. + */ + +import org.apache.tools.ant.taskdefs.condition.Os + +apply plugin: 'java' + +def graalvmFlag = "" + +task testExamples { + if (project.hasProperty("balGraalVMTest")) { + graalvmFlag = "--graalvm" + } + doLast { + try { + exec { + workingDir project.projectDir + println("Working dir: ${workingDir}") + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine 'sh', "/c", "chmod +x ./build.sh && ./build.sh run && exit %%ERRORLEVEL%%" + } else { + commandLine 'sh', "-c", "chmod +x ./build.sh && ./build.sh run" + } + } + } catch (Exception e) { + println("Example Build failed: " + e.message) + throw e + } + } +} + +task buildExamples { + gradle.taskGraph.whenReady { graph -> + if (graph.hasTask(":ai.weaviate-ballerina:test")) { + buildExamples.enabled = false + } else { + testExamples.enabled = false + } + } + doLast { + try { + exec { + workingDir project.projectDir + println("Working dir: ${workingDir}") + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine 'sh', "/c", "chmod +x ./build.sh && ./build.sh build && exit %%ERRORLEVEL%%" + } else { + commandLine 'sh', "-c", "chmod +x ./build.sh && ./build.sh build" + } + } + } catch (Exception e) { + println("Example Build failed: " + e.message) + throw e + } + } +} + +buildExamples.dependsOn ":ai.weaviate-ballerina:build" +testExamples.dependsOn ":ai.weaviate-ballerina:build" + +build.dependsOn buildExamples diff --git a/examples/build.sh b/examples/build.sh new file mode 100755 index 0000000..e2659d3 --- /dev/null +++ b/examples/build.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +BAL_EXAMPLES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BAL_CENTRAL_DIR="$HOME/.ballerina/repositories/central.ballerina.io/" +BAL_HOME_DIR="$BAL_EXAMPLES_DIR/../ballerina" + +set -e + +case "$1" in +build) + BAL_CMD="build" + ;; +run) + BAL_CMD="run" + ;; +*) + echo "Invalid command provided: '$1'. Please provide 'build' or 'run' as the command." + exit 1 + ;; +esac + +# Read Ballerina package name +BAL_PACKAGE_NAME=$(awk -F'"' '/^name/ {print $2}' "$BAL_HOME_DIR/Ballerina.toml") + +# Push the package to the local repository +cd "$BAL_HOME_DIR" && + bal pack && + bal push --repository=local + +# Remove the cache directories in the repositories +rm -rf "$BAL_CENTRAL_DIR/cache-*" + +# Update the central repository +BAL_DESTINATION_DIR="$HOME/.ballerina/repositories/central.ballerina.io/bala/ballerinax" +BAL_SOURCE_DIR="$HOME/.ballerina/repositories/local/bala/ballerinax/$BAL_PACKAGE_NAME" + +mkdir -p "$BAL_DESTINATION_DIR" + +[ -d "$BAL_SOURCE_DIR" ] && cp -r "$BAL_SOURCE_DIR" "$BAL_DESTINATION_DIR" +echo "Successfully updated the local central repositories" + +# Loop through examples in the examples directory +cd "$BAL_EXAMPLES_DIR" +for dir in $(find "$BAL_EXAMPLES_DIR" -type d -maxdepth 1 -mindepth 1); do + # Skip the build directory + if [[ "$dir" == *build ]]; then + continue + fi + (cd "$dir" && bal "$BAL_CMD" --offline && cd ..); +done + +# Remove generated JAR files +find "$BAL_HOME_DIR" -maxdepth 1 -type f -name "*.jar" | while read -r JAR_FILE; do + rm "$JAR_FILE" +done diff --git a/settings.gradle b/settings.gradle index 9a3be6b..164b737 100644 --- a/settings.gradle +++ b/settings.gradle @@ -33,8 +33,10 @@ plugins { rootProject.name = 'module-balerinax-ai.weaviate' include ':ai.weaviate-ballerina' +include ':ai.weaviate-examples' project(':ai.weaviate-ballerina').projectDir = file("ballerina") +project(':ai.weaviate-examples').projectDir = file("examples") gradleEnterprise { buildScan { From dd609173cd0b4d9adc8b8a9e837c2a8788ee44b6 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Wed, 27 Aug 2025 13:31:37 +0530 Subject: [PATCH 04/19] Include examples in docs --- README.md | 41 ++++--- ballerina/README.md | 9 +- examples/README.md | 52 +++++++++ .../book-recommendation-system.md | 107 ++++++++++++++++++ 4 files changed, 191 insertions(+), 18 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/book-recommendation-system/book-recommendation-system.md diff --git a/README.md b/README.md index 0e0813f..ee212a5 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,13 @@ import ballerinax/ai.weaviate; ```ballerina ai:VectorStore vectorStore = check new weaviate:VectorStore( - serviceUrl = "add-weaviate-service-url", - config = { - className: "add-collection-name" - }, - auth = { - token: "add-access-token" - } + serviceUrl = "add-weaviate-service-url", + config = { + collectionName: "add-collection-name" + }, + auth = { + token: "add-access-token" + } ); ``` @@ -80,19 +80,26 @@ ai:VectorStore vectorStore = check new weaviate:VectorStore( ```ballerina ai:Error? result = vectorStore.add( - [ - { - id: uuid:createRandomUuid(), - embedding: [1.0, 2.0, 3.0], - chunk: { - 'type: "text", - content: "This is a chunk" - } - } - ] + [ + { + id: uuid:createRandomUuid(), + embedding: [1.0, 2.0, 3.0], + chunk: { + 'type: "text", + content: "This is a chunk" + } + } + ] ); ``` +## Examples + +The Ballerina Weaviate vector store module provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/ballerina-platform/module-ballerinax-ai.weaviate/tree/main/examples). + +1. [Book Recommendation System](https://github.com/ballerina-platform/module-ballerinax-ai.weaviate/tree/main/examples/book-recommendation-system) + This example shows how to use Weaviate vector store APIs to implement a book recommendation system that stores book embeddings and queries them to find similar books based on vector similarity and metadata filtering. + ## Issues and projects Issues and Projects tabs are disabled for this repository as this is part of the Ballerina Library. To report bugs, request new features, start new discussions, view project boards, etc., go to the [Ballerina Library parent repository](https://github.com/ballerina-platform/ballerina-standard-library). diff --git a/ballerina/README.md b/ballerina/README.md index 1d8057d..6a4d1bf 100644 --- a/ballerina/README.md +++ b/ballerina/README.md @@ -63,7 +63,7 @@ import ballerinax/ai.weaviate; ai:VectorStore vectorStore = check new weaviate:VectorStore( serviceUrl = "add-weaviate-service-url", config = { - className: "add-collection-name" + collectionName: "add-collection-name" }, auth = { token: "add-access-token" @@ -87,3 +87,10 @@ ai:Error? result = vectorStore.add( ] ); ``` + +## Examples + +The Ballerina Weaviate vector store module provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/ballerina-platform/module-ballerinax-ai.weaviate/tree/main/examples). + +1. [Book Recommendation System](https://github.com/ballerina-platform/module-ballerinax-ai.weaviate/tree/main/examples/book-recommendation-system) + This example shows how to use Weaviate vector store APIs to implement a book recommendation system that stores book embeddings and queries them to find similar books based on vector similarity and metadata filtering. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..57175ee --- /dev/null +++ b/examples/README.md @@ -0,0 +1,52 @@ +## Examples + +The Ballerina Weaviate vector store module provides practical examples illustrating usage in various scenarios. Explore these [examples](https://github.com/ballerina-platform/module-ballerinax-ai.weaviate/tree/main/examples). + +1. [Book Recommendation System](https://github.com/ballerina-platform/module-ballerinax-ai.weaviate/tree/main/examples/book-recommendation-system) + This example shows how to use Weaviate vector store APIs to implement a book recommendation system that stores book embeddings and queries them to find similar books based on vector similarity and metadata filtering. + +## Prerequisites + +1. Follow the [instructions](https://github.com/ballerina-platform/module-ballerinax-ai.weaviate#set-up-guide) to set up the Weaviate cluster and obtain API credentials. + +2. For each example, create a `Config.toml` file with your Weaviate service URL, collection name, and API token. Here's an example of how your `Config.toml` file should look: + + ```toml + serviceUrl = "" + collectionName = "" + token = "" + ``` + +## Running an Example + +Execute the following commands to build an example from the source: + +* To build an example: + + ```bash + bal build + ``` + +* To run an example: + + ```bash + bal run + ``` + +## Building the Examples with the Local Module + +**Warning**: Due to the absence of support for reading local repositories for single Ballerina files, the Bala of the module is manually written to the central repository as a workaround. Consequently, the bash script may modify your local Ballerina repositories. + +Execute the following commands to build all the examples against the changes you have made to the module locally: + +* To build all the examples: + + ```bash + ./build.sh build + ``` + +* To run all the examples: + + ```bash + ./build.sh run + ``` diff --git a/examples/book-recommendation-system/book-recommendation-system.md b/examples/book-recommendation-system/book-recommendation-system.md new file mode 100644 index 0000000..eaad3ea --- /dev/null +++ b/examples/book-recommendation-system/book-recommendation-system.md @@ -0,0 +1,107 @@ +# Book recommendation system with Weaviate vector store + +This example demonstrates the use of the Ballerina Weaviate vector store module for building a book recommendation system. The system stores book embeddings and queries them to find similar books based on vector similarity and metadata filtering. + +## Step 1: Import the modules + +Import the required modules for AI operations, I/O operations, UUID generation, and Weaviate vector store. + +```ballerina +import ballerina/ai; +import ballerina/io; +import ballerina/uuid; +import ballerinax/ai.weaviate; +``` + +## Step 2: Configure the application + +Set up configurable variables for Weaviate connection parameters. + +```ballerina +configurable string serviceUrl = ?; +configurable string collectionName = ?; +configurable string token = ?; +``` + +## Step 3: Create a vector store instance + +Initialize the Weaviate vector store with your service URL, collection name, and authentication token. + +```ballerina +weaviate:VectorStore vectorStore = check new (serviceUrl, { + collectionName +}, { + auth: { + token + } +}); +``` + +Now, the `weaviate:VectorStore` instance can be used for storing and querying book embeddings. + +## Step 4: Add book embeddings to the vector store + +Store book information with their vector embeddings and metadata in the Weaviate vector store. + +```ballerina +// This is the embedding of a sample book entry +ai:Vector bookEmbedding = [0.1, 0.2, 0.3]; + +ai:Error? addResult = vectorStore.add([ + { + id: uuid:createRandomUuid(), + embedding: bookEmbedding, + chunk: { + 'type: "text", + content: "A Game of Thrones", + metadata: { + "genre": "Fantasy" + } + } + } +]); + +if addResult is ai:Error { + io:println("Error occurred while adding an entry to the vector store", addResult); + return; +} +``` + +## Step 5: Query for book recommendations + +Search for similar books using vector similarity and apply metadata filters to refine the results. + +```ballerina +// This is the embedding of the search query. It should use the same model as the embedding of the book entries. +ai:Vector searchEmbedding = [0.05, 0.1, 0.15]; + +ai:VectorMatch[] query = check vectorStore.query({ + embedding: searchEmbedding, + filters: { + filters: [ + { + 'key: "genre", + operator: ai:EQUAL, + value: "Fantasy" + } + ] + } +}); +io:println("Query Results: ", query); +``` + +## Step 6: Understanding the results + +The query results contain book recommendations with similarity scores and metadata. Each result includes: + +- **id**: Unique identifier for the book entry +- **embedding**: The vector representation of the book +- **chunk**: Contains the book information including type, content, and metadata +- **similarityScore**: How similar the book is to your search query (higher scores indicate better matches) + +The system can be extended to, + +- Add more books with richer metadata (author, publication year, ISBN, etc.) +- Use real embeddings from language models instead of sample vectors +- Implement more complex filtering logic +- Build a REST API around the recommendation system From 5655cb3086cc83b5f8fd73abd74138cf3ae4f4cf Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Wed, 27 Aug 2025 13:36:14 +0530 Subject: [PATCH 05/19] Add symlink for the example doc --- examples/book-recommendation-system/.github/README.md | 1 + 1 file changed, 1 insertion(+) create mode 120000 examples/book-recommendation-system/.github/README.md diff --git a/examples/book-recommendation-system/.github/README.md b/examples/book-recommendation-system/.github/README.md new file mode 120000 index 0000000..73de6ce --- /dev/null +++ b/examples/book-recommendation-system/.github/README.md @@ -0,0 +1 @@ +../book-recommendation-system.md \ No newline at end of file From 6392f45d659ad051b25de870a054ded94ec958b4 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Thu, 28 Aug 2025 21:25:39 +0530 Subject: [PATCH 06/19] Update query, delete APIs to support new changes --- ballerina/types.bal | 2 -- ballerina/vector_store.bal | 38 ++++++++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/ballerina/types.bal b/ballerina/types.bal index ed71265..439ef25 100644 --- a/ballerina/types.bal +++ b/ballerina/types.bal @@ -19,11 +19,9 @@ # # + collectionName - The name of the collection to use # + chunkFieldName - The name of the field to contain the chunk details -# + topK - The number of top similar vectors to return in queries public type Configuration record {| string collectionName; string chunkFieldName?; - int topK = 10; |}; type QueryResult record { diff --git a/ballerina/vector_store.bal b/ballerina/vector_store.bal index b7f8a8d..4043d96 100644 --- a/ballerina/vector_store.bal +++ b/ballerina/vector_store.bal @@ -47,7 +47,6 @@ public isolated class VectorStore { } self.weaviateClient = weaviateClient; self.config = config.cloneReadOnly(); - self.topK = self.config.topK; lock { string? chunkFieldName = self.config.cloneReadOnly().chunkFieldName; self.chunkFieldName = chunkFieldName is () ? "content" : chunkFieldName; @@ -93,15 +92,21 @@ public isolated class VectorStore { # Deletes a vector entry from the Weaviate vector store. # - # + id - The ID of the vector entry to delete + # + ids - ID/s of the vector entries to delete # + return - An `ai:Error` if the deletion fails, otherwise returns `()` - public isolated function delete(string id) returns ai:Error? { + public isolated function delete(string|string[] ids) returns ai:Error? { lock { string path = self.config.collectionName; - http:Response|error result = self.weaviateClient->/objects/[path]/[id].delete(); - if result is error { - return error("Failed to query vector store", result); + if ids is string[] { + foreach string id in ids.cloneReadOnly() { + ai:Error? result = deleteById(id, path, self.weaviateClient); + if result is error { + return result; + } + } + return; } + return deleteById(ids, path, self.weaviateClient); } } @@ -123,10 +128,12 @@ public isolated class VectorStore { string gqlQuery = string `{ Get { ${self.config.collectionName}( - limit: ${self.topK} + ${query.topK > -1 ? string `limit: ${query.topK}` : string ``} ${filterSection} - nearVector: { - vector: ${query.embedding.toJsonString()} + ${query.embedding !is () ? + string `nearVector: { + vector: ${query.embedding.toJsonString()} + }` : string `` } ) { content @@ -166,9 +173,7 @@ public isolated class VectorStore { }, similarityScore: element._additional.certainty }); - } on fail error err { - return error("Failed to parse vector store query", err); - } + } finalMatches = matches.cloneReadOnly(); } on fail error err { return error("Failed to query vector store", err); @@ -176,3 +181,12 @@ public isolated class VectorStore { return finalMatches; } } + +isolated function deleteById(string id, string path, weaviate:Client weaviateClient) returns ai:Error? { + lock { + http:Response|error result = weaviateClient->/objects/[path]/[id].delete(); + if result is error { + return error("Failed to query vector store", result); + } + } +} From f76fd5078a7ae97bc582f290142fd08e2460e0a5 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Thu, 28 Aug 2025 21:25:52 +0530 Subject: [PATCH 07/19] Remove unnecessary file --- ballerina/tests/mock_weaviate_service.bal | 76 ----------------------- 1 file changed, 76 deletions(-) delete mode 100644 ballerina/tests/mock_weaviate_service.bal diff --git a/ballerina/tests/mock_weaviate_service.bal b/ballerina/tests/mock_weaviate_service.bal deleted file mode 100644 index 67621a2..0000000 --- a/ballerina/tests/mock_weaviate_service.bal +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2025 WSO2 LLC (http://www.wso2.com). -// -// WSO2 LLC. licenses this file to you 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. - -import ballerina/http; -import ballerinax/weaviate; - -listener http:Listener weaviateListener = check new http:Listener(9090); - -service / on weaviateListener { - resource function delete objects/[string path]/[string id]() returns error? { - if id != "mock-id" { - return error("This is an error"); - } - } - - resource function post graphql(weaviate:GraphQLQuery payload) returns weaviate:GraphQLResponse|error { - return { - data: { - "Get": { - "Chunk": [ - { - _additional: { - certainty: 0.9999999403953552, - id: "e7d58e49-4ac6-45ca-a921-e6de941b4d99", - vector: [1, 2, 3] - }, - content: "This is a test chunk" - }, - { - _additional: { - certainty: 0.9999999403953552, - id: "121a8352-4cd6-4aea-8897-49c8357682cb", - vector: [1, 2, 3] - }, - content: "This is a test chunk" - } - ] - } - } - }; - } - - resource function post batch/objects(weaviate:Batch_objects_body payload, - string? consistency_level = ()) returns weaviate:ObjectsGetResponse[]|error { - return [ - { - deprecations: null, - result: { - status: "SUCCESS" - }, - 'class: "Chunk", - properties: { - "content": "This is a test chunk", - "type": "text" - }, - id: "mock-id", - creationTimeUnix: 1754499912863, - lastUpdateTimeUnix: 1754499912863, - vector: [1.0, 2.0, 3.0] - } - ]; - } -}; From 6cf7ea9b18a94f27da37361745b181303504bc3e Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Thu, 28 Aug 2025 21:26:22 +0530 Subject: [PATCH 08/19] Add multiple embeddings to the vector store example --- examples/book-recommendation-system/main.bal | 54 ++++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/examples/book-recommendation-system/main.bal b/examples/book-recommendation-system/main.bal index 7dbb9c8..5e38b90 100644 --- a/examples/book-recommendation-system/main.bal +++ b/examples/book-recommendation-system/main.bal @@ -23,6 +23,12 @@ configurable string serviceUrl = ?; configurable string collectionName = ?; configurable string token = ?; +type BookEntry record { + float[] embedding; + string title; + string genre; +}; + public function main() returns error? { weaviate:VectorStore vectorStore = check new (serviceUrl, { collectionName @@ -32,26 +38,42 @@ public function main() returns error? { } }); - // This is the embedding of a sample book entry - ai:Vector bookEmbedding = [0.1, 0.2, 0.3]; - - ai:Error? addResult = vectorStore.add([ + BookEntry[] entries = [ + { + title: "A Game of Thrones", + genre: "Fantasy", + embedding: [0.1011, 0.20012, 0.3024] + }, + { + title: "Crime And Punishment", + genre: "Literary fiction", + embedding: [0.98543, 0.347843, 0.845395] + }, { - id: uuid:createRandomUuid(), - embedding: bookEmbedding, - chunk: { - 'type: "text", - content: "A Game of Thrones", - metadata: { - "genre": "Fantasy" + title: "1984", + genre: "Science fiction", + embedding: [0.5645, 0.574, 0.3384] + } + ]; + + foreach BookEntry entry in entries { + ai:Error? addResult = vectorStore.add([ + { + id: uuid:createRandomUuid(), + embedding: entry.embedding, + chunk: { + 'type: "text", + content: entry.title, + metadata: { + "genre": entry.genre + } } } + ]); + if addResult is ai:Error { + io:println("Error occurred while adding an entry to the vector store", addResult); + return; } - ]); - - if addResult is ai:Error { - io:println("Error occurred while adding an entry to the vector store", addResult); - return; } // This is the embedding of the search query. It should use the same model as the embedding of the book entries. From 4c2b6cd72eebc600b782817393aa265fc1143a9c Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Sun, 31 Aug 2025 08:46:12 +0530 Subject: [PATCH 09/19] Apply review suggestions --- ballerina/types.bal | 2 +- ballerina/vector_store.bal | 26 +++++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/ballerina/types.bal b/ballerina/types.bal index 439ef25..98566c2 100644 --- a/ballerina/types.bal +++ b/ballerina/types.bal @@ -27,7 +27,7 @@ public type Configuration record {| type QueryResult record { record { string? id; - float certainty; + float? certainty; float[] vector; } _additional; string content?; diff --git a/ballerina/vector_store.bal b/ballerina/vector_store.bal index 4043d96..a85845d 100644 --- a/ballerina/vector_store.bal +++ b/ballerina/vector_store.bal @@ -17,6 +17,7 @@ import ballerina/ai; import ballerina/http; import ballerinax/weaviate; +import ballerina/log; # Weaviate Vector Store implementation with support for Dense, Sparse, and Hybrid vector search modes. # @@ -86,22 +87,28 @@ public isolated class VectorStore { objects }); } on fail error err { - return error("Failed to query vector store", err); + return error("failed to add entries to the vector store", err); } } # Deletes a vector entry from the Weaviate vector store. # - # + ids - ID/s of the vector entries to delete + # + ids - One or more identifiers of the vector entries to delete # + return - An `ai:Error` if the deletion fails, otherwise returns `()` public isolated function delete(string|string[] ids) returns ai:Error? { lock { string path = self.config.collectionName; if ids is string[] { - foreach string id in ids.cloneReadOnly() { - ai:Error? result = deleteById(id, path, self.weaviateClient); - if result is error { - return result; + transaction { + foreach string id in ids.cloneReadOnly() { + ai:Error? result = deleteById(id, path, self.weaviateClient); + if result is error { + log:printError(string `failed to delete vector entry with id: ${id}`, result); + } + } + error? commitResult = commit; + if commitResult is error { + return error("failed to delete vector entries", commitResult); } } return; @@ -171,12 +178,13 @@ public isolated class VectorStore { 'type: element.'type is () ? "" : check element.'type.cloneWithType(), content: element.content }, - similarityScore: element._additional.certainty + similarityScore: element._additional.certainty !is () ? + check element._additional.certainty.cloneWithType() : 0.0 }); } finalMatches = matches.cloneReadOnly(); } on fail error err { - return error("Failed to query vector store", err); + return error("failed to query vector store", err); } return finalMatches; } @@ -186,7 +194,7 @@ isolated function deleteById(string id, string path, weaviate:Client weaviateCli lock { http:Response|error result = weaviateClient->/objects/[path]/[id].delete(); if result is error { - return error("Failed to query vector store", result); + return error("failed to delete entry from the vector store", result); } } } From ae928ff60bf1ae401518c0e02dfe6a80458b259d Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Sun, 31 Aug 2025 08:46:41 +0530 Subject: [PATCH 10/19] Update test cases to validate query API using filters only --- ballerina/tests/test.bal | 1 - 1 file changed, 1 deletion(-) diff --git a/ballerina/tests/test.bal b/ballerina/tests/test.bal index c4ee5fa..2d7ed34 100644 --- a/ballerina/tests/test.bal +++ b/ballerina/tests/test.bal @@ -53,7 +53,6 @@ function testDeleteValuesFromVectorStore() returns error? { @test:Config {} function testQueryValuesFromVectorStore() returns error? { ai:VectorStoreQuery query = { - embedding: [1.0, 2.0, 3.0], filters: { filters: [ { From ad2898359536f755c33720488b968161ab29f959 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Sun, 31 Aug 2025 08:47:00 +0530 Subject: [PATCH 11/19] Fix license header --- examples/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/build.gradle b/examples/build.gradle index 659c91d..d592abf 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -1,7 +1,7 @@ /* - * Copyright (c) 2023, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com) * - * WSO2 Inc. licenses this file to you under the Apache License, + * WSO2 LLC. licenses this file to you 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 From ee836bd55998f28ddb0a0cdc27eeee730fe6fa39 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Sun, 31 Aug 2025 08:47:19 +0530 Subject: [PATCH 12/19] Update the code in the example --- examples/book-recommendation-system/main.bal | 28 +++++++++----------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/examples/book-recommendation-system/main.bal b/examples/book-recommendation-system/main.bal index 5e38b90..6834a75 100644 --- a/examples/book-recommendation-system/main.bal +++ b/examples/book-recommendation-system/main.bal @@ -56,24 +56,22 @@ public function main() returns error? { } ]; - foreach BookEntry entry in entries { - ai:Error? addResult = vectorStore.add([ - { - id: uuid:createRandomUuid(), - embedding: entry.embedding, - chunk: { - 'type: "text", - content: entry.title, - metadata: { - "genre": entry.genre - } + ai:Error? addResult = vectorStore.add(from BookEntry entry in entries + select { + id: uuid:createRandomUuid(), + embedding: entry.embedding, + chunk: { + 'type: "text", + content: entry.title, + metadata: { + "genre": entry.genre } } - ]); - if addResult is ai:Error { - io:println("Error occurred while adding an entry to the vector store", addResult); - return; } + ); + if addResult is ai:Error { + io:println("Error occurred while adding an entry to the vector store", addResult); + return; } // This is the embedding of the search query. It should use the same model as the embedding of the book entries. From 531644452e32d8031155363db61714d0c8a16681 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Sun, 31 Aug 2025 08:49:17 +0530 Subject: [PATCH 13/19] [Automated] Update the toml files --- ballerina/Dependencies.toml | 38 +++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index f4d924f..aea4db2 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -10,7 +10,7 @@ distribution-version = "2201.12.0" [[package]] org = "ballerina" name = "ai" -version = "1.1.4" +version = "1.5.0" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "data.jsondata"}, @@ -87,7 +87,7 @@ dependencies = [ [[package]] org = "ballerina" name = "data.xmldata" -version = "1.4.2" +version = "1.5.0" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "lang.object"} @@ -241,6 +241,14 @@ dependencies = [ {org = "ballerina", name = "lang.regexp"} ] +[[package]] +org = "ballerina" +name = "lang.transaction" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "jballerina.java"} +] + [[package]] org = "ballerina" name = "lang.value" @@ -259,6 +267,9 @@ dependencies = [ {org = "ballerina", name = "lang.value"}, {org = "ballerina", name = "observe"} ] +modules = [ + {org = "ballerina", packageName = "log", moduleName = "log"} +] [[package]] org = "ballerina" @@ -391,6 +402,27 @@ dependencies = [ {org = "ballerina", name = "observe"} ] +[[package]] +org = "ballerinai" +name = "transaction" +version = "0.0.0" +dependencies = [ + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "http"}, + {org = "ballerina", name = "io"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.runtime"}, + {org = "ballerina", name = "lang.transaction"}, + {org = "ballerina", name = "lang.value"}, + {org = "ballerina", name = "log"}, + {org = "ballerina", name = "task"}, + {org = "ballerina", name = "time"}, + {org = "ballerina", name = "uuid"} +] +modules = [ + {org = "ballerinai", packageName = "transaction", moduleName = "transaction"} +] + [[package]] org = "ballerinax" name = "ai.weaviate" @@ -398,8 +430,10 @@ version = "1.0.0" dependencies = [ {org = "ballerina", name = "ai"}, {org = "ballerina", name = "http"}, + {org = "ballerina", name = "log"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "uuid"}, + {org = "ballerinai", name = "transaction"}, {org = "ballerinax", name = "weaviate"} ] modules = [ From f163c39b323d2756d6345059be4a897fbb7b6dc0 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Sun, 31 Aug 2025 08:51:35 +0530 Subject: [PATCH 14/19] Apply review suggestion --- ballerina/vector_store.bal | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ballerina/vector_store.bal b/ballerina/vector_store.bal index a85845d..d29ce46 100644 --- a/ballerina/vector_store.bal +++ b/ballerina/vector_store.bal @@ -101,10 +101,7 @@ public isolated class VectorStore { if ids is string[] { transaction { foreach string id in ids.cloneReadOnly() { - ai:Error? result = deleteById(id, path, self.weaviateClient); - if result is error { - log:printError(string `failed to delete vector entry with id: ${id}`, result); - } + _ = check deleteById(id, path, self.weaviateClient); } error? commitResult = commit; if commitResult is error { From 3ece0b35e6f6c6323ae78f74ac6d2b0a77462572 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Sun, 31 Aug 2025 08:56:20 +0530 Subject: [PATCH 15/19] Validate having zero value as topK --- ballerina/vector_store.bal | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ballerina/vector_store.bal b/ballerina/vector_store.bal index d29ce46..e5117f1 100644 --- a/ballerina/vector_store.bal +++ b/ballerina/vector_store.bal @@ -17,7 +17,6 @@ import ballerina/ai; import ballerina/http; import ballerinax/weaviate; -import ballerina/log; # Weaviate Vector Store implementation with support for Dense, Sparse, and Hybrid vector search modes. # @@ -121,6 +120,9 @@ public isolated class VectorStore { public isolated function query(ai:VectorStoreQuery query) returns ai:VectorMatch[]|ai:Error { ai:VectorMatch[] finalMatches; lock { + if query.topK == 0 { + return error("Invalid value for topK. The value cannot be 0."); + } string filterSection = ""; if query.hasKey("filters") && query.filters is ai:MetadataFilters { ai:MetadataFilters? filters = query.cloneReadOnly().filters; From 9a711d1ebcd792ac86d00ab73432285a6cb886df Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Sun, 31 Aug 2025 08:58:05 +0530 Subject: [PATCH 16/19] [Automated] Update the toml files --- ballerina/Dependencies.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index aea4db2..cff9a3e 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -267,9 +267,6 @@ dependencies = [ {org = "ballerina", name = "lang.value"}, {org = "ballerina", name = "observe"} ] -modules = [ - {org = "ballerina", packageName = "log", moduleName = "log"} -] [[package]] org = "ballerina" @@ -430,7 +427,6 @@ version = "1.0.0" dependencies = [ {org = "ballerina", name = "ai"}, {org = "ballerina", name = "http"}, - {org = "ballerina", name = "log"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "uuid"}, {org = "ballerinai", name = "transaction"}, From 8700605a83a6172562ad406420757026528ad59f Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Sun, 31 Aug 2025 10:46:23 +0530 Subject: [PATCH 17/19] Update validation for topK values --- ballerina/vector_store.bal | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ballerina/vector_store.bal b/ballerina/vector_store.bal index e5117f1..69f8f15 100644 --- a/ballerina/vector_store.bal +++ b/ballerina/vector_store.bal @@ -59,7 +59,7 @@ public isolated class VectorStore { # + return - An `ai:Error` if the addition fails, otherwise returns `()` public isolated function add(ai:VectorEntry[] entries) returns ai:Error? { if entries.length() == 0 { - return; + return error("No entries provided to add to the vector store"); } lock { weaviate:Object[] objects = []; @@ -120,8 +120,8 @@ public isolated class VectorStore { public isolated function query(ai:VectorStoreQuery query) returns ai:VectorMatch[]|ai:Error { ai:VectorMatch[] finalMatches; lock { - if query.topK == 0 { - return error("Invalid value for topK. The value cannot be 0."); + if query.topK == 0 || query.topK < -1 { + return error("Invalid value for topK. The value cannot be 0 or less than -1."); } string filterSection = ""; if query.hasKey("filters") && query.filters is ai:MetadataFilters { From 0e784e09a341f3474faf702d8b3696f3e6ad9340 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Sun, 31 Aug 2025 10:46:35 +0530 Subject: [PATCH 18/19] Add test cases to validate error handling --- ballerina/tests/test.bal | 45 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/ballerina/tests/test.bal b/ballerina/tests/test.bal index 2d7ed34..99ef559 100644 --- a/ballerina/tests/test.bal +++ b/ballerina/tests/test.bal @@ -66,3 +66,48 @@ function testQueryValuesFromVectorStore() returns error? { ai:VectorMatch[]|ai:Error result = mockVectorStore.query(query); test:assertTrue(result !is error); } + +@test:Config {} +function testVectorStoreInitializationWithInvalidURL() returns error? { + VectorStore store = check new ( + serviceUrl = "invalid-url", + config = { + collectionName: "TestChunk" + }, + auth = { + token: "test-token" + } + ); + ai:VectorMatch[]|ai:Error result = store.query({ + topK: 0, + embedding: [1.0, 2.0, 3.0] + }); + test:assertTrue(result is ai:Error); +} + +@test:Config {} +function testAddEmptyVectorEntriesArray() returns error? { + ai:VectorEntry[] emptyEntries = []; + ai:Error? result = mockVectorStore.add(emptyEntries); + test:assertTrue(result is error, ""); +} + +@test:Config {} +function testQueryWithTopKZero() returns error? { + ai:VectorStoreQuery query = { + topK: 0, + embedding: [1.0, 2.0, 3.0] + }; + ai:VectorMatch[]|ai:Error result = mockVectorStore.query(query); + test:assertTrue(result is error); +} + +@test:Config {} +function testQueryWithNegativeTopK() returns error? { + ai:VectorStoreQuery query = { + topK: -5, + embedding: [1.0, 2.0, 3.0] + }; + ai:VectorMatch[]|ai:Error result = mockVectorStore.query(query); + test:assertTrue(result is error); +} From 5dcfce013a807d5f2e2a02cb44d06f447c455150 Mon Sep 17 00:00:00 2001 From: Nuvindu Date: Sun, 31 Aug 2025 11:06:45 +0530 Subject: [PATCH 19/19] Add test cases to validate the delete APIs --- ballerina/tests/test.bal | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/ballerina/tests/test.bal b/ballerina/tests/test.bal index 99ef559..c6c869a 100644 --- a/ballerina/tests/test.bal +++ b/ballerina/tests/test.bal @@ -28,11 +28,13 @@ final VectorStore mockVectorStore = check new ( } ); +string id = uuid:createRandomUuid(); + @test:Config {} function testAddingValuesToVectorStore() returns error? { ai:VectorEntry[] entries = [ { - id: uuid:createRandomUuid(), + id, embedding: [1.0, 2.0, 3.0], chunk: { 'type: "text", @@ -45,8 +47,26 @@ function testAddingValuesToVectorStore() returns error? { } @test:Config {} -function testDeleteValuesFromVectorStore() returns error? { - ai:Error? result = mockVectorStore.delete("mock-id"); +function testDeleteValueFromVectorStore() returns error? { + ai:Error? result = mockVectorStore.delete(id); + test:assertTrue(result !is error); +} + +@test:Config {} +function testDeleteMultipleValuesFromVectorStore() returns error? { + string index = uuid:createRandomUuid(); + ai:VectorEntry[] entries = [ + { + id: index, + embedding: [1.0, 2.0, 3.0], + chunk: { + 'type: "text", + content: "This is a test chunk" + } + } + ]; + _ = check mockVectorStore.add(entries); + ai:Error? result = mockVectorStore.delete([id, index]); test:assertTrue(result !is error); }