first commit
This commit is contained in:
649
framework/vectorstore/pinecone.go
Normal file
649
framework/vectorstore/pinecone.go
Normal file
@@ -0,0 +1,649 @@
|
||||
package vectorstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/maximhq/bifrost/core/schemas"
|
||||
"github.com/pinecone-io/go-pinecone/v5/pinecone"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
// PineconeConfig represents the configuration for the Pinecone vector store.
|
||||
type PineconeConfig struct {
|
||||
APIKey schemas.EnvVar `json:"api_key"` // Pinecone API key - REQUIRED
|
||||
IndexHost schemas.EnvVar `json:"index_host"` // Index host URL from Pinecone console - REQUIRED
|
||||
}
|
||||
|
||||
// PineconeStore represents the Pinecone vector store.
|
||||
type PineconeStore struct {
|
||||
client *pinecone.Client
|
||||
indexConn *pinecone.IndexConnection
|
||||
config *PineconeConfig
|
||||
logger schemas.Logger
|
||||
mu sync.RWMutex // Protects namespaces and dimension
|
||||
namespaces map[string]*pinecone.IndexConnection
|
||||
dimension int // Store dimension for zero vector queries in GetAll
|
||||
}
|
||||
|
||||
// Ping checks if the Pinecone server is reachable.
|
||||
func (s *PineconeStore) Ping(ctx context.Context) error {
|
||||
_, err := s.indexConn.DescribeIndexStats(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateNamespace creates a new namespace in the Pinecone vector store.
|
||||
// Note: Pinecone namespaces are created implicitly when upserting vectors.
|
||||
// This method is a no-op but ensures the connection is valid.
|
||||
func (s *PineconeStore) CreateNamespace(ctx context.Context, namespace string, dimension int, properties map[string]VectorStoreProperties) error {
|
||||
// Store dimension for use in GetAll (zero vector queries)
|
||||
s.mu.Lock()
|
||||
s.dimension = dimension
|
||||
s.mu.Unlock()
|
||||
|
||||
// Pinecone namespaces are created automatically on first upsert.
|
||||
// We just verify the index connection is valid.
|
||||
_, err := s.indexConn.DescribeIndexStats(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify index connection: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteNamespace deletes a namespace from the Pinecone vector store.
|
||||
func (s *PineconeStore) DeleteNamespace(ctx context.Context, namespace string) error {
|
||||
idxConn, err := s.getNamespaceConnection(namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return idxConn.DeleteAllVectorsInNamespace(ctx)
|
||||
}
|
||||
|
||||
// GetChunk retrieves a single vector from the Pinecone vector store.
|
||||
func (s *PineconeStore) GetChunk(ctx context.Context, namespace string, id string) (SearchResult, error) {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return SearchResult{}, fmt.Errorf("id is required")
|
||||
}
|
||||
|
||||
idxConn, err := s.getNamespaceConnection(namespace)
|
||||
if err != nil {
|
||||
return SearchResult{}, err
|
||||
}
|
||||
|
||||
res, err := idxConn.FetchVectors(ctx, []string{id})
|
||||
if err != nil {
|
||||
return SearchResult{}, fmt.Errorf("failed to fetch vector: %w", err)
|
||||
}
|
||||
|
||||
if len(res.Vectors) == 0 {
|
||||
return SearchResult{}, fmt.Errorf("not found: %s", id)
|
||||
}
|
||||
|
||||
vec, exists := res.Vectors[id]
|
||||
if !exists || vec == nil {
|
||||
return SearchResult{}, fmt.Errorf("not found: %s", id)
|
||||
}
|
||||
|
||||
return SearchResult{
|
||||
ID: id,
|
||||
Properties: metadataToMap(vec.Metadata),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetChunks retrieves multiple vectors from the Pinecone vector store.
|
||||
func (s *PineconeStore) GetChunks(ctx context.Context, namespace string, ids []string) ([]SearchResult, error) {
|
||||
if len(ids) == 0 {
|
||||
return []SearchResult{}, nil
|
||||
}
|
||||
|
||||
// Filter out empty IDs
|
||||
validIDs := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if strings.TrimSpace(id) != "" {
|
||||
validIDs = append(validIDs, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validIDs) == 0 {
|
||||
return []SearchResult{}, nil
|
||||
}
|
||||
|
||||
idxConn, err := s.getNamespaceConnection(namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := idxConn.FetchVectors(ctx, validIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch vectors: %w", err)
|
||||
}
|
||||
|
||||
results := make([]SearchResult, 0, len(res.Vectors))
|
||||
for id, vec := range res.Vectors {
|
||||
if vec != nil {
|
||||
results = append(results, SearchResult{
|
||||
ID: id,
|
||||
Properties: metadataToMap(vec.Metadata),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetAll retrieves all vectors with optional filtering and pagination.
|
||||
// Note: This implementation uses QueryByVectorValues with a zero vector instead of ListVectors
|
||||
// because ListVectors has severe eventual consistency issues on Pinecone Serverless/Starter indexes.
|
||||
// The metadata filtering is done server-side by Pinecone, providing much better consistency.
|
||||
func (s *PineconeStore) GetAll(ctx context.Context, namespace string, queries []Query, selectFields []string, cursor *string, limit int64) ([]SearchResult, *string, error) {
|
||||
idxConn, err := s.getNamespaceConnection(namespace)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
topK := uint32(limit)
|
||||
if limit <= 0 {
|
||||
topK = 100
|
||||
}
|
||||
|
||||
// Create zero vector for query - this allows us to use QueryByVectorValues
|
||||
// which has much better consistency than ListVectors
|
||||
s.mu.RLock()
|
||||
dim := s.dimension
|
||||
s.mu.RUnlock()
|
||||
if dim <= 0 {
|
||||
return nil, nil, fmt.Errorf("dimension not set: CreateNamespace must be called before GetAll to set the vector dimension")
|
||||
}
|
||||
zeroVector := make([]float32, dim)
|
||||
|
||||
queryReq := &pinecone.QueryByVectorValuesRequest{
|
||||
Vector: zeroVector,
|
||||
TopK: topK,
|
||||
IncludeValues: false,
|
||||
IncludeMetadata: true,
|
||||
}
|
||||
|
||||
// Build metadata filter from queries - filtering is done server-side
|
||||
if len(queries) > 0 {
|
||||
filter, filterErr := buildPineconeFilter(queries)
|
||||
if filterErr != nil {
|
||||
s.logger.Warn("failed to build pinecone filter, queries may not be applied: %v", filterErr)
|
||||
}
|
||||
if filter != nil {
|
||||
queryReq.MetadataFilter = filter
|
||||
}
|
||||
}
|
||||
|
||||
res, err := idxConn.QueryByVectorValues(ctx, queryReq)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to query vectors: %w", err)
|
||||
}
|
||||
|
||||
results := make([]SearchResult, 0, len(res.Matches))
|
||||
for _, match := range res.Matches {
|
||||
if match.Vector == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
props := metadataToMap(match.Vector.Metadata)
|
||||
filteredProps := filterPropertiesPinecone(props, selectFields)
|
||||
|
||||
results = append(results, SearchResult{
|
||||
ID: match.Vector.Id,
|
||||
Properties: filteredProps,
|
||||
})
|
||||
}
|
||||
|
||||
// Note: QueryByVectorValues doesn't support pagination tokens like ListVectors
|
||||
// For direct hash lookup (the main use case), we only need 1 result anyway
|
||||
return results, nil, nil
|
||||
}
|
||||
|
||||
// GetNearest retrieves the nearest vectors to a given vector.
|
||||
func (s *PineconeStore) GetNearest(ctx context.Context, namespace string, vector []float32, queries []Query, selectFields []string, threshold float64, limit int64) ([]SearchResult, error) {
|
||||
idxConn, err := s.getNamespaceConnection(namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
topK := uint32(limit)
|
||||
if limit <= 0 {
|
||||
topK = 10
|
||||
}
|
||||
|
||||
queryReq := &pinecone.QueryByVectorValuesRequest{
|
||||
Vector: vector,
|
||||
TopK: topK,
|
||||
IncludeValues: false,
|
||||
IncludeMetadata: true,
|
||||
}
|
||||
|
||||
// Build metadata filter from queries
|
||||
if len(queries) > 0 {
|
||||
filter, err := buildPineconeFilter(queries)
|
||||
if err != nil {
|
||||
s.logger.Debug(fmt.Sprintf("failed to build pinecone filter: %v", err))
|
||||
} else if filter != nil {
|
||||
queryReq.MetadataFilter = filter
|
||||
}
|
||||
}
|
||||
|
||||
res, err := idxConn.QueryByVectorValues(ctx, queryReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query vectors: %w", err)
|
||||
}
|
||||
|
||||
results := make([]SearchResult, 0, len(res.Matches))
|
||||
for _, match := range res.Matches {
|
||||
if match.Vector == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
score := float64(match.Score)
|
||||
|
||||
// Apply threshold filter
|
||||
if score < threshold {
|
||||
continue
|
||||
}
|
||||
|
||||
props := metadataToMap(match.Vector.Metadata)
|
||||
filteredProps := filterPropertiesPinecone(props, selectFields)
|
||||
|
||||
results = append(results, SearchResult{
|
||||
ID: match.Vector.Id,
|
||||
Score: &score,
|
||||
Properties: filteredProps,
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// convertMetadataForStructpb converts metadata map to be compatible with structpb.NewStruct.
|
||||
// Specifically, it converts []string to []interface{} since structpb doesn't handle []string directly.
|
||||
func convertMetadataForStructpb(metadata map[string]interface{}) map[string]interface{} {
|
||||
if metadata == nil {
|
||||
return nil
|
||||
}
|
||||
converted := make(map[string]interface{}, len(metadata))
|
||||
for k, v := range metadata {
|
||||
switch val := v.(type) {
|
||||
case []string:
|
||||
// Convert []string to []interface{}
|
||||
interfaceSlice := make([]interface{}, len(val))
|
||||
for i, s := range val {
|
||||
interfaceSlice[i] = s
|
||||
}
|
||||
converted[k] = interfaceSlice
|
||||
default:
|
||||
converted[k] = v
|
||||
}
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
// Add stores a new vector in the Pinecone vector store.
|
||||
func (s *PineconeStore) Add(ctx context.Context, namespace string, id string, embedding []float32, metadata map[string]interface{}) error {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return fmt.Errorf("id is required")
|
||||
}
|
||||
|
||||
idxConn, err := s.getNamespaceConnection(namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert metadata to structpb (handle []string -> []interface{} conversion)
|
||||
var pbMetadata *structpb.Struct
|
||||
if len(metadata) > 0 {
|
||||
convertedMetadata := convertMetadataForStructpb(metadata)
|
||||
pbMetadata, err = structpb.NewStruct(convertedMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert metadata: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
vec := &pinecone.Vector{
|
||||
Id: id,
|
||||
Metadata: pbMetadata,
|
||||
}
|
||||
|
||||
if len(embedding) > 0 {
|
||||
vec.Values = &embedding
|
||||
}
|
||||
|
||||
_, err = idxConn.UpsertVectors(ctx, []*pinecone.Vector{vec})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upsert vector: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a vector from the Pinecone vector store.
|
||||
func (s *PineconeStore) Delete(ctx context.Context, namespace string, id string) error {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return fmt.Errorf("id is required")
|
||||
}
|
||||
|
||||
idxConn, err := s.getNamespaceConnection(namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return idxConn.DeleteVectorsById(ctx, []string{id})
|
||||
}
|
||||
|
||||
// DeleteAll removes multiple vectors matching the filter.
|
||||
func (s *PineconeStore) DeleteAll(ctx context.Context, namespace string, queries []Query) ([]DeleteResult, error) {
|
||||
idxConn, err := s.getNamespaceConnection(namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If we have queries, use filter-based deletion
|
||||
if len(queries) > 0 {
|
||||
filter, err := buildPineconeFilter(queries)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build filter: %w", err)
|
||||
}
|
||||
|
||||
if filter != nil {
|
||||
err = idxConn.DeleteVectorsByFilter(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete vectors by filter: %w", err)
|
||||
}
|
||||
// Pinecone doesn't return individual results for filter-based deletion
|
||||
return []DeleteResult{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If no queries, list and delete all vectors in the namespace
|
||||
listRes, err := idxConn.ListVectors(ctx, &pinecone.ListVectorsRequest{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list vectors: %w", err)
|
||||
}
|
||||
|
||||
if len(listRes.VectorIds) == 0 {
|
||||
return []DeleteResult{}, nil
|
||||
}
|
||||
|
||||
// Convert []*string to []string
|
||||
deleteIDs := make([]string, 0, len(listRes.VectorIds))
|
||||
for _, id := range listRes.VectorIds {
|
||||
if id != nil {
|
||||
deleteIDs = append(deleteIDs, *id)
|
||||
}
|
||||
}
|
||||
|
||||
results := make([]DeleteResult, len(deleteIDs))
|
||||
for i, id := range deleteIDs {
|
||||
results[i] = DeleteResult{
|
||||
ID: id,
|
||||
Status: DeleteStatusSuccess,
|
||||
}
|
||||
}
|
||||
|
||||
err = idxConn.DeleteVectorsById(ctx, deleteIDs)
|
||||
if err != nil {
|
||||
for i := range results {
|
||||
results[i].Status = DeleteStatusError
|
||||
results[i].Error = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Close closes the Pinecone client connection.
|
||||
// If namespace is non-empty, only that namespace connection is closed.
|
||||
// If namespace is empty, all connections (indexConn and all namespaces) are closed.
|
||||
func (s *PineconeStore) Close(ctx context.Context, namespace string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// If a specific namespace is provided, close only that connection
|
||||
if namespace != "" {
|
||||
if conn, exists := s.namespaces[namespace]; exists && conn != nil {
|
||||
conn.Close()
|
||||
delete(s.namespaces, namespace)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Close all connections when namespace is empty
|
||||
var errs []error
|
||||
// Close the main index connection
|
||||
if s.indexConn != nil {
|
||||
s.indexConn.Close()
|
||||
s.indexConn = nil
|
||||
}
|
||||
// Close and remove all namespace connections
|
||||
for ns, conn := range s.namespaces {
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
delete(s.namespaces, ns)
|
||||
}
|
||||
// Return aggregated errors if any occurred
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("errors closing connections: %v", errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequiresVectors returns true because Pinecone is a dedicated vector database
|
||||
// that requires vectors for all entries with a specific dimension.
|
||||
func (s *PineconeStore) RequiresVectors() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// newPineconeStore creates a new Pinecone vector store.
|
||||
func newPineconeStore(ctx context.Context, config *PineconeConfig, logger schemas.Logger) (*PineconeStore, error) {
|
||||
if strings.TrimSpace(config.APIKey.GetValue()) == "" {
|
||||
return nil, fmt.Errorf("pinecone api_key is required")
|
||||
}
|
||||
if strings.TrimSpace(config.IndexHost.GetValue()) == "" {
|
||||
return nil, fmt.Errorf("pinecone index_host is required")
|
||||
}
|
||||
// Creating new client
|
||||
client, err := pinecone.NewClient(pinecone.NewClientParams{
|
||||
ApiKey: config.APIKey.GetValue(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create pinecone client: %w", err)
|
||||
}
|
||||
// Prepare the host URL
|
||||
// For local connections (Pinecone Local), prefix with http:// to disable TLS
|
||||
// See: https://docs.pinecone.io/guides/operations/local-development
|
||||
host := config.IndexHost.GetValue()
|
||||
if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") {
|
||||
// Check if this looks like a local connection
|
||||
if strings.HasPrefix(host, "localhost") || strings.HasPrefix(host, "127.0.0.1") {
|
||||
host = "http://" + host
|
||||
}
|
||||
}
|
||||
// Create index connection
|
||||
idxConn, err := client.Index(pinecone.NewIndexConnParams{
|
||||
Host: host,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create index connection: %w", err)
|
||||
}
|
||||
// Verify connection by getting index stats
|
||||
_, err = idxConn.DescribeIndexStats(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to pinecone index: %w", err)
|
||||
}
|
||||
return &PineconeStore{
|
||||
client: client,
|
||||
indexConn: idxConn,
|
||||
config: config,
|
||||
logger: logger,
|
||||
namespaces: make(map[string]*pinecone.IndexConnection),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getHostWithScheme returns the host with the appropriate scheme.
|
||||
// For local connections (localhost/127.0.0.1), it adds http:// to disable TLS.
|
||||
func (s *PineconeStore) getHostWithScheme() string {
|
||||
host := s.config.IndexHost.GetValue()
|
||||
if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") {
|
||||
if strings.HasPrefix(host, "localhost") || strings.HasPrefix(host, "127.0.0.1") {
|
||||
return "http://" + host
|
||||
}
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// getNamespaceConnection returns or creates a connection for the given namespace.
|
||||
func (s *PineconeStore) getNamespaceConnection(namespace string) (*pinecone.IndexConnection, error) {
|
||||
if namespace == "" {
|
||||
return s.indexConn, nil
|
||||
}
|
||||
// Check if we already have a connection for this namespace (optimistic read)
|
||||
s.mu.RLock()
|
||||
if conn, exists := s.namespaces[namespace]; exists {
|
||||
s.mu.RUnlock()
|
||||
return conn, nil
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
// Acquire write lock to create new connection
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// Double-check after acquiring write lock (another goroutine may have created it)
|
||||
if conn, exists := s.namespaces[namespace]; exists {
|
||||
return conn, nil
|
||||
}
|
||||
// Create a new connection for this namespace
|
||||
conn, err := s.client.Index(pinecone.NewIndexConnParams{
|
||||
Host: s.getHostWithScheme(),
|
||||
Namespace: namespace,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create namespace connection: %w", err)
|
||||
}
|
||||
s.namespaces[namespace] = conn
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// metadataToMap converts protobuf Struct to map[string]interface{}.
|
||||
func metadataToMap(metadata *structpb.Struct) map[string]interface{} {
|
||||
if metadata == nil {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
return metadata.AsMap()
|
||||
}
|
||||
|
||||
// filterPropertiesPinecone filters properties based on selected fields.
|
||||
func filterPropertiesPinecone(props map[string]interface{}, selectFields []string) map[string]interface{} {
|
||||
if len(selectFields) == 0 {
|
||||
return props
|
||||
}
|
||||
filtered := make(map[string]interface{}, len(selectFields))
|
||||
for _, field := range selectFields {
|
||||
if val, ok := props[field]; ok {
|
||||
filtered[field] = val
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// matchesQueries checks if properties match all query conditions.
|
||||
func matchesQueries(props map[string]interface{}, queries []Query) bool {
|
||||
if len(queries) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, q := range queries {
|
||||
val, exists := props[q.Field]
|
||||
if !matchesQuery(val, exists, q) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// matchesQuery checks if a single value matches a query condition.
|
||||
func matchesQuery(val interface{}, exists bool, q Query) bool {
|
||||
switch q.Operator {
|
||||
case QueryOperatorIsNull:
|
||||
return !exists || val == nil
|
||||
case QueryOperatorIsNotNull:
|
||||
return exists && val != nil
|
||||
case QueryOperatorEqual:
|
||||
return exists && fmt.Sprintf("%v", val) == fmt.Sprintf("%v", q.Value)
|
||||
case QueryOperatorNotEqual:
|
||||
return !exists || fmt.Sprintf("%v", val) != fmt.Sprintf("%v", q.Value)
|
||||
default:
|
||||
// For complex operators, default to true (filter at query time)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// buildPineconeFilter converts queries to Pinecone metadata filter.
|
||||
func buildPineconeFilter(queries []Query) (*structpb.Struct, error) {
|
||||
if len(queries) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
filterMap := make(map[string]interface{})
|
||||
|
||||
for _, q := range queries {
|
||||
condition := buildPineconeCondition(q)
|
||||
if condition != nil {
|
||||
filterMap[q.Field] = condition
|
||||
}
|
||||
}
|
||||
|
||||
if len(filterMap) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return structpb.NewStruct(filterMap)
|
||||
}
|
||||
|
||||
// buildPineconeCondition builds a single Pinecone filter condition.
|
||||
func buildPineconeCondition(q Query) interface{} {
|
||||
switch q.Operator {
|
||||
case QueryOperatorEqual:
|
||||
return map[string]interface{}{"$eq": q.Value}
|
||||
case QueryOperatorNotEqual:
|
||||
return map[string]interface{}{"$ne": q.Value}
|
||||
case QueryOperatorGreaterThan:
|
||||
return map[string]interface{}{"$gt": q.Value}
|
||||
case QueryOperatorGreaterThanOrEqual:
|
||||
return map[string]interface{}{"$gte": q.Value}
|
||||
case QueryOperatorLessThan:
|
||||
return map[string]interface{}{"$lt": q.Value}
|
||||
case QueryOperatorLessThanOrEqual:
|
||||
return map[string]interface{}{"$lte": q.Value}
|
||||
case QueryOperatorIsNull:
|
||||
return map[string]interface{}{"$eq": nil}
|
||||
case QueryOperatorIsNotNull:
|
||||
return map[string]interface{}{"$ne": nil}
|
||||
case QueryOperatorContainsAny:
|
||||
return map[string]interface{}{"$in": q.Value}
|
||||
case QueryOperatorContainsAll:
|
||||
// Build an $and array of equality checks so all values must match
|
||||
values, ok := q.Value.([]interface{})
|
||||
if !ok {
|
||||
// Try to convert []string to []interface{}
|
||||
if strValues, ok := q.Value.([]string); ok {
|
||||
values = make([]interface{}, len(strValues))
|
||||
for i, v := range strValues {
|
||||
values[i] = v
|
||||
}
|
||||
} else {
|
||||
// Fallback to single value equality
|
||||
return map[string]interface{}{"$eq": q.Value}
|
||||
}
|
||||
}
|
||||
andConditions := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
andConditions[i] = map[string]interface{}{"$eq": v}
|
||||
}
|
||||
return map[string]interface{}{"$and": andConditions}
|
||||
default:
|
||||
return map[string]interface{}{"$eq": q.Value}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user