Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions app/Actions/Project/ArchiveProject.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace App\Actions\Project;

use App\Models\Project;
use App\Support\Public\PublicMoleculeAggregates;
use App\Support\Public\PublicMoleculeCatalogIndexer;

class ArchiveProject
{
Expand Down Expand Up @@ -36,7 +36,7 @@ public function toggleArchive($project)
}
$project->save();

PublicMoleculeAggregates::forgetPublicCatalogTotalCache();
app(PublicMoleculeCatalogIndexer::class)->refreshForProject($project);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions app/Actions/Project/PublishProject.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use App\Jobs\ProcessMetadataExtractionBagitGenerationJob;
use App\Models\Project;
use App\Support\Public\PublicMoleculeAggregates;
use App\Support\Public\PublicMoleculeCatalogIndexer;

class PublishProject
{
Expand Down Expand Up @@ -32,6 +32,6 @@ public function publish($project)
}
}

PublicMoleculeAggregates::forgetPublicCatalogTotalCache();
app(PublicMoleculeCatalogIndexer::class)->refreshForProject($project);
}
}
4 changes: 2 additions & 2 deletions app/Actions/Project/UnPublishProject.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace App\Actions\Project;

use App\Models\Project;
use App\Support\Public\PublicMoleculeAggregates;
use App\Support\Public\PublicMoleculeCatalogIndexer;

class UnPublishProject
{
Expand All @@ -29,6 +29,6 @@ public function unPublish($project)
}
}

PublicMoleculeAggregates::forgetPublicCatalogTotalCache();
app(PublicMoleculeCatalogIndexer::class)->refreshForProject($project);
}
}
3 changes: 2 additions & 1 deletion app/Actions/Study/PublishStudy.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use App\Jobs\ProcessMetadataExtractionBagitGenerationJob;
use App\Models\Study;
use App\Services\ChemotionRepositoryTrackerService;
use App\Support\Public\PublicMoleculeAggregates;
use App\Support\Public\PublicMoleculeCatalogIndexer;
use Illuminate\Support\Facades\Log;

class PublishStudy
Expand All @@ -26,6 +26,7 @@ public function publish($study)
$dataset->save();
}

app(PublicMoleculeCatalogIndexer::class)->refreshForStudy($study);
if ($study->is_public && $study->has_nmrium && filled($study->download_url)) {
ProcessMetadataExtractionBagitGenerationJob::dispatch($study->id);
}
Expand Down
30 changes: 30 additions & 0 deletions app/Console/Commands/IndexPublicMoleculeCatalogCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace App\Console\Commands;

use App\Support\Public\PublicMoleculeCatalogIndexer;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;

#[Signature('nmrxiv:index-public-molecule-catalog {--molecule= : Single molecule id} {--chunk=100 : Molecules per enrichment chunk}')]
#[Description('Denormalize public-catalog membership and compound-card badges onto molecules')]
class IndexPublicMoleculeCatalogCommand extends Command
{
public function handle(PublicMoleculeCatalogIndexer $indexer): int
{
$chunk = max(1, (int) $this->option('chunk'));
$moleculeOption = $this->option('molecule');
$moleculeIds = filled($moleculeOption) ? [(int) $moleculeOption] : null;

$result = $indexer->refresh($moleculeIds, $chunk);

$this->info(sprintf(
'Indexed public molecule catalog (%s in catalog, %s cleared).',
number_format($result['indexed']),
number_format($result['cleared']),
));

return self::SUCCESS;
}
}
18 changes: 9 additions & 9 deletions app/Http/Controllers/API/SearchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ public function search(Request $request, ?string $smiles = null)

$queryType = strtolower($queryType);

$publicSpectraExists = PublicMoleculeAggregates::hasPublicSpectraExistsSql('molecules.id');
$publicSpectraFilter = PublicMoleculeAggregates::hasPublicSpectraColumnSql();
$orderByRecentSql = $sort === 'recent' ? 'ORDER BY molecules.created_at DESC' : '';

$ids = [];
Expand All @@ -774,7 +774,7 @@ public function search(Request $request, ?string $smiles = null)
['ids' => $ids, 'total' => $count] = PublicMoleculeAggregates::paginateIds(
[
'from' => 'FROM molecules',
'where' => "WHERE identifier IS NOT NULL AND (smiles LIKE ? OR absolute_smiles LIKE ? OR canonical_smiles LIKE ?) AND {$publicSpectraExists}",
'where' => "WHERE identifier IS NOT NULL AND (smiles LIKE ? OR absolute_smiles LIKE ? OR canonical_smiles LIKE ?) AND {$publicSpectraFilter}",
'order' => $orderByRecentSql,
],
['%'.$query.'%', '%'.$query.'%', '%'.$query.'%'],
Expand All @@ -786,7 +786,7 @@ public function search(Request $request, ?string $smiles = null)
['ids' => $ids, 'total' => $count] = PublicMoleculeAggregates::paginateIds(
[
'from' => 'FROM mols INNER JOIN molecules ON molecules.id = mols.id',
'where' => "WHERE m@>? AND molecules.identifier IS NOT NULL AND {$publicSpectraExists}",
'where' => "WHERE m@>? AND molecules.identifier IS NOT NULL AND {$publicSpectraFilter}",
'id' => 'mols.id',
'order' => $orderByRecentSql,
],
Expand All @@ -803,7 +803,7 @@ public function search(Request $request, ?string $smiles = null)
['ids' => $ids, 'total' => $count] = PublicMoleculeAggregates::paginateIds(
[
'from' => 'FROM molecules',
'where' => "WHERE identifier IS NOT NULL AND (inchi LIKE ? OR standard_inchi LIKE ?) AND {$publicSpectraExists}",
'where' => "WHERE identifier IS NOT NULL AND (inchi LIKE ? OR standard_inchi LIKE ?) AND {$publicSpectraFilter}",
'order' => $orderByRecentSql,
],
['%'.$query.'%', '%'.$query.'%'],
Expand All @@ -814,7 +814,7 @@ public function search(Request $request, ?string $smiles = null)
['ids' => $ids, 'total' => $count] = PublicMoleculeAggregates::paginateIds(
[
'from' => 'FROM molecules',
'where' => "WHERE identifier IS NOT NULL AND (inchi_key LIKE ? OR standard_inchi_key LIKE ?) AND {$publicSpectraExists}",
'where' => "WHERE identifier IS NOT NULL AND (inchi_key LIKE ? OR standard_inchi_key LIKE ?) AND {$publicSpectraFilter}",
'order' => $orderByRecentSql,
],
['%'.$query.'%', '%'.$query.'%'],
Expand All @@ -826,7 +826,7 @@ public function search(Request $request, ?string $smiles = null)
['ids' => $ids, 'total' => $count] = PublicMoleculeAggregates::paginateIds(
[
'from' => 'FROM mols INNER JOIN molecules ON molecules.id = mols.id',
'where' => "WHERE m@=? AND molecules.identifier IS NOT NULL AND {$publicSpectraExists}",
'where' => "WHERE m@=? AND molecules.identifier IS NOT NULL AND {$publicSpectraFilter}",
'id' => 'mols.id',
'order' => $orderByRecentSql,
],
Expand All @@ -844,7 +844,7 @@ public function search(Request $request, ?string $smiles = null)
['ids' => $ids, 'total' => $count] = PublicMoleculeAggregates::paginateIds(
[
'from' => 'FROM fps INNER JOIN molecules ON molecules.id = fps.id',
'where' => "WHERE mfp2%morganbv_fp(?) AND molecules.identifier IS NOT NULL AND {$publicSpectraExists}",
'where' => "WHERE mfp2%morganbv_fp(?) AND molecules.identifier IS NOT NULL AND {$publicSpectraFilter}",
'id' => 'fps.id',
'order' => $orderByRecentSql,
],
Expand Down Expand Up @@ -884,7 +884,7 @@ public function search(Request $request, ?string $smiles = null)
['ids' => $ids, 'total' => $count] = PublicMoleculeAggregates::paginateIds(
[
'from' => 'FROM molecules',
'where' => "WHERE identifier IS NOT NULL AND (name::TEXT ILIKE ? OR iupac_name ILIKE ? OR synonyms::TEXT ILIKE ? OR identifier::TEXT ILIKE ?) AND {$publicSpectraExists}",
'where' => "WHERE identifier IS NOT NULL AND (name::TEXT ILIKE ? OR iupac_name ILIKE ? OR synonyms::TEXT ILIKE ? OR identifier::TEXT ILIKE ?) AND {$publicSpectraFilter}",
'order' => $orderByRecentSql,
],
['%'.$query.'%', '%'.$query.'%', '%'.$query.'%', '%'.$query.'%'],
Expand Down Expand Up @@ -1135,7 +1135,7 @@ private function buildSecureFilterQuery(string $query, array $filterMap, int $li
}

$whereClause = implode(' OR ', $whereConditions);
$publicSpectraFilter = PublicMoleculeAggregates::hasPublicSpectraExistsSql('molecules.id');
$publicSpectraFilter = PublicMoleculeAggregates::hasPublicSpectraColumnSql();

['ids' => $ids, 'total' => $count] = PublicMoleculeAggregates::paginateIds(
[
Expand Down
4 changes: 4 additions & 0 deletions app/Models/Molecule.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ protected function casts(): array
{
return [
'workspace_experiment_type_counts' => 'array',
'has_public_spectra' => 'boolean',
'public_samples_count' => 'integer',
'public_experiment_type_counts' => 'array',
'public_catalog_indexed_at' => 'datetime',
];
}

Expand Down
70 changes: 53 additions & 17 deletions app/Support/Public/PublicMoleculeAggregates.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,17 @@ final class PublicMoleculeAggregates

private const PUBLIC_CATALOG_TOTAL_CACHE_SECONDS = 300;

/**
* Indexed public-catalog membership written by PublicMoleculeCatalogIndexer.
*/
public static function hasPublicSpectraColumnSql(string $table = 'molecules'): string
{
return "{$table}.has_public_spectra = true";
}

/**
* Correlated EXISTS: molecule has ≥1 spectrum in the public catalog.
* Used by the catalog indexer, not by request-path search.
*/
public static function hasPublicSpectraExistsSql(string $moleculeIdColumn = 'molecules.id'): string
{
Expand Down Expand Up @@ -133,12 +142,12 @@ public static function paginatePublicCatalog(
int $offset,
bool $orderByRecent = true,
): array {
$exists = self::hasPublicSpectraExistsSql('molecules.id');
$public = self::hasPublicSpectraColumnSql();

return self::paginateIds(
[
'from' => 'FROM molecules',
'where' => "WHERE molecules.identifier IS NOT NULL AND {$exists}",
'where' => "WHERE molecules.identifier IS NOT NULL AND {$public}",
'id' => 'molecules.id',
'order' => $orderByRecent ? 'ORDER BY molecules.created_at DESC' : '',
],
Expand Down Expand Up @@ -192,11 +201,11 @@ public static function forgetPublicCatalogTotalCache(): void
*/
public static function publicCatalogTotal(): int
{
$exists = self::hasPublicSpectraExistsSql('molecules.id');
$public = self::hasPublicSpectraColumnSql();

return self::countIds(
'FROM molecules',
"WHERE molecules.identifier IS NOT NULL AND {$exists}",
"WHERE molecules.identifier IS NOT NULL AND {$public}",
'molecules.id',
[],
self::PUBLIC_CATALOG_TOTAL_CACHE_KEY,
Expand All @@ -211,7 +220,7 @@ public static function scopePublicCatalog(Builder $query): Builder
{
return $query
->whereNotNull('identifier')
->whereRaw(self::hasPublicSpectraExistsSql($query->getModel()->getTable().'.id'));
->where('has_public_spectra', true);
}

/**
Expand All @@ -224,19 +233,9 @@ public static function enrich(array $molecules): array
return $molecules;
}

$ids = array_values(array_unique(array_map(
fn ($molecule) => $molecule instanceof Molecule ? (int) $molecule->id : (int) $molecule->id,
$molecules,
)));

$sampleCounts = self::sampleCountsByMoleculeId($ids);
$experimentCounts = (new MoleculeExperimentTypeCounts)->forPublicCatalog($ids);

foreach ($molecules as $molecule) {
$id = $molecule instanceof Molecule ? (int) $molecule->id : (int) $molecule->id;

$samples = $sampleCounts[$id] ?? 0;
$experiments = $experimentCounts[$id] ?? [];
$samples = self::publicSamplesCountFromRow($molecule);
$experiments = self::publicExperimentTypeCountsFromRow($molecule);

if ($molecule instanceof Molecule) {
$molecule->setAttribute('workspace_samples_count', $samples);
Expand All @@ -250,6 +249,20 @@ public static function enrich(array $molecules): array
return $molecules;
}

/**
* Sample and experiment-type badge payload for the catalog indexer.
*
* @param list<int> $moleculeIds
* @return array{sample_counts: array<int, int>, experiment_counts: array<int, array<string, int>>}
*/
public static function catalogCardPayload(array $moleculeIds): array
{
return [
'sample_counts' => self::sampleCountsByMoleculeId($moleculeIds),
'experiment_counts' => (new MoleculeExperimentTypeCounts)->forPublicCatalog($moleculeIds),
];
}

/**
* @param list<mixed> $bindings
*/
Expand Down Expand Up @@ -277,6 +290,29 @@ private static function countIds(
return (int) Cache::remember($totalCacheKey, $totalCacheSeconds, $resolve);
}

private static function publicSamplesCountFromRow(object|Molecule $molecule): int
{
return (int) ($molecule->public_samples_count ?? 0);
}

/**
* @return array<string, int>
*/
private static function publicExperimentTypeCountsFromRow(object|Molecule $molecule): array
{
$value = $molecule->public_experiment_type_counts ?? [];

if (is_string($value)) {
$value = json_decode($value, true);
}

if (is_object($value)) {
$value = json_decode(json_encode($value), true);
}

return is_array($value) ? $value : [];
}

/**
* @param array<int, int> $moleculeIds
* @return array<int, int>
Expand Down
Loading
Loading