Skip to content
Merged

Dev #5377

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d8d58cf
move annotation dataset type filtering into the managers
hanars Mar 20, 2026
a2e08b2
move other dataset type filtering into manager
hanars Mar 23, 2026
61afe8a
exclude sv clean up
hanars Mar 23, 2026
818f780
fetch sample data per dtataset type
hanars Mar 23, 2026
f5176e3
Merge branch 'dev' of https://github.com/broadinstitute/seqr into sea…
hanars Mar 23, 2026
55e096f
seaparte locus filter step
hanars Mar 23, 2026
a44580f
separate annotations parsing
hanars Mar 23, 2026
673db5d
multi type cleanup
hanars Mar 23, 2026
f1cee06
fix agg
hanars Mar 24, 2026
12f09ea
fix x-linked recessive
hanars Mar 24, 2026
ff4c7a9
fix multi dt comp het
hanars Mar 24, 2026
793e9b7
fix location search
hanars Mar 24, 2026
223cd6c
fix no access project search
hanars Mar 24, 2026
ac0f607
fix new sv filtering
hanars Mar 24, 2026
5ef58f8
fix validation
hanars Mar 24, 2026
ed76899
fix sv type validation
hanars Mar 24, 2026
1a7a059
fix mito type validation
hanars Mar 24, 2026
d427e7d
Merge branch 'search-cache-cleanup' of https://github.com/broadinstit…
hanars Mar 24, 2026
2b9b159
shared helper for single variant search
hanars Mar 24, 2026
535cec2
clean up
hanars Mar 24, 2026
5d4465b
clean up
hanars Mar 24, 2026
498d87e
Merge branch 'shared-search-helper-func' of https://github.com/broadi…
hanars Mar 24, 2026
68ee8ba
fix prioritized tagging
hanars Mar 24, 2026
9140e96
variant lookup clean up
hanars Mar 24, 2026
4b21ce4
clean up
hanars Mar 24, 2026
1291feb
clean up
hanars Mar 25, 2026
06f0c9f
Merge branch 'shared-search-helper-func' of https://github.com/broadi…
hanars Mar 25, 2026
175fe0b
Merge branch 'shared-search-helper-func' of https://github.com/broadi…
hanars Mar 25, 2026
f2f455e
Merge branch 'dev' of https://github.com/broadinstitute/seqr into sea…
hanars Mar 25, 2026
17683d2
clean up
hanars Mar 26, 2026
c1c5732
clean up
hanars Mar 26, 2026
3bee937
Merge branch 'dev' of https://github.com/broadinstitute/seqr into sea…
hanars Mar 26, 2026
8e53c81
clean up
hanars Mar 26, 2026
e367b49
Bump node-forge from 1.3.2 to 1.4.0 in /ui
dependabot[bot] Mar 29, 2026
8f08c1b
Merge pull request #5369 from broadinstitute/dependabot/npm_and_yarn/…
hanars Mar 30, 2026
9f58b3b
add rna tissue type options;
hanars Mar 30, 2026
16064c1
Merge pull request #5370 from broadinstitute/rna-tissue-additions
hanars Mar 31, 2026
bcd06ad
Merge pull request #5360 from broadinstitute/search-sample-clean-up
hanars Apr 1, 2026
ec53a99
fix selector
hanars Apr 6, 2026
1b93bbe
fix gene lookup blank page
hanars Apr 6, 2026
8daeafe
Merge pull request #5376 from broadinstitute/gene-lookup-blank-page
hanars Apr 6, 2026
22ad5ac
Merge pull request #5375 from broadinstitute/project-loaded-data-summ…
hanars Apr 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 59 additions & 31 deletions clickhouse_search/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
from seqr.utils.xpos_utils import get_xpos, MIN_POS, MAX_POS, CHROMOSOME_CHOICES


class InvalidDatasetTypeException(Exception):
pass


class SearchQuerySet(QuerySet):

@property
Expand Down Expand Up @@ -414,37 +418,30 @@ def populations(self):
def _filter_frequency(self, results, **kwargs):
return results

def _filter_annotations(self, results, annotations=None, pathogenicity=None, genes=None, intervals=None, exclude_locations=False, **kwargs):
def _filter_annotations(self, results, annotations_filter_q=None, transcript_filters=None, genes=None, intervals=None, exclude_locations=False, **kwargs):
if exclude_locations and genes:
intervals = self._format_gene_intervals(genes)
genes = None
results = self._filter_locations(results, genes, intervals, exclude_locations=exclude_locations, **kwargs)

filter_qs, transcript_filters = self._parse_annotation_filters(annotations or {}, pathogenicity) if (annotations or pathogenicity) else ([], [])
results = self._filter_locations(results, genes, intervals, exclude_locations=exclude_locations, **kwargs)

if not (filter_qs or transcript_filters):
if any(val for key, val in (annotations or {}).items() if key != NEW_SV_FIELD) or any(val for val in (pathogenicity or {}).values()):
# Annotation filters restrict search to other dataset types
results = results.none()
if not (annotations_filter_q or transcript_filters):
return results

filter_q = filter_qs[0] if filter_qs else None
for q in filter_qs[1:]:
filter_q |= q
if filter_q:
results = results.annotate(passes_annotation=filter_q)
filter_q = Q(passes_annotation=True)
if annotations_filter_q:
results = results.annotate(passes_annotation=annotations_filter_q)
annotations_filter_q = Q(passes_annotation=True)

if transcript_filters:
consequence_field = self.GENE_CONSEQUENCE_FIELD if genes else self.TRANSCRIPT_FIELD
results = self._annotate_filtered_transcripts(results, consequence_field, transcript_filters, **kwargs)
transcript_q = Q(filtered_transcript_consequences__not_empty=True)
if filter_q:
filter_q |= transcript_q
if annotations_filter_q:
annotations_filter_q |= transcript_q
else:
filter_q = transcript_q
annotations_filter_q = transcript_q

return results.filter(filter_q)
return results.filter(annotations_filter_q)

def _annotate_filtered_transcripts(self, results, consequence_field, transcript_filters, **kwargs):
return results.annotate(**{
Expand Down Expand Up @@ -472,6 +469,16 @@ def _interval_query(self, chrom, start, end, offset=None, **kwargs):
end = min(end + offset_pos, MAX_POS)
return Q(xpos__range=(get_xpos(chrom, start), get_xpos(chrom, end)))

def get_parsed_annotations_filters(self, annotations=None, pathogenicity=None, **kwargs):
annotations = {k: v for k, v in (annotations or {}).items() if v}
pathogenicity = {k: v for k, v in (pathogenicity or {}).items() if v}
filter_qs, transcript_filters = self._parse_annotation_filters(annotations, pathogenicity)
filter_q = filter_qs[0] if filter_qs else None
for q in filter_qs[1:]:
filter_q |= q

return {'annotations_filter_q': filter_q, 'transcript_filters': transcript_filters}

def _parse_annotation_filters(self, annotations, pathogenicity):
raise NotImplementedError

Expand Down Expand Up @@ -642,11 +649,7 @@ def _parse_annotation_filters(self, annotations, pathogenicity):
filter_qs.append(Q(mitomapPathogenic=value))
elif field == SPLICE_AI_FIELD:
if value and SPLICE_AI_FIELD in self.entry_model.PREDICTIONS:
pred_index = next (
i for i, (pred_field, _) in enumerate(self.query.annotations['preds'].output_field.base_fields)
if pred_field == SPLICE_AI_FIELD
)
filter_qs.append(Q(**{f'preds__{pred_index}__gte': float(value)}))
filter_qs.append(Q(preds__0__gte=float(value)))
elif field not in SV_ANNOTATION_TYPES:
allowed_consequences += value

Expand All @@ -665,6 +668,9 @@ def _parse_annotation_filters(self, annotations, pathogenicity):
if allowed_consequences:
transcript_filters += self._allowed_consequences_filters(allowed_consequences)

if (annotations or pathogenicity) and not (filter_qs or transcript_filters):
raise InvalidDatasetTypeException

return filter_qs, transcript_filters

def _allowed_consequences_filters(self, allowed_consequences):
Expand Down Expand Up @@ -823,6 +829,10 @@ def _parse_annotation_filters(self, annotations, pathogenicity):
if sv_type.startswith(self.model.SV_TYPE_FILTER_PREFIX)
]))

if not (filter_qs or transcript_filters) and (pathogenicity or any(key for key in annotations.keys() if key != NEW_SV_FIELD)):
# Annotation filters restrict search to other dataset types
raise InvalidDatasetTypeException

return filter_qs, transcript_filters

def _filter_locations(self, results, genes, intervals, exclude_locations=False, padded_interval_end=None, **kwargs):
Expand Down Expand Up @@ -1007,6 +1017,10 @@ def gt_stats_dict_rel(self):
def genome_version(self):
return self.annotations_model.ANNOTATION_CONSTANTS['genomeVersion']

@property
def filtered_chrom(self):
return self.annotations_model.ANNOTATION_CONSTANTS.get('chrom')

@property
def callset_filter_field(self):
return 'callset'
Expand All @@ -1016,7 +1030,7 @@ def _clinvar_path_q(cls, pathogenicity):
return cls._clinvar_filter_q(pathogenicity, allowed_filters=CLINVAR_PATH_SIGNIFICANCES)

def search(self, sample_data, exclude_keys=None, **kwargs):
entries = self.filter_locus(**kwargs)
entries = self

if exclude_keys:
entries = entries.exclude(key__in=exclude_keys)
Expand Down Expand Up @@ -1072,9 +1086,6 @@ def _search_call_data(self, entries, sample_data, inheritance_mode=None, inherit
elif exclude_projects:
entries = entries.exclude(project_guid__in=exclude_projects)

if inheritance_mode in {X_LINKED_RECESSIVE, X_LINKED_RECESSIVE_MALE_AFFECTED}:
entries = entries.filter(self._interval_query('X', start=MIN_POS, end=MAX_POS))

inheritance_q = None
quality_q = None
gt_filter = None
Expand Down Expand Up @@ -1420,15 +1431,28 @@ def _family_passes_expression(pass_field):
mapped_expression='x.1', output_field=models.ArrayField(models.StringField()),
)

def filter_locus(self, exclude_locations=False, require_gene_filter=False, intervals=None, genes=None, variant_ids=None, **kwargs):
def filter_locus(self, exclude_locations=False, require_gene_filter=False, intervals=None, genes=None, variant_ids=None, inheritance_mode=None, **kwargs):
entries = self

if variant_ids:
entries = entries.filter_variant_ids(variant_ids)

if not (genes or intervals):
return entries
if inheritance_mode in {X_LINKED_RECESSIVE, X_LINKED_RECESSIVE_MALE_AFFECTED}:
if self.filtered_chrom and self.filtered_chrom != 'X':
raise InvalidDatasetTypeException
entries = entries.filter(self._interval_query('X', start=MIN_POS, end=MAX_POS))

if intervals and self.filtered_chrom:
intervals = [interval for interval in intervals if interval['chrom'] == self.filtered_chrom]
if not intervals and not exclude_locations:
raise InvalidDatasetTypeException

if genes or intervals:
entries = self._filter_locations(entries, genes, intervals, exclude_locations=exclude_locations, require_gene_filter=require_gene_filter)

return entries

def _filter_locations(self, entries, genes, intervals, exclude_locations=False, require_gene_filter=False):
locus_q = None
if genes:
should_filter_interval = self._can_filter_gene_interval(genes) or exclude_locations
Expand All @@ -1452,7 +1476,7 @@ def filter_locus(self, exclude_locations=False, require_gene_filter=False, inter
return filter_func(locus_q)

def _can_filter_gene_interval(self, genes):
return (not hasattr(self.model, 'geneId_ids')) or len(genes) < self.MAX_XPOS_FILTER_INTERVALS
return (not hasattr(self.model, 'geneId_ids')) or len(genes) < self.MAX_XPOS_FILTER_INTERVALS or self.filtered_chrom

def search_padded_interval(self, chrom, pos, padding):
interval_q = self._interval_query(chrom, start=max(pos - padding, MIN_POS), end=min(pos + padding, MAX_POS))
Expand Down Expand Up @@ -1618,6 +1642,8 @@ def filter_locus(self, *args, require_any_gene=False, parsed_variant_ids=None, i
# improve performance by using the xpos projection
intervals = [{'chrom': chrom, 'start': pos, 'end': pos} for chrom, pos, _, _ in parsed_variant_ids]
variant_ids = ['-'.join([str(o) for o in variant_id]) for variant_id in parsed_variant_ids]
elif variant_ids:
raise InvalidDatasetTypeException

entries = super().filter_locus(*args, intervals=intervals, genes=genes, variant_ids=variant_ids, **kwargs)

Expand Down Expand Up @@ -1689,7 +1715,9 @@ def _annotate_calls(self, entries, *args, **kwargs):

return entries

def filter_locus(self, *args, exclude_locations=False, intervals=None, genes=None, **kwargs):
def filter_locus(self, *args, exclude_locations=False, intervals=None, genes=None, exclude_svs=False, parsed_variant_ids=None, **kwargs):
if exclude_svs or parsed_variant_ids:
raise InvalidDatasetTypeException
# SV interval filtering occurs after joining on annotations to correctly incorporate end position
can_filter_gene_interval = self._can_filter_gene_interval(genes)
if exclude_locations:
Expand Down
4 changes: 2 additions & 2 deletions clickhouse_search/models/search_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,11 +300,11 @@ class BaseEntriesSnvIndel(BaseEntries):
('dp', models.UInt16Field(null=True, blank=True)),
]
PREDICTIONS = {
'dbnsfp': {},
'eigen': {},
'splice_ai': {
'consequence_id': ('splice_ai_consequence', models.Enum8Field(choices=[(csq, csq_id) for csq_id, csq in BaseSpliceAi.CONSEQUENCE_CHOICES])),
},
'dbnsfp': {},
'eigen': {},
}
RANGE_PREDICTIONS = {}
POPULATIONS = ['gnomad_exomes', 'gnomad_genomes', 'topmed']
Expand Down
Loading
Loading