feat(providers): add OpenSearch vector store provider#4952
feat(providers): add OpenSearch vector store provider#4952emirsimsek00 wants to merge 1 commit intollamastack:mainfrom
Conversation
|
Hi @emirsimsek00! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
There was a problem hiding this comment.
Pull request overview
This PR adds OpenSearch vector store provider support to Llama Stack, enabling users to leverage OpenSearch (and Amazon OpenSearch Service) as a vector database backend. The implementation follows the existing provider pattern with both remote and inline configurations, supporting vector search (k-NN/HNSW), keyword search (BM25), and hybrid search capabilities.
Changes:
- Implements OpenSearch vector store adapter with
OpenSearchIndexfor index operations andOpenSearchVectorIOAdapterfor the provider interface - Adds configuration schema for OpenSearch connection settings (host, port, SSL, authentication)
- Registers both
remote::opensearchandinline::opensearchproviders in the vector_io registry with appropriate documentation
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 23 comments.
Show a summary per file
| File | Description |
|---|---|
| src/llama_stack/providers/remote/vector_io/opensearch/opensearch.py | Core implementation of OpenSearch adapter with index operations (add, query, delete chunks) and vector store management |
| src/llama_stack/providers/remote/vector_io/opensearch/config.py | Configuration schema for OpenSearch connection parameters including host, port, SSL settings, and authentication |
| src/llama_stack/providers/inline/vector_io/opensearch/init.py | Inline provider entry point that delegates to remote implementation |
| src/llama_stack/providers/inline/vector_io/opensearch/config.py | Inline provider config that aliases remote config |
| src/llama_stack/providers/registry/vector_io.py | Registers OpenSearch as both remote and inline provider with documentation and dependency specifications |
| tests/unit/providers/vector_io/test_opensearch.py | Unit tests covering initialization, registration, insertion, and querying with mocked OpenSearch client |
| pyproject.toml | Adds opensearch-py to test dependencies |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Copyright (c) Meta Platforms, Inc. and affiliates. | ||
| # All rights reserved. | ||
| # | ||
| # This source code is licensed under the terms described in the LICENSE file in | ||
| # the root directory of this source tree. | ||
|
|
There was a problem hiding this comment.
The remote OpenSearch provider is missing an __init__.py file. All other remote vector_io providers (chroma, elasticsearch, milvus, etc.) have an __init__.py file that exports a get_adapter_impl function. This file is required for the module to be properly imported and initialized by the Llama Stack provider system.
You need to create src/llama_stack/providers/remote/vector_io/opensearch/__init__.py with content similar to the elasticsearch provider, containing a get_adapter_impl async function that instantiates the adapter and calls initialize().
| async def initialize(self) -> None: | ||
| if OpenSearch is None: | ||
| raise ImportError( | ||
| "opensearch-py is not installed. Please install it with `pip install opensearch-py`." | ||
| ) | ||
|
|
||
| auth = None | ||
| if self.config.username and self.config.password: | ||
| auth = (self.config.username, self.config.password.get_secret_value()) | ||
|
|
||
| self.client = OpenSearch( | ||
| hosts=[{"host": self.config.host, "port": self.config.port}], | ||
| http_compress=True, | ||
| http_auth=auth, | ||
| use_ssl=self.config.use_ssl, | ||
| verify_certs=self.config.verify_certs, | ||
| ) | ||
|
|
||
| # Try to ping or get info to verify connection | ||
| try: | ||
| await asyncio.to_thread(self.client.info) | ||
| except Exception as e: | ||
| logger.warning(f"Could not connect to OpenSearch at startup: {e}") | ||
|
|
||
| await self.initialize_openai_vector_stores() |
There was a problem hiding this comment.
The OpenSearchVectorIOAdapter doesn't properly integrate with KVStore for persistent storage of vector store metadata. Looking at the elasticsearch provider (lines 383-395 in elasticsearch.py), it uses KVStore to persist and reload vector store configurations across restarts. The OpenSearch implementation only stores vector stores in memory (self.cache), which means they'll be lost on restart.
You should:
- Add persistence configuration to OpenSearchVectorIOConfig
- Initialize kvstore in the initialize() method
- Persist vector stores to kvstore in register_vector_store()
- Load existing vector stores from kvstore during initialize()
- Remove from kvstore in unregister_vector_store()
| ) | ||
|
|
||
|
|
||
| class OpenSearchVectorIOAdapter(OpenAIVectorStoreMixin, VectorIO): |
There was a problem hiding this comment.
The OpenSearchVectorIOAdapter should inherit from VectorStoresProtocolPrivate in addition to OpenAIVectorStoreMixin and VectorIO. Looking at the elasticsearch provider (line 367 in elasticsearch.py), it inherits from all three: ElasticsearchVectorIOAdapter(OpenAIVectorStoreMixin, VectorIO, VectorStoresProtocolPrivate). This protocol is needed to properly support the full vector stores API.
| async def query_hybrid( | ||
| self, | ||
| embedding: List[float], | ||
| query_string: str, | ||
| k: int, | ||
| score_threshold: float, | ||
| reranker_type: str, | ||
| reranker_params: dict[str, Any] | None = None, | ||
| ) -> QueryChunksResponse: | ||
| # Simple hybrid using compound bool query | ||
| query = { | ||
| "size": k, | ||
| "query": { | ||
| "bool": { | ||
| "should": [ | ||
| { | ||
| "knn": { | ||
| "embedding": { | ||
| "vector": embedding, | ||
| "k": k, | ||
| "boost": 0.5 | ||
| } | ||
| } | ||
| }, | ||
| { | ||
| "match": { | ||
| "content": { | ||
| "query": query_string, | ||
| "boost": 0.5 | ||
| } | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The query_hybrid method ignores the reranker_type and reranker_params arguments and always uses fixed 0.5 boost values. This means users cannot control the weighting between vector and keyword search.
The method should respect the reranker_params to allow customization. For example, if reranker_type is "weighted" and reranker_params contains an "alpha" value, the boost values should be adjusted accordingly (alpha for vector, 1-alpha for keyword). See the elasticsearch provider's query_hybrid implementation for reference on how to properly handle reranker parameters.
|
|
||
| # We ignore 404s (not found) during bulk delete to be safe | ||
| await asyncio.to_thread( | ||
| helpers.bulk, self.client, actions, raise_on_error=False, refresh=True |
There was a problem hiding this comment.
Inconsistent indentation: line 250 has an extra space at the beginning. The line should align with line 249 (no extra leading space). This violates Python's consistent indentation conventions.
| helpers.bulk, self.client, actions, raise_on_error=False, refresh=True | |
| helpers.bulk, self.client, actions, raise_on_error=False, refresh=True |
| try: | ||
| await asyncio.to_thread(self.client.indices.delete, index=self.index_name, ignore=[404]) | ||
| except Exception as e: | ||
| logger.error(f"Failed to delete index {self.index_name}: {e}") |
There was a problem hiding this comment.
The delete() method logs an error but doesn't re-raise the exception. This means that callers won't know if the deletion failed. Looking at the elasticsearch provider (lines 357-364 in elasticsearch.py), it logs the error and then re-raises it, allowing the caller to handle the failure appropriately.
The OpenSearch implementation should also re-raise the exception after logging to maintain consistent error handling behavior.
| logger.error(f"Failed to delete index {self.index_name}: {e}") | |
| logger.error(f"Failed to delete index {self.index_name}: {e}") | |
| raise |
| async def delete_chunks(self, chunks_for_deletion: List[ChunkForDeletion]): | ||
| actions = [] | ||
| for chunk in chunks_for_deletion: | ||
| actions.append({ |
There was a problem hiding this comment.
Inconsistent indentation: line 242 has an extra space at the beginning. The line should align with lines 241 and 246 (no extra leading space). This violates Python's consistent indentation conventions.
| def _reconstruct_chunk(self, source: dict) -> EmbeddedChunk: | ||
| chunk_content_json = source.get("chunk_content") | ||
| if chunk_content_json: | ||
| # It was stored as JSON string |
There was a problem hiding this comment.
Inconsistent indentation: line 256 has an extra space at the beginning. The line should align with lines 254-255 and 258-260 (no extra leading space). This violates Python's consistent indentation conventions.
|
|
||
| from typing import Optional | ||
|
|
||
| from llama_stack_api.shared.schemas import AdapterSpec, StackComponentConfig |
There was a problem hiding this comment.
The import AdapterSpec from llama_stack_api.shared.schemas is unused. Looking at other vector_io provider configs (chroma, elasticsearch), they don't import AdapterSpec. This import should be removed.
| from llama_stack_api.shared.schemas import AdapterSpec, StackComponentConfig | |
| from llama_stack_api.shared.schemas import StackComponentConfig |
| except ImportError: | ||
| OpenSearch = None | ||
| helpers = None | ||
|
|
There was a problem hiding this comment.
When opensearch-py is not installed, the helpers module is set to None (line 40), but it's used in multiple places (lines 109, 250) without checking if it's None first. This will cause an AttributeError at runtime if opensearch-py is not installed, even though there's a check for OpenSearch being None in the initialize() method (line 289).
The check at line 289 should also verify that helpers is not None, or the helpers import should raise an ImportError that gets caught, similar to how OpenSearch is handled.
| except ImportError: | |
| OpenSearch = None | |
| helpers = None | |
| except ImportError as _opensearch_import_error: | |
| OpenSearch = None | |
| class _MissingHelpers: | |
| def __getattr__(self, name: str) -> Any: | |
| # Raised when opensearch-py is not installed but helpers is used. | |
| raise ImportError( | |
| "opensearch-py is required to use the OpenSearch vector store " | |
| "(missing helpers module)." | |
| ) from _opensearch_import_error | |
| helpers = _MissingHelpers() |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
What does this PR do?
This PR implements the OpenSearch vector store provider for Llama Stack, enabling users to use OpenSearch (and Amazon OpenSearch Service) as a vector database backend.
It introduces:
src/llama_stack/providers/remote/vector_io/opensearch/containing the coreOpenSearchVectorIOAdapterandOpenSearchIndex.src/llama_stack/providers/inline/vector_io/opensearch/.remote::opensearchandinline::opensearchinsrc/llama_stack/providers/registry/vector_io.py.opensearch-pyas an optional dependency.Supports:
Test Plan
I verified the changes using:
tests/unit/providers/vector_io/test_opensearch.pywhich mocks the OpenSearch client to test initialization, registration, insertion, and query logic.verify_opensearch.pyto test against a local OpenSearch instance.Testing Script (
verify_opensearch.py):