diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index b3941784aa08..e064461cfe16 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -65,7 +65,7 @@
-
+
diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index f9e83098f6ba..0a4bf8c39da2 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -487,6 +487,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol", "sam
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SqlServerIntegrationTests", "src\VectorDataIntegrationTests\SqlServerIntegrationTests\SqlServerIntegrationTests.csproj", "{A5E6193C-8431-4C6E-B674-682CB41EAA0C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PineconeIntegrationTests", "src\VectorDataIntegrationTests\PineconeIntegrationTests\PineconeIntegrationTests.csproj", "{E9A74E0C-BC02-4DDD-A487-89847EDF8026}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1334,6 +1336,12 @@ Global
{A5E6193C-8431-4C6E-B674-682CB41EAA0C}.Publish|Any CPU.Build.0 = Debug|Any CPU
{A5E6193C-8431-4C6E-B674-682CB41EAA0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5E6193C-8431-4C6E-B674-682CB41EAA0C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E9A74E0C-BC02-4DDD-A487-89847EDF8026}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E9A74E0C-BC02-4DDD-A487-89847EDF8026}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E9A74E0C-BC02-4DDD-A487-89847EDF8026}.Publish|Any CPU.ActiveCfg = Release|Any CPU
+ {E9A74E0C-BC02-4DDD-A487-89847EDF8026}.Publish|Any CPU.Build.0 = Release|Any CPU
+ {E9A74E0C-BC02-4DDD-A487-89847EDF8026}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E9A74E0C-BC02-4DDD-A487-89847EDF8026}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1516,6 +1524,7 @@ Global
{8C658E1E-83C8-4127-B8BF-27A638A45DDD} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9}
{B16AC373-3DA8-4505-9510-110347CD635D} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
{A5E6193C-8431-4C6E-B674-682CB41EAA0C} = {4F381919-F1BE-47D8-8558-3187ED04A84F}
+ {E9A74E0C-BC02-4DDD-A487-89847EDF8026} = {4F381919-F1BE-47D8-8558-3187ED04A84F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
diff --git a/dotnet/src/Connectors/Connectors.Memory.Pinecone/Connectors.Memory.Pinecone.csproj b/dotnet/src/Connectors/Connectors.Memory.Pinecone/Connectors.Memory.Pinecone.csproj
index b2127f5131b0..3b51b03623ae 100644
--- a/dotnet/src/Connectors/Connectors.Memory.Pinecone/Connectors.Memory.Pinecone.csproj
+++ b/dotnet/src/Connectors/Connectors.Memory.Pinecone/Connectors.Memory.Pinecone.csproj
@@ -19,7 +19,7 @@
-
+
diff --git a/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeFilterTranslator.cs b/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeFilterTranslator.cs
new file mode 100644
index 000000000000..54a7202eaa07
--- /dev/null
+++ b/dotnet/src/Connectors/Connectors.Memory.Pinecone/PineconeFilterTranslator.cs
@@ -0,0 +1,267 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using Pinecone;
+
+namespace Microsoft.SemanticKernel.Connectors.Pinecone;
+
+// This class is a modification of MongoDBFilterTranslator that uses the same query language
+// (https://docs.pinecone.io/guides/data/understanding-metadata#metadata-query-language),
+// with the difference of representing everything as Metadata rather than BsonDocument.
+// For representing collections of any kinds, we use List,
+// as we sometimes need to extend the collection (with for example another condition).
+internal class PineconeFilterTranslator
+{
+ private IReadOnlyDictionary _storagePropertyNames = null!;
+ private ParameterExpression _recordParameter = null!;
+
+ internal Metadata Translate(LambdaExpression lambdaExpression, IReadOnlyDictionary storagePropertyNames)
+ {
+ this._storagePropertyNames = storagePropertyNames;
+
+ Debug.Assert(lambdaExpression.Parameters.Count == 1);
+ this._recordParameter = lambdaExpression.Parameters[0];
+
+ return this.Translate(lambdaExpression.Body);
+ }
+
+ private Metadata Translate(Expression? node)
+ => node switch
+ {
+ BinaryExpression
+ {
+ NodeType: ExpressionType.Equal or ExpressionType.NotEqual
+ or ExpressionType.GreaterThan or ExpressionType.GreaterThanOrEqual
+ or ExpressionType.LessThan or ExpressionType.LessThanOrEqual
+ } binary
+ => this.TranslateEqualityComparison(binary),
+
+ BinaryExpression { NodeType: ExpressionType.AndAlso or ExpressionType.OrElse } andOr
+ => this.TranslateAndOr(andOr),
+ UnaryExpression { NodeType: ExpressionType.Not } not
+ => this.TranslateNot(not),
+
+ // MemberExpression is generally handled within e.g. TranslateEqualityComparison; this is used to translate direct bool inside filter (e.g. Filter => r => r.Bool)
+ MemberExpression member when member.Type == typeof(bool) && this.TryTranslateFieldAccess(member, out _)
+ => this.TranslateEqualityComparison(Expression.Equal(member, Expression.Constant(true))),
+
+ MethodCallExpression methodCall => this.TranslateMethodCall(methodCall),
+
+ _ => throw new NotSupportedException("The following NodeType is unsupported: " + node?.NodeType)
+ };
+
+ private Metadata TranslateEqualityComparison(BinaryExpression binary)
+ {
+ if ((this.TryTranslateFieldAccess(binary.Left, out var storagePropertyName) && TryGetConstant(binary.Right, out var value))
+ || (this.TryTranslateFieldAccess(binary.Right, out storagePropertyName) && TryGetConstant(binary.Left, out value)))
+ {
+ if (value is null)
+ {
+ throw new NotSupportedException("Pincone does not support null checks in vector search pre-filters");
+ }
+
+ // Short form of equality (instead of $eq)
+ if (binary.NodeType is ExpressionType.Equal)
+ {
+ return new Metadata { [storagePropertyName] = ToMetadata(value) };
+ }
+
+ var filterOperator = binary.NodeType switch
+ {
+ ExpressionType.NotEqual => "$ne",
+ ExpressionType.GreaterThan => "$gt",
+ ExpressionType.GreaterThanOrEqual => "$gte",
+ ExpressionType.LessThan => "$lt",
+ ExpressionType.LessThanOrEqual => "$lte",
+
+ _ => throw new UnreachableException()
+ };
+
+ return new Metadata { [storagePropertyName] = new Metadata { [filterOperator] = ToMetadata(value) } };
+ }
+
+ throw new NotSupportedException("Invalid equality/comparison");
+ }
+
+ private Metadata TranslateAndOr(BinaryExpression andOr)
+ {
+ var mongoOperator = andOr.NodeType switch
+ {
+ ExpressionType.AndAlso => "$and",
+ ExpressionType.OrElse => "$or",
+ _ => throw new UnreachableException()
+ };
+
+ var (left, right) = (this.Translate(andOr.Left), this.Translate(andOr.Right));
+
+ List? nestedLeft = GetListOrNull(left, mongoOperator);
+ List? nestedRight = GetListOrNull(right, mongoOperator);
+
+ switch ((nestedLeft, nestedRight))
+ {
+ case (not null, not null):
+ nestedLeft.AddRange(nestedRight);
+ return left;
+ case (not null, null):
+ nestedLeft.Add(right);
+ return left;
+ case (null, not null):
+ nestedRight.Insert(0, left);
+ return right;
+ case (null, null):
+ return new Metadata { [mongoOperator] = new MetadataValue(new List { left, right }) };
+ }
+ }
+
+ private Metadata TranslateNot(UnaryExpression not)
+ {
+ switch (not.Operand)
+ {
+ // Special handling for !(a == b) and !(a != b)
+ case BinaryExpression { NodeType: ExpressionType.Equal or ExpressionType.NotEqual } binary:
+ return this.TranslateEqualityComparison(
+ Expression.MakeBinary(
+ binary.NodeType is ExpressionType.Equal ? ExpressionType.NotEqual : ExpressionType.Equal,
+ binary.Left,
+ binary.Right));
+
+ // Not over bool field (Filter => r => !r.Bool)
+ case MemberExpression member when member.Type == typeof(bool) && this.TryTranslateFieldAccess(member, out _):
+ return this.TranslateEqualityComparison(Expression.Equal(member, Expression.Constant(false)));
+ }
+
+ var operand = this.Translate(not.Operand);
+
+ // Identify NOT over $in, transform to $nin (https://www.mongodb.com/docs/manual/reference/operator/query/nin/#mongodb-query-op.-nin)
+ if (operand.Count == 1 && operand.First() is { Key: var fieldName, Value: MetadataValue nested } && nested.Value is Metadata nestedMetadata
+ && GetListOrNull(nestedMetadata, "$in") is List values)
+ {
+ return new Metadata { [fieldName] = new Metadata { ["$nin"] = values } };
+ }
+
+ throw new NotSupportedException("Pinecone does not support the NOT operator in vector search pre-filters");
+ }
+
+ private Metadata TranslateMethodCall(MethodCallExpression methodCall)
+ => methodCall switch
+ {
+ // Enumerable.Contains()
+ { Method.Name: nameof(Enumerable.Contains), Arguments: [var source, var item] } contains
+ when contains.Method.DeclaringType == typeof(Enumerable)
+ => this.TranslateContains(source, item),
+
+ // List.Contains()
+ {
+ Method:
+ {
+ Name: nameof(Enumerable.Contains),
+ DeclaringType: { IsGenericType: true } declaringType
+ },
+ Object: Expression source,
+ Arguments: [var item]
+ } when declaringType.GetGenericTypeDefinition() == typeof(List<>) => this.TranslateContains(source, item),
+
+ _ => throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}")
+ };
+
+ private Metadata TranslateContains(Expression source, Expression item)
+ {
+ switch (source)
+ {
+ // Contains over array column (r => r.Strings.Contains("foo"))
+ case var _ when this.TryTranslateFieldAccess(source, out _):
+ throw new NotSupportedException("Pinecone does not support Contains within array fields ($elemMatch) in vector search pre-filters");
+
+ // Contains over inline enumerable
+ case NewArrayExpression newArray:
+ var elements = new object?[newArray.Expressions.Count];
+
+ for (var i = 0; i < newArray.Expressions.Count; i++)
+ {
+ if (!TryGetConstant(newArray.Expressions[i], out var elementValue))
+ {
+ throw new NotSupportedException("Invalid element in array");
+ }
+
+ elements[i] = elementValue;
+ }
+
+ return ProcessInlineEnumerable(elements, item);
+
+ // Contains over captured enumerable (we inline)
+ case var _ when TryGetConstant(source, out var constantEnumerable)
+ && constantEnumerable is IEnumerable enumerable and not string:
+ return ProcessInlineEnumerable(enumerable, item);
+
+ default:
+ throw new NotSupportedException("Unsupported Contains expression");
+ }
+
+ Metadata ProcessInlineEnumerable(IEnumerable elements, Expression item)
+ {
+ if (!this.TryTranslateFieldAccess(item, out var storagePropertyName))
+ {
+ throw new NotSupportedException("Unsupported item type in Contains");
+ }
+
+ return new Metadata
+ {
+ [storagePropertyName] = new Metadata
+ {
+ ["$in"] = new MetadataValue(elements.Cast