Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 260 additions & 0 deletions 20260428-BCRS-Search-and-vector-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
# Search and Vector Building Blocks

Author: Mike Nguyen - @mikeee (hey@mike.ee)

## Overview

This is a proposal surrounding the implementation of two new specialised state stores without bringing into
scope the deep integrations planned on implementation of these building blocks. Whilst the two building
blocks share almost the same envelope, the method of requesting the records can vary significantly and should

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if it'd make sense to ship a single building block with both of the APIs combined into one. With a single component, we get a unified API for index/upsert/delete documents, and different endpoints for fetching, one for lexical search and one for vector search.
This would be beneficial if a user needs to use both APIs at the same time.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless there's some scenario you can think of where a query to a provider might do some sort of join operation between lexical and vector searches (e.g. we can offload that heavy lifting to the provider - we absolutely should not do any manipulation of this data in the runtime), I would vote they remain separate.

This obviates the need for another (and differing) capabilities functionality at the building block level, it keeps the APIs for each building block simple and focused and it potentially improves the rate at which provider implementations can be developed and added (e.g. they need only implement the API associated with one block at a time, not have to implement a broader set just for v1.0).

Especially if the results from either are instead used alongside local logic in the app, that's the domain of the SDK to offer a usage smooth experience where it's the domain of the runtime to establish connectivity, issue commands and perform data transfer. I think runtime's role is better suited to small, lean building block APIs going forward.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was considering this too - I can make it clearer in the proposal but I share the same sentiment as Whit.

The two building blocks can be objectively very different. I feel like it may be running the risk of becoming a feature flag mess/very confusing for implementation where providers have differing feature sets that fall distinctly within the lexical or vector search buckets.

We should focus on having distinct building blocks, if you need to use both APIs at the same time then you'd just need to register another component type with the same provider.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I'd love to see explicitly spelled out is the story for someone who wants to use both search & vector against the same provider. From ^ it sounds like the answer is to register 2 components pointing to the same server.

When that happens, is the underlying data shared - so an item indexed for search and its vector can be tied together by a common ID or are they independent? I think that matters with AI use cases where you might want to store something once and both search it by keyword && find it by similarity.

And is there a scenario where a single query should do both at once - keyword & similarity combined by the provider itself?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than trying to design a feature like @cicoyle is describing as a one-off feature for this, I'd much rather take that idea back to the drawing board and figure out if the idea is applicable across more components and design that as a separate runtime feature than necessarily jam in something that potentially complex as a one-off here. Seems like that would dramatically increase the complexity of what's otherwise a pretty straightforward proposal here.

be kept distinct shapes.

## Background

The current implementations of state stores do not accomodate the necessity to access data not only by
Comment thread
mikeee marked this conversation as resolved.
key-value but an extended querying layer. New 'specialised' building blocks facilitate rapid development.

There is a specific need from AI workloads to not only query by key but also by 'relevant' records returned

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a Use Cases section? I believe it would help ground the separate vs combined API decision - might be good to note under an Alternatives Considered section or similar. There are RAG/agent retrieval patterns where lexical & vector results get fused together. I believe its a first class AI usecase that should be considered, rather than it being an edge case. Its worth deciding if we should have the APIs separate and/or have a hybrid, even if the hybrid API is out of scope for now - it should be thought thru ahead of implementation to prevent breaking changes later on.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the key ideas of my proposal to split out into discrete building blocks is to explicitly pivot away from what the key/value state store turned into: a far more use-case governed platform with runtime shims holding together what providers failed to provide.

I would dramatically prefer that if there's an AI use-case, it be proposed as a separate block - more of a composite component that's able to express the individual functionality but which only applies to those providers that can do both operations. This is a central tenant of my handily rejected planes proposal, but is exactly along the lines of what I was thinking.

Some people will just need a vector store. Some people will just need a search store. For those that need both, build something that specifically speaks to that use-case but atop far more generic basic components.

through lexical/structured matching or geometric proximity for example.

## Providers in scope

| Provider | Lexical Search | Vector Search |
|----------|---|---|
| Meilisearch | ✓ | ✓ |
| Elasticsearch | ✓ | ✓ |
| Chroma | ✓ | ✓ |

Other providers with existing component implementations may be leveraged.

## HTTP API / Protos

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add HTTP to the proposal? I only see the grpc protos


A HTTP api will be exposed mirroring the below.

### Search

```protobuf
rpc IndexDocumentsAlpha1(IndexDocumentsRequestAlpha1) returns (IndexDocumentsResponseAlpha1) {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also consider additional rpcs for CreateIndex, CreateCollection, DescribeIndex, ListIndexes, DeleteIndexes, etc?

rpc DeleteDocumentsAlpha1(DeleteDocumentsRequestAlpha1) returns (google.protobuf.Empty) {}
rpc SearchAlpha1(SearchRequestAlpha1) returns (SearchResponseAlpha1) {}

message SearchDocument {
string id = 1;
bytes content = 2;
map<string, string> metadata = 3;
}

message SearchHit {
SearchDocument document = 1;
double score = 2;
map<string, string> highlights = 3;
}

enum SortOrder {
SORT_ORDER_UNSPECIFIED = 0;
SORT_ORDER_ASC = 1;
SORT_ORDER_DESC = 2;
}

message SortClause {
string field = 1;
SortOrder order = 2;
}

enum IndexAck {
INDEX_ACK_UNSPECIFIED = 0;
INDEX_ACK_QUEUED = 1;
INDEX_ACK_DURABLE = 2;
}

message IndexDocumentsRequestAlpha1 {
string store_name = 1;
string index = 2;
repeated SearchDocument documents = 3;
map<string, string> metadata = 10;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
map<string, string> metadata = 10;
map<string, string> metadata = 4;

}

message IndexDocumentsResponseAlpha1 {
repeated string failed_ids = 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also return a reason or code so we can determine if we should retry or not? It might make sense to add something like the following (not exact syntax, but for ex):

repeated FailedItem {
string id; 
uint32 code; 
string message;
}

IndexAck ack = 2;
}

message DeleteDocumentsRequestAlpha1 {
string store_name = 1;
string index = 2;
repeated string ids = 3;
map<string, string> metadata = 10;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
map<string, string> metadata = 10;
map<string, string> metadata = 4;

}

message SearchRequestAlpha1 {
string store_name = 1;
string index = 2;

oneof query {
string text = 3;
google.protobuf.Struct native = 4;
}

google.protobuf.Struct filter = 5;
uint32 top_k = 6;
uint32 offset = 7;
repeated string return_fields = 8;
bool include_content = 9;
map<string, string> metadata = 10;
repeated string search_fields = 11;
repeated SortClause sort = 12;
repeated string highlight_fields = 13;

reserved 14, 15, 16;
}

message SearchResponseAlpha1 {
repeated SearchHit hits = 1;
uint64 total_hits = 2;
string continuation_token = 3;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see offset in the req, are we intending to do offset based or cursor based pagination?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be updating this to cursor-based pagination with an opaque token to abstract the provider specific behaviour.

}
```

### Vector

```protobuf
rpc UpsertVectorsAlpha1(UpsertVectorsRequestAlpha1) returns (UpsertVectorsResponseAlpha1) {}
rpc DeleteVectorsAlpha1(DeleteVectorsRequestAlpha1) returns (google.protobuf.Empty) {}
rpc GetVectorsAlpha1(GetVectorsRequestAlpha1) returns (GetVectorsResponseAlpha1) {}
rpc QueryVectorsAlpha1(QueryVectorsRequestAlpha1) returns (QueryVectorsResponseAlpha1) {}
rpc BatchQueryVectorsAlpha1(BatchQueryVectorsRequestAlpha1) returns (BatchQueryVectorsResponseAlpha1) {}

enum DistanceMetric {
DISTANCE_METRIC_UNSPECIFIED = 0;
DISTANCE_METRIC_COSINE = 1;
DISTANCE_METRIC_DOT_PRODUCT = 2;
DISTANCE_METRIC_EUCLIDEAN = 3;
}

message SparseVector {
repeated uint32 indices = 1;
repeated float values = 2;
}

message NamedVector {
repeated float values = 1;
SparseVector sparse_values = 2;
}

message VectorRecord {
string id = 1;
repeated float values = 2;
bytes payload = 3;
map<string, string> metadata = 4;
SparseVector sparse_values = 5;
map<string, NamedVector> named_vectors = 6;
}

message VectorMatch {
VectorRecord record = 1;
double score = 2;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we notate what a higher/lower score indicates for clarity?

}

message UpsertVectorsRequestAlpha1 {
string store_name = 1;
string collection = 2;
repeated VectorRecord records = 3;
map<string, string> metadata = 10;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
map<string, string> metadata = 10;
map<string, string> metadata = 4;

}

message UpsertVectorsResponseAlpha1 {
repeated string failed_ids = 1;
}

message DeleteVectorsRequestAlpha1 {
string store_name = 1;
string collection = 2;
repeated string ids = 3;
map<string, string> metadata = 10;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
map<string, string> metadata = 10;
map<string, string> metadata = 4;

}

message GetVectorsRequestAlpha1 {
string store_name = 1;
string collection = 2;
repeated string ids = 3;
bool include_values = 4;
map<string, string> metadata = 10;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
map<string, string> metadata = 10;
map<string, string> metadata = 4;

}

message GetVectorsResponseAlpha1 {
repeated VectorRecord records = 1;
}

message QueryVectorsRequestAlpha1 {
string store_name = 1;
string collection = 2;

oneof query {
VectorRecord vector = 3;
string by_id = 4;
}

uint32 top_k = 5;
google.protobuf.Struct filter = 6;
bool include_values = 7;
bool include_payload = 8;
DistanceMetric metric = 9;
map<string, string> metadata = 10;
SparseVector sparse_query = 11;
optional float alpha = 12;
optional string vector_name = 13;
optional float score_threshold = 14;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you notate what a higher/lower score indicates for clarity?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s the raw value for the specified distance metric, so whether a higher or lower score is better depends on that metric—for example, lower is better for Euclidean distance.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'm not planning on doing any normalisation here to reduce confusion)

}

message QueryVectorsResponseAlpha1 {
repeated VectorMatch matches = 1;
}

message BatchQueryVectorsRequestAlpha1 {
string store_name = 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see store_name && collection here and inside QueryVectorsRequestAlpha1 - shall we trim the inner msg?

string collection = 2;
repeated QueryVectorsRequestAlpha1 queries = 3;
map<string, string> metadata = 10;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
map<string, string> metadata = 10;
map<string, string> metadata = 4;

}

message BatchQueryVectorsResponseAlpha1 {
repeated QueryVectorsResponseAlpha1 results = 1;
}
```

## Consumption Examples (Go)

```go
// Search
hits, err := client.SearchAlpha1(ctx, &client.SearchRequestAlpha1{
StoreName: "meili-products",
Index: "products",
Query: &client.SearchRequestAlpha1Text{Text: "wireless headphones"},
SearchFields: []string{"title", "description"},
TopK: 10,
})

// Vector query
matches, err := client.QueryVectorsAlpha1(ctx, &client.QueryVectorsRequestAlpha1{
StoreName: "meili-docs",
Collection: "manuals",
Query: &client.QueryVectorsRequestAlpha1_Vector{
Vector: &client.VectorRecord{Values: dense},
},
SparseQuery: &client.SparseVector{Indices: sparseIdx, Values: sparseVal},
Alpha: proto.Float32(0.7),
TopK: 5,
IncludePayload: true,
ScoreThreshold: proto.Float32(0.6),
})

// Batch query
batch, err := client.BatchQueryVectorsAlpha1(ctx, &client.BatchQueryVectorsRequestAlpha1{
StoreName: "meili-docs",
Collection: "manuals",
Queries: []*client.QueryVectorsRequestAlpha1{
{Query: &client.QueryVectorsRequestAlpha1_Vector{Vector: &client.VectorRecord{Values: q1}}, TopK: 5},
{Query: &client.QueryVectorsRequestAlpha1_Vector{Vector: &client.VectorRecord{Values: q2}}, TopK: 5},
},
})
```