Haystack pipelines with zero embedder components

A normal Haystack pipeline needs two embedders — one for documents, one for queries. With Opensolr it needs none: everything embeds server-side on GPU, and retrieval fuses BM25 + kNN natively.

$ pip install opensolr-haystack

The whole pipeline

Count the embedder components. There are none.

from haystack import Document, Pipeline
from haystack_integrations.document_stores.opensolr import OpensolrDocumentStore
from haystack_integrations.components.retrievers.opensolr import OpensolrHybridRetriever

# credentials default to OPENSOLR_EMAIL / OPENSOLR_API_KEY env vars
store = OpensolrDocumentStore(index="mysite__dense", create_if_missing=True)

store.write_documents([
    Document(content="Hybrid search fuses BM25 with vector similarity"),
    Document(content="Cats sleep sixteen hours a day"),
])

pipe = Pipeline()
pipe.add_component("retriever", OpensolrHybridRetriever(document_store=store))
result = pipe.run({"retriever": {"query": "how do keyword and semantic search combine?"}})

Try it without an account

A public demo account, so the quickstart above runs before you decide anything.

export OPENSOLR_EMAIL=mcp@opensolr.com
export OPENSOLR_API_KEY=420b8b23e7b12dc8ab838932145a5065

The index mcp_demo_d1__dense is already loaded with 300 news articles, so search, filtering and grounded answers work the moment you connect. You also get the write path: create your own index on the account, ingest into it and query it. Deletion is not available on this shared key — no index here can be deleted or reconfigured by hand. Whatever you create is removed automatically after 3 days.

  • Anything you create there is deleted after 3 days. Automatically, without warning or export — indexes you created included.
  • The account is shared with everyone reading this page. Your index is visible to them and they can add documents to it, as you can to theirs. Nobody can delete or reconfigure an index here — that is switched off for this key — but never put anything real, private or client-owned in it.
  • The limits are per index, and deliberately small. 200 MB of bandwidth and 50 MB of disk per index. Bandwidth is the one you will hit first: it covers a demo, a tutorial and a proof of concept, and it will not carry an application.

When you want an index that is private, yours and still there next week, get your own key — the free plan is free forever, no card — and change the two variables above. Nothing else in your code changes.

Built for real pipelines

Standard Haystack contracts, managed Solr underneath.

Full DocumentStore protocol

write_documents with DuplicatePolicy (skip / overwrite / fail), filter_documents with the standard filter dict, counts, deletes — plus Secret-based credentials and full serde for saved pipelines.

Hybrid retriever

OpensolrHybridRetriever fuses BM25 and kNN scores per document via the native {!hybrid} Solr parser, with a tunable alpha balance and per-run overrides.

Plain Apache Solr underneath

Every index is a real Apache Solr core with the native /select API — facets, highlighting, spellcheck are all there when your pipeline needs more than retrieval.

Also available for: LangChain · LlamaIndex · MCP / AI Agents · Laravel

Search with a photo

Hand the store a picture instead of a query. Opensolr reads it three ways — what it depicts, the text printed on it, and any barcode or QR code — then searches with those words. Nothing new is stored in Solr; the image simply becomes a query.

# What the picture reads as — labels, OCR text, barcodes. No search yet:
read = store.image_to_words("shelf-photo.jpg")   # path, bytes, or base64
# {'text': 'red running shoe', 'mode': 'clip',
#  'labels': ['running shoe', 'sneaker'], 'codes': ['0123456789012']}

# Search the index with the picture — pick which reading drives it:
docs = store.search_by_image("shelf-photo.jpg", top_k=4)            # engine's choice
docs = store.search_by_image("shelf-photo.jpg", using="meaning")   # what it depicts
docs = store.search_by_image("shelf-photo.jpg", using="text")      # OCR text only
docs = store.search_by_image("shelf-photo.jpg", using="code")      # exact barcode / QR
docs = store.search_by_image("shelf-photo.jpg", using="all")       # labels + OCR + codes

The same hybrid, lexical, alpha, fresh_bias and filters as search() apply — an image query runs the exact same pipeline a text query does.

Local files, PDFs page by page

Each file is read on Opensolr’s servers — its text, the text in its pictures (OCR, more than 100 languages), a short description of the pictures without text — and a PDF becomes one document per page, so a search lands on the page that answers it instead of a 200-page file that mentions it somewhere. Word, RTF, OpenDocument, text, HTML and scanned pictures are one document each. In results each PDF then comes back once, as its best page, with a link that opens it right there.

