Browse Source

feat: Add "CompareTo" and "IndexOf" translations

pull/1554/head
colin 1 week ago
parent
commit
73593aed52
  1. 68
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.Comparison.cs
  2. 110
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.String.cs
  3. 6
      aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs
  4. 180
      aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator_Tests.cs

68
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
{
/// <summary>
/// 尝试翻译包含方法调用的比较表达式
/// </summary>
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;
}
/// <summary>
/// 翻译方法调用比较
/// </summary>
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;
}
/// <summary>
/// 反转比较操作符
/// </summary>
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
};
}
}

110
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.String.cs

@ -288,6 +288,116 @@ public partial class ExpressionQueryTranslator
}; };
} }
/// <summary>
/// 翻译 string.CompareTo 比较
/// </summary>
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;
}
}
/// <summary>
/// 翻译 string.IndexOf 比较
/// </summary>
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;
}
}
/// <summary> /// <summary>
/// 翻译字符串实例方法(获取字段和值) /// 翻译字符串实例方法(获取字段和值)
/// </summary> /// </summary>

6
aspnet-core/framework/elasticsearch/LINGYUN.Abp.Elasticsearch/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator.cs

@ -213,6 +213,12 @@ public partial class ExpressionQueryTranslator : IExpressionQueryTranslator, ISi
/// </summary> /// </summary>
protected virtual Query TranslateComparison(BinaryExpression node, string? prefix, IndexMappingInfo? mappingInfo) 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 (fieldExpression, valueExpression) = ResolveOperands(node);
var field = ResolveField(fieldExpression, prefix, mappingInfo); var field = ResolveField(fieldExpression, prefix, mappingInfo);

180
aspnet-core/tests/LINGYUN.Abp.Elasticsearch.Tests/LINGYUN/Abp/Elasticsearch/ExpressionQueryTranslator_Tests.cs

@ -691,6 +691,186 @@ public abstract class ExpressionQueryTranslatorTests<TStartupModule> : AbpIntegr
value.ShouldBe("test"); value.ShouldBe("test");
} }
[Fact]
public async virtual Task Translate_StringCompareTo_ReturnsTermQuery()
{
// Act
var query = await _expressionQueryTranslator.TranslateAsync<TestDocument>(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<TestDocument>(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<TestDocument>(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<TestDocument>(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<TestDocument>(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<TestDocument>(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<TestDocument>(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<TestDocument>(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<TestDocument>(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<TestDocument>(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 #endregion
#region 集合方法测试 #region 集合方法测试

Loading…
Cancel
Save