Files
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

34 KiB

status, contact, date, deciders, consulted, informed
status contact date deciders consulted informed
accepted westey-m 2024-03-10 westey-m, rbarreto, markwallace, sergeymenshykh, eavanvalkenburg, roji, dmytrostruk rbarreto, markwallace, sergeymenshykh, eavanvalkenburg, roji, dmytrostruk rbarreto, markwallace, sergeymenshykh, eavanvalkenburg, roji, dmytrostruk

Support Hybrid Search in VectorStore abstractions

Context and Problem Statement

In addition to simple vector search, many databases also support Hybrid search. Hybrid search typically results in higher quality search results, and therefore the ability to do Hybrid search via VectorStore abstractions is an important feature to add.

The way in which Hybrid search is supported varies by database. The two most common ways of supporting hybrid search is:

  1. Using dense vector search and keyword/fulltext search in parallel, and then combining the results.
  2. Using dense vector search and sparse vector search in parallel, and then combining the results.

Sparse vectors are different from dense vectors in that they typically have many more dimensions, but with many of the dimensions being zero. Sparse vectors, when used with text search, have a dimension for each word/token in a vocabulary, with the value indicating the importance of the word in the source text. The more common the word in a specific chunk of text, and the less common the word is in the corpus, the higher the value in the sparse vector.

There are various mechanisms for generating sparse vectors, such as

While these are supported well in Python, they are not well supported in .net today. Adding support for generating sparse vectors is out of scope of this ADR.

More background information:

ML.Net contains an implementation of TF-IDF that could be used to generate sparse vectors in .net. See here for an example.

Hybrid search support in different databases

Feature Azure AI Search Weaviate Redis Chroma Pinecone PostgreSql Qdrant Milvus Elasticsearch CosmosDB NoSql MongoDB
Hybrid search supported Y Y N (No parallel execution with fusion) N Y Y Y Y Y Y Y
Hybrid search definition Vector + FullText Vector + Keyword (BM25F) Vector + Sparse Vector for keywords Vector + Keyword Vector + SparseVector / Keyword Vector + SparseVector Vector + FullText Vector + Fulltext (BM25) Vector + FullText
Fusion method configurable N Y ? Y Y Y Y, but only one option Y, but only one option N
Fusion methods RRF Ranked/RelativeScore ? Build your own RRF / DBSF RRF / Weighted RRF RRF RRF
Hybrid Search Input Params Vector + string Vector + string Vector + SparseVector Vector + String Vector + SparseVector Vector + SparseVector Vector + string Vector + string array Vector + string
Sparse Distance Function n/a n/a dotproduct only for both dense and sparse, 1 setting for both n/a dotproduct Inner Product n/a n/a n/a
Sparse Indexing options n/a n/a no separate config to dense n/a ondisk / inmemory + IDF SPARSE_INVERTED_INDEX / SPARSE_WAND n/a n/a n/a
Sparse data model n/a n/a indices & values arrays n/a indices & values arrays sparse matrix / List of dict / list of tuples n/a n/a n/a
Keyword matching behavior Space Separated with SearchMode=any does OR, searchmode=all does AND Tokenization with split by space, affects ranking n/a Tokenization

No FTS Index: Exact Substring match

FTS Index present: All words must be present

n/a And/Or capabilities - Allows multiple multi-word phrases with OR and a single multi-word prhase where the words can be OR'd or AND'd

Glossary:

  • RRF = Reciprical Rank Fusion
  • DBSF = Distribution-Based Score Fusion
  • IDF = Inverse Document Frequency

Language required for Cosmos DB NoSQL full text search configuration

Cosmos DB NoSQL requires a language to be specified for full text search and it requires full text search indexing for hybrid search to be enabled. We therefore need to support a way of specifying the language when creating the index.

Cosmos DB NoSQL is the only database from our sample that has a required setting of this type.

Feature Azure AI Search Weaviate Redis Chroma Pinecone PostgreSql Qdrant Milvus Elasticsearch CosmosDB NoSql MongoDB
Requires FullTextSearch indexing for hybrid search Y Y n/a n/a n/a Y N optional n/a Y Y Y
Required FullTextSearch index options None required, many optional None required, none optional language required none required, some optional None required, many optional Language Required None required, many optional

Keyword Search interface options

Each DB has different keyword search capabilities. Some only support a very basic interface when it comes to listing keywords for hybrid search. The following table is to list the compatibility of each DB with a specific keyword public interface we may want to support.

