diff --git a/src/ChromaDB.VectorData/ChromaModelBuilder.cs b/src/ChromaDB.VectorData/ChromaModelBuilder.cs index 1d8948a..de54c02 100644 --- a/src/ChromaDB.VectorData/ChromaModelBuilder.cs +++ b/src/ChromaDB.VectorData/ChromaModelBuilder.cs @@ -44,6 +44,14 @@ protected override void ValidateProperty(PropertyModel propertyModel, VectorStor { base.ValidateProperty(propertyModel, definition); + // Chroma reserves the metadata keys that start with #, like #id and #document of the Search API: a filter on #id selects + // the record with that id. + if (propertyModel is DataPropertyModel && propertyModel.StorageName.StartsWith("#", StringComparison.Ordinal)) + { + throw new NotSupportedException( + $"Property '{propertyModel.ModelName}' has the storage name '{propertyModel.StorageName}', but Chroma reserves the metadata keys that start with #."); + } + switch (propertyModel) { case VectorPropertyModel vectorProperty: diff --git a/test/ChromaDB.VectorData.UnitTests/ChromaCollectionTests.cs b/test/ChromaDB.VectorData.UnitTests/ChromaCollectionTests.cs index 3dbe0dc..f7aa61e 100644 --- a/test/ChromaDB.VectorData.UnitTests/ChromaCollectionTests.cs +++ b/test/ChromaDB.VectorData.UnitTests/ChromaCollectionTests.cs @@ -53,6 +53,68 @@ public ChromaCollectionTests() public void RejectsADictionaryRecord() => Assert.Throws(() => new ChromaCollection>(this._chromaClientMock.Object, TestCollectionName, ownsClient: false)); + [Theory] + [InlineData("#id")] + [InlineData("#document")] + [InlineData("#embedding")] + [InlineData("#metadata")] + [InlineData("#score")] + [InlineData("#other")] + public void RejectsADataPropertyWhoseStorageNameStartsWithAHash(string storageName) + { + // Chroma reserves the metadata keys that start with #: a filter on #id would select the record with that id. + var definition = new VectorStoreCollectionDefinition + { + Properties = + [ + new VectorStoreKeyProperty("Key", typeof(string)), + new VectorStoreDataProperty("Tag", typeof(string)) { StorageName = storageName }, + new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory), 4), + ] + }; + + var exception = Assert.Throws(() => new ChromaDynamicCollection(this._chromaClientMock.Object, TestCollectionName, ownsClient: false, new() { Definition = definition })); + Assert.Contains($"'{storageName}'", exception.Message); + } + + [Fact] + public void RejectsAStorageNameThatStartsWithAHashOnARecordType() + => Assert.Throws(() => new ChromaCollection(this._chromaClientMock.Object, TestCollectionName, ownsClient: false)); + + [Fact] + public void RejectsAFullTextPropertyWhoseStorageNameStartsWithAHash() + { + // The full-text property stored as the document is also a metadata key: the client copies the document into it. + var definition = new VectorStoreCollectionDefinition + { + Properties = + [ + new VectorStoreKeyProperty("Key", typeof(string)), + new VectorStoreDataProperty("Text", typeof(string)) { StorageName = "#document", IsFullTextIndexed = true }, + new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory), 4), + ] + }; + + Assert.Throws(() => new ChromaDynamicCollection(this._chromaClientMock.Object, TestCollectionName, ownsClient: false, new() { Definition = definition })); + } + + [Fact] + public void AcceptsAHashInsideAStorageName() + { + var definition = new VectorStoreCollectionDefinition + { + Properties = + [ + new VectorStoreKeyProperty("Key", typeof(string)), + new VectorStoreDataProperty("Tag", typeof(string)) { StorageName = "tag#1" }, + new VectorStoreVectorProperty("Vector", typeof(ReadOnlyMemory), 4), + ] + }; + + using var sut = new ChromaDynamicCollection(this._chromaClientMock.Object, TestCollectionName, ownsClient: false, new() { Definition = definition }); + Assert.Equal(TestCollectionName, sut.Name); + } + [Fact] public void DynamicCollectionsTakeAClientAndADefinition() { @@ -514,4 +576,16 @@ private static ChromaCollection HybridChromaCollection(params string[] indexes) private static string Bm25Index(string key, string sourceKey) => "\"" + key + "\":{\"sparse_vector\":{\"sparse_vector_index\":{\"enabled\":true,\"config\":{\"source_key\":\"" + sourceKey + "\",\"bm25\":true," + "\"embedding_function\":{\"type\":\"known\",\"name\":\"chroma_bm25\",\"config\":{\"k\":1.2,\"b\":0.75,\"avg_doc_length\":256,\"token_max_length\":40}}}}}}"; + + private sealed class HashStorageNameRecord + { + [VectorStoreKey] + public string Key { get; set; } = ""; + + [VectorStoreData(StorageName = "#id")] + public string? Tag { get; set; } + + [VectorStoreVector(4)] + public ReadOnlyMemory Vector { get; set; } + } }