Skip to content

Commit 63049b5

Browse files
committed
#44 Implemented as rest/v2/search/fragments endpoint
1 parent 3ee4ad2 commit 63049b5

File tree

3 files changed

+220
-14
lines changed

3 files changed

+220
-14
lines changed

fragnet-search/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,34 @@ hit as well as various information about the query. Example output (part of data
588588
}
589589
```
590590

591+
### Fragments search
592+
593+
This search returns all the child fragments (recursive) of a molecule. The input is the same as the Molecule search,
594+
comprising a single molecule in SMILES or Molfile format.
595+
596+
If that molecule is not found your get a 404 response. If it is found you get a 200 response containing a list of child
597+
fragments in JSON format.
598+
599+
Typical execution looks like this:
600+
```
601+
curl "$FRAGNET_SERVER/fragnet-search/rest/v2/search/fragments/OC(Cn1ccnn1)C1CC1"
602+
```
603+
604+
The response would look like this:
605+
```
606+
[
607+
"[Xe]C1CC1",
608+
"C1CC1",
609+
"c1c[nH]nn1",
610+
"CC(O)[Xe]",
611+
"[Xe]C1CC1.[Xe]n1ccnn1",
612+
"OC(C[Xe])C1CC1",
613+
"[Xe]n1ccnn1",
614+
"OC([Xe])Cn1ccnn1",
615+
"OC([Xe])C[Xe]",
616+
"OCC[Xe]"
617+
]
618+
```
591619

592620
## Authentication
593621

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) 2021 Informatics Matters Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.squonk.fragnet.search.queries.v2;
17+
18+
import org.neo4j.driver.v1.Record;
19+
import org.neo4j.driver.v1.Session;
20+
import org.neo4j.driver.v1.StatementResult;
21+
import org.neo4j.driver.v1.Value;
22+
import org.neo4j.driver.v1.types.Relationship;
23+
import org.squonk.fragnet.chem.MolStandardize;
24+
import org.squonk.fragnet.search.queries.AbstractQuery;
25+
26+
import javax.validation.constraints.NotNull;
27+
import java.util.ArrayList;
28+
import java.util.HashSet;
29+
import java.util.List;
30+
import java.util.logging.Logger;
31+
32+
import static org.neo4j.driver.v1.Values.parameters;
33+
34+
public class FragmentQuery extends AbstractQuery {
35+
36+
private static final Logger LOG = Logger.getLogger(FragmentQuery.class.getName());
37+
38+
public FragmentQuery(Session session) {
39+
super(session);
40+
41+
}
42+
43+
private final String SYNTHON_QUERY = "MATCH (fa:F2 {smiles: $smiles})-[e:FRAG*]->(f:F2) RETURN e";
44+
45+
@Override
46+
protected String getQueryTemplate() {
47+
return SYNTHON_QUERY;
48+
}
49+
50+
public List<String> execute(@NotNull String mol, @NotNull String mimeType) {
51+
52+
// standardize the mol. It can be in smiles or molfile formats
53+
String stdSmiles = MolStandardize.prepareNonisoMol(mol, mimeType);
54+
55+
HashSet<String> values = getSession().writeTransaction((tx) -> {
56+
LOG.fine("Executing MoleculeQuery: " + SYNTHON_QUERY);
57+
StatementResult result = tx.run(SYNTHON_QUERY, parameters(new Object[]{"smiles", stdSmiles}));
58+
HashSet<String> smiles = new HashSet<>();
59+
while (result.hasNext()) {
60+
Record rec = result.next();
61+
Value val = rec.get(0);
62+
List<Object> edges = val.asList();
63+
for (Object o : edges) {
64+
Relationship rel = (Relationship)o;
65+
String label = rel.get("label").asString();
66+
String[] tokens = label.split("\\|");
67+
LOG.fine("Label: " + label + " Tokens: " + tokens[1] + " " + tokens[4]);
68+
smiles.add(tokens[1]);
69+
smiles.add(tokens[4]);
70+
}
71+
}
72+
return smiles;
73+
});
74+
75+
return new ArrayList(values);
76+
}
77+
}

fragnet-search/src/main/java/org/squonk/fragnet/service/v2/FragnetSearchRouteBuilder.java

Lines changed: 115 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ public class FragnetSearchRouteBuilder extends AbstractFragnetSearchRouteBuilder
5959
.help("Total number of molecule search requests")
6060
.register();
6161

62+
private final Counter fragmentSearchRequestsTotal = Counter.build()
63+
.name("requests_fragment_total")
64+
.help("Total number of fragment search requests")
65+
.register();
66+
6267
private final Counter neighbourhoodSearchRequestsTotal = Counter.build()
6368
.name("requests_neighbourhood_total")
6469
.help("Total number of neighbourhood search requests")
@@ -84,6 +89,11 @@ public class FragnetSearchRouteBuilder extends AbstractFragnetSearchRouteBuilder
8489
.help("Total duration of molecule Neo4j cypher query")
8590
.register();
8691

92+
private final Counter fragmentSearchNeo4jSearchDuration = Counter.build()
93+
.name("duration_fragment_neo4j_ns")
94+
.help("Total duration of fragment Neo4j cypher query")
95+
.register();
96+
8797
private final Counter neighbourhoodSearchNeo4jSearchDuration = Counter.build()
8898
.name("duration_neighbourhood_neo4j_ns")
8999
.help("Total duration of neighbourhood Neo4j cypher query")
@@ -104,6 +114,16 @@ public class FragnetSearchRouteBuilder extends AbstractFragnetSearchRouteBuilder
104114
.help("Total number of molecule search misses")
105115
.register();
106116

117+
private final Counter fragmentSearchMissesTotal = Counter.build()
118+
.name("results_fragment_misses_molecules")
119+
.help("Total number of fragment search misses")
120+
.register();
121+
122+
private final Counter fragmentSearchMoleculesTotal = Counter.build()
123+
.name("results_fragments_molecules")
124+
.help("Total number of fragment search fragments")
125+
.register();
126+
107127
private final Counter neighbourhoodSearchHitsTotal = Counter.build()
108128
.name("results_neighbourhood_hits_molecules")
109129
.help("Total number of molecules found for neighbourhood search")
@@ -191,7 +211,7 @@ public void configure() throws Exception {
191211
executeMoleculeQuery(exch);
192212
})
193213
.endRest()
194-
.post("molecule/").description("Molecule search")
214+
.post("molecule").description("Molecule search")
195215
.bindingMode(RestBindingMode.off)
196216
.param().name("molfile").type(RestParamType.body).description("Molfile query").endParam()
197217
.produces("application/json")
@@ -201,6 +221,27 @@ public void configure() throws Exception {
201221
})
202222
.marshal().json(JsonLibrary.Jackson)
203223
.endRest()
224+
// Is this molecule part of the fragment network
225+
// example:
226+
// curl "$FRAGNET_SERVER/fragnet-search/rest/v2/search/molecule/OC(Cn1ccnn1)C1CC1"
227+
.get("fragments/{smiles}").description("Find fragments of a molecule")
228+
.param().name("smiles").type(RestParamType.path).description("SMILES query").endParam()
229+
.produces("application/json")
230+
.route()
231+
.process((Exchange exch) -> {
232+
executeFragmentQuery(exch);
233+
})
234+
.endRest()
235+
.post("fragments").description("MFind fragments of a molecule")
236+
.bindingMode(RestBindingMode.off)
237+
.param().name("molfile").type(RestParamType.body).description("Molfile query").endParam()
238+
.produces("application/json")
239+
.route()
240+
.process((Exchange exch) -> {
241+
executeFragmentQuery(exch);
242+
})
243+
.marshal().json(JsonLibrary.Jackson)
244+
.endRest()
204245
// example:
205246
// curl "$FRAGNET_SERVER/fragnet-search/rest/v2/search/neighbourhood/c1ccc%28Nc2nc3ccccc3o2%29cc1?hac=3&rac=1&hops=2&calcs=LOGP,SIM_RDKIT_TANIMOTO"
206247
.get("neighbourhood/{smiles}").description("Neighbourhood search")
@@ -568,6 +609,23 @@ void executeExpansionMultiQuery(Exchange exch) {
568609
}
569610
}
570611

612+
private String[] fetchSmilesOrMolfile(Message message) {
613+
String queryMol = null;
614+
String mimeType = message.getHeader(Exchange.CONTENT_TYPE, String.class);
615+
LOG.info("mime-type is " + mimeType);
616+
if (mimeType == null) {
617+
mimeType = Constants.MIME_TYPE_SMILES;
618+
}
619+
if (Constants.MIME_TYPE_SMILES.equals(mimeType)) {
620+
queryMol = message.getHeader("smiles", String.class);
621+
} else if (Constants.MIME_TYPE_MOLFILE.equals(mimeType)) {
622+
queryMol = message.getBody(String.class);
623+
} else {
624+
throw new IllegalStateException("Only support SMILES using GET or molfile using POST");
625+
}
626+
return new String[] {queryMol, mimeType};
627+
}
628+
571629
void executeMoleculeQuery(Exchange exch) {
572630

573631
LOG.info("Executing executeMoleculeQuery");
@@ -580,19 +638,9 @@ void executeMoleculeQuery(Exchange exch) {
580638
String username = getUsername(exch);
581639

582640
try {
583-
String queryMol = null;
584-
String mimeType = message.getHeader(Exchange.CONTENT_TYPE, String.class);
585-
LOG.info("mime-type is " + mimeType);
586-
if (mimeType == null) {
587-
mimeType = Constants.MIME_TYPE_SMILES;
588-
}
589-
if (Constants.MIME_TYPE_SMILES.equals(mimeType)) {
590-
queryMol = message.getHeader("smiles", String.class);
591-
} else if (Constants.MIME_TYPE_MOLFILE.equals(mimeType)) {
592-
queryMol = message.getBody(String.class);
593-
} else {
594-
throw new IllegalStateException("Only support SMILES using GET or molfile using POST");
595-
}
641+
String[] data = fetchSmilesOrMolfile(message);
642+
String queryMol = data[0];
643+
String mimeType = data[1];
596644

597645
if (queryMol == null || queryMol.isEmpty()) {
598646
throw new IllegalArgumentException("Query molecule must be specified");
@@ -630,6 +678,59 @@ void executeMoleculeQuery(Exchange exch) {
630678
}
631679
}
632680

681+
void executeFragmentQuery(Exchange exch) {
682+
LOG.info("Executing executeMoleculeQuery");
683+
684+
fragmentSearchRequestsTotal.inc();
685+
686+
Message message = exch.getIn();
687+
688+
long t0 = System.nanoTime();
689+
String username = getUsername(exch);
690+
691+
try {
692+
String[] data = fetchSmilesOrMolfile(message);
693+
String queryMol = data[0];
694+
String mimeType = data[1];
695+
696+
if (queryMol == null || queryMol.isEmpty()) {
697+
throw new IllegalArgumentException("Query molecule must be specified");
698+
}
699+
700+
List<String> smiles;
701+
try (Session session = graphdb.getSession()) {
702+
// execute the query
703+
FragmentQuery query = new FragmentQuery(session);
704+
705+
long n0 = System.nanoTime();
706+
smiles = query.execute(queryMol, mimeType);
707+
long n1 = System.nanoTime();
708+
fragmentSearchNeo4jSearchDuration.inc((double) (n1 - n0));
709+
if (smiles == null || smiles.isEmpty()) {
710+
fragmentSearchMissesTotal.inc(1.0d);
711+
// throw 404
712+
message.setBody("{\"error\": \"MoleculeQuery Failed\",\"message\": \"Molecule not found\"}");
713+
message.setHeader(Exchange.HTTP_RESPONSE_CODE, 404);
714+
} else {
715+
int size = smiles.size();
716+
fragmentSearchMoleculesTotal.inc((double)size);
717+
LOG.info(size + " fragments found");
718+
message.setBody(smiles);
719+
message.setHeader(Exchange.HTTP_RESPONSE_CODE, 200);
720+
}
721+
}
722+
723+
} catch (Exception ex) {
724+
LOG.log(Level.SEVERE, "MoleculeQuery Failed", ex);
725+
neighbourhoodSearchErrorsTotal.inc();
726+
message.setBody("{\"error\": \"MoleculeQuery Failed\",\"message\":\"" + ex.getLocalizedMessage() + "\"}");
727+
message.setHeader(Exchange.HTTP_RESPONSE_CODE, 500);
728+
729+
long t1 = System.nanoTime();
730+
writeErrorToQueryLog(username, "MoleculeQuery", t1 - t0, ex.getLocalizedMessage());
731+
}
732+
}
733+
633734
void executeNeighbourhoodQuery(Exchange exch) {
634735

635736
neighbourhoodSearchRequestsTotal.inc();

0 commit comments

Comments
 (0)