Feature Azure AI Search Weaviate PostgreSql Qdrant Elasticsearch CosmosDB NoSql MongoDB

string[] keyword

One word per element

Any matching word boosts ranking.

Y Y (have to join with spaces) Y (have to join with spaces) Y (via filter with multiple OR'd matches) Y Y Y (have to join with spaces)

string[] keyword

One or more words per element

All words in a single element have to be present to boost the ranking.

Y N Y Y (via filter with multiple OR'd matches and FTS Index) - N N

string[] keyword

One or more words per element

Multiple words in a single element is a phrase that must match exactly to boost the ranking.

Y N Y Only via filter with multiple OR'd matches and NO Index - N Y

string keyword

Space separated words

Any matching word boosts ranking.

Y Y Y N (would need to split words) - N (would need to split words) Y

Naming Options

Interface Name Method Name Parameters Options Class Name Keyword Property Selector Dense Vector Property Selector
KeywordVectorizedHybridSearch KeywordVectorizedHybridSearch string[] + Dense Vector KeywordVectorizedHybridSearchOptions FullTextPropertyName VectorPropertyName
SparseVectorizedHybridSearch SparseVectorizedHybridSearch Sparse Vector + Dense Vector SparseVectorizedHybridSearchOptions SparseVectorPropertyName VectorPropertyName
KeywordVectorizableTextHybridSearch KeywordVectorizableTextHybridSearch string[] + string KeywordVectorizableTextHybridSearchOptions FullTextPropertyName VectorPropertyName
SparseVectorizableTextHybridSearch SparseVectorizableTextHybridSearch string[] + string SparseVectorizableTextHybridSearchOptions SparseVectorPropertyName VectorPropertyName
Interface Name Method Name Parameters Options Class Name Keyword Property Selector Dense Vector Property Selector
KeywordVectorizedHybridSearch HybridSearch string[] + Dense Vector KeywordVectorizedHybridSearchOptions FullTextPropertyName VectorPropertyName
SparseVectorizedHybridSearch HybridSearch Sparse Vector + Dense Vector SparseVectorizedHybridSearchOptions SparseVectorPropertyName VectorPropertyName
KeywordVectorizableTextHybridSearch HybridSearch string[] + string KeywordVectorizableTextHybridSearchOptions FullTextPropertyName VectorPropertyName
SparseVectorizableTextHybridSearch HybridSearch string[] + string SparseVectorizableTextHybridSearchOptions SparseVectorPropertyName VectorPropertyName
Interface Name Method Name Parameters Options Class Name Keyword Property Selector Dense Vector Property Selector
HybridSearchWithKeywords HybridSearch string[] + Dense Vector HybridSearchOptions FullTextPropertyName VectorPropertyName
HybridSearchWithSparseVector HybridSearchWithSparseVector Sparse Vector + Dense Vector HybridSearchWithSparseVectorOptions SparseVectorPropertyName VectorPropertyName
HybridSearchWithKeywordsAndVectorizableText HybridSearch string[] + string HybridSearchOptions FullTextPropertyName VectorPropertyName
HybridSearchWithVectorizableKeywordsAndText HybridSearchWithSparseVector string[] + string HybridSearchWithSparseVectorOptions SparseVectorPropertyName VectorPropertyName
Area Type of search Params Method Name
Non-vector Search
Non-vector Search Regular, without vector Search
Vector Search with named methods
Vector Search With Vector ReadonlyMemory<float> vector VectorSearch
Vector Search With Vectorizable Text string text VectorSearchWithText
Vector Search With Vectorizable Image string/byte[]/other image VectorSearchWithImage
Vector Search With Vectorizable Image+Text string/byte[]/other image, string text VectorSearchWithImageAndText
Vector Search with named params
Vector Search With Vector new Vector(ReadonlyMemory<float>) VectorSearch
Vector Search With Vectorizable Text new VectorizableText(string text) VectorSearch
Vector Search With Vectorizable Image new VectorizableImage(string/byte[]/other image) VectorSearch
Vector Search With Vectorizable Image+Text VectorizableMultimodal(string/byte[]/other image, string text) VectorSearch
Hybrid Search
Hybrid Search With DenseVector and string[] keywords ReadonlyMemory<float> vector, string[] keywords HybridSearch
Hybrid Search With vectorizable string and string[] keywords string vectorizableText, string[] keywords HybridSearch
Hybrid Search With DenseVector and SparseVector ReadonlyMemory<float> vector, ? sparseVector HybridSearchWithSparseVector
Hybrid Search With vectorizable string and sparse vectorisable string[] keywords string vectorizableText, string[] vectorizableKeywords HybridSearchWithSparseVector
var collection;

// ----------------------- Method names vary -----------------------
// We'll need to add a new interface with a new method name for each data type that we want to search for.

public Task VectorSearch(ReadonlyMemory<float> vector, VectorSearchOptions options = null, CancellationToken cancellationToken);
public Task VectorSearchWithText(string text, VectorSearchOptions options = null, CancellationToken cancellationToken = null);
public Task VectorSearchWithImage(VectorizableData image, VectorSearchOptions options = null, CancellationToken cancellationToken = null);
collection.VectorSearchWithImageAndText(VectorizableData image, string text, VectorSearchOptions options = null, CancellationToken cancellationToken = null);

collection.VectorSearch(new ReadonlyMemory<float>([...]));
collection.VectorSearchWithText("Apples and oranges are tasty.");
collection.VectorSearchWithImage("fdslkjfskdlfjdslkjfdskljfdslkjfsd");
collection.VectorSearchWithImageAndText("fdslkjfskdlfjdslkjfdskljfdslkjfsd", "Apples and oranges are tasty.");

// ----------------------- Param types vary -----------------------
// We'll need to add a new interface for each data type that we want to search for.

// Vector Search
public Task VectorSearch<TRecord>(Embedding embedding, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken);
public Task VectorSearch<TRecord>(VectorizableImage vectorizableImage, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);
public Task VectorSearch<TRecord>(VectorizableMultimodal vectorizableMultiModal, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);

collection.VectorSearch(new Embedding(new ReadonlyMemory<float>([...])));
collection.VectorSearch(new VectorizableText("Apples and oranges are tasty."));
collection.VectorSearch(new VectorizableImage("fdslkjfskdlfjdslkjfdskljfdslkjfsd"));
collection.VectorSearch(new VectorizableMultimodal("fdslkjfskdlfjdslkjfdskljfdslkjfsd", "Apples and oranges are tasty."));

// Hybrid search
// Same as next option, since hybrid is currently explicitly dense vectors plus keywords.

// ----------------------- Array of params inheriting from a common base type -----------------------
// We can potentially add extension methods, to make it easier to use.
// We just need to add new embedding or vectorizable data types for new data types that we want to search for.

// Vector Search
public Task VectorSearch<TRecord>(Embedding embedding, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);
public Task VectorSearch<TRecord>(VectorizableData vectorizableData, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);
public Task VectorSearch<TRecord>(VectorizableData[] vectorizableData, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);
public Task VectorSearch<TRecord, TVectorType>(TVectorType embedding, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken);

// Convenience extension methods
public Task VectorSearch<TRecord>(Embedding embedding, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken);
public Task VectorSearch<TRecord>(string text, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);

public Task Search<TRecord>(NonVectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken);

collection.VectorSearch(new Embedding(new ReadonlyMemory<float>([...])));
collection.VectorSearch("Apples and oranges are tasty."); // Via extension?
collection.VectorSearch(new VectorizableData("Apples and oranges are tasty.", "text/plain"));

collection.VectorSearch(["Apples and oranges are tasty."]); // Via extension?
collection.VectorSearch([new VectorizableData("Apples and oranges are tasty.", "text/plain")]);
collection.VectorSearch([new VectorizableData("fdslkjfskdlfjdslkjfdskljfdslkjfsd", "image/jpeg")]);
collection.VectorSearch([new VectorizableData("fdslkjfskdlfjdslkjfdskljfdslkjfsd", "image/jpeg"), new VectorizableText("Apples and oranges are tasty.")]);

// Hybrid search
public Task HybridSearch<TRecord, TVectorType>(TVector vector, VectorizableData vectorizableData, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);

public Task HybridSearch<TRecord>(Embedding denseVector, Embedding sparseVector, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);
public Task HybridSearch<TRecord>(Embedding Densevector, VectorizableData sparseVectorizableData, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);
public Task HybridSearch<TRecord>(VectorizableData denseVectorizableData, VectorizableData sparseVectorizableData, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);
public Task HybridSearch<TRecord>(VectorizableData denseVectorizableData, Embedding sparseVector, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);

collection.HybridSearch(new Embedding(new ReadonlyMemory<float>([...])), ["Apples", "Oranges"], new() { VectorPropertyName = "DescriptionEmbedding", FullTextPropertyName = "Keywords" })
collection.HybridSearch(new VectorizableText("Apples and oranges are tasty."), ["Apples", "Oranges"], new() { VectorPropertyName = "DescriptionEmbedding", FullTextPropertyName = "Keywords" });
collection.HybridSearchWithSparseVector(new Embedding(new ReadonlyMemory<float>([...])), new SparseEmbedding(), new() { VectorPropertyName = "DescriptionEmbedding", SparseVectorPropertyName = "KeywordsEmbedding" });
collection.HybridSearchWithSparseVector(new VectorizableText("Apples and oranges are tasty."), new SparseEmbedding(), new() { VectorPropertyName = "DescriptionEmbedding", SparseVectorPropertyName = "KeywordsEmbedding" });
collection.HybridSearchWithSparseVector(new VectorizableText("Apples and oranges are tasty."), new SparseVectorizableText("Apples", "Oranges"), new() { VectorPropertyName = "DescriptionEmbedding", SparseVectorPropertyName = "KeywordsEmbedding" });

// ----------------------- One name, regular params, common options, with target property type determining search type -----------------------

// With generic vector (short term)
public Task HybridSearch<TRecord, TVectorType>(TVector vector, string[] keywords, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken);

// With embedding (long term)
public Task HybridSearch<TRecord>(Embedding vector, string[] keywords, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken);
public Task HybridSearch<TRecord>(Embedding vector, SparseEmbedding sparseVector, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken);
public Task HybridSearch<TRecord>(string vectorizableText, SparseEmbedding sparseVector, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken);
public Task HybridSearch<TRecord>(string vectorizableText, string[] sparseVectorizableText, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken);
public Task HybridSearch<TRecord>(Embedding vector, string[] sparseVectorizableText, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken);

// Is there a good name for the fulltextsearchproperty/sparsevectorproperty.
HybridSearchPropertyName
AdditionalSearchPropertyName
AdditionalPropertyName
SecondaryPropertyName
HybridSearchSecondaryPropertyName
KeywordsPropertyName
KeywordsSearchPropertyName

// ----------------------- Pass Embedding/VectorizableContent via common base class with target property name -----------------------

class SearchTarget<TRecord>();
class VectorSearchTarget<TRecord, TVectorType>(ReadonlyMemory<TVectorType> vector, Expression<Func<TRecord, object>> targetProperty) : SearchTarget<TRecord>();
class KeywordsSearchTarget<TRecord>(string[] keywords, Expression<Func<TRecord, object>> targetProperty) : SearchTarget<TRecord>();
class SparseSearchTarget<TRecord>(SparseVector vector, Expression<Func<TRecord, object>> targetProperty) : SearchTarget<TRecord>();

public Task HybridSearch(
    SearchTarget<TRecord>[] searchParams,
    HybridSearchOptions options = null,
    CancellationToken cancellationToken);
// Extension Methods:
public Task HybridSearch(
    ReadonlyMemory<float> vector vector,
    string targetVectorPropertyName,
    string[] keywords,
    string targetHybridSearchPropertyName,
    HybridSearchOptions options = null,
    CancellationToken cancellationToken);
public Task HybridSearch(
    ReadonlyMemory<float> vector vector,
    string targetVectorFieldName,
    SparseVector sparseVector,
    string targetHybridSearchPropertyName,
    HybridSearchOptions options = null,
    CancellationToken cancellationToken);
interface IKeywordVectorizedHybridSearch<TRecord>
{
    Task<VectorSearchResults<TRecord>> KeywordVectorizedHybridSearch<TVector>(
        TVector vector,
        ICollection<string> keywords,
        KeywordVectorizedHybridSearchOptions options,
        CancellationToken cancellationToken);
}

class KeywordVectorizedHybridSearchOptions
{
    // The name of the property to target the vector search against.
    public string? VectorPropertyName { get; init; }

    // The name of the property to target the text search against.
    public string? FullTextPropertyName { get; init; }

    public VectorSearchFilter? Filter { get; init; }
    public int Top { get; init; } = 3;
    public int Skip { get; init; } = 0;
    public bool IncludeVectors { get; init; } = false;
    public bool IncludeTotalCount { get; init; } = false;
}
interface ISparseVectorizedHybridSearch<TRecord>
{
    Task<VectorSearchResults<TRecord>> SparseVectorizedHybridSearch<TVector, TSparseVector>(
        TVector vector,
        TSparseVector sparsevector,
        SparseVectorizedHybridSearchOptions options,
        CancellationToken cancellationToken);
}

class SparseVectorizedHybridSearchOptions
{
    // The name of the property to target the dense vector search against.
    public string? VectorPropertyName { get; init; }
    // The name of the property to target the sparse vector search against.
    public string? SparseVectorPropertyName { get; init; }

    public VectorSearchFilter? Filter { get; init; }
    public int Top { get; init; } = 3;
    public int Skip { get; init; } = 0;
    public bool IncludeVectors { get; init; } = false;
    public bool IncludeTotalCount { get; init; } = false;
}
interface IKeywordVectorizableHybridSearch<TRecord>
{
    Task<VectorSearchResults<TRecord>> KeywordVectorizableHybridSearch(
        string searchText,
        ICollection<string> keywords,
        KeywordVectorizableHybridSearchOptions options = default,
        CancellationToken cancellationToken = default);
}

class KeywordVectorizableHybridSearchOptions
{
    // The name of the property to target the dense vector search against.
    public string? VectorPropertyName { get; init; }
    // The name of the property to target the text search against.
    public string? FullTextPropertyName { get; init; }

    public VectorSearchFilter? Filter { get; init; }
    public int Top { get; init; } = 3;
    public int Skip { get; init; } = 0;
    public bool IncludeVectors { get; init; } = false;
    public bool IncludeTotalCount { get; init; } = false;
}
interface ISparseVectorizableTextHybridSearch<TRecord>
{
    Task<VectorSearchResults<TRecord>> SparseVectorizableTextHybridSearch(
        string searchText,
        ICollection<string> keywords,
        SparseVectorizableTextHybridSearchOptions options = default,
        CancellationToken cancellationToken = default);
}

class SparseVectorizableTextHybridSearchOptions
{
    // The name of the property to target the dense vector search against.
    public string? VectorPropertyName { get; init; }
    // The name of the property to target the sparse vector search against.
    public string? SparseVectorPropertyName { get; init; }

    public VectorSearchFilter? Filter { get; init; }
    public int Top { get; init; } = 3;
    public int Skip { get; init; } = 0;
    public bool IncludeVectors { get; init; } = false;
    public bool IncludeTotalCount { get; init; } = false;
}

Decision Drivers

  • Support for generating sparse vectors is required to make sparse vector based hybrid search viable.
  • Multiple vectors per record scenarios need to be supported.
  • No database in our evaluation set have been identified as supporting converting text to sparse vectors in the database on upsert and storing those sparse vectors in a retrievable field. Of course some of these DBs may use sparse vectors internally to implement keyword search, without exposing them to the caller.

Scoping Considered Options

1. Keyword Hybrid Search Only

Only implement KeywordVectorizedHybridSearch & KeywordVectorizableTextHybridSearch for now, until we can add support for generating sparse vectors.

Implement KeywordVectorizedHybridSearch & KeywordVectorizableTextHybridSearch but only KeywordVectorizableTextHybridSearch, since no database in our evaluation set supports generating sparse vectors in the database. This will require us to produce code that can generate sparse vectors from text.

Create all four interfaces and implement an implementation of SparseVectorizableTextHybridSearch that generates the sparse vector in the client code. This will require us to produce code that can generate sparse vectors from text.

Some databases support a more generalized version of hybrid search, where you can take two (or sometimes more) searches of any type and combine the results of these using your chosen fusion method. You can implement Vector + Keyword search using this more generalized search. For databases that support only Vector + Keyword hybrid search though, it is not possible to implement the generalized hybrid search on top of those databases.

PropertyName Naming Considered Options

1. Explicit Dense naming

DenseVectorPropertyName SparseVectorPropertyName

DenseVectorPropertyName FullTextPropertyName

  • Pros: This is more explicit, considering that there are also sparse vectors involved.
  • Cons: It is inconsistent with the naming in the non-hybrid vector search.

2. Implicit Dense naming

VectorPropertyName SparseVectorPropertyName

VectorPropertyName FullTextPropertyName

  • Pros: This is consistent with the naming in the non-hybrid vector search.
  • Cons: It is internally inconsistent, i.e. we have sparse vector, but for dense it's just vector.

Keyword splitting Considered Options

1. Accept Split keywords in interface

Accept an ICollection of string where each value is a separate keyword. A version that takes a single keyword and calls the ICollection<string> version can also be provided as an extension method.

    Task<VectorSearchResults<TRecord>> KeywordVectorizedHybridSearch(
        TVector vector,
        ICollection<string> keywords,
        KeywordVectorizedHybridSearchOptions options,
        CancellationToken cancellationToken);
  • Pros: Easier to use in the connector if the underlying DB requires split keywords
  • Pros: Only solution broadly supported, see comparison table above.

2. Accept single string in interface

Accept a single string containing all the keywords.

    Task<VectorSearchResults<TRecord>> KeywordVectorizedHybridSearch(
        TVector vector,
        string keywords,
        KeywordVectorizedHybridSearchOptions options,
        CancellationToken cancellationToken);
  • Pros: Easier for a user to use, since they don't need to do any keyword splitting.
  • Cons: We don't have the capabilities to properly sanitise the string, e.g. splitting words appropriately for the language, and potentially removing filler words.

3. Accept either in interface

Accept either option and either combine or split the keywords in the connector as needed by the underlying db.

    Task<VectorSearchResults<TRecord>> KeywordVectorizedHybridSearch(
        TVector vector,
        ICollection<string> keywords,
        KeywordVectorizedHybridSearchOptions options,
        CancellationToken cancellationToken);
    Task<VectorSearchResults<TRecord>> KeywordVectorizedHybridSearch(
        TVector vector,
        string keywords,
        KeywordVectorizedHybridSearchOptions options,
        CancellationToken cancellationToken);
  • Pros: Easier for a user to use, since they can pick whichever suits them better
  • Cons: We have to still convert to/from the internal presentation by either combining keywords or splitting them.
  • Cons: We don't have the capabilities to properly sanitise the single string, e.g. splitting words appropriately for the language, and potentially removing filler words.

4. Accept either in interface but throw for not supported

Accept either option but throw for the one not supported by the underlying DB.

  • Pros: Easier for us to implement.
  • Cons: Harder for users to use.

5. Separate interfaces for each

Create a separate interface for the Enumerable and single string options, and only implement the one that is supported by the underlying system for each db.

  • Pros: Easier for us to implement.
  • Cons: Harder for users to use.

Full text search index mandatory configuration Considered Options

Cosmos DB NoSQL requires a language to be specified when creating a full text search index. Other DBs have optional values that can be set.

1. Pass option in via collection options

This option does the minimum by just adding a language option to the collection's options class. This language would then be used for all full text search indexes created by the collection.

  • Pros: Simplest to implement
  • Cons: Doesn't allow multiple languages to be used for different fields in one record
  • Cons: Doesn't add support for all full text search options for all dbs

2. Add extensions for RecordDefinition and data model Attributes

Add a property bag to the VectorStoreRecordProperty allowing database specific metadata to be provided. Add an abstract base attribute that can be inherited from that allows extra metadata to be added to the data model, where each database has their own attributes to specify their settings, with a method to convert the contents to the property bag required by VectorStoreRecordProperty.

  • Pros: Allows multiple languages to be used for different fields in one record
  • Pros: Allows other DBs to add their own settings via their own attributes
  • Cons: More work to implement

Decision Outcome

Scoping

Chosen option "1. Keyword Hybrid Search Only", since enterprise support for generating sparse vectors is poor and without an end to end story, the value is low.

PropertyName Naming

Chosen option "2. Implicit Dense naming", since it is consistent with the existing vector search options naming.

Keyword splitting

Chosen option "1. Accept Split keywords in interface", since it is the only one with broad support amongst databases.

Naming Options decision

We agreed that our north star design would be to support the Embedding type and some form of vectorizable data (probably DataContent from MEAI) as input for both Regular search and Hybrid search.

public Task VectorSearch<TRecord>(Embedding embedding, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);
public Task VectorSearch<TRecord>(VectorizableData vectorizableData, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);
public Task VectorSearch<TRecord>(VectorizableData[] vectorizableData, VectorSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);

public Task HybridSearch<TRecord, TVectorType>(TVector vector, VectorizableData vectorizableData, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken = null);

We will have a single HybridSearch method name, with different overloads in future for different inputs, however there will be a single options class. The property selector for choosing the target keyword field or in future the sparse vector field will be called AdditionalPropertyName.

While we work on getting the right data types and Embedding types to be available, we will ship the following interface.

public Task HybridSearch<TVector>(TVector vector, ICollection<string> keywords, HybridSearchOptions<TRecord> options = null, CancellationToken cancellationToken);