Apache Solr to OpenSearch Migration Advisor An agent skill for migrating from Apache Solr to OpenSearch. This skill provides a transport-agnostic migration advisor that can reason about Solr query behavior, configuration, and cluster architecture. When to Use Use this skill when: - A user needs to migrate a Solr collection or SolrCloud deployment to OpenSearch. - A user wants a comprehensive migration advisor that can handle conversational interaction and maintain session context. - A user has a or Solr Schema API JSON document and needs an equivalent OpenSearch index mapping. - A user has So…

, re.DOTALL\n)\n_PHRASE_RE = re.compile(r'^\"(?P\u003cphrase>[^\"]+)\"

Apache Solr to OpenSearch Migration Advisor An agent skill for migrating from Apache Solr to OpenSearch. This skill provides a transport-agnostic migration advisor that can reason about Solr query behavior, configuration, and cluster architecture. When to Use Use this skill when: - A user needs to migrate a Solr collection or SolrCloud deployment to OpenSearch. - A user wants a comprehensive migration advisor that can handle conversational interaction and maintain session context. - A user has a or Solr Schema API JSON document and needs an equivalent OpenSearch index mapping. - A user has So…

)\n_WILDCARD_RE = re.compile(r'[*?]')\n_RANGE_RE = re.compile(\n r'^(?P\u003copen>[\\[{])\\s*(?P\u003clow>\\S+)\\s+TO\\s+(?P\u003chigh>\\S+)\\s*(?P\u003cclose>[\\]}])

Apache Solr to OpenSearch Migration Advisor An agent skill for migrating from Apache Solr to OpenSearch. This skill provides a transport-agnostic migration advisor that can reason about Solr query behavior, configuration, and cluster architecture. When to Use Use this skill when: - A user needs to migrate a Solr collection or SolrCloud deployment to OpenSearch. - A user wants a comprehensive migration advisor that can handle conversational interaction and maintain session context. - A user has a or Solr Schema API JSON document and needs an equivalent OpenSearch index mapping. - A user has So…

\n)\n_BOOST_RE = re.compile(r'\\^[\\d.]+

Apache Solr to OpenSearch Migration Advisor An agent skill for migrating from Apache Solr to OpenSearch. This skill provides a transport-agnostic migration advisor that can reason about Solr query behavior, configuration, and cluster architecture. When to Use Use this skill when: - A user needs to migrate a Solr collection or SolrCloud deployment to OpenSearch. - A user wants a comprehensive migration advisor that can handle conversational interaction and maintain session context. - A user has a or Solr Schema API JSON document and needs an equivalent OpenSearch index mapping. - A user has So…

)\n\n\ndef _strip_boost(value: str) -> str:\n return _BOOST_RE.sub('', value).strip()\n\n\ndef _build_term_query(field: str, raw_value: str) -> dict[str, Any]:\n \"\"\"Build a single field→value query clause.\"\"\"\n value = _strip_boost(raw_value)\n\n # Phrase\n phrase_match = _PHRASE_RE.match(value)\n if phrase_match:\n return {\"match_phrase\": {field: phrase_match.group(\"phrase\")}}\n\n # Range\n range_match = _RANGE_RE.match(value)\n if range_match:\n low = range_match.group(\"low\")\n high = range_match.group(\"high\")\n inclusive_low = range_match.group(\"open\") == \"[\"\n inclusive_high = range_match.group(\"close\") == \"]\"\n\n range_clause: dict[str, Any] = {}\n if low != \"*\":\n range_clause[\"gte\" if inclusive_low else \"gt\"] = _coerce_number(low)\n if high != \"*\":\n range_clause[\"lte\" if inclusive_high else \"lt\"] = _coerce_number(high)\n\n return {\"range\": {field: range_clause}}\n\n # Wildcard\n if _WILDCARD_RE.search(value):\n return {\"wildcard\": {field: value.lower()}}\n\n # Plain term → match\n return {\"match\": {field: value}}\n\n\ndef _coerce_number(value: str) -> int | float | str:\n \"\"\"Try to return a numeric type; fall back to string.\"\"\"\n try:\n return int(value)\n except ValueError:\n pass\n try:\n return float(value)\n except ValueError:\n pass\n return value\n\n\n# ---------------------------------------------------------------------------\n# Boolean splitting helpers\n# ---------------------------------------------------------------------------\n\ndef _split_boolean(query: str) -> tuple[str, list[str]] | None:\n \"\"\"Split a query on a top-level AND or OR operator.\n\n Returns ``(operator, [parts])`` or ``None`` if no top-level operator is\n found. Only splits on AND/OR that are not inside parentheses or quotes.\n \"\"\"\n depth = 0\n in_quote = False\n\n for i, ch in enumerate(query):\n if ch == '\"':\n in_quote = not in_quote\n continue\n if in_quote:\n continue\n\n if ch == '(':\n depth += 1\n elif ch == ')':\n depth -= 1\n elif depth == 0:\n found = _find_op_at_index(query, i)\n if found:\n return found\n\n return None\n\n\ndef _find_op_at_index(query: str, i: int) -> tuple[str, list[str]] | None:\n \"\"\"Check for AND/OR at the given index and return the operator and parts.\"\"\"\n for op in (' AND ', ' OR '):\n if query[i:].upper().startswith(op):\n left = query[:i].strip()\n right = query[i + len(op):].strip()\n return op.strip(), [left, right]\n return None\n\n\ndef _build_must_not(inner_query: str) -> dict[str, Any]:\n \"\"\"Wrap a query string in a must_not boolean.\"\"\"\n inner = _convert_simple(inner_query)\n return {\"bool\": {\"must_not\": [inner]}}\n\n\ndef _convert_simple(query: str) -> dict[str, Any]:\n \"\"\"Convert a single (non-compound) Solr query clause to OpenSearch DSL.\"\"\"\n query = query.strip()\n\n # match_all\n if query in (\"*:*\", \"*\"):\n return {\"match_all\": {}}\n\n # NOT prefix\n if query.upper().startswith(\"NOT \"):\n return _build_must_not(query[4:].strip())\n\n # +/- prefix (required / prohibited)\n if query.startswith(\"+\") or query.startswith(\"-\"):\n return _handle_prefixed_query(query)\n\n # field:value\n fv_match = _FIELD_VALUE_RE.match(query)\n if fv_match:\n field = fv_match.group(\"field\")\n value = fv_match.group(\"value\")\n return _build_term_query(field, value)\n\n # Bare term with no field — use query_string\n return {\"query_string\": {\"query\": query}}\n\n\ndef _handle_prefixed_query(query: str) -> dict[str, Any]:\n \"\"\"Handle queries starting with + or -.\"\"\"\n must: list[dict[str, Any]] = []\n must_not: list[dict[str, Any]] = []\n # Split on space-separated +/- tokens\n tokens = _tokenize_prefixed(query)\n for sign, tok in tokens:\n clause = _convert_simple(tok)\n if sign == \"+\":\n must.append(clause)\n else:\n must_not.append(clause)\n\n bool_query: dict[str, Any] = {}\n if must:\n bool_query[\"must\"] = must\n if must_not:\n bool_query[\"must_not\"] = must_not\n return {\"bool\": bool_query}\n\n\ndef _tokenize_prefixed(query: str) -> list[tuple[str, str]]:\n \"\"\"Break a ``+a -b +c`` style query into ``[(sign, term), …]``.\"\"\"\n tokens: list[tuple[str, str]] = []\n # Split respecting quoted strings\n parts = re.findall(r'[+-](?:\"[^\"]*\"|\\S+)', query)\n for part in parts:\n sign = \"+\" if part[0] == \"+\" else \"-\"\n tokens.append((sign, part[1:].strip()))\n return tokens\n\n\nclass QueryConverter:\n \"\"\"Converts Solr query strings to OpenSearch Query DSL dicts.\n\n Usage::\n\n converter = QueryConverter()\n\n # Simple field query\n dsl = converter.convert(\"title:opensearch\")\n\n # Range query\n dsl = converter.convert(\"price:[10 TO 100]\")\n\n # Boolean query\n dsl = converter.convert(\"title:search AND category:docs\")\n\n print(json.dumps(dsl, indent=2))\n \"\"\"\n\n def convert(self, solr_query: str) -> dict[str, Any]:\n \"\"\"Convert a Solr query string to an OpenSearch Query DSL dict.\n\n The returned dict is the full ``query`` object, i.e. it can be used\n directly as the value of the ``\"query\"`` key in an OpenSearch search\n request body.\n\n Args:\n solr_query: A Solr query string (``q`` parameter value).\n\n Returns:\n An OpenSearch Query DSL dict.\n\n Raises:\n ValueError: If ``solr_query`` is empty.\n \"\"\"\n if not solr_query or not solr_query.strip():\n raise ValueError(\"solr_query must not be empty\")\n\n query = solr_query.strip()\n\n # Remove wrapping parentheses if they span the whole expression.\n query = _unwrap_parens(query)\n\n # Top-level AND/OR\n result = _split_boolean(query)\n if result:\n operator, parts = result\n return self._handle_boolean_operator(operator, parts)\n\n return {\"query\": _convert_simple(query)}\n\n def _handle_boolean_operator(self, operator: str, parts: list[str]) -> dict[str, Any]:\n \"\"\"Handle boolean AND/OR operators by building the appropriate bool query.\"\"\"\n clauses = [self.convert(p)[\"query\"] for p in parts]\n if operator == \"AND\":\n return {\"query\": {\"bool\": {\"must\": clauses}}}\n # OR\n return {\"query\": {\"bool\": {\"should\": clauses, \"minimum_should_match\": 1}}}\n\n def convert_edismax(\n self,\n q: str,\n *,\n qf: str | None = None,\n mm: str | None = None,\n pf: str | None = None,\n pf2: str | None = None,\n pf3: str | None = None,\n ps: int | None = None,\n qs: int | None = None,\n tie: float | None = None,\n bq: str | list[str] | None = None,\n bf: str | None = None,\n ) -> dict[str, Any]:\n \"\"\"Convert an eDisMax query to OpenSearch Query DSL.\n\n Args:\n q: The main query text (Solr ``q`` parameter).\n qf: Query fields with optional boosts, e.g. ``\"title^2 body^0.5\"``.\n When provided, a ``multi_match`` query is used instead of the\n standard query conversion.\n mm: Minimum should match, passed through verbatim to the bool query\n (e.g. ``\"75%\"`` or ``\"2\"``).\n pf: Phrase boost fields. Translated to ``should`` ``multi_match``\n clauses with ``type: phrase``.\n pf2: Bigram phrase boost fields (same translation as ``pf``).\n pf3: Trigram phrase boost fields (same translation as ``pf``).\n ps: Phrase slop applied to ``pf`` phrase clauses.\n qs: Query slop applied to the ``qf`` ``multi_match`` clause.\n tie: Tiebreaker for ``multi_match`` cross-field scoring.\n bq: Additive boost query (or list of queries). Each is translated\n and added as a ``should`` clause. Note: Solr ``bq`` is additive\n while OpenSearch ``should`` is multiplicative — this is a\n Behavioral difference.\n bf: Boost function expression. Wrapped in a ``script_score`` query\n as a Painless script approximation. Complex Solr function\n expressions will require manual adjustment.\n\n Returns:\n An OpenSearch Query DSL dict (the full ``{\"query\": …}`` envelope).\n\n Raises:\n ValueError: If ``q`` is empty.\n \"\"\"\n if not q or not q.strip():\n raise ValueError(\"q must not be empty\")\n\n query_text = q.strip()\n\n # --- Main query clause ---\n main_clause = _build_edismax_main_clause(\n query_text, qf=qf, tie=tie, qs=qs,\n fallback=self.convert(query_text)[\"query\"] if not qf else None,\n )\n\n # --- Phrase boost clauses (pf / pf2 / pf3) ---\n should_clauses: list[dict[str, Any]] = []\n for phrase_field_str in filter(None, [pf, pf2, pf3]):\n should_clauses.extend(\n _build_phrase_should_clauses(phrase_field_str, query_text, ps)\n )\n\n # --- Boost queries (bq) ---\n if isinstance(bq, str):\n bq_list: list[str] = [bq]\n elif bq:\n bq_list = list(bq)\n else:\n bq_list = []\n for bq_item in bq_list:\n should_clauses.append(self.convert(bq_item.strip())[\"query\"])\n\n # --- Assemble bool query ---\n assembled = _assemble_edismax_bool(main_clause, should_clauses, mm)\n\n # --- Boost function (bf) wraps everything in script_score ---\n if bf:\n assembled = {\n \"script_score\": {\n \"query\": assembled,\n \"script\": {\"source\": bf},\n }\n }\n\n return {\"query\": assembled}\n\n\ndef _build_edismax_main_clause(\n query_text: str,\n *,\n qf: str | None,\n tie: float | None,\n qs: int | None,\n fallback: dict[str, Any] | None,\n) -> dict[str, Any]:\n \"\"\"Build the main query clause for an eDisMax query.\"\"\"\n if not qf:\n return fallback # type: ignore[return-value]\n clause: dict[str, Any] = {\n \"multi_match\": {\n \"query\": query_text,\n \"fields\": _parse_qf(qf),\n \"type\": \"best_fields\",\n }\n }\n if tie is not None:\n clause[\"multi_match\"][\"tie_breaker\"] = tie\n if qs is not None:\n clause[\"multi_match\"][\"slop\"] = qs\n return clause\n\n\ndef _assemble_edismax_bool(\n main_clause: dict[str, Any],\n should_clauses: list[dict[str, Any]],\n mm: str | None,\n) -> dict[str, Any]:\n \"\"\"Assemble the final bool query from main + should clauses, applying mm.\"\"\"\n if should_clauses:\n bool_query: dict[str, Any] = {\n \"bool\": {\"must\": [main_clause], \"should\": should_clauses}\n }\n if mm is not None:\n bool_query[\"bool\"][\"minimum_should_match\"] = mm\n return bool_query\n\n # No should clauses — apply mm directly if possible\n if mm is not None:\n if isinstance(main_clause, dict) and \"bool\" in main_clause:\n main_clause[\"bool\"][\"minimum_should_match\"] = mm\n return main_clause\n return {\"bool\": {\"must\": [main_clause], \"minimum_should_match\": mm}}\n\n return main_clause\n\n\ndef _unwrap_parens(query: str) -> str:\n \"\"\"Remove a single layer of matching outer parentheses if present.\"\"\"\n query = query.strip()\n if not (query.startswith(\"(\") and query.endswith(\")\")):\n return query\n # Verify the opening paren actually matches the last char.\n depth = 0\n for i, ch in enumerate(query):\n if ch == \"(\":\n depth += 1\n elif ch == \")\":\n depth -= 1\n if depth == 0 and i \u003c len(query) - 1:\n # Closing paren found before end — outer parens are not a single\n # group, don't strip.\n return query\n return query[1:-1].strip()\n\n\n# ---------------------------------------------------------------------------\n# eDisMax helpers\n# ---------------------------------------------------------------------------\n\n_QF_FIELD_RE = re.compile(r'^(?P\u003cfield>\\w+)(?:\\^(?P\u003cboost>[\\d.]+))?

Apache Solr to OpenSearch Migration Advisor An agent skill for migrating from Apache Solr to OpenSearch. This skill provides a transport-agnostic migration advisor that can reason about Solr query behavior, configuration, and cluster architecture. When to Use Use this skill when: - A user needs to migrate a Solr collection or SolrCloud deployment to OpenSearch. - A user wants a comprehensive migration advisor that can handle conversational interaction and maintain session context. - A user has a or Solr Schema API JSON document and needs an equivalent OpenSearch index mapping. - A user has So…

)\n\n\ndef _parse_qf(qf: str) -> list[str]:\n \"\"\"Parse a Solr ``qf`` string into a list of ``field^boost`` strings\n suitable for OpenSearch ``multi_match`` ``fields``.\n\n Examples::\n\n \"title^2 body^0.5\" → [\"title^2\", \"body^0.5\"]\n \"title body\" → [\"title\", \"body\"]\n \"\"\"\n fields: list[str] = []\n for token in qf.split():\n m = _QF_FIELD_RE.match(token)\n if m:\n field = m.group(\"field\")\n boost = m.group(\"boost\")\n fields.append(f\"{field}^{boost}\" if boost else field)\n return fields\n\n\ndef _build_phrase_should_clauses(\n pf: str, query_text: str, slop: int | None\n) -> list[dict[str, Any]]:\n \"\"\"Build ``should`` ``multi_match`` phrase clauses from a ``pf``/``pf2``/``pf3`` string.\"\"\"\n fields = _parse_qf(pf)\n if not fields:\n return []\n clause: dict[str, Any] = {\n \"multi_match\": {\n \"query\": query_text,\n \"type\": \"phrase\",\n \"fields\": fields,\n }\n }\n if slop is not None:\n clause[\"multi_match\"][\"slop\"] = slop\n return [clause]\n","content_type":"text/x-python; charset=utf-8","language":"python","size":16355,"content_sha256":"6880241ceac59e80c3b81d627304b40adbd435087503d6b22e6673d56fc537d9"},{"filename":"scripts/report.py","content":"from typing import List, Dict, TYPE_CHECKING\n\nif TYPE_CHECKING:\n from storage import Incompatibility, ClientIntegration\n\n\nclass MigrationReport:\n \"\"\"Generates a migration report from session context.\"\"\"\n\n def __init__(\n self,\n milestones: List[str] = None,\n blockers: List[str] = None,\n implementation_points: List[str] = None,\n cost_estimates: Dict[str, str] = None,\n incompatibilities: List[\"Incompatibility\"] = None,\n client_integrations: List[\"ClientIntegration\"] = None,\n ):\n self.milestones = milestones or []\n self.blockers = blockers or []\n self.implementation_points = implementation_points or []\n self.cost_estimates = cost_estimates or {}\n self.incompatibilities = incompatibilities or []\n self.client_integrations = client_integrations or []\n\n def generate(self) -> str:\n report = []\n report.append(\"# Solr to OpenSearch Migration Report\\n\")\n \n self._generate_incompatibilities_section(report)\n self._generate_client_integrations_section(report)\n self._generate_milestones_section(report)\n self._generate_blockers_section(report)\n self._generate_implementation_section(report)\n self._generate_cost_estimates_section(report)\n \n return \"\\n\".join(report)\n\n def _generate_incompatibilities_section(self, report: List[str]) -> None:\n \"\"\"Generate the incompatibilities section of the report.\"\"\"\n report.append(\"## Incompatibilities\")\n if self.incompatibilities:\n for severity in (\"Breaking\", \"Unsupported\", \"Behavioral\"):\n items = [i for i in self.incompatibilities if i.severity == severity]\n if not items:\n continue\n report.append(f\"\\n### {severity}\")\n for item in items:\n report.append(f\"- **[{item.category}]** {item.description}\")\n report.append(f\" - *Recommendation:* {item.recommendation}\")\n critical = [\n i for i in self.incompatibilities\n if i.severity in (\"Breaking\", \"Unsupported\")\n ]\n if critical:\n report.append(\n \"\\n> **Action required:** The items above marked Breaking or \"\n \"Unsupported must be resolved before cutover.\"\n )\n else:\n report.append(\"- No incompatibilities identified.\")\n report.append(\"\")\n\n def _generate_client_integrations_section(self, report: List[str]) -> None:\n \"\"\"Generate the client & front-end impact section of the report.\"\"\"\n report.append(\"## Client & Front-end Impact\")\n if self.client_integrations:\n self._render_client_integrations_by_kind(report)\n else:\n report.append(\"- No client or front-end integrations recorded.\")\n report.append(\"\")\n\n def _render_client_integrations_by_kind(self, report: List[str]) -> None:\n \"\"\"Render client integrations grouped by kind.\"\"\"\n kind_order = [\"library\", \"ui\", \"http\", \"other\"]\n kind_labels = {\n \"library\": \"Client Libraries\",\n \"ui\": \"Front-end / UI\",\n \"http\": \"HTTP / Custom Clients\",\n \"other\": \"Other Integrations\",\n }\n rendered_kinds = set()\n \n for kind in kind_order:\n items = [c for c in self.client_integrations if c.kind == kind]\n if not items:\n continue\n rendered_kinds.add(kind)\n report.append(f\"\\n### {kind_labels[kind]}\")\n for c in items:\n self._render_client_integration(report, c)\n \n # Catch any kinds not in kind_order\n for c in self.client_integrations:\n if c.kind not in rendered_kinds and c.kind not in kind_order:\n report.append(f\"- **{c.name}** ({c.kind})\")\n if c.notes:\n report.append(f\" - *Current usage:* {c.notes}\")\n report.append(f\" - *Migration action:* {c.migration_action}\")\n\n def _render_client_integration(self, report: List[str], integration: \"ClientIntegration\") -> None:\n \"\"\"Render a single client integration entry.\"\"\"\n report.append(f\"- **{integration.name}**\")\n if integration.notes:\n report.append(f\" - *Current usage:* {integration.notes}\")\n report.append(f\" - *Migration action:* {integration.migration_action}\")\n\n def _generate_milestones_section(self, report: List[str]) -> None:\n \"\"\"Generate the major milestones section of the report.\"\"\"\n report.append(\"## Major Milestones\")\n for i, m in enumerate(self.milestones, 1):\n report.append(f\"{i}. {m}\")\n report.append(\"\")\n\n def _generate_blockers_section(self, report: List[str]) -> None:\n \"\"\"Generate the potential blockers section of the report.\"\"\"\n report.append(\"## Potential Blockers\")\n for b in self.blockers:\n report.append(f\"- {b}\")\n if not self.blockers:\n report.append(\"- No immediate blockers identified.\")\n report.append(\"\")\n\n def _generate_implementation_section(self, report: List[str]) -> None:\n \"\"\"Generate the implementation points section of the report.\"\"\"\n report.append(\"## Implementation Points\")\n for ip in self.implementation_points:\n report.append(f\"- {ip}\")\n report.append(\"\")\n\n def _generate_cost_estimates_section(self, report: List[str]) -> None:\n \"\"\"Generate the cost estimates section of the report.\"\"\"\n report.append(\"## Cost Estimates\")\n for item, est in self.cost_estimates.items():\n report.append(f\"- **{item}**: {est}\")\n if not self.cost_estimates:\n report.append(\"- TBD based on further infra analysis.\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5890,"content_sha256":"581e7de357c698aaea821d8ae6a797b8b1e41bb8eb17a7f9d763508d8655a19b"},{"filename":"scripts/schema_converter.py","content":"\"\"\"\nConverts Apache Solr schema definitions to OpenSearch index mappings.\n\nSupports both Solr schema.xml format and the Solr Schema API JSON format.\n\"\"\"\n\nimport json\nimport xml.etree.ElementTree as ET\nfrom typing import Any\n\n\n# Maps Solr field type class names (short and fully-qualified) to OpenSearch\n# mapping types.\nSOLR_TYPE_TO_OPENSEARCH: dict[str, str] = {\n # Text\n \"solr.TextField\": \"text\",\n \"TextField\": \"text\",\n # Keyword / string\n \"solr.StrField\": \"keyword\",\n \"StrField\": \"keyword\",\n # Integers\n \"solr.IntPointField\": \"integer\",\n \"IntPointField\": \"integer\",\n \"solr.TrieIntField\": \"integer\",\n \"TrieIntField\": \"integer\",\n # Longs\n \"solr.LongPointField\": \"long\",\n \"LongPointField\": \"long\",\n \"solr.TrieLongField\": \"long\",\n \"TrieLongField\": \"long\",\n # Floats\n \"solr.FloatPointField\": \"float\",\n \"FloatPointField\": \"float\",\n \"solr.TrieFloatField\": \"float\",\n \"TrieFloatField\": \"float\",\n # Doubles\n \"solr.DoublePointField\": \"double\",\n \"DoublePointField\": \"double\",\n \"solr.TrieDoubleField\": \"double\",\n \"TrieDoubleField\": \"double\",\n # Dates\n \"solr.DatePointField\": \"date\",\n \"DatePointField\": \"date\",\n \"solr.TrieDateField\": \"date\",\n \"TrieDateField\": \"date\",\n # Booleans\n \"solr.BoolField\": \"boolean\",\n \"BoolField\": \"boolean\",\n # Binary\n \"solr.BinaryField\": \"binary\",\n \"BinaryField\": \"binary\",\n # Geo\n \"solr.LatLonPointSpatialField\": \"geo_point\",\n \"LatLonPointSpatialField\": \"geo_point\",\n \"solr.SpatialRecursivePrefixTreeFieldType\": \"geo_shape\",\n \"SpatialRecursivePrefixTreeFieldType\": \"geo_shape\",\n}\n\n# Solr field attributes that influence OpenSearch mapping options.\n_INDEXED_ATTR = \"indexed\"\n_STORED_ATTR = \"stored\"\n_MULTI_VALUED_ATTR = \"multiValued\"\n_DOC_VALUES_ATTR = \"docValues\"\n\n\ndef _solr_bool(value: str | None, default: bool = True) -> bool:\n \"\"\"Convert a Solr XML attribute string to a Python bool.\"\"\"\n if value is None:\n return default\n return value.strip().lower() == \"true\"\n\n\nclass SchemaConverter:\n \"\"\"Converts a Solr schema to an OpenSearch index mapping.\n\n Usage::\n\n converter = SchemaConverter()\n\n # From schema.xml content\n mapping = converter.convert_xml(schema_xml_string)\n\n # From Solr Schema API JSON\n mapping = converter.convert_json(schema_api_json_string)\n\n print(json.dumps(mapping, indent=2))\n \"\"\"\n\n # ------------------------------------------------------------------\n # Public API\n # ------------------------------------------------------------------\n\n def _get_field_type_map_xml(self, root: ET.Element) -> dict[str, str]:\n \"\"\"Build a lookup from field-type name → Solr class name from XML.\"\"\"\n field_type_map: dict[str, str] = {}\n for ft in root.iter(\"fieldType\"):\n name = ft.get(\"name\")\n class_ = ft.get(\"class\", \"\")\n if name:\n field_type_map[name] = class_\n return field_type_map\n\n def _process_fields_xml(\n self,\n root: ET.Element,\n field_type_map: dict[str, str]\n ) -> dict[str, Any]:\n \"\"\"Convert Solr fields to OpenSearch properties from XML.\"\"\"\n properties: dict[str, Any] = {}\n for field in root.iter(\"field\"):\n field_name = field.get(\"name\")\n if not field_name or field_name.startswith(\"_\"):\n # Skip internal Solr fields (e.g. _version_)\n continue\n\n field_type_name = field.get(\"type\", \"\")\n solr_class = field_type_map.get(field_type_name, field_type_name)\n os_type = SOLR_TYPE_TO_OPENSEARCH.get(solr_class, \"keyword\")\n\n prop: dict[str, Any] = {\"type\": os_type}\n\n # Propagate store/index hints where relevant.\n if not _solr_bool(field.get(_STORED_ATTR)):\n prop[\"store\"] = False\n\n if not _solr_bool(field.get(_INDEXED_ATTR)):\n prop[\"index\"] = False\n\n if _solr_bool(field.get(_DOC_VALUES_ATTR), default=False):\n prop[\"doc_values\"] = True\n\n properties[field_name] = prop\n return properties\n\n def _process_dynamic_fields_xml(\n self,\n root: ET.Element,\n field_type_map: dict[str, str]\n ) -> list[dict[str, Any]]:\n \"\"\"Convert Solr dynamic fields to OpenSearch dynamic templates from XML.\"\"\"\n dynamic_templates: list[dict[str, Any]] = []\n for df in root.iter(\"dynamicField\"):\n name_pattern = df.get(\"name\", \"\")\n field_type_name = df.get(\"type\", \"\")\n solr_class = field_type_map.get(field_type_name, field_type_name)\n os_type = SOLR_TYPE_TO_OPENSEARCH.get(solr_class, \"keyword\")\n\n # Build a best-effort dynamic template.\n if name_pattern.startswith(\"*_\"):\n suffix = name_pattern[2:]\n template_name = f\"dynamic_{suffix}\"\n dynamic_templates.append(\n {\n template_name: {\n \"match\": name_pattern,\n \"match_pattern\": \"wildcard\",\n \"mapping\": {\"type\": os_type},\n }\n }\n )\n return dynamic_templates\n\n def convert_xml(self, schema_xml: str) -> dict[str, Any]:\n \"\"\"Convert a Solr ``schema.xml`` document to an OpenSearch mapping.\n\n Args:\n schema_xml: The full text content of a Solr ``schema.xml`` file.\n\n Returns:\n A dict representing the OpenSearch index mapping, suitable for\n serialisation with :func:`json.dumps`.\n\n Raises:\n ValueError: If the XML cannot be parsed or does not look like a\n Solr schema document.\n \"\"\"\n try:\n root = ET.fromstring(schema_xml)\n except ET.ParseError as exc:\n raise ValueError(f\"Invalid XML: {exc}\") from exc\n\n if root.tag != \"schema\":\n raise ValueError(\n f\"Expected root element \u003cschema>, got \u003c{root.tag}>\"\n )\n\n field_type_map = self._get_field_type_map_xml(root)\n properties = self._process_fields_xml(root, field_type_map)\n dynamic_templates = self._process_dynamic_fields_xml(root, field_type_map)\n\n mapping: dict[str, Any] = {\"mappings\": {\"properties\": properties}}\n if dynamic_templates:\n mapping[\"mappings\"][\"dynamic_templates\"] = dynamic_templates\n\n return mapping\n\n def _get_field_type_map(self, schema: dict[str, Any]) -> dict[str, str]:\n \"\"\"Build a lookup from field-type name → Solr class name.\"\"\"\n field_type_map: dict[str, str] = {}\n for ft in schema.get(\"fieldTypes\", []):\n name = ft.get(\"name\")\n class_ = ft.get(\"class\", \"\")\n if name:\n field_type_map[name] = class_\n return field_type_map\n\n def _process_fields(\n self,\n schema: dict[str, Any],\n field_type_map: dict[str, str]\n ) -> dict[str, Any]:\n \"\"\"Convert Solr fields to OpenSearch properties.\"\"\"\n properties: dict[str, Any] = {}\n for field in schema.get(\"fields\", []):\n field_name = field.get(\"name\")\n if not field_name or field_name.startswith(\"_\"):\n continue\n\n field_type_name = field.get(\"type\", \"\")\n solr_class = field_type_map.get(field_type_name, field_type_name)\n os_type = SOLR_TYPE_TO_OPENSEARCH.get(solr_class, \"keyword\")\n\n prop: dict[str, Any] = {\"type\": os_type}\n\n if not field.get(\"stored\", True):\n prop[\"store\"] = False\n\n if not field.get(\"indexed\", True):\n prop[\"index\"] = False\n\n if field.get(\"docValues\", False):\n prop[\"doc_values\"] = True\n\n properties[field_name] = prop\n return properties\n\n def _process_dynamic_fields(\n self,\n schema: dict[str, Any],\n field_type_map: dict[str, str]\n ) -> list[dict[str, Any]]:\n \"\"\"Convert Solr dynamic fields to OpenSearch dynamic templates.\"\"\"\n dynamic_templates: list[dict[str, Any]] = []\n for df in schema.get(\"dynamicFields\", []):\n name_pattern = df.get(\"name\", \"\")\n field_type_name = df.get(\"type\", \"\")\n solr_class = field_type_map.get(field_type_name, field_type_name)\n os_type = SOLR_TYPE_TO_OPENSEARCH.get(solr_class, \"keyword\")\n\n if name_pattern.startswith(\"*_\"):\n suffix = name_pattern[2:]\n template_name = f\"dynamic_{suffix}\"\n dynamic_templates.append(\n {\n template_name: {\n \"match\": name_pattern,\n \"match_pattern\": \"wildcard\",\n \"mapping\": {\"type\": os_type},\n }\n }\n )\n return dynamic_templates\n\n def convert_json(self, schema_api_json: str) -> dict[str, Any]:\n \"\"\"Convert a Solr Schema API JSON document to an OpenSearch mapping.\n\n The Solr Schema API returns JSON that looks like::\n\n {\n \"schema\": {\n \"fieldTypes\": [...],\n \"fields\": [...],\n \"dynamicFields\": [...]\n }\n }\n\n Args:\n schema_api_json: JSON string from the Solr Schema API\n (``/solr/\u003ccollection>/schema``).\n\n Returns:\n A dict representing the OpenSearch index mapping.\n\n Raises:\n ValueError: If the JSON cannot be parsed or is missing required\n keys.\n \"\"\"\n try:\n data = json.loads(schema_api_json)\n except json.JSONDecodeError as exc:\n raise ValueError(f\"Invalid JSON: {exc}\") from exc\n\n schema = data.get(\"schema\", data)\n\n field_type_map = self._get_field_type_map(schema)\n properties = self._process_fields(schema, field_type_map)\n dynamic_templates = self._process_dynamic_fields(schema, field_type_map)\n\n mapping: dict[str, Any] = {\"mappings\": {\"properties\": properties}}\n if dynamic_templates:\n mapping[\"mappings\"][\"dynamic_templates\"] = dynamic_templates\n\n return mapping\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10371,"content_sha256":"9883ec5bb234e91eaab77da6428241bb905fb0c4263b8f8742b45a8b601b96fa"},{"filename":"scripts/skill.py","content":"\"\"\"\nMain agent skill class for migrating from Apache Solr to OpenSearch.\n\nThe :class:`SolrToOpenSearchMigrationSkill` class acts as a high-level facade\nthat can be used as an agent tool in the OpenSearch ML agent framework. Each\npublic method corresponds to a discrete migration capability that an agent can\ninvoke.\n\nSession state is fully resumable: every call to :meth:`handle_message` loads\nthe existing :class:`~storage.SessionState` for the given *session_id*,\nupdates it, and persists it back through the configured\n:class:`~storage.StorageBackend`. Swapping the backend (e.g. from\n:class:`~storage.FileStorage` to a database-backed implementation) requires\nonly passing a different backend to the constructor.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport urllib.request\nimport urllib.error\nfrom typing import Dict, Optional\n\nfrom schema_converter import SchemaConverter\nfrom query_converter import QueryConverter\nfrom storage import StorageBackend, FileStorage, SessionState\nfrom report import MigrationReport\n\n\nclass SolrToOpenSearchMigrationSkill:\n \"\"\"Agent skill for migrating from Apache Solr to OpenSearch.\n\n Capabilities\n ------------\n * :meth:`convert_schema_xml` — Translate ``schema.xml`` → OpenSearch mapping.\n * :meth:`convert_schema_json` — Translate Solr Schema API JSON → OpenSearch mapping.\n * :meth:`convert_query` — Translate Solr query string → Query DSL.\n * :meth:`get_migration_checklist` — Return a human-readable migration checklist.\n * :meth:`get_field_type_mapping_reference` — Return a Solr→OpenSearch type table.\n * :meth:`generate_report` — Generate a comprehensive migration report.\n * :meth:`handle_message` — Transport-agnostic conversational interface.\n\n Session resumability\n --------------------\n All state (conversation history, discovered facts, migration progress, and\n incompatibilities) is stored via the injected :class:`~storage.StorageBackend`.\n Pass ``storage=InMemoryStorage()`` for ephemeral use or tests, or\n ``storage=FileStorage(path)`` (the default) for persistent sessions.\n\n Usage::\n\n skill = SolrToOpenSearchMigrationSkill()\n response = skill.handle_message(\"Convert this schema...\", session_id=\"user-123\")\n # Resume the same session later — state is automatically reloaded.\n response = skill.handle_message(\"Now generate the report.\", session_id=\"user-123\")\n \"\"\"\n\n def __init__(self, storage: Optional[StorageBackend] = None) -> None:\n self._schema_converter = SchemaConverter()\n self._query_converter = QueryConverter()\n self._storage = storage or FileStorage()\n self._steering_docs = self._load_steering_docs()\n self._aws_knowledge_url = \"https://knowledge-mcp.global.api.aws\"\n\n # ------------------------------------------------------------------\n # Internal helpers\n # ------------------------------------------------------------------\n\n def _load_steering_docs(self) -> Dict[str, str]:\n \"\"\"Load steering documents from the data directory.\"\"\"\n docs: Dict[str, str] = {}\n data_dir = os.path.join(\n os.path.dirname(os.path.dirname(__file__)), \"data\", \"steering\"\n )\n if os.path.exists(data_dir):\n for filename in os.listdir(data_dir):\n if filename.endswith(\".md\"):\n with open(os.path.join(data_dir, filename), \"r\") as fh:\n docs[filename[:-3]] = fh.read()\n return docs\n\n def _load_session(self, session_id: str) -> SessionState:\n \"\"\"Load or create a session.\"\"\"\n return self._storage.load_or_new(session_id)\n\n def _save_session(self, state: SessionState) -> None:\n self._storage.save(state)\n\n def _query_aws_knowledge(self, query: str, topic: str = \"general\") -> str:\n \"\"\"Query the AWS Knowledge MCP Server for accurate AWS information.\n\n Falls back gracefully if the server is unreachable.\n\n Args:\n query: Natural-language search phrase.\n topic: Documentation topic (e.g. \"general\", \"reference_documentation\",\n \"troubleshooting\", \"current_awareness\").\n\n Returns:\n Relevant documentation excerpts, or an empty string on failure.\n \"\"\"\n payload = json.dumps({\n \"jsonrpc\": \"2.0\",\n \"id\": 1,\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"search_documentation\",\n \"arguments\": {\"search_phrase\": query, \"topics\": [topic]},\n },\n }).encode(\"utf-8\")\n req = urllib.request.Request(\n self._aws_knowledge_url,\n data=payload,\n method=\"POST\",\n headers={\"Content-Type\": \"application/json\"},\n )\n try:\n with urllib.request.urlopen(req, timeout=10) as resp:\n result = json.loads(resp.read().decode(\"utf-8\"))\n content = result.get(\"result\", {}).get(\"content\", [])\n return \"\\n\".join(\n item.get(\"text\", \"\")\n for item in content\n if item.get(\"type\") == \"text\"\n )\n except Exception: # noqa: BLE001 — degrade gracefully\n return \"\"\n\n # ------------------------------------------------------------------\n # Conversational interface\n # ------------------------------------------------------------------\n\n def handle_message(self, message: str, session_id: str) -> str:\n \"\"\"Transport-agnostic core interface.\n\n Loads the session for *session_id*, processes *message*, updates all\n relevant state fields (history, facts, progress, incompatibilities),\n and persists the session before returning.\n\n Args:\n message: The user's message.\n session_id: Unique identifier for the migration session.\n\n Returns:\n A string response from the advisor.\n \"\"\"\n state = self._load_session(session_id)\n response = self._dispatch(message, state, session_id)\n state.append_turn(message, response)\n self._save_session(state)\n return response\n\n def _dispatch(self, message: str, state: SessionState, session_id: str) -> str:\n \"\"\"Route *message* to the appropriate handler.\"\"\"\n message_lc = message.lower()\n\n if \"report\" in message_lc:\n return self.generate_report(session_id)\n\n if self._looks_like_schema(message, message_lc):\n return self._handle_schema(message, state)\n\n if \"query\" in message_lc or \"translate\" in message_lc:\n return self._handle_query(message, message_lc, state)\n\n if \"checklist\" in message_lc:\n return self.get_migration_checklist()\n\n if \"field type\" in message_lc or \"type mapping\" in message_lc:\n return self.get_field_type_mapping_reference()\n\n return self._handle_general(message, message_lc)\n\n # ------------------------------------------------------------------\n # Message handlers (one per intent)\n # ------------------------------------------------------------------\n\n _SCHEMA_START_TAG = \"\u003cschema\"\n _SCHEMA_END_TAG = \"\u003c/schema>\"\n\n @staticmethod\n def _looks_like_schema(message: str, message_lc: str) -> bool:\n \"\"\"Return True if *message* appears to contain or request a schema conversion.\"\"\"\n if (\n SolrToOpenSearchMigrationSkill._SCHEMA_START_TAG in message and\n SolrToOpenSearchMigrationSkill._SCHEMA_END_TAG in message\n ):\n return True\n schema_keywords = (\"schema\" in message_lc or \"migrate\" in message_lc or \"convert\" in message_lc)\n return schema_keywords and SolrToOpenSearchMigrationSkill._SCHEMA_START_TAG in message\n\n def _handle_schema(self, message: str, state: SessionState) -> str:\n \"\"\"Handle a schema conversion request.\"\"\"\n schema_start = message.find(self._SCHEMA_START_TAG)\n schema_end = message.find(self._SCHEMA_END_TAG)\n if schema_start == -1 or schema_end == -1:\n return (\n \"I detected you want to convert a schema, but I couldn't find \"\n \"the XML content. Please paste your full `schema.xml` content.\"\n )\n schema_xml = message[schema_start: schema_end + len(self._SCHEMA_END_TAG)]\n mapping = self.convert_schema_xml(schema_xml)\n state.set_fact(\"schema_migrated\", True)\n state.advance_progress(1)\n return (\n f\"I've converted your Solr schema to an OpenSearch mapping:\"\n f\"\\n\\n```json\\n{mapping}\\n```\"\n )\n\n def _handle_query(self, message: str, message_lc: str, state: SessionState) -> str:\n \"\"\"Handle a query translation request.\"\"\"\n q = self._extract_query_text(message, message_lc)\n if not q:\n return \"What query would you like me to translate?\"\n try:\n dsl = self.convert_query(q)\n except ValueError:\n return (\n \"I couldn't parse that query. Please provide a valid Solr \"\n \"query string (e.g. `title:opensearch AND year:[2020 TO *]`).\"\n )\n state.advance_progress(3)\n return (\n f\"The OpenSearch equivalent of your query is:\"\n f\"\\n\\n```json\\n{dsl}\\n```\"\n )\n\n @staticmethod\n def _extract_query_text(message: str, message_lc: str) -> str:\n \"\"\"Pull the raw Solr query string out of the user's message.\"\"\"\n for keyword in (\"query:\", \"query\", \"translate:\"):\n idx = message_lc.find(keyword)\n if idx != -1:\n return message[idx + len(keyword):].strip().lstrip(\": \").strip()\n return \"\"\n\n _OPENSEARCH_KEYWORDS = (\n \"opensearch\", \"index\", \"shard\", \"replica\", \"mapping\",\n \"cluster\", \"node\", \"query dsl\", \"aggregation\", \"analyzer\",\n \"aws\", \"service\", \"region\", \"pricing\", \"instance\",\n )\n\n def _handle_general(self, message: str, message_lc: str) -> str:\n \"\"\"Fallback handler: try AWS Knowledge enrichment, else greet.\"\"\"\n if any(kw in message_lc for kw in self._OPENSEARCH_KEYWORDS):\n aws_context = self._query_aws_knowledge(message, topic=\"general\")\n if aws_context:\n return \"Here is accurate information from AWS documentation:\\n\\n\" + aws_context\n return (\n \"I'm your Solr to OpenSearch migration advisor. How can I help you \"\n \"today? I can convert schemas, translate queries, or generate a \"\n \"migration report.\"\n )\n\n # ------------------------------------------------------------------\n # Report generation\n # ------------------------------------------------------------------\n\n def generate_report(self, session_id: str) -> str:\n \"\"\"Generate a comprehensive migration report for the session.\n\n The report prominently surfaces all incompatibilities collected during\n the migration workflow, grouped by severity (Breaking → Unsupported →\n Behavioral), followed by milestones, blockers, implementation points,\n and cost estimates.\n\n Args:\n session_id: The session identifier.\n\n Returns:\n A Markdown-formatted migration report.\n \"\"\"\n state = self._storage.load_or_new(session_id)\n facts = state.facts\n\n milestones = [\n \"Infrastructure setup and sizing\",\n \"Schema and analysis chain migration\",\n \"Data re-indexing and validation\",\n \"Application query and client migration\",\n \"Parallel testing and cutover\",\n ]\n\n blockers: list[str] = []\n if not facts.get(\"schema_migrated\"):\n blockers.append(\"Schema not yet analyzed for incompatibilities.\")\n\n # Surface Breaking/Unsupported incompatibilities as explicit blockers.\n for inc in state.incompatibilities:\n if inc.severity in (\"Breaking\", \"Unsupported\"):\n blockers.append(f\"[{inc.severity}] {inc.description}\")\n\n ip = [\n \"Map Solr field types to OpenSearch equivalents (see steering documents).\",\n \"Replace Solr copyField with OpenSearch copy_to.\",\n \"Update client libraries from SolrJ/SolrPy to OpenSearch clients.\",\n ]\n\n # Append customization migration items collected in Step 4.\n for solr_item, os_solution in facts.get(\"customizations\", {}).items():\n ip.append(f\"Customization — {solr_item}: {os_solution}\")\n\n costs = {\n \"Infrastructure\": (\n \"Estimated 10% increase over Solr due to shard management overhead.\"\n ),\n \"Effort\": \"Moderate (2-4 weeks for typical mid-sized workload).\",\n }\n\n report = MigrationReport(\n milestones=milestones,\n blockers=blockers,\n implementation_points=ip,\n cost_estimates=costs,\n incompatibilities=state.incompatibilities,\n client_integrations=state.client_integrations,\n )\n return report.generate()\n\n # ------------------------------------------------------------------\n # Schema conversion\n # ------------------------------------------------------------------\n\n def convert_schema_xml(self, schema_xml: str, *, indent: int = 2) -> str:\n \"\"\"Convert a Solr ``schema.xml`` to an OpenSearch index mapping.\n\n Args:\n schema_xml: Full text content of a Solr ``schema.xml`` file.\n indent: JSON indentation level for the returned string.\n\n Returns:\n A JSON string representing the OpenSearch index mapping.\n\n Raises:\n ValueError: If the XML cannot be parsed or is not a valid Solr schema.\n \"\"\"\n mapping = self._schema_converter.convert_xml(schema_xml)\n return json.dumps(mapping, indent=indent)\n\n def convert_schema_json(self, schema_api_json: str, *, indent: int = 2) -> str:\n \"\"\"Convert a Solr Schema API JSON document to an OpenSearch mapping.\n\n Args:\n schema_api_json: JSON string returned by the Solr Schema API.\n indent: JSON indentation level for the returned string.\n\n Returns:\n A JSON string representing the OpenSearch index mapping.\n\n Raises:\n ValueError: If the JSON cannot be parsed or is missing required keys.\n \"\"\"\n mapping = self._schema_converter.convert_json(schema_api_json)\n return json.dumps(mapping, indent=indent)\n\n # ------------------------------------------------------------------\n # Query conversion\n # ------------------------------------------------------------------\n\n def convert_query(self, solr_query: str, *, indent: int = 2) -> str:\n \"\"\"Convert a Solr query string to an OpenSearch Query DSL JSON string.\n\n Args:\n solr_query: A Solr query string (the ``q`` parameter value).\n indent: JSON indentation level for the returned string.\n\n Returns:\n A JSON string representing the OpenSearch Query DSL.\n\n Raises:\n ValueError: If ``solr_query`` is empty.\n \"\"\"\n dsl = self._query_converter.convert(solr_query)\n return json.dumps(dsl, indent=indent)\n\n # ------------------------------------------------------------------\n # Migration guidance\n # ------------------------------------------------------------------\n\n def get_migration_checklist(self) -> str:\n \"\"\"Return a human-readable checklist of migration steps.\"\"\"\n return _MIGRATION_CHECKLIST\n\n def get_field_type_mapping_reference(self) -> str:\n \"\"\"Return a Markdown reference table of Solr → OpenSearch field type mappings.\"\"\"\n from schema_converter import SOLR_TYPE_TO_OPENSEARCH\n\n lines: list[str] = [\n \"| Solr Field Type | OpenSearch Type |\",\n \"|---|---|\",\n ]\n seen: dict[str, str] = {}\n for solr_type, os_type in SOLR_TYPE_TO_OPENSEARCH.items():\n short = solr_type.replace(\"solr.\", \"\")\n if short not in seen:\n seen[short] = os_type\n lines.append(f\"| {short} | {os_type} |\")\n return \"\\n\".join(lines)\n\n\n# ---------------------------------------------------------------------------\n# Static content\n# ---------------------------------------------------------------------------\n\n_MIGRATION_CHECKLIST: str = \"\"\"\\\nApache Solr → OpenSearch Migration Checklist\n=============================================\n\n1. PREPARATION\n [ ] Back up all Solr collections and configuration files.\n [ ] Document all Solr field types, fields, and dynamic fields.\n [ ] Record all custom tokenizers, filters, and analyzers.\n [ ] List all Solr request handlers, search components, and plugins.\n [ ] Identify any SolrCloud-specific configuration (ZooKeeper, shards,\n replicas).\n\n2. SCHEMA / MAPPING MIGRATION\n [ ] Convert Solr schema.xml to an OpenSearch index mapping using the\n convert_schema_xml() skill method.\n [ ] Review the generated mapping for accuracy and completeness.\n [ ] Map custom Solr field types to appropriate OpenSearch types.\n [ ] Translate custom analyzers (char filters, tokenizers, token filters)\n to OpenSearch analysis settings.\n [ ] Handle multi-valued fields (OpenSearch arrays are native).\n [ ] Replace Solr copyField directives with OpenSearch copy_to.\n\n3. INDEX SETTINGS\n [ ] Define OpenSearch index settings (number_of_shards,\n number_of_replicas, refresh_interval, etc.).\n [ ] Translate Solr synonyms.txt and stopwords.txt to OpenSearch analysis\n synonym / stop token filter configuration.\n [ ] Configure dynamic mappings or disable them to match Solr's\n schemaFactory setting.\n\n4. QUERY MIGRATION\n [ ] Identify all query types in use (standard, edismax, dismax, spatial,\n facet, etc.).\n [ ] Convert standard Solr queries to OpenSearch Query DSL using the\n convert_query() skill method.\n [ ] Translate eDismax parameters (qf, pf, mm, boost, etc.) to\n OpenSearch multi_match / function_score queries.\n [ ] Migrate facet queries to OpenSearch aggregations.\n [ ] Replace Solr highlighting parameters with OpenSearch highlight API.\n [ ] Convert Solr spatial queries to OpenSearch geo_distance / geo_shape\n queries.\n [ ] Migrate Solr MoreLikeThis queries to OpenSearch more_like_this.\n\n5. DATA MIGRATION\n [ ] Choose a migration strategy:\n a) Re-index from source data (recommended for clean migration).\n b) Export from Solr via Data Import Handler or curl and import into\n OpenSearch using the Bulk API.\n [ ] Validate document counts and spot-check field values after migration.\n\n6. APPLICATION / CLIENT MIGRATION\n [ ] Replace the Solr Java/Python/Ruby client with the appropriate\n OpenSearch client.\n [ ] Update HTTP endpoints (Solr uses /solr/\u003ccollection>/select;\n OpenSearch uses /\u003cindex>/_search).\n [ ] Migrate Solr Admin UI usage to OpenSearch Dashboards.\n [ ] Replace SolrCloud management API calls with OpenSearch Cluster API\n calls.\n\n7. TESTING\n [ ] Run query equivalence tests: same inputs should return equivalent\n results from both Solr and OpenSearch.\n [ ] Validate relevance scores and ranking.\n [ ] Load-test the OpenSearch cluster under production-level traffic.\n [ ] Test failover and replica behaviour.\n\n8. CUTOVER\n [ ] Run both systems in parallel and compare results.\n [ ] Switch application traffic to OpenSearch.\n [ ] Monitor OpenSearch cluster health and query latency.\n [ ] Decommission Solr after a suitable stabilization period.\n\nUSEFUL OPENSEARCH DOCUMENTATION\n--------------------------------\n* Index API: https://opensearch.org/docs/latest/api-reference/index-apis/\n* Mapping: https://opensearch.org/docs/latest/field-types/\n* Query DSL: https://opensearch.org/docs/latest/query-dsl/\n* Aggregations: https://opensearch.org/docs/latest/aggregations/\n* Analysis: https://opensearch.org/docs/latest/analyzers/\n* Migration guide: https://opensearch.org/docs/latest/migration-guide/\n\"\"\"\n","content_type":"text/x-python; charset=utf-8","language":"python","size":20289,"content_sha256":"cfc97f9d767eef406dddd47bab301be189cf3eda7c2bf29711f4f0786f2deb93"},{"filename":"scripts/storage.py","content":"\"\"\"\nPluggable, session-resumable memory for the Solr-to-OpenSearch migration skill.\n\nSession state is modelled as a typed :class:`SessionState` dataclass so that\nall backends store and return the same structure. New backends only need to\nimplement the four abstract methods on :class:`StorageBackend`.\n\nBuilt-in backends\n-----------------\n* :class:`InMemoryStorage` — ephemeral, process-scoped (good for tests and\n single-turn use).\n* :class:`FileStorage` — JSON files on disk, one file per session\n (default for persistent use).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass, field, asdict\nfrom typing import Any, Dict, List, Optional\n\n\n# ---------------------------------------------------------------------------\n# Session state schema\n# ---------------------------------------------------------------------------\n\n@dataclass\nclass Incompatibility:\n \"\"\"A single discovered incompatibility between Solr and OpenSearch.\"\"\"\n category: str # e.g. \"schema\", \"query\", \"plugin\"\n severity: str # \"Breaking\" | \"Behavioral\" | \"Unsupported\"\n description: str\n recommendation: str\n\n def to_dict(self) -> Dict[str, str]:\n return asdict(self)\n\n @classmethod\n def from_dict(cls, d: Dict[str, Any]) -> \"Incompatibility\":\n return cls(\n category=d.get(\"category\", \"\"),\n severity=d.get(\"severity\", \"\"),\n description=d.get(\"description\", \"\"),\n recommendation=d.get(\"recommendation\", \"\"),\n )\n\n\n@dataclass\nclass ClientIntegration:\n \"\"\"Describes one client-side or front-end integration with Solr.\n\n Collected during Step 6 and surfaced in the migration report's\n Client & Front-end Impact section.\n \"\"\"\n name: str # e.g. \"SolrJ\", \"pysolr\", \"React search UI\"\n kind: str # \"library\" | \"ui\" | \"http\" | \"other\"\n notes: str # free-text description of current usage\n migration_action: str # what needs to change for OpenSearch\n\n def to_dict(self) -> Dict[str, str]:\n return asdict(self)\n\n @classmethod\n def from_dict(cls, d: Dict[str, Any]) -> \"ClientIntegration\":\n return cls(\n name=d.get(\"name\", \"\"),\n kind=d.get(\"kind\", \"other\"),\n notes=d.get(\"notes\", \"\"),\n migration_action=d.get(\"migration_action\", \"\"),\n )\n\n\n@dataclass\nclass SessionState:\n \"\"\"Complete, resumable state for one migration session.\n\n Fields\n ------\n session_id:\n Unique identifier for this session.\n history:\n Ordered list of ``{\"user\": ..., \"assistant\": ...}`` turn dicts.\n facts:\n Arbitrary key/value store for discovered migration facts\n (e.g. ``schema_migrated``, ``customizations``).\n progress:\n Current workflow step number (0 = not started).\n incompatibilities:\n All incompatibilities discovered across every workflow step.\n client_integrations:\n Client-side and front-end integrations collected in Step 6.\n \"\"\"\n session_id: str\n history: List[Dict[str, str]] = field(default_factory=list)\n facts: Dict[str, Any] = field(default_factory=dict)\n progress: int = 0\n incompatibilities: List[Incompatibility] = field(default_factory=list)\n client_integrations: List[ClientIntegration] = field(default_factory=list)\n\n # ------------------------------------------------------------------\n # Convenience helpers\n # ------------------------------------------------------------------\n\n def add_incompatibility(\n self,\n category: str,\n severity: str,\n description: str,\n recommendation: str,\n ) -> None:\n \"\"\"Append an incompatibility, avoiding exact duplicates.\"\"\"\n entry = Incompatibility(category, severity, description, recommendation)\n if entry not in self.incompatibilities:\n self.incompatibilities.append(entry)\n\n def add_client_integration(\n self,\n name: str,\n kind: str,\n notes: str,\n migration_action: str,\n ) -> None:\n \"\"\"Record a client-side or front-end integration, avoiding exact duplicates.\"\"\"\n entry = ClientIntegration(name, kind, notes, migration_action)\n if entry not in self.client_integrations:\n self.client_integrations.append(entry)\n\n def set_fact(self, key: str, value: Any) -> None:\n self.facts[key] = value\n\n def get_fact(self, key: str, default: Any = None) -> Any:\n return self.facts.get(key, default)\n\n def advance_progress(self, step: int) -> None:\n \"\"\"Move progress forward (never backwards).\"\"\"\n if step > self.progress:\n self.progress = step\n\n def append_turn(self, user: str, assistant: str) -> None:\n self.history.append({\"user\": user, \"assistant\": assistant})\n\n # ------------------------------------------------------------------\n # Serialisation\n # ------------------------------------------------------------------\n\n def to_dict(self) -> Dict[str, Any]:\n return {\n \"session_id\": self.session_id,\n \"history\": self.history,\n \"facts\": self.facts,\n \"progress\": self.progress,\n \"incompatibilities\": [i.to_dict() for i in self.incompatibilities],\n \"client_integrations\": [c.to_dict() for c in self.client_integrations],\n }\n\n @classmethod\n def from_dict(cls, d: Dict[str, Any]) -> \"SessionState\":\n return cls(\n session_id=d.get(\"session_id\", \"\"),\n history=d.get(\"history\", []),\n facts=d.get(\"facts\", {}),\n progress=d.get(\"progress\", 0),\n incompatibilities=[\n Incompatibility.from_dict(i)\n for i in d.get(\"incompatibilities\", [])\n ],\n client_integrations=[\n ClientIntegration.from_dict(c)\n for c in d.get(\"client_integrations\", [])\n ],\n )\n\n @classmethod\n def new(cls, session_id: str) -> \"SessionState\":\n \"\"\"Create a blank session.\"\"\"\n return cls(session_id=session_id)\n\n\n# ---------------------------------------------------------------------------\n# Storage interface\n# ---------------------------------------------------------------------------\n\nclass StorageBackend(ABC):\n \"\"\"Abstract base for pluggable session storage backends.\n\n Implementors only need to handle raw JSON-serialisable dicts; the\n :class:`SessionState` serialisation is handled by the base helpers.\n \"\"\"\n\n # --- raw dict operations (implement these) ---\n\n @abstractmethod\n def _save_raw(self, session_id: str, data: Dict[str, Any]) -> None:\n \"\"\"Persist a raw dict for *session_id*.\"\"\"\n\n @abstractmethod\n def _load_raw(self, session_id: str) -> Optional[Dict[str, Any]]:\n \"\"\"Return the raw dict for *session_id*, or ``None`` if absent.\"\"\"\n\n @abstractmethod\n def delete(self, session_id: str) -> None:\n \"\"\"Remove a session entirely.\"\"\"\n\n @abstractmethod\n def list_sessions(self) -> List[str]:\n \"\"\"Return all known session IDs.\"\"\"\n\n # --- typed helpers (use these in application code) ---\n\n def save(self, state: SessionState) -> None:\n \"\"\"Persist a :class:`SessionState`.\"\"\"\n self._save_raw(state.session_id, state.to_dict())\n\n def load(self, session_id: str) -> Optional[SessionState]:\n \"\"\"Load a :class:`SessionState`, or ``None`` if the session is new.\"\"\"\n raw = self._load_raw(session_id)\n if raw is None:\n return None\n return SessionState.from_dict(raw)\n\n def load_or_new(self, session_id: str) -> SessionState:\n \"\"\"Load an existing session or create a blank one.\"\"\"\n return self.load(session_id) or SessionState.new(session_id)\n\n\n# ---------------------------------------------------------------------------\n# Backwards-compatibility shim\n# ---------------------------------------------------------------------------\n\nclass StorageInterface(StorageBackend, ABC):\n \"\"\"Deprecated alias kept for backwards compatibility.\n\n New code should subclass :class:`StorageBackend` directly.\n The old ``save(session_id, data)`` / ``load(session_id)`` signatures are\n preserved via overloads so existing callers continue to work.\n \"\"\"\n\n # Provide the old raw-dict signatures as concrete pass-throughs so that\n # subclasses that only implement the old API still satisfy the ABC.\n def _save_raw(self, session_id: str, data: Dict[str, Any]) -> None:\n pass # overridden by legacy subclasses via save()\n\n def _load_raw(self, session_id: str) -> Optional[Dict[str, Any]]:\n return None # overridden by legacy subclasses via load()\n\n def delete(self, session_id: str) -> None:\n pass # optional for legacy subclasses\n\n\n# ---------------------------------------------------------------------------\n# Built-in backends\n# ---------------------------------------------------------------------------\n\nclass InMemoryStorage(StorageBackend):\n \"\"\"Ephemeral in-process storage.\n\n All data is lost when the process exits. Useful for tests and\n single-session CLI usage.\n \"\"\"\n\n def __init__(self) -> None:\n self._store: Dict[str, Dict[str, Any]] = {}\n\n def _save_raw(self, session_id: str, data: Dict[str, Any]) -> None:\n self._store[session_id] = data\n\n def _load_raw(self, session_id: str) -> Optional[Dict[str, Any]]:\n return self._store.get(session_id)\n\n def delete(self, session_id: str) -> None:\n self._store.pop(session_id, None)\n\n def list_sessions(self) -> List[str]:\n return list(self._store.keys())\n\n\nclass FileStorage(StorageBackend):\n \"\"\"JSON-file-per-session storage.\n\n Each session is stored as ``\u003cbase_path>/\u003csession_id>.json``.\n The directory is created on first use if it does not exist.\n \"\"\"\n\n def __init__(self, base_path: str = \"sessions\") -> None:\n self.base_path = base_path\n os.makedirs(base_path, exist_ok=True)\n\n def _get_path(self, session_id: str) -> str:\n return os.path.join(self.base_path, f\"{session_id}.json\")\n\n def _save_raw(self, session_id: str, data: Dict[str, Any]) -> None:\n with open(self._get_path(session_id), \"w\", encoding=\"utf-8\") as fh:\n json.dump(data, fh, indent=2)\n\n def _load_raw(self, session_id: str) -> Optional[Dict[str, Any]]:\n path = self._get_path(session_id)\n if not os.path.exists(path):\n return None\n with open(path, \"r\", encoding=\"utf-8\") as fh:\n return json.load(fh)\n\n def delete(self, session_id: str) -> None:\n path = self._get_path(session_id)\n if os.path.exists(path):\n os.remove(path)\n\n def list_sessions(self) -> List[str]:\n if not os.path.exists(self.base_path):\n return []\n return [\n f[:-5]\n for f in os.listdir(self.base_path)\n if f.endswith(\".json\")\n ]\n","content_type":"text/x-python; charset=utf-8","language":"python","size":11028,"content_sha256":"7493b6d2ce5ca43f004d1a96c104860b0e3a5d1eb33a4e4175b2787181334baf"},{"filename":"setup/docker/claude/build_image.sh","content":"#!/bin/bash\n\nset -eo pipefail\n\nSCRIPT_DIR=\"$(dirname \"$0\")\"\nCWD=$(pwd)\n# we jump into the solr-opensearch-migration-advisor root to build docker image from there,\n# to allow file copy from root within the docker file.\n# Docker does not allow file copy to image from parent folders\ncd \"$SCRIPT_DIR\"/../../.. || exit\n\necho \"Building docker image\"\ndocker build . -t claude_image:0.0.1 -f ./setup/docker/claude/Dockerfile\n\ncd \"$CWD\" || exit","content_type":"application/x-sh; charset=utf-8","language":"bash","size":436,"content_sha256":"eb82f5c6342e6c8f0e8badbc5e343f202b3ec62a28573e492b0b4ca87ef85b6a"},{"filename":"setup/docker/claude/Dockerfile","content":"FROM public.ecr.aws/amazonlinux/amazonlinux:2023\n\nRUN dnf install -y jq shadow-utils && dnf clean all\n\nRUN useradd -m -s /bin/bash user && \\\n mkdir -p /home/user/claude/.claude/skills/solr-opensearch-migration-advisor && \\\n mkdir -p /home/user/claude/processing/.claude/skills\n\n# We copy only entrypoint.sh to the main folder, which will switch cwd to\n# the claude/processing subfolder before starting claude, to which we also mount the host volume to\n# preserve work.\n# To avoid ownership and thus visibility conflicts between host and container,\n# we copy all relevant agent files to /home/user/claude/.claude.\n# While cwd of agent is claude/processing, it'll discover the level higher.\n# NOTE that the docker build assumes we are in the solr-opensearch-migration-advisor root to be\n# able to copy agent files, thus we need to add the path to entrypoint.sh in the below.\nCOPY ./setup/docker/claude/entrypoint.sh /home/user/entrypoint.sh\nCOPY SKILL.md /home/user/claude/.claude/skills/solr-opensearch-migration-advisor/SKILL.md\nCOPY references /home/user/claude/.claude/skills/solr-opensearch-migration-advisor/references\nCOPY scripts /home/user/claude/.claude/skills/solr-opensearch-migration-advisor/scripts\nCOPY steering /home/user/claude/.claude/skills/solr-opensearch-migration-advisor/steering\nRUN chmod +x /home/user/entrypoint.sh\nRUN chown user /home/user/entrypoint.sh\nRUN chown -R user /home/user/claude\n\nUSER user\nWORKDIR /home/user\n\nRUN curl -fsSL https://claude.ai/install.sh | bash\n\nENV PATH=\"/home/user/.local/bin:${PATH}\"\n\nRUN claude --version\n\nCMD [\"tail\", \"-f\", \"/dev/null\"]","content_type":"text/plain; charset=utf-8","language":"docker","size":1599,"content_sha256":"fc5a0026394a1eeb7c916a681f5bf654cfa5385b2d17232bff01c5e4335b7b7c"},{"filename":"setup/docker/claude/entrypoint.sh","content":"# see here: https://github.com/anthropics/claude-code/issues/8938; needed to adjust to allow\n# circumventing intro instructions when passing outh key to enable direct start without\n# answering setup questions\necho \"$(jq '. += {\"hasCompletedOnboarding\": true}' ~/.claude.json)\" > ~/.claude.json\n\ncd claude/processing || exit\nclaude","content_type":"application/x-sh; charset=utf-8","language":"bash","size":330,"content_sha256":"daa4a4f6b858d7a0372ad4bf08f8d258e78cd1a63070776afe26a62aaf04c029"},{"filename":"setup/docker/claude/README.md","content":"### Claude Agent packaging\n\nThe container-packaged claude agent provides an isolated setup with claude installation \nand startup script to build the image, start up the container, switch into container shell and start up claude.\nThe container already contains all needed files.\n\n- Image needs initial build and rebuild after content changes: `build_image.sh`\n - To build the image, we change the working dir to `solr-opensearch-migration-advisor` root since docker build needs \n to be in hierarchy of needed files, not in deeper subdirectories.\n- simply run `start_container.sh` for startup of container and claude session\n within container from host shell. After running it you find yourself directly in the container shell with \n a running claude code session and migration agent skills discoverable.\n - make sure env var `CLAUDE_CODE_OAUTH_TOKEN` is set in `solr-opensearch-migration-advisor/.env`\n - in case you dont have such a token yet, you can generate it via `claude setup-token`. This allows you to use\n your existing claude code subscription rather than needing another api token.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1103,"content_sha256":"9577b04cb7b1983d5e501887a56c2d4ecbf5584ad47cdc6335dd2bcc0e342a2d"},{"filename":"setup/docker/claude/start_container.sh","content":"#!/bin/bash\n\nset -eo pipefail\n\nSCRIPT_DIR=\"$(dirname \"$0\")\"\nAGENT_VOLUME_DIR=\"$SCRIPT_DIR\"/CLAUDE_ADVISOR_VOLUME\nsource \"$SCRIPT_DIR\"/../../../.env\nmkdir -p \"$AGENT_VOLUME_DIR\"\n\necho \"Starting up claude container\"\ndocker run -d --rm \\\n --name claude-container \\\n -v \"$AGENT_VOLUME_DIR\":/home/user/claude/processing \\\n -e CLAUDE_CODE_OAUTH_TOKEN=\"$CLAUDE_CODE_OAUTH_TOKEN\" \\\n claude_image:0.0.1\n\necho \"Switching shell into container and starting up claude\"\ndocker exec -it claude-container bash ./entrypoint.sh\n\n\n\n\n\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":520,"content_sha256":"90f3796a09df23f220741f2ae493b2d33f44e04c85bb5658534ce20f6810d785"},{"filename":"steering/accuracy.md","content":"# Solr to OpenSearch Migration Accuracy\n\nAccuracy is the top priority in this migration. Correctness always takes precedence over speed, brevity, or convenience.\n\nWhen translating Solr constructs to OpenSearch equivalents, never guess or approximate. If a mapping is uncertain, say so explicitly. A wrong answer is worse than no answer.\n\n## Rules\n\n- Verify every query translation produces semantically equivalent results before presenting it. Behavior must match, not just syntax.\n- Flag any Solr feature that has no direct OpenSearch equivalent rather than silently substituting a close-but-different alternative.\n- Do not omit edge cases. If a translation works in most cases but breaks under specific conditions (e.g., null values, multi-valued fields, nested docs), document those conditions.\n- Prefer explicit over implicit. If OpenSearch has a default that differs from Solr's default, call it out.\n- When in doubt about a mapping, consult `references/01-schema-migration.md`, `steering/incompatibilities.md`, and `references/02-query-translation.md` before responding.\n- Do not conflate similar-but-different concepts (e.g., Solr `fq` caching behavior vs. OpenSearch `filter` context — functionally similar, but caching mechanics differ).\n- If a user's Solr query or schema cannot be accurately migrated with the current information available, say so and ask for clarification rather than producing a best-guess output.\n\n## What Accuracy Means Here\n\n- Query translation: the OpenSearch query must return the same documents in the same relevance order as the Solr query, given the same index data.\n- Schema mapping: field types, analysis chains, and storage settings must preserve the same indexing and retrieval behavior.\n- Sizing: estimates must be clearly labeled as estimates with stated assumptions, not presented as exact values.\n- Incompatibilities: must be surfaced proactively, not discovered after implementation.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1933,"content_sha256":"9d683899021e4194d002f5f94b17eaca66a611f653445367beabdfdbaa94213c"},{"filename":"steering/authentication.md","content":"# Solr to Opensearch Authentication Steering\n\nFor details mentioned here, make sure to include relevant parts in the response where suitable.\n\n## Kerberos Authentication Notes\n- as noted in kerberos reference for opensearch (link: `https://docs.opensearch.org/latest/security/authentication-backends/kerberos/`) reference:\n ```text\n Because of security restrictions, the keytab and krb5.conf files must be placed in the config directory or its \n subdirectory, and their paths in opensearch.yml must be relative, not absolute.\n ```\n Make this very clear in migration suggestions.\n\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":585,"content_sha256":"68469d8ccc90fa354ac606dcf0314f23be66920c265fbe2967fa58149342ab56"},{"filename":"steering/incompatibilities.md","content":"# Solr to OpenSearch Incompatibilities Steering\n\nEvery incompatibility found must be recorded in `SessionState.incompatibilities` before proceeding. Never silently skip a known issue. When in doubt, flag it — a false positive is less harmful than a missed breaking change.\n\n## Incompatibilities\n\n- **Custom plugins** (**Breaking**): Solr custom `RequestHandler` and `SearchComponent` Java plugins have no direct equivalent. Rebuild logic using OpenSearch Search Pipelines (2.9+), Ingest Pipelines, or client-side code.\n- **Cross-collection joins** (**Breaking**): Solr `{!join fromIndex=...}` cross-collection joins are not supported. Denormalize at index time or use application-side two-query joins.\n- **Trie field types** (**Breaking**): `TrieIntField`, `TrieLongField`, `TrieFloatField`, `TrieDoubleField` have no OpenSearch equivalent. Map to `integer`, `long`, `float`, `double` and cast values to native JSON numbers.\n- **Function queries** (**Warning**): Solr function queries (`recip`, `log`, `product`, `bf`) map to OpenSearch `function_score` with `script_score` or built-in functions, but syntax differs significantly.\n- **eDisMax `pf`/`pf2`/`pf3`/`mm`/`tie`** (**Warning**): These eDisMax parameters have no direct OpenSearch equivalents. Approximate with `multi_match` (`cross_fields`, `best_fields`) and `bool` query combinations; validate result parity.\n- **Dynamic fields** (**Warning**): Solr `dynamicField` patterns map to OpenSearch `dynamic_templates`. Behavior is similar but rule syntax differs — review every pattern.\n- **Nested / block join docs** (**Warning**): Solr `_childDocuments_` (block join) maps to OpenSearch `nested` type or `join` field type. Query syntax is completely different; parent-child queries must be rewritten.\n- **Spatial fields** (**Warning**): Solr `LatLonPointSpatialField` and `SpatialRecursivePrefixTreeFieldType` map to `geo_point` and `geo_shape`. Convert `\"lat,lon\"` strings to `{\"lat\": ..., \"lon\": ...}` objects.\n- **Date math syntax** (**Warning**): Solr date math (`NOW-1DAY/DAY`) differs from OpenSearch (`now-1d/d`). Translate all date math expressions in queries and range filters.\n- **Default query operator** (**Warning**): Solr defaults to `OR`; OpenSearch `query_string` defaults to `OR` but `match` uses `OR` — verify `minimum_should_match` and `operator` settings match intended behavior.\n- **Similarity / scoring** (**Info**): Both default to BM25 since Solr 7 / OpenSearch 1.0, but parameter defaults differ. If custom similarity was configured in `schema.xml`, replicate it via `similarity` settings in the OpenSearch mapping.\n- **ZooKeeper removed** (**Info**): SolrCloud requires an external ZooKeeper ensemble. OpenSearch uses a built-in cluster manager — decommission ZooKeeper after migration.\n\n## What Counts as a Breaking Incompatibility\n\n- A Solr feature used in production that has no functional OpenSearch equivalent.\n- A query that cannot be translated without changing result semantics.\n- A field type that requires data transformation before indexing.\n- Any plugin or custom handler that must be rebuilt before the application can go live.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3132,"content_sha256":"f728915955a48b29ae7bc23450d32d37636c0555a6fd161ce950daf8870a1639"},{"filename":"steering/sizing.md","content":"# Solr to OpenSearch Cluster Sizing\n\nSizing estimates must always be labeled as estimates with stated assumptions. Never present a sizing recommendation as exact. When in doubt, size up — under-provisioning is a harder problem to fix than over-provisioning.\n\n## Rules\n\n- **Shard size target:** Keep each shard between 10 GB and 50 GB. Shards outside this range should be split or merged before migration.\n- **Shard count formula:** `primary_shards = ceil(expected_index_size_GB / target_shard_size_GB)`. Use the Solr shard count as a baseline, but recalculate — Solr collections are often over-sharded.\n- **Replica default:** Use `number_of_replicas: 1` (2 total copies) for production. Set replicas to 0 during bulk load, then restore after indexing completes.\n- **JVM heap:** Set `-Xms` equal to `-Xmx`. Never exceed 50% of available RAM or 32 GB (above 32 GB, JVM compressed OOPs are disabled). Recommended range: 16–31 GB for data nodes.\n- **Node resource baseline:** Start with a 1:1 CPU/RAM mapping from Solr nodes to OpenSearch data nodes as a minimum. Adjust based on measured workload.\n- **Storage formula:** `total_storage = raw_data_size × (1 + replicas) × 1.3 overhead × 1.25 headroom`. Use NVMe SSD for data nodes.\n- **Cluster manager nodes:** Deploy 3 dedicated cluster manager nodes (odd number for quorum). These replace the SolrCloud ZooKeeper ensemble — no external ZooKeeper is needed.\n- **Coordinating nodes:** Add ≥ 2 coordinating-only nodes behind a load balancer for production search workloads to offload scatter-gather from data nodes.\n- **Hot-warm tiering:** Tag nodes with `node.attr.temp: hot/warm` and use Index State Management (ISM) to move older indices to warm nodes — equivalent to Solr PULL replicas on slower hardware.\n- **Disk watermarks:** Alert at 75% disk usage; OpenSearch stops allocating shards at 85% and blocks writes at 90%. Size storage to stay below 75% under normal load.\n\n## What Counts as a Sizing Error\n\n- Recommending a shard size outside the 10–50 GB range without explicit justification.\n- Setting JVM heap above 32 GB or above 50% of available RAM.\n- Proposing fewer than 3 cluster manager nodes in a production cluster.\n- Presenting a storage or shard estimate without stating the assumptions behind it.\n- Ignoring replica count when calculating total storage requirements.\n\nFlag any of the above before presenting a sizing recommendation to the user.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2426,"content_sha256":"837b09bc0bc562e64c5e32392baf2838d71eedf5928fe365e7f265d67c9840e1"},{"filename":"steering/stakeholders.md","content":"# Stakeholders\n\n## Search Relevance Engineer\n\nFocuses on search quality, schema design, and query behavior. Cares about field types, analyzers, tokenizers, synonym handling, scoring (BM25, LTR), Query DSL translation, and precision/recall outcomes. Expects deep technical detail on schema mappings and query conversion.\n\n## DevOps / Platform Engineer\n\nFocuses on infrastructure, deployment, and operations. Cares about cluster sizing, shard strategy, JVM heap, node roles, index lifecycle, monitoring, and cost. Expects guidance on architecture decisions and operational best practices for Amazon OpenSearch Service.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":617,"content_sha256":"d9da25565ab6eff329c12d958fa9aff504819a77abd4f00798f8d665c7111b05"},{"filename":"steering/transformation-rules.md","content":"# Data Transformation Rules: Solr to OpenSearch\n\nApply these rules to every document during migration. Do not skip a rule because a field looks correct — validate each transformation explicitly.\n\n## Rules\n\n- **Timestamps:** Convert all Solr date fields (ISO-8601 strings, e.g. `2024-01-15T10:30:00Z`) to `epoch_millis` (long integer). Map the OpenSearch field type to `\"date\"` with `\"format\": \"epoch_millis\"`. Never store date strings in a `date` field.\n- **Trie / numeric fields:** Cast values from Solr Trie types (`TrieIntField`, `TrieLongField`, `TrieFloatField`, `TrieDoubleField`) to native JSON numbers. Remove any string encoding. Map to OpenSearch `integer`, `long`, `float`, or `double` accordingly.\n- **String-encoded numbers:** If a field is typed as numeric in the schema but arrives as a string (e.g. `\"price\": \"29.99\"`), coerce to the correct numeric type before indexing. Reject documents where coercion fails rather than indexing a null.\n- **Multi-value fields:** Solr multi-valued fields arrive as arrays. Preserve array structure in OpenSearch. Never flatten a multi-value field to a single value — this causes silent data loss.\n- **Booleans:** Normalize to JSON `true`/`false`. Reject string variants (`\"yes\"`, `\"1\"`, `\"TRUE\"`) — coerce them to boolean before indexing.\n- **Geo fields:** Convert Solr `LatLonPointSpatialField` string format (`\"lat,lon\"`) to an OpenSearch `geo_point` object: `{\"lat\": \u003cfloat>, \"lon\": \u003cfloat>}`. Map the field type to `\"geo_point\"`.\n- **Field names with dots:** Solr allows dots in field names; OpenSearch interprets dots as object path separators. Replace dots with underscores (e.g. `product.id` → `product_id`) and update the mapping accordingly.\n- **Solr internal fields:** Strip `_version_`, `_root_`, and `_nest_path_` from every document before indexing. These fields are Solr internals with no OpenSearch equivalent and will cause mapping conflicts.\n- **Document identity:** Use the Solr `uniqueKey` field value as the OpenSearch `_id`. Set it explicitly on every index request to preserve upsert behavior. Do not rely on auto-generated IDs unless the source collection had no `uniqueKey`.\n- **Text cleanup:** Strip residual HTML/XML markup from text fields unless the application intentionally stores markup. Normalize whitespace (collapse runs of spaces/tabs/newlines to a single space, trim leading/trailing whitespace).\n\n## What Counts as a Transformation Error\n\n- A date field indexed as a string in a `date`-typed mapping.\n- A numeric field indexed as a string in a numeric-typed mapping.\n- A multi-value field silently truncated to one value.\n- A document indexed with a Solr internal field present.\n- A geo field stored as a `\"lat,lon\"` string in a `geo_point` mapping.\n\nFlag any of the above as a **Breaking** incompatibility and surface it to the user before proceeding.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2849,"content_sha256":"71cf9d102e5bfc3859d487d4d43a3d16e1dbed72dbb513a7380076d8af444e37"},{"filename":"tests/__init__.py","content":"","content_type":"text/x-python; charset=utf-8","language":"python","size":0,"content_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},{"filename":"tests/evals/claude_requests.py","content":"from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage\nimport os\n\nscript_path = \"/\".join(os.path.realpath(__file__).split(\"/\")[:-1])\ncwd = f\"{script_path}/../../../solr-opensearch-migration-advisor\"\n\n\nasync def call_api(prompt: str, options: dict, context: dict) -> dict:\n # check if test has continue flag set, and only then continue sessions\n continue_conversation = (context.get(\"test\", {}).get(\"metadata\", {}).get(\"continue\", False))\n agent_options = ClaudeAgentOptions(\n # picks up most revent conversation (allows sequential tests, would fail on parallelized)\n continue_conversation=continue_conversation,\n allowed_tools=[\"Read\", \"Edit\", \"Glob\", \"Grep\", \"Skill\", \"WebFetch\"], # Tools Claude can use\n permission_mode=\"acceptEdits\", # Auto-approve file edits\n setting_sources=[\"project\"], # for paths: https://platform.claude.com/docs/en/agent-sdk/skills\n effort=\"medium\",\n cwd=cwd\n )\n async for message in query(\n prompt=prompt,\n options=agent_options\n ):\n if isinstance(message, ResultMessage):\n return {\"output\": message.result}\n","content_type":"text/x-python; charset=utf-8","language":"python","size":1161,"content_sha256":"a8dd30fa725701720761ed7eba5b0affc8ed6db7b65312c4096bfd08aa981cca"},{"filename":"tests/evals/eval.yaml","content":"description: \"Config for claude agent based migration advisor tests\"\n\nproviders:\n - id: 'file://claude_requests.py'\n label: 'Claude agent invocation'\n config:\n timeout: 3000000\n\ndefaultTest:\n options:\n provider:\n id: bedrock:us.anthropic.claude-sonnet-4-5-20250929-v1:0\n config:\n region: us-east-1\n max_tokens: 512\n # Prohibit error messages 'Could not extract JSON from llm-rubric response':\n # https://github.com/promptfoo/promptfoo/issues/2084\n response_format: json_object\n\nprompts:\n - |-\n Do only stick to resources within the the directory set as your cwd, but ignore all resources under the test folder.\n Load the skill in .claude/skills/migration-advisor (if you have not already done so) before answering the following query: \n {{query}}\n\ntests:\n\n # Note the sequence of continue: False to start new session, and continue: True if building on the previous prompts\n - description: \"Ensure the agent asks for the specific role of the user.\"\n metadata:\n continue: false # flag on / off if the last used session should be picked up or not\n vars:\n query: |\n Help me migrate my solr system to opensearch.\n assert:\n - type: llm-rubric\n value: >-\n Response clarifies the actual role of the user. Provides 2 explicit role examples to choose from:\n search relevance engineer and devops / platform engineer.\n metric: clarify-role\n\n - description: \"Ask for Solr version\"\n metadata:\n continue: true # flag on / off if the last used session should be picked up or not\n vars:\n query: |\n To your previous question: Search Engineer is the correct role.\n assert:\n - type: llm-rubric\n value: >-\n Response asks for the Solr version the user is migrating from.\n metric: clarify-solr-version\n\n - description: \"Provide Solr version 6 and expect version acknowledgement and schema request\"\n metadata:\n continue: true\n vars:\n query: |\n We are migrating from Solr 6.6.\n assert:\n - type: llm-rubric\n value: >-\n Response acknowledges Solr version 6.6 response then asks the user to provide their Solr schema.\n metric: solr-version-acknowledged\n - description: \"Provide Solr 6 schema and verify OpenSearch mapping is generated correctly\"\n metadata:\n continue: true\n vars:\n query: |\n Here is our Solr 6 schema:\n\n \u003c?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n \u003cschema name=\"example-solr6\" version=\"1.6\">\n \u003cfields>\n \u003cfield name=\"id\" type=\"string\" indexed=\"true\" stored=\"true\" required=\"true\" multiValued=\"false\" />\n \u003cfield name=\"product_name\" type=\"text_general\" indexed=\"true\" stored=\"true\" />\n \u003cfield name=\"price\" type=\"currency\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"priority\" type=\"priority_enum\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"_version_\" type=\"long\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"copy_source\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"copy_target\" type=\"text_general\" indexed=\"true\" stored=\"false\"/>\n \u003ccopyField source=\"copy_source\" dest=\"copy_target\"/>\n \u003cfield name=\"collation_field\" type=\"coll_de\" indexed=\"true\" stored=\"false\"/>\n \u003c/fields>\n \u003cuniqueKey>id\u003c/uniqueKey>\n \u003ctypes>\n \u003cfieldType name=\"string\" class=\"solr.StrField\" sortMissingLast=\"true\" />\n \u003cfieldType name=\"long\" class=\"solr.TrieLongField\" precisionStep=\"0\" positionIncrementGap=\"0\"/>\n \u003cfieldType name=\"text_general\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n \u003canalyzer>\n \u003ctokenizer class=\"solr.StandardTokenizerFactory\"/>\n \u003cfilter class=\"solr.LowerCaseFilterFactory\"/>\n \u003c/analyzer>\n \u003c/fieldType>\n \u003cfieldType name=\"currency\" class=\"solr.CurrencyField\"\n precisionStep=\"8\"\n defaultCurrency=\"USD\"\n currencyConfig=\"currency.xml\" />\n \u003cfieldType name=\"priority_enum\" class=\"solr.EnumField\"\n enumsConfig=\"enumsConfig.xml\"\n enumName=\"priority\" />\n \u003cfieldType name=\"coll_de\"\n class=\"solr.ICUCollationField\"\n locale=\"de@collation=phonebook\"\n strength=\"primary\"\n caseLevel=\"false\"/>\n \u003c/types>\n \u003c/schema>\n assert:\n - type: llm-rubric\n value: >-\n Response produces a valid OpenSearch index mapping JSON that satisfies all of the following:\n (0) '_version_' is not included in the OpenSearch mapping.\n (1) 'id' is mapped to type 'keyword';\n (2) 'product_name' is mapped to type 'text' with a standard or equivalent analyzer;\n (3) The response should note that the TrieLongField type has no direct OpenSearch equivalent,\n recommending and using the `long` field type instead;\n (4) 'price' (solr.CurrencyField) is flagged as unsupported in OpenSearch.\n (5) 'priority' (solr.EnumField) is flagged as unsupported in OpenSearch.\n (6) 'collationField' (solr.ICUCollationField) is flagged as unsupported in OpenSearch.\n (7) 'copyField' conversion via `copy_to`.\n (8) the response notes the TF-IDF to BM25 similarity change as a behavioral incompatibility\n given the source is Solr 6.x.\n metric: solr6-schema-mapping-correct\n\n - description: \"Translate Solr queries to OpenSearch\"\n metadata:\n continue: true\n vars:\n query: |\n My Solr queries to translate are:\n Query #1: title:opensearch AND price:[10 TO 100]\n Query #2: defType=edismax&q=the dark knight&qf=title^10 tagline^2 overview^1 cast^0.5&pf=title^20 overview^5&mm=2\u003c-25%\n assert:\n - type: llm-rubric\n value: >-\n Response translates Query #1 to an OpenSearch bool query with a must clause containing\n a match on the title field for \"opensearch\" and a range on the price field with gte 10 and lte 100.\n metric: query1-bool-match-range\n\n - type: llm-rubric\n value: >-\n Response translates Query #2 using multi_match with type best_fields across the fields\n title (boost 10), tagline (boost 2), overview (boost 1), and cast (boost 0.5).\n metric: query2-multimatch-qf-fields\n\n - type: llm-rubric\n value: >-\n Response translates Query #2 with phrase boost (pf) clauses for title (boost 20) and\n overview (boost 5) expressed as multi_match with type phrase in a should clause.\n metric: query2-pf-phrase-boost\n\n - type: llm-rubric\n value: >-\n Response translates Query #2 with minimum_should_match set to reflect the mm value of \"2\u003c-25%\".\n metric: query2-mm-minimum-should-match\n\n - type: llm-rubric\n value: >-\n Response notes that eDisMax pf phrase boost fields have no direct OpenSearch equivalent\n and that the translation is a behavioral approximation.\n metric: query2-edismax-behavioral-note\n\n - description: \"Provide full migration plan for search engineer\"\n metadata:\n continue: true\n vars:\n query: |\n On top of that, the operational setup consists of 3 nodes with 8gb ram (4gb heap) and 4 cores each, 2 shards, \n 50M docs, 10GB index. \n Provide me with the final migration plan, given all so far provided information.\n assert:\n - type: llm-rubric\n value: >-\n Response contains a full solr to opensearch migration plan for the role of Search Engineer, \n covering the main steps and for migrating Solr 6.6. \n It should be suitable for the following Solr 6 Schema:\n \u003c?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n \u003cschema name=\"example-solr6\" version=\"1.6\">\n \u003cfields>\n \u003cfield name=\"id\" type=\"string\" indexed=\"true\" stored=\"true\" required=\"true\" multiValued=\"false\" />\n \u003cfield name=\"product_name\" type=\"text_general\" indexed=\"true\" stored=\"true\" />\n \u003cfield name=\"price\" type=\"currency\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"priority\" type=\"priority_enum\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"_version_\" type=\"long\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"copy_source\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"copy_target\" type=\"text_general\" indexed=\"true\" stored=\"false\"/>\n \u003ccopyField source=\"copy_source\" dest=\"copy_target\"/>\n \u003cfield name=\"collation_field\" type=\"coll_de\" indexed=\"true\" stored=\"false\"/>\n \u003c/fields>\n \u003cuniqueKey>id\u003c/uniqueKey>\n \u003ctypes>\n \u003cfieldType name=\"string\" class=\"solr.StrField\" sortMissingLast=\"true\" />\n \u003cfieldType name=\"long\" class=\"solr.TrieLongField\" precisionStep=\"0\" positionIncrementGap=\"0\"/>\n \u003cfieldType name=\"text_general\" class=\"solr.TextField\" positionIncrementGap=\"100\">\n \u003canalyzer>\n \u003ctokenizer class=\"solr.StandardTokenizerFactory\"/>\n \u003cfilter class=\"solr.LowerCaseFilterFactory\"/>\n \u003c/analyzer>\n \u003c/fieldType>\n \u003cfieldType name=\"currency\" class=\"solr.CurrencyField\"\n precisionStep=\"8\"\n defaultCurrency=\"USD\"\n currencyConfig=\"currency.xml\" />\n \u003cfieldType name=\"priority_enum\" class=\"solr.EnumField\"\n enumsConfig=\"enumsConfig.xml\"\n enumName=\"priority\" />\n \u003cfieldType name=\"coll_de\"\n class=\"solr.ICUCollationField\"\n locale=\"de@collation=phonebook\"\n strength=\"primary\"\n caseLevel=\"false\"/>\n \u003c/types>\n \u003c/schema>\n \n It should also incorporate ops considerations based on an initial solr hardware setup of 3 nodes with 8gb ram \n (4gb heap) and 4 cores each, 2 shards, 50M docs, 10GB index.\n It also contains dedicated sections on incompatibilities, milestones, blockers and costs. Under the following\n bullet points you find some facts that should be mentioned under the respective sections:\n \n # Incompatibilities:\n - Trie field types (Solr) need mapping to Point types (OpenSearch)\n - Relevance scores will differ between Solr (TF-IDF) and OpenSearch (BM25) without additional changes\n - Do not migrate the ` _version_` field to the OpenSearch index mapping\n - `solr.EnumField` unsupported\n - `solr.ICUCollationField` unsupported\n - Fields marked `stored=\"true\"` but `indexed=\"false\"` in Solr may behave differently under `_source` filtering\n in OpenSearch\n - eDismax `pf` phrase boost field with no direct equivalent; approximate with `multi_match` type `phrase` in a `should` clause\n \n # Milestones\n - Section provides plausible milestones where steps build logically on top of each other and later steps \n do not come before other steps that are logically reqiured before.\n - Since the role asking for the report is a Search Engineer, check if the report leads with full \n incompatibility list and query translation details and contains the previously passed query translations\n as examples for translated queries:\n Query #1: title:opensearch AND price:[10 TO 100]\n Query #2: defType=edismax&q=the dark knight&qf=title^10 tagline^2 overview^1 cast^0.5&pf=title^20 overview^5&mm=2\u003c-25%\n \n # Blockers\n - Section describing expectable difficulties or stoppers when migrating Solr 6.6 to OpenSearch.\n \n # Cost\n - Reasonable cost estimates for infrastructure, effort, and required tooling changes where applicable.\n metric: generate-full-report\n - description: \"Provide full migration plan for Devops Engineer\"\n metadata:\n continue: true\n vars:\n query: |\n Now provide the migration plan assuming I would be a Devops Engineer.\n assert:\n - type: llm-rubric\n value: >-\n Response contains a full solr to opensearch migration plan for the role of DevOps Engineer, \n covering the main steps and for migrating Solr 6.6. \n The plan should emphasize the following points:\n - Breaking issues that could cause index creation or reindex failures\n - required cluster level changes\n - emphasizes the ops side of things over the search relevancy topics\n - emphasizes resource intensive queries and infrastructure implications\n - emphasizes authentication, authorization, and operational constraints\n - gives details regarding the following aspects: instance types, storage (EBS vs. instance store), \n node roles (data, coordinating, cluster manager), auto-scaling, monitoring, and deployment automation\n \n Regarding the migration plan outline, the response should reflect the following:\n - lead with the cluster sizing recommendation and infrastructure plan\n - most prominent sections: deployment sequencing and operational runbook\n metric: generate-full-report\n - description: \"Translate SearchHandler to OpenSearch equivalent\"\n metadata:\n continue: false\n vars:\n query: |\n I am a Search Engineer and for a solr to opensearch migration I need to migrate the following requestHandler\n to its opensearch equivalent. Give an example json that can directly be used in\n PUT requests against opensearch PUT _scripts/[template-name] endpoints.\n Assume the template will be available as search template with name `my-search-template`.\n Give examples how to invoke it against an existing index of name `my-index`.\n\n \u003crequestHandler name=\"/custom-search\" class=\"solr.SearchHandler\">\n \u003clst name=\"defaults\">\n \u003cstr name=\"defType\">edismax\u003c/str>\n \u003cstr name=\"qf\">title^2.0 content^1.0 author^1.5\u003c/str>\n \u003cstr name=\"pf\">title^3.0 content^1.5\u003c/str>\n \u003cstr name=\"mm\">75%\u003c/str>\n \u003cint name=\"rows\">10\u003c/int>\n \u003cstr name=\"fl\">id,title,author,score\u003c/str>\n \u003cstr name=\"wt\">json\u003c/str>\n\n \u003c!-- Highlighting -->\n \u003cstr name=\"hl\">true\u003c/str>\n \u003cstr name=\"hl.fl\">title,content\u003c/str>\n \u003cstr name=\"hl.snippets\">3\u003c/str>\n \u003cstr name=\"hl.fragsize\">150\u003c/str>\n\n \u003c!-- Faceting -->\n \u003cstr name=\"facet\">true\u003c/str>\n \u003cstr name=\"facet.field\">category\u003c/str>\n \u003cstr name=\"facet.field\">author\u003c/str>\n \u003cstr name=\"facet.mincount\">1\u003c/str>\n \u003c/lst>\n \u003c/requestHandler>\n assert:\n - type: llm-rubric\n value: >-\n Give your evaluation response in json format (!!), and properly escape chars where this is needed.\n The response contains a search template json payload equivalent to the following:\n ```json\n {\n \"script\": {\n \"lang\": \"mustache\",\n \"source\": {\n \"size\": {{ \"{{size}}{{^size}}10{{/size}}\" }},\n \"_source\": [\"id\", \"title\", \"author\"],\n \"query\": {\n \"bool\": {\n \"must\": [\n {\n \"multi_match\": {\n \"query\": \"{{query_string}}\",\n \"type\": \"best_fields\",\n \"fields\": [\"title^2.0\", \"content^1.0\", \"author^1.5\"],\n \"minimum_should_match\": \"75%\"\n }\n }\n ],\n \"should\": [\n {\n \"match_phrase\": {\n \"title\": {\n \"query\": \"{{query_string}}\",\n \"boost\": 3.0\n }\n }\n },\n {\n \"match_phrase\": {\n \"content\": {\n \"query\": \"{{query_string}}\",\n \"boost\": 1.5\n }\n }\n }\n ]\n }\n },\n \"highlight\": {\n \"fields\": {\n \"title\": { \"number_of_fragments\": 3, \"fragment_size\": 150 },\n \"content\": { \"number_of_fragments\": 3, \"fragment_size\": 150 }\n }\n },\n \"aggs\": {\n \"category\": {\n \"terms\": { \"field\": \"category\", \"min_doc_count\": 1 }\n },\n \"author\": {\n \"terms\": { \"field\": \"author\", \"min_doc_count\": 1 }\n }\n }\n }\n }\n }\n ```\n\n Be lenient on the following deviations:\n - It is ok if the should block condenses the multiple match_phrase blocks to a single multi_match block\n of type 'phrase' and a 'fields' attribute containing title and content field with the respective boosts.\n - For the naming of facet aggregations, both the field names as well as field names with some facet-indicating\n pre- or suffix are valid.\n - If 'from' attribute is contained and contains a 0 as default value, that is fine, too.\n\n Further, the response shall contain an example search request formulated as POST request\n to the endpoint `my-index/_search/template` with json payload parameters\n id: \"my-search-template\" and a params section with query_string parameter and\n optionally size, as in the following example, where parameter values are solely\n examples and can be different in the response, with `size` as optional parameter:\n\n POST my-index/_search/template\n ```json\n {\n \"id\": \"my-search-template\",\n \"params\": {\n \"query_string\": \"my query\",\n \"size\": 10\n }\n }\n ```\n\n The response should also explicitly summarize the mappings utilized to reflect the example solr edismax\n query in opensearch. Basic facts highlighted should match the ones given below:\n\n ┌──────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────┐\n │ Solr (SearchHandler) │ OpenSearch Equivalent │\n ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤\n │ defType=edismax │ multi_match with best_fields │\n ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤\n │ qf=title^2.0 content^1.0 author^1.5 │ \"fields\": [\"title^2.0\", \"content^1.0\", \"author^1.5\"] │\n ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤\n │ pf=title^3.0 content^1.5 │ match_phrase in bool.should with boosts │\n ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤\n │ mm=75% │ \"minimum_should_match\": \"75%\" │\n ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤\n │ rows=10 │ \"size\": 10 │\n ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤\n │ fl=id,title,author,score │ \"_source\": [\"id\",\"title\",\"author\"] (score is always returned in _score) │\n ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤\n │ hl=true, hl.fl, hl.snippets, hl.fragsize │ \"highlight\" with number_of_fragments and fragment_size │\n ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤\n │ facet.field=category,author │ \"aggs\" with terms aggregation │\n ├──────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────┤\n metric: assess-customizations-search-handler\n - description: \"Migrate Kerberos auth\"\n metadata:\n continue: false\n vars:\n query: |\n I am a Devops Engineer and want to migrate solr to opensearch.\n In our current solr setup we use kerberos authentication. Help me migrate this authentication to opensearch.\n assert:\n - type: llm-rubric\n value: >-\n Give your evaluation response in json format, and properly escape chars where this is needed.\n\n The answer mentions a range of steps to configure kerberos in opensearch, representing the following steps:\n\n ### Opensearch Node Configuration Steps:\n 1) Create a service principal and keytab for your OpenSearch nodes.\n 2) Within opensearch.yml:\n Configure the following attributes (the response should also include descriptions of the attributes similar to what is provided below):\n - `plugins.security.kerberos.krb5_filepath`: The path to your Kerberos configuration file.\n - `plugins.security.kerberos.acceptor_keytab_filepath`: The path to the keytab file, which contains the principal that the Security plugin uses to issue requests through Kerberos.\n - `plugins.security.kerberos.acceptor_principal`: The principal that the Security plugin uses to issue requests through Kerberos. This value must be present in the keytab file.\n\n The response should highlight that both keytab and krb5 config file must be placed in the `config` directory of opensearch or a subdirectory.\n Further the response should mention that their paths in opensearch.yml must be relative, not absolute.\n\n ### Cluster security configuration steps:\n 1) Configure Kerberos authentication domain in the config.yml.\n An example is given below. The specific settings might differ, but ensure its a valid configuration for a Kerberos authentication domain:\n ```yaml\n kerberos_auth_domain:\n enabled: true\n order: 1\n http_authenticator:\n type: kerberos\n challenge: true\n config:\n krb_debug: false\n strip_realm_from_principal: true\n authentication_backend:\n type: noop\n ```\n metric: assess-customizations-kerberos-auth-auth\n - description: \"Ask nonexistent feature\"\n metadata:\n continue: false\n vars:\n query: |\n I am a Search Engineer and want to migrate solr to opensearch.\n Id like to migrate the solr feature of wormhole vectors to opensearch, help me with that.\n assert:\n - type: llm-rubric\n value: >-\n The response should clearly state that no such feature exists and might ask for clarification.\n metric: accuracy-no-hallucination\n - description: \"Maps libraries correctly\"\n metadata:\n continue: false\n vars:\n query: |\n I am a Search Engineer and want to migrate solr to opensearch.\n We are using the libraries SolrJ and pysolr. Help me migrating those.\n assert:\n - type: llm-rubric\n value: >-\n The response should mention the following replacements:\n - opensearch-java as solrJ replacement\n - opensearch-py as pysolr replacement\n metric: map-client-integrations\n - type: icontains-all\n value:\n - opensearch-java\n - opensearch-py\n metric: map-client-integrations\n - description: \"Translate cluster sizing\"\n metadata:\n continue: false\n vars:\n query: |\n I am a DevOps engineer. We are currently running a solr setup consisting of the following:\n 3-node SolrCloud with 8gb ram (4gb heap) and 4 cores each, 2 shards, 50M docs, 10GB index. Help me migrate this to respective infra needed to run this\n migrated to OpenSearch.\n assert:\n - type: llm-rubric\n value: >-\n Response shall be close to the following recommendations:\n - 2 - 4 OpenSearch shards\n - replicas: 1\n - 3 OpenSearch nodes with 8gb - 16gb ram and 4 cores each\n - 4gb - 8gb jvm heap per node\n - SolrCloud setup needs migration to OpenSearch ClusterManager\n - if ZooKeeper was mentioned as SolrCloud dependency, that is not valid for OpenSearch, this dependency is not needed anymore\n metric: sizing-recommendation\n - description: \"Resumes session 1\"\n metadata:\n continue: false\n vars:\n query: |\n I am a Search Relevancy Engineer. We want to migrate our system based on Solr 8.0 to OpenSearch.\n We are using the TMDB schema. And my name is Peter Parker. Remember this session as 'my-test-session'\n - description: \"Resume session 2\"\n metadata:\n continue: false\n vars:\n query: |\n Hi there, please resume the session 'my-test-session' and fill me in on already stated facts about my role,\n used solr version, schema used, and my name :).\n assert:\n - type: llm-rubric\n value: >-\n Response shall state the following facts:\n - Role is Search Relevancy Engineer\n - Solr is Solr 8.0\n - Schema is the TMDB schema\n - Name is Peter Parker\n metric: session-resumption\n - description: \"Resumes session 3 - Clear session\"\n metadata:\n continue: false\n vars:\n query: |\n Hi there, please clear the session state for the session name 'my-test-session'.\n - description: \"Resumes session 4 - No session resumption after clearance\"\n metadata:\n continue: false\n vars:\n query: |\n Hi there, please resume the session 'my-test-session' and fill me in on already stated facts about my role,\n used solr version, schema used, and my name :).\n assert:\n - type: llm-rubric\n value: >-\n Response shall not contain any of the following facts:\n - Role is Search Relevancy Engineer\n - Solr is Solr 8.0\n - Schema is the TMDB schema\n - Name is Peter Parker\n The response should state that no session with name 'my-test-session' exists.","content_type":"application/yaml; charset=utf-8","language":"yaml","size":29728,"content_sha256":"358ae5f3b9221282ff1f712e80c9d25bc23da40e90465adc163a0b05c68c6a20"},{"filename":"tests/evals/README.md","content":"### Eval Notes\n\n- General docs:\n - python sdk: https://platform.claude.com/docs/en/agent-sdk/quickstart#python-(uv)\n - listing of session handling: https://platform.claude.com/docs/en/agent-sdk/sessions\n- Claude usage requires api token.\n If you have a claude code subscription plan, you can just create a token there via:\n - `claude setup-token`\n - `export CLAUDE_CODE_OAUTH_TOKEN=\u003ctoken>` (or put in .env file, will be picked up by the claude_requests.py)\n - see also `https://github.com/anthropics/claude-agent-sdk-python/issues/559`\n- Sequential tests need setting of `continue=true` metadata, otherwise each request starts a new session.\n- for reading Skills from filesystem, agent needs corresponding settings on client init: `https://platform.claude.com/docs/en/agent-sdk/skills`\n - `setting_sources=[\"user\", \"project\"]`\n - adding \"Skills\" to `allowed_tools` setting\n- setting project in above setting_resources should allow Claude to look within configured cwd for folder `.claude/skills/` with SKILL.md,\n no clear way to configure the path to custom as in our case\n- there could be possibility for renaming sessions to make them usable in tests, check `https://github.com/anthropics/claude-code/issues/2112`\n\n\n### PYTHON ENV SETUP\n- Run uv sync with eval extras: `uv sync --extra eval` \n- `source .venv/bin/activate`\n- copy the `.env.example` file to `.env` file and fill in properties\n- run evals: `./scripts/run_evals.sh`\n\n\n### PROMPTFOO LLM JUDGE PARSING ERRORS\n- sometimes you might see the error message \"Could not extract JSON from llm-rubric response\". This means that\n the judge does not return valid json. To alleviate this two steps:\n - prefix `Give your evaluation response in json format, and properly escape chars where this is needed.` to the rubric\n - add response_format in llm judge config, such as:\n ```yaml\n defaultTest:\n options:\n provider:\n id: bedrock:us.anthropic.claude-sonnet-4-5-20250929-v1:0\n config:\n region: us-east-1\n max_tokens: 256\n # Prohibit error messages 'Could not extract JSON from llm-rubric response':\n # https://github.com/promptfoo/promptfoo/issues/2084\n response_format: json_object\n ```\n\n\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2226,"content_sha256":"aceded9aa997e6870ada72e2eca6f26ae118192c8f14c5d0dac86fdc32b5fd12"},{"filename":"tests/scripts/run_evals.sh","content":"#!/bin/bash\n\nset -o pipefail\n\nSCRIPT_DIR=\"$(dirname \"$0\")\"\nsource \"$SCRIPT_DIR\"/../../.env\n\n# creating symlink to path where claude sdk expects the skills\nmkdir -p \"$SCRIPT_DIR\"/../../.claude/skills/migration-advisor\nln -s \"$SCRIPT_DIR\"/../../SKILL.md \"$SCRIPT_DIR\"/../../.claude/skills/migration-advisor/SKILL.md\n\nAWS_REGION=$AWS_REGION \\\nAWS_DEFAULT_REGION=$AWS_DEFAULT_REGION \\\nAWS_BEARER_TOKEN_BEDROCK=$AWS_BEARER_TOKEN_BEDROCK \\\nBEDROCK_INFERENCE_PROFILE_ARN=$BEDROCK_INFERENCE_PROFILE_ARN \\\npromptfoo eval -c $SCRIPT_DIR/../evals/eval.yaml --no-cache --max-concurrency 1","content_type":"application/x-sh; charset=utf-8","language":"bash","size":576,"content_sha256":"872ebb4a1f970fb4a444905b20b8847b0d024168fe418a25400ccc9d741071f9"},{"filename":"tests/test_query_converter.py","content":"\"\"\"Tests for query_converter.py\"\"\"\nimport sys\nimport os\nimport pytest\n\nsys.path.insert(0, os.path.join(os.path.dirname(__file__), \"..\", \"scripts\"))\nfrom query_converter import QueryConverter, _unwrap_parens, _split_boolean\n\n\[email protected]\ndef converter():\n return QueryConverter()\n\n\ndef test_match_all(converter):\n result = converter.convert(\"*:*\")\n assert result == {\"query\": {\"match_all\": {}}}\n\n\ndef test_match_all_bare_star(converter):\n result = converter.convert(\"*\")\n assert result == {\"query\": {\"match_all\": {}}}\n\n\ndef test_field_value_match(converter):\n result = converter.convert(\"title:opensearch\")\n assert result == {\"query\": {\"match\": {\"title\": \"opensearch\"}}}\n\n\ndef test_phrase_query(converter):\n result = converter.convert('title:\"hello world\"')\n assert result == {\"query\": {\"match_phrase\": {\"title\": \"hello world\"}}}\n\n\ndef test_wildcard_query(converter):\n result = converter.convert(\"title:open*\")\n assert result[\"query\"][\"wildcard\"][\"title\"] == \"open*\"\n\n\ndef test_range_inclusive(converter):\n result = converter.convert(\"price:[10 TO 100]\")\n assert result == {\"query\": {\"range\": {\"price\": {\"gte\": 10, \"lte\": 100}}}}\n\n\ndef test_range_exclusive(converter):\n result = converter.convert(\"price:{10 TO 100}\")\n assert result == {\"query\": {\"range\": {\"price\": {\"gt\": 10, \"lt\": 100}}}}\n\n\ndef test_range_open_high(converter):\n result = converter.convert(\"year:[2020 TO *]\")\n assert result == {\"query\": {\"range\": {\"year\": {\"gte\": 2020}}}}\n\n\ndef test_range_open_low(converter):\n result = converter.convert(\"year:[* TO 2024]\")\n assert result == {\"query\": {\"range\": {\"year\": {\"lte\": 2024}}}}\n\n\ndef test_boolean_and(converter):\n result = converter.convert(\"title:search AND category:docs\")\n assert result[\"query\"][\"bool\"][\"must\"] == [\n {\"match\": {\"title\": \"search\"}},\n {\"match\": {\"category\": \"docs\"}},\n ]\n\n\ndef test_boolean_or(converter):\n result = converter.convert(\"title:search OR title:find\")\n should = result[\"query\"][\"bool\"][\"should\"]\n assert {\"match\": {\"title\": \"search\"}} in should\n assert {\"match\": {\"title\": \"find\"}} in should\n assert result[\"query\"][\"bool\"][\"minimum_should_match\"] == 1\n\n\ndef test_not_prefix(converter):\n result = converter.convert(\"NOT status:deleted\")\n assert result == {\"query\": {\"bool\": {\"must_not\": [{\"match\": {\"status\": \"deleted\"}}]}}}\n\n\ndef test_required_prohibited_prefix(converter):\n result = converter.convert(\"+title:search -status:deleted\")\n bool_q = result[\"query\"][\"bool\"]\n assert {\"match\": {\"title\": \"search\"}} in bool_q[\"must\"]\n assert {\"match\": {\"status\": \"deleted\"}} in bool_q[\"must_not\"]\n\n\ndef test_bare_term_falls_back_to_query_string(converter):\n result = converter.convert(\"opensearch\")\n assert result == {\"query\": {\"query_string\": {\"query\": \"opensearch\"}}}\n\n\ndef test_boost_stripped(converter):\n result = converter.convert(\"title:search^2\")\n assert result == {\"query\": {\"match\": {\"title\": \"search\"}}}\n\n\ndef test_empty_query_raises(converter):\n with pytest.raises(ValueError):\n converter.convert(\"\")\n\n\ndef test_whitespace_only_raises(converter):\n with pytest.raises(ValueError):\n converter.convert(\" \")\n\n\ndef test_unwrap_parens_simple():\n assert _unwrap_parens(\"(hello)\") == \"hello\"\n\n\ndef test_unwrap_parens_no_outer():\n assert _unwrap_parens(\"(a) OR (b)\") == \"(a) OR (b)\"\n\n\ndef test_split_boolean_and():\n op, parts = _split_boolean(\"a:1 AND b:2\")\n assert op == \"AND\"\n assert parts == [\"a:1\", \"b:2\"]\n\n\ndef test_split_boolean_or():\n op, parts = _split_boolean(\"a:1 OR b:2\")\n assert op == \"OR\"\n assert parts == [\"a:1\", \"b:2\"]\n\n\ndef test_split_boolean_none():\n assert _split_boolean(\"a:1\") is None\n\n\n# ---------------------------------------------------------------------------\n# convert_edismax tests\n# ---------------------------------------------------------------------------\n\ndef test_edismax_empty_query_raises(converter):\n with pytest.raises(ValueError):\n converter.convert_edismax(\"\")\n\n\ndef test_edismax_whitespace_query_raises(converter):\n with pytest.raises(ValueError):\n converter.convert_edismax(\" \")\n\n\ndef test_edismax_q_only_no_qf(converter):\n # Without qf, falls back to standard query conversion\n result = converter.convert_edismax(\"opensearch\")\n assert result == {\"query\": {\"query_string\": {\"query\": \"opensearch\"}}}\n\n\ndef test_edismax_qf_single_field(converter):\n result = converter.convert_edismax(\"opensearch\", qf=\"title\")\n mm = result[\"query\"][\"multi_match\"]\n assert mm[\"query\"] == \"opensearch\"\n assert mm[\"fields\"] == [\"title\"]\n assert mm[\"type\"] == \"best_fields\"\n\n\ndef test_edismax_qf_multiple_fields_with_boosts(converter):\n result = converter.convert_edismax(\"search\", qf=\"title^2 body^0.5 description\")\n mm = result[\"query\"][\"multi_match\"]\n assert mm[\"fields\"] == [\"title^2\", \"body^0.5\", \"description\"]\n\n\ndef test_edismax_tie(converter):\n result = converter.convert_edismax(\"search\", qf=\"title body\", tie=0.3)\n assert result[\"query\"][\"multi_match\"][\"tie_breaker\"] == 0.3\n\n\ndef test_edismax_qs_slop(converter):\n result = converter.convert_edismax(\"search\", qf=\"title body\", qs=2)\n assert result[\"query\"][\"multi_match\"][\"slop\"] == 2\n\n\ndef test_edismax_mm_with_qf(converter):\n result = converter.convert_edismax(\"search\", qf=\"title body\", mm=\"75%\")\n bool_q = result[\"query\"][\"bool\"]\n assert bool_q[\"minimum_should_match\"] == \"75%\"\n assert any(\"multi_match\" in c for c in bool_q[\"must\"])\n\n\ndef test_edismax_mm_without_qf_wraps_in_bool(converter):\n result = converter.convert_edismax(\"title:search\", mm=\"2\")\n bool_q = result[\"query\"][\"bool\"]\n assert bool_q[\"minimum_should_match\"] == \"2\"\n assert {\"match\": {\"title\": \"search\"}} in bool_q[\"must\"]\n\n\ndef test_edismax_pf_adds_phrase_should(converter):\n result = converter.convert_edismax(\"hello world\", qf=\"title body\", pf=\"title^1.5\")\n bool_q = result[\"query\"][\"bool\"]\n phrase_clause = bool_q[\"should\"][0][\"multi_match\"]\n assert phrase_clause[\"type\"] == \"phrase\"\n assert phrase_clause[\"query\"] == \"hello world\"\n assert \"title^1.5\" in phrase_clause[\"fields\"]\n\n\ndef test_edismax_pf2_adds_phrase_should(converter):\n result = converter.convert_edismax(\"hello world\", qf=\"title\", pf2=\"title body\")\n should = result[\"query\"][\"bool\"][\"should\"]\n assert any(c[\"multi_match\"][\"type\"] == \"phrase\" for c in should)\n\n\ndef test_edismax_pf3_adds_phrase_should(converter):\n result = converter.convert_edismax(\"hello world\", qf=\"title\", pf3=\"title\")\n should = result[\"query\"][\"bool\"][\"should\"]\n assert any(c[\"multi_match\"][\"type\"] == \"phrase\" for c in should)\n\n\ndef test_edismax_ps_slop_on_phrase_clauses(converter):\n result = converter.convert_edismax(\"hello world\", qf=\"title\", pf=\"title body\", ps=3)\n should = result[\"query\"][\"bool\"][\"should\"]\n phrase_clauses = [c for c in should if c[\"multi_match\"][\"type\"] == \"phrase\"]\n assert all(c[\"multi_match\"][\"slop\"] == 3 for c in phrase_clauses)\n\n\ndef test_edismax_pf_pf2_pf3_all_present(converter):\n result = converter.convert_edismax(\n \"hello world\", qf=\"title\", pf=\"title\", pf2=\"body\", pf3=\"description\"\n )\n should = result[\"query\"][\"bool\"][\"should\"]\n # One phrase clause per pf/pf2/pf3\n phrase_clauses = [c for c in should if c[\"multi_match\"][\"type\"] == \"phrase\"]\n assert len(phrase_clauses) == 3\n\n\ndef test_edismax_bq_string(converter):\n result = converter.convert_edismax(\"search\", qf=\"title\", bq=\"category:docs\")\n should = result[\"query\"][\"bool\"][\"should\"]\n assert {\"match\": {\"category\": \"docs\"}} in should\n\n\ndef test_edismax_bq_list(converter):\n result = converter.convert_edismax(\n \"search\", qf=\"title\", bq=[\"category:docs\", \"status:published\"]\n )\n should = result[\"query\"][\"bool\"][\"should\"]\n assert {\"match\": {\"category\": \"docs\"}} in should\n assert {\"match\": {\"status\": \"published\"}} in should\n\n\ndef test_edismax_bf_wraps_in_script_score(converter):\n result = converter.convert_edismax(\"search\", qf=\"title\", bf=\"log(popularity)\")\n ss = result[\"query\"][\"script_score\"]\n assert ss[\"script\"][\"source\"] == \"log(popularity)\"\n # Inner query should still be the assembled bool/multi_match\n assert \"multi_match\" in ss[\"query\"] or \"bool\" in ss[\"query\"]\n\n\ndef test_edismax_bf_with_pf_wraps_bool_in_script_score(converter):\n result = converter.convert_edismax(\n \"search\", qf=\"title\", pf=\"title\", bf=\"doc['rank'].value\"\n )\n ss = result[\"query\"][\"script_score\"]\n assert ss[\"script\"][\"source\"] == \"doc['rank'].value\"\n assert \"bool\" in ss[\"query\"]\n\n\ndef test_edismax_combined_params(converter):\n # Smoke test: all major params together\n result = converter.convert_edismax(\n \"hello world\",\n qf=\"title^2 body\",\n mm=\"1\",\n pf=\"title^1.5\",\n ps=2,\n tie=0.1,\n bq=\"featured:true\",\n )\n bool_q = result[\"query\"][\"bool\"]\n assert bool_q[\"minimum_should_match\"] == \"1\"\n main = bool_q[\"must\"][0][\"multi_match\"]\n assert main[\"tie_breaker\"] == 0.1\n assert main[\"fields\"] == [\"title^2\", \"body\"]\n should = bool_q[\"should\"]\n phrase_clauses = [c for c in should if \"multi_match\" in c and c[\"multi_match\"][\"type\"] == \"phrase\"]\n assert len(phrase_clauses) == 1\n assert phrase_clauses[0][\"multi_match\"][\"slop\"] == 2\n assert {\"match\": {\"featured\": \"true\"}} in should\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9377,"content_sha256":"380f41b9efa173ca11c27ee530b198e73588ce3ca39911b7fa55530f8667f283"},{"filename":"tests/test_report.py","content":"\"\"\"Tests for report.py\"\"\"\nimport sys\nimport os\n\nsys.path.insert(0, os.path.join(os.path.dirname(__file__), \"..\", \"scripts\"))\nfrom report import MigrationReport\nfrom storage import Incompatibility, ClientIntegration\n\n\ndef test_generate_contains_header():\n assert \"# Solr to OpenSearch Migration Report\" in MigrationReport().generate()\n\n\ndef test_generate_milestones():\n output = MigrationReport(milestones=[\"Step A\", \"Step B\"]).generate()\n assert \"1. Step A\" in output\n assert \"2. Step B\" in output\n\n\ndef test_generate_blockers():\n output = MigrationReport(blockers=[\"Blocker 1\", \"Blocker 2\"]).generate()\n assert \"- Blocker 1\" in output\n assert \"- Blocker 2\" in output\n\n\ndef test_generate_no_blockers_default_message():\n assert \"No immediate blockers identified.\" in MigrationReport().generate()\n\n\ndef test_generate_implementation_points():\n output = MigrationReport(implementation_points=[\"Do X\", \"Do Y\"]).generate()\n assert \"- Do X\" in output\n assert \"- Do Y\" in output\n\n\ndef test_generate_cost_estimates():\n output = MigrationReport(cost_estimates={\"Infrastructure\": \"$500/mo\"}).generate()\n assert \"Infrastructure\" in output\n assert \"$500/mo\" in output\n\n\ndef test_generate_no_cost_estimates_default_message():\n assert \"TBD\" in MigrationReport().generate()\n\n\ndef test_generate_all_sections_present():\n output = MigrationReport().generate()\n assert \"## Incompatibilities\" in output\n assert \"## Client & Front-end Impact\" in output\n assert \"## Major Milestones\" in output\n assert \"## Potential Blockers\" in output\n assert \"## Implementation Points\" in output\n assert \"## Cost Estimates\" in output\n\n\ndef test_generate_returns_string():\n assert isinstance(MigrationReport(milestones=[\"m1\"], blockers=[\"b1\"]).generate(), str)\n\n\n# ---------------------------------------------------------------------------\n# Incompatibilities section\n# ---------------------------------------------------------------------------\n\ndef test_incompatibilities_section_no_items():\n output = MigrationReport().generate()\n assert \"No incompatibilities identified.\" in output\n\n\ndef test_incompatibilities_breaking_appears_first():\n incs = [\n Incompatibility(\"query\", \"Behavioral\", \"BM25 vs TF-IDF\", \"Configure similarity\"),\n Incompatibility(\"schema\", \"Breaking\", \"copyField unsupported\", \"Use copy_to\"),\n ]\n output = MigrationReport(incompatibilities=incs).generate()\n breaking_pos = output.index(\"Breaking\")\n behavioral_pos = output.index(\"Behavioral\")\n assert breaking_pos \u003c behavioral_pos\n\n\ndef test_incompatibilities_unsupported_before_behavioral():\n incs = [\n Incompatibility(\"query\", \"Behavioral\", \"desc b\", \"rec b\"),\n Incompatibility(\"plugin\", \"Unsupported\", \"desc u\", \"rec u\"),\n ]\n output = MigrationReport(incompatibilities=incs).generate()\n assert output.index(\"Unsupported\") \u003c output.index(\"Behavioral\")\n\n\ndef test_incompatibilities_description_and_recommendation_present():\n incs = [Incompatibility(\"schema\", \"Breaking\", \"My description\", \"My recommendation\")]\n output = MigrationReport(incompatibilities=incs).generate()\n assert \"My description\" in output\n assert \"My recommendation\" in output\n\n\ndef test_incompatibilities_action_required_for_breaking():\n incs = [Incompatibility(\"schema\", \"Breaking\", \"desc\", \"rec\")]\n output = MigrationReport(incompatibilities=incs).generate()\n assert \"Action required\" in output\n\n\ndef test_incompatibilities_action_required_for_unsupported():\n incs = [Incompatibility(\"plugin\", \"Unsupported\", \"desc\", \"rec\")]\n output = MigrationReport(incompatibilities=incs).generate()\n assert \"Action required\" in output\n\n\ndef test_incompatibilities_no_action_required_for_behavioral_only():\n incs = [Incompatibility(\"query\", \"Behavioral\", \"desc\", \"rec\")]\n output = MigrationReport(incompatibilities=incs).generate()\n assert \"Action required\" not in output\n\n\ndef test_incompatibilities_category_shown():\n incs = [Incompatibility(\"mycat\", \"Behavioral\", \"desc\", \"rec\")]\n output = MigrationReport(incompatibilities=incs).generate()\n assert \"mycat\" in output\n\n\n# ---------------------------------------------------------------------------\n# Client & Front-end Impact section\n# ---------------------------------------------------------------------------\n\ndef test_client_section_no_integrations():\n output = MigrationReport().generate()\n assert \"No client or front-end integrations recorded.\" in output\n\n\ndef test_client_section_library_shown():\n clients = [ClientIntegration(\"SolrJ\", \"library\", \"Java search client\", \"Replace with opensearch-java\")]\n output = MigrationReport(client_integrations=clients).generate()\n assert \"Client Libraries\" in output\n assert \"SolrJ\" in output\n assert \"Replace with opensearch-java\" in output\n\n\ndef test_client_section_ui_shown():\n clients = [ClientIntegration(\"React Search UI\", \"ui\", \"Solr widgets\", \"Rewrite with OpenSearch JS client\")]\n output = MigrationReport(client_integrations=clients).generate()\n assert \"Front-end / UI\" in output\n assert \"React Search UI\" in output\n\n\ndef test_client_section_http_shown():\n clients = [ClientIntegration(\"Custom HTTP\", \"http\", \"Raw requests to /select\", \"Update endpoint to /_search\")]\n output = MigrationReport(client_integrations=clients).generate()\n assert \"HTTP / Custom Clients\" in output\n assert \"Custom HTTP\" in output\n\n\ndef test_client_section_notes_shown():\n clients = [ClientIntegration(\"pysolr\", \"library\", \"Python search client\", \"Replace with opensearch-py\")]\n output = MigrationReport(client_integrations=clients).generate()\n assert \"Python search client\" in output\n\n\ndef test_client_section_migration_action_shown():\n clients = [ClientIntegration(\"pysolr\", \"library\", \"\", \"Replace with opensearch-py\")]\n output = MigrationReport(client_integrations=clients).generate()\n assert \"Replace with opensearch-py\" in output\n\n\ndef test_client_section_multiple_kinds():\n clients = [\n ClientIntegration(\"SolrJ\", \"library\", \"\", \"Replace with opensearch-java\"),\n ClientIntegration(\"Velocity UI\", \"ui\", \"\", \"Rewrite with OpenSearch Dashboards\"),\n ]\n output = MigrationReport(client_integrations=clients).generate()\n assert \"Client Libraries\" in output\n assert \"Front-end / UI\" in output\n assert \"SolrJ\" in output\n assert \"Velocity UI\" in output\n\n\ndef test_client_section_ordering_library_before_ui():\n clients = [\n ClientIntegration(\"My UI\", \"ui\", \"\", \"action\"),\n ClientIntegration(\"My Lib\", \"library\", \"\", \"action\"),\n ]\n output = MigrationReport(client_integrations=clients).generate()\n assert output.index(\"Client Libraries\") \u003c output.index(\"Front-end / UI\")\n","content_type":"text/x-python; charset=utf-8","language":"python","size":6747,"content_sha256":"6d769756421c4c5294fca1a06a60718b709e8bd26eb6c95db68ad9d71482869e"},{"filename":"tests/test_schema_converter.py","content":"\"\"\"Tests for schema_converter.py\"\"\"\nimport sys\nimport os\nimport json\nimport pytest\n\nsys.path.insert(0, os.path.join(os.path.dirname(__file__), \"..\", \"scripts\"))\nfrom schema_converter import SchemaConverter, SOLR_TYPE_TO_OPENSEARCH\n\n\nSIMPLE_SCHEMA_XML = \"\"\"\u003c?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\u003cschema name=\"test\" version=\"1.6\">\n \u003cfieldType name=\"string\" class=\"solr.StrField\"/>\n \u003cfieldType name=\"text_general\" class=\"solr.TextField\"/>\n \u003cfieldType name=\"pint\" class=\"solr.IntPointField\"/>\n \u003cfieldType name=\"plong\" class=\"solr.LongPointField\"/>\n \u003cfieldType name=\"pfloat\" class=\"solr.FloatPointField\"/>\n \u003cfieldType name=\"pdouble\" class=\"solr.DoublePointField\"/>\n \u003cfieldType name=\"pdate\" class=\"solr.DatePointField\"/>\n \u003cfieldType name=\"boolean\" class=\"solr.BoolField\"/>\n \u003cfieldType name=\"location\" class=\"solr.LatLonPointSpatialField\"/>\n\n \u003cfield name=\"id\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"title\" type=\"text_general\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"count\" type=\"pint\" indexed=\"true\" stored=\"false\"/>\n \u003cfield name=\"score\" type=\"pfloat\" indexed=\"false\" stored=\"true\"/>\n \u003cfield name=\"active\" type=\"boolean\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"location\" type=\"location\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"_version_\" type=\"plong\" indexed=\"true\" stored=\"false\"/>\n\u003c/schema>\"\"\"\n\nDYNAMIC_FIELD_SCHEMA_XML = \"\"\"\u003c?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n\u003cschema name=\"test\" version=\"1.6\">\n \u003cfieldType name=\"string\" class=\"solr.StrField\"/>\n \u003cfieldType name=\"pint\" class=\"solr.IntPointField\"/>\n \u003cfield name=\"id\" type=\"string\"/>\n \u003cdynamicField name=\"*_i\" type=\"pint\"/>\n \u003cdynamicField name=\"*_s\" type=\"string\"/>\n\u003c/schema>\"\"\"\n\nSIMPLE_SCHEMA_JSON = json.dumps({\n \"schema\": {\n \"fieldTypes\": [\n {\"name\": \"string\", \"class\": \"solr.StrField\"},\n {\"name\": \"text_general\", \"class\": \"solr.TextField\"},\n {\"name\": \"plong\", \"class\": \"solr.LongPointField\"},\n ],\n \"fields\": [\n {\"name\": \"id\", \"type\": \"string\", \"indexed\": True, \"stored\": True},\n {\"name\": \"title\", \"type\": \"text_general\", \"indexed\": True, \"stored\": True},\n {\"name\": \"count\", \"type\": \"plong\", \"indexed\": True, \"stored\": False},\n {\"name\": \"_version_\", \"type\": \"plong\"},\n ],\n \"dynamicFields\": [\n {\"name\": \"*_s\", \"type\": \"string\"},\n ],\n }\n})\n\n\[email protected]\ndef converter():\n return SchemaConverter()\n\n\n# --- convert_xml ---\n\ndef test_xml_basic_field_types(converter):\n mapping = converter.convert_xml(SIMPLE_SCHEMA_XML)\n props = mapping[\"mappings\"][\"properties\"]\n assert props[\"id\"][\"type\"] == \"keyword\"\n assert props[\"title\"][\"type\"] == \"text\"\n assert props[\"count\"][\"type\"] == \"integer\"\n assert props[\"score\"][\"type\"] == \"float\"\n assert props[\"active\"][\"type\"] == \"boolean\"\n assert props[\"location\"][\"type\"] == \"geo_point\"\n\n\ndef test_xml_internal_fields_excluded(converter):\n mapping = converter.convert_xml(SIMPLE_SCHEMA_XML)\n assert \"_version_\" not in mapping[\"mappings\"][\"properties\"]\n\n\ndef test_xml_stored_false(converter):\n mapping = converter.convert_xml(SIMPLE_SCHEMA_XML)\n assert mapping[\"mappings\"][\"properties\"][\"count\"][\"store\"] is False\n\n\ndef test_xml_indexed_false(converter):\n mapping = converter.convert_xml(SIMPLE_SCHEMA_XML)\n assert mapping[\"mappings\"][\"properties\"][\"score\"][\"index\"] is False\n\n\ndef test_xml_dynamic_templates(converter):\n mapping = converter.convert_xml(DYNAMIC_FIELD_SCHEMA_XML)\n templates = mapping[\"mappings\"][\"dynamic_templates\"]\n names = [list(t.keys())[0] for t in templates]\n assert \"dynamic_i\" in names\n assert \"dynamic_s\" in names\n\n\ndef test_xml_invalid_raises(converter):\n with pytest.raises(ValueError, match=\"Invalid XML\"):\n converter.convert_xml(\"not xml at all \u003c\u003c\u003c\")\n\n\ndef test_xml_wrong_root_raises(converter):\n with pytest.raises(ValueError, match=\"Expected root element\"):\n converter.convert_xml(\"\u003cnotschema/>\")\n\n\n# --- convert_json ---\n\ndef test_json_basic_field_types(converter):\n mapping = converter.convert_json(SIMPLE_SCHEMA_JSON)\n props = mapping[\"mappings\"][\"properties\"]\n assert props[\"id\"][\"type\"] == \"keyword\"\n assert props[\"title\"][\"type\"] == \"text\"\n assert props[\"count\"][\"type\"] == \"long\"\n\n\ndef test_json_internal_fields_excluded(converter):\n mapping = converter.convert_json(SIMPLE_SCHEMA_JSON)\n assert \"_version_\" not in mapping[\"mappings\"][\"properties\"]\n\n\ndef test_json_stored_false(converter):\n mapping = converter.convert_json(SIMPLE_SCHEMA_JSON)\n assert mapping[\"mappings\"][\"properties\"][\"count\"][\"store\"] is False\n\n\ndef test_json_dynamic_templates(converter):\n mapping = converter.convert_json(SIMPLE_SCHEMA_JSON)\n templates = mapping[\"mappings\"][\"dynamic_templates\"]\n names = [list(t.keys())[0] for t in templates]\n assert \"dynamic_s\" in names\n\n\ndef test_json_invalid_raises(converter):\n with pytest.raises(ValueError, match=\"Invalid JSON\"):\n converter.convert_json(\"{bad json\")\n\n\ndef test_json_no_schema_wrapper(converter):\n # Schema API JSON without the outer \"schema\" key should still work.\n raw = json.dumps({\n \"fieldTypes\": [{\"name\": \"string\", \"class\": \"solr.StrField\"}],\n \"fields\": [{\"name\": \"title\", \"type\": \"string\"}],\n \"dynamicFields\": [],\n })\n mapping = converter.convert_json(raw)\n assert mapping[\"mappings\"][\"properties\"][\"title\"][\"type\"] == \"keyword\"\n\n\n# --- SOLR_TYPE_TO_OPENSEARCH coverage ---\n\ndef test_type_map_contains_common_types():\n assert SOLR_TYPE_TO_OPENSEARCH[\"solr.TextField\"] == \"text\"\n assert SOLR_TYPE_TO_OPENSEARCH[\"solr.StrField\"] == \"keyword\"\n assert SOLR_TYPE_TO_OPENSEARCH[\"solr.IntPointField\"] == \"integer\"\n assert SOLR_TYPE_TO_OPENSEARCH[\"solr.BoolField\"] == \"boolean\"\n assert SOLR_TYPE_TO_OPENSEARCH[\"solr.LatLonPointSpatialField\"] == \"geo_point\"\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5883,"content_sha256":"1be668654668ce9623d8eb8c46b00160a7fb67684c37c5cab3eac1bab9b95f11"},{"filename":"tests/test_skill.py","content":"\"\"\"Tests for skill.py\"\"\"\nimport sys\nimport os\nimport json\nimport pytest\n\nsys.path.insert(0, os.path.join(os.path.dirname(__file__), \"..\", \"scripts\"))\nfrom skill import SolrToOpenSearchMigrationSkill\nfrom storage import InMemoryStorage\n\n\[email protected]\ndef skill():\n return SolrToOpenSearchMigrationSkill(storage=InMemoryStorage())\n\n\nSIMPLE_SCHEMA_XML = \"\"\"\u003cschema name=\"test\" version=\"1.6\">\n \u003cfieldType name=\"string\" class=\"solr.StrField\"/>\n \u003cfield name=\"id\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n \u003cfield name=\"title\" type=\"string\" indexed=\"true\" stored=\"true\"/>\n\u003c/schema>\"\"\"\n\n\n# ---------------------------------------------------------------------------\n# convert_schema_xml\n# ---------------------------------------------------------------------------\n\ndef test_convert_schema_xml_returns_json(skill):\n result = skill.convert_schema_xml(SIMPLE_SCHEMA_XML)\n parsed = json.loads(result)\n assert \"mappings\" in parsed\n assert parsed[\"mappings\"][\"properties\"][\"id\"][\"type\"] == \"keyword\"\n\n\ndef test_convert_schema_xml_invalid_raises(skill):\n with pytest.raises(ValueError):\n skill.convert_schema_xml(\"not xml\")\n\n\n# ---------------------------------------------------------------------------\n# convert_schema_json\n# ---------------------------------------------------------------------------\n\ndef test_convert_schema_json_returns_json(skill):\n schema_json = json.dumps({\n \"schema\": {\n \"fieldTypes\": [{\"name\": \"string\", \"class\": \"solr.StrField\"}],\n \"fields\": [{\"name\": \"title\", \"type\": \"string\"}],\n }\n })\n result = skill.convert_schema_json(schema_json)\n parsed = json.loads(result)\n assert parsed[\"mappings\"][\"properties\"][\"title\"][\"type\"] == \"keyword\"\n\n\ndef test_convert_schema_json_invalid_raises(skill):\n with pytest.raises(ValueError):\n skill.convert_schema_json(\"{bad}\")\n\n\n# ---------------------------------------------------------------------------\n# convert_query\n# ---------------------------------------------------------------------------\n\ndef test_convert_query_match_all(skill):\n assert json.loads(skill.convert_query(\"*:*\")) == {\"query\": {\"match_all\": {}}}\n\n\ndef test_convert_query_field_value(skill):\n parsed = json.loads(skill.convert_query(\"title:opensearch\"))\n assert parsed[\"query\"][\"match\"][\"title\"] == \"opensearch\"\n\n\ndef test_convert_query_empty_raises(skill):\n with pytest.raises(ValueError):\n skill.convert_query(\"\")\n\n\n# ---------------------------------------------------------------------------\n# get_migration_checklist\n# ---------------------------------------------------------------------------\n\ndef test_get_migration_checklist_contains_sections(skill):\n checklist = skill.get_migration_checklist()\n assert \"PREPARATION\" in checklist\n assert \"SCHEMA\" in checklist\n assert \"QUERY MIGRATION\" in checklist\n assert \"CUTOVER\" in checklist\n\n\n# ---------------------------------------------------------------------------\n# get_field_type_mapping_reference\n# ---------------------------------------------------------------------------\n\ndef test_get_field_type_mapping_reference_is_markdown_table(skill):\n ref = skill.get_field_type_mapping_reference()\n assert \"| Solr Field Type | OpenSearch Type |\" in ref\n assert \"text\" in ref\n assert \"keyword\" in ref\n\n\n# ---------------------------------------------------------------------------\n# handle_message — routing\n# ---------------------------------------------------------------------------\n\ndef test_handle_message_schema_conversion(skill):\n response = skill.handle_message(f\"Please convert this schema: {SIMPLE_SCHEMA_XML}\", \"s1\")\n assert \"mappings\" in response or \"OpenSearch\" in response\n\n\ndef test_handle_message_query_translation(skill):\n response = skill.handle_message(\"translate query: title:opensearch\", \"s2\")\n assert \"match\" in response or \"OpenSearch\" in response\n\n\ndef test_handle_message_checklist(skill):\n response = skill.handle_message(\"show me the checklist\", \"s3\")\n assert \"PREPARATION\" in response or \"checklist\" in response.lower()\n\n\ndef test_handle_message_report(skill):\n response = skill.handle_message(\"generate report\", \"s4\")\n assert \"Migration Report\" in response\n\n\ndef test_handle_message_unknown_returns_greeting(skill):\n response = skill.handle_message(\"hello there\", \"s5\")\n assert len(response) > 0\n\n\n# ---------------------------------------------------------------------------\n# handle_message — session state persistence\n# ---------------------------------------------------------------------------\n\ndef test_handle_message_persists_history(skill):\n skill.handle_message(\"hello\", \"persist-test\")\n state = skill._storage.load(\"persist-test\")\n assert state is not None\n assert len(state.history) == 1\n assert state.history[0][\"user\"] == \"hello\"\n\n\ndef test_handle_message_schema_sets_fact_and_progress(skill):\n skill.handle_message(f\"convert: {SIMPLE_SCHEMA_XML}\", \"schema-session\")\n state = skill._storage.load(\"schema-session\")\n assert state.get_fact(\"schema_migrated\") is True\n assert state.progress >= 1\n\n\ndef test_handle_message_query_advances_progress(skill):\n skill.handle_message(\"translate query: title:test\", \"q-session\")\n state = skill._storage.load(\"q-session\")\n assert state.progress >= 3\n\n\ndef test_session_resumes_across_calls(skill):\n skill.handle_message(\"hello\", \"resume-test\")\n skill.handle_message(\"world\", \"resume-test\")\n state = skill._storage.load(\"resume-test\")\n assert len(state.history) == 2\n\n\n# ---------------------------------------------------------------------------\n# generate_report\n# ---------------------------------------------------------------------------\n\ndef test_generate_report_no_session(skill):\n report = skill.generate_report(\"empty-session\")\n assert \"Migration Report\" in report\n\n\ndef test_generate_report_flags_missing_schema(skill):\n report = skill.generate_report(\"no-schema-session\")\n assert \"Schema not yet analyzed\" in report\n\n\ndef test_generate_report_no_missing_schema_blocker_when_migrated(skill):\n state = skill._storage.load_or_new(\"migrated-session\")\n state.set_fact(\"schema_migrated\", True)\n skill._storage.save(state)\n report = skill.generate_report(\"migrated-session\")\n assert \"Schema not yet analyzed\" not in report\n\n\ndef test_generate_report_includes_incompatibilities(skill):\n state = skill._storage.load_or_new(\"inc-session\")\n state.add_incompatibility(\"schema\", \"Breaking\", \"copyField unsupported\", \"Use copy_to\")\n state.add_incompatibility(\"query\", \"Behavioral\", \"TF-IDF vs BM25\", \"Configure similarity\")\n skill._storage.save(state)\n report = skill.generate_report(\"inc-session\")\n assert \"Breaking\" in report\n assert \"copyField unsupported\" in report\n assert \"Behavioral\" in report\n assert \"TF-IDF vs BM25\" in report\n\n\ndef test_generate_report_breaking_incompatibility_appears_as_blocker(skill):\n state = skill._storage.load_or_new(\"blocker-session\")\n state.add_incompatibility(\"plugin\", \"Breaking\", \"Custom plugin X has no equivalent\", \"Rewrite\")\n skill._storage.save(state)\n report = skill.generate_report(\"blocker-session\")\n # Breaking items should appear in both the Incompatibilities section and Blockers\n assert \"Custom plugin X has no equivalent\" in report\n assert \"Potential Blockers\" in report\n\n\ndef test_generate_report_includes_customizations(skill):\n state = skill._storage.load_or_new(\"custom-session\")\n state.set_fact(\"customizations\", {\"Custom SearchHandler\": \"Use Search API\"})\n skill._storage.save(state)\n report = skill.generate_report(\"custom-session\")\n assert \"Custom SearchHandler\" in report\n assert \"Use Search API\" in report\n\n\ndef test_generate_report_no_incompatibilities_message(skill):\n report = skill.generate_report(\"clean-session\")\n assert \"No incompatibilities identified.\" in report\n\n\n# ---------------------------------------------------------------------------\n# generate_report — client integrations\n# ---------------------------------------------------------------------------\n\ndef test_generate_report_no_client_integrations_message(skill):\n report = skill.generate_report(\"no-clients-session\")\n assert \"No client or front-end integrations recorded.\" in report\n\n\ndef test_generate_report_includes_client_integrations(skill):\n state = skill._storage.load_or_new(\"client-session\")\n state.add_client_integration(\"SolrJ\", \"library\", \"Java client\", \"Replace with opensearch-java\")\n state.add_client_integration(\"React UI\", \"ui\", \"Solr widgets\", \"Rewrite with OpenSearch JS\")\n skill._storage.save(state)\n report = skill.generate_report(\"client-session\")\n assert \"SolrJ\" in report\n assert \"Replace with opensearch-java\" in report\n assert \"React UI\" in report\n assert \"Rewrite with OpenSearch JS\" in report\n\n\ndef test_generate_report_client_section_present(skill):\n report = skill.generate_report(\"section-check\")\n assert \"## Client & Front-end Impact\" in report\n","content_type":"text/x-python; charset=utf-8","language":"python","size":9020,"content_sha256":"948a826b7abb3e4f658c9e32319dc55a10074d7da834d65480d3063c3674644d"},{"filename":"tests/test_storage.py","content":"\"\"\"Tests for storage.py — SessionState, InMemoryStorage, FileStorage.\"\"\"\nimport sys\nimport os\nimport pytest\n\nsys.path.insert(0, os.path.join(os.path.dirname(__file__), \"..\", \"scripts\"))\nfrom storage import (\n SessionState,\n Incompatibility,\n ClientIntegration,\n StorageBackend,\n StorageInterface,\n InMemoryStorage,\n FileStorage,\n)\n\n\n# ---------------------------------------------------------------------------\n# SessionState\n# ---------------------------------------------------------------------------\n\ndef test_session_state_new():\n s = SessionState.new(\"s1\")\n assert s.session_id == \"s1\"\n assert s.history == []\n assert s.facts == {}\n assert s.progress == 0\n assert s.incompatibilities == []\n\n\ndef test_session_state_append_turn():\n s = SessionState.new(\"s1\")\n s.append_turn(\"hello\", \"hi\")\n assert s.history == [{\"user\": \"hello\", \"assistant\": \"hi\"}]\n\n\ndef test_session_state_set_and_get_fact():\n s = SessionState.new(\"s1\")\n s.set_fact(\"schema_migrated\", True)\n assert s.get_fact(\"schema_migrated\") is True\n assert s.get_fact(\"missing\", \"default\") == \"default\"\n\n\ndef test_session_state_advance_progress():\n s = SessionState.new(\"s1\")\n s.advance_progress(3)\n assert s.progress == 3\n s.advance_progress(2) # should not go backwards\n assert s.progress == 3\n s.advance_progress(5)\n assert s.progress == 5\n\n\ndef test_session_state_add_incompatibility():\n s = SessionState.new(\"s1\")\n s.add_incompatibility(\"schema\", \"Breaking\", \"copyField not supported\", \"Use copy_to\")\n assert len(s.incompatibilities) == 1\n assert s.incompatibilities[0].severity == \"Breaking\"\n\n\ndef test_session_state_add_incompatibility_no_duplicates():\n s = SessionState.new(\"s1\")\n s.add_incompatibility(\"schema\", \"Breaking\", \"desc\", \"rec\")\n s.add_incompatibility(\"schema\", \"Breaking\", \"desc\", \"rec\")\n assert len(s.incompatibilities) == 1\n\n\ndef test_session_state_roundtrip():\n s = SessionState.new(\"abc\")\n s.append_turn(\"u\", \"a\")\n s.set_fact(\"key\", 42)\n s.advance_progress(2)\n s.add_incompatibility(\"query\", \"Behavioral\", \"TF-IDF vs BM25\", \"Configure similarity\")\n s.add_client_integration(\"SolrJ\", \"library\", \"Java search client\", \"Replace with opensearch-java\")\n d = s.to_dict()\n s2 = SessionState.from_dict(d)\n assert s2.session_id == \"abc\"\n assert s2.history == [{\"user\": \"u\", \"assistant\": \"a\"}]\n assert s2.facts == {\"key\": 42}\n assert s2.progress == 2\n assert len(s2.incompatibilities) == 1\n assert s2.incompatibilities[0].category == \"query\"\n assert len(s2.client_integrations) == 1\n assert s2.client_integrations[0].name == \"SolrJ\"\n\n\n# ---------------------------------------------------------------------------\n# ClientIntegration\n# ---------------------------------------------------------------------------\n\ndef test_client_integration_roundtrip():\n c = ClientIntegration(\"pysolr\", \"library\", \"Python client\", \"Replace with opensearch-py\")\n assert ClientIntegration.from_dict(c.to_dict()) == c\n\n\ndef test_session_state_add_client_integration():\n s = SessionState.new(\"s1\")\n s.add_client_integration(\"SolrJ\", \"library\", \"Java client\", \"Replace with opensearch-java\")\n assert len(s.client_integrations) == 1\n assert s.client_integrations[0].kind == \"library\"\n\n\ndef test_session_state_add_client_integration_no_duplicates():\n s = SessionState.new(\"s1\")\n s.add_client_integration(\"SolrJ\", \"library\", \"notes\", \"action\")\n s.add_client_integration(\"SolrJ\", \"library\", \"notes\", \"action\")\n assert len(s.client_integrations) == 1\n\n\n# ---------------------------------------------------------------------------\n# Incompatibility\n# ---------------------------------------------------------------------------\n\ndef test_incompatibility_roundtrip():\n i = Incompatibility(\"schema\", \"Breaking\", \"desc\", \"rec\")\n assert Incompatibility.from_dict(i.to_dict()) == i\n\n\n# ---------------------------------------------------------------------------\n# InMemoryStorage\n# ---------------------------------------------------------------------------\n\[email protected]\ndef mem():\n return InMemoryStorage()\n\n\ndef test_inmemory_load_or_new_creates_blank(mem):\n s = mem.load_or_new(\"new-session\")\n assert s.session_id == \"new-session\"\n assert s.history == []\n\n\ndef test_inmemory_save_and_load(mem):\n s = SessionState.new(\"s1\")\n s.set_fact(\"x\", 1)\n mem.save(s)\n loaded = mem.load(\"s1\")\n assert loaded.get_fact(\"x\") == 1\n\n\ndef test_inmemory_load_nonexistent_returns_none(mem):\n assert mem.load(\"ghost\") is None\n\n\ndef test_inmemory_list_sessions(mem):\n mem.save(SessionState.new(\"a\"))\n mem.save(SessionState.new(\"b\"))\n assert set(mem.list_sessions()) == {\"a\", \"b\"}\n\n\ndef test_inmemory_delete(mem):\n mem.save(SessionState.new(\"s1\"))\n mem.delete(\"s1\")\n assert mem.load(\"s1\") is None\n\n\ndef test_inmemory_delete_nonexistent_is_safe(mem):\n mem.delete(\"ghost\") # should not raise\n\n\ndef test_inmemory_is_storage_backend():\n assert issubclass(InMemoryStorage, StorageBackend)\n\n\n# ---------------------------------------------------------------------------\n# FileStorage\n# ---------------------------------------------------------------------------\n\[email protected]\ndef file_storage(tmp_path):\n return FileStorage(base_path=str(tmp_path))\n\n\ndef test_file_save_and_load(file_storage):\n s = SessionState.new(\"s1\")\n s.set_fact(\"migrated\", True)\n file_storage.save(s)\n loaded = file_storage.load(\"s1\")\n assert loaded.get_fact(\"migrated\") is True\n\n\ndef test_file_load_nonexistent_returns_none(file_storage):\n assert file_storage.load(\"ghost\") is None\n\n\ndef test_file_load_or_new(file_storage):\n s = file_storage.load_or_new(\"brand-new\")\n assert s.session_id == \"brand-new\"\n\n\ndef test_file_list_sessions(file_storage):\n file_storage.save(SessionState.new(\"x\"))\n file_storage.save(SessionState.new(\"y\"))\n assert set(file_storage.list_sessions()) == {\"x\", \"y\"}\n\n\ndef test_file_delete(file_storage):\n file_storage.save(SessionState.new(\"s1\"))\n file_storage.delete(\"s1\")\n assert file_storage.load(\"s1\") is None\n\n\ndef test_file_delete_nonexistent_is_safe(file_storage):\n file_storage.delete(\"ghost\") # should not raise\n\n\ndef test_file_creates_directory(tmp_path):\n new_dir = str(tmp_path / \"nested\" / \"sessions\")\n FileStorage(base_path=new_dir)\n assert os.path.exists(new_dir)\n\n\ndef test_file_is_storage_backend():\n assert issubclass(FileStorage, StorageBackend)\n\n\ndef test_file_persists_incompatibilities(file_storage):\n s = SessionState.new(\"s1\")\n s.add_incompatibility(\"schema\", \"Breaking\", \"desc\", \"rec\")\n file_storage.save(s)\n loaded = file_storage.load(\"s1\")\n assert len(loaded.incompatibilities) == 1\n assert loaded.incompatibilities[0].severity == \"Breaking\"\n\n\ndef test_file_persists_client_integrations(file_storage):\n s = SessionState.new(\"s1\")\n s.add_client_integration(\"SolrJ\", \"library\", \"Java client\", \"Replace with opensearch-java\")\n file_storage.save(s)\n loaded = file_storage.load(\"s1\")\n assert len(loaded.client_integrations) == 1\n assert loaded.client_integrations[0].name == \"SolrJ\"\n\n\ndef test_file_overwrites_on_save(file_storage):\n s = SessionState.new(\"s1\")\n s.set_fact(\"v\", 1)\n file_storage.save(s)\n s.set_fact(\"v\", 2)\n file_storage.save(s)\n assert file_storage.load(\"s1\").get_fact(\"v\") == 2\n\n\n# ---------------------------------------------------------------------------\n# Backwards-compatibility shim\n# ---------------------------------------------------------------------------\n\ndef test_storage_interface_is_subclass_of_backend():\n assert issubclass(StorageInterface, StorageBackend)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":7699,"content_sha256":"c642ddb408b11fa7934c8fae8c1fced4bbdd3e818aa3383a7c061b43b1eb8deb"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Apache Solr to OpenSearch Migration Advisor","type":"text"}]},{"type":"paragraph","content":[{"text":"An agent skill for migrating from Apache Solr to OpenSearch. This skill provides a transport-agnostic migration advisor that can reason about Solr query behavior, configuration, and cluster architecture.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"When to Use","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill when:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A user needs to migrate a Solr collection or SolrCloud deployment to OpenSearch.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A user wants a comprehensive migration advisor that can handle conversational interaction and maintain session context.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A user has a ","type":"text"},{"text":"schema.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" or Solr Schema API JSON document and needs an equivalent OpenSearch index mapping.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A user has Solr query strings and needs them translated to OpenSearch Query DSL.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A user needs a migration report covering milestones, blockers, and cost estimates.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A user has questions about Amazon OpenSearch Service features, regional availability, or AWS best practices.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A user has questions about migrating authentication from Solr to OpenSearch.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Trigger phrases:","type":"text","marks":[{"type":"strong"}]},{"text":" \"migrate from Solr\", \"convert Solr schema\", \"translate Solr query\", \"Solr to OpenSearch\", \"migration advisor\", \"migration report\", \"OpenSearch best practices\", \"AWS OpenSearch Service\".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"AWS Knowledge Integration","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill integrates with the ","type":"text"},{"text":"AWS Knowledge MCP Server","type":"text","marks":[{"type":"link","attrs":{"href":"https://awslabs.github.io/mcp/servers/aws-knowledge-mcp-server/","title":null}}]},{"text":" (","type":"text"},{"text":"https://knowledge-mcp.global.api.aws","type":"text","marks":[{"type":"code_inline"}]},{"text":") to provide accurate, up-to-date information about:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Amazon OpenSearch Service features and configuration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OpenSearch regional availability across AWS regions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"AWS best practices for search workloads","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Current AWS documentation and API references","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"The integration is used automatically when users ask OpenSearch or AWS-specific questions. Two dedicated MCP tools are also exposed:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"aws_knowledge_search(query, topic)","type":"text","marks":[{"type":"code_inline"}]},{"text":" — search AWS docs for any AWS/OpenSearch topic","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"aws_opensearch_regional_availability(region)","type":"text","marks":[{"type":"code_inline"}]},{"text":" — check OpenSearch Service regional availability","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"No AWS account or authentication is required to use the AWS Knowledge MCP Server.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Migration Workflow","type":"text"}]},{"type":"paragraph","content":[{"text":"Walk the user through each step in order. Do not skip ahead — complete each step before moving to the next.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 0 — Stakeholder Identification","type":"text"}]},{"type":"paragraph","content":[{"text":"Before diving into the migration, identify who you are working with so you can tailor the depth and focus of your guidance throughout the conversation.","type":"text"}]},{"type":"paragraph","content":[{"text":"Prompt the user with:","type":"text"}]},{"type":"paragraph","content":[{"text":"\"Welcome to the Solr to OpenSearch Migration Advisor. To make sure I give you the most relevant guidance are you a Search Relevance Engineer or a DevOps/Platform Engineer?\"","type":"text","marks":[{"type":"em"}]}]},{"type":"paragraph","content":[{"text":"Use the stakeholder definitions in the ","type":"text"},{"text":"Stakeholders","type":"text","marks":[{"type":"strong"}]},{"text":" steering document to interpret their answer. If the user describes a role that doesn't map cleanly to one of the defined roles, pick the closest match and confirm it with them.","type":"text"}]},{"type":"paragraph","content":[{"text":"Once the role is identified:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Store it in the session under ","type":"text"},{"text":"facts.stakeholder_role","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Briefly acknowledge the role and explain how you'll tailor the session. For example:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A ","type":"text"},{"text":"Search Relevance Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" gets full technical depth on schema, analyzers, and Query DSL. Search Relevance Engineers are typically interested in topics like BM25, Learning to Rank (LTR), NLP, query intent, precision and recall, and ranking and scoring.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"A ","type":"text"},{"text":"DevOps / Platform Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" gets emphasis on cluster sizing, deployment, and operations.","type":"text"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"Move to Step 1.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1 — Solr Version","type":"text"}]},{"type":"paragraph","content":[{"text":"Ask the user which version of Apache Solr they are migrating from:","type":"text"}]},{"type":"paragraph","content":[{"text":"\"Which version of Apache Solr are you migrating from? (e.g. 6.6, 7.7, 8.11, 9.4)\"","type":"text","marks":[{"type":"em"}]}]},{"type":"paragraph","content":[{"text":"Accept any valid Apache Solr version number (major, major.minor, or major.minor.patch). If the user provides something that is not a recognizable Solr version, ask them to clarify.","type":"text"}]},{"type":"paragraph","content":[{"text":"Once confirmed:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Store it in the session under ","type":"text"},{"text":"facts.solr_version","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Briefly acknowledge the version. Some versions have known migration considerations worth flagging early — for example:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 6.x and earlier","type":"text","marks":[{"type":"strong"}]},{"text":" — Trie field types (","type":"text"},{"text":"TrieIntField","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"TrieLongField","type":"text","marks":[{"type":"code_inline"}]},{"text":", etc.) are still in common use; flag that these have no direct OpenSearch equivalent and will need to be mapped to Point field equivalents.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 7.x","type":"text","marks":[{"type":"strong"}]},{"text":" — Trie fields are deprecated; confirm whether the schema has already migrated to Point fields.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 8.x / 9.x","type":"text","marks":[{"type":"strong"}]},{"text":" — Generally closer to modern OpenSearch field type conventions; fewer low-level type incompatibilities expected.","type":"text"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"Move to Step 2.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2 — Schema Acquisition","type":"text"}]},{"type":"paragraph","content":[{"text":"Get the Solr schema that will be the basis for the OpenSearch index mapping. There are two paths:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Path A — Existing schema:","type":"text","marks":[{"type":"strong"}]},{"text":" Ask the user to paste their ","type":"text"},{"text":"schema.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" or the JSON response from the Solr Schema API (","type":"text"},{"text":"GET /solr/\u003ccollection>/schema","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Call ","type":"text"},{"text":"convert_schema_xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"convert_schema_json","type":"text","marks":[{"type":"code_inline"}]},{"text":" accordingly and show the resulting OpenSearch mapping.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Path B — No schema yet:","type":"text","marks":[{"type":"strong"}]},{"text":" If the user has no existing Solr schema, ask them to provide a sample JSON document that represents the data they plan to index. Infer field names and types from the JSON structure and generate a starter OpenSearch index mapping. Confirm the inferred types with the user before proceeding.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Before converting, apply version-specific expectations based on ","type":"text"},{"text":"facts.solr_version","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 6.x and earlier","type":"text","marks":[{"type":"strong"}]},{"text":" — expect ","type":"text"},{"text":"schema.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" format (Managed Schema may not be in use); Trie field types will almost certainly be present; classic similarity (TF-IDF) is the default.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 7.x","type":"text","marks":[{"type":"strong"}]},{"text":" — Managed Schema is the default; Trie fields are deprecated but may still appear; BM25 became the default similarity in 7.0.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 8.x / 9.x","type":"text","marks":[{"type":"strong"}]},{"text":" — Managed Schema and Point field types are standard; ","type":"text"},{"text":"schema.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" is less common but still valid.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Once a mapping is agreed upon, save it to the session.","type":"text"}]},{"type":"paragraph","content":[{"text":"Optional — Create the index in OpenSearch:","type":"text","marks":[{"type":"strong"}]},{"text":" After presenting the mapping, ask the user: ","type":"text"},{"text":"\"Would you like me to create this index in OpenSearch now?\"","type":"text","marks":[{"type":"em"}]},{"text":" Only call ","type":"text"},{"text":"create_opensearch_index","type":"text","marks":[{"type":"code_inline"}]},{"text":" if the user explicitly agrees. Pass the agreed-upon index name and the mapping JSON. If the user declines or does not respond affirmatively, skip this step and move on. Inform the user that ","type":"text"},{"text":"OPENSEARCH_URL","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"OPENSEARCH_USER","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"OPENSEARCH_PASSWORD","type":"text","marks":[{"type":"code_inline"}]},{"text":" environment variables can be set to point to their cluster (defaults to ","type":"text"},{"text":"http://localhost:9200","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]},{"type":"paragraph","content":[{"text":"Stakeholder guidance:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search Relevance Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — show the full mapping JSON with field-by-field annotations; explain every type decision.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DevOps / Platform Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — note index settings (number of shards, replicas) alongside the mapping; flag anything that affects cluster resource usage.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Move to Step 3.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3 — Schema Review & Incompatibility Analysis","type":"text"}]},{"type":"paragraph","content":[{"text":"This step is the primary incompatibility gate. Treat every finding as a potential blocker and be thorough — missed incompatibilities discovered late in a migration are expensive to fix.","type":"text"}]},{"type":"paragraph","content":[{"text":"Systematically check the converted mapping against every category in the ","type":"text"},{"text":"Incompatibility Reference","type":"text","marks":[{"type":"strong"}]},{"text":" section below. For each issue found:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Classify it as one of: ","type":"text"},{"text":"Breaking","type":"text","marks":[{"type":"strong"}]},{"text":" (will cause data loss or index failure), ","type":"text"},{"text":"Behavioral","type":"text","marks":[{"type":"strong"}]},{"text":" (works but produces different results), or ","type":"text"},{"text":"Unsupported","type":"text","marks":[{"type":"strong"}]},{"text":" (feature has no OpenSearch equivalent).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Record it in the session under ","type":"text"},{"text":"facts.incompatibilities","type":"text","marks":[{"type":"code_inline"}]},{"text":" as a list of objects with keys ","type":"text"},{"text":"category","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"severity","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"description","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"recommendation","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Present it to the user immediately with a clear explanation and the recommended resolution.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Specific checks to perform on the schema:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"copyField","type":"text","marks":[{"type":"strong"}]},{"text":" — flag every ","type":"text"},{"text":"\u003ccopyField>","type":"text","marks":[{"type":"code_inline"}]},{"text":" directive; explain replacement with ","type":"text"},{"text":"copy_to","type":"text","marks":[{"type":"code_inline"}]},{"text":" on the source field definition.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Field type gaps","type":"text","marks":[{"type":"strong"}]},{"text":" — flag ","type":"text"},{"text":"solr.ICUCollationField","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"solr.EnumField","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"solr.ExternalFileField","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"solr.PreAnalyzedField","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"solr.SortableTextField","type":"text","marks":[{"type":"code_inline"}]},{"text":" as unsupported or requiring manual workarounds.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Custom analyzers","type":"text","marks":[{"type":"strong"}]},{"text":" — identify any ","type":"text"},{"text":"\u003canalyzer>","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"\u003ctokenizer>","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"\u003cfilter>","type":"text","marks":[{"type":"code_inline"}]},{"text":" referencing a non-standard class. Check whether an equivalent exists in OpenSearch's built-in analysis chain; flag those that do not.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Dynamic fields","type":"text","marks":[{"type":"strong"}]},{"text":" — note that OpenSearch ","type":"text"},{"text":"dynamic_templates","type":"text","marks":[{"type":"code_inline"}]},{"text":" match on field name patterns or data types, not Solr's glob syntax; verify the converted templates preserve the intended behavior.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stored vs. source","type":"text","marks":[{"type":"strong"}]},{"text":" — Solr stores fields individually; OpenSearch stores the original ","type":"text"},{"text":"_source","type":"text","marks":[{"type":"code_inline"}]},{"text":" document. Fields marked ","type":"text"},{"text":"stored=\"true\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" but ","type":"text"},{"text":"indexed=\"false\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" in Solr may behave differently under ","type":"text"},{"text":"_source","type":"text","marks":[{"type":"code_inline"}]},{"text":" filtering.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DocValues","type":"text","marks":[{"type":"strong"}]},{"text":" — Solr requires explicit ","type":"text"},{"text":"docValues=\"true\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" for sorting/faceting on most field types; in OpenSearch, ","type":"text"},{"text":"doc_values","type":"text","marks":[{"type":"code_inline"}]},{"text":" is enabled by default for most types. Flag any field where the Solr schema explicitly disables docValues, as the OpenSearch default may change behavior.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Nested / child documents","type":"text","marks":[{"type":"strong"}]},{"text":" — Solr block join (","type":"text"},{"text":"{!parent}","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"{!child}","type":"text","marks":[{"type":"code_inline"}]},{"text":") has no direct equivalent; flag and recommend OpenSearch ","type":"text"},{"text":"nested objects","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/field-types/supported-field-types/nested/","title":null}}]},{"text":" or ","type":"text"},{"text":"join field type","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/field-types/supported-field-types/join/","title":null}}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No compatible field types","type":"text","marks":[{"type":"strong"}]},{"text":" — Some fields like ","type":"text"},{"text":"TrieIntField","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"TrieLongField","type":"text","marks":[{"type":"code_inline"}]},{"text":", etc. have no direct equivalent in OpenSearch. For these fields, map them to the closest OpenSearch equivalent. Include in the response these fields are not compatible and the closest field type has been chosen. Include this in the migration report.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Similarity / scoring model","type":"text","marks":[{"type":"strong"}]},{"text":" — Solr 6.x and earlier default to TF-IDF (ClassicSimilarity); Solr 7.0+ defaults to BM25. If ","type":"text"},{"text":"facts.solr_version","type":"text","marks":[{"type":"code_inline"}]},{"text":" is 6.x or earlier, flag the scoring model change as a Behavioral incompatibility — relevance scores will differ in OpenSearch even without any other changes.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"** ","type":"text"},{"text":"version","type":"text","marks":[{"type":"em"}]},{"text":"** - Do not migrate the ","type":"text"},{"text":" _version_","type":"text","marks":[{"type":"code_inline"}]},{"text":" field to the OpenSearch index mapping.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Present all findings as a prioritized list: Breaking first, then Behavioral, then Unsupported. If no incompatibilities are found, state that explicitly so the user has confidence to proceed.","type":"text"}]},{"type":"paragraph","content":[{"text":"Stakeholder guidance:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search Relevance Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — go deep on every finding; show the exact Solr construct, the OpenSearch equivalent, and any edge cases in the conversion.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DevOps / Platform Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — prioritise Breaking issues that could cause index creation or reindex failures; note any that require cluster-level configuration changes.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 4 — Query Translation","type":"text"}]},{"type":"paragraph","content":[{"text":"Ask the user for representative Solr queries — at minimum one of each type they use in production (standard, dismax/edismax, facet, range, spatial if applicable). For each query:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Call ","type":"text"},{"text":"convert_query","type":"text","marks":[{"type":"code_inline"}]},{"text":" and show the OpenSearch Query DSL equivalent.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Actively check for query-level incompatibilities and behavioral differences. For each one found, record it in ","type":"text"},{"text":"facts.incompatibilities","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"category: \"query\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" before moving on.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Flag queries that cannot be automatically translated and explain what manual work is needed.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Known query incompatibilities to check for:","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Apply version-specific awareness: if ","type":"text"},{"text":"facts.solr_version","type":"text","marks":[{"type":"code_inline"}]},{"text":" is 6.x or earlier, Streaming Expressions and the Graph query parser may not be present at all — skip those checks and note the version. eDisMax was available from Solr 3.x but matured significantly in 4.x–6.x; flag any eDisMax-specific parameters accordingly. If 7.x+, all items in the table below are relevant.","type":"text"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solr feature","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Severity","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OpenSearch situation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"eDismax ","type":"text"},{"text":"pf","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"pf2","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"pf3","type":"text","marks":[{"type":"code_inline"}]},{"text":" phrase boost fields","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Behavioral","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No direct equivalent; approximate with ","type":"text"},{"text":"multi_match","type":"text","marks":[{"type":"code_inline"}]},{"text":" type ","type":"text"},{"text":"phrase","type":"text","marks":[{"type":"code_inline"}]},{"text":" in a ","type":"text"},{"text":"should","type":"text","marks":[{"type":"code_inline"}]},{"text":" clause.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"eDismax ","type":"text"},{"text":"bq","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"bf","type":"text","marks":[{"type":"code_inline"}]},{"text":" additive boost","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Behavioral","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"function_score","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"script_score","type":"text","marks":[{"type":"code_inline"}]},{"text":"; additive vs. multiplicative semantics differ.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{!join}","type":"text","marks":[{"type":"code_inline"}]},{"text":" cross-collection join","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Breaking","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Not supported; restructure as nested documents or application-side join.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{!collapse}","type":"text","marks":[{"type":"code_inline"}]},{"text":" field collapsing","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Behavioral","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"collapse","type":"text","marks":[{"type":"code_inline"}]},{"text":" via the ","type":"text"},{"text":"Search API collapse parameter","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/search-plugins/searching-data/collapse/","title":null}}]},{"text":" — available but syntax differs.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solr Streaming Expressions","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Unsupported","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No equivalent; move aggregation logic to the application layer or use OpenSearch aggregations.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{!graph}","type":"text","marks":[{"type":"code_inline"}]},{"text":" graph traversal","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Unsupported","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No equivalent in OpenSearch.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Spatial ","type":"text"},{"text":"{!geofilt}","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"{!bbox}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Behavioral","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"geo_distance","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"geo_bounding_box","type":"text","marks":[{"type":"code_inline"}]},{"text":" queries; parameter names differ.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"MoreLikeThis","type":"text","marks":[{"type":"code_inline"}]},{"text":" handler","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Behavioral","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"more_like_this","type":"text","marks":[{"type":"code_inline"}]},{"text":" query; ","type":"text"},{"text":"mindf","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"mintf","type":"text","marks":[{"type":"code_inline"}]},{"text":" parameter names differ slightly.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Facet pivots","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Behavioral","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use nested ","type":"text"},{"text":"terms","type":"text","marks":[{"type":"code_inline"}]},{"text":" aggregations; result shape differs.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"cursorMark","type":"text","marks":[{"type":"code_inline"}]},{"text":" deep pagination","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Behavioral","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"search_after","type":"text","marks":[{"type":"code_inline"}]},{"text":" in OpenSearch; semantics are similar but not identical.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solr relevance TF-IDF (classic)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Behavioral","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OpenSearch defaults to BM25; scores will differ. Configurable via ","type":"text"},{"text":"similarity","type":"text","marks":[{"type":"code_inline"}]},{"text":" setting.","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Stakeholder guidance:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search Relevance Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — show the full before/after Query DSL for every translated query; explain scoring differences (TF-IDF vs BM25) and how to tune ","type":"text"},{"text":"similarity","type":"text","marks":[{"type":"code_inline"}]},{"text":" settings if needed.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DevOps / Platform Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — flag queries that imply resource-intensive patterns (deep pagination, large facet pivots, graph traversal) and note their infrastructure implications.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 5 — Solr Customizations","type":"text"}]},{"type":"paragraph","content":[{"text":"Ask the user whether they rely on any Solr-specific customizations. Use this prompt:","type":"text"}]},{"type":"paragraph","content":[{"text":"\"Before we look at infrastructure, I'd like to understand any Solr customizations you're using. Do any of the following apply to your deployment? Please describe what you have for each that's relevant:\"","type":"text","marks":[{"type":"em"}]}]},{"type":"paragraph","content":[{"text":"Apply version-specific awareness when interpreting the user's answers:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 6.x and earlier","type":"text","marks":[{"type":"strong"}]},{"text":" — the security model is minimal (Basic Auth plugin was added in 5.3; Rule-Based Authorization in 6.0). If the user is on 6.x, ask explicitly whether they have any security configured, as it may be absent entirely.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 7.x","type":"text","marks":[{"type":"strong"}]},{"text":" — the security framework is stable; PKI auth and the Authorization plugin are well-established.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 8.x / 9.x","type":"text","marks":[{"type":"strong"}]},{"text":" — JWT authentication and more granular permission models are available; ask whether they use any of these newer security features.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Request handlers","type":"text","marks":[{"type":"strong"}]},{"text":" — custom ","type":"text"},{"text":"SearchHandler","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"UpdateRequestHandler","type":"text","marks":[{"type":"code_inline"}]},{"text":", or other handlers defined in ","type":"text"},{"text":"solrconfig.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Plugins","type":"text","marks":[{"type":"strong"}]},{"text":" — custom ","type":"text"},{"text":"QParserPlugin","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"SearchComponent","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"TokenFilterFactory","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"UpdateRequestProcessorChain","type":"text","marks":[{"type":"code_inline"}]},{"text":", or other plugin types.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Authentication & authorization","type":"text","marks":[{"type":"strong"}]},{"text":" — Basic Auth, Kerberos, PKI, Rule-Based Authorization Plugin, or a custom security plugin.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Operational constraints","type":"text","marks":[{"type":"strong"}]},{"text":" — specific SLA requirements, air-gapped environments, compliance requirements (e.g. FIPS, FedRAMP), multi-tenancy needs, or read/write traffic isolation.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"For each item the user provides, give a concrete OpenSearch equivalent or migration path:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solr customization","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OpenSearch equivalent / approach","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom ","type":"text"},{"text":"SearchHandler","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use the ","type":"text"},{"text":"Search API","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/api-reference/search/","title":null}}]},{"text":" with a custom request body; complex handler logic moves to the application layer or an ingest pipeline.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"UpdateRequestProcessorChain","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Replace with an ","type":"text"},{"text":"Ingest Pipeline","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/ingest-pipelines/","title":null}}]},{"text":" using built-in or custom processors.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom ","type":"text"},{"text":"QParserPlugin","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Implement equivalent logic in Query DSL (e.g. ","type":"text"},{"text":"function_score","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"script_score","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"percolate","type":"text","marks":[{"type":"code_inline"}]},{"text":") or a search pipeline.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom ","type":"text"},{"text":"TokenFilterFactory","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"CharFilterFactory","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Re-express as a custom ","type":"text"},{"text":"analyzer definition","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/analyzers/","title":null}}]},{"text":" in the index settings using the equivalent built-in filter, or implement a custom plugin via the OpenSearch plugin SDK.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Basic Auth","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use the ","type":"text"},{"text":"OpenSearch Security plugin","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/security/","title":null}}]},{"text":" (bundled) with internal user database or LDAP/Active Directory backend.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Kerberos","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OpenSearch Security supports Kerberos via the ","type":"text"},{"text":"kerberos","type":"text","marks":[{"type":"code_inline"}]},{"text":" authentication domain.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PKI / mutual TLS","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Configure node-to-node and client TLS in ","type":"text"},{"text":"opensearch.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":"; the Security plugin handles certificate-based auth.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rule-Based Authorization Plugin","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Map to OpenSearch Security ","type":"text"},{"text":"roles and role mappings","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/security/access-control/","title":null}}]},{"text":".","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Air-gapped / offline deployment","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OpenSearch supports fully offline installation; use the tarball or RPM/DEB packages and mirror the plugin registry internally.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"FIPS 140-2 compliance","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OpenSearch provides a ","type":"text"},{"text":"FIPS-compliant distribution","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/install-and-configure/install-opensearch/fips/","title":null}}]},{"text":".","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Multi-tenancy","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use OpenSearch Security ","type":"text"},{"text":"tenants","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/security/multi-tenancy/","title":null}}]},{"text":" for Dashboards isolation, and index-level permissions for data isolation.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Read/write traffic isolation","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Route via separate ","type":"text"},{"text":"coordinating-only nodes","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/tuning-your-cluster/cluster-formation/cluster-manager/","title":null}}]},{"text":" or use a load balancer with separate pools.","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"If the user mentions a customization not in the table above, reason about the closest OpenSearch equivalent and flag it as a manual migration item.","type":"text"}]},{"type":"paragraph","content":[{"text":"Store all identified customizations and their OpenSearch mappings in the session under ","type":"text"},{"text":"facts.customizations","type":"text","marks":[{"type":"code_inline"}]},{"text":" so they are included in the migration report.","type":"text"}]},{"type":"paragraph","content":[{"text":"Stakeholder guidance:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search Relevance Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — go deep on plugin internals; show the OpenSearch plugin SDK or analysis chain equivalent for each custom component.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DevOps / Platform Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — prioritise authentication, authorization, and operational constraints (air-gapped, FIPS, multi-tenancy); these drive infrastructure and deployment decisions. This is a high-priority step for this role.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 6 — Cluster & Infrastructure Assessment","type":"text"}]},{"type":"paragraph","content":[{"text":"Ask the user about their current deployment topology:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Standalone Solr or SolrCloud? Number of nodes, shards, and replicas?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Approximate document count and index size?","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Peak query throughput and indexing rate?","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Apply version-specific awareness when assessing the topology:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 6.x and earlier","type":"text","marks":[{"type":"strong"}]},{"text":" — SolrCloud relies on ZooKeeper for cluster coordination; the ZooKeeper dependency is completely absent in OpenSearch (which uses its own Raft-based cluster manager). Flag this as an operational change regardless of stakeholder role.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 7.x","type":"text","marks":[{"type":"strong"}]},{"text":" — same ZooKeeper dependency; also ask whether they use CDCR (Cross Data Center Replication), which has no direct OpenSearch equivalent — cross-cluster replication (CCR) in OpenSearch is the closest analog.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Solr 8.x / 9.x","type":"text","marks":[{"type":"strong"}]},{"text":" — ask whether they use Solr's autoscaling framework (deprecated in 8.x, removed in 9.x); if so, note that OpenSearch has no equivalent and autoscaling must be handled at the infrastructure layer (e.g. AWS Auto Scaling).","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Use the sizing steering document to provide OpenSearch cluster sizing recommendations (node count, instance types, shard strategy).","type":"text"}]},{"type":"paragraph","content":[{"text":"Stakeholder guidance:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search Relevance Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — include shard sizing rationale, JVM heap recommendations, and index lifecycle management strategy.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DevOps / Platform Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — this is the highest-priority step for this role. Go deep: instance types, storage (EBS vs. instance store), node roles (data, coordinating, cluster manager), auto-scaling, monitoring, and deployment automation. Ask about their target environment (self-managed vs. Amazon OpenSearch Service).","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 7 — Client & Front-end Integration","type":"text"}]},{"type":"paragraph","content":[{"text":"Ask the user what client-side code talks to Solr today. Use these prompts:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"What client libraries are you using — SolrJ, pysolr, a custom HTTP client, or something else?\"","type":"text","marks":[{"type":"em"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Do you have a front-end search UI (e.g. Solr-specific widgets, Velocity templates, or a custom React/Vue app)?\"","type":"text","marks":[{"type":"em"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Are there any other systems or services that make direct HTTP calls to Solr's ","type":"text","marks":[{"type":"em"}]},{"text":"/select","type":"text","marks":[{"type":"code_inline"},{"type":"em"}]},{"text":", ","type":"text","marks":[{"type":"em"}]},{"text":"/update","type":"text","marks":[{"type":"code_inline"},{"type":"em"}]},{"text":", or admin endpoints?\"","type":"text","marks":[{"type":"em"}]}]}]}]},{"type":"paragraph","content":[{"text":"For each integration the user describes, record it in the session via ","type":"text"},{"text":"SessionState.add_client_integration","type":"text","marks":[{"type":"code_inline"}]},{"text":" with:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Field","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What to capture","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"name","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The library, framework, or component name (e.g. \"SolrJ\", \"pysolr\", \"React Search UI\")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"kind","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"One of: ","type":"text"},{"text":"library","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"ui","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"http","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"other","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"notes","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"How it is currently used (endpoints called, features relied on)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"migration_action","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"The concrete change required for OpenSearch","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Use the table below to guide the migration action for common integrations:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solr client / UI","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Kind","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Migration action","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SolrJ","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"library","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Replace with ","type":"text"},{"text":"opensearch-java","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/opensearch-project/opensearch-java","title":null}}]},{"text":"; update endpoint URLs and request/response models.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pysolr","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"library","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Replace with ","type":"text"},{"text":"opensearch-py","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/opensearch-project/opensearch-py","title":null}}]},{"text":"; update query construction and response parsing.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"solr-ruby / rsolr","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"library","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Replace with ","type":"text"},{"text":"opensearch-ruby","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/opensearch-project/opensearch-ruby","title":null}}]},{"text":".","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom HTTP client","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"http","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Update base URL from ","type":"text"},{"text":"/solr/\u003ccollection>/select","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"/\u003cindex>/_search","type":"text","marks":[{"type":"code_inline"}]},{"text":"; migrate request body to Query DSL JSON.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solr Admin UI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ui","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Migrate to ","type":"text"},{"text":"OpenSearch Dashboards","type":"text","marks":[{"type":"link","attrs":{"href":"https://opensearch.org/docs/latest/dashboards/","title":null}}]},{"text":"; index management, query dev tools, and monitoring are all available.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Velocity / Solr response writer templates","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ui","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Remove; OpenSearch returns JSON natively — render in the application layer.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"React/Vue/Angular with Solr-specific widgets","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ui","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Replace Solr-specific components with OpenSearch-compatible equivalents or generic REST-based components.","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solr SolrJ CloudSolrClient (SolrCloud)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"library","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Replace with OpenSearch client pointed at the cluster load balancer; no ZooKeeper dependency.","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"If the user describes an integration not in the table, reason about the endpoint and request/response shape changes needed and provide a concrete before/after example.","type":"text"}]},{"type":"paragraph","content":[{"text":"Identify any authentication changes required (e.g. moving from Solr Basic Auth to OpenSearch Security headers) and note them in ","type":"text"},{"text":"migration_action","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Stakeholder guidance:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search Relevance Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — note any query or response shape differences between the Solr and OpenSearch client APIs that require logic changes beyond a library swap.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DevOps / Platform Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — focus on authentication changes and any integrations that make direct admin API calls; flag anything that requires network or firewall rule changes.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 8 — Migration Report","type":"text"}]},{"type":"paragraph","content":[{"text":"Call ","type":"text"},{"text":"generate_report","type":"text","marks":[{"type":"code_inline"}]},{"text":" to produce the final report. The report must cover:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Source version","type":"text","marks":[{"type":"strong"}]},{"text":" — state ","type":"text"},{"text":"facts.solr_version","type":"text","marks":[{"type":"code_inline"}]},{"text":" prominently at the top of the report so all findings are clearly scoped to the specific Solr version being migrated.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Incompatibilities","type":"text","marks":[{"type":"strong"}]},{"text":" (prominent, dedicated section at the top) — every item collected in ","type":"text"},{"text":"facts.incompatibilities","type":"text","marks":[{"type":"code_inline"}]},{"text":" across all steps, grouped by severity: Breaking → Unsupported → Behavioral. Each entry must include the category, description, and recommended resolution. Breaking and Unsupported items are also surfaced as explicit blockers.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Client & Front-end Impact","type":"text","marks":[{"type":"strong"}]},{"text":" — every ","type":"text"},{"text":"ClientIntegration","type":"text","marks":[{"type":"code_inline"}]},{"text":" recorded in Step 7, grouped by kind (libraries, UI, HTTP clients). Each entry shows the current usage and the concrete migration action required. If no integrations were recorded, state that explicitly.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Major milestones and suggested sequencing.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Blockers surfaced in Steps 3–7.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Implementation points with enough detail for an engineer to act on.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cost estimates for infrastructure, effort, and any required tooling changes.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Present the report to the user and offer to drill into any section.","type":"text"}]},{"type":"paragraph","content":[{"text":"Stakeholder guidance — tailor the report structure and emphasis:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Search Relevance Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — lead with the full incompatibility list and query translation details; include the complete OpenSearch mapping and all Query DSL examples as appendices.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DevOps / Platform Engineer","type":"text","marks":[{"type":"strong"}]},{"text":" — lead with the cluster sizing recommendation and infrastructure plan; make the deployment sequencing and operational runbook the most prominent section.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Resuming a Conversation","type":"text"}]},{"type":"paragraph","content":[{"text":"Migration plans can span weeks or months, and conversations may be restarted many times. All session state — schema mappings, incompatibilities, query translations, client integrations, and workflow progress — is persisted automatically after every turn using the ","type":"text"},{"text":"session_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" you provide.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Migration Progress File","type":"text"}]},{"type":"paragraph","content":[{"text":"In addition to the JSON session state, maintain a human-readable Markdown file at ","type":"text"},{"text":"sessions/\u003csession_id>.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". This file is the user's living record of their migration journey — update it at the end of every step so it always reflects the current state of the migration.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"When to update","type":"text"}]},{"type":"paragraph","content":[{"text":"Update ","type":"text"},{"text":"sessions/\u003csession_id>.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" after every step completes. Do not wait until the end of the migration. Each update should reflect only what is known at that point — do not leave placeholder sections for steps not yet reached.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"File structure","type":"text"}]},{"type":"paragraph","content":[{"text":"The file must always contain the following sections, updated in place as the migration progresses:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"markdown"},"content":[{"text":"# Solr to OpenSearch Migration — \u003csession_id>\n\n**Stakeholder role:** \u003crole>\n**Solr version:** \u003cversion, or \"not yet provided\">\n**Current step:** \u003cstep number and name>\n**Last updated:** \u003cdate of last update>\n\n---\n\n## Progress\n\n| Step | Name | Status |\n|---|---|---|\n| 0 | Stakeholder Identification | ✅ Complete / 🔄 In Progress / ⬜ Not Started |\n| 1 | Solr Version | ... |\n| 2 | Schema Acquisition | ... |\n| 3 | Schema Review & Incompatibility Analysis | ... |\n| 4 | Query Translation | ... |\n| 5 | Solr Customizations | ... |\n| 6 | Cluster & Infrastructure Assessment | ... |\n| 7 | Client & Front-end Integration | ... |\n| 8 | Migration Report | ... |\n\n---\n\n## Key Facts\n\n- **Solr version:** \u003cvalue from facts.solr_version>\n- **Stakeholder role:** \u003cvalue from facts.stakeholder_role>\n- **Index name:** \u003cagreed index name, if known>\n- **Schema migrated:** \u003cyes / no / in progress>\n- **Customizations identified:** \u003clist or \"none identified yet\">\n\n---\n\n## Incompatibilities\n\n\u003cIf none found yet, write \"No incompatibilities identified yet.\">\n\n| Severity | Category | Description | Recommendation |\n|---|---|---|---|\n| Breaking | ... | ... | ... |\n| Behavioral | ... | ... | ... |\n| Unsupported | ... | ... | ... |\n\n---\n\n## Client Integrations\n\n\u003cIf none recorded yet, write \"No client integrations recorded yet.\">\n\n| Name | Kind | Current Usage | Migration Action |\n|---|---|---|---|\n| ... | ... | ... | ... |\n\n---\n\n## Notes\n\n\u003cFree-form notes added during the session — decisions made, open questions, user preferences, anything worth remembering across restarts.>","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Rules","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create the file at the end of Step 0","type":"text","marks":[{"type":"strong"}]},{"text":", once the stakeholder role is known. Initialize all step statuses to ⬜ Not Started except Step 0 which becomes ✅ Complete.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mark a step 🔄 In Progress","type":"text","marks":[{"type":"strong"}]},{"text":" when it begins and ","type":"text"},{"text":"✅ Complete","type":"text","marks":[{"type":"strong"}]},{"text":" when the user confirms they are satisfied and ready to move on.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Append to Notes","type":"text","marks":[{"type":"strong"}]},{"text":" whenever the user makes a decision, expresses a preference, or raises an open question that should be remembered across restarts.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Update Incompatibilities","type":"text","marks":[{"type":"strong"}]},{"text":" immediately when a new incompatibility is recorded in ","type":"text"},{"text":"facts.incompatibilities","type":"text","marks":[{"type":"code_inline"}]},{"text":" — do not batch them until the report.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Update Client Integrations","type":"text","marks":[{"type":"strong"}]},{"text":" immediately when a new integration is recorded via ","type":"text"},{"text":"SessionState.add_client_integration","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When deleting information","type":"text","marks":[{"type":"strong"}]},{"text":" keep the structure described, only delete information that has shown to be irrelevant, and place a note highlighting aspects that were shown during the conversation to be irrelevant, giving reasons why this is the case. Do not delete any information relevant to the migration effort - only add or update where suitable.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The file is the source of truth for human readers.","type":"text","marks":[{"type":"strong"}]},{"text":" Write it as if the user will share it with a colleague who has no access to the JSON session file.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"How to resume","type":"text"}]},{"type":"paragraph","content":[{"text":"When starting a new conversation, pass the same ","type":"text"},{"text":"session_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" you used previously:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"# Resume an existing session — all prior context is restored automatically\nresponse = skill.handle_message(\"Let's continue the migration\", session_id=\"my-project-migration\")","type":"text"}]},{"type":"paragraph","content":[{"text":"Via MCP:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{ \"tool\": \"handle_message\", \"arguments\": { \"message\": \"Let's continue\", \"session_id\": \"my-project-migration\" } }","type":"text"}]},{"type":"paragraph","content":[{"text":"The advisor will reload the full ","type":"text"},{"text":"SessionState","type":"text","marks":[{"type":"code_inline"}]},{"text":" (history, facts, progress, incompatibilities, client integrations) and pick up exactly where you left off. The Markdown progress file at ","type":"text"},{"text":"sessions/\u003csession_id>.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" will also be updated to reflect the resumed state.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Choosing a session ID","type":"text"}]},{"type":"paragraph","content":[{"text":"Use a stable, meaningful identifier tied to your project — not a random UUID — so it is easy to recall across restarts:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"acme-solr-migration","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"projectname-prod-cluster","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"team-search-migration-2025","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Listing and inspecting existing sessions","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"from scripts.storage import FileStorage\n\nstorage = FileStorage(\"sessions\")\n\n# List all saved sessions\nprint(storage.list_sessions())\n\n# Inspect a specific session\nstate = storage.load(\"my-project-migration\")\nprint(f\"Progress: Step {state.progress}\")\nprint(f\"Incompatibilities found: {len(state.incompatibilities)}\")\nprint(f\"Facts: {state.facts}\")","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Session files","type":"text"}]},{"type":"paragraph","content":[{"text":"With the default ","type":"text"},{"text":"FileStorage","type":"text","marks":[{"type":"code_inline"}]},{"text":" backend, each session produces two files:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sessions/\u003csession_id>.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" — machine-readable JSON containing the full conversation history, all discovered facts, incompatibilities, client integrations, and progress. Used by the skill for session resumption.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sessions/\u003csession_id>.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — human-readable Markdown progress file. Updated after every step. Safe to share with colleagues, attach to tickets, or check into version control. See the ","type":"text"},{"text":"Migration Progress File","type":"text","marks":[{"type":"strong"}]},{"text":" section above for the full format.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Starting fresh","type":"text"}]},{"type":"paragraph","content":[{"text":"To reset a session and start over:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"storage.delete(\"my-project-migration\")","type":"text"}]},{"type":"paragraph","content":[{"text":"Or simply use a new ","type":"text"},{"text":"session_id","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Reference Knowledge Base","type":"text"}]},{"type":"paragraph","content":[{"text":"You have access to a verified knowledge base of technical information about Apache Solr and OpenSearch located under the ","type":"text"},{"text":"references","type":"text","marks":[{"type":"code_inline"}]},{"text":" directory. Consult these files proactively — do not wait for the user to ask. Use the table below to select the most relevant file(s) for the current topic, then cite the specific section you drew from.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"When to Use Each Reference File","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"File","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Content Summary","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use When…","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/01-schema-migration.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Field type mappings, ","type":"text"},{"text":"schema.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" constructs, dynamic fields, copy fields, and similarity configuration","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Converting a Solr schema to an OpenSearch mapping (Step 2); answering field type questions","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/02-query-translation.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solr Standard, DisMax, and eDisMax query syntax translated to OpenSearch Query DSL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Translating Solr queries (Step 4); explaining query parser differences","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/03-analysis-pipelines.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tokenizers, token filters, char filters, and analyzer chain migration","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Migrating custom analyzers; replicating Solr text analysis behavior","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/03b-synonyms-and-language.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Synonym handling, language-specific analyzers, and multilingual index strategies","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Migrating ","type":"text"},{"text":"synonyms.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":"; configuring language analyzers in OpenSearch","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/04-architecture.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SolrCloud vs. OpenSearch cluster architecture, ZooKeeper removal, sharding, replication, and document identity","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Explaining cluster topology differences; planning infrastructure migration","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/05-legacy-features.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Data Import Handler (DIH), BlockJoin, function queries, and other Solr-specific features with no direct OpenSearch equivalent","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Identifying feature gaps; recommending migration strategies for legacy Solr features","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/05b-legacy-features-continued.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Joins, Streaming Expressions, SpellCheck, MoreLikeThis, custom request handlers, atomic update modifiers, ","type":"text"},{"text":"_version_","type":"text","marks":[{"type":"code_inline"}]},{"text":" concurrency, ","type":"text"},{"text":"QueryElevationComponent","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"ExternalFileField","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"PreAnalyzedField","type":"text","marks":[{"type":"code_inline"}]},{"text":", and a full feature gap summary table","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Same as above — continuation covering additional legacy features and indexing-level gaps","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/06-feature-compatibility-matrix.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Side-by-side compatibility ratings (✅/⚠️/❌) across schema, query parsers, search components, analysis, indexing, and cluster operations","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Quick compatibility lookup; scoping migration effort; identifying blockers","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/07-solrconfig-migration.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"solrconfig.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" constructs (request handlers, caches, update settings, merge policy, similarity) mapped to OpenSearch equivalents","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Migrating ","type":"text"},{"text":"solrconfig.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":"; configuring OpenSearch index and node settings","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/08-query-behavior-edge-cases.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Known behavioral differences between Solr query parsers and OpenSearch Query DSL: default operator, fuzzy scale, date math, scoring, highlighting, sorting, deep pagination, Solr-only query parsers (","type":"text"},{"text":"{!complexphrase}","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"{!surround}","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"{!graph}","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"{!switch}","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"{!rerank}","type":"text","marks":[{"type":"code_inline"}]},{"text":") with no OpenSearch equivalent","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Debugging query result differences; validating query parity after migration; identifying unsupported query parsers","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/09-sizing-and-performance.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Node roles, shard sizing formulas, JVM/heap tuning, bulk indexing settings, cache configuration, hardware recommendations, and monitoring metrics","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sizing a new OpenSearch cluster; performance tuning; capacity planning (Step 3 / DevOps stakeholder)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Usage Guidelines","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cite your sources.","type":"text","marks":[{"type":"strong"}]},{"text":" When drawing on a reference file, name the file and section (e.g., ","type":"text"},{"text":"\"per ","type":"text","marks":[{"type":"em"}]},{"text":"references/06-feature-compatibility-matrix.md","type":"text","marks":[{"type":"code_inline"},{"type":"em"}]},{"text":", section 3 — Query Parsers\"","type":"text","marks":[{"type":"em"}]},{"text":").","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prefer reference files over general knowledge","type":"text","marks":[{"type":"strong"}]},{"text":" for any topic covered above. The reference files reflect decisions and conventions specific to this migration skill.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Combine files when needed.","type":"text","marks":[{"type":"strong"}]},{"text":" For example, a schema question may require both ","type":"text"},{"text":"01-schema-migration.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (field types) and ","type":"text"},{"text":"03-analysis-pipelines.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" (analyzer chains).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stakeholder filtering.","type":"text","marks":[{"type":"strong"}]},{"text":" For a DevOps / Platform Engineer, prioritize ","type":"text"},{"text":"04-architecture.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"09-sizing-and-performance.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"07-solrconfig-migration.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". For a Search Relevance Engineer, prioritize ","type":"text"},{"text":"01-schema-migration.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"02-query-translation.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"03-analysis-pipelines.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"08-query-behavior-edge-cases.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"#","type":"text"},{"type":"wikilink","attrs":{"alias":null,"embed":false,"target":"file:references/01-schema-migration.md","blockId":null,"heading":null}},{"text":" #","type":"text"},{"type":"wikilink","attrs":{"alias":null,"embed":false,"target":"file:references/02-query-translation.md","blockId":null,"heading":null}},{"text":" #","type":"text"},{"type":"wikilink","attrs":{"alias":null,"embed":false,"target":"file:references/03-analysis-pipelines.md","blockId":null,"heading":null}},{"text":" #","type":"text"},{"type":"wikilink","attrs":{"alias":null,"embed":false,"target":"file:references/03b-synonyms-and-language.md","blockId":null,"heading":null}},{"text":" #","type":"text"},{"type":"wikilink","attrs":{"alias":null,"embed":false,"target":"file:references/04-architecture.md","blockId":null,"heading":null}},{"text":" #","type":"text"},{"type":"wikilink","attrs":{"alias":null,"embed":false,"target":"file:references/05-legacy-features.md","blockId":null,"heading":null}},{"text":" #","type":"text"},{"type":"wikilink","attrs":{"alias":null,"embed":false,"target":"file:references/05b-legacy-features-continued.md","blockId":null,"heading":null}},{"text":" #","type":"text"},{"type":"wikilink","attrs":{"alias":null,"embed":false,"target":"file:references/06-feature-compatibility-matrix.md","blockId":null,"heading":null}},{"text":" #","type":"text"},{"type":"wikilink","attrs":{"alias":null,"embed":false,"target":"file:references/07-solrconfig-migration.md","blockId":null,"heading":null}},{"text":" #","type":"text"},{"type":"wikilink","attrs":{"alias":null,"embed":false,"target":"file:references/08-query-behavior-edge-cases.md","blockId":null,"heading":null}},{"text":" #","type":"text"},{"type":"wikilink","attrs":{"alias":null,"embed":false,"target":"file:references/09-sizing-and-performance.md","blockId":null,"heading":null}}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Instructions","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Always maintain the session context using the ","type":"text"},{"text":"session_id","type":"text","marks":[{"type":"code_inline"}]},{"text":". Every call loads the full ","type":"text"},{"text":"SessionState","type":"text","marks":[{"type":"code_inline"}]},{"text":" (history, facts, progress, incompatibilities) and saves it back before returning — sessions are fully resumable across restarts.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Follow the steps in order. If the user jumps ahead, acknowledge their input, store it in the session, and guide them back to complete any skipped steps.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If a user asks for migration advice but hasn't provided technical details, proactively request the Solr schema or a sample JSON document (Step 2).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text","marks":[{"type":"strong"}]},{"text":"facts.solr_version","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" throughout every step.","type":"text","marks":[{"type":"strong"}]},{"text":" Once the Solr version is known, apply version-specific checks, flag version-specific incompatibilities, and tailor all recommendations accordingly. Never give generic advice when a version-specific answer is more accurate.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use the steering documents (Stakeholders, Query Translation, Index Design, Sizing, Incompatibilities, Authentication) to inform all reasoning.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Incompatibility tracking is mandatory.","type":"text","marks":[{"type":"strong"}]},{"text":" Every incompatibility found in any step must be recorded in ","type":"text"},{"text":"facts.incompatibilities","type":"text","marks":[{"type":"code_inline"}]},{"text":" (via ","type":"text"},{"text":"SessionState.add_incompatibility","type":"text","marks":[{"type":"code_inline"}]},{"text":") before moving on. Never silently skip a known issue.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When in doubt about whether something is an incompatibility, flag it conservatively — a false positive is far less harmful than a missed breaking change.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cite reference sources.","type":"text","marks":[{"type":"strong"}]},{"text":" Whenever a response draws on information from a ","type":"text"},{"text":"references/","type":"text","marks":[{"type":"code_inline"}]},{"text":" file, name the file and section inline — e.g., ","type":"text"},{"text":"\"per ","type":"text","marks":[{"type":"em"}]},{"text":"references/06-feature-compatibility-matrix.md","type":"text","marks":[{"type":"code_inline"},{"type":"em"}]},{"text":", section 2 — Query Parsers\"","type":"text","marks":[{"type":"em"}]},{"text":". Do not present reference-derived content as general knowledge.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Session State Fields","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"SessionState","type":"text","marks":[{"type":"code_inline"}]},{"text":" object persisted for each session contains:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Field","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Type","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"session_id","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"str","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Unique session identifier","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"history","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"list[{user, assistant}]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Full conversation turns","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"facts","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"dict","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Discovered migration facts (e.g. ","type":"text"},{"text":"schema_migrated","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"customizations","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"progress","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"int","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Current workflow step (0 = not started; advances forward only)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"incompatibilities","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"list[Incompatibility]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"All incompatibilities found, with ","type":"text"},{"text":"category","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"severity","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"description","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"recommendation","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"client_integrations","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"list[ClientIntegration]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Client-side and front-end integrations collected in Step 7, with ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"kind","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"notes","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"migration_action","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Pluggable Storage Backends","type":"text"}]},{"type":"paragraph","content":[{"text":"The storage backend is injected at construction time. Built-in options:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"InMemoryStorage","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ephemeral, process-scoped; useful for tests and single-turn use.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"FileStorage(base_path)","type":"text","marks":[{"type":"code_inline"}]},{"text":" — JSON file per session on disk; the default for persistent deployments.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Custom backends implement ","type":"text"},{"text":"StorageBackend","type":"text","marks":[{"type":"code_inline"}]},{"text":" (four methods: ","type":"text"},{"text":"_save_raw","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"_load_raw","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"delete","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"list_sessions","type":"text","marks":[{"type":"code_inline"}]},{"text":") and are drop-in replacements with no changes to skill logic.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Usage","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Library Usage","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import sys\nimport os\n# Add scripts directory to sys.path\nsys.path.append(os.path.join(os.getcwd(), \".kiro/skills/solr-to-opensearch/scripts\"))\n\nfrom skill import SolrToOpenSearchMigrationSkill\n\n# Initialize advisor\nskill = SolrToOpenSearchMigrationSkill()\n\n# Handle conversational message\nsession_id = \"user-123\"\nresponse = skill.handle_message(\"Help me migrate my Solr schema: \u003cschema>...\u003c/schema>\", session_id)\nprint(response)\n\n# Generate final report\nreport = skill.generate_report(session_id)\nprint(report)","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"MCP Server Usage","type":"text"}]},{"type":"paragraph","content":[{"text":"Install dependencies and run the MCP server over stdio:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"pip install -e \".kiro/skills/solr-to-opensearch[mcp]\"\npython .kiro/skills/solr-to-opensearch/scripts/mcp_server.py","type":"text"}]},{"type":"paragraph","content":[{"text":"Or configure it in your MCP client (e.g. ","type":"text"},{"text":".kiro/settings/mcp.json","type":"text","marks":[{"type":"code_inline"}]},{"text":"):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"mcpServers\": {\n \"solr-to-opensearch\": {\n \"command\": \"python3\",\n \"args\": [\".kiro/skills/solr-to-opensearch/scripts/mcp_server.py\"],\n \"disabled\": false,\n \"autoApprove\": []\n }\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Persistence Fallback","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"In case you are not successful using provided session persistence tools for persistence as a JSON file at ","type":"text"},{"text":"sessions/\u003csession_id>.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", persist such a file yourself at the given location within the solr-opensearch-migration-advisor directory. The file is human-readable and contains the full conversation history, all discovered facts, and migration progress. Similarly, always maintain the Markdown progress file at ","type":"text"},{"text":"sessions/\u003csession_id>.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" as described in the ","type":"text"},{"text":"Migration Progress File","type":"text","marks":[{"type":"strong"}]},{"text":" section. If the JSON session file cannot be written, the Markdown file must still be kept up to date — it is the human-readable record of the migration and must never be skipped.","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"solr-opensearch-migration-advisor","author":"@skillopedia","source":{"stars":21,"repo_name":"opensearch-agent-skills","origin_url":"https://github.com/opensearch-project/opensearch-agent-skills/blob/HEAD/skills/solr-opensearch-migration-advisor/SKILL.md","repo_owner":"opensearch-project","body_sha256":"5dd261b83809fd1cb49052af7293928d96a39066555b4a832423e75cba0e3fd6","cluster_key":"c99d0a86a25f49028d844c3709ec27cd77dca1b2e94069a7fc8b239559f9b45a","clean_bundle":{"format":"clean-skill-bundle-v1","source":"opensearch-project/opensearch-agent-skills/skills/solr-opensearch-migration-advisor/SKILL.md","attachments":[{"id":"1483c0ba-5d83-5758-8110-4d8fce128837","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1483c0ba-5d83-5758-8110-4d8fce128837/attachment.example","path":".env.example","size":149,"sha256":"9eff2549f7bf172dd2b29c3fc7cf0d669a22fca359620ebe47a39fabf6e982fc","contentType":"text/plain; charset=utf-8"},{"id":"e1259116-4b07-589f-838d-017b71774893","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e1259116-4b07-589f-838d-017b71774893/attachment","path":".gitignore","size":4,"sha256":"e9cbb0224c4a3d23a6019ba557e0cd568c1ad5e1582ff1e335fb7d99b7a1055d","contentType":"text/plain; charset=utf-8"},{"id":"dc54fe2c-50a6-576b-b7fd-2090701c0328","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dc54fe2c-50a6-576b-b7fd-2090701c0328/attachment.md","path":"README.md","size":3296,"sha256":"218f079c5cc6340cdde8547ea8434210998207d3916842fd07be7832cfde1cb9","contentType":"text/markdown; charset=utf-8"},{"id":"1a7bffaa-9dd2-5123-a686-862b681af7d4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1a7bffaa-9dd2-5123-a686-862b681af7d4/attachment.toml","path":"pyproject.toml","size":1038,"sha256":"b4c725390a4c32ac7579f2e5aee506c190af1f4377229af0edd03c543c72d59c","contentType":"text/plain; charset=utf-8"},{"id":"21039232-b8a6-54d5-b42f-12a00885ca85","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/21039232-b8a6-54d5-b42f-12a00885ca85/attachment.md","path":"references/01-schema-migration.md","size":7693,"sha256":"e599936eca86bf99ea74649de2baba50282af8697e767686540f82eff0a39fb2","contentType":"text/markdown; charset=utf-8"},{"id":"4695fd10-8919-5f70-bfa2-e807b82ec23f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4695fd10-8919-5f70-bfa2-e807b82ec23f/attachment.md","path":"references/02-query-translation.md","size":13113,"sha256":"07f9b4dbd3db7fe37383adced2962dd73cffaf79ee4386f90533232ef5c3a860","contentType":"text/markdown; charset=utf-8"},{"id":"d4f22a3a-8aff-521a-aca7-b96ba133b145","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d4f22a3a-8aff-521a-aca7-b96ba133b145/attachment.md","path":"references/03-analysis-pipelines.md","size":8326,"sha256":"0b9453a64b51b19c0af455501f0284720de3dfd859e9c0ad4d06bf8a4007a875","contentType":"text/markdown; charset=utf-8"},{"id":"20f000ce-3b14-515d-adb6-c57e3f03c113","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/20f000ce-3b14-515d-adb6-c57e3f03c113/attachment.md","path":"references/03b-synonyms-and-language.md","size":7960,"sha256":"c16622c13d0658286289d7fb237a07b96f8891ec575f4015a2ecd0f563c9d076","contentType":"text/markdown; charset=utf-8"},{"id":"34d1bf22-9263-505f-8373-e3d16da6c5b7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/34d1bf22-9263-505f-8373-e3d16da6c5b7/attachment.md","path":"references/04-architecture.md","size":13311,"sha256":"85d7bcbe699d40f509f8e052c5cca451c99781f56093a54f8ca10c5d7c9a2a7b","contentType":"text/markdown; charset=utf-8"},{"id":"65b558a6-dbe1-5ad3-95ed-a1a856fa9c82","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/65b558a6-dbe1-5ad3-95ed-a1a856fa9c82/attachment.md","path":"references/05-legacy-features.md","size":7402,"sha256":"f72a277fcf723c1f0605499d981290be584bad75244cfcadec88df988b579bc2","contentType":"text/markdown; charset=utf-8"},{"id":"99144742-4376-561e-970f-eb10f05fd5fc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/99144742-4376-561e-970f-eb10f05fd5fc/attachment.md","path":"references/05b-legacy-features-continued.md","size":9648,"sha256":"dfb3ad44d32c3219b3c26b1dc0ccd53601b1a85274af217c594cc17db21b889e","contentType":"text/markdown; charset=utf-8"},{"id":"b80b77c2-5948-59d9-90f9-555bee9f6062","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b80b77c2-5948-59d9-90f9-555bee9f6062/attachment.md","path":"references/06-feature-compatibility-matrix.md","size":9806,"sha256":"9af0cb042d9fe816baf6c0d2a062caf2c90359b250aafa83e6ef562930a3031b","contentType":"text/markdown; charset=utf-8"},{"id":"eb45bcce-9efe-5dac-bad9-a8fbd16ce2c7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eb45bcce-9efe-5dac-bad9-a8fbd16ce2c7/attachment.md","path":"references/07-solrconfig-migration.md","size":9132,"sha256":"490e3590d018c36fd57f726aaf584b9d83c0df272d6df60d913a4b3dfaf0a8c9","contentType":"text/markdown; charset=utf-8"},{"id":"40ef3dc9-a894-5a0d-9106-16adfc1465f7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/40ef3dc9-a894-5a0d-9106-16adfc1465f7/attachment.md","path":"references/08-query-behavior-edge-cases.md","size":6735,"sha256":"145601f384ad5f4aa6f6038713eb1de0faf12b6cbc09eac2bac289b11afbcc73","contentType":"text/markdown; charset=utf-8"},{"id":"b812492d-4b15-53d9-b6d8-1bff076c92b2","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b812492d-4b15-53d9-b6d8-1bff076c92b2/attachment.md","path":"references/09-sizing-and-performance.md","size":6351,"sha256":"0c0be49db09beccf48fb6616869c746c061f4798fed85b872682724f3f042599","contentType":"text/markdown; charset=utf-8"},{"id":"5d8e2358-a09a-555d-b695-4a5fe23898aa","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5d8e2358-a09a-555d-b695-4a5fe23898aa/attachment.py","path":"scripts/__init__.py","size":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","contentType":"text/x-python; charset=utf-8"},{"id":"bfdccb6a-7630-575f-bc7e-66b61d117405","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bfdccb6a-7630-575f-bc7e-66b61d117405/attachment.py","path":"scripts/query_converter.py","size":16355,"sha256":"6880241ceac59e80c3b81d627304b40adbd435087503d6b22e6673d56fc537d9","contentType":"text/x-python; charset=utf-8"},{"id":"03e9204a-9dca-5543-a033-fa344de636b1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/03e9204a-9dca-5543-a033-fa344de636b1/attachment.py","path":"scripts/report.py","size":5890,"sha256":"581e7de357c698aaea821d8ae6a797b8b1e41bb8eb17a7f9d763508d8655a19b","contentType":"text/x-python; charset=utf-8"},{"id":"3ae216a4-42fb-54c8-89b5-627abfe03885","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3ae216a4-42fb-54c8-89b5-627abfe03885/attachment.py","path":"scripts/schema_converter.py","size":10371,"sha256":"9883ec5bb234e91eaab77da6428241bb905fb0c4263b8f8742b45a8b601b96fa","contentType":"text/x-python; charset=utf-8"},{"id":"3ded4a37-484f-5ac0-a9cf-bb72e99c6cdd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3ded4a37-484f-5ac0-a9cf-bb72e99c6cdd/attachment.py","path":"scripts/skill.py","size":20289,"sha256":"cfc97f9d767eef406dddd47bab301be189cf3eda7c2bf29711f4f0786f2deb93","contentType":"text/x-python; charset=utf-8"},{"id":"3d717542-2bde-5168-bf95-7d25912e4f00","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3d717542-2bde-5168-bf95-7d25912e4f00/attachment.py","path":"scripts/storage.py","size":11028,"sha256":"7493b6d2ce5ca43f004d1a96c104860b0e3a5d1eb33a4e4175b2787181334baf","contentType":"text/x-python; charset=utf-8"},{"id":"c1b11e30-53e3-5140-95cb-8aa7496fe57b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c1b11e30-53e3-5140-95cb-8aa7496fe57b/attachment","path":"setup/docker/claude/Dockerfile","size":1599,"sha256":"fc5a0026394a1eeb7c916a681f5bf654cfa5385b2d17232bff01c5e4335b7b7c","contentType":"text/plain; charset=utf-8"},{"id":"77d6c99d-43d3-5a15-922a-30c39d4d25a3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/77d6c99d-43d3-5a15-922a-30c39d4d25a3/attachment.md","path":"setup/docker/claude/README.md","size":1103,"sha256":"9577b04cb7b1983d5e501887a56c2d4ecbf5584ad47cdc6335dd2bcc0e342a2d","contentType":"text/markdown; charset=utf-8"},{"id":"a026dc57-e657-55da-bf6a-fb2a5e6e70ec","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a026dc57-e657-55da-bf6a-fb2a5e6e70ec/attachment.sh","path":"setup/docker/claude/build_image.sh","size":436,"sha256":"eb82f5c6342e6c8f0e8badbc5e343f202b3ec62a28573e492b0b4ca87ef85b6a","contentType":"application/x-sh; charset=utf-8"},{"id":"29be0a28-5267-5d9c-8d13-ce2394461ed9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/29be0a28-5267-5d9c-8d13-ce2394461ed9/attachment.sh","path":"setup/docker/claude/entrypoint.sh","size":330,"sha256":"daa4a4f6b858d7a0372ad4bf08f8d258e78cd1a63070776afe26a62aaf04c029","contentType":"application/x-sh; charset=utf-8"},{"id":"3c1ff76f-a77c-52c2-8a78-c5e96011eb15","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3c1ff76f-a77c-52c2-8a78-c5e96011eb15/attachment.sh","path":"setup/docker/claude/start_container.sh","size":520,"sha256":"90f3796a09df23f220741f2ae493b2d33f44e04c85bb5658534ce20f6810d785","contentType":"application/x-sh; charset=utf-8"},{"id":"9aee3f24-b03d-5a2f-b16b-5b77d45400fc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9aee3f24-b03d-5a2f-b16b-5b77d45400fc/attachment.md","path":"steering/accuracy.md","size":1933,"sha256":"9d683899021e4194d002f5f94b17eaca66a611f653445367beabdfdbaa94213c","contentType":"text/markdown; charset=utf-8"},{"id":"13122efd-9793-504f-9954-05c73c65c6cb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/13122efd-9793-504f-9954-05c73c65c6cb/attachment.md","path":"steering/authentication.md","size":585,"sha256":"68469d8ccc90fa354ac606dcf0314f23be66920c265fbe2967fa58149342ab56","contentType":"text/markdown; charset=utf-8"},{"id":"6f63d09e-dd93-5f40-b0a8-86bbee83e568","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6f63d09e-dd93-5f40-b0a8-86bbee83e568/attachment.md","path":"steering/incompatibilities.md","size":3132,"sha256":"f728915955a48b29ae7bc23450d32d37636c0555a6fd161ce950daf8870a1639","contentType":"text/markdown; charset=utf-8"},{"id":"3802ee18-61a9-58de-90e0-c1d627b2fffc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3802ee18-61a9-58de-90e0-c1d627b2fffc/attachment.md","path":"steering/sizing.md","size":2426,"sha256":"837b09bc0bc562e64c5e32392baf2838d71eedf5928fe365e7f265d67c9840e1","contentType":"text/markdown; charset=utf-8"},{"id":"eaf28318-f1f9-5aa8-80cc-ae21cb115235","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/eaf28318-f1f9-5aa8-80cc-ae21cb115235/attachment.md","path":"steering/stakeholders.md","size":617,"sha256":"d9da25565ab6eff329c12d958fa9aff504819a77abd4f00798f8d665c7111b05","contentType":"text/markdown; charset=utf-8"},{"id":"444f4c4d-2c13-5fce-818e-d5ba1ec172c9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/444f4c4d-2c13-5fce-818e-d5ba1ec172c9/attachment.md","path":"steering/transformation-rules.md","size":2849,"sha256":"71cf9d102e5bfc3859d487d4d43a3d16e1dbed72dbb513a7380076d8af444e37","contentType":"text/markdown; charset=utf-8"},{"id":"f139b705-1767-5ffe-bc42-dacbb642e057","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f139b705-1767-5ffe-bc42-dacbb642e057/attachment.py","path":"tests/__init__.py","size":0,"sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","contentType":"text/x-python; charset=utf-8"},{"id":"c1e365d7-1405-5fac-b9e0-2e4a6dea0ae0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c1e365d7-1405-5fac-b9e0-2e4a6dea0ae0/attachment.md","path":"tests/evals/README.md","size":2226,"sha256":"aceded9aa997e6870ada72e2eca6f26ae118192c8f14c5d0dac86fdc32b5fd12","contentType":"text/markdown; charset=utf-8"},{"id":"4061499e-0718-5158-b7a1-cfdc9fabb1d0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4061499e-0718-5158-b7a1-cfdc9fabb1d0/attachment.py","path":"tests/evals/claude_requests.py","size":1161,"sha256":"a8dd30fa725701720761ed7eba5b0affc8ed6db7b65312c4096bfd08aa981cca","contentType":"text/x-python; charset=utf-8"},{"id":"1c34dc32-8268-5a85-9825-d4ccbb70d6dc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1c34dc32-8268-5a85-9825-d4ccbb70d6dc/attachment.yaml","path":"tests/evals/eval.yaml","size":29728,"sha256":"358ae5f3b9221282ff1f712e80c9d25bc23da40e90465adc163a0b05c68c6a20","contentType":"application/yaml; charset=utf-8"},{"id":"0a16fa8a-96fb-56f2-b5c6-4d39bc8c8b55","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0a16fa8a-96fb-56f2-b5c6-4d39bc8c8b55/attachment.sh","path":"tests/scripts/run_evals.sh","size":576,"sha256":"872ebb4a1f970fb4a444905b20b8847b0d024168fe418a25400ccc9d741071f9","contentType":"application/x-sh; charset=utf-8"},{"id":"8c36f9f0-1c9e-555c-a75a-1ceb6e391730","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8c36f9f0-1c9e-555c-a75a-1ceb6e391730/attachment.py","path":"tests/test_query_converter.py","size":9377,"sha256":"380f41b9efa173ca11c27ee530b198e73588ce3ca39911b7fa55530f8667f283","contentType":"text/x-python; charset=utf-8"},{"id":"56aa2fe5-8732-55cc-9a86-f8a00eb34b48","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/56aa2fe5-8732-55cc-9a86-f8a00eb34b48/attachment.py","path":"tests/test_report.py","size":6747,"sha256":"6d769756421c4c5294fca1a06a60718b709e8bd26eb6c95db68ad9d71482869e","contentType":"text/x-python; charset=utf-8"},{"id":"ea85d975-f649-51fd-97ae-ea817ea11611","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ea85d975-f649-51fd-97ae-ea817ea11611/attachment.py","path":"tests/test_schema_converter.py","size":5883,"sha256":"1be668654668ce9623d8eb8c46b00160a7fb67684c37c5cab3eac1bab9b95f11","contentType":"text/x-python; charset=utf-8"},{"id":"dafbd8e0-787e-5d47-a8b6-ac9d3af703dd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dafbd8e0-787e-5d47-a8b6-ac9d3af703dd/attachment.py","path":"tests/test_skill.py","size":9020,"sha256":"948a826b7abb3e4f658c9e32319dc55a10074d7da834d65480d3063c3674644d","contentType":"text/x-python; charset=utf-8"},{"id":"0e554183-0381-522f-9883-369f5e497ae8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/0e554183-0381-522f-9883-369f5e497ae8/attachment.py","path":"tests/test_storage.py","size":7699,"sha256":"c642ddb408b11fa7934c8fae8c1fced4bbdd3e818aa3383a7c061b43b1eb8deb","contentType":"text/x-python; charset=utf-8"},{"id":"6c7c2269-3597-51b8-bf07-319e38b73cd4","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6c7c2269-3597-51b8-bf07-319e38b73cd4/attachment.lock","path":"uv.lock","size":176529,"sha256":"c2d24c31251b49a380defcbc7ac800f18c2fbb4cf6c96ab86e29deb9acf7aa2b","contentType":"text/plain; charset=utf-8"}],"bundle_sha256":"4b2a2acc460db5564acb8a3291cb9478dedddfe33c687edd18910a7630600cf3","attachment_count":43,"text_attachments":40,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":3,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/solr-opensearch-migration-advisor/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"devops-infrastructure","category_label":"DevOps"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"devops-infrastructure","metadata":{"author":"jzonthemtn","version":"0.2.0","keywords":["Solr to OpenSearch","migrate Solr","schema.xml to mapping","solrconfig.xml","edismax to bool query","synonyms.txt","SolrCloud vs OpenSearch Cluster","OpenSearch best practices","AWS OpenSearch Service","OpenSearch regional availability","Authentication migration from Solr to OpenSearch"],"capability":"translation-engine","displayName":"Solr to OpenSearch Migration Advisor"},"import_tag":"clean-skills-v1","description":"Expert in migrating Apache Solr collections to OpenSearch indexes. Translates Solr XML/JSON schemas to OpenSearch mappings and converts Solr syntax (Standard, DisMax, eDisMax) into OpenSearch DSL. Provides sizing for nodes, shards, and JVM heap. Provides guidance auf authentication migration from Solr to OpenSearch. Uses the AWS Knowledge MCP Server for accurate, up-to-date OpenSearch and AWS service information.\n"}},"renderedAt":1782981326660}

Apache Solr to OpenSearch Migration Advisor An agent skill for migrating from Apache Solr to OpenSearch. This skill provides a transport-agnostic migration advisor that can reason about Solr query behavior, configuration, and cluster architecture. When to Use Use this skill when: - A user needs to migrate a Solr collection or SolrCloud deployment to OpenSearch. - A user wants a comprehensive migration advisor that can handle conversational interaction and maintain session context. - A user has a or Solr Schema API JSON document and needs an equivalent OpenSearch index mapping. - A user has So…