Repository: sashabaranov/go-openai
Pull request #492 (2023-09-13 00:48:56)

Add DotProduct Method and README Example for Embedding Similarity Search
**Describe the change**
- Implement a DotProduct() method for the Embedding struct to calculate the dot product between two embeddings.
- Add a custom error type for vector length mismatch.

**Describe your solution**
- Finish implementing the following requested changes from @sashabaranov:

```
We don't need two huge examples [see](https://github.com/sashabaranov/go-openai/pull/274/files#r1232058195) — Please make one small example without encoding/gob etc.
I'm not sure this change is relevant [here](https://github.com/sashabaranov/go-openai/pull/274/files#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L596)
Let's make DotProduct a method of Embedding struct [see](https://github.com/sashabaranov/go-openai/pull/274/files#r1232242888)
```

**Tests**
- Add unit tests to validate the new DotProduct() method and error handling.

**Additional context**
- Update README.md with a complete example demonstrating how to perform an embedding similarity search for user queries.

Issue: #XXXX


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/492?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#492](https://app.codecov.io/gh/sashabaranov/go-openai/pull/492?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (e83dad1) into [master](https://app.codecov.io/gh/sashabaranov/go-openai/commit/3589837b229aeace205f312aa839bf73154e2820?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (3589837) will **increase** coverage by `0.15%`.
> Report is 1 commits behind head on master.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master     #492      +/-   ##
==========================================
+ Coverage   97.30%   97.46%   +0.15%     
==========================================
  Files          18       18              
  Lines         780      829      +49     
==========================================
+ Hits          759      808      +49     
  Misses         15       15              
  Partials        6        6              
```


| [Files Changed](https://app.codecov.io/gh/sashabaranov/go-openai/pull/492?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [embeddings.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/492?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZW1iZWRkaW5ncy5nbw==) | `100.00% <100.00%> (ø)` | |


sashabaranov: Thank you so much, this was coming for a long time :D 

Patch:
diff --git a/README.md b/README.md
index 440c409..c618cd7 100644
--- a/README.md
+++ b/README.md
@@ -483,6 +483,62 @@ func main() {
 ```
 </details>
 
+<detail>
+<summary>Embedding Semantic Similarity</summary>
+
+```go
+package main
+
+import (
+	"context"
+	"log"
+	openai "github.com/sashabaranov/go-openai"
+
+)
+
+func main() {
+	client := openai.NewClient("your-token")
+
+	// Create an EmbeddingRequest for the user query
+	queryReq := openai.EmbeddingRequest{
+		Input: []string{"How many chucks would a woodchuck chuck"},
+		Model: openai.AdaEmbeddingv2,
+	}
+
+	// Create an embedding for the user query
+	queryResponse, err := client.CreateEmbeddings(context.Background(), queryReq)
+	if err != nil {
+		log.Fatal("Error creating query embedding:", err)
+	}
+
+	// Create an EmbeddingRequest for the target text
+	targetReq := openai.EmbeddingRequest{
+		Input: []string{"How many chucks would a woodchuck chuck if the woodchuck could chuck wood"},
+		Model: openai.AdaEmbeddingv2,
+	}
+
+	// Create an embedding for the target text
+	targetResponse, err := client.CreateEmbeddings(context.Background(), targetReq)
+	if err != nil {
+		log.Fatal("Error creating target embedding:", err)
+	}
+
+	// Now that we have the embeddings for the user query and the target text, we
+	// can calculate their similarity.
+	queryEmbedding := queryResponse.Data[0]
+	targetEmbedding := targetResponse.Data[0]
+
+	similarity, err := queryEmbedding.DotProduct(&targetEmbedding)
+	if err != nil {
+		log.Fatal("Error calculating dot product:", err)
+	}
+
+	log.Printf("The similarity score between the query and the target is %f", similarity)
+}
+
+```
+</detail>
+
 <details>
 <summary>Azure OpenAI Embeddings</summary>
 
diff --git a/embeddings.go b/embeddings.go
index 5ba91f2..660bc24 100644
--- a/embeddings.go
+++ b/embeddings.go
@@ -4,10 +4,13 @@ import (
 	"context"
 	"encoding/base64"
 	"encoding/binary"
+	"errors"
 	"math"
 	"net/http"
 )
 
+var ErrVectorLengthMismatch = errors.New("vector length mismatch")
+
 // EmbeddingModel enumerates the models which can be used
 // to generate Embedding vectors.
 type EmbeddingModel int
@@ -124,6 +127,23 @@ type Embedding struct {
 	Index     int       `json:"index"`
 }
 
