opensolr-haystack 0.7.0__tar.gz → 0.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/PKG-INFO +82 -12
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/README.md +81 -11
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/haystack_integrations/document_stores/opensolr/__init__.py +2 -1
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/haystack_integrations/document_stores/opensolr/client.py +146 -34
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/haystack_integrations/document_stores/opensolr/store.py +28 -5
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/opensolr_haystack.egg-info/PKG-INFO +82 -12
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/pyproject.toml +1 -1
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/LICENSE +0 -0
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/haystack_integrations/components/converters/opensolr/__init__.py +0 -0
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/haystack_integrations/components/converters/opensolr/converter.py +0 -0
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/haystack_integrations/components/retrievers/opensolr/__init__.py +0 -0
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/haystack_integrations/components/retrievers/opensolr/retriever.py +0 -0
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/opensolr_haystack.egg-info/SOURCES.txt +0 -0
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/opensolr_haystack.egg-info/dependency_links.txt +0 -0
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/opensolr_haystack.egg-info/requires.txt +0 -0
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/opensolr_haystack.egg-info/top_level.txt +0 -0
- {opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opensolr-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Haystack integration for Opensolr — managed Apache Solr DocumentStore with server-side embeddings and hybrid BM25+kNN retrieval
|
|
5
5
|
Author-email: Opensolr <support@opensolr.com>
|
|
6
6
|
License: MIT
|
|
@@ -61,8 +61,8 @@ the query. The store embeds server-side at both index and query time.
|
|
|
61
61
|
|
|
62
62
|
## Try it without an account
|
|
63
63
|
|
|
64
|
-
There is a public demo account. Point the package at it and
|
|
65
|
-
|
|
64
|
+
There is a public demo account. Point the package at it and everything in this
|
|
65
|
+
README works immediately, with no signup:
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
export OPENSOLR_EMAIL=mcp@opensolr.com
|
|
@@ -70,17 +70,18 @@ export OPENSOLR_API_KEY=420b8b23e7b12dc8ab838932145a5065
|
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
`mcp_demo_d1__dense` is already loaded with 300 news articles, so search, filtering
|
|
73
|
-
and grounded answers work the moment you connect. You also get the
|
|
74
|
-
create your own index on the account, ingest into it
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
and grounded answers work the moment you connect. You also get the write path:
|
|
74
|
+
create your own index on the account, ingest into it and query it. Deletion is not
|
|
75
|
+
available on this shared key: no index here can be deleted or reconfigured by hand.
|
|
76
|
+
Whatever you create is removed automatically after 3 days.
|
|
77
77
|
|
|
78
78
|
Know what you are working with:
|
|
79
79
|
|
|
80
80
|
- **Anything you create there is deleted after 3 days.** Automatically, without warning
|
|
81
81
|
or export. That includes indexes you created and every document in them.
|
|
82
|
-
- **The account is shared with everyone reading this.** Your index is visible to them
|
|
83
|
-
they can
|
|
82
|
+
- **The account is shared with everyone reading this.** Your index is visible to them
|
|
83
|
+
and they can add documents to it, as you can to theirs. Nobody can delete or
|
|
84
|
+
reconfigure an index here (that is switched off for this key), but never put anything
|
|
84
85
|
real, private or client-owned in it.
|
|
85
86
|
- **The limits are per index, and deliberately small.** 200 MB of bandwidth and 50 MB
|
|
86
87
|
of disk per index. Bandwidth is the one you will hit first: it covers a demo, a
|
|
@@ -172,8 +173,9 @@ text from PDF/DOCX/XLSX for you.
|
|
|
172
173
|
## Lexical-only mode
|
|
173
174
|
|
|
174
175
|
Don't need vectors? Pure keyword search skips the embedding call entirely —
|
|
175
|
-
zero AI quota
|
|
176
|
-
|
|
176
|
+
zero AI quota. It runs on any Opensolr index that has `title`, `description` and
|
|
177
|
+
`text` fields, including indexes outside the vector regions and older Solr
|
|
178
|
+
versions (see [Index requirements](#index-requirements)).
|
|
177
179
|
|
|
178
180
|
## Search by image
|
|
179
181
|
|
|
@@ -241,7 +243,7 @@ Data Ingestion with `rtf`, PDFs found by the Web Crawler), a search returns each
|
|
|
241
243
|
**once**, as its best page. Its metadata carries `page_number` (the page number),
|
|
242
244
|
`page_url` (opens the PDF at that page, `...pdf#page=14`), `document_id`,
|
|
243
245
|
`more_pages` (its other matching pages, each with `page` and `page_url`) and
|
|
244
|
-
`matching_pages`;
|
|
246
|
+
`matching_pages`, and its `uri`; any other hit keeps exactly the metadata it was sent with. `group_pages=False` returns the pages
|
|
245
247
|
as separate results.
|
|
246
248
|
|
|
247
249
|
## Searching a period
|
|
@@ -257,6 +259,74 @@ OpensolrHybridRetriever(document_store=store, date_from="2026-01-01")
|
|
|
257
259
|
|
|
258
260
|
They combine with every other option — filters, `fresh_bias`, hybrid or lexical.
|
|
259
261
|
|
|
262
|
+
## Index requirements
|
|
263
|
+
|
|
264
|
+
Everything this package does runs on an Opensolr index that sits in a **vector
|
|
265
|
+
region** and carries the **Opensolr vector schema** — the configuration every
|
|
266
|
+
Opensolr vector index runs. An index created through the package (`create_index`,
|
|
267
|
+
`create_if_missing`) has both. An index made elsewhere may not: a server outside
|
|
268
|
+
the vector regions, an older Solr version, or a schema of its own.
|
|
269
|
+
|
|
270
|
+
**The check is automatic.** The first time the package touches an index, it asks
|
|
271
|
+
the platform once whether that index can do what is asked: one request per index
|
|
272
|
+
per client, kept for the client's life, never one per search. If the check itself
|
|
273
|
+
cannot run (a network error, the platform busy), nothing is blocked: the
|
|
274
|
+
operation runs exactly as it always did, and the check is tried again after 5
|
|
275
|
+
minutes. When an index differs from the Opensolr vector schema, a warning is logged
|
|
276
|
+
once, on the `opensolr` logger, with the reasons and the fix.
|
|
277
|
+
|
|
278
|
+
What the check gates — only what cannot work otherwise:
|
|
279
|
+
|
|
280
|
+
| Capability | Needed for | When it is missing |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| `ingestion` | Data Ingestion: adding documents and indexing files | The index is outside the vector regions: refused before a document or a byte is sent |
|
|
283
|
+
| `vector_search` | Vector and hybrid search | The index has no vector field of the right size: refused before any embedding is paid for |
|
|
284
|
+
| `pdf_pages` | Grouping the pages of a PDF in search results | Results come back ungrouped, as before |
|
|
285
|
+
| `keyword_search` | Keyword (lexical) search | Never blocked |
|
|
286
|
+
|
|
287
|
+
**The errors say exactly what is going on.** A blocked operation raises
|
|
288
|
+
`OpensolrSchemaError` (a subclass of `OpensolrError`) with `.index`, `.capability`
|
|
289
|
+
and `.report` (the full check), and a message with every reason and the fix:
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
Index 'shop_it' cannot take documents or files through Data Ingestion. Index shop_it
|
|
293
|
+
is on IT-SOLR-9-6 (Milano, Italy), which is not a vector region: Data Ingestion,
|
|
294
|
+
server-side embeddings and hybrid search run only on the servers of a vector region
|
|
295
|
+
(DE-SOLR-9 (Germany, Solr 9.00), FINLAND9 (Finland, Solr 9.60), ...). This index
|
|
296
|
+
cannot be made ready: create a new index in one of the vector regions (...), for
|
|
297
|
+
example with create_index, and send your documents there.
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
When Solr itself refuses a request, `OpensolrSolrError` carries Solr's own reason
|
|
301
|
+
and the index name; it is still an `httpx.HTTPStatusError`, so code that caught
|
|
302
|
+
that before keeps working.
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from haystack_integrations.document_stores.opensolr import OpensolrDocumentStore, OpensolrSchemaError, OpensolrSolrError
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
store.write_files("./contracts")
|
|
309
|
+
except OpensolrSchemaError as e:
|
|
310
|
+
print(e.capability, e.report["fix"]["action"]) # ingestion new_index
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**See the details, and fix what can be fixed:**
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
store.check_schema() # ready, capabilities, server, Solr version, missing / different fields, problems, fix
|
|
317
|
+
store.apply_schema(force=False) # installs the Opensolr vector schema; force=True when the index holds documents
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`apply_schema` exists so an index of a vector region whose configuration was
|
|
321
|
+
changed, or never set, can take everything this package does without being
|
|
322
|
+
recreated: it installs the same configuration every Opensolr vector index runs,
|
|
323
|
+
and **replaces the index configuration**. An index that already holds documents
|
|
324
|
+
needs `force=True`; those documents keep the fields they were written with and
|
|
325
|
+
must be sent again. An index outside the vector regions cannot take it — create
|
|
326
|
+
a new index in a region from `vector_regions` and send your documents there. The
|
|
327
|
+
package never installs the schema on its own.
|
|
328
|
+
|
|
329
|
+
|
|
260
330
|
## Your index schema
|
|
261
331
|
|
|
262
332
|
Documents follow the Opensolr document model (`title`, `description`, `text`,
|
|
@@ -43,8 +43,8 @@ the query. The store embeds server-side at both index and query time.
|
|
|
43
43
|
|
|
44
44
|
## Try it without an account
|
|
45
45
|
|
|
46
|
-
There is a public demo account. Point the package at it and
|
|
47
|
-
|
|
46
|
+
There is a public demo account. Point the package at it and everything in this
|
|
47
|
+
README works immediately, with no signup:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
50
|
export OPENSOLR_EMAIL=mcp@opensolr.com
|
|
@@ -52,17 +52,18 @@ export OPENSOLR_API_KEY=420b8b23e7b12dc8ab838932145a5065
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
`mcp_demo_d1__dense` is already loaded with 300 news articles, so search, filtering
|
|
55
|
-
and grounded answers work the moment you connect. You also get the
|
|
56
|
-
create your own index on the account, ingest into it
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
and grounded answers work the moment you connect. You also get the write path:
|
|
56
|
+
create your own index on the account, ingest into it and query it. Deletion is not
|
|
57
|
+
available on this shared key: no index here can be deleted or reconfigured by hand.
|
|
58
|
+
Whatever you create is removed automatically after 3 days.
|
|
59
59
|
|
|
60
60
|
Know what you are working with:
|
|
61
61
|
|
|
62
62
|
- **Anything you create there is deleted after 3 days.** Automatically, without warning
|
|
63
63
|
or export. That includes indexes you created and every document in them.
|
|
64
|
-
- **The account is shared with everyone reading this.** Your index is visible to them
|
|
65
|
-
they can
|
|
64
|
+
- **The account is shared with everyone reading this.** Your index is visible to them
|
|
65
|
+
and they can add documents to it, as you can to theirs. Nobody can delete or
|
|
66
|
+
reconfigure an index here (that is switched off for this key), but never put anything
|
|
66
67
|
real, private or client-owned in it.
|
|
67
68
|
- **The limits are per index, and deliberately small.** 200 MB of bandwidth and 50 MB
|
|
68
69
|
of disk per index. Bandwidth is the one you will hit first: it covers a demo, a
|
|
@@ -154,8 +155,9 @@ text from PDF/DOCX/XLSX for you.
|
|
|
154
155
|
## Lexical-only mode
|
|
155
156
|
|
|
156
157
|
Don't need vectors? Pure keyword search skips the embedding call entirely —
|
|
157
|
-
zero AI quota
|
|
158
|
-
|
|
158
|
+
zero AI quota. It runs on any Opensolr index that has `title`, `description` and
|
|
159
|
+
`text` fields, including indexes outside the vector regions and older Solr
|
|
160
|
+
versions (see [Index requirements](#index-requirements)).
|
|
159
161
|
|
|
160
162
|
## Search by image
|
|
161
163
|
|
|
@@ -223,7 +225,7 @@ Data Ingestion with `rtf`, PDFs found by the Web Crawler), a search returns each
|
|
|
223
225
|
**once**, as its best page. Its metadata carries `page_number` (the page number),
|
|
224
226
|
`page_url` (opens the PDF at that page, `...pdf#page=14`), `document_id`,
|
|
225
227
|
`more_pages` (its other matching pages, each with `page` and `page_url`) and
|
|
226
|
-
`matching_pages`;
|
|
228
|
+
`matching_pages`, and its `uri`; any other hit keeps exactly the metadata it was sent with. `group_pages=False` returns the pages
|
|
227
229
|
as separate results.
|
|
228
230
|
|
|
229
231
|
## Searching a period
|
|
@@ -239,6 +241,74 @@ OpensolrHybridRetriever(document_store=store, date_from="2026-01-01")
|
|
|
239
241
|
|
|
240
242
|
They combine with every other option — filters, `fresh_bias`, hybrid or lexical.
|
|
241
243
|
|
|
244
|
+
## Index requirements
|
|
245
|
+
|
|
246
|
+
Everything this package does runs on an Opensolr index that sits in a **vector
|
|
247
|
+
region** and carries the **Opensolr vector schema** — the configuration every
|
|
248
|
+
Opensolr vector index runs. An index created through the package (`create_index`,
|
|
249
|
+
`create_if_missing`) has both. An index made elsewhere may not: a server outside
|
|
250
|
+
the vector regions, an older Solr version, or a schema of its own.
|
|
251
|
+
|
|
252
|
+
**The check is automatic.** The first time the package touches an index, it asks
|
|
253
|
+
the platform once whether that index can do what is asked: one request per index
|
|
254
|
+
per client, kept for the client's life, never one per search. If the check itself
|
|
255
|
+
cannot run (a network error, the platform busy), nothing is blocked: the
|
|
256
|
+
operation runs exactly as it always did, and the check is tried again after 5
|
|
257
|
+
minutes. When an index differs from the Opensolr vector schema, a warning is logged
|
|
258
|
+
once, on the `opensolr` logger, with the reasons and the fix.
|
|
259
|
+
|
|
260
|
+
What the check gates — only what cannot work otherwise:
|
|
261
|
+
|
|
262
|
+
| Capability | Needed for | When it is missing |
|
|
263
|
+
|---|---|---|
|
|
264
|
+
| `ingestion` | Data Ingestion: adding documents and indexing files | The index is outside the vector regions: refused before a document or a byte is sent |
|
|
265
|
+
| `vector_search` | Vector and hybrid search | The index has no vector field of the right size: refused before any embedding is paid for |
|
|
266
|
+
| `pdf_pages` | Grouping the pages of a PDF in search results | Results come back ungrouped, as before |
|
|
267
|
+
| `keyword_search` | Keyword (lexical) search | Never blocked |
|
|
268
|
+
|
|
269
|
+
**The errors say exactly what is going on.** A blocked operation raises
|
|
270
|
+
`OpensolrSchemaError` (a subclass of `OpensolrError`) with `.index`, `.capability`
|
|
271
|
+
and `.report` (the full check), and a message with every reason and the fix:
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
Index 'shop_it' cannot take documents or files through Data Ingestion. Index shop_it
|
|
275
|
+
is on IT-SOLR-9-6 (Milano, Italy), which is not a vector region: Data Ingestion,
|
|
276
|
+
server-side embeddings and hybrid search run only on the servers of a vector region
|
|
277
|
+
(DE-SOLR-9 (Germany, Solr 9.00), FINLAND9 (Finland, Solr 9.60), ...). This index
|
|
278
|
+
cannot be made ready: create a new index in one of the vector regions (...), for
|
|
279
|
+
example with create_index, and send your documents there.
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
When Solr itself refuses a request, `OpensolrSolrError` carries Solr's own reason
|
|
283
|
+
and the index name; it is still an `httpx.HTTPStatusError`, so code that caught
|
|
284
|
+
that before keeps working.
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
from haystack_integrations.document_stores.opensolr import OpensolrDocumentStore, OpensolrSchemaError, OpensolrSolrError
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
store.write_files("./contracts")
|
|
291
|
+
except OpensolrSchemaError as e:
|
|
292
|
+
print(e.capability, e.report["fix"]["action"]) # ingestion new_index
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**See the details, and fix what can be fixed:**
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
store.check_schema() # ready, capabilities, server, Solr version, missing / different fields, problems, fix
|
|
299
|
+
store.apply_schema(force=False) # installs the Opensolr vector schema; force=True when the index holds documents
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`apply_schema` exists so an index of a vector region whose configuration was
|
|
303
|
+
changed, or never set, can take everything this package does without being
|
|
304
|
+
recreated: it installs the same configuration every Opensolr vector index runs,
|
|
305
|
+
and **replaces the index configuration**. An index that already holds documents
|
|
306
|
+
needs `force=True`; those documents keep the fields they were written with and
|
|
307
|
+
must be sent again. An index outside the vector regions cannot take it — create
|
|
308
|
+
a new index in a region from `vector_regions` and send your documents there. The
|
|
309
|
+
package never installs the schema on its own.
|
|
310
|
+
|
|
311
|
+
|
|
242
312
|
## Your index schema
|
|
243
313
|
|
|
244
314
|
Documents follow the Opensolr document model (`title`, `description`, `text`,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Opensolr DocumentStore for Haystack — managed Apache Solr with server-side
|
|
2
2
|
embeddings and native hybrid (BM25 + kNN) search."""
|
|
3
3
|
|
|
4
|
+
from haystack_integrations.document_stores.opensolr.client import OpensolrError, OpensolrSchemaError, OpensolrSolrError
|
|
4
5
|
from haystack_integrations.document_stores.opensolr.store import OpensolrDocumentStore
|
|
5
6
|
|
|
6
|
-
__all__ = ["OpensolrDocumentStore"]
|
|
7
|
+
__all__ = ["OpensolrDocumentStore", "OpensolrError", "OpensolrSchemaError", "OpensolrSolrError"]
|
|
@@ -14,12 +14,15 @@ import re
|
|
|
14
14
|
import json
|
|
15
15
|
import time
|
|
16
16
|
import hashlib
|
|
17
|
+
import logging
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
19
20
|
from urllib.parse import quote
|
|
20
21
|
|
|
21
22
|
import httpx
|
|
22
23
|
|
|
24
|
+
_log = logging.getLogger("opensolr")
|
|
25
|
+
|
|
23
26
|
MGMT_BASE = "https://opensolr.com/solr_manager/api"
|
|
24
27
|
AI_BASE = "https://api.opensolr.com/solr_manager/api"
|
|
25
28
|
|
|
@@ -743,6 +746,29 @@ class OpensolrError(RuntimeError):
|
|
|
743
746
|
"""Raised when an Opensolr API call fails."""
|
|
744
747
|
|
|
745
748
|
|
|
749
|
+
class OpensolrSolrError(OpensolrError, httpx.HTTPStatusError):
|
|
750
|
+
"""Solr refused a request on an index: the message carries Solr's own reason. Also an
|
|
751
|
+
``httpx.HTTPStatusError``, so code that caught that before still catches it."""
|
|
752
|
+
|
|
753
|
+
def __init__(self, message: str, response: "httpx.Response") -> None:
|
|
754
|
+
httpx.HTTPStatusError.__init__(self, message, request=response.request, response=response)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
class OpensolrSchemaError(OpensolrError):
|
|
758
|
+
"""The index cannot do what was asked: it is outside the vector regions, or its schema
|
|
759
|
+
lacks what the operation needs. ``report`` is the full check (see ``check_schema``)."""
|
|
760
|
+
|
|
761
|
+
def __init__(self, index: str, capability: str, report: Dict[str, Any]) -> None:
|
|
762
|
+
self.index = index
|
|
763
|
+
self.capability = capability
|
|
764
|
+
self.report = report
|
|
765
|
+
what = OpensolrClient.CAPABILITY_TEXT.get(capability, capability)
|
|
766
|
+
problems = " ".join(report.get("problems") or []) or "The index check says it cannot."
|
|
767
|
+
fix = (report.get("fix") or {}).get("text") or ""
|
|
768
|
+
super().__init__(f"Index {index!r} cannot {what}. {problems} {fix} "
|
|
769
|
+
f"(client.check_schema({index!r}) shows the details.)".replace(" ", " "))
|
|
770
|
+
|
|
771
|
+
|
|
746
772
|
class OpensolrClient:
|
|
747
773
|
"""Authenticated client for Opensolr management + AI endpoints.
|
|
748
774
|
|
|
@@ -758,8 +784,9 @@ class OpensolrClient:
|
|
|
758
784
|
self.api_key = api_key
|
|
759
785
|
self._http = httpx.Client(timeout=timeout, follow_redirects=True)
|
|
760
786
|
self._core_info_cache: Dict[str, Dict[str, Any]] = {}
|
|
761
|
-
#
|
|
762
|
-
self.
|
|
787
|
+
# the schema check of each index, made once (check_schema); when it could not run
|
|
788
|
+
self._schema_reports: Dict[str, Dict[str, Any]] = {}
|
|
789
|
+
self._schema_failed: Dict[str, float] = {}
|
|
763
790
|
|
|
764
791
|
# ------------------------------------------------------------------ #
|
|
765
792
|
# low level #
|
|
@@ -792,9 +819,21 @@ class OpensolrClient:
|
|
|
792
819
|
except json.JSONDecodeError as exc:
|
|
793
820
|
raise OpensolrError(f"{label}: non-JSON response: {resp.text[:200]}") from exc
|
|
794
821
|
if isinstance(body, dict) and body.get("status") is False:
|
|
795
|
-
|
|
822
|
+
detail = body.get("detail")
|
|
823
|
+
raise OpensolrError(f"{label}: {body.get('msg', body)}" + (f": {detail}" if detail else ""))
|
|
796
824
|
return body
|
|
797
825
|
|
|
826
|
+
def _solr_answer(self, index: str, what: str, resp: "httpx.Response") -> Dict[str, Any]:
|
|
827
|
+
"""Solr's JSON, or :class:`OpensolrSolrError` with Solr's own reason."""
|
|
828
|
+
if resp.status_code >= 400:
|
|
829
|
+
try:
|
|
830
|
+
why = (resp.json().get("error") or {}).get("msg") or ""
|
|
831
|
+
except (ValueError, AttributeError):
|
|
832
|
+
why = re.sub(r"<[^>]+>", " ", resp.text)[:300].strip()
|
|
833
|
+
raise OpensolrSolrError(
|
|
834
|
+
f"Solr refused the {what} on index {index} (HTTP {resp.status_code}): {why or resp.reason_phrase}", resp)
|
|
835
|
+
return resp.json()
|
|
836
|
+
|
|
798
837
|
def _post_with_retry(self, *, url: str, label: str, **kwargs: Any) -> "httpx.Response":
|
|
799
838
|
"""POST, retrying only when the connection failed BEFORE a response was produced.
|
|
800
839
|
|
|
@@ -940,6 +979,7 @@ class OpensolrClient:
|
|
|
940
979
|
``wait=True``, polls ``ingest_status`` until the job completes
|
|
941
980
|
(the queue is processed every minute).
|
|
942
981
|
"""
|
|
982
|
+
self.require_capability(index, "ingestion")
|
|
943
983
|
resp = self._post_with_retry(label="ingest", url=
|
|
944
984
|
f"{AI_BASE}/ingest",
|
|
945
985
|
json={**self._auth_params(), "core_name": index, "documents": documents},
|
|
@@ -1036,8 +1076,7 @@ class OpensolrClient:
|
|
|
1036
1076
|
def solr_select(self, index: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
1037
1077
|
base, auth = self.solr_endpoint(index)
|
|
1038
1078
|
resp = self._post_with_retry(label="solr_select", url=f"{base}/select", data={"wt": "json", **params}, auth=auth)
|
|
1039
|
-
|
|
1040
|
-
return resp.json()
|
|
1079
|
+
return self._solr_answer(index, "search", resp)
|
|
1041
1080
|
|
|
1042
1081
|
def hybrid_search(
|
|
1043
1082
|
self,
|
|
@@ -1073,6 +1112,7 @@ class OpensolrClient:
|
|
|
1073
1112
|
if not 0.0 <= alpha_f <= 1.0:
|
|
1074
1113
|
raise ValueError(f"alpha must be between 0 and 1, got {alpha_f}")
|
|
1075
1114
|
|
|
1115
|
+
self.require_capability(index, "vector_search")
|
|
1076
1116
|
clean = query.replace("{", " ").replace("}", " ").replace('"', " ")
|
|
1077
1117
|
vector = self.embed(index, query, is_query=True)
|
|
1078
1118
|
compact = json.dumps(vector, separators=(",", ":"))
|
|
@@ -1232,8 +1272,91 @@ class OpensolrClient:
|
|
|
1232
1272
|
json=payload,
|
|
1233
1273
|
auth=auth,
|
|
1234
1274
|
)
|
|
1235
|
-
|
|
1236
|
-
|
|
1275
|
+
return self._solr_answer(index, "update", resp)
|
|
1276
|
+
|
|
1277
|
+
# ------------------------------------------------------------------ #
|
|
1278
|
+
# the Opensolr vector schema: checked once per index, installed on request #
|
|
1279
|
+
# ------------------------------------------------------------------ #
|
|
1280
|
+
|
|
1281
|
+
#: Seconds before a schema check that could not run is tried again (never on every call).
|
|
1282
|
+
SCHEMA_RETRY_SECONDS = 300.0
|
|
1283
|
+
|
|
1284
|
+
#: What each capability lets a client do, for the error messages.
|
|
1285
|
+
CAPABILITY_TEXT = {
|
|
1286
|
+
"ingestion": "take documents or files through Data Ingestion",
|
|
1287
|
+
"vector_search": "be searched by meaning (vector or hybrid search)",
|
|
1288
|
+
"pdf_pages": "return PDF pages",
|
|
1289
|
+
"keyword_search": "be searched by keywords",
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
def check_schema(self, index: str) -> Dict[str, Any]:
|
|
1293
|
+
"""Whether an index is ready for the Opensolr packages, and every reason when it is not.
|
|
1294
|
+
|
|
1295
|
+
One request to the platform, which reads the index's schema and compares it with the
|
|
1296
|
+
Opensolr vector schema. The answer carries ``ready``, ``capabilities``
|
|
1297
|
+
(``ingestion``, ``vector_search``, ``pdf_pages``, ``keyword_search``), the server,
|
|
1298
|
+
the Solr version, whether the server is in a vector region, the fields and types
|
|
1299
|
+
that are missing or different, ``problems`` (sentences) and ``fix`` (``none``,
|
|
1300
|
+
``apply_schema`` or ``new_index``, with what to do).
|
|
1301
|
+
"""
|
|
1302
|
+
return self.mgmt("check_schema", core_name=index)
|
|
1303
|
+
|
|
1304
|
+
def apply_schema(self, index: str, force: bool = False) -> Dict[str, Any]:
|
|
1305
|
+
"""Install the Opensolr vector schema on an index of a vector region.
|
|
1306
|
+
|
|
1307
|
+
It replaces the index configuration with the one every Opensolr vector index runs,
|
|
1308
|
+
so Data Ingestion, vector and hybrid search and PDF pages all work on it. An index
|
|
1309
|
+
that already holds documents needs ``force=True``: those documents keep the fields
|
|
1310
|
+
they were written with and must be sent again. An index outside the vector regions
|
|
1311
|
+
cannot take it: the error says where to create one. Returns the new check.
|
|
1312
|
+
"""
|
|
1313
|
+
self._schema_reports.pop(index, None)
|
|
1314
|
+
self._schema_failed.pop(index, None)
|
|
1315
|
+
body = self.mgmt("apply_schema", core_name=index, force="1" if force else "0")
|
|
1316
|
+
check = body.get("check") if isinstance(body, dict) else None
|
|
1317
|
+
if isinstance(check, dict) and check.get("status") is True:
|
|
1318
|
+
self._schema_reports[index] = check
|
|
1319
|
+
return body
|
|
1320
|
+
|
|
1321
|
+
def schema_report(self, index: str) -> Optional[Dict[str, Any]]:
|
|
1322
|
+
"""The check of an index, made once for this client (a check that could not run is
|
|
1323
|
+
tried again after :data:`SCHEMA_RETRY_SECONDS`, and nothing is concluded from it)."""
|
|
1324
|
+
if index in self._schema_reports:
|
|
1325
|
+
return self._schema_reports[index]
|
|
1326
|
+
failed_at = self._schema_failed.get(index)
|
|
1327
|
+
if failed_at is not None and time.time() - failed_at < self.SCHEMA_RETRY_SECONDS:
|
|
1328
|
+
return None
|
|
1329
|
+
try:
|
|
1330
|
+
report = self.check_schema(index)
|
|
1331
|
+
except OpensolrError as exc:
|
|
1332
|
+
self._schema_failed[index] = time.time()
|
|
1333
|
+
_log.warning("Opensolr: the schema of index %s could not be checked (%s); working as before, without the check", index, exc)
|
|
1334
|
+
return None
|
|
1335
|
+
self._schema_failed.pop(index, None)
|
|
1336
|
+
self._schema_reports[index] = report
|
|
1337
|
+
if not report.get("ready"):
|
|
1338
|
+
fix = (report.get("fix") or {}).get("text") or ""
|
|
1339
|
+
_log.warning("Opensolr: index %s is not on the Opensolr vector schema: %s %s", index,
|
|
1340
|
+
" ".join(report.get("problems") or []), fix)
|
|
1341
|
+
return report
|
|
1342
|
+
|
|
1343
|
+
def require_capability(self, index: str, capability: str) -> None:
|
|
1344
|
+
"""Raise :class:`OpensolrSchemaError` when the index's check says it cannot do this.
|
|
1345
|
+
|
|
1346
|
+
Uses the check made once for this client; when the check could not run, nothing is
|
|
1347
|
+
blocked and the operation runs as it always did.
|
|
1348
|
+
"""
|
|
1349
|
+
report = self.schema_report(index)
|
|
1350
|
+
if report is None:
|
|
1351
|
+
return
|
|
1352
|
+
caps = report.get("capabilities") or {}
|
|
1353
|
+
if caps.get(capability) is False:
|
|
1354
|
+
raise OpensolrSchemaError(index, capability, report)
|
|
1355
|
+
|
|
1356
|
+
def has_capability(self, index: str, capability: str) -> bool:
|
|
1357
|
+
"""True only when the index's check says it can (an unknown answer is False)."""
|
|
1358
|
+
report = self.schema_report(index)
|
|
1359
|
+
return bool(report and (report.get("capabilities") or {}).get(capability))
|
|
1237
1360
|
|
|
1238
1361
|
# ------------------------------------------------------------------ #
|
|
1239
1362
|
# PDF pages, documents, pictures #
|
|
@@ -1242,22 +1365,16 @@ class OpensolrClient:
|
|
|
1242
1365
|
def select_grouped(self, index: str, params: Dict[str, Any], group_pages: bool = True) -> Dict[str, Any]:
|
|
1243
1366
|
"""``solr_select`` with the pages of each PDF grouped under its best page.
|
|
1244
1367
|
|
|
1245
|
-
See :func:`apply_page_grouping`.
|
|
1246
|
-
|
|
1368
|
+
See :func:`apply_page_grouping`. Grouped only when the index's check (made once)
|
|
1369
|
+
says it has the PDF page fields; otherwise searched as it is.
|
|
1247
1370
|
"""
|
|
1248
|
-
if not group_pages or
|
|
1371
|
+
if not group_pages or not self.has_capability(index, "pdf_pages"):
|
|
1249
1372
|
return self.solr_select(index, params)
|
|
1250
1373
|
grouped = dict(params)
|
|
1251
1374
|
if isinstance(grouped.get("fq"), list):
|
|
1252
1375
|
grouped["fq"] = list(grouped["fq"])
|
|
1253
1376
|
apply_page_grouping(grouped)
|
|
1254
|
-
|
|
1255
|
-
return self.solr_select(index, grouped)
|
|
1256
|
-
except httpx.HTTPStatusError as exc:
|
|
1257
|
-
if exc.response.status_code == 400 and PARENT_FIELD in exc.response.text:
|
|
1258
|
-
self._flat_indexes.add(index)
|
|
1259
|
-
return self.solr_select(index, params)
|
|
1260
|
-
raise
|
|
1377
|
+
return self.solr_select(index, grouped)
|
|
1261
1378
|
|
|
1262
1379
|
def read_document(
|
|
1263
1380
|
self,
|
|
@@ -1285,24 +1402,17 @@ class OpensolrClient:
|
|
|
1285
1402
|
last = min(last, first + READ_MAX_PAGES - 1)
|
|
1286
1403
|
esc = key.replace("\\", "\\\\").replace('"', '\\"')
|
|
1287
1404
|
fl = "id,uri,title,description,text,content_type,creation_date,meta_ext_id"
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
})
|
|
1298
|
-
docs = body.get("response", {}).get("docs", [])
|
|
1299
|
-
except httpx.HTTPStatusError as exc:
|
|
1300
|
-
if exc.response.status_code != 400 or (PAGE_FIELD not in exc.response.text and PARENT_FIELD not in exc.response.text):
|
|
1301
|
-
raise
|
|
1302
|
-
self._flat_indexes.add(index)
|
|
1303
|
-
if index in self._flat_indexes:
|
|
1405
|
+
if self.has_capability(index, "pdf_pages"):
|
|
1406
|
+
body = self.solr_select(index, {
|
|
1407
|
+
"q": f'id:"{esc}" OR {PARENT_FIELD}:"{esc}" OR meta_ext_id:"{esc}"',
|
|
1408
|
+
"fq": f"(*:* -{PAGE_FIELD}:[* TO *]) OR {PAGE_FIELD}:[{first} TO {last}]",
|
|
1409
|
+
"sort": f"{PAGE_FIELD} asc",
|
|
1410
|
+
"rows": last - first + 1,
|
|
1411
|
+
"fl": f"{fl},{PAGE_FIELD},{PARENT_FIELD}",
|
|
1412
|
+
})
|
|
1413
|
+
else:
|
|
1304
1414
|
body = self.solr_select(index, {"q": f'id:"{esc}" OR meta_ext_id:"{esc}"', "rows": 1, "fl": fl})
|
|
1305
|
-
|
|
1415
|
+
docs: List[Dict[str, Any]] = body.get("response", {}).get("docs", [])
|
|
1306
1416
|
if not docs:
|
|
1307
1417
|
raise OpensolrError(f"read_document: nothing in {index} for {document!r}")
|
|
1308
1418
|
|
|
@@ -1519,6 +1629,8 @@ class OpensolrClient:
|
|
|
1519
1629
|
Returns ``{"documents": [...], "pending": [...], "failed": [...], "jobs": [...]}``;
|
|
1520
1630
|
``jobs`` are the Data Ingestion job ids (``wait=True`` blocks until they are done).
|
|
1521
1631
|
"""
|
|
1632
|
+
# before a byte is sent: an index that cannot take Data Ingestion says so now, not after the uploads
|
|
1633
|
+
self.require_capability(index, "ingestion")
|
|
1522
1634
|
meta = {str(k): v for k, v in (metadata or {}).items() if isinstance(v, (str, int, float, bool))}
|
|
1523
1635
|
root = Path(path).expanduser()
|
|
1524
1636
|
files = list_files(root, pattern, recursive)
|
|
@@ -217,15 +217,16 @@ class OpensolrDocumentStore:
|
|
|
217
217
|
score = solr_doc.get("score")
|
|
218
218
|
ext = _flat(solr_doc.get("meta_ext_id"))
|
|
219
219
|
doc_id = str(ext) if ext else str(_flat(solr_doc.get("id", "")))
|
|
220
|
-
#
|
|
220
|
+
# A page of a PDF also says where it lives (uri, page_number, page_url, document_id, more_pages);
|
|
221
|
+
# any other document keeps exactly the meta it was sent with
|
|
221
222
|
where = page_info(solr_doc, expanded)
|
|
222
223
|
if where.get("page"):
|
|
223
224
|
where["page_number"] = where.pop("page")
|
|
224
225
|
# every page of a PDF carries the PDF's meta_ext_id: the page number keeps ids apart
|
|
225
226
|
doc_id = f"{doc_id}#page={where['page_number']}"
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
for key, value in where.items():
|
|
228
|
+
if value not in ("", None):
|
|
229
|
+
meta.setdefault(key, value)
|
|
229
230
|
return Document(
|
|
230
231
|
id=doc_id,
|
|
231
232
|
content=str(content),
|
|
@@ -290,6 +291,26 @@ class OpensolrDocumentStore:
|
|
|
290
291
|
self.client.ingest(self.index, docs[i : i + 50], wait=self.ingest_wait)
|
|
291
292
|
return len(docs)
|
|
292
293
|
|
|
294
|
+
def check_schema(self) -> Dict[str, Any]:
|
|
295
|
+
"""Whether this index is ready for every feature of the store, and exactly why not.
|
|
296
|
+
|
|
297
|
+
Returns the platform's check: ``ready``, ``capabilities`` (``ingestion``,
|
|
298
|
+
``vector_search``, ``pdf_pages``, ``keyword_search``), the server, its Solr
|
|
299
|
+
version, the fields and types that differ from the Opensolr vector schema,
|
|
300
|
+
``problems`` and ``fix``. The store runs this check once on its own.
|
|
301
|
+
"""
|
|
302
|
+
self._ensure_index()
|
|
303
|
+
return self.client.check_schema(self.index)
|
|
304
|
+
|
|
305
|
+
def apply_schema(self, force: bool = False) -> Dict[str, Any]:
|
|
306
|
+
"""Install the Opensolr vector schema on this index (an index of a vector region).
|
|
307
|
+
|
|
308
|
+
It replaces the index configuration; an index that already holds documents needs
|
|
309
|
+
``force=True``, and those documents must be sent again. Returns the new check.
|
|
310
|
+
"""
|
|
311
|
+
self._ensure_index()
|
|
312
|
+
return self.client.apply_schema(self.index, force=force)
|
|
313
|
+
|
|
293
314
|
def write_files(
|
|
294
315
|
self,
|
|
295
316
|
path: Union[str, Path],
|
|
@@ -402,7 +423,7 @@ class OpensolrDocumentStore:
|
|
|
402
423
|
dated in that period (``creation_date``). ``group_pages`` (on by default) returns
|
|
403
424
|
a PDF indexed page by page once, as its best page: ``meta`` carries
|
|
404
425
|
``page_number``, ``page_url`` (opens the PDF at that page), ``document_id`` and
|
|
405
|
-
``more_pages`` (its other matching pages)
|
|
426
|
+
``more_pages`` (its other matching pages) and its ``uri``.
|
|
406
427
|
"""
|
|
407
428
|
self._ensure_index()
|
|
408
429
|
params: Dict[str, Any] = {"rows": top_k, "fl": "*,score"}
|
|
@@ -434,6 +455,8 @@ class OpensolrDocumentStore:
|
|
|
434
455
|
expanded = body.get("expanded") or {}
|
|
435
456
|
return [self._doc_from_solr(d, expanded) for d in body["response"]["docs"]]
|
|
436
457
|
|
|
458
|
+
# An index without the vector fields says so before an embedding is paid for
|
|
459
|
+
self.client.require_capability(self.index, "vector_search")
|
|
437
460
|
# The embedder must never see an operator — it has no concept of negation, so a '-'
|
|
438
461
|
# term reads as one more word and pulls results towards what the caller asked to drop.
|
|
439
462
|
vector = self.client.embed(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opensolr-haystack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Haystack integration for Opensolr — managed Apache Solr DocumentStore with server-side embeddings and hybrid BM25+kNN retrieval
|
|
5
5
|
Author-email: Opensolr <support@opensolr.com>
|
|
6
6
|
License: MIT
|
|
@@ -61,8 +61,8 @@ the query. The store embeds server-side at both index and query time.
|
|
|
61
61
|
|
|
62
62
|
## Try it without an account
|
|
63
63
|
|
|
64
|
-
There is a public demo account. Point the package at it and
|
|
65
|
-
|
|
64
|
+
There is a public demo account. Point the package at it and everything in this
|
|
65
|
+
README works immediately, with no signup:
|
|
66
66
|
|
|
67
67
|
```bash
|
|
68
68
|
export OPENSOLR_EMAIL=mcp@opensolr.com
|
|
@@ -70,17 +70,18 @@ export OPENSOLR_API_KEY=420b8b23e7b12dc8ab838932145a5065
|
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
`mcp_demo_d1__dense` is already loaded with 300 news articles, so search, filtering
|
|
73
|
-
and grounded answers work the moment you connect. You also get the
|
|
74
|
-
create your own index on the account, ingest into it
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
and grounded answers work the moment you connect. You also get the write path:
|
|
74
|
+
create your own index on the account, ingest into it and query it. Deletion is not
|
|
75
|
+
available on this shared key: no index here can be deleted or reconfigured by hand.
|
|
76
|
+
Whatever you create is removed automatically after 3 days.
|
|
77
77
|
|
|
78
78
|
Know what you are working with:
|
|
79
79
|
|
|
80
80
|
- **Anything you create there is deleted after 3 days.** Automatically, without warning
|
|
81
81
|
or export. That includes indexes you created and every document in them.
|
|
82
|
-
- **The account is shared with everyone reading this.** Your index is visible to them
|
|
83
|
-
they can
|
|
82
|
+
- **The account is shared with everyone reading this.** Your index is visible to them
|
|
83
|
+
and they can add documents to it, as you can to theirs. Nobody can delete or
|
|
84
|
+
reconfigure an index here (that is switched off for this key), but never put anything
|
|
84
85
|
real, private or client-owned in it.
|
|
85
86
|
- **The limits are per index, and deliberately small.** 200 MB of bandwidth and 50 MB
|
|
86
87
|
of disk per index. Bandwidth is the one you will hit first: it covers a demo, a
|
|
@@ -172,8 +173,9 @@ text from PDF/DOCX/XLSX for you.
|
|
|
172
173
|
## Lexical-only mode
|
|
173
174
|
|
|
174
175
|
Don't need vectors? Pure keyword search skips the embedding call entirely —
|
|
175
|
-
zero AI quota
|
|
176
|
-
|
|
176
|
+
zero AI quota. It runs on any Opensolr index that has `title`, `description` and
|
|
177
|
+
`text` fields, including indexes outside the vector regions and older Solr
|
|
178
|
+
versions (see [Index requirements](#index-requirements)).
|
|
177
179
|
|
|
178
180
|
## Search by image
|
|
179
181
|
|
|
@@ -241,7 +243,7 @@ Data Ingestion with `rtf`, PDFs found by the Web Crawler), a search returns each
|
|
|
241
243
|
**once**, as its best page. Its metadata carries `page_number` (the page number),
|
|
242
244
|
`page_url` (opens the PDF at that page, `...pdf#page=14`), `document_id`,
|
|
243
245
|
`more_pages` (its other matching pages, each with `page` and `page_url`) and
|
|
244
|
-
`matching_pages`;
|
|
246
|
+
`matching_pages`, and its `uri`; any other hit keeps exactly the metadata it was sent with. `group_pages=False` returns the pages
|
|
245
247
|
as separate results.
|
|
246
248
|
|
|
247
249
|
## Searching a period
|
|
@@ -257,6 +259,74 @@ OpensolrHybridRetriever(document_store=store, date_from="2026-01-01")
|
|
|
257
259
|
|
|
258
260
|
They combine with every other option — filters, `fresh_bias`, hybrid or lexical.
|
|
259
261
|
|
|
262
|
+
## Index requirements
|
|
263
|
+
|
|
264
|
+
Everything this package does runs on an Opensolr index that sits in a **vector
|
|
265
|
+
region** and carries the **Opensolr vector schema** — the configuration every
|
|
266
|
+
Opensolr vector index runs. An index created through the package (`create_index`,
|
|
267
|
+
`create_if_missing`) has both. An index made elsewhere may not: a server outside
|
|
268
|
+
the vector regions, an older Solr version, or a schema of its own.
|
|
269
|
+
|
|
270
|
+
**The check is automatic.** The first time the package touches an index, it asks
|
|
271
|
+
the platform once whether that index can do what is asked: one request per index
|
|
272
|
+
per client, kept for the client's life, never one per search. If the check itself
|
|
273
|
+
cannot run (a network error, the platform busy), nothing is blocked: the
|
|
274
|
+
operation runs exactly as it always did, and the check is tried again after 5
|
|
275
|
+
minutes. When an index differs from the Opensolr vector schema, a warning is logged
|
|
276
|
+
once, on the `opensolr` logger, with the reasons and the fix.
|
|
277
|
+
|
|
278
|
+
What the check gates — only what cannot work otherwise:
|
|
279
|
+
|
|
280
|
+
| Capability | Needed for | When it is missing |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| `ingestion` | Data Ingestion: adding documents and indexing files | The index is outside the vector regions: refused before a document or a byte is sent |
|
|
283
|
+
| `vector_search` | Vector and hybrid search | The index has no vector field of the right size: refused before any embedding is paid for |
|
|
284
|
+
| `pdf_pages` | Grouping the pages of a PDF in search results | Results come back ungrouped, as before |
|
|
285
|
+
| `keyword_search` | Keyword (lexical) search | Never blocked |
|
|
286
|
+
|
|
287
|
+
**The errors say exactly what is going on.** A blocked operation raises
|
|
288
|
+
`OpensolrSchemaError` (a subclass of `OpensolrError`) with `.index`, `.capability`
|
|
289
|
+
and `.report` (the full check), and a message with every reason and the fix:
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
Index 'shop_it' cannot take documents or files through Data Ingestion. Index shop_it
|
|
293
|
+
is on IT-SOLR-9-6 (Milano, Italy), which is not a vector region: Data Ingestion,
|
|
294
|
+
server-side embeddings and hybrid search run only on the servers of a vector region
|
|
295
|
+
(DE-SOLR-9 (Germany, Solr 9.00), FINLAND9 (Finland, Solr 9.60), ...). This index
|
|
296
|
+
cannot be made ready: create a new index in one of the vector regions (...), for
|
|
297
|
+
example with create_index, and send your documents there.
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
When Solr itself refuses a request, `OpensolrSolrError` carries Solr's own reason
|
|
301
|
+
and the index name; it is still an `httpx.HTTPStatusError`, so code that caught
|
|
302
|
+
that before keeps working.
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
from haystack_integrations.document_stores.opensolr import OpensolrDocumentStore, OpensolrSchemaError, OpensolrSolrError
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
store.write_files("./contracts")
|
|
309
|
+
except OpensolrSchemaError as e:
|
|
310
|
+
print(e.capability, e.report["fix"]["action"]) # ingestion new_index
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**See the details, and fix what can be fixed:**
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
store.check_schema() # ready, capabilities, server, Solr version, missing / different fields, problems, fix
|
|
317
|
+
store.apply_schema(force=False) # installs the Opensolr vector schema; force=True when the index holds documents
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`apply_schema` exists so an index of a vector region whose configuration was
|
|
321
|
+
changed, or never set, can take everything this package does without being
|
|
322
|
+
recreated: it installs the same configuration every Opensolr vector index runs,
|
|
323
|
+
and **replaces the index configuration**. An index that already holds documents
|
|
324
|
+
needs `force=True`; those documents keep the fields they were written with and
|
|
325
|
+
must be sent again. An index outside the vector regions cannot take it — create
|
|
326
|
+
a new index in a region from `vector_regions` and send your documents there. The
|
|
327
|
+
package never installs the schema on its own.
|
|
328
|
+
|
|
329
|
+
|
|
260
330
|
## Your index schema
|
|
261
331
|
|
|
262
332
|
Documents follow the Opensolr document model (`title`, `description`, `text`,
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "opensolr-haystack"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "Haystack integration for Opensolr — managed Apache Solr DocumentStore with server-side embeddings and hybrid BM25+kNN retrieval"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/opensolr_haystack.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{opensolr_haystack-0.7.0 → opensolr_haystack-0.8.0}/opensolr_haystack.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|