Elastic.SemanticKernel.Connectors.Elasticsearch/ElasticsearchVectorStoreCollectionSearchMapping.cs (89 lines of code) (raw):

// Licensed to Elasticsearch B.V under one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Linq; using Elastic.Clients.Elasticsearch; using Elastic.Clients.Elasticsearch.QueryDsl; using Microsoft.Extensions.VectorData; using Microsoft.SemanticKernel; namespace Elastic.SemanticKernel.Connectors.Elasticsearch; /// <summary> /// Contains mapping helpers to use when searching for documents using Elasticsearch. /// </summary> internal static class ElasticsearchVectorStoreCollectionSearchMapping { /// <summary> /// Build a list of Elasticsearch filter <see cref="Query"/> from the provided <see cref="VectorSearchFilter"/>. /// </summary> /// <param name="basicVectorSearchFilter">The <see cref="VectorSearchFilter"/> to build the Elasticsearch filter queries from.</param> /// <param name="propertyToStorageName">A mapping from <see cref="VectorStoreRecordDefinition" /> to storage model property name.</param> /// <returns>The Elasticsearch filter queries.</returns> /// <exception cref="NotSupportedException">Thrown when the provided filter contains unsupported types, values or unknown properties.</exception> public static ICollection<Query> BuildFilter(VectorSearchFilter? basicVectorSearchFilter, Dictionary<VectorStoreRecordProperty, string> propertyToStorageName) { Verify.NotNull(propertyToStorageName); if (basicVectorSearchFilter is null) { return []; } var filterClauses = basicVectorSearchFilter.FilterClauses.ToArray(); var filterQueries = new List<Query>(); foreach (var filterClause in filterClauses) { switch (filterClause) { case EqualToFilterClause equalToClause: { var mapping = GetPropertyNameMapping(equalToClause.FieldName); VerifyFilterable(mapping.Key); filterQueries.Add(Query.Term(new TermQuery(mapping.Value!) { Value = FieldValueFromValue(equalToClause.Value) })); break; } case AnyTagEqualToFilterClause anyTagEqualToClause: { var mapping = GetPropertyNameMapping(anyTagEqualToClause.FieldName); VerifyFilterable(mapping.Key); filterQueries.Add(Query.Terms(new TermsQuery { Field = mapping.Value!, Terms = new TermsQueryField([FieldValueFromValue(anyTagEqualToClause.Value)]) })); break; } default: throw new NotSupportedException($"Filter clause of type {filterClause.GetType().FullName} is not supported."); } } return filterQueries; KeyValuePair<VectorStoreRecordProperty, string> GetPropertyNameMapping(string dataModelPropertyName) { var result = propertyToStorageName .FirstOrDefault(x => string.Equals(x.Key.DataModelPropertyName, dataModelPropertyName, StringComparison.Ordinal)); if (result.Key is null) { throw new NotSupportedException($"Property '{dataModelPropertyName}' is not supported as a filter value."); } return result; } static void VerifyFilterable(VectorStoreRecordProperty property) { if (property is not VectorStoreRecordDataProperty { IsFilterable: true }) { throw new NotSupportedException($"Property '{property.DataModelPropertyName}' can not be used for filtering."); } } } /// <summary> /// TODO: TBC /// </summary> /// <param name="value"></param> /// <returns></returns> /// <exception cref="NotSupportedException"></exception> private static FieldValue FieldValueFromValue(object? value) { // TODO: Implement FieldValue.FromValue() in Elasticsearch client // TODO: FieldValue.Any() // TODO: FieldValue.Array() return value switch { null => FieldValue.Null, bool v => v, float v => v, double v => v, sbyte v => v, short v => v, int v => v, long v => v, byte v => v, ushort v => v, uint v => v, ulong v => v, string v => v, char v => v, _ => throw new NotSupportedException($"Unsupported filter value type '{value!.GetType().Name}'.") }; } }