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 集合方法测试