+// DotProduct calculates the dot product of the embedding vector with another
+// embedding vector. Both vectors must have the same length; otherwise, an
+// ErrVectorLengthMismatch is returned. The method returns the calculated dot
+// product as a float32 value.
+func (e *Embedding) DotProduct(other *Embedding) (float32, error) {
+	if len(e.Embedding) != len(other.Embedding) {
+		return 0, ErrVectorLengthMismatch
+	}
+
+	var dotProduct float32
+	for i := range e.Embedding {
+		dotProduct += e.Embedding[i] * other.Embedding[i]
+	}
+
+	return dotProduct, nil
+}
+
 // EmbeddingResponse is the response from a Create embeddings request.
 type EmbeddingResponse struct {
 	Object string         `json:"object"`
diff --git a/models.go b/models.go
index 560402e..c207f0a 100644
--- a/models.go
+++ b/models.go
@@ -33,6 +33,13 @@ type Permission struct {
 	IsBlocking         bool        `json:"is_blocking"`
 }
 
+// FineTuneModelDeleteResponse represents the deletion status of a fine-tuned model.
+type FineTuneModelDeleteResponse struct {
+	ID      string `json:"id"`
+	Object  string `json:"object"`
+	Deleted bool   `json:"deleted"`
+}
+
 // ModelsList is a list of models, including those that belong to the user or organization.
 type ModelsList struct {
 	Models []Model `json:"data"`
@@ -62,3 +69,16 @@ func (c *Client) GetModel(ctx context.Context, modelID string) (model Model, err
 	err = c.sendRequest(req, &model)
 	return
 }
+
+// DeleteFineTuneModel Deletes a fine-tune model. You must have the Owner
+// role in your organization to delete a model.
+func (c *Client) DeleteFineTuneModel(ctx context.Context, modelID string) (
+	response FineTuneModelDeleteResponse, err error) {
+	req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL("/models/"+modelID))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}


Test patch:
diff --git a/client_test.go b/client_test.go
index 9b50468..2c1d749 100644
--- a/client_test.go
+++ b/client_test.go
@@ -271,6 +271,9 @@ func TestClientReturnsRequestBuilderErrors(t *testing.T) {
 		{"GetModel", func() (any, error) {
 			return client.GetModel(ctx, "text-davinci-003")
 		}},
+		{"DeleteFineTuneModel", func() (any, error) {
+			return client.DeleteFineTuneModel(ctx, "")
+		}},
 	}
 
 	for _, testCase := range testCases {
diff --git a/embeddings_test.go b/embeddings_test.go
index 9c48c5b..72e8c24 100644
--- a/embeddings_test.go
+++ b/embeddings_test.go
@@ -4,7 +4,9 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
+	"math"
 	"net/http"
 	"reflect"
 	"testing"
@@ -233,3 +235,39 @@ func TestEmbeddingResponseBase64_ToEmbeddingResponse(t *testing.T) {
 		})
 	}
 }
+
+func TestDotProduct(t *testing.T) {
+	v1 := &Embedding{Embedding: []float32{1, 2, 3}}
+	v2 := &Embedding{Embedding: []float32{2, 4, 6}}
+	expected := float32(28.0)
+
+	result, err := v1.DotProduct(v2)
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+
+	if math.Abs(float64(result-expected)) > 1e-12 {
+		t.Errorf("Unexpected result. Expected: %v, but got %v", expected, result)
+	}
+
+	v1 = &Embedding{Embedding: []float32{1, 0, 0}}
+	v2 = &Embedding{Embedding: []float32{0, 1, 0}}
+	expected = float32(0.0)
+
+	result, err = v1.DotProduct(v2)
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+
+	if math.Abs(float64(result-expected)) > 1e-12 {
+		t.Errorf("Unexpected result. Expected: %v, but got %v", expected, result)
+	}
+
+	// Test for VectorLengthMismatchError
+	v1 = &Embedding{Embedding: []float32{1, 0, 0}}
+	v2 = &Embedding{Embedding: []float32{0, 1}}
+	_, err = v1.DotProduct(v2)
+	if !errors.Is(err, ErrVectorLengthMismatch) {
+		t.Errorf("Expected Vector Length Mismatch Error, but got: %v", err)
+	}
+}
diff --git a/models_test.go b/models_test.go
index 59b4f5e..9ff7304 100644
--- a/models_test.go
+++ b/models_test.go
@@ -14,6 +14,8 @@ import (
 	"testing"
 )
 
+const testFineTuneModelID = "fine-tune-model-id"
+
 // TestListModels Tests the list models endpoint of the API using the mocked server.
 func TestListModels(t *testing.T) {
 	client, server, teardown := setupOpenAITestServer()
@@ -78,3 +80,16 @@ func TestGetModelReturnTimeoutError(t *testing.T) {
 		t.Fatal("Did not return timeout error")
 	}
 }
+
+func TestDeleteFineTuneModel(t *testing.T) {
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+	server.RegisterHandler("/v1/models/"+testFineTuneModelID, handleDeleteFineTuneModelEndpoint)
+	_, err := client.DeleteFineTuneModel(context.Background(), testFineTuneModelID)
+	checks.NoError(t, err, "DeleteFineTuneModel error")
+}
+
+func handleDeleteFineTuneModelEndpoint(w http.ResponseWriter, _ *http.Request) {
+	resBytes, _ := json.Marshal(FineTuneModelDeleteResponse{})
+	fmt.Fprintln(w, string(resBytes))
+}