mariadb-haystack 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from haystack_integrations.components.retrievers.mariadb.embedding_retriever import MariaDBEmbeddingRetriever
6
+ from haystack_integrations.components.retrievers.mariadb.keyword_retriever import MariaDBKeywordRetriever
7
+
8
+ __all__ = ["MariaDBEmbeddingRetriever", "MariaDBKeywordRetriever"]
@@ -0,0 +1,112 @@
1
+ # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from typing import Any
6
+
7
+ from haystack import component, default_from_dict, default_to_dict
8
+ from haystack.dataclasses import Document
9
+ from haystack.document_stores.types import FilterPolicy
10
+ from haystack.document_stores.types.filter_policy import apply_filter_policy
11
+
12
+ from haystack_integrations.document_stores.mariadb import MariaDBDocumentStore
13
+
14
+
15
+ @component
16
+ class MariaDBEmbeddingRetriever:
17
+ """
18
+ Retrieves documents from `MariaDBDocumentStore` using vector similarity search.
19
+
20
+ Uses MariaDB's native `VEC_DISTANCE_COSINE` or `VEC_DISTANCE_EUCLIDEAN` functions
21
+ with MHNSW indexing for efficient approximate nearest-neighbour search.
22
+
23
+ ### Usage example
24
+
25
+ ```python
26
+ from haystack_integrations.document_stores.mariadb import MariaDBDocumentStore
27
+ from haystack_integrations.components.retrievers.mariadb import MariaDBEmbeddingRetriever
28
+
29
+ store = MariaDBDocumentStore(host="localhost", database="haystack", embedding_dimension=768)
30
+ retriever = MariaDBEmbeddingRetriever(document_store=store, top_k=5)
31
+ result = retriever.run(query_embedding=[0.1] * 768)
32
+ documents = result["documents"]
33
+ ```
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ *,
39
+ document_store: MariaDBDocumentStore,
40
+ filters: dict[str, Any] | None = None,
41
+ top_k: int = 10,
42
+ score_threshold: float | None = None,
43
+ filter_policy: str | FilterPolicy = FilterPolicy.REPLACE,
44
+ ) -> None:
45
+ """
46
+ Initialize the MariaDBEmbeddingRetriever.
47
+
48
+ :param document_store: A `MariaDBDocumentStore` instance.
49
+ :param filters: Default Haystack metadata filters applied to every query.
50
+ :param top_k: Maximum number of documents to return.
51
+ :param score_threshold: Minimum score to include a document. Documents below this score are excluded.
52
+ :param filter_policy: How runtime filters interact with init-time filters.
53
+ :raises ValueError: If `document_store` is not a `MariaDBDocumentStore`.
54
+ """
55
+ if not isinstance(document_store, MariaDBDocumentStore):
56
+ msg = "document_store must be an instance of MariaDBDocumentStore"
57
+ raise ValueError(msg)
58
+
59
+ self.document_store = document_store
60
+ self.filters = filters or {}
61
+ self.top_k = top_k
62
+ self.score_threshold = score_threshold
63
+ self.filter_policy = (
64
+ filter_policy if isinstance(filter_policy, FilterPolicy) else FilterPolicy.from_str(filter_policy)
65
+ )
66
+
67
+ def to_dict(self) -> dict[str, Any]:
68
+ """Serialize the component to a dictionary."""
69
+ return default_to_dict(
70
+ self,
71
+ filters=self.filters,
72
+ top_k=self.top_k,
73
+ score_threshold=self.score_threshold,
74
+ filter_policy=self.filter_policy.value,
75
+ document_store=self.document_store.to_dict(),
76
+ )
77
+
78
+ @classmethod
79
+ def from_dict(cls, data: dict[str, Any]) -> "MariaDBEmbeddingRetriever":
80
+ """Deserialize the component from a dictionary."""
81
+ doc_store_params = data["init_parameters"]["document_store"]
82
+ data["init_parameters"]["document_store"] = MariaDBDocumentStore.from_dict(doc_store_params)
83
+ if filter_policy := data["init_parameters"].get("filter_policy"):
84
+ data["init_parameters"]["filter_policy"] = FilterPolicy.from_str(filter_policy)
85
+ return default_from_dict(cls, data)
86
+
87
+ @component.output_types(documents=list[Document])
88
+ def run(
89
+ self,
90
+ query_embedding: list[float],
91
+ filters: dict[str, Any] | None = None,
92
+ top_k: int | None = None,
93
+ score_threshold: float | None = None,
94
+ ) -> dict[str, list[Document]]:
95
+ """
96
+ Retrieve documents similar to the query embedding.
97
+
98
+ :param query_embedding: The query vector.
99
+ :param filters: Runtime filters merged with init-time filters per `filter_policy`.
100
+ :param top_k: Override the retriever's `top_k`.
101
+ :param score_threshold: Override the retriever's `score_threshold`.
102
+ :returns: Dictionary with `"documents"` key containing the ranked results.
103
+ """
104
+ filters = apply_filter_policy(self.filter_policy, self.filters, filters)
105
+ return {
106
+ "documents": self.document_store._embedding_retrieval(
107
+ query_embedding=query_embedding,
108
+ filters=filters,
109
+ top_k=top_k or self.top_k,
110
+ score_threshold=score_threshold if score_threshold is not None else self.score_threshold,
111
+ )
112
+ }
@@ -0,0 +1,105 @@
1
+ # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from typing import Any
6
+
7
+ from haystack import component, default_from_dict, default_to_dict
8
+ from haystack.dataclasses import Document
9
+ from haystack.document_stores.types import FilterPolicy
10
+ from haystack.document_stores.types.filter_policy import apply_filter_policy
11
+
12
+ from haystack_integrations.document_stores.mariadb import MariaDBDocumentStore
13
+
14
+
15
+ @component
16
+ class MariaDBKeywordRetriever:
17
+ """
18
+ Retrieves documents from `MariaDBDocumentStore` using full-text keyword search.
19
+
20
+ Uses MariaDB's `MATCH ... AGAINST` full-text search in natural language mode,
21
+ backed by a FULLTEXT index on the `content` column.
22
+
23
+ ### Usage example
24
+
25
+ ```python
26
+ from haystack_integrations.document_stores.mariadb import MariaDBDocumentStore
27
+ from haystack_integrations.components.retrievers.mariadb import MariaDBKeywordRetriever
28
+
29
+ store = MariaDBDocumentStore(host="localhost", database="haystack", embedding_dimension=768)
30
+ retriever = MariaDBKeywordRetriever(document_store=store, top_k=5)
31
+ result = retriever.run(query="climate change")
32
+ documents = result["documents"]
33
+ ```
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ *,
39
+ document_store: MariaDBDocumentStore,
40
+ filters: dict[str, Any] | None = None,
41
+ top_k: int = 10,
42
+ filter_policy: str | FilterPolicy = FilterPolicy.REPLACE,
43
+ ) -> None:
44
+ """
45
+ Initialize the MariaDBKeywordRetriever.
46
+
47
+ :param document_store: A `MariaDBDocumentStore` instance.
48
+ :param filters: Default Haystack metadata filters.
49
+ :param top_k: Maximum number of documents to return.
50
+ :param filter_policy: How runtime filters interact with init-time filters.
51
+ :raises ValueError: If `document_store` is not a `MariaDBDocumentStore`.
52
+ """
53
+ if not isinstance(document_store, MariaDBDocumentStore):
54
+ msg = "document_store must be an instance of MariaDBDocumentStore"
55
+ raise ValueError(msg)
56
+
57
+ self.document_store = document_store
58
+ self.filters = filters or {}
59
+ self.top_k = top_k
60
+ self.filter_policy = (
61
+ filter_policy if isinstance(filter_policy, FilterPolicy) else FilterPolicy.from_str(filter_policy)
62
+ )
63
+
64
+ def to_dict(self) -> dict[str, Any]:
65
+ """Serialize the component to a dictionary."""
66
+ return default_to_dict(
67
+ self,
68
+ filters=self.filters,
69
+ top_k=self.top_k,
70
+ filter_policy=self.filter_policy.value,
71
+ document_store=self.document_store.to_dict(),
72
+ )
73
+
74
+ @classmethod
75
+ def from_dict(cls, data: dict[str, Any]) -> "MariaDBKeywordRetriever":
76
+ """Deserialize the component from a dictionary."""
77
+ doc_store_params = data["init_parameters"]["document_store"]
78
+ data["init_parameters"]["document_store"] = MariaDBDocumentStore.from_dict(doc_store_params)
79
+ if filter_policy := data["init_parameters"].get("filter_policy"):
80
+ data["init_parameters"]["filter_policy"] = FilterPolicy.from_str(filter_policy)
81
+ return default_from_dict(cls, data)
82
+
83
+ @component.output_types(documents=list[Document])
84
+ def run(
85
+ self,
86
+ query: str,
87
+ filters: dict[str, Any] | None = None,
88
+ top_k: int | None = None,
89
+ ) -> dict[str, list[Document]]:
90
+ """
91
+ Retrieve documents matching the query via full-text search.
92
+
93
+ :param query: The keyword query string.
94
+ :param filters: Runtime filters merged with init-time filters per `filter_policy`.
95
+ :param top_k: Override the retriever's `top_k`.
96
+ :returns: Dictionary with `"documents"` key containing results ranked by relevance.
97
+ """
98
+ filters = apply_filter_policy(self.filter_policy, self.filters, filters)
99
+ return {
100
+ "documents": self.document_store._keyword_retrieval(
101
+ query=query,
102
+ filters=filters,
103
+ top_k=top_k or self.top_k,
104
+ )
105
+ }
File without changes
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from haystack_integrations.document_stores.mariadb.document_store import MariaDBDocumentStore
6
+
7
+ __all__ = ["MariaDBDocumentStore"]
@@ -0,0 +1,548 @@
1
+ # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import json
6
+ import re
7
+ import struct
8
+ from dataclasses import replace
9
+ from typing import Any
10
+
11
+ from haystack import default_from_dict, default_to_dict, logging
12
+ from haystack.dataclasses import ByteStream, Document
13
+ from haystack.document_stores.errors import DocumentStoreError, DuplicateDocumentError
14
+ from haystack.document_stores.types import DuplicatePolicy
15
+ from haystack.utils.auth import Secret, deserialize_secrets_inplace
16
+
17
+ import mariadb
18
+
19
+ from .filters import _convert_filters_to_where_clause_and_params, _validate_filters
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _VALID_TABLE_NAME_RE = re.compile(r"^[A-Za-z0-9_]+$")
24
+
25
+ _VALID_DISTANCES = ("cosine", "euclidean")
26
+
27
+ _DISTANCE_TO_SQL = {
28
+ "cosine": "VEC_DISTANCE_COSINE",
29
+ "euclidean": "VEC_DISTANCE_EUCLIDEAN",
30
+ }
31
+
32
+ _CREATE_TABLE_STATEMENT = """
33
+ CREATE TABLE IF NOT EXISTS `{table_name}` (
34
+ id VARCHAR(128) PRIMARY KEY,
35
+ embedding VECTOR({embedding_dimension}),
36
+ content LONGTEXT,
37
+ blob_data LONGBLOB,
38
+ blob_meta JSON,
39
+ blob_mime_type VARCHAR(255),
40
+ meta JSON,
41
+ FULLTEXT KEY content_ft_idx (content)
42
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
43
+ """
44
+
45
+ _CREATE_TABLE_WITH_VECTOR_INDEX_STATEMENT = """
46
+ CREATE TABLE IF NOT EXISTS `{table_name}` (
47
+ id VARCHAR(128) PRIMARY KEY,
48
+ embedding VECTOR({embedding_dimension}) NOT NULL,
49
+ content LONGTEXT,
50
+ blob_data LONGBLOB,
51
+ blob_meta JSON,
52
+ blob_mime_type VARCHAR(255),
53
+ meta JSON,
54
+ FULLTEXT KEY content_ft_idx (content),
55
+ VECTOR INDEX (embedding) DISTANCE={distance}
56
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
57
+ """
58
+
59
+ _INSERT_STATEMENT = """
60
+ INSERT INTO `{table_name}`
61
+ (id, embedding, content, blob_data, blob_meta, blob_mime_type, meta)
62
+ VALUES (?, ?, ?, ?, ?, ?, ?)
63
+ """
64
+
65
+ _INSERT_IGNORE_STATEMENT = """
66
+ INSERT IGNORE INTO `{table_name}`
67
+ (id, embedding, content, blob_data, blob_meta, blob_mime_type, meta)
68
+ VALUES (?, ?, ?, ?, ?, ?, ?)
69
+ """
70
+
71
+ _UPSERT_STATEMENT = """
72
+ INSERT INTO `{table_name}`
73
+ (id, embedding, content, blob_data, blob_meta, blob_mime_type, meta)
74
+ VALUES (?, ?, ?, ?, ?, ?, ?)
75
+ ON DUPLICATE KEY UPDATE
76
+ embedding = VALUES(embedding),
77
+ content = VALUES(content),
78
+ blob_data = VALUES(blob_data),
79
+ blob_meta = VALUES(blob_meta),
80
+ blob_mime_type = VALUES(blob_mime_type),
81
+ meta = VALUES(meta)
82
+ """
83
+
84
+ _KEYWORD_QUERY = """
85
+ SELECT *, MATCH(content) AGAINST(? IN NATURAL LANGUAGE MODE) AS score
86
+ FROM `{table_name}`
87
+ {where_clause}
88
+ HAVING score > 0
89
+ ORDER BY score DESC
90
+ LIMIT ?
91
+ """
92
+
93
+ _EMBEDDING_QUERY = """
94
+ SELECT *, {vec_func}(embedding, ?) AS score
95
+ FROM `{table_name}`
96
+ WHERE embedding IS NOT NULL
97
+ {extra_where}
98
+ ORDER BY score ASC
99
+ LIMIT ?
100
+ """
101
+
102
+
103
+ class MariaDBDocumentStore:
104
+ """
105
+ A Document Store backed by MariaDB 11.7+ using native VECTOR support.
106
+
107
+ Uses MariaDB's `VECTOR` datatype with `MHNSW` indexing for approximate nearest-neighbour
108
+ vector search, and `MATCH ... AGAINST` for full-text keyword search.
109
+
110
+ ### Usage example
111
+
112
+ ```python
113
+ from haystack_integrations.document_stores.mariadb import MariaDBDocumentStore
114
+
115
+ store = MariaDBDocumentStore(
116
+ host="localhost",
117
+ port=3306,
118
+ database="haystack",
119
+ embedding_dimension=768,
120
+ )
121
+ store.write_documents(documents)
122
+ ```
123
+ """
124
+
125
+ def __init__(
126
+ self,
127
+ *,
128
+ host: str = "localhost",
129
+ port: int = 3306,
130
+ database: str = "haystack",
131
+ user: Secret = Secret.from_env_var("MARIADB_USER"),
132
+ password: Secret = Secret.from_env_var("MARIADB_PASSWORD"),
133
+ table_name: str = "haystack_documents",
134
+ recreate_table: bool = False,
135
+ embedding_dimension: int = 768,
136
+ distance: str = "cosine",
137
+ create_vector_index: bool = False,
138
+ ) -> None:
139
+ """
140
+ Initialize the MariaDBDocumentStore.
141
+
142
+ :param host: MariaDB host.
143
+ :param port: MariaDB port.
144
+ :param database: Database name.
145
+ :param user: Database user, read from the `MARIADB_USER` environment variable.
146
+ :param password: Database password, read from the `MARIADB_PASSWORD` environment variable.
147
+ :param table_name: Table used to store documents. Must contain only letters, digits, and underscores.
148
+ :param recreate_table: Drop and recreate the table on init. **Deletes all data.**
149
+ :param embedding_dimension: Dimension of embedding vectors. Applied only when the table is created;
150
+ ignored on an existing table.
151
+ :param distance: Distance function for vector similarity — `"cosine"` or `"euclidean"`. Applied only when
152
+ the table is created; ignored on an existing table.
153
+ :param create_vector_index: If `True`, creates an MHNSW vector index for fast ANN search. Requires every
154
+ document to have a non-null embedding. Applied only when the table is created; ignored on an existing
155
+ table.
156
+ """
157
+ self._connection: Any = None
158
+ self._cursor: Any = None
159
+ self._table_initialized = False
160
+
161
+ if not _VALID_TABLE_NAME_RE.match(table_name):
162
+ msg = f"table_name must contain only letters, digits, and underscores, got '{table_name}'"
163
+ raise ValueError(msg)
164
+ if distance not in _VALID_DISTANCES:
165
+ msg = f"distance must be one of {_VALID_DISTANCES}, got '{distance}'"
166
+ raise ValueError(msg)
167
+
168
+ self.host = host
169
+ self.port = port
170
+ self.database = database
171
+ self.user = user
172
+ self.password = password
173
+ self.table_name = table_name
174
+ self.embedding_dimension = embedding_dimension
175
+ self.distance = distance
176
+ self.recreate_table = recreate_table
177
+ self.create_vector_index = create_vector_index
178
+
179
+ def to_dict(self) -> dict[str, Any]:
180
+ """
181
+ Serialize this document store to a dictionary.
182
+
183
+ :returns: Dictionary with serialized data.
184
+ """
185
+ return default_to_dict(
186
+ self,
187
+ host=self.host,
188
+ port=self.port,
189
+ database=self.database,
190
+ user=self.user.to_dict(),
191
+ password=self.password.to_dict(),
192
+ table_name=self.table_name,
193
+ embedding_dimension=self.embedding_dimension,
194
+ distance=self.distance,
195
+ recreate_table=self.recreate_table,
196
+ create_vector_index=self.create_vector_index,
197
+ )
198
+
199
+ @classmethod
200
+ def from_dict(cls, data: dict[str, Any]) -> "MariaDBDocumentStore":
201
+ """
202
+ Deserialize this document store from a dictionary.
203
+
204
+ :param data: Dictionary to deserialize from.
205
+ :returns: Deserialized document store.
206
+ """
207
+ deserialize_secrets_inplace(data["init_parameters"], ["user", "password"])
208
+ return default_from_dict(cls, data)
209
+
210
+ def _ensure_connection(self) -> None:
211
+ """Lazily establish the DB connection and initialize the table."""
212
+ if self._connection is not None and self._cursor is not None:
213
+ try:
214
+ self._connection.ping()
215
+ return
216
+ except Exception:
217
+ self._close_connection()
218
+
219
+ try:
220
+ self._connection = mariadb.connect(
221
+ host=self.host,
222
+ port=self.port,
223
+ database=self.database,
224
+ user=self.user.resolve_value() or "",
225
+ password=self.password.resolve_value() or "",
226
+ autocommit=True,
227
+ )
228
+ except mariadb.Error as e:
229
+ msg = (
230
+ f"Failed to connect to MariaDB at {self.host}:{self.port}/{self.database}. "
231
+ "Ensure MariaDB 11.7+ is running and credentials are correct."
232
+ )
233
+ raise DocumentStoreError(msg) from e
234
+
235
+ self._cursor = self._connection.cursor(dictionary=True)
236
+
237
+ if not self._table_initialized:
238
+ self._initialize_table()
239
+
240
+ def _initialize_table(self) -> None:
241
+ if self.recreate_table:
242
+ self._drop_table()
243
+
244
+ if self.create_vector_index:
245
+ sql = _CREATE_TABLE_WITH_VECTOR_INDEX_STATEMENT.format(
246
+ table_name=self.table_name,
247
+ embedding_dimension=self.embedding_dimension,
248
+ distance=self.distance,
249
+ )
250
+ else:
251
+ sql = _CREATE_TABLE_STATEMENT.format(
252
+ table_name=self.table_name,
253
+ embedding_dimension=self.embedding_dimension,
254
+ )
255
+ try:
256
+ self._cursor.execute(sql)
257
+ self._table_initialized = True
258
+ except mariadb.Error as e:
259
+ msg = f"Could not create table '{self.table_name}'"
260
+ raise DocumentStoreError(msg) from e
261
+
262
+ def _drop_table(self) -> None:
263
+ try:
264
+ self._cursor.execute(f"DROP TABLE IF EXISTS `{self.table_name}`")
265
+ self._table_initialized = False
266
+ except mariadb.Error as e:
267
+ msg = f"Could not drop table '{self.table_name}'"
268
+ raise DocumentStoreError(msg) from e
269
+
270
+ def _close_connection(self) -> None:
271
+ if self._cursor is not None:
272
+ try:
273
+ self._cursor.close()
274
+ except Exception: # noqa: S110
275
+ pass
276
+ self._cursor = None
277
+ if self._connection is not None:
278
+ try:
279
+ self._connection.close()
280
+ except Exception: # noqa: S110
281
+ pass
282
+ self._connection = None
283
+ self._table_initialized = False
284
+
285
+ def close(self) -> None:
286
+ """
287
+ Release the associated synchronous resources.
288
+ """
289
+ self._close_connection()
290
+
291
+ def delete_table(self) -> None:
292
+ """Drop the documents table"""
293
+ self._ensure_connection()
294
+ self._drop_table()
295
+
296
+ def count_documents(self) -> int:
297
+ """
298
+ Return how many documents are present in the document store.
299
+
300
+ :returns: Number of documents in the document store.
301
+ """
302
+ self._ensure_connection()
303
+ self._cursor.execute(f"SELECT COUNT(*) AS cnt FROM `{self.table_name}`") # noqa: S608
304
+ row = self._cursor.fetchone()
305
+ return row["cnt"] if row else 0
306
+
307
+ def filter_documents(self, filters: dict[str, Any] | None = None) -> list[Document]:
308
+ """
309
+ Return the documents that match the filters provided.
310
+
311
+ For a detailed specification of the filters,
312
+ refer to the [documentation](https://docs.haystack.deepset.ai/docs/metadata-filtering).
313
+
314
+ :param filters: The filters to apply to the document list.
315
+ :raises TypeError: If `filters` is not a dictionary.
316
+ :raises ValueError: If `filters` syntax is invalid.
317
+ :returns: A list of Documents that match the given filters.
318
+ """
319
+ _validate_filters(filters)
320
+ self._ensure_connection()
321
+
322
+ sql = f"SELECT * FROM `{self.table_name}`" # noqa: S608
323
+ params: list[Any] = []
324
+
325
+ if filters:
326
+ where_clause, params = _convert_filters_to_where_clause_and_params(filters)
327
+ sql += where_clause
328
+
329
+ self._cursor.execute(sql, params)
330
+ records = self._cursor.fetchall()
331
+ return _rows_to_documents(records)
332
+
333
+ def write_documents(self, documents: list[Document], policy: DuplicatePolicy = DuplicatePolicy.NONE) -> int:
334
+ """
335
+ Write documents to the store.
336
+
337
+ :param documents: A list of Documents to write to the document store.
338
+ :param policy: The duplicate policy to use when writing documents.
339
+ :raises ValueError: If `documents` contains objects that are not of type `Document`.
340
+ :raises DuplicateDocumentError: If a document with the same id already exists in the document store
341
+ and the policy is set to `DuplicatePolicy.FAIL` (or not specified).
342
+ :raises DocumentStoreError: If the write operation fails for any other reason.
343
+ :returns: The number of documents written to the document store.
344
+ """
345
+ if not documents:
346
+ return 0
347
+ if not isinstance(documents[0], Document):
348
+ msg = "param 'documents' must be a list of Document objects"
349
+ raise ValueError(msg)
350
+
351
+ if policy == DuplicatePolicy.NONE:
352
+ policy = DuplicatePolicy.FAIL
353
+
354
+ self._ensure_connection()
355
+
356
+ if policy == DuplicatePolicy.OVERWRITE:
357
+ sql = _UPSERT_STATEMENT.format(table_name=self.table_name)
358
+ elif policy == DuplicatePolicy.SKIP:
359
+ sql = _INSERT_IGNORE_STATEMENT.format(table_name=self.table_name)
360
+ else:
361
+ sql = _INSERT_STATEMENT.format(table_name=self.table_name)
362
+
363
+ rows = [_document_to_row(doc) for doc in documents]
364
+ try:
365
+ self._connection.begin()
366
+ self._cursor.executemany(sql, rows)
367
+ # ON DUPLICATE KEY UPDATE counts updated rows as 2, inserted as 1 in MariaDB.
368
+ # For OVERWRITE all documents are written regardless, so return len(rows).
369
+ written = len(rows) if policy == DuplicatePolicy.OVERWRITE else self._cursor.rowcount
370
+ self._connection.commit()
371
+ except mariadb.IntegrityError as e:
372
+ self._connection.rollback()
373
+ msg = "Some documents already exist and policy is FAIL"
374
+ raise DuplicateDocumentError(msg) from e
375
+ except mariadb.Error as e:
376
+ self._connection.rollback()
377
+ msg = "Failed to write documents"
378
+ raise DocumentStoreError(msg) from e
379
+
380
+ return written
381
+
382
+ def delete_documents(self, document_ids: list[str]) -> None:
383
+ """
384
+ Delete documents that match the provided `document_ids` from the document store.
385
+
386
+ :param document_ids: The document ids to delete.
387
+ """
388
+ if not document_ids:
389
+ return
390
+ self._ensure_connection()
391
+ placeholders = ", ".join(["?"] * len(document_ids))
392
+ try:
393
+ self._cursor.execute(
394
+ f"DELETE FROM `{self.table_name}` WHERE id IN ({placeholders})", # noqa: S608
395
+ tuple(document_ids),
396
+ )
397
+ except mariadb.Error as e:
398
+ msg = "Failed to delete documents"
399
+ raise DocumentStoreError(msg) from e
400
+
401
+ def _embedding_retrieval(
402
+ self,
403
+ query_embedding: list[float],
404
+ *,
405
+ filters: dict[str, Any] | None = None,
406
+ top_k: int = 10,
407
+ score_threshold: float | None = None,
408
+ ) -> list[Document]:
409
+ """
410
+ Retrieve documents by vector similarity.
411
+
412
+ :param query_embedding: Query vector.
413
+ :param filters: Optional Haystack filters.
414
+ :param top_k: Maximum results.
415
+ :param score_threshold: Minimum score to include a document. Documents below this score are excluded.
416
+ :returns: List of Documents ordered by similarity (most similar first).
417
+ """
418
+ if len(query_embedding) != self.embedding_dimension:
419
+ msg = (
420
+ f"query_embedding has {len(query_embedding)} dimensions but the store "
421
+ f"was configured with embedding_dimension={self.embedding_dimension}"
422
+ )
423
+ raise ValueError(msg)
424
+
425
+ _validate_filters(filters)
426
+ self._ensure_connection()
427
+
428
+ vec_func = _DISTANCE_TO_SQL[self.distance]
429
+ embedding_bytes = _embedding_to_bytes(query_embedding)
430
+
431
+ extra_where = ""
432
+ params: list[Any] = [embedding_bytes]
433
+
434
+ if filters:
435
+ where_clause, filter_params = _convert_filters_to_where_clause_and_params(filters, operator="AND")
436
+ extra_where = where_clause
437
+ params.extend(filter_params)
438
+
439
+ params.append(top_k)
440
+
441
+ sql = _EMBEDDING_QUERY.format(
442
+ vec_func=vec_func,
443
+ table_name=self.table_name,
444
+ extra_where=extra_where,
445
+ )
446
+
447
+ self._cursor.execute(sql, params)
448
+ records = self._cursor.fetchall()
449
+
450
+ docs = _rows_to_documents(records)
451
+ # VEC_DISTANCE_* returns distance (lower = more similar); convert to a positive score
452
+ docs = [
453
+ replace(doc, score=float(1.0 - record["score"]) if self.distance == "cosine" else float(-record["score"]))
454
+ if record.get("score") is not None
455
+ else doc
456
+ for doc, record in zip(docs, records, strict=True)
457
+ ]
458
+ if score_threshold is not None:
459
+ docs = [doc for doc in docs if doc.score is not None and doc.score >= score_threshold]
460
+ return docs
461
+
462
+ def _keyword_retrieval(
463
+ self,
464
+ query: str,
465
+ *,
466
+ filters: dict[str, Any] | None = None,
467
+ top_k: int = 10,
468
+ ) -> list[Document]:
469
+ """
470
+ Retrieve documents by full-text keyword search.
471
+
472
+ :param query: Search query string.
473
+ :param filters: Optional Haystack filters.
474
+ :param top_k: Maximum results.
475
+ :returns: List of Documents ordered by relevance.
476
+ """
477
+ _validate_filters(filters)
478
+ self._ensure_connection()
479
+
480
+ params: list[Any] = [query]
481
+ where_clause = ""
482
+
483
+ if filters:
484
+ where_clause, filter_params = _convert_filters_to_where_clause_and_params(filters)
485
+ params = [query, *filter_params]
486
+
487
+ params.append(top_k)
488
+
489
+ sql = _KEYWORD_QUERY.format(table_name=self.table_name, where_clause=where_clause)
490
+ self._cursor.execute(sql, params)
491
+ records = self._cursor.fetchall()
492
+
493
+ docs = _rows_to_documents(records)
494
+ docs = [
495
+ replace(doc, score=float(record.get("score") or 0.0)) for doc, record in zip(docs, records, strict=True)
496
+ ]
497
+ return docs
498
+
499
+
500
+ def _embedding_to_bytes(embedding: list[float]) -> bytes:
501
+ return struct.pack(f"{len(embedding)}f", *embedding)
502
+
503
+
504
+ def _bytes_to_embedding(data: bytes | bytearray) -> list[float]:
505
+ n = len(data) // 4
506
+ return list(struct.unpack(f"{n}f", data))
507
+
508
+
509
+ def _document_to_row(doc: Document) -> tuple:
510
+ embedding_bytes = _embedding_to_bytes(doc.embedding) if doc.embedding is not None else None
511
+ blob_data = doc.blob.data if doc.blob else None
512
+ blob_meta = json.dumps(doc.blob.meta) if doc.blob and doc.blob.meta else None
513
+ blob_mime_type = doc.blob.mime_type if doc.blob else None
514
+ meta = json.dumps(doc.meta) if doc.meta else json.dumps({})
515
+ return (doc.id, embedding_bytes, doc.content, blob_data, blob_meta, blob_mime_type, meta)
516
+
517
+
518
+ def _rows_to_documents(records: list[dict[str, Any]]) -> list[Document]:
519
+ docs = []
520
+ for record in records:
521
+ row = dict(record)
522
+ row.pop("score", None)
523
+
524
+ blob_data = row.pop("blob_data", None)
525
+ blob_meta_raw = row.pop("blob_meta", None)
526
+ blob_mime_type = row.pop("blob_mime_type", None)
527
+
528
+ if isinstance(row.get("meta"), str):
529
+ row["meta"] = json.loads(row["meta"])
530
+ elif row.get("meta") is None:
531
+ row.pop("meta", None)
532
+
533
+ emb = row.get("embedding")
534
+ if emb is not None:
535
+ if hasattr(emb, "tolist"):
536
+ row["embedding"] = emb.tolist()
537
+ elif isinstance(emb, (bytes, bytearray)):
538
+ row["embedding"] = _bytes_to_embedding(emb)
539
+
540
+ doc = Document.from_dict(row)
541
+
542
+ if blob_data:
543
+ if isinstance(blob_meta_raw, str):
544
+ blob_meta_raw = json.loads(blob_meta_raw)
545
+ doc = replace(doc, blob=ByteStream(data=blob_data, meta=blob_meta_raw or {}, mime_type=blob_mime_type))
546
+
547
+ docs.append(doc)
548
+ return docs
@@ -0,0 +1,207 @@
1
+ # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import re
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+ from haystack.errors import FilterError
10
+
11
+ NO_VALUE = "no_value"
12
+
13
+
14
+ def _validate_filters(filters: dict[str, Any] | None = None) -> None:
15
+ if filters:
16
+ if not isinstance(filters, dict):
17
+ msg = "Filters must be a dictionary"
18
+ raise TypeError(msg)
19
+ if "operator" not in filters and "conditions" not in filters:
20
+ msg = "Invalid filter syntax. See https://docs.haystack.deepset.ai/docs/metadata-filtering for details."
21
+ raise ValueError(msg)
22
+
23
+
24
+ def _convert_filters_to_where_clause_and_params(
25
+ filters: dict[str, Any],
26
+ operator: str = "WHERE",
27
+ ) -> tuple[str, list[Any]]:
28
+ """Convert Haystack filters to a MariaDB WHERE clause string and parameter list."""
29
+ if "field" in filters:
30
+ clause, values = _parse_comparison_condition(filters)
31
+ else:
32
+ clause, values = _parse_logical_condition(filters)
33
+
34
+ params = [v for v in values if v != NO_VALUE]
35
+ return f" {operator} {clause}", params
36
+
37
+
38
+ def _parse_logical_condition(condition: dict[str, Any]) -> tuple[str, list[Any]]:
39
+ if "operator" not in condition:
40
+ msg = f"'operator' key missing in {condition}"
41
+ raise FilterError(msg)
42
+ if "conditions" not in condition:
43
+ msg = f"'conditions' key missing in {condition}"
44
+ raise FilterError(msg)
45
+
46
+ operator = condition["operator"]
47
+ if operator not in ("AND", "OR", "NOT"):
48
+ msg = f"Unknown logical operator '{operator}'. Valid operators are: 'AND', 'OR', 'NOT'"
49
+ raise FilterError(msg)
50
+
51
+ parts, all_params = [], []
52
+ for c in condition["conditions"]:
53
+ if "field" in c:
54
+ clause, vals = _parse_comparison_condition(c)
55
+ else:
56
+ clause, vals = _parse_logical_condition(c)
57
+ parts.append(clause)
58
+ all_params.extend(vals)
59
+
60
+ if operator == "NOT":
61
+ inner = parts[0] if len(parts) == 1 else "(" + " AND ".join(parts) + ")"
62
+ return f"COALESCE(NOT {inner}, TRUE)", all_params
63
+
64
+ joined = f" {operator} ".join(parts)
65
+ return f"({joined})", all_params
66
+
67
+
68
+ def _parse_comparison_condition(condition: dict[str, Any]) -> tuple[str, list[Any]]:
69
+ field: str = condition["field"]
70
+ if "operator" not in condition:
71
+ msg = f"'operator' key missing in {condition}"
72
+ raise FilterError(msg)
73
+ if "value" not in condition:
74
+ msg = f"'value' key missing in {condition}"
75
+ raise FilterError(msg)
76
+
77
+ operator: str = condition["operator"]
78
+ if operator not in COMPARISON_OPERATORS:
79
+ msg = f"Unknown comparison operator '{operator}'. Valid operators are: {list(COMPARISON_OPERATORS.keys())}"
80
+ raise FilterError(msg)
81
+
82
+ value: Any = condition["value"]
83
+ # MariaDB stores JSON booleans as the literals 'true'/'false'; convert before comparison.
84
+ if isinstance(value, bool):
85
+ value = "true" if value else "false"
86
+ sql_field = _build_field_expr(field, value)
87
+ clause, val = COMPARISON_OPERATORS[operator](sql_field, value)
88
+ # _in/_not_in return the value list directly; flatten it so params stay flat
89
+ if isinstance(val, list):
90
+ return clause, val
91
+ return clause, [val]
92
+
93
+
94
+ _SAFE_FIELD_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.]*$")
95
+
96
+
97
+ def _build_field_expr(field: str, value: Any) -> str:
98
+ """Return a safe SQL expression for a document field or meta JSON path."""
99
+ if field.startswith("meta."):
100
+ field_name = field.split(".", 1)[1]
101
+ if not _SAFE_FIELD_RE.match(field_name):
102
+ msg = f"Invalid meta field name: '{field_name}'"
103
+ raise FilterError(msg)
104
+ # JSON_UNQUOTE(JSON_EXTRACT(meta, '$.field')) returns the value as a string.
105
+ # Cast to numeric types when the comparison value is numeric.
106
+ base = f"JSON_UNQUOTE(JSON_EXTRACT(meta, '$.{field_name}'))"
107
+ if isinstance(value, int):
108
+ return f"CAST({base} AS SIGNED)"
109
+ if isinstance(value, float):
110
+ return f"CAST({base} AS DECIMAL(65,30))"
111
+ if isinstance(value, list) and value:
112
+ first = value[0]
113
+ if isinstance(first, int) and not isinstance(first, bool):
114
+ return f"CAST({base} AS SIGNED)"
115
+ if isinstance(first, float):
116
+ return f"CAST({base} AS DECIMAL(65,30))"
117
+ return base
118
+ # Top-level document field (id, content, etc.) — backtick-quoted
119
+ return f"`{field}`"
120
+
121
+
122
+ def _equal(field: str, value: Any) -> tuple[str, Any]:
123
+ if value is None:
124
+ return f"{field} IS NULL", NO_VALUE
125
+ return f"{field} = ?", value
126
+
127
+
128
+ def _not_equal(field: str, value: Any) -> tuple[str, Any]:
129
+ if value is None:
130
+ return f"{field} IS NOT NULL", NO_VALUE
131
+ return f"({field} IS NULL OR {field} != ?)", value
132
+
133
+
134
+ def _greater_than(field: str, value: Any) -> tuple[str, Any]:
135
+ _check_comparable(value, ">")
136
+ return f"{field} > ?", value
137
+
138
+
139
+ def _greater_than_equal(field: str, value: Any) -> tuple[str, Any]:
140
+ _check_comparable(value, ">=")
141
+ return f"{field} >= ?", value
142
+
143
+
144
+ def _less_than(field: str, value: Any) -> tuple[str, Any]:
145
+ _check_comparable(value, "<")
146
+ return f"{field} < ?", value
147
+
148
+
149
+ def _less_than_equal(field: str, value: Any) -> tuple[str, Any]:
150
+ _check_comparable(value, "<=")
151
+ return f"{field} <= ?", value
152
+
153
+
154
+ def _in(field: str, value: Any) -> tuple[str, list]:
155
+ if not isinstance(value, list):
156
+ msg = f"'in' operator requires a list value, got {type(value)}"
157
+ raise FilterError(msg)
158
+ placeholders = ", ".join(["?"] * len(value))
159
+ return f"{field} IN ({placeholders})", value
160
+
161
+
162
+ def _not_in(field: str, value: Any) -> tuple[str, list]:
163
+ if not isinstance(value, list):
164
+ msg = f"'not in' operator requires a list value, got {type(value)}"
165
+ raise FilterError(msg)
166
+ placeholders = ", ".join(["?"] * len(value))
167
+ return f"({field} IS NULL OR {field} NOT IN ({placeholders}))", value
168
+
169
+
170
+ def _like(field: str, value: Any) -> tuple[str, Any]:
171
+ if not isinstance(value, str):
172
+ msg = f"'like' operator requires a string value, got {type(value)}"
173
+ raise FilterError(msg)
174
+ return f"{field} LIKE ?", value
175
+
176
+
177
+ def _not_like(field: str, value: Any) -> tuple[str, Any]:
178
+ if not isinstance(value, str):
179
+ msg = f"'not like' operator requires a string value, got {type(value)}"
180
+ raise FilterError(msg)
181
+ return f"{field} NOT LIKE ?", value
182
+
183
+
184
+ def _check_comparable(value: Any, op: str) -> None:
185
+ if isinstance(value, str):
186
+ try:
187
+ datetime.fromisoformat(value)
188
+ except (ValueError, TypeError) as exc:
189
+ msg = f"Cannot compare strings with '{op}'. Strings are only comparable if they are ISO formatted dates."
190
+ raise FilterError(msg) from exc
191
+ if isinstance(value, list):
192
+ msg = f"Filter value cannot be a list when using '{op}'"
193
+ raise FilterError(msg)
194
+
195
+
196
+ COMPARISON_OPERATORS = {
197
+ "==": _equal,
198
+ "!=": _not_equal,
199
+ ">": _greater_than,
200
+ ">=": _greater_than_equal,
201
+ "<": _less_than,
202
+ "<=": _less_than_equal,
203
+ "in": _in,
204
+ "not in": _not_in,
205
+ "like": _like,
206
+ "not like": _not_like,
207
+ }
File without changes
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.5
2
+ Name: mariadb-haystack
3
+ Version: 0.0.1
4
+ Summary: An integration of MariaDB 11.7+ (native VECTOR support) with Haystack
5
+ Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations
6
+ Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/mariadb/README.md
7
+ Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
8
+ Author-email: deepset GmbH <info@deepset.ai>
9
+ License-Expression: Apache-2.0
10
+ License-File: LICENSE.txt
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: Implementation :: CPython
20
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: haystack-ai>=2.28.0
23
+ Requires-Dist: mariadb>=1.1.6
24
+ Description-Content-Type: text/markdown
25
+
26
+ # mariadb-haystack
27
+
28
+ [![PyPI - Version](https://img.shields.io/pypi/v/mariadb-haystack.svg)](https://pypi.org/project/mariadb-haystack)
29
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mariadb-haystack.svg)](https://pypi.org/project/mariadb-haystack)
30
+
31
+ - [Integration page](https://haystack.deepset.ai/integrations/mariadb-documentstore)
32
+ - [Changelog](https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/mariadb/CHANGELOG.md)
33
+
34
+ ---
35
+
36
+ ## Contributing
37
+
38
+ Refer to the general [Contribution Guidelines](https://github.com/deepset-ai/haystack-core-integrations/blob/main/CONTRIBUTING.md).
39
+
40
+ The `mariadb` connector is a C extension built from source, so it needs the MariaDB Connector/C system library
41
+ (`mariadb_config`): `sudo apt-get install -y libmariadb-dev` on Ubuntu, or `brew install mariadb-connector-c`
42
+ on macOS.
43
+
44
+ To run integration tests locally, you need a MariaDB 11.7+ instance running.
45
+ You can start one using Docker:
46
+
47
+ ```console
48
+ docker run -d --name mariadb-haystack -e MARIADB_ROOT_PASSWORD=password -e MARIADB_DATABASE=haystack -p 3306:3306 mariadb:11.7
49
+ ```
@@ -0,0 +1,12 @@
1
+ haystack_integrations/components/retrievers/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ haystack_integrations/components/retrievers/mariadb/__init__.py,sha256=-2LFLFyCYQx1MPNLJbGCZkRRZchy6eL6yN-ylEE1RiA,395
3
+ haystack_integrations/components/retrievers/mariadb/embedding_retriever.py,sha256=LrcQRJrefh5Skz8XqnRMJwkmMQVt3x7Ivf4wXxa1m_c,4644
4
+ haystack_integrations/components/retrievers/mariadb/keyword_retriever.py,sha256=bT3yCOukh1sRBqYO-Mjecm6ynEqsXz8EuXT_0VxRn_0,4077
5
+ haystack_integrations/document_stores/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ haystack_integrations/document_stores/mariadb/__init__.py,sha256=cWSChV85fsrz3lxqycd_tkYB3l81V0YA7CFcmccASCM,241
7
+ haystack_integrations/document_stores/mariadb/document_store.py,sha256=YADg_PzTb9OELiDzMXXtD27nHv8HeegoFRteJYyYkh8,19439
8
+ haystack_integrations/document_stores/mariadb/filters.py,sha256=7peSD0_CHQ9vgEdX4pJrbjuz5YjPfCtvAF7kUZghGz4,7227
9
+ mariadb_haystack-0.0.1.dist-info/METADATA,sha256=bOf9eTRcOFFfdZeiFcyVc8Ruvqm5a9EDwe2vK7u4JqE,2313
10
+ mariadb_haystack-0.0.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
11
+ mariadb_haystack-0.0.1.dist-info/licenses/LICENSE.txt,sha256=HutsHrNflRknTSqX9PFpbYgHFaF4cNSB-xYFWp65Zls,10280
12
+ mariadb_haystack-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,73 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
10
+
11
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
12
+
13
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
14
+
15
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
16
+
17
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
18
+
19
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
20
+
21
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
22
+
23
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
24
+
25
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
26
+
27
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
28
+
29
+ 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
30
+
31
+ 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent passes licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
32
+
33
+ 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
34
+
35
+ (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
36
+
37
+ (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
38
+
39
+ (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
40
+
41
+ (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
42
+
43
+ You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
44
+
45
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
46
+
47
+ 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
48
+
49
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
50
+
51
+ 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
52
+
53
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
54
+
55
+ END OF TERMS AND CONDITIONS
56
+
57
+ APPENDIX: How to apply the Apache License to your work.
58
+
59
+ To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
60
+
61
+ Copyright [yyyy] [name of copyright owner]
62
+
63
+ Licensed under the Apache License, Version 2.0 (the "License");
64
+ you may not use this file except in compliance with the License.
65
+ You may obtain a copy of the License at
66
+
67
+ http://www.apache.org/licenses/LICENSE-2.0
68
+
69
+ Unless required by applicable law or agreed to in writing, software
70
+ distributed under the License is distributed on an "AS IS" BASIS,
71
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
72
+ See the License for the specific language governing permissions and
73
+ limitations under the License.