|
3 | 3 | using System; |
4 | 4 | using System.Collections.Generic; |
5 | 5 | using System.Diagnostics.CodeAnalysis; |
| 6 | +using System.Linq; |
| 7 | +using System.Linq.Expressions; |
| 8 | +using System.Reflection; |
6 | 9 | using System.Runtime.CompilerServices; |
7 | 10 | using System.Threading; |
8 | 11 | using System.Threading.Tasks; |
@@ -276,14 +279,27 @@ private TextSearchStringMapper CreateTextSearchStringMapper() |
276 | 279 | private async IAsyncEnumerable<VectorSearchResult<TRecord>> ExecuteVectorSearchAsync(string query, TextSearchOptions? searchOptions, [EnumeratorCancellation] CancellationToken cancellationToken) |
277 | 280 | { |
278 | 281 | searchOptions ??= new TextSearchOptions(); |
| 282 | + |
| 283 | + var linqFilter = ConvertTextSearchFilterToLinq(searchOptions.Filter); |
279 | 284 | var vectorSearchOptions = new VectorSearchOptions<TRecord> |
280 | 285 | { |
281 | | -#pragma warning disable CS0618 // VectorSearchFilter is obsolete |
282 | | - OldFilter = searchOptions.Filter?.FilterClauses is not null ? new VectorSearchFilter(searchOptions.Filter.FilterClauses) : null, |
283 | | -#pragma warning restore CS0618 // VectorSearchFilter is obsolete |
284 | 286 | Skip = searchOptions.Skip, |
285 | 287 | }; |
286 | 288 |
|
| 289 | + // Use modern LINQ filtering if conversion was successful |
| 290 | + if (linqFilter != null) |
| 291 | + { |
| 292 | + vectorSearchOptions.Filter = linqFilter; |
| 293 | + } |
| 294 | + else if (searchOptions.Filter?.FilterClauses != null && searchOptions.Filter.FilterClauses.Any()) |
| 295 | + { |
| 296 | + // For complex filters that couldn't be converted to LINQ, |
| 297 | + // fall back to the legacy approach but with minimal overhead |
| 298 | +#pragma warning disable CS0618 // VectorSearchFilter is obsolete |
| 299 | + vectorSearchOptions.OldFilter = new VectorSearchFilter(searchOptions.Filter.FilterClauses); |
| 300 | +#pragma warning restore CS0618 // VectorSearchFilter is obsolete |
| 301 | + } |
| 302 | + |
287 | 303 | await foreach (var result in this.ExecuteVectorSearchCoreAsync(query, vectorSearchOptions, searchOptions.Top, cancellationToken).ConfigureAwait(false)) |
288 | 304 | { |
289 | 305 | yield return result; |
@@ -406,5 +422,72 @@ private async IAsyncEnumerable<string> GetResultsAsStringAsync(IAsyncEnumerable< |
406 | 422 | } |
407 | 423 | } |
408 | 424 |
|
| 425 | + /// <summary> |
| 426 | + /// Converts a legacy TextSearchFilter to a modern LINQ expression for direct filtering. |
| 427 | + /// This eliminates the need for obsolete VectorSearchFilter conversion. |
| 428 | + /// </summary> |
| 429 | + /// <param name="filter">The legacy TextSearchFilter to convert.</param> |
| 430 | + /// <returns>A LINQ expression equivalent to the filter, or null if no filter is provided.</returns> |
| 431 | + private static Expression<Func<TRecord, bool>>? ConvertTextSearchFilterToLinq(TextSearchFilter? filter) |
| 432 | + { |
| 433 | + if (filter?.FilterClauses == null || !filter.FilterClauses.Any()) |
| 434 | + { |
| 435 | + return null; |
| 436 | + } |
| 437 | + |
| 438 | + // For now, handle simple equality filters (most common case) |
| 439 | + // This covers the basic TextSearchFilter.Equality(fieldName, value) usage |
| 440 | + var clauses = filter.FilterClauses.ToList(); |
| 441 | + |
| 442 | + if (clauses.Count == 1 && clauses[0] is EqualToFilterClause equalityClause) |
| 443 | + { |
| 444 | + return CreateEqualityExpression(equalityClause.FieldName, equalityClause.Value); |
| 445 | + } |
| 446 | + |
| 447 | + // For complex filters, return null to maintain backward compatibility |
| 448 | + // These cases are rare and would require more complex expression building |
| 449 | + return null; |
| 450 | + } |
| 451 | + |
| 452 | + /// <summary> |
| 453 | + /// Creates a LINQ equality expression for a given field name and value. |
| 454 | + /// </summary> |
| 455 | + /// <param name="fieldName">The property name to compare.</param> |
| 456 | + /// <param name="value">The value to compare against.</param> |
| 457 | + /// <returns>A LINQ expression representing fieldName == value.</returns> |
| 458 | + private static Expression<Func<TRecord, bool>>? CreateEqualityExpression(string fieldName, object value) |
| 459 | + { |
| 460 | + try |
| 461 | + { |
| 462 | + // Create parameter: record => |
| 463 | + var parameter = Expression.Parameter(typeof(TRecord), "record"); |
| 464 | + |
| 465 | + // Get property: record.FieldName |
| 466 | + var property = typeof(TRecord).GetProperty(fieldName, BindingFlags.Public | BindingFlags.Instance); |
| 467 | + if (property == null) |
| 468 | + { |
| 469 | + // Property not found, return null to maintain compatibility |
| 470 | + return null; |
| 471 | + } |
| 472 | + |
| 473 | + var propertyAccess = Expression.Property(parameter, property); |
| 474 | + |
| 475 | + // Create constant: value |
| 476 | + var constant = Expression.Constant(value); |
| 477 | + |
| 478 | + // Create equality: record.FieldName == value |
| 479 | + var equality = Expression.Equal(propertyAccess, constant); |
| 480 | + |
| 481 | + // Create lambda: record => record.FieldName == value |
| 482 | + return Expression.Lambda<Func<TRecord, bool>>(equality, parameter); |
| 483 | + } |
| 484 | + catch (Exception) |
| 485 | + { |
| 486 | + // If any reflection or expression building fails, return null |
| 487 | + // This maintains backward compatibility rather than throwing exceptions |
| 488 | + return null; |
| 489 | + } |
| 490 | + } |
| 491 | + |
409 | 492 | #endregion |
410 | 493 | } |
0 commit comments