From 0f00db0419cfd4b3ef0573d465ac8c3337400e7e Mon Sep 17 00:00:00 2001 From: abhishekreddyallu Date: Tue, 14 Jul 2026 13:39:29 +0200 Subject: [PATCH 1/7] :memo: Add reranking to aligner_pipeline --- docs/source/developerguide/pipeline.rst | 82 ++++++++++++++-------- examples/aligner_pipeline_reranking.py | 91 +++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 28 deletions(-) create mode 100644 examples/aligner_pipeline_reranking.py diff --git a/docs/source/developerguide/pipeline.rst b/docs/source/developerguide/pipeline.rst index 3f944ab..89e8319 100644 --- a/docs/source/developerguide/pipeline.rst +++ b/docs/source/developerguide/pipeline.rst @@ -14,7 +14,7 @@ Pipeline ``AlignerPipeline`` provides a reusable execution flow for running one user-provided encoder and one ontology matching aligner over a collected ontology matching dataset. It is useful when users want direct control over the encoder, aligner, model loading, -LLM dataset batching, and optional postprocessing. +LLM dataset batching, optional reranking, and optional postprocessing. Unlike a full orchestration pipeline, :class:`AlignerPipeline` does not collect datasets, choose methods, define model-specific configurations, evaluate predictions, @@ -22,11 +22,11 @@ or save outputs. It focuses only on running the configured encoder-aligner setup returning predictions. Given two ontologies :math:`O_1` and :math:`O_2`, :class:`AlignerPipeline` produces -a list of correspondence predictions through four stages: +a list of correspondence predictions through five stages: **πŸ”§ 1. Component Setup**: Provide the encoder, aligner, dataset, and optional -pipeline settings such as ``load_params``, ``llm_dataset_class``, ``postprocessor``, -or ``postprocessor_params``. +pipeline settings such as ``load_params``, ``llm_dataset_class``, ``reranker``, +``reranker_load_params``, ``postprocessor``, or ``postprocessor_params``. **βš™οΈ 2. Encoding**: Convert the collected ontology matching dataset into the format expected by the aligner. @@ -34,7 +34,12 @@ expected by the aligner. **🧠 3. Prediction Generation**: Generate predictions from encoded ontology data, with optional LLM dataset batching when ``llm_dataset_class`` is provided. -**🧹 4. Optional Postprocessing**: Apply a user-provided postprocessor to convert, +**πŸ”€ 4. Optional Reranking**: Reorder candidate predictions with a user-provided +reranker before postprocessing. If predictions are flat ``source``/``target``/``score`` +records, the pipeline groups them into ``target-cands`` and ``score-cands`` before +reranking. + +**🧹 5. Optional Postprocessing**: Apply a user-provided postprocessor to convert, filter, or normalize predictions before returning the final pipeline output. Usage @@ -165,8 +170,10 @@ This module guides you through a step-by-step process for running a single ontol .. note:: - A complete aligner pipeline example is available at - `examples/aligner_pipeline.py `_. + Complete examples are available at: + + * `examples/aligner_pipeline.py `_ + * `examples/aligner_pipeline_reranking.py `_ Configuration -------------------- @@ -209,6 +216,22 @@ Configuration - bool - ``False`` - Whether to shuffle LLM dataset batches. + * - **reranker** + - BaseOMModel + - ``None`` + - Optional reranking model used to reorder candidate predictions before postprocessing. + * - **reranker_load_params** + - dict + - ``None`` + - Parameters forwarded to the reranker ``load`` method. + * - **reranker_encoder** + - BaseEncoder + - ``None`` + - Optional encoder used to prepare source and target ontology text for reranking. + * - **reranker_om_dataset** + - dict + - ``None`` + - Optional ontology matching dataset used by the reranker encoder. * - **postprocessor** - Any - ``None`` @@ -221,7 +244,7 @@ Configuration - bool - ``False`` - Whether to pass reference matchings to the encoder. - * - ****kwargs** + * - ``**kwargs`` - dict - ``{}`` - Additional keyword arguments forwarded to the base ontology matching model. @@ -230,24 +253,27 @@ Configuration Example: .. code-block:: python - #FewShotRAG + # Retrieval with optional reranking AlignerPipeline( - encoder=ConceptParentFewShotEncoder(), - aligner=MistralLLMBERTRetrieverFSRAG( - positive_ratio=1.0, - n_shots=1, - retriever_config=retriever_config, - llm_config=llm_config, - ), - om_dataset=dataset, - load_params={ - "llm_path": llm_model_path, - "ir_path": ir_model_path, - }, - postprocessor=rag_heuristic_postprocessor, - postprocessor_params={ - "topk_confidence_ratio": 3, - "topk_confidence_score": 3, - }, - include_reference=True, - ) + encoder=ConceptParentLightweightEncoder(), + aligner=SBERTRetrieval( + device=device, + top_k=10, + ), + om_dataset=dataset, + load_params={ + "path": "all-MiniLM-L6-v2", + }, + reranker=CrossEncoderReranking( + device=device, + top_k=5, + normalize_score="sigmoid", + ), + reranker_load_params={ + "path": "cross-encoder/ms-marco-MiniLM-L6-v2", + }, + postprocessor=retriever_postprocessor, + postprocessor_params={ + "threshold": 0.5, + }, + ) \ No newline at end of file diff --git a/examples/aligner_pipeline_reranking.py b/examples/aligner_pipeline_reranking.py new file mode 100644 index 0000000..52b8412 --- /dev/null +++ b/examples/aligner_pipeline_reranking.py @@ -0,0 +1,91 @@ +import json +import torch +# import os + +from ontoaligner.ontology import MaterialInformationMatOntoOMDataset +from ontoaligner.utils import metrics, xmlify +from ontoaligner.encoder import ConceptParentLightweightEncoder +from ontoaligner.aligner import ( + SBERTRetrieval, + CrossEncoderReranking, + # CohereReranking, +) +from ontoaligner.postprocess import retriever_postprocessor +from ontoaligner import AlignerPipeline + + +# Step 1: Initialize the dataset object +task = MaterialInformationMatOntoOMDataset() +print("Test Task:", task) + +# Step 2: Load source and target ontologies with reference matchings +dataset = task.collect( + source_ontology_path="assets/MI-MatOnto/mi_ontology.xml", + target_ontology_path="assets/MI-MatOnto/matonto_ontology.xml", + reference_matching_path="assets/MI-MatOnto/matchings.xml", +) + +# Step 3: Select the runtime device +device = "cuda" if torch.cuda.is_available() else "cpu" + +# Step 4: Define the aligner pipeline with reranking +reranker = CrossEncoderReranking( + device=device, + top_k=5, + normalize_score="sigmoid", +) + +reranker_load_params = { + "path": "cross-encoder/ms-marco-MiniLM-L6-v2", +} + +# To use Cohere reranking instead of CrossEncoderReranking, replace the +# reranker and reranker_load_params blocks above with: +# +# reranker = CohereReranking( +# cohere_key=os.environ["COHERE_API_KEY"], +# top_k=5, +# normalize_score="none", +# ) +# +# reranker_load_params = { +# "path": "rerank-v3.5", +# } + +aligner_pipeline = AlignerPipeline( + encoder=ConceptParentLightweightEncoder(), + aligner=SBERTRetrieval( + device=device, + top_k=10, + ), + om_dataset=dataset, + load_params={ + "path": "all-MiniLM-L6-v2", + }, + reranker=reranker, + reranker_load_params=reranker_load_params, + postprocessor=retriever_postprocessor, + postprocessor_params={ + "threshold": 0.5, + }, +) + +# Step 5: Generate predictions +matchings = aligner_pipeline.generate() + +# Step 6: Evaluate predictions +evaluation = metrics.evaluation_report( + predicts=matchings, + references=dataset["reference"], +) + +print("\nAligner Pipeline with Reranking Evaluation Report:") +print(json.dumps(evaluation, indent=4)) + +# Step 7: Save XML output +xml_str = xmlify.xml_alignment_generator(matchings=matchings) + +with open("aligner_pipeline_reranking_matchings.xml", "w", encoding="utf-8") as xml_file: + xml_file.write(xml_str) + +print("Saved XML: aligner_pipeline_reranking_matchings.xml") \ No newline at end of file From 800f72cd8ae2fbfb6df62c0a57dd92c3d5cb6724 Mon Sep 17 00:00:00 2001 From: abhishekreddyallu Date: Tue, 14 Jul 2026 13:49:12 +0200 Subject: [PATCH 2/7] :memo: minor changes --- docs/source/developerguide/pipeline.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/source/developerguide/pipeline.rst b/docs/source/developerguide/pipeline.rst index 89e8319..ccd3a07 100644 --- a/docs/source/developerguide/pipeline.rst +++ b/docs/source/developerguide/pipeline.rst @@ -170,11 +170,10 @@ This module guides you through a step-by-step process for running a single ontol .. note:: - Complete examples are available at: + Complete aligner pipeline examples are available at: * `examples/aligner_pipeline.py `_ * `examples/aligner_pipeline_reranking.py `_ - Configuration -------------------- @@ -244,7 +243,7 @@ Configuration - bool - ``False`` - Whether to pass reference matchings to the encoder. - * - ``**kwargs`` + * - ****kwargs** - dict - ``{}`` - Additional keyword arguments forwarded to the base ontology matching model. From a3ec08efa24a5551021bb360340267abe44d602b Mon Sep 17 00:00:00 2001 From: abhishekreddyallu Date: Tue, 14 Jul 2026 15:35:56 +0200 Subject: [PATCH 3/7] :memo: add nested-ensemble-learning-aligners --- docs/source/aligner/ensemble_learning.rst | 93 +++++++++++++++++++ ...le-learning-aligners-in-ontoaligner.ipynb} | 2 +- tutorial/README.md | 14 +-- 3 files changed, 101 insertions(+), 8 deletions(-) rename tutorial/{04-nested-ensemble-aligners-in-ontoaligner.ipynb => 04-nested-ensemble-learning-aligners-in-ontoaligner.ipynb} (99%) diff --git a/docs/source/aligner/ensemble_learning.rst b/docs/source/aligner/ensemble_learning.rst index 557ecf0..105206e 100644 --- a/docs/source/aligner/ensemble_learning.rst +++ b/docs/source/aligner/ensemble_learning.rst @@ -311,6 +311,99 @@ This module guides you through a step-by-step process for performing ensemble-ba A complete ensemble learning example is available at `examples/ensemble.py `_. +Nested Ensemble Learning +---------------------------- + +Nested ensemble learning extends the standard ensemble learning workflow by combining multiple +ensemble groups into one final ensemble. Instead of placing every aligner pipeline in a +single flat ensemble, related pipelines are first grouped with +:class:`EnsembleLearningAligner`. These group-level ensembles are then combined again +with another :class:`EnsembleLearningAligner`. + +This is useful when an alignment workflow uses different groups of signals, such as +retrieval, reranking, graph structure, and LLM-based reasoning. + +.. hint:: + + Nested ensembles can mix different :class:`AlignerPipeline` configurations and + ensemble groups as long as they expose the standard ``generate()`` flow. This makes + it possible to combine pipelines with different encoders, aligners, postprocessors, + and rerankers in the same workflow. + +The nested ensemble follows the flow below: + +.. code-block:: text + + Mouse-Human dataset + β”‚ + β”œβ”€ llm_pipeline ────────┐ + β”œβ”€ rag_pipeline ────────┼─ llm_ensemble ────────────┐ + └─ fsrag_pipeline β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β”‚ + β”œβ”€ lightweight_pipeline ─┐ β”‚ + β”œβ”€ tfidf_pipeline ───────┼─ retrieval_ensemble ─────┼─ nested_ensemble + └─ sbert_pipeline β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ + β”‚ β”‚ + β”œβ”€ sbert_reranking_pipeline ─┐ β”‚ β”‚ + β”œβ”€ tfidf_reranking_pipeline ─┼─ reranking_ensemble β”€β”˜ β”‚ + └─ graph_reranking_pipeline β”€β”˜ β”‚ + ↓ + final_matchings + β”‚ + ↓ + evaluation report + β”‚ + ↓ + XML and JSON export + +A nested ensemble can be configured by first creating the group-level ensembles and then +passing those ensembles into the final ensemble. + +.. code-block:: python + + llm_ensemble = EnsembleLearningAligner( + aligners=[ + ("llm", llm_pipeline, 1.0), + ("rag", rag_pipeline, 1.0), + ("fsrag", fsrag_pipeline, 1.0), + ], + voting=ReciprocalRankFusionVoting(k=60), + ) + + retrieval_ensemble = EnsembleLearningAligner( + aligners=[ + ("lightweight", lightweight_pipeline, 1.0), + ("tfidf", tfidf_pipeline, 1.0), + ("sbert", sbert_pipeline, 1.0), + ], + voting=ReciprocalRankFusionVoting(k=60), + ) + + reranking_ensemble = EnsembleLearningAligner( + aligners=[ + ("sbert_reranking", sbert_reranking_pipeline, 1.0), + ("tfidf_reranking", tfidf_reranking_pipeline, 1.0), + ("graph_reranking", graph_reranking_pipeline, 1.0), + ], + voting=ScoreAverageVoting(), + ) + + nested_ensemble = EnsembleLearningAligner( + aligners=[ + ("llm_ensemble", llm_ensemble, 1.0), + ("retrieval_ensemble", retrieval_ensemble, 1.0), + ("reranking_ensemble", reranking_ensemble, 1.0), + ], + voting=ReciprocalRankFusionVoting(k=60), + ) + + final_matchings = nested_ensemble.generate() + +.. note:: + + A complete tutorial notebook is available at + `tutorial/04-nested-ensemble-aligners-in-ontoaligner.ipynb `_. + Voting Strategies ----------------------- diff --git a/tutorial/04-nested-ensemble-aligners-in-ontoaligner.ipynb b/tutorial/04-nested-ensemble-learning-aligners-in-ontoaligner.ipynb similarity index 99% rename from tutorial/04-nested-ensemble-aligners-in-ontoaligner.ipynb rename to tutorial/04-nested-ensemble-learning-aligners-in-ontoaligner.ipynb index c018e21..f4465b3 100644 --- a/tutorial/04-nested-ensemble-aligners-in-ontoaligner.ipynb +++ b/tutorial/04-nested-ensemble-learning-aligners-in-ontoaligner.ipynb @@ -20,7 +20,7 @@ "--------\n", "\n", "\n", - "# Nested Ensemble Aligners in OntoAligner" + "# Nested Ensemble learning Aligners in OntoAligner" ], "id": "6c39714a95fee23e" }, diff --git a/tutorial/README.md b/tutorial/README.md index f9a17a1..f7e4f02 100644 --- a/tutorial/README.md +++ b/tutorial/README.md @@ -6,13 +6,13 @@ The tutorial is notebook-based and can be run directly in **Google Colab** ![Ope ## πŸ“š Tutorial Contents -| # | Notebook | Description | Open Notebook in Colab | -|:-:|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 1 | [01-quick-introduction-to-ontoaligner.ipynb](01-quick-introduction-to-ontoaligner.ipynb) | **Quick Introduction to OntoAligner**: Overview of OntoAligner, basic concepts, and a simple end-to-end example | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1sDO-vW1SwGrTi9nzrhD0vJeWVQHWPv-z?usp=sharing) | -| 2 | [02-deep-dive-into-ontoaligner-modules-1.ipynb](02-deep-dive-into-ontoaligner-modules-1.ipynb) | **Deep Dive into OntoAligner Modules – Part 1**: Detailed explanation of core modules and internal components | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://drive.google.com/file/d/1lKQ8ChSROiG_KHG2zyTQ0fIQhQzg4cmP/view?usp=sharing) | -| 3 | [03-deep-dive-into-ontoaligner-modules-2.ipynb](03-deep-dive-into-ontoaligner-modules-2.ipynb) | **Deep Dive into OntoAligner Modules – Part 2**: Detailed explanation of Retrieval, LLM, and RAG based Aligners| [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://drive.google.com/file/d/1qx7JDn5WnAiSVUUoOBqWjYvMNjKsBfBN/view?usp=sharing) | -| 4 | [04-nested-ensemble-aligners-in-ontoaligner.ipynb](04-nested-ensemble-aligners-in-ontoaligner.ipynb) | **Nested Ensemble Aligners in OntoAligner**: Building group-level ensembles and combining them into one nested ensemble workflow | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1vKnKYx5ul1bAsKWcUAehRY2OGRex_k3J?usp=sharing) | -| 5 | [05-reusable-reranking-in-ontoaligner.ipynb](05-reusable-reranking-in-ontoaligner.ipynb) | **Reusable Reranking in OntoAligner**: Applying reranking across retrieval, graph-based, flat-output, and RAG-style workflows | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1KJ8wKjsT__TPovDAwhfFEUqOH1ecN5uW?usp=sharing) | +| # | Notebook | Description | Open Notebook in Colab | +|:-:|------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 1 | [01-quick-introduction-to-ontoaligner.ipynb](01-quick-introduction-to-ontoaligner.ipynb) | **Quick Introduction to OntoAligner**: Overview of OntoAligner, basic concepts, and a simple end-to-end example | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1sDO-vW1SwGrTi9nzrhD0vJeWVQHWPv-z?usp=sharing) | +| 2 | [02-deep-dive-into-ontoaligner-modules-1.ipynb](02-deep-dive-into-ontoaligner-modules-1.ipynb) | **Deep Dive into OntoAligner Modules – Part 1**: Detailed explanation of core modules and internal components | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://drive.google.com/file/d/1lKQ8ChSROiG_KHG2zyTQ0fIQhQzg4cmP/view?usp=sharing) | +| 3 | [03-deep-dive-into-ontoaligner-modules-2.ipynb](03-deep-dive-into-ontoaligner-modules-2.ipynb) | **Deep Dive into OntoAligner Modules – Part 2**: Detailed explanation of Retrieval, LLM, and RAG based Aligners | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://drive.google.com/file/d/1qx7JDn5WnAiSVUUoOBqWjYvMNjKsBfBN/view?usp=sharing) | +| 4 | [04-nested-ensemble-learning-aligners-in-ontoaligner.ipynb](04-nested-ensemble-learning-aligners-in-ontoaligner.ipynb) | **Nested Ensemble learning Aligners in OntoAligner**: Building group-level ensembles and combining them into one nested ensemble workflow | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1vKnKYx5ul1bAsKWcUAehRY2OGRex_k3J?usp=sharing) | +| 5 | [05-reusable-reranking-in-ontoaligner.ipynb](05-reusable-reranking-in-ontoaligner.ipynb) | **Reusable Reranking in OntoAligner**: Applying reranking across retrieval, graph-based, flat-output, and RAG-style workflows | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1KJ8wKjsT__TPovDAwhfFEUqOH1ecN5uW?usp=sharing) | ## πŸš€ Getting Started From a747e5c97758c1cc6a9d2b788cef75cc6718c870 Mon Sep 17 00:00:00 2001 From: abhishekreddyallu Date: Tue, 14 Jul 2026 15:54:20 +0200 Subject: [PATCH 4/7] :pencil2: minor change --- docs/source/aligner/ensemble_learning.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/aligner/ensemble_learning.rst b/docs/source/aligner/ensemble_learning.rst index 105206e..3a6a9bb 100644 --- a/docs/source/aligner/ensemble_learning.rst +++ b/docs/source/aligner/ensemble_learning.rst @@ -402,7 +402,7 @@ passing those ensembles into the final ensemble. .. note:: A complete tutorial notebook is available at - `tutorial/04-nested-ensemble-aligners-in-ontoaligner.ipynb `_. + `tutorial/04-nested-ensemble-learning-aligners-in-ontoaligner.ipynb `_. Voting Strategies ----------------------- From eafbeeaedd7476475af5ab9baeece0e462fbfe00 Mon Sep 17 00:00:00 2001 From: abhishekreddyallu Date: Tue, 14 Jul 2026 21:51:29 +0200 Subject: [PATCH 5/7] :memo: add reusable reranking --- docs/source/developerguide/reranking.rst | 478 ++++++++++++++++++ docs/source/index.rst | 1 + ...05-reusable-reranking-in-ontoaligner.ipynb | 11 +- 3 files changed, 487 insertions(+), 3 deletions(-) create mode 100644 docs/source/developerguide/reranking.rst diff --git a/docs/source/developerguide/reranking.rst b/docs/source/developerguide/reranking.rst new file mode 100644 index 0000000..c5064cf --- /dev/null +++ b/docs/source/developerguide/reranking.rst @@ -0,0 +1,478 @@ +Reranking +===================================================== + +.. sidebar:: Useful links: + + * `Developer Guide > Pipeline `_ + * `Retrieval Aligner > Reranking `_ + + +This guide shows how reranking can be used as a reusable candidate-refinement step across OntoAligner workflows. +Reranking is not tied to one specific aligner. It can be applied after any component +that produces multiple target candidates for the same source concept. + +The examples cover the main OntoAligner output styles, including grouped retrieval +candidates, graph-based candidates, RAG-style outputs, and flat predictions from OLaLA, +ensemble, FLORA, LLM-style, and custom workflows. In each case, reranking can be applied +when multiple target candidates are available for a source concept, either directly as +grouped candidates or after grouping flat predictions. + +.. note:: + + Reranking is useful only when multiple target candidates are available for a source. + Single-target outputs, such as PropMatch or fuzzy lightweight results, are usually + not suitable after final selection. + +The common reranking flows are: + +.. code-block:: text + + grouped candidate output ---------------β†’ reranker β†’ postprocessor + + flat source-target-score output --------β†’ group predictions β†’ reranker β†’ postprocessor + + RAG / FSRAG / ICV / LLM final output ---β†’ model-specific postprocessor β†’ group predictions β†’ reranker β†’ postprocessor + + RAG IR output before LLM verification --β†’ reranker β†’ LLM verification β†’ RAG postprocessor + + AlignerPipeline ------------------------β†’ built-in reranker + + +Usage +---------------------------- + +.. tab:: βš™οΈ Setup + + The examples use the MaterialInformation-MatOnto dataset and a CrossEncoder + reranker. + + .. code-block:: python + + # Import required modules + import torch + + from ontoaligner.ontology import MaterialInformationMatOntoOMDataset + from ontoaligner.aligner import CrossEncoderReranking + from ontoaligner.postprocess import retriever_postprocessor + + # Load source and target ontologies + task = MaterialInformationMatOntoOMDataset() + + dataset = task.collect( + source_ontology_path="assets/MI-MatOnto/mi_ontology.xml", + target_ontology_path="assets/MI-MatOnto/matonto_ontology.xml", + reference_matching_path="assets/MI-MatOnto/matchings.xml", + ) + + # Select runtime device + device = "cuda" if torch.cuda.is_available() else "cpu" + + # Initialize and load the reranker + reranker = CrossEncoderReranking( + device=device, + top_k=5, + normalize_score="sigmoid", + ) + + reranker.load( + path="cross-encoder/ms-marco-MiniLM-L6-v2", + ) + + The helper below is used when an aligner returns flat + ``source``-``target``-``score`` predictions. + + .. code-block:: python + + # Convert flat predictions into grouped candidate format + def group_predictions_for_reranking(predictions): + grouped_predictions = {} + + for prediction in predictions: + source = prediction["source"] + target = prediction["target"] + score = prediction.get("score", 0.0) + + if source not in grouped_predictions: + grouped_predictions[source] = { + "source": source, + "target-cands": [], + "score-cands": [], + } + + grouped_predictions[source]["target-cands"].append(target) + grouped_predictions[source]["score-cands"].append(float(score)) + + return list(grouped_predictions.values()) + + +.. tab:: πŸ” Normal Reranking + + Use this pattern when the aligner already returns grouped candidates with + ``target-cands`` and ``score-cands``. + + .. code-block:: python + + # Encode source and target concepts + from ontoaligner.encoder import ConceptParentLightweightEncoder + from ontoaligner.aligner import SBERTRetrieval + + encoder_model = ConceptParentLightweightEncoder() + + source_onto, target_onto = encoder_model( + source=dataset["source"], + target=dataset["target"], + ) + + # Generate grouped retrieval candidates + retriever = SBERTRetrieval( + device=device, + top_k=10, + ) + + retriever.load( + path="all-MiniLM-L6-v2", + ) + + retrieval_outputs = retriever.generate( + input_data=[ + source_onto, + target_onto, + ] + ) + + # Rerank the grouped candidates + reranked_outputs = reranker.generate( + input_data=[ + source_onto, + target_onto, + retrieval_outputs, + ] + ) + + # Convert reranked candidates into final matchings + matchings = retriever_postprocessor( + predicts=reranked_outputs, + threshold=0.5, + ) + + .. note:: + + This pattern applies to retrieval-style aligners and other outputs that already + use ``target-cands`` and ``score-cands``. + + +.. tab:: 🧩 Flat-Output Reranking + + Use this pattern when a workflow produces final flat ``source``-``target``-``score`` + predictions. For RAG-style workflows, apply the RAG postprocessor first and then + group the final matchings for reranking. + + .. code-block:: python + + # Apply RAG postprocessing to produce final flat matchings + flat_predictions, configs = rag_hybrid_postprocessor( + predicts=predicts, + ir_score_threshold=0.5, + llm_confidence_th=0.8, + ) + + # Group flat matchings for reranking + grouped_candidates = group_predictions_for_reranking( + predictions=flat_predictions, + ) + + # Rerank grouped candidates + reranked_outputs = reranker.generate( + input_data=[ + source_onto, + target_onto, + grouped_candidates, + ] + ) + + # Convert reranked candidates into final matchings + matchings = retriever_postprocessor( + predicts=reranked_outputs, + threshold=0.5, + ) + + For direct flat outputs, such as OLaLA, the output from ``generate()`` can be grouped + directly before reranking. + + .. code-block:: python + + # Generate flat OLaLA alignments + flat_predictions = olala.generate( + input_data=encoded_ontology, + ) + + # Group flat alignments for reranking + grouped_candidates = group_predictions_for_reranking( + predictions=flat_predictions, + ) + + .. note:: + + This pattern applies to OLaLA, ensemble outputs, FLORA-style outputs, and final + flat outputs from RAG, FewShotRAG, ICV, standalone LLM workflows, or custom + aligners. + +.. tab:: πŸ•ΈοΈ Graph-Based Reranking + + Graph-based aligners can be reranked when they keep multiple candidates per source. + In the tutorial, ``ConvEAligner`` is used with ``retriever=True``. + + .. code-block:: python + + # Load graph ontology data + from ontoaligner.ontology import GraphTripleOMDataset + from ontoaligner.encoder import GraphTripleEncoder, ConceptParentLightweightEncoder + from ontoaligner.aligner import ConvEAligner + + graph_task = GraphTripleOMDataset( + ontology_name="MI-MatOnto", + ) + + graph_dataset = graph_task.collect( + source_ontology_path="assets/MI-MatOnto/mi_ontology.xml", + target_ontology_path="assets/MI-MatOnto/matonto_ontology.xml", + reference_matching_path="assets/MI-MatOnto/matchings.xml", + ) + + # Encode graph triples + graph_encoder = GraphTripleEncoder() + + encoded_graph_dataset = graph_encoder(**graph_dataset) + + # Encode source and target text for reranking + text_task = MaterialInformationMatOntoOMDataset() + + text_dataset = text_task.collect( + source_ontology_path="assets/MI-MatOnto/mi_ontology.xml", + target_ontology_path="assets/MI-MatOnto/matonto_ontology.xml", + reference_matching_path="assets/MI-MatOnto/matchings.xml", + ) + + text_encoder = ConceptParentLightweightEncoder() + + source_onto, target_onto = text_encoder( + source=text_dataset["source"], + target=text_dataset["target"], + ) + + # Generate graph-based candidate outputs + aligner = ConvEAligner( + model="ConvE", + device="cpu", + embedding_dim=300, + num_epochs=3, + train_batch_size=128, + eval_batch_size=64, + num_negs_per_pos=5, + random_seed=42, + retriever=True, + top_k=10, + ) + + graph_candidates = aligner.generate( + input_data=encoded_graph_dataset, + ) + + # Rerank graph-generated candidates + reranked_graph_candidates = reranker.generate( + input_data=[ + source_onto, + target_onto, + graph_candidates, + ] + ) + + # Convert reranked candidates into final matchings + reranked_graph_matchings = retriever_postprocessor( + predicts=reranked_graph_candidates, + threshold=0.3, + ) + + .. note:: + + This pattern applies to graph-based aligners when candidate retrieval is enabled, + such as ``ConvEAligner`` with ``retriever=True``. + + +.. tab:: 🧠 RAG IR-Output Reranking + + RAG-style aligners produce IR candidates before LLM verification. These + ``ir-outputs`` are grouped candidates, so they can be reranked directly before being + sent to the LLM. + + .. code-block:: python + + # Encode source and target concepts for RAG + from ontoaligner.encoder import ConceptParentRAGEncoder + from ontoaligner.aligner.rag.rag import RAG, AutoModelDecoderRAGLLMV2 + from ontoaligner.aligner.retrieval.models import SBERTRetrieval + from ontoaligner.postprocess import rag_hybrid_postprocessor + + encoder_model = ConceptParentRAGEncoder() + + encoded_ontology = encoder_model( + source=dataset["source"], + target=dataset["target"], + ) + + # Initialize and load the RAG aligner + model = RAG( + retriever=SBERTRetrieval, + llm=AutoModelDecoderRAGLLMV2, + retriever_config={ + "device": device, + "top_k": 10, + "threshold": 0.1, + }, + llm_config={ + "device": "cpu", + "max_length": 300, + "max_new_tokens": 10, + "batch_size": 15, + "answer_set": { + "yes": ["yes", "correct", "true", "positive", "valid"], + "no": ["no", "incorrect", "false", "negative", "invalid"], + }, + }, + ) + + model.load( + llm_path="distilgpt2", + ir_path="all-MiniLM-L6-v2", + ) + + # Generate IR candidate outputs + retrieval_input = encoded_ontology["retriever-encoder"]()( + **encoded_ontology["task-args"] + ) + + source_onto = retrieval_input[0] + target_onto = retrieval_input[1] + + ir_outputs = model.Retrieval.generate( + input_data=retrieval_input, + ) + + # Rerank IR candidates before LLM verification + reranked_ir_outputs = reranker.generate( + input_data=[ + source_onto, + target_onto, + ir_outputs, + ] + ) + + # Convert reranked IR candidates into source-target pairs + reranked_ir_matchings = retriever_postprocessor( + predicts=reranked_ir_outputs, + threshold=0.5, + ) + + # Verify reranked candidates with the LLM + llm_predictions = model.llm_generate( + input_data=encoded_ontology, + ir_output=reranked_ir_matchings, + ) + + # Apply RAG postprocessing + predicts = [ + {"ir-outputs": reranked_ir_outputs}, + {"llm-output": llm_predictions}, + ] + + matchings, configs = rag_hybrid_postprocessor( + predicts=predicts, + ir_score_threshold=0.5, + llm_confidence_th=0.8, + ) + + .. note:: + + This pattern applies to RAG, FewShotRAG, ICV, and custom RAG-style workflows + when retrieval candidates are reranked before LLM verification. + + +.. tab:: 🧬 AlignerPipeline Reranking + + ``AlignerPipeline`` can apply reranking inside the pipeline when the aligner's raw + output is already rerankable. The built-in ``AlignerPipeline`` flow is: + + .. code-block:: text + + encoder + ↓ + aligner + ↓ + reranker + ↓ + postprocessor + ↓ + final matchings + + .. code-block:: python + + # Configure AlignerPipeline with a reranker + from ontoaligner import AlignerPipeline + from ontoaligner.encoder import ConceptParentLightweightEncoder + from ontoaligner.aligner import SBERTRetrieval, CrossEncoderReranking + + aligner_pipeline = AlignerPipeline( + encoder=ConceptParentLightweightEncoder(), + aligner=SBERTRetrieval( + device=device, + top_k=10, + ), + om_dataset=dataset, + load_params={ + "path": "all-MiniLM-L6-v2", + }, + reranker=CrossEncoderReranking( + device=device, + top_k=5, + normalize_score="sigmoid", + ), + reranker_load_params={ + "path": "cross-encoder/ms-marco-MiniLM-L6-v2", + }, + postprocessor=retriever_postprocessor, + postprocessor_params={ + "threshold": 0.5, + }, + ) + + # Generate final matchings + matchings = aligner_pipeline.generate() + + + .. note:: + + This pattern applies to ``AlignerPipeline`` when the aligner's raw output is + already grouped or flat candidate output before postprocessing. + +Key Takeaways +---------------------------- + +- Reranking is a reusable candidate-refinement step in OntoAligner. +- Reranking can be applied when an aligner output keeps multiple target candidates for + each source concept. +- Grouped outputs with ``target-cands`` and ``score-cands`` can be reranked directly. +- Flat outputs with ``source``, ``target``, and ``score`` can be grouped by source + before reranking. +- Direct flat outputs, such as OLaLA, ensemble, FLORA, and custom flat aligner outputs, + can be grouped and reranked using the flat-output reranking pattern. +- RAG, FewShotRAG, ICV, and LLM-style workflows should first use their model-specific + postprocessor to produce flat matchings before final-output reranking. +- RAG-style ``ir-outputs`` can also be reranked before LLM verification. +- Reranking is usually not useful after final single-target selection, such as + PropMatch or fuzzy lightweight outputs. + + +.. note:: + + A complete tutorial notebook is available at + `tutorial/05-reusable-reranking-in-ontoaligner.ipynb `_. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 26d5a34..1c124c5 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -117,6 +117,7 @@ or if you are using Knowledge Graph Embeddings refer to `OntoAligner Meets Knowl developerguide/parsers developerguide/metrics developerguide/pipeline + developerguide/reranking .. toctree:: :caption: Aligners diff --git a/tutorial/05-reusable-reranking-in-ontoaligner.ipynb b/tutorial/05-reusable-reranking-in-ontoaligner.ipynb index eeaebd7..740fdb6 100644 --- a/tutorial/05-reusable-reranking-in-ontoaligner.ipynb +++ b/tutorial/05-reusable-reranking-in-ontoaligner.ipynb @@ -440,15 +440,18 @@ "cell_type": "markdown", "source": [ "---\n", + "\n", "## 3️⃣ Flat-Output Reranking\n", "\n", "Flat-output reranking applies to aligners that return flat `source`–`target`–`score` predictions.\n", "\n", - "In this section, [OLaLA](https://ontoaligner.readthedocs.io/aligner/olala.html) is used to show the flat-output case. The same idea also applies to other aligners that produce flat predictions, such as ensemble outputs, final LLM/RAG outputs, or FLORA-style outputs.\n", + "In this section, [OLaLA](https://ontoaligner.readthedocs.io/aligner/olala.html) is used to show the flat-output case. The same idea also applies to ensemble outputs, FLORA-style outputs, custom aligners, and final LLM/RAG-style outputs.\n", + "\n", + "Some aligners, such as OLaLA, return flat predictions directly. RAG-style workflows usually need their postprocessor first, such as `rag_hybrid_postprocessor` or `rag_heuristic_postprocessor`, to convert IR and LLM outputs into final flat matchings.\n", "\n", - "Before reranking, the flat predictions are grouped by source concept. After grouping, they follow the same candidate format used by retrieval-based reranking and can be passed to `CrossEncoderReranking`.\n", + "Before reranking, flat predictions are grouped by source concept. After grouping, they follow the same candidate format used by retrieval-based reranking and can be passed to `CrossEncoderReranking`.\n", "\n", - "This strategy is most useful when the flat output contains multiple target candidates for the same source concept. If an aligner already returns only one final target per source, such as a one-best or one-to-one matcher, there may be nothing meaningful left to rerank.\n", + "This strategy is most useful when the flat output contains multiple target candidates for the same source concept. If an aligner already returns only one final target per source, there may be nothing meaningful left to rerank.\n", "\n", "The workflow below shows how flat alignment outputs are converted into grouped candidates and reranked.\n", "\n", @@ -457,6 +460,8 @@ " ↓\n", "Flat source-target-score alignments\n", " ↓\n", + "[For RAG-style workflows: RAG/model-specific postprocessor first]\n", + " ↓\n", "group_predictions_for_reranking\n", " ↓\n", "Grouped candidate format\n", From 135276707d1c2709bee2b71a4ce055d3613a2e05 Mon Sep 17 00:00:00 2001 From: abhishekreddyallu Date: Thu, 16 Jul 2026 23:07:56 +0200 Subject: [PATCH 6/7] :memo: minor fixes to docs --- docs/source/aligner/ensemble_learning.rst | 28 +----- docs/source/developerguide/pipeline.rst | 25 ++--- docs/source/developerguide/reranking.rst | 110 +++------------------- docs/source/index.rst | 2 +- 4 files changed, 34 insertions(+), 131 deletions(-) diff --git a/docs/source/aligner/ensemble_learning.rst b/docs/source/aligner/ensemble_learning.rst index 3a6a9bb..3a086e6 100644 --- a/docs/source/aligner/ensemble_learning.rst +++ b/docs/source/aligner/ensemble_learning.rst @@ -332,29 +332,11 @@ retrieval, reranking, graph structure, and LLM-based reasoning. The nested ensemble follows the flow below: -.. code-block:: text - - Mouse-Human dataset - β”‚ - β”œβ”€ llm_pipeline ────────┐ - β”œβ”€ rag_pipeline ────────┼─ llm_ensemble ────────────┐ - └─ fsrag_pipeline β”€β”€β”€β”€β”€β”€β”˜ β”‚ - β”‚ - β”œβ”€ lightweight_pipeline ─┐ β”‚ - β”œβ”€ tfidf_pipeline ───────┼─ retrieval_ensemble ─────┼─ nested_ensemble - └─ sbert_pipeline β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ - β”‚ β”‚ - β”œβ”€ sbert_reranking_pipeline ─┐ β”‚ β”‚ - β”œβ”€ tfidf_reranking_pipeline ─┼─ reranking_ensemble β”€β”˜ β”‚ - └─ graph_reranking_pipeline β”€β”˜ β”‚ - ↓ - final_matchings - β”‚ - ↓ - evaluation report - β”‚ - ↓ - XML and JSON export +.. raw:: html + +
+ +
A nested ensemble can be configured by first creating the group-level ensembles and then passing those ensembles into the final ensemble. diff --git a/docs/source/developerguide/pipeline.rst b/docs/source/developerguide/pipeline.rst index ccd3a07..9b31112 100644 --- a/docs/source/developerguide/pipeline.rst +++ b/docs/source/developerguide/pipeline.rst @@ -14,7 +14,7 @@ Pipeline ``AlignerPipeline`` provides a reusable execution flow for running one user-provided encoder and one ontology matching aligner over a collected ontology matching dataset. It is useful when users want direct control over the encoder, aligner, model loading, -LLM dataset batching, optional reranking, and optional postprocessing. +LLM dataset batching, reranking, and postprocessing. Unlike a full orchestration pipeline, :class:`AlignerPipeline` does not collect datasets, choose methods, define model-specific configurations, evaluate predictions, @@ -24,24 +24,27 @@ returning predictions. Given two ontologies :math:`O_1` and :math:`O_2`, :class:`AlignerPipeline` produces a list of correspondence predictions through five stages: -**πŸ”§ 1. Component Setup**: Provide the encoder, aligner, dataset, and optional -pipeline settings such as ``load_params``, ``llm_dataset_class``, ``reranker``, +**πŸ”§ 1. Component Setup**: Provide the encoder, aligner, dataset, and pipeline settings such as ``load_params``, ``llm_dataset_class``, ``reranker``, ``reranker_load_params``, ``postprocessor``, or ``postprocessor_params``. **βš™οΈ 2. Encoding**: Convert the collected ontology matching dataset into the format expected by the aligner. **🧠 3. Prediction Generation**: Generate predictions from encoded ontology data, with -optional LLM dataset batching when ``llm_dataset_class`` is provided. +LLM dataset batching when ``llm_dataset_class`` is provided. -**πŸ”€ 4. Optional Reranking**: Reorder candidate predictions with a user-provided +**πŸ”€ 4. Reranking**: Reorder candidate predictions with a user-provided reranker before postprocessing. If predictions are flat ``source``/``target``/``score`` records, the pipeline groups them into ``target-cands`` and ``score-cands`` before reranking. -**🧹 5. Optional Postprocessing**: Apply a user-provided postprocessor to convert, +**🧹 5. Postprocessing**: Apply a user-provided postprocessor to convert, filter, or normalize predictions before returning the final pipeline output. +.. note:: + + Reranking and postprocessing are optional pipeline stages. Skip them when raw aligner outputs are needed. + Usage ---------- @@ -218,7 +221,7 @@ Configuration * - **reranker** - BaseOMModel - ``None`` - - Optional reranking model used to reorder candidate predictions before postprocessing. + - reranking model used to reorder candidate predictions before postprocessing. * - **reranker_load_params** - dict - ``None`` @@ -226,15 +229,15 @@ Configuration * - **reranker_encoder** - BaseEncoder - ``None`` - - Optional encoder used to prepare source and target ontology text for reranking. + - encoder used to prepare source and target ontology text for reranking. * - **reranker_om_dataset** - dict - ``None`` - - Optional ontology matching dataset used by the reranker encoder. + - ontology matching dataset used by the reranker encoder. * - **postprocessor** - Any - ``None`` - - Optional postprocessor applied to pipeline predictions. + - postprocessor applied to pipeline predictions. * - **postprocessor_params** - dict - ``None`` @@ -252,7 +255,7 @@ Configuration Example: .. code-block:: python - # Retrieval with optional reranking + # Retrieval with reranking AlignerPipeline( encoder=ConceptParentLightweightEncoder(), aligner=SBERTRetrieval( diff --git a/docs/source/developerguide/reranking.rst b/docs/source/developerguide/reranking.rst index c5064cf..2dad276 100644 --- a/docs/source/developerguide/reranking.rst +++ b/docs/source/developerguide/reranking.rst @@ -3,8 +3,8 @@ Reranking .. sidebar:: Useful links: - * `Developer Guide > Pipeline `_ * `Retrieval Aligner > Reranking `_ + * `Developer Guide > Pipeline `_ This guide shows how reranking can be used as a reusable candidate-refinement step across OntoAligner workflows. @@ -23,20 +23,13 @@ grouped candidates or after grouping flat predictions. Single-target outputs, such as PropMatch or fuzzy lightweight results, are usually not suitable after final selection. -The common reranking flows are: - -.. code-block:: text - - grouped candidate output ---------------β†’ reranker β†’ postprocessor +The common reranking workflows in OntoAligner are: - flat source-target-score output --------β†’ group predictions β†’ reranker β†’ postprocessor - - RAG / FSRAG / ICV / LLM final output ---β†’ model-specific postprocessor β†’ group predictions β†’ reranker β†’ postprocessor - - RAG IR output before LLM verification --β†’ reranker β†’ LLM verification β†’ RAG postprocessor - - AlignerPipeline ------------------------β†’ built-in reranker +.. raw:: html +
+ +
Usage ---------------------------- @@ -105,7 +98,7 @@ Usage return list(grouped_predictions.values()) -.. tab:: πŸ” Normal Reranking +.. tab:: πŸ” Grouped Output Reranking Use this pattern when the aligner already returns grouped candidates with ``target-cands`` and ``score-cands``. @@ -161,7 +154,7 @@ Usage use ``target-cands`` and ``score-cands``. -.. tab:: 🧩 Flat-Output Reranking +.. tab:: 🧩 Flat Output Reranking Use this pattern when a workflow produces final flat ``source``-``target``-``score`` predictions. For RAG-style workflows, apply the RAG postprocessor first and then @@ -217,10 +210,11 @@ Usage flat outputs from RAG, FewShotRAG, ICV, standalone LLM workflows, or custom aligners. -.. tab:: πŸ•ΈοΈ Graph-Based Reranking +.. tab:: πŸ•ΈοΈ Graph Candidate Reranking - Graph-based aligners can be reranked when they keep multiple candidates per source. - In the tutorial, ``ConvEAligner`` is used with ``retriever=True``. + Graph candidate reranking applies when a graph-based aligner keeps multiple target + candidates per source. In the tutorial, ``ConvEAligner`` is used with + ``retriever=True``. .. code-block:: python @@ -295,8 +289,8 @@ Usage .. note:: - This pattern applies to graph-based aligners when candidate retrieval is enabled, - such as ``ConvEAligner`` with ``retriever=True``. + This pattern applies to graph-based aligners when candidate retrieval is enabled. + When ``retriever=False``, the output may not contain multiple candidates to rerank. .. tab:: 🧠 RAG IR-Output Reranking @@ -396,82 +390,6 @@ Usage This pattern applies to RAG, FewShotRAG, ICV, and custom RAG-style workflows when retrieval candidates are reranked before LLM verification. - -.. tab:: 🧬 AlignerPipeline Reranking - - ``AlignerPipeline`` can apply reranking inside the pipeline when the aligner's raw - output is already rerankable. The built-in ``AlignerPipeline`` flow is: - - .. code-block:: text - - encoder - ↓ - aligner - ↓ - reranker - ↓ - postprocessor - ↓ - final matchings - - .. code-block:: python - - # Configure AlignerPipeline with a reranker - from ontoaligner import AlignerPipeline - from ontoaligner.encoder import ConceptParentLightweightEncoder - from ontoaligner.aligner import SBERTRetrieval, CrossEncoderReranking - - aligner_pipeline = AlignerPipeline( - encoder=ConceptParentLightweightEncoder(), - aligner=SBERTRetrieval( - device=device, - top_k=10, - ), - om_dataset=dataset, - load_params={ - "path": "all-MiniLM-L6-v2", - }, - reranker=CrossEncoderReranking( - device=device, - top_k=5, - normalize_score="sigmoid", - ), - reranker_load_params={ - "path": "cross-encoder/ms-marco-MiniLM-L6-v2", - }, - postprocessor=retriever_postprocessor, - postprocessor_params={ - "threshold": 0.5, - }, - ) - - # Generate final matchings - matchings = aligner_pipeline.generate() - - - .. note:: - - This pattern applies to ``AlignerPipeline`` when the aligner's raw output is - already grouped or flat candidate output before postprocessing. - -Key Takeaways ----------------------------- - -- Reranking is a reusable candidate-refinement step in OntoAligner. -- Reranking can be applied when an aligner output keeps multiple target candidates for - each source concept. -- Grouped outputs with ``target-cands`` and ``score-cands`` can be reranked directly. -- Flat outputs with ``source``, ``target``, and ``score`` can be grouped by source - before reranking. -- Direct flat outputs, such as OLaLA, ensemble, FLORA, and custom flat aligner outputs, - can be grouped and reranked using the flat-output reranking pattern. -- RAG, FewShotRAG, ICV, and LLM-style workflows should first use their model-specific - postprocessor to produce flat matchings before final-output reranking. -- RAG-style ``ir-outputs`` can also be reranked before LLM verification. -- Reranking is usually not useful after final single-target selection, such as - PropMatch or fuzzy lightweight outputs. - - .. note:: A complete tutorial notebook is available at diff --git a/docs/source/index.rst b/docs/source/index.rst index 1c124c5..5fd8abd 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -116,8 +116,8 @@ or if you are using Knowledge Graph Embeddings refer to `OntoAligner Meets Knowl developerguide/parsers developerguide/metrics - developerguide/pipeline developerguide/reranking + developerguide/pipeline .. toctree:: :caption: Aligners From 409380f97f0963fd3593a422420bc1e9d0ab97f3 Mon Sep 17 00:00:00 2001 From: abhishekreddyallu Date: Wed, 29 Jul 2026 15:07:38 +0200 Subject: [PATCH 7/7] :memo: minor improvements to docs --- docs/source/aligner/ensemble_learning.rst | 2 +- docs/source/developerguide/reranking.rst | 39 ++++++++++++++++++---- docs/source/img/nested_ensemble.png | Bin 0 -> 99803 bytes 3 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 docs/source/img/nested_ensemble.png diff --git a/docs/source/aligner/ensemble_learning.rst b/docs/source/aligner/ensemble_learning.rst index 3a086e6..1dea227 100644 --- a/docs/source/aligner/ensemble_learning.rst +++ b/docs/source/aligner/ensemble_learning.rst @@ -335,7 +335,7 @@ The nested ensemble follows the flow below: .. raw:: html
- +
A nested ensemble can be configured by first creating the group-level ensembles and then diff --git a/docs/source/developerguide/reranking.rst b/docs/source/developerguide/reranking.rst index 2dad276..827c372 100644 --- a/docs/source/developerguide/reranking.rst +++ b/docs/source/developerguide/reranking.rst @@ -23,13 +23,6 @@ grouped candidates or after grouping flat predictions. Single-target outputs, such as PropMatch or fuzzy lightweight results, are usually not suitable after final selection. -The common reranking workflows in OntoAligner are: - -.. raw:: html - -
- -
Usage ---------------------------- @@ -390,6 +383,38 @@ Usage This pattern applies to RAG, FewShotRAG, ICV, and custom RAG-style workflows when retrieval candidates are reranked before LLM verification. +Configuration +---------------------------- + +Reranking can be applied through different workflow patterns depending on the aligner +output format: + +.. list-table:: + :header-rows: 1 + :widths: 28 42 30 + + * - Aligner output + - Before reranking + - After reranking + * - Grouped output + - Use ``target-cands`` and ``score-cands`` directly. + - Apply ``retriever_postprocessor``. + * - Flat output + - Group flat ``source``-``target``-``score`` predictions by source. + - Apply ``retriever_postprocessor``. + * - Graph candidate output + - Use ``retriever=True`` and encode source and target text for reranking. + - Apply ``retriever_postprocessor``. + * - RAG IR output + - Use ``ir-outputs`` directly before LLM verification. + - Run LLM verification and RAG postprocessing. + * - RAG / FewShotRAG / ICV / LLM final output + - Apply the model-specific postprocessor, then group flat predictions by source. + - Apply ``retriever_postprocessor``. + * - ``AlignerPipeline`` + - Configure ``reranker`` and ``postprocessor`` during pipeline initialization. + - The configured postprocessor is applied internally. + .. note:: A complete tutorial notebook is available at diff --git a/docs/source/img/nested_ensemble.png b/docs/source/img/nested_ensemble.png new file mode 100644 index 0000000000000000000000000000000000000000..7206a858694a675f470c419d2893328904ebc96d GIT binary patch literal 99803 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 ImCSGdKYFIh$^ZZW literal 0 HcmV?d00001