diff --git a/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Comparison.cs b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Comparison.cs new file mode 100644 index 000000000..7435ae1c4 --- /dev/null +++ b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Comparison.cs @@ -0,0 +1,68 @@ +using Elastic.Clients.Elasticsearch.QueryDsl; +using System; +using System.Linq.Expressions; + +namespace LINGYUN.Abp.Elasticsearch; + +public partial class ExpressionQueryTranslator +{ + /// + /// 尝试翻译包含方法调用的比较表达式 + /// + private Query? TryTranslateMethodComparison(BinaryExpression node, string? prefix, IndexMappingInfo? mappingInfo) + { + // 处理 (field.Method(value) == 0) 或 (field.Method(value) >= 0) 这类表达式 + if (node.Left is MethodCallExpression leftMethodCall) + { + return TranslateMethodComparison(leftMethodCall, node.Right, node.NodeType, prefix, mappingInfo); + } + + // 处理 (0 == field.Method(value)) 或 (0 <= field.Method(value)) 这类表达式 + if (node.Right is MethodCallExpression rightMethodCall) + { + // 反转比较操作符 + var reversedNodeType = ReverseComparisonOperator(node.NodeType); + return TranslateMethodComparison(rightMethodCall, node.Left, reversedNodeType, prefix, mappingInfo); + } + + return null; + } + + /// + /// 翻译方法调用比较 + /// + private Query? TranslateMethodComparison( + MethodCallExpression methodCall, + Expression constantExpression, + ExpressionType comparisonType, + string? prefix, + IndexMappingInfo? mappingInfo) + { + // 限定string方法调用 + if (methodCall.Method.DeclaringType != typeof(string)) + { + return methodCall.Method.Name switch + { + nameof(string.CompareTo) => TranslateStringCompareToComparison(methodCall, constantExpression, comparisonType, prefix, mappingInfo), + nameof(string.IndexOf) => TranslateStringIndexOfComparison(methodCall, constantExpression, comparisonType, prefix, mappingInfo), + _ => null, + }; + } + return null; + } + + /// + /// 反转比较操作符 + /// + private ExpressionType ReverseComparisonOperator(ExpressionType nodeType) + { + return nodeType switch + { + ExpressionType.GreaterThan => ExpressionType.LessThan, + ExpressionType.GreaterThanOrEqual => ExpressionType.LessThanOrEqual, + ExpressionType.LessThan => ExpressionType.GreaterThan, + ExpressionType.LessThanOrEqual => ExpressionType.GreaterThanOrEqual, + _ => nodeType + }; + } +} diff --git a/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.String.cs b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.String.cs index 47d1ff6d2..29b9af748 100644 --- a/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.String.cs +++ b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.String.cs @@ -288,6 +288,116 @@ public partial class ExpressionQueryTranslator }; } + /// + /// 翻译 string.CompareTo 比较 + /// + private Query? TranslateStringCompareToComparison( + MethodCallExpression methodCall, + Expression constantExpression, + ExpressionType comparisonType, + string? prefix, + IndexMappingInfo? mappingInfo) + { + var compareValue = Evaluate(constantExpression)?.ToString() ?? string.Empty; + + // CompareTo 返回值: + // 0: 相等 + // > 0: 当前字符串在排序顺序中位于参数之后 + // < 0: 当前字符串在排序顺序中位于参数之前 + + switch (comparisonType) + { + case ExpressionType.Equal: + // CompareTo == 0 表示相等 + var (field, value) = GetStringMethodOperands(methodCall, prefix, mappingInfo); + return BuildEquality(field, value); + + case ExpressionType.NotEqual: + // CompareTo != 0 表示不相等 + var (notEqualField, notEqualValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo); + var notEqualQuery = BuildEquality(notEqualField, notEqualValue); + return new BoolQuery + { + MustNot = new Query[] { notEqualQuery } + }; + + case ExpressionType.GreaterThan: + // CompareTo > 0 表示当前字段值大于比较值 + // 这可以简化处理,但这里先返回 null 让默认处理逻辑处理 + return null; + + case ExpressionType.GreaterThanOrEqual: + // CompareTo >= 0 表示当前字段值大于或等于比较值 + return null; + + case ExpressionType.LessThan: + // CompareTo < 0 表示当前字段值小于比较值 + return null; + + case ExpressionType.LessThanOrEqual: + // CompareTo <= 0 表示当前字段值小于或等于比较值 + return null; + + default: + return null; + } + } + + /// + /// 翻译 string.IndexOf 比较 + /// + private Query? TranslateStringIndexOfComparison( + MethodCallExpression methodCall, + Expression constantExpression, + ExpressionType comparisonType, + string? prefix, + IndexMappingInfo? mappingInfo) + { + var indexValue = Evaluate(constantExpression)?.ToString() ?? string.Empty; + + // IndexOf 返回值: + // >= 0: 找到了子字符串 + // < 0: 没有找到子字符串 + + switch (comparisonType) + { + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.GreaterThan: + // IndexOf >= 0 表示包含 + var (field, value) = GetStringMethodOperands(methodCall, prefix, mappingInfo); + var fieldMapping = mappingInfo?.GetField(field.Path); + return TranslateStringContains(field, fieldMapping, value); + + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + // IndexOf < 0 表示不包含 + var (notContainsField, notContainsValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo); + var notContainsFieldMapping = mappingInfo?.GetField(notContainsField.Path); + var containsQuery = TranslateStringContains(notContainsField, notContainsFieldMapping, notContainsValue); + return new BoolQuery + { + MustNot = new Query[] { containsQuery } + }; + + case ExpressionType.Equal: + // IndexOf == 某个值,这里简化处理,只处理 == -1(不包含)的情况 + if (indexValue == "-1") + { + var (eqField, eqValue) = GetStringMethodOperands(methodCall, prefix, mappingInfo); + var eqFieldMapping = mappingInfo?.GetField(eqField.Path); + var eqContainsQuery = TranslateStringContains(eqField, eqFieldMapping, eqValue); + return new BoolQuery + { + MustNot = new Query[] { eqContainsQuery } + }; + } + return null; + + default: + return null; + } + } + /// /// 翻译字符串实例方法(获取字段和值) /// diff --git a/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs index a52687e52..a8352be86 100644 --- a/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs +++ b/aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs @@ -213,6 +213,12 @@ public partial class ExpressionQueryTranslator : IExpressionQueryTranslator, ISi /// protected virtual Query TranslateComparison(BinaryExpression node, string? prefix, IndexMappingInfo? mappingInfo) { + var methodCallQuery = TryTranslateMethodComparison(node, prefix, mappingInfo); + if (methodCallQuery != null) + { + return methodCallQuery; + } + var (fieldExpression, valueExpression) = ResolveOperands(node); var field = ResolveField(fieldExpression, prefix, mappingInfo); diff --git a/aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator_Tests.cs b/aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator_Tests.cs index 1d279f720..dbb60a6d3 100644 --- a/aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator_Tests.cs +++ b/aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator_Tests.cs @@ -691,6 +691,186 @@ public abstract class ExpressionQueryTranslatorTests : AbpIntegr value.ShouldBe("test"); } + [Fact] + public async virtual Task Translate_StringCompareTo_ReturnsTermQuery() + { + // Act + var query = await _expressionQueryTranslator.TranslateAsync(TestDocumentIndexNames.Index, x => x.Name!.CompareTo("test") == 0); + + // Assert + query.ShouldNotBeNull(); + query.Term.ShouldNotBeNull(); + query.Term.Field.ToString().ShouldBe("Name.keyword"); + query.Term.Value.TryGetString(out var value).ShouldBeTrue(); + value.ShouldBe("test"); + } + + [Fact] + public async virtual Task Translate_StringIndexOf_ReturnsWildcardQuery() + { + // Act + var query = await _expressionQueryTranslator.TranslateAsync(TestDocumentIndexNames.Index, x => x.Name!.IndexOf("test") >= 0); + + // Assert + query.ShouldNotBeNull(); + query.Wildcard.ShouldNotBeNull(); + query.Wildcard.Field.ToString().ShouldBe("Name.keyword"); + query.Wildcard.Value.ShouldBe("*test*"); + } + + [Fact] + public async virtual Task Translate_StringIsNullOrEmpty_ReturnsBoolShouldQuery() + { + // Act + var query = await _expressionQueryTranslator.TranslateAsync(TestDocumentIndexNames.Index, x => string.IsNullOrEmpty(x.Name)); + + // Assert + query.ShouldNotBeNull(); + query.Bool.ShouldNotBeNull(); + query.Bool.Should.ShouldNotBeNull(); + query.Bool.Should.Count.ShouldBe(2); + query.Bool.MinimumShouldMatch.ShouldNotBeNull(); + query.Bool.MinimumShouldMatch.Value1.ShouldBe(1); + + // 验证包含 Exists 查询(must_not) + var nullCheckQuery = query.Bool.Should.FirstOrDefault(q => q.Bool?.MustNot != null); + nullCheckQuery.ShouldNotBeNull(); + nullCheckQuery.Bool.ShouldNotBeNull(); + nullCheckQuery.Bool.MustNot.ShouldNotBeNull(); + nullCheckQuery.Bool.MustNot.Count.ShouldBe(1); + nullCheckQuery.Bool.MustNot.First().Exists.ShouldNotBeNull(); + + // 验证包含空字符串 Term 查询 + var emptyStringQuery = query.Bool.Should.FirstOrDefault(q => q.Term != null); + emptyStringQuery.ShouldNotBeNull(); + emptyStringQuery.Term.ShouldNotBeNull(); + emptyStringQuery.Term.Value.TryGetString(out var value).ShouldBeTrue(); + value.ShouldBe(string.Empty); + } + + [Fact] + public async virtual Task Translate_StringIsNullOrWhiteSpace_ReturnsBoolShouldQuery() + { + // Act + var query = await _expressionQueryTranslator.TranslateAsync(TestDocumentIndexNames.Index, x => string.IsNullOrWhiteSpace(x.Name)); + + // Assert + query.ShouldNotBeNull(); + query.Bool.ShouldNotBeNull(); + query.Bool.Should.ShouldNotBeNull(); + query.Bool.Should.Count.ShouldBe(3); + query.Bool.MinimumShouldMatch.ShouldNotBeNull(); + query.Bool.MinimumShouldMatch.Value1.ShouldBe(1); + + // 验证包含 Exists 查询(must_not) + var nullCheckQuery = query.Bool.Should.FirstOrDefault(q => q.Bool?.MustNot != null); + nullCheckQuery.ShouldNotBeNull(); + nullCheckQuery.Bool.ShouldNotBeNull(); + nullCheckQuery.Bool.MustNot.ShouldNotBeNull(); + nullCheckQuery.Bool.MustNot.Count.ShouldBe(1); + nullCheckQuery.Bool.MustNot.First().Exists.ShouldNotBeNull(); + + // 验证包含空字符串 Term 查询 + var emptyStringQuery = query.Bool.Should.FirstOrDefault(q => q.Term != null); + emptyStringQuery.ShouldNotBeNull(); + emptyStringQuery.Term.ShouldNotBeNull(); + emptyStringQuery.Term.Value.TryGetString(out var termValue).ShouldBeTrue(); + termValue.ShouldBe(string.Empty); + + // 验证包含正则表达式查询 + var regexpQuery = query.Bool.Should.FirstOrDefault(q => q.Regexp != null); + regexpQuery.ShouldNotBeNull(); + regexpQuery.Regexp.ShouldNotBeNull(); + regexpQuery.Regexp.Value.ShouldBe(@"^\s*$"); + } + + [Fact] + public async virtual Task Translate_StringEquals_WithStringComparisonOrdinal_ReturnsTermQuery() + { + // Act + var query = await _expressionQueryTranslator.TranslateAsync(TestDocumentIndexNames.Index, + x => x.Name!.Equals("test", StringComparison.Ordinal)); + + // Assert + query.ShouldNotBeNull(); + query.Term.ShouldNotBeNull(); + query.Term.Field.ToString().ShouldBe("Name.keyword"); + query.Term.Value.TryGetString(out var value).ShouldBeTrue(); + value.ShouldBe("test"); + } + + [Fact] + public async virtual Task Translate_StringEquals_WithStringComparisonIgnoreCase_ReturnsTermQuery() + { + // Act + var query = await _expressionQueryTranslator.TranslateAsync(TestDocumentIndexNames.Index, + x => x.Name!.Equals("test", StringComparison.OrdinalIgnoreCase)); + + // Assert + query.ShouldNotBeNull(); + query.Term.ShouldNotBeNull(); + query.Term.Field.ToString().ShouldBe("Name.keyword"); + query.Term.Value.TryGetString(out var value).ShouldBeTrue(); + value.ShouldBe("test"); + } + + [Fact] + public async virtual Task Translate_StaticStringEquals_ReturnsTermQuery() + { + // Act + var query = await _expressionQueryTranslator.TranslateAsync(TestDocumentIndexNames.Index, + x => string.Equals(x.Name, "test")); + + // Assert + query.ShouldNotBeNull(); + query.Term.ShouldNotBeNull(); + query.Term.Field.ToString().ShouldBe("Name.keyword"); + query.Term.Value.TryGetString(out var value).ShouldBeTrue(); + value.ShouldBe("test"); + } + + [Fact] + public async virtual Task Translate_StringContains_WithEmptyString_ReturnsWildcardQueryWithOnlyWildcards() + { + // Act + var query = await _expressionQueryTranslator.TranslateAsync(TestDocumentIndexNames.Index, + x => x.Name!.Contains(string.Empty)); + + // Assert + query.ShouldNotBeNull(); + query.Wildcard.ShouldNotBeNull(); + query.Wildcard.Field.ToString().ShouldBe("Name.keyword"); + query.Wildcard.Value.ShouldBe("**"); + } + + [Fact] + public async virtual Task Translate_StringContains_WildcardField_EscapesBackslash() + { + // Act + var query = await _expressionQueryTranslator.TranslateAsync(TestDocumentIndexNames.Index, + x => x.Exceptions!.Contains("test\\value")); + + // Assert + query.ShouldNotBeNull(); + query.Wildcard.ShouldNotBeNull(); + query.Wildcard.Field.ToString().ShouldBe("Exceptions"); + query.Wildcard.Value.ShouldBe("*test\\\\value*"); + } + + [Fact] + public async virtual Task Translate_StringEndsWith_TextWithoutKeyword_ReturnsMatchPhraseQuery() + { + // Act + var query = await _expressionQueryTranslator.TranslateAsync(TestDocumentIndexNames.Index, + x => x.Description!.EndsWith("world")); + + // Assert + query.ShouldNotBeNull(); + query.MatchPhrase.ShouldNotBeNull(); + query.MatchPhrase.Field.ToString().ShouldBe("Description"); + query.MatchPhrase.Query.ShouldBe("world"); + } + #endregion #region 集合方法测试