from haystack_integrations.components.converters.opensolr import OpensolrConverter

# Files read on Opensolr: one Document per PDF page, scans through OCR
converter = OpensolrConverter(document_store=store)
docs = converter.run(sources=["./contracts", "scan.png"])["documents"]
# meta: file_path, page_number, is_ocr, language

# Or straight into the store — no converter, no splitter, PDFs page by page:
store.write_files("./contracts", pattern="*.pdf", metadata={"team": "legal"})

# A PDF comes back as its best page (page_number, page_url, more_pages); a period narrows it
OpensolrHybridRetriever(document_store=store, date_from="2025-01-01")

The bytes travel once: a file the account already sent is not uploaded again, and sending the same files again replaces their documents. Pictures read by OCR count 0.5 of an AI request each; a file read before is free. Big files are read in the background: whatever is still being read comes back as pending — run again to finish them.

Index requirements, checked for you

Data Ingestion, vector and hybrid search and PDF pages need an index in a vector region that runs the Opensolr vector schema. Every index the package creates has it. On any other index the package checks once, with one request — never on every search — and tells you exactly what the index cannot do and how to fix it.

from haystack_integrations.document_stores.opensolr import OpensolrSchemaError

store.check_schema()      # ready, capabilities, problems, fix
store.apply_schema()      # installs the Opensolr vector schema (force=True if it holds documents)

try:
    store.write_files("./contracts")
except OpensolrSchemaError as e:
    print(e)      # what the index cannot do, why, and how to fix it
  • Only what cannot work is stopped. Data Ingestion on an index outside the vector regions, and vector or hybrid search on an index without the vector field, are refused with the exact reason before anything is uploaded or embedded. Keyword search runs on any index that has title, description and text fields.
  • store.check_schema() answers whether the index is ready, what it can do (ingestion, vector search, PDF pages, keyword search), what differs from the Opensolr vector schema, and the fix.
  • store.apply_schema() installs the Opensolr vector schema, the configuration every Opensolr vector index runs, so everything above works on the index. It replaces the index configuration: on an index that already holds documents it needs force=True, and those documents must be sent again.
  • An index outside the vector regions cannot take it. Create a new index (create_index) in one of the regions of store.client.vector_regions() and send your documents there.

Your Search Tuning follows you

Relevance is configured once, in your Control Panel — not re-implemented in every codebase.

  • Saved per index: everything you set in Index Settings → Search Tuning (semantic↔lexical balance, field weights, minimum match, search mode, vector candidate pool, content quality boost) is stored with your index.
  • Applies automatically here: every search and every RAG answer from this integration runs through the same tuned pipeline as your hosted search page — change a slider in the Control Panel and the very next query uses it. No redeploy, no code change.
  • Overridable per call: tuning={"search_mode": "keywords_required", "fw_title": 0.2, "mm": "strict"} beats the saved settings for that one request. Defaults match the platform exactly when you send nothing.
  • Fresh Results Bias: pass fresh_bias to rank newer documents higher — scores are multiplied by a recency curve on creation_date. It re-orders and never filters: the hit count is unchanged and undated documents keep their place. This is the same control visitors get as the Fresh toggle on the hosted search page. Not to be confused with freshness_boost, which is a hard date window in days and does remove results.

Tested on our own production index

Every release runs a live end-to-end suite against real Opensolr infrastructure — no mocks.

  • Full write path through the async Data Ingestion queue: queued → server-side embeddings & enrichment → searchable, with metadata and id round-trips verified. Metadata lands in typed Solr fields; the suffixes and what they enable are in the Site Search Schema Reference.
  • Real-corpus retrieval against a replica of opensolr.com's own search index: pure-semantic hits with zero keyword overlap („how do I get my data back after a disaster” → backup docs), cross-lingual queries (non-English → English content), all hybrid modes and the full alpha range.
  • PDF ingestion via rtf:true: server-side text extraction and content-type detection, then semantic retrieval from the extracted content.
  • Grounded RAG answers: one call runs hybrid retrieval and feeds the top hits to the LLM (configurable docs/words, custom instruction) — verified with a question answerable only from the ingested PDF.
  • Haystack specifics: DuplicatePolicy SKIP / FAIL, pipeline runs with the hybrid retriever, saved-pipeline serialization round-trip.

Open source

This integration is free, MIT licensed and maintained in the open. Browse the code, open an issue, or sponsor the work that keeps it current.

Simplify your pipeline tonight

Free forever plan, no credit card — the included AI quota covers the whole quickstart.