Skip to content

Elasticsearch and OpenSearch

New in 0.3.0. ElasticsearchStore and OpenSearchStore keep vectors, text, and metadata in one index and serve both halves of hybrid search: k-NN for the dense side and BM25 with the built-in persian analyzer for the sparse side. They talk to the REST API directly, so no client package is required.

from persian_rag import HashingEmbedder, HybridRetriever, build_retriever
from persian_rag.vectorstores import ElasticsearchStore, OpenSearchStore, store_from_url

store = ElasticsearchStore("http://localhost:9200", "persian_docs")
# store = OpenSearchStore("https://search.example.com", "persian_docs", username="admin", password="...")
# store = store_from_url("opensearch+https://search.example.com", "persian_docs")

embedder = HashingEmbedder()
retriever = build_retriever(texts, embedder, document_ids=ids, store=store)

# Later, in another process: the index already holds everything.
retriever = HybridRetriever(embedder, store=store, sparse_backend=store)
results = retriever.retrieve("دیوان حافظ", k=5)  # hybrid, dense, or sparse

With sparse_backend=store the in-process BM25 index is not used, so a new process does not need to load any chunks before searching.

Options

Argument Default Purpose
url ELASTICSEARCH_URL or OPENSEARCH_URL, then http://localhost:9200 Cluster address
index persian_rag Lowercase index name
dimension read from the mapping Required only to create a new index before the first add
api_key ELASTICSEARCH_API_KEY / OPENSEARCH_API_KEY Sent as Authorization: ApiKey ...
username, password password from ..._PASSWORD Basic authentication
analyzer persian Analyzer for the text field
refresh wait_for Refresh policy for writes: true, false, or wait_for
num_candidates 100 Candidates per shard for Elasticsearch k-NN
verify_tls, ca_file, timeout Connection settings
transport urllib Inject a custom HTTP function, for tests or a different client

The index is created on first write with a dense_vector (Elasticsearch, cosine similarity, HNSW) or knn_vector (OpenSearch, Lucene HNSW, cosine) field. Documents are written with the _bulk API, add refuses ids that already exist, upsert replaces them, and delete(ids) removes them. drop_index() deletes the whole index.

Incremental updates

The stores support deletes, so HybridRetriever.delete_document, delete_chunks, and the incremental indexer work with them.

Local servers

docker run -p 9200:9200 -e discovery.type=single-node -e xpack.security.enabled=false \
  docker.elastic.co/elasticsearch/elasticsearch:8.15.3
docker run -p 9201:9200 -e discovery.type=single-node -e DISABLE_SECURITY_PLUGIN=true \
  -e DISABLE_INSTALL_DEMO_CONFIG=true opensearchproject/opensearch:2.17.1

The integration tests run against both in CI:

PERSIAN_RAG_ES_URL=http://localhost:9200 PERSIAN_RAG_OPENSEARCH_URL=http://localhost:9201 pytest -m search

The command line keeps using the JSON index; the search engine stores are a library feature.