Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Pinecone Local testing support in CI #77

Merged
merged 17 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d679ec3
update ci workflow to run local integration test steps using pclocal …
austin-denoble Sep 18, 2024
3d56e16
update ci workflow to run local integration tests even if the tests s…
austin-denoble Sep 19, 2024
72a486e
fix go test command
austin-denoble Sep 19, 2024
b532193
run local_test.go directly, fix host environment variable names
austin-denoble Sep 19, 2024
c43e551
update go test command again, fix targeting package
austin-denoble Sep 19, 2024
9ce2de2
add logs, tweak add -tags=localServer
austin-denoble Sep 19, 2024
8945c6f
fix assert in TestQueryVectors
austin-denoble Sep 19, 2024
fc1aabb
try targeting different ports
austin-denoble Sep 19, 2024
2e6773e
try localhost in CI environment
austin-denoble Sep 19, 2024
165e7f4
add basic tests for fetch, Query, Update, and DescribeIndexStats
austin-denoble Sep 19, 2024
be98dd7
update pinecone local test suite to test across a large dimension of …
austin-denoble Sep 19, 2024
7e3913a
make sure to actually delete all the vectors and check for double the…
austin-denoble Sep 20, 2024
2912ad7
fix checking indexes after cleanup
austin-denoble Sep 20, 2024
34b1c09
only delete by filter for cleanup on pod indexes
austin-denoble Sep 20, 2024
7a54b5c
fix possible access of nil on ListResponse.Usage when creating ListVe…
austin-denoble Sep 20, 2024
c1a037f
add additional query tests, filter by metadata, test we get metadata …
austin-denoble Sep 20, 2024
5e3958f
use toUsage instead of something inline
austin-denoble Sep 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ on:
jobs:
build:
runs-on: ubuntu-latest
services:
pc-index-serverless:
image: ghcr.io/pinecone-io/pinecone-index:latest
ports:
- 5081:5081
env:
PORT: 5081
DIMENSION: 1536
METRIC: dot-product
INDEX_TYPE: serverless
pc-index-pod:
image: ghcr.io/pinecone-io/pinecone-index:latest
ports:
- 5082:5082
env:
PORT: 5082
DIMENSION: 1536
METRIC: cosine
INDEX_TYPE: pod
steps:
- uses: actions/checkout@v4
- name: Setup Go
Expand All @@ -17,6 +36,13 @@ jobs:
run: |
go get ./pinecone
- name: Run tests
continue-on-error: true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still want to run the subsequent step that specifically runs local integration tests.

run: go test -count=1 -v ./pinecone
env:
PINECONE_API_KEY: ${{ secrets.API_KEY }}
- name: Run local integration tests
run: go test -count=1 -v ./pinecone -run TestRunLocalIntegrationSuite -tags=localServer
env:
PINECONE_INDEX_URL_POD: http://localhost:5082
PINECONE_INDEX_URL_SERVERLESS: http://localhost:5081
PINECONE_DIMENSION: 1536
2 changes: 1 addition & 1 deletion pinecone/index_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ func (idx *IndexConnection) ListVectors(ctx context.Context, in *ListVectorsRequ

return &ListVectorsResponse{
VectorIds: vectorIds,
Usage: &Usage{ReadUnits: derefOrDefault(res.Usage.ReadUnits, 0)},
Usage: toUsage(res.Usage),
NextPaginationToken: toPaginationToken(res.Pagination),
Namespace: idx.Namespace,
}, nil
Expand Down
6 changes: 3 additions & 3 deletions pinecone/index_connection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (ts *IntegrationTests) TestDeleteVectorsById() {
assert.NoError(ts.T(), err)
ts.vectorIds = []string{}

vectors := GenerateVectors(5, ts.dimension, true)
vectors := GenerateVectors(5, ts.dimension, true, nil)

_, err = ts.idxConn.UpsertVectors(ctx, vectors)
if err != nil {
Expand Down Expand Up @@ -96,7 +96,7 @@ func (ts *IntegrationTests) TestDeleteVectorsByFilter() {
}
ts.vectorIds = []string{}

vectors := GenerateVectors(5, ts.dimension, true)
vectors := GenerateVectors(5, ts.dimension, true, nil)

_, err = ts.idxConn.UpsertVectors(ctx, vectors)
if err != nil {
Expand All @@ -117,7 +117,7 @@ func (ts *IntegrationTests) TestDeleteAllVectorsInNamespace() {
assert.NoError(ts.T(), err)
ts.vectorIds = []string{}

vectors := GenerateVectors(5, ts.dimension, true)
vectors := GenerateVectors(5, ts.dimension, true, nil)

_, err = ts.idxConn.UpsertVectors(ctx, vectors)
if err != nil {
Expand Down
246 changes: 246 additions & 0 deletions pinecone/local_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
//go:build localServer
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isolating these tests from the other tests. I think this works for not running these tests as a part of the unit / integration test suite, but there may be a better way to do this in Go.


package pinecone

import (
"context"
"fmt"
"os"
"strconv"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb"
)

type LocalIntegrationTests struct {
suite.Suite
client *Client
host string
dimension int32
indexType string
namespace string
metadata *Metadata
vectorIds []string
idxConns []*IndexConnection
}

func (ts *LocalIntegrationTests) SetupSuite() {
ctx := context.Background()

// Deterministically create vectors
vectors := GenerateVectors(100, ts.dimension, true, ts.metadata)

// Get vector ids for the suite
vectorIds := make([]string, len(vectors))
for i, v := range vectors {
vectorIds[i] = v.Id
}

// Upsert vectors into each index connection
for _, idxConn := range ts.idxConns {
upsertedVectors, err := idxConn.UpsertVectors(ctx, vectors)
require.NoError(ts.T(), err)
fmt.Printf("Upserted vectors: %v into host: %s in namespace: %s \n", upsertedVectors, ts.host, idxConn.Namespace)
}

ts.vectorIds = append(ts.vectorIds, vectorIds...)
}

func (ts *LocalIntegrationTests) TearDownSuite() {
// test deleting vectors as a part of cleanup for each index connection
for _, idxConn := range ts.idxConns {
// Delete a slice of vectors by id
err := idxConn.DeleteVectorsById(context.Background(), ts.vectorIds[10:20])
require.NoError(ts.T(), err)

// Delete vectors by filter
if ts.indexType == "pods" {
err = idxConn.DeleteVectorsByFilter(context.Background(), ts.metadata)
require.NoError(ts.T(), err)
}

// Delete all remaining vectors
err = idxConn.DeleteAllVectorsInNamespace(context.Background())
require.NoError(ts.T(), err)
}

description, err := ts.idxConns[0].DescribeIndexStats(context.Background())
require.NoError(ts.T(), err)
assert.NotNil(ts.T(), description, "Index description should not be nil")
assert.Equal(ts.T(), uint32(0), description.TotalVectorCount, "Total vector count should be 0 after deleting")
}

// This is the entry point for all local integration tests
// This test function is picked up by go test and triggers the suite runs when
// the build tag localServer is set
func TestRunLocalIntegrationSuite(t *testing.T) {
fmt.Println("Running local integration tests")
RunLocalSuite(t)
}

func RunLocalSuite(t *testing.T) {
fmt.Println("Running local integration tests")
localHostPod, present := os.LookupEnv("PINECONE_INDEX_URL_POD")
assert.True(t, present, "PINECONE_INDEX_URL_POD env variable not set")

localHostServerless, present := os.LookupEnv("PINECONE_INDEX_URL_SERVERLESS")
assert.True(t, present, "PINECONE_INDEX_URL_SERVERLESS env variable not set")

dimension, present := os.LookupEnv("PINECONE_DIMENSION")
assert.True(t, present, "PINECONE_DIMENSION env variable not set")

parsedDimension, err := strconv.ParseInt(dimension, 10, 32)
require.NoError(t, err)

namespace := "test-namespace"
metadata := &structpb.Struct{
Fields: map[string]*structpb.Value{
"genre": {Kind: &structpb.Value_StringValue{StringValue: "classical"}},
},
}

client, err := NewClientBase(NewClientBaseParams{})
require.NotNil(t, client, "Client should not be nil after creation")
require.NoError(t, err)

// Create index connections for pod and serverless indexes with both default namespace
// and a custom namespace
var podIdxConns []*IndexConnection
idxConnPod, err := client.Index(NewIndexConnParams{Host: localHostPod})
require.NoError(t, err)
podIdxConns = append(podIdxConns, idxConnPod)

idxConnPodNamespace, err := client.Index(NewIndexConnParams{Host: localHostPod, Namespace: namespace})
require.NoError(t, err)
podIdxConns = append(podIdxConns, idxConnPodNamespace)

var serverlessIdxConns []*IndexConnection
idxConnServerless, err := client.Index(NewIndexConnParams{Host: localHostServerless},
grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
serverlessIdxConns = append(serverlessIdxConns, idxConnServerless)

idxConnServerless, err = client.Index(NewIndexConnParams{Host: localHostServerless, Namespace: namespace})
require.NoError(t, err)
serverlessIdxConns = append(serverlessIdxConns, idxConnServerless)

localHostPodSuite := &LocalIntegrationTests{
client: client,
idxConns: podIdxConns,
indexType: "pods",
host: localHostPod,
namespace: namespace,
metadata: metadata,
dimension: int32(parsedDimension),
}

localHostSuiteServerless := &LocalIntegrationTests{
client: client,
idxConns: serverlessIdxConns,
indexType: "serverless",
host: localHostServerless,
namespace: namespace,
metadata: metadata,
dimension: int32(parsedDimension),
}

suite.Run(t, localHostPodSuite)
suite.Run(t, localHostSuiteServerless)
}

func (ts *LocalIntegrationTests) TestFetchVectors() {
fetchVectorId := ts.vectorIds[0]

for _, idxConn := range ts.idxConns {
fetchVectorsResponse, err := idxConn.FetchVectors(context.Background(), []string{fetchVectorId})
require.NoError(ts.T(), err)

assert.NotNil(ts.T(), fetchVectorsResponse, "Fetch vectors response should not be nil")
assert.Equal(ts.T(), 1, len(fetchVectorsResponse.Vectors), "Fetch vectors response should have 1 vector")
assert.Equal(ts.T(), fetchVectorId, fetchVectorsResponse.Vectors[fetchVectorId].Id, "Fetched vector id should match")
}
}

func (ts *LocalIntegrationTests) TestQueryVectors() {
queryVectorId := ts.vectorIds[0]
topK := 10

for _, idxConn := range ts.idxConns {
queryVectorsByIdResponse, err := idxConn.QueryByVectorId(context.Background(), &QueryByVectorIdRequest{
VectorId: queryVectorId,
TopK: uint32(topK),
IncludeValues: true,
IncludeMetadata: true,
})
require.NoError(ts.T(), err)

assert.NotNil(ts.T(), queryVectorsByIdResponse, "QueryByVectorId results should not be nil")
assert.Equal(ts.T(), topK, len(queryVectorsByIdResponse.Matches), "QueryByVectorId results should have 10 matches")
assert.Equal(ts.T(), queryVectorId, queryVectorsByIdResponse.Matches[0].Vector.Id, "Top QueryByVectorId result's vector id should match queryVectorId")

queryByVectorValuesResponse, err := idxConn.QueryByVectorValues(context.Background(), &QueryByVectorValuesRequest{
Vector: queryVectorsByIdResponse.Matches[0].Vector.Values,
TopK: uint32(topK),
MetadataFilter: ts.metadata,
IncludeValues: true,
IncludeMetadata: true,
})
require.NoError(ts.T(), err)

assert.NotNil(ts.T(), queryByVectorValuesResponse, "QueryByVectorValues results should not be nil")
assert.Equal(ts.T(), topK, len(queryByVectorValuesResponse.Matches), "QueryByVectorValues results should have 10 matches")

resultMetadata, err := protojson.Marshal(queryByVectorValuesResponse.Matches[0].Vector.Metadata)
assert.NoError(ts.T(), err)
suiteMetadata, err := protojson.Marshal(ts.metadata)
assert.NoError(ts.T(), err)

assert.Equal(ts.T(), resultMetadata, suiteMetadata, "Top QueryByVectorValues result's metadata should match the test suite's metadata")
}
}

func (ts *LocalIntegrationTests) TestUpdateVectors() {
updateVectorId := ts.vectorIds[0]
newValues := generateVectorValues(ts.dimension)

for _, idxConn := range ts.idxConns {
err := idxConn.UpdateVector(context.Background(), &UpdateVectorRequest{Id: updateVectorId, Values: newValues})
require.NoError(ts.T(), err)

fetchVectorsResponse, err := idxConn.FetchVectors(context.Background(), []string{updateVectorId})
require.NoError(ts.T(), err)
assert.Equal(ts.T(), newValues, fetchVectorsResponse.Vectors[updateVectorId].Values, "Updated vector values should match")
}
}

func (ts *LocalIntegrationTests) TestDescribeIndexStats() {
for _, idxConn := range ts.idxConns {
description, err := idxConn.DescribeIndexStats(context.Background())
require.NoError(ts.T(), err)

assert.NotNil(ts.T(), description, "Index description should not be nil")
assert.Equal(ts.T(), description.TotalVectorCount, uint32(len(ts.vectorIds)*2), "Index host should match")
}
}

func (ts *LocalIntegrationTests) TestListVectorIds() {
limit := uint32(25)
// Listing vector ids is only available for serverless indexes
if ts.indexType == "serverless" {
for _, idxConn := range ts.idxConns {
listVectorIdsResponse, err := idxConn.ListVectors(context.Background(), &ListVectorsRequest{
Limit: &limit,
})
require.NoError(ts.T(), err)

assert.NotNil(ts.T(), listVectorIdsResponse, "ListVectors response should not be nil")
assert.Equal(ts.T(), limit, uint32(len(listVectorIdsResponse.VectorIds)), "ListVectors response should have %d vector ids", limit)
}
}
}
14 changes: 4 additions & 10 deletions pinecone/test_suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"math/rand"
"time"

"google.golang.org/protobuf/types/known/structpb"

"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
Expand Down Expand Up @@ -47,14 +45,13 @@ func (ts *IntegrationTests) SetupSuite() {
ts.idxConn = idxConn

// Deterministically create vectors
vectors := GenerateVectors(10, ts.dimension, false)
vectors := GenerateVectors(10, ts.dimension, false, nil)

// Add vector ids to the suite
vectorIds := make([]string, len(vectors))
for i, v := range vectors {
vectorIds[i] = v.Id
}
ts.vectorIds = append(ts.vectorIds, vectorIds...)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was being done twice. I think it's better to keep the actual appending where we upsert successfully. It's inside the upsertVectors helper function.


// Upsert vectors
err = upsertVectors(ts, ctx, vectors)
Expand Down Expand Up @@ -158,7 +155,7 @@ func WaitUntilIndexReady(ts *IntegrationTests, ctx context.Context) (bool, error
}
}

func GenerateVectors(numOfVectors int, dimension int32, isSparse bool) []*Vector {
func GenerateVectors(numOfVectors int, dimension int32, isSparse bool, metadata *Metadata) []*Vector {
vectors := make([]*Vector, numOfVectors)

for i := 0; i < int(numOfVectors); i++ {
Expand All @@ -177,12 +174,9 @@ func GenerateVectors(numOfVectors int, dimension int32, isSparse bool) []*Vector
vectors[i].SparseValues = &sparseValues
}

metadata := &structpb.Struct{
Fields: map[string]*structpb.Value{
"genre": {Kind: &structpb.Value_StringValue{StringValue: "classical"}},
},
if metadata != nil {
vectors[i].Metadata = metadata
}
vectors[i].Metadata = metadata
}

return vectors
Expand Down