-
Notifications
You must be signed in to change notification settings - Fork 40
Search & Vector Stores #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||||||
| be kept distinct shapes. | ||||||
|
|
||||||
| ## Background | ||||||
|
|
||||||
| The current implementations of state stores do not accomodate the necessity to access data not only by | ||||||
|
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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add a
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) {} | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also consider additional rpcs for |
||||||
| 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; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| message IndexDocumentsResponseAlpha1 { | ||||||
| repeated string failed_ids = 1; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): |
||||||
| IndexAck ack = 2; | ||||||
| } | ||||||
|
|
||||||
| message DeleteDocumentsRequestAlpha1 { | ||||||
| string store_name = 1; | ||||||
| string index = 2; | ||||||
| repeated string ids = 3; | ||||||
| map<string, string> metadata = 10; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| 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; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| 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; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| message GetVectorsRequestAlpha1 { | ||||||
| string store_name = 1; | ||||||
| string collection = 2; | ||||||
| repeated string ids = 3; | ||||||
| bool include_values = 4; | ||||||
| map<string, string> metadata = 10; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| 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; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you notate what a higher/lower score indicates for clarity?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see |
||||||
| string collection = 2; | ||||||
| repeated QueryVectorsRequestAlpha1 queries = 3; | ||||||
| map<string, string> metadata = 10; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| 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}, | ||||||
| }, | ||||||
| }) | ||||||
| ``` | ||||||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.