Repository: allenai/ir_datasets (language: python)

Issue:
"every doc needs a text field/property" policy
**Is your feature request related to a problem? Please describe.**

@andrewyates points out these two use cases when working with docs:
 1. do something sane for the situation where you want to swap out the datasets without specializing your model to one (e.g., treat cord section headers as part of the paragraph)
 2. tailor your approach to collection-specific markup, like cord section headers, extra WaPo fields, maybe dates in robust04, etc

Right now the design favours (2) -- trying to remain as true to the original dataset as possible. But the (1) use case is still pretty valuable, and right now it can be tricky -- especially when some fields contain lists of sub-elements. (Without this, it's not so hard to always have the user specify a list of fields to concatenate.)

**Describe the solution you'd like**

Enforce a new policy that all document types need a `text` field that concatenates all "valuable" text (judgment call here), without markup. To avoid excessive redundancy/memory/etc., this could be in the form of a property of the named tuple, like so:

```
>>> from typing import NamedTuple
>>> class Doc(NamedTuple):
...  doc_id: str
...  title: str
...  body: str
...  @property
...  def text(self):
...   return f'{self.title} {self.body}'
... 
>>> Doc('1', 'title', 'body').text
'title body'
```

**Describe alternatives you've considered**

As mentioned above, the original design was pushing the concatenation work on to the user. But this gets much more challenging for list-based fields. This technique can still be used, even if there's this new policy.

**Additional context**

We already have a policy where all documents need a `doc_id`.

Do we need a similar policy for queries? Probably not, as the query fields tend to be distinct representations of the information need---which probably shouldn't be combined, as that would not really reflect a real-life situation.

Since properties are not represented in the NamedTuple fields, how is this incorporated into the documentation?

Does any code that relies on `_fields` or `__annotations__` need to be updated to reflect the new `text` property?

Does this have an effect on pickled content (i.e., in built docstores)? Will we need to include migration logic for a ton of datasets (uggh) or will the new property flow through automatically in new versions?

Is there a complication with the trend for approaches to passage the document body and prepend the title to every passage (e.g., in ColBERT and others, I think)? This would mean that the first passage may have the title repeated, for instance.

How much is too much work to be done in the property? Could HTML parsing be done there? Should it be cached once processed?
"every doc needs a text field/property" policy
**Is your feature request related to a problem? Please describe.**

@andrewyates points out these two use cases when working with docs:
 1. do something sane for the situation where you want to swap out the datasets without specializing your model to one (e.g., treat cord section headers as part of the paragraph)
 2. tailor your approach to collection-specific markup, like cord section headers, extra WaPo fields, maybe dates in robust04, etc

Right now the design favours (2) -- trying to remain as true to the original dataset as possible. But the (1) use case is still pretty valuable, and right now it can be tricky -- especially when some fields contain lists of sub-elements. (Without this, it's not so hard to always have the user specify a list of fields to concatenate.)

**Describe the solution you'd like**

Enforce a new policy that all document types need a `text` field that concatenates all "valuable" text (judgment call here), without markup. To avoid excessive redundancy/memory/etc., this could be in the form of a property of the named tuple, like so:

```
>>> from typing import NamedTuple
>>> class Doc(NamedTuple):
...  doc_id: str
...  title: str
...  body: str
...  @property
...  def text(self):
...   return f'{self.title} {self.body}'
... 
>>> Doc('1', 'title', 'body').text
'title body'
```

**Describe alternatives you've considered**

As mentioned above, the original design was pushing the concatenation work on to the user. But this gets much more challenging for list-based fields. This technique can still be used, even if there's this new policy.

**Additional context**

We already have a policy where all documents need a `doc_id`.

Do we need a similar policy for queries? Probably not, as the query fields tend to be distinct representations of the information need---which probably shouldn't be combined, as that would not really reflect a real-life situation.

Since properties are not represented in the NamedTuple fields, how is this incorporated into the documentation?

Does any code that relies on `_fields` or `__annotations__` need to be updated to reflect the new `text` property?

Does this have an effect on pickled content (i.e., in built docstores)? Will we need to include migration logic for a ton of datasets (uggh) or will the new property flow through automatically in new versions?

Is there a complication with the trend for approaches to passage the document body and prepend the title to every passage (e.g., in ColBERT and others, I think)? This would mean that the first passage may have the title repeated, for instance.

How much is too much work to be done in the property? Could HTML parsing be done there? Should it be cached once processed?


Discussion / hints:
Re-visit pyterrier examples (#73) when this change is made. Should it always use `text` instead of specifying individual fields? Should this be the new default setting in pyterrier itself?
Chatting with @eugene-yang -- having this as a method called `.default_text()` may be better.
 1. It shows the user that it may involve some compute (as a method instead of a property)
 2. It indicates that it's just the "default" configuration -- not necessarily all the text.
 3. Doesn't conflict with existing `text` fields
Re-visit pyterrier examples (#73) when this change is made. Should it always use `text` instead of specifying individual fields? Should this be the new default setting in pyterrier itself?
Chatting with @eugene-yang -- having this as a method called `.default_text()` may be better.
 1. It shows the user that it may involve some compute (as a method instead of a property)
 2. It indicates that it's just the "default" configuration -- not necessarily all the text.
 3. Doesn't conflict with existing `text` fields

Fix patch:
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
index 06481df6..8fe84c22 100644
--- a/.github/workflows/push.yml
+++ b/.github/workflows/push.yml
@@ -45,12 +45,12 @@ jobs:
       if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOs-latest'
       run: |
         pip install pytest
-        pytest test/util.py test/metadata.py test/integration/dummy.py test/integration/vaswani.py test/formats/
+        pytest test/util.py test/metadata.py test/integration/dummy.py test/integration/vaswani.py test/formats/ test/test_defaulttext.py
     - name: Test-windows with pytest
       if: matrix.os == 'windows-latest'
       shell: cmd
       run: |
         pip install pytest
-        pytest test\util.py test\metadata.py test\integration\dummy.py test\integration\vaswani.py test\formats\
+        pytest test\util.py test\metadata.py test\integration\dummy.py test\integration\vaswani.py test\formats\ test\test_defaulttext.py
       env:
         PATH: 'C:/Program Files/zlib/bin/'
diff --git a/ir_datasets/datasets/aol_ia.py b/ir_datasets/datasets/aol_ia.py
index baada6bd..78431c74 100644
--- a/ir_datasets/datasets/aol_ia.py
+++ b/ir_datasets/datasets/aol_ia.py
@@ -43,6 +43,11 @@ class AolIaDoc(NamedTuple):
     text: str
     url: str
     ia_url: str
+    def default_text(self):
+        """
+        title and text
+        """
+        return f'{self.title} {self.text}'
 
 
 class AolQlogs(BaseQlogs):
diff --git a/ir_datasets/datasets/beir.py b/ir_datasets/datasets/beir.py
index b027a702..d31e4936 100644
--- a/ir_datasets/datasets/beir.py
+++ b/ir_datasets/datasets/beir.py
@@ -18,18 +18,33 @@ class BeirDoc(NamedTuple):
     text: str
     title: str
     metadata: Dict[str, str]
+    def default_text(self):
+        """
+        title text
+        """
+        return f'{self.title} {self.text}'
 
 
 class BeirTitleDoc(NamedTuple):
     doc_id: str
     text: str
     title: str
+    def default_text(self):
+        """
+        title text
+        """
+        return f'{self.title} {self.text}'
 
 class BeirTitleUrlDoc(NamedTuple):
     doc_id: str
     text: str
     title: str
     url: str
+    def default_text(self):
+        """
+        title text
+        """
+        return f'{self.title} {self.text}'
 
 class BeirSciDoc(NamedTuple):
     doc_id: str
@@ -46,6 +61,11 @@ class BeirCordDoc(NamedTuple):
     title: str
     url: str
     pubmed_id: str
+    def default_text(self):
+        """
+        title text
+        """
+        return f'{self.title} {self.text}'
 
 class BeirToucheDoc(NamedTuple):
     doc_id: str
@@ -59,6 +79,11 @@ class BeirCqaDoc(NamedTuple):
     text: str
     title: str
     tags: List[str]
+    def default_text(self):
+        """
+        title text
+        """
+        return f'{self.title} {self.text}'
 
 class BeirUrlQuery(NamedTuple):
     query_id: str
@@ -84,11 +109,21 @@ class BeirCovidQuery(NamedTuple):
     text: str
     query: str
     narrative: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 class BeirCqaQuery(NamedTuple):
     query_id: str
     text: str
     tags: List[str]
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 def _map_field(field, data):
     if field in ('doc_id', 'query_id'):
diff --git a/ir_datasets/datasets/c4.py b/ir_datasets/datasets/c4.py
index 0ba7d91e..4781bed4 100644
--- a/ir_datasets/datasets/c4.py
+++ b/ir_datasets/datasets/c4.py
@@ -22,6 +22,11 @@ class C4Doc(NamedTuple):
     text: str
     url: str
     timestamp: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 
 class MisinfoQuery(NamedTuple):
@@ -32,6 +37,11 @@ class MisinfoQuery(NamedTuple):
     disclaimer: str
     stance: str
     evidence: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 
 class C4Source(DocSource):
diff --git a/ir_datasets/datasets/car.py b/ir_datasets/datasets/car.py
index b46129d9..d6630be1 100644
--- a/ir_datasets/datasets/car.py
+++ b/ir_datasets/datasets/car.py
@@ -29,6 +29,11 @@ class CarQuery(NamedTuple):
     text: str
     title: str
     headings: Tuple[str, ...]
+    def default_text(self):
+        """
+        text (which is title + headings)
+        """
+        return self.text
 
 
 class CarDocs(BaseDocs):
diff --git a/ir_datasets/datasets/clueweb09.py b/ir_datasets/datasets/clueweb09.py
index b34658d7..f6061874 100644
--- a/ir_datasets/datasets/clueweb09.py
+++ b/ir_datasets/datasets/clueweb09.py
@@ -40,6 +40,11 @@ class TrecWebTrackQuery(NamedTuple):
     description: str
     type: str
     subtopics: Tuple[TrecSubtopic, ...]
+    def default_text(self):
+        """
+        query
+        """
+        return self.query
 
 
 class ClueWeb09Docs(WarcDocs):
diff --git a/ir_datasets/datasets/clueweb12.py b/ir_datasets/datasets/clueweb12.py
index 9201c7e8..13536de6 100644
--- a/ir_datasets/datasets/clueweb12.py
+++ b/ir_datasets/datasets/clueweb12.py
@@ -60,12 +60,22 @@ class TrecWebTrackQuery(NamedTuple):
     description: str
     type: str
     subtopics: Tuple[TrecSubtopic, ...]
+    def default_text(self):
+        """
+        query
+        """
+        return self.query
 
 
 class NtcirQuery(NamedTuple):
     query_id: str
     title: str
     description: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 
 class MisinfoQuery(NamedTuple):
@@ -74,6 +84,11 @@ class MisinfoQuery(NamedTuple):
     cochranedoi: str
     description: str
     narrative: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 
 class MisinfoQrel(NamedTuple):
diff --git a/ir_datasets/datasets/codec.py b/ir_datasets/datasets/codec.py
index ef2d0422..266a7ac0 100644
--- a/ir_datasets/datasets/codec.py
+++ b/ir_datasets/datasets/codec.py
@@ -35,6 +35,11 @@ class CodecQuery(NamedTuple):
     query: str
     domain: str
     guidelines: str
+    def default_text(self):
+        """
+        query
+        """
+        return self.query
 
 
 class CodecQueries(BaseQueries):
diff --git a/ir_datasets/datasets/cord19.py b/ir_datasets/datasets/cord19.py
index d284a7eb..981d871c 100644
--- a/ir_datasets/datasets/cord19.py
+++ b/ir_datasets/datasets/cord19.py
@@ -27,6 +27,11 @@ class Cord19Doc(NamedTuple):
     doi: str
     date: str
     abstract: str
+    def default_text(self):
+        """
+        title + abstract
+        """
+        return f'{self.title} {self.abstract}'
 
 
 class Cord19FullTextSection(NamedTuple):
@@ -41,6 +46,12 @@ class Cord19FullTextDoc(NamedTuple):
     date: str
     abstract: str
     body: Tuple[Cord19FullTextSection, ...]
+    def default_text(self):
+        """
+        title + abstract + body
+        """
+        body = ' '.join(f'{b.title} {b.text}' for b in self.body)
+        return f'{self.title} {self.abstract} {body}'
 
 
 
diff --git a/ir_datasets/datasets/cranfield.py b/ir_datasets/datasets/cranfield.py
index 4a912112..92a6800a 100644
--- a/ir_datasets/datasets/cranfield.py
+++ b/ir_datasets/datasets/cranfield.py
@@ -28,6 +28,11 @@ class CranfieldDoc(NamedTuple):
     text: str
     author: str
     bib: str
+    def default_text(self):
+        """
+        title + text
+        """
+        return f'{self.title} {self.text}'
 
 
 def prefix_sentinel_splitter(it, sentinel):
diff --git a/ir_datasets/datasets/dpr_w100.py b/ir_datasets/datasets/dpr_w100.py
index a3449916..3de8a1bc 100644
--- a/ir_datasets/datasets/dpr_w100.py
+++ b/ir_datasets/datasets/dpr_w100.py
@@ -24,12 +24,22 @@ class DprW100Doc(NamedTuple):
     doc_id: str
     text: str
     title: str
+    def default_text(self):
+        """
+        title + text
+        """
+        return f'{self.title} {self.text}'
 
 
 class DprW100Query(NamedTuple):
     query_id: str
     text: str
     answers: Tuple[str, ]
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 
 class DprW100Manager:
diff --git a/ir_datasets/datasets/gov.py b/ir_datasets/datasets/gov.py
index 6c98c217..f0fa6794 100644
--- a/ir_datasets/datasets/gov.py
+++ b/ir_datasets/datasets/gov.py
@@ -50,6 +50,11 @@ class GovWeb02Query(NamedTuple):
     query_id: str
     title: str
     description: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 
 class GovDoc(NamedTuple):
@@ -58,6 +63,8 @@ class GovDoc(NamedTuple):
     http_headers: str
     body: bytes
     body_content_type: str
+    def default_text(self):
+        return ir_datasets.util.sax_html_parser(self.body, headers=self.http_headers, fields=[{'title', 'body'}])[0]
 
 
 class GovDocs(BaseDocs):
diff --git a/ir_datasets/datasets/gov2.py b/ir_datasets/datasets/gov2.py
index ad46692f..b134aabd 100644
--- a/ir_datasets/datasets/gov2.py
+++ b/ir_datasets/datasets/gov2.py
@@ -46,6 +46,8 @@ class Gov2Doc(NamedTuple):
     http_headers: str
     body: bytes
     body_content_type: str
+    def default_text(self):
+        return ir_datasets.util.sax_html_parser(self.body, headers=self.http_headers, fields=[{'title', 'body'}])[0]
 
 
 
diff --git a/ir_datasets/datasets/highwire.py b/ir_datasets/datasets/highwire.py
index e1772567..ff634db3 100644
--- a/ir_datasets/datasets/highwire.py
+++ b/ir_datasets/datasets/highwire.py
@@ -36,6 +36,11 @@ class HighwireDoc(NamedTuple):
     journal: str
     title: str
     spans: Tuple[HighwireSpan, ...]
+    def default_text(self):
+        """
+        title + spans
+        """
+        return self.title + ' ' + ' '.join(s.text for s in self.spans)
 
 
 class TrecGenomicsQrel(NamedTuple):
diff --git a/ir_datasets/datasets/istella22.py b/ir_datasets/datasets/istella22.py
index 6c1164d3..981d24b0 100644
--- a/ir_datasets/datasets/istella22.py
+++ b/ir_datasets/datasets/istella22.py
@@ -18,6 +18,11 @@ class Istella22Doc(NamedTuple):
     extra_text: str
     lang: str
     lang_pct: int
+    def default_text(self):
+        """
+        title + text + extra_text
+        """
+        return f'{self.title} {self.text} {self.extra_text}'
 
 
 NAME = 'istella22'
diff --git a/ir_datasets/datasets/kilt.py b/ir_datasets/datasets/kilt.py
index 41523bbd..bc5c2484 100644
--- a/ir_datasets/datasets/kilt.py
+++ b/ir_datasets/datasets/kilt.py
@@ -43,6 +43,11 @@ class KiltDoc(NamedTuple):
     history_parentid: str
     history_pageid: str
     history_url: str
+    def default_text(self):
+        """
+        title + text
+        """
+        return f'{self.title} {self.text}'
 
 
 def strip_markup(text):
diff --git a/ir_datasets/datasets/medline.py b/ir_datasets/datasets/medline.py
index e218bcc4..32052d10 100644
--- a/ir_datasets/datasets/medline.py
+++ b/ir_datasets/datasets/medline.py
@@ -36,6 +36,11 @@ class MedlineDoc(NamedTuple):
     doc_id: str
     title: str
     abstract: str
+    def default_text(self):
+        """
+        title + abstract
+        """
+        return f'{self.title} {self.abstract}'
 
 
 class TrecGenomicsQuery(NamedTuple):
@@ -43,6 +48,11 @@ class TrecGenomicsQuery(NamedTuple):
     title: str
     need: str
     context: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 
 class TrecPm2017Query(NamedTuple):
@@ -51,6 +61,11 @@ class TrecPm2017Query(NamedTuple):
     gene: str
     demographic: str
     other: str
+    def default_text(self):
+        """
+        disease, gene, demographic, and other
+        """
+        return f'{self.disease} {self.gene} {self.demographic} {self.other}'
 
 
 class TrecPmQuery(NamedTuple):
@@ -58,6 +73,11 @@ class TrecPmQuery(NamedTuple):
     disease: str
     gene: str
     demographic: str
+    def default_text(self):
+        """
+        disease, gene, and demographic
+        """
+        return f'{self.disease} {self.gene} {self.demographic}'
 
 
 class ConcatFile:
diff --git a/ir_datasets/datasets/msmarco_document.py b/ir_datasets/datasets/msmarco_document.py
index 5bc874c9..e880898b 100644
--- a/ir_datasets/datasets/msmarco_document.py
+++ b/ir_datasets/datasets/msmarco_document.py
@@ -32,6 +32,11 @@ class MsMarcoDocument(NamedTuple):
     url: str
     title: str
     body: str
+    def default_text(self):
+        """
+        title + body
+        """
+        return f'{self.title} {self.body}'
 
 
 # Use the TREC-formatted docs so we get all the available formatting (namely, line breaks)
@@ -63,6 +68,11 @@ class MsMarcoAnchorTextDocument(NamedTuple):
     doc_id: str
     text: str
     anchors: List[str]
+    def default_text(self):
+        """
+        text + anchors
+        """
+        return f'{self.text} ' + ' '.join(self.anchors)
 
 
 class MsMarcoAnchorTextDocs(BaseDocs):
diff --git a/ir_datasets/datasets/msmarco_document_v2.py b/ir_datasets/datasets/msmarco_document_v2.py
index e6971fa1..0d528fd7 100644
--- a/ir_datasets/datasets/msmarco_document_v2.py
+++ b/ir_datasets/datasets/msmarco_document_v2.py
@@ -28,6 +28,11 @@ class MsMarcoV2Document(NamedTuple):
     title: str
     headings: str
     body: str
+    def default_text(self):
+        """
+        title + headings + body
+        """
+        return f'{self.title} {self.headings} {self.body}'
 
 
 class MsMarcoV2Docs(BaseDocs):
@@ -101,6 +106,11 @@ class MsMarcoV2AnchorTextDocument(NamedTuple):
     doc_id: str
     text: str
     anchors: List[str]
+    def default_text(self):
+        """
+        text + anchors
+        """
+        return f'{self.text} ' + ' '.join(self.anchors)
 
 
 class MsMarcoV2AnchorTextDocs(BaseDocs):
diff --git a/ir_datasets/datasets/msmarco_passage_v2.py b/ir_datasets/datasets/msmarco_passage_v2.py
index cf79848a..d74327d7 100644
--- a/ir_datasets/datasets/msmarco_passage_v2.py
+++ b/ir_datasets/datasets/msmarco_passage_v2.py
@@ -28,6 +28,11 @@ class MsMarcoV2Passage(NamedTuple):
     text: str
     spans: Tuple[Tuple[int, int], ...]
     msmarco_document_id: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 
 def parse_msmarco_passage(line):
diff --git a/ir_datasets/datasets/msmarco_qna.py b/ir_datasets/datasets/msmarco_qna.py
index 6122429d..1695aaa6 100644
--- a/ir_datasets/datasets/msmarco_qna.py
+++ b/ir_datasets/datasets/msmarco_qna.py
@@ -30,12 +30,22 @@ class MsMarcoQnAQuery(NamedTuple):
     text: str
     type: str
     answers: Tuple[str, ...]
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 
 class MsMarcoQnAEvalQuery(NamedTuple):
     query_id: str
     text: str
     type: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 
 class MsMarcoQnADoc(NamedTuple):
@@ -44,6 +54,11 @@ class MsMarcoQnADoc(NamedTuple):
     url: str
     msmarco_passage_id: str
     msmarco_document_id: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 
 # The MS MARCO QnA data files are in a super inconvenient format. They have a script to convert it
diff --git a/ir_datasets/datasets/natural_questions.py b/ir_datasets/datasets/natural_questions.py
index d42a457b..4adccb66 100644
--- a/ir_datasets/datasets/natural_questions.py
+++ b/ir_datasets/datasets/natural_questions.py
@@ -21,6 +21,12 @@ class NqPassageDoc(NamedTuple):
     document_title: str # from document itself
     document_url: str # from document itself
     parent_doc_id: str # doc_id of the largest passage it's under (e.g., a sentence under a paragraph), or None if it's a top-level passage
+    def default_text(self):
+        """
+        document_title and text
+        """
+        return f'{self.document_title} {self.text}'
+
 
 
 class NqQrel(NamedTuple):
diff --git a/ir_datasets/datasets/nfcorpus.py b/ir_datasets/datasets/nfcorpus.py
index 083ce966..0a7d5e0f 100644
--- a/ir_datasets/datasets/nfcorpus.py
+++ b/ir_datasets/datasets/nfcorpus.py
@@ -22,16 +22,31 @@ class NfCorpusDoc(NamedTuple):
     url: str
     title: str
     abstract: str
+    def default_text(self):
+        """
+        title and abstract
+        """
+        return f'{self.title} {self.abstract}'
 
 class NfCorpusQuery(NamedTuple):
     query_id: str
     title: str
     all: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 class NfCorpusVideoQuery(NamedTuple):
     query_id: str
     title: str
     desc: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 class ZipQueries(BaseQueries):
     def __init__(self, queries, idxs, qtype):
diff --git a/ir_datasets/datasets/nyt.py b/ir_datasets/datasets/nyt.py
index 274d14f7..6e6f75c4 100644
--- a/ir_datasets/datasets/nyt.py
+++ b/ir_datasets/datasets/nyt.py
@@ -30,6 +30,12 @@ class NytDoc(NamedTuple):
     headline: str
     body: str
     source_xml: str
+    def default_text(self):
+        """
+        headline and body
+        """
+        return f'{self.headline} {self.body}'
+
 
 
 class NytDocs(BaseDocs):
diff --git a/ir_datasets/datasets/pmc.py b/ir_datasets/datasets/pmc.py
index 05c8e1a1..1ad3ee36 100644
--- a/ir_datasets/datasets/pmc.py
+++ b/ir_datasets/datasets/pmc.py
@@ -36,6 +36,11 @@ class PmcDoc(NamedTuple):
     title: str
     abstract: str
     body: str
+    def default_text(self):
+        """
+        title, abstract, and body
+        """
+        return f'{self.title} {self.abstract} {self.body}'
 
 
 class TrecCdsQuery(NamedTuple):
@@ -43,6 +48,11 @@ class TrecCdsQuery(NamedTuple):
     type: str
     description: str
     summary: str
+    def default_text(self):
+        """
+        description
+        """
+        return self.description
 
 
 class TrecCds2016Query(NamedTuple):
@@ -51,6 +61,11 @@ class TrecCds2016Query(NamedTuple):
     note: str
     description: str
     summary: str
+    def default_text(self):
+        """
+        description
+        """
+        return self.description
 
 
 class PmcDocs(BaseDocs):
diff --git a/ir_datasets/datasets/sara.py b/ir_datasets/datasets/sara.py
index 6f27855c..96b0fefd 100644
--- a/ir_datasets/datasets/sara.py
+++ b/ir_datasets/datasets/sara.py
@@ -30,6 +30,8 @@ class SaraDoc(NamedTuple):
     doc_id: str
     text: str
     sensitivity: int
+    def default_text(self):
+        return self.text
 
 class SaraDocs(BaseDocs):
     def __init__(self,dlc):
diff --git a/ir_datasets/datasets/trec_cast.py b/ir_datasets/datasets/trec_cast.py
index c6f69e64..f6698c0b 100644
--- a/ir_datasets/datasets/trec_cast.py
+++ b/ir_datasets/datasets/trec_cast.py
@@ -35,6 +35,11 @@ class Cast2019Query(NamedTuple):
     turn_number: int
     topic_title: str
     topic_description: str
+    def default_text(self):
+        """
+        raw_utterance
+        """
+        return self.raw_utterance
 
 
 class Cast2020Query(NamedTuple):
@@ -45,6 +50,11 @@ class Cast2020Query(NamedTuple):
     manual_canonical_result_id: str
     topic_number: int
     turn_number: int
+    def default_text(self):
+        """
+        raw_utterance
+        """
+        return self.raw_utterance
 
 
 class CastDocs(BaseDocs):
diff --git a/ir_datasets/datasets/trec_fair.py b/ir_datasets/datasets/trec_fair.py
index d4857354..a02a9719 100644
--- a/ir_datasets/datasets/trec_fair.py
+++ b/ir_datasets/datasets/trec_fair.py
@@ -28,6 +28,11 @@ class FairTrecDoc(NamedTuple):
     quality_score: Optional[float]
     geographic_locations: Optional[List[str]]
     quality_score_disk: Optional[str]
+    def default_text(self):
+        """
+        title and text
+        """
+        return f"{self.title} {self.text}"
 
 
 class FairTrec2022Doc(NamedTuple):
@@ -67,11 +72,21 @@ class FairTrecQuery(NamedTuple):
     keywords: List[str]
     scope: str
     homepage: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 class FairTrec2022TrainQuery(NamedTuple):
     query_id: str
     text: str
     url: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 
 class FairTrecEvalQuery(NamedTuple):
@@ -79,6 +94,11 @@ class FairTrecEvalQuery(NamedTuple):
     text: str
     keywords: List[str]
     scope: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 
 class FairTrecDocs(BaseDocs):
diff --git a/ir_datasets/datasets/trec_mandarin.py b/ir_datasets/datasets/trec_mandarin.py
index a9dda9b7..eaf723eb 100644
--- a/ir_datasets/datasets/trec_mandarin.py
+++ b/ir_datasets/datasets/trec_mandarin.py
@@ -16,6 +16,11 @@ class TrecMandarinQuery(NamedTuple):
     description_zh: str
     narrative_en: str
     narrative_zh: str
+    def default_text(self):
+        """
+        title_zh
+        """
+        return self.title_zh
 
 
 QREL_DEFS = {
diff --git a/ir_datasets/datasets/trec_spanish.py b/ir_datasets/datasets/trec_spanish.py
index 16c62d62..f416748e 100644
--- a/ir_datasets/datasets/trec_spanish.py
+++ b/ir_datasets/datasets/trec_spanish.py
@@ -10,6 +10,11 @@
 class TrecDescOnlyQuery(NamedTuple):
     query_id: str
     description: str
+    def default_text(self):
+        """
+        description
+        """
+        return self.description
 
 class TrecSpanish3Query(NamedTuple):
     query_id: str
@@ -19,6 +24,11 @@ class TrecSpanish3Query(NamedTuple):
     description_en: str
     narrative_es: str
     narrative_en: str
+    def default_text(self):
+        """
+        title_es
+        """
+        return self.title_es
 
 class TrecSpanish4Query(NamedTuple):
     query_id: str
@@ -26,6 +36,11 @@ class TrecSpanish4Query(NamedTuple):
     description_en1: str
     description_es2: str
     description_en2: str
+    def default_text(self):
+        """
+        description_es1
+        """
+        return self.description_es1
 
 QREL_DEFS = {
     1: 'relevant',
diff --git a/ir_datasets/datasets/tripclick.py b/ir_datasets/datasets/tripclick.py
index 67d4df2a..78bddfe5 100644
--- a/ir_datasets/datasets/tripclick.py
+++ b/ir_datasets/datasets/tripclick.py
@@ -109,6 +109,11 @@ class TripClickPartialDoc(NamedTuple):
     doc_id: str
     title: str
     url: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 
 class TripClickQlogs(BaseQlogs):
diff --git a/ir_datasets/datasets/tweets2013_ia.py b/ir_datasets/datasets/tweets2013_ia.py
index a1caa3ba..5b61d52e 100644
--- a/ir_datasets/datasets/tweets2013_ia.py
+++ b/ir_datasets/datasets/tweets2013_ia.py
@@ -56,6 +56,11 @@ class TweetDoc(NamedTuple):
     retweet_doc_id: str
     source: bytes
     source_content_type: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 
 class TrecMb13Query(NamedTuple):
@@ -63,6 +68,11 @@ class TrecMb13Query(NamedTuple):
     query: str
     time: str
     tweet_time: str
+    def default_text(self):
+        """
+        query
+        """
+        return self.query
 
 
 class TrecMb14Query(NamedTuple):
@@ -71,6 +81,11 @@ class TrecMb14Query(NamedTuple):
     time: str
     tweet_time: str
     description: str
+    def default_text(self):
+        """
+        query
+        """
+        return self.query
 
 
 """
diff --git a/ir_datasets/datasets/wapo.py b/ir_datasets/datasets/wapo.py
index 77747082..078c8f24 100644
--- a/ir_datasets/datasets/wapo.py
+++ b/ir_datasets/datasets/wapo.py
@@ -51,6 +51,11 @@ class WapoDoc(NamedTuple):
     body: str
     body_paras_html: Tuple[str, ...]
     body_media: Tuple[WapoDocMedia, ...]
+    def default_text(self):
+        """
+        title and body
+        """
+        return f'{self.title} {self.body}'
 
 
 class TrecBackgroundLinkingQuery(NamedTuple):
diff --git a/ir_datasets/datasets/wikiclir.py b/ir_datasets/datasets/wikiclir.py
index 1d3a6823..62561ee7 100644
--- a/ir_datasets/datasets/wikiclir.py
+++ b/ir_datasets/datasets/wikiclir.py
@@ -20,12 +20,22 @@ class WikiClirQuery(NamedTuple):
     query_id: str
     title: str
     first_sent: str
+    def default_text(self):
+        """
+        title
+        """
+        return f"{self.title}"
 
 
 class WikiClirDoc(NamedTuple):
     doc_id: str
     title: str
     text: str
+    def default_text(self):
+        """
+        title and text
+        """
+        return f"{self.title} {self.text}"
 
 
 def _init():
diff --git a/ir_datasets/formats/argsme.py b/ir_datasets/formats/argsme.py
index 4d369002..90bce32b 100644
--- a/ir_datasets/formats/argsme.py
+++ b/ir_datasets/formats/argsme.py
@@ -166,6 +166,12 @@ class ArgsMeDoc(NamedTuple):
     author_role: Optional[str]
     mode: Optional[ArgsMeMode]
 
+    def default_text(self):
+        """
+        premises + conclusion
+        """
+        return f"{self.premises_texts} {self.conclusion}"
+
     @staticmethod
     def from_json(json: dict) -> "ArgsMeDoc":
         context_json = json["context"]
diff --git a/ir_datasets/formats/base.py b/ir_datasets/formats/base.py
index 72377516..301e63f2 100644
--- a/ir_datasets/formats/base.py
+++ b/ir_datasets/formats/base.py
@@ -10,10 +10,14 @@
 class GenericDoc(NamedTuple):
     doc_id: str
     text: str
+    def default_text(self):
+        return self.text
 
 class GenericQuery(NamedTuple):
     query_id: str
     text: str
+    def default_text(self):
+        return self.text
 
 class GenericQrel(NamedTuple):
     query_id: str
diff --git a/ir_datasets/formats/extracted_cc.py b/ir_datasets/formats/extracted_cc.py
index 440f710c..7b10aa6d 100644
--- a/ir_datasets/formats/extracted_cc.py
+++ b/ir_datasets/formats/extracted_cc.py
@@ -18,6 +18,11 @@ class ExctractedCCDoc(NamedTuple):
     url: str
     time: str
     cc_file: str
+    def default_text(self):
+        """
+        title and text
+        """
+        return f'{self.title} {self.text}'
 
 
 class ExctractedCCDocs(BaseDocs):
@@ -83,6 +88,11 @@ class ExctractedCCQuery(NamedTuple):
     report_url: str
     report_date: str
     translation_lang: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 class ExctractedCCQueries(BaseQueries):
     def __init__(self, queries_dlc, subset_lang, namespace=None):
diff --git a/ir_datasets/formats/touche.py b/ir_datasets/formats/touche.py
index 9090d909..64e6d66d 100644
--- a/ir_datasets/formats/touche.py
+++ b/ir_datasets/formats/touche.py
@@ -14,11 +14,21 @@ class ToucheQuery(NamedTuple):
     title: str
     description: str
     narrative: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 
 class ToucheTitleQuery(NamedTuple):
     query_id: str
     title: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 
 class ToucheComparativeQuery(NamedTuple):
@@ -27,6 +37,11 @@ class ToucheComparativeQuery(NamedTuple):
     objects: Tuple[str, str]
     description: str
     narrative: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 
 class ToucheQualityQrel(NamedTuple):
@@ -79,6 +94,11 @@ class TouchePassageDoc(NamedTuple):
     doc_id: str
     text: str
     chatnoir_url: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 
 class ToucheQueries(BaseQueries):
diff --git a/ir_datasets/formats/trec.py b/ir_datasets/formats/trec.py
index a01e8c10..cf01cac0 100644
--- a/ir_datasets/formats/trec.py
+++ b/ir_datasets/formats/trec.py
@@ -17,24 +17,44 @@ class TrecDoc(NamedTuple):
     doc_id: str
     text: str
     marked_up_doc: str
+    def default_text(self):
+        """
+        text
+        """
+        return self.text
 
 class TitleUrlTextDoc(NamedTuple):
     doc_id: str
     title: str
     url: str
     text: str
+    def default_text(self):
+        """
+        title and text
+        """
+        return f'{self.title} {self.text}'
 
 class TrecParsedDoc(NamedTuple):
     doc_id: str
     title: str
     body: str
     marked_up_doc: bytes
+    def default_text(self):
+        """
+        title and body
+        """
+        return f'{self.title} {self.body}'
 
 class TrecQuery(NamedTuple):
     query_id: str
     title: str
     description: str
     narrative: str
+    def default_text(self):
+        """
+        title
+        """
+        return self.title
 
 class TrecSubtopic(NamedTuple):
     number: str
diff --git a/ir_datasets/formats/webarc.py b/ir_datasets/formats/webarc.py
index e1a19d95..d8b4c668 100644
--- a/ir_datasets/formats/webarc.py
+++ b/ir_datasets/formats/webarc.py
@@ -5,6 +5,7 @@
 import ir_datasets
 from ir_datasets.formats import BaseDocs
 
+_logger = ir_datasets.log.easy()
 
 class WarcDoc(NamedTuple):
     doc_id: str
@@ -13,6 +14,12 @@ class WarcDoc(NamedTuple):
     http_headers: bytes
     body: bytes
     body_content_type: str
+    def default_text(self):
+        try:
+            return ir_datasets.util.sax_html_parser(self.body, headers=self.http_headers, fields=[{'title', 'body'}])[0]
+        except UnicodeDecodeError:
+            _logger.info(f'UnicodeDecodeError when parsing doc_id={self.doc_id}')
+            return ''
 
 
 class WarcDocs(BaseDocs):


Test patch:
diff --git a/test/test_defaulttext.py b/test/test_defaulttext.py
new file mode 100644
index 00000000..1bf09f00
--- /dev/null
+++ b/test/test_defaulttext.py
@@ -0,0 +1,46 @@
+import unittest
+import ir_datasets
+
+def template_instance(Cls):
+    if hasattr(Cls, '_fields'):
+        return Cls(*[template_instance(v) for k, v in Cls.__annotations__.items()])
+    elif hasattr(Cls, '_name') and Cls._name == 'List':
+        return [template_instance(Cls.__args__[0]) for _ in range(3)]
+    elif hasattr(Cls, '_name') and Cls._name == 'Tuple':
+        return tuple([template_instance(Cls.__args__[0]) for _ in range(3)])
+    elif hasattr(Cls, '__members__'):
+        return Cls(next(iter(Cls.__members__.values())))
+    elif Cls is str:
+        return 'test string'
+    else:
+        try:
+            return Cls()
+        except:
+            return None
+
+class TestMetadata(unittest.TestCase):
+    def test_all_defualttext(self):
+        for dsid in ir_datasets.registry._registered:
+            self._test_defaulttet(dsid)
+
+    def _test_defaulttet(self, dsid):
+        with self.subTest(dsid):
+            dataset = ir_datasets.load(dsid)
+            if dataset.has_docs():
+                Cls = dataset.docs_cls()
+                instance = template_instance(Cls)
+                if hasattr(instance, 'default_text'):
+                    instance.default_text() # test it doesn't raise an error
+                else:
+                    print(dsid, 'missing doc default_text')
+            if dataset.has_queries():
+                Cls = dataset.queries_cls()
+                instance = template_instance(Cls)
+                if hasattr(instance, 'default_text'):
+                    instance.default_text() # test it doesn't raise an error
+                else:
+                    print(dsid, 'missing query default_text')
+
+
+if __name__ == '__main__':
+    unittest.main()