Skip to content

Commit 5776b62

Browse files
committed
#43 added /rest/v2/search/molecule endpoint for finding out if a molecule is in the fragment network
1 parent ef65640 commit 5776b62

File tree

8 files changed

+226
-23
lines changed

8 files changed

+226
-23
lines changed

docker-compose.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,26 @@ services:
1010
# CREATE INDEX ON :F2(smiles);
1111
#
1212
neo4j:
13-
image: informaticsmatters/fragnet-test:fragalysis-ro4-xchem-11
13+
#image: informaticsmatters/fragnet-test:fragalysis-ro4-xchem-11
14+
image: informaticsmatters/fragnet-test:3.5.25-xchem-combi-sample-2021-02
1415
environment:
1516
NEO4J_dbms_memory_pagecache_size: 2g
1617
NEO4J_dbms_memory_heap_initial__size: 2g
1718
NEO4J_dbms_memory_heap_max__size: 2g
18-
NEO4J_AUTH: neo4j/test123
19-
NEO4J_USERNAME: neo4j
20-
NEO4J_PASSWORD: test123
19+
#NEO4J_AUTH: neo4j/neo4j
20+
#NEO4J_USERNAME: neo4j
21+
#NEO4J_PASSWORD: neo4j
22+
GRAPH_PASSWORD: test123
2123
ports:
2224
- "7474:7474"
2325
- "7687:7687"
2426

2527
# This container runs the REST API that queries the test database.
2628
# Some useful queries are:
2729
# export FRAGNET_SERVER=http://localhost
28-
# curl $FRAGNET_SERVER:8080/fragnet-search/rest/v2/search/suppliers
2930
# curl $FRAGNET_SERVER:8080/fragnet-search/rest/ping
31+
# curl $FRAGNET_SERVER:8080/fragnet-search/rest/v2/search/suppliers
32+
# curl "$FRAGNET_SERVER:8080/fragnet-search/rest/v2/search/molecule/OC(Cn1ccnn1)C1CC1"
3033
# curl "$FRAGNET_SERVER:8080/fragnet-search/rest/v2/search/neighbourhood/OC(Cn1ccnn1)C1CC1?hac=3&rac=1&hops=2"
3134
# curl "$FRAGNET_SERVER:8080/fragnet-search/rest/v2/search/availability/OC(Cn1ccnn1)C1CC1"
3235
# curl "$FRAGNET_SERVER:8080/fragnet-search/rest/v2/search/expand/OC(Cn1ccnn1)C1CC1?hacMin=5&hacMax=10&racMin=3&racMax=3&hops=2"
@@ -46,4 +49,4 @@ services:
4649
fragnet-depict:
4750
image: squonk/fragnet-depict:latest
4851
ports:
49-
- "8090:8080"
52+
- "8090:8080"

fragnet-search/README.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ The aim is to provide strongly opinionated searches against specific types of da
66
that is independent of the Neo4j graph model that can be used on the client side and that somewhat insulates client code
77
for the exact details of the Neo4j data model.
88

9-
Currently this work is at an early stage and much more is planned.
10-
119
## API versions
1210

1311
The APIs are versioned. Currently `v2` is in use, but the older `v1` is still supported. See [README_v1.md]() for details.
@@ -27,10 +25,11 @@ The current search types that are supported are:
2725

2826
1. Supplier search - find the different suppliers that are in the database. Other searches can be
2927
restricted to specific suppliers.
30-
2. Molecule neighbourhood search - find the local graph network surrounding a specific molecule.
31-
3. Availability search - find the forms of a molecule from the fragment network that are available from suppliers
32-
4. Expansion search - expand out a single molecule returning isomeric molecules that can be purchased
33-
5. Expansion multi search - expand out a set of molecules returning isomeric molecules that can be purchased
28+
2. Molecule search - is the specified molecule part of the fragment network.
29+
3. Molecule neighbourhood search - find the local graph network surrounding a specific molecule.
30+
4. Availability search - find the forms of a molecule from the fragment network that are available from suppliers
31+
5. Expansion search - expand out a single molecule returning isomeric molecules that can be purchased
32+
6. Expansion multi search - expand out a set of molecules returning isomeric molecules that can be purchased
3433

3534

3635
### Supplier search
@@ -44,6 +43,33 @@ Each supplier object has a name and label property.
4443
If restricting searches to specific suppliers then specify the suppliers query parameter and give it the value
4544
of a comma separated list of supplier names. These must be specified __exactly__ as found in the result of this query.
4645

46+
### Molecule search
47+
48+
This allows you to find out if the specified molecule is part of the fragment network.
49+
This is available from the `fragnet-search/rest/v2/search/molecule/{smiles}` endpoint.
50+
If the molecule is not present you get a 404 response. If it is present you get back a 200 response containing JSON
51+
with basic information about the molecule, e.g.
52+
```
53+
{
54+
"id": 3101538,
55+
"smiles": "OC(Cn1ccnn1)C1CC1",
56+
"molType": "NET_FRAG",
57+
"labels": [
58+
"F2"
59+
],
60+
"props": {
61+
"inchik": "RRDAVGHEBCZBLM-UHFFFAOYNA-N",
62+
"osmiles": "OC(CC1CCCC1)C1CC1",
63+
"chac": 8,
64+
"hac": 11,smiles
65+
"inchis": "InChI=1/C7H11N3O/c11-7(6-1-2-6)5-10-4-3-8-9-10/h3-4,6-7,11H,1-2,5H2MA"
66+
}
67+
}
68+
```
69+
The molecule is typically specified as SMILES as illustrated above, but it can be specified as in Molfile format in which
70+
case the molfile must be POSTed to the `fragnet-search/rest/v2/search/molecule` endpoint AND the mime-type must be set to
71+
`chemical/x-mdl-molfile` using the `Content-type` header.
72+
4773
### Molecule neighbourhood search
4874

4975
This is available from the `fragnet-search/rest/v2/search/neighbourhood/{smiles}` endpoint.

fragnet-search/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ task buildDockerFile(type: Dockerfile) {
132132
]
133133

134134
destFile = project.file('build/Dockerfile')
135-
from "informaticsmatters/rdkit-tomcat-debian:Release_2019_09"
135+
from "informaticsmatters/rdkit-tomcat-debian:Release_2020_09"
136136
label(['maintainer': 'Tim Dudgeon "[email protected]"'])
137137

138138
// include the keycloak adapters

fragnet-search/src/main/java/org/squonk/fragnet/chem/MolStandardize.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019 Informatics Matters Ltd.
2+
* Copyright (c) 2021 Informatics Matters Ltd.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -46,7 +46,7 @@ public static String prepareNonisoMol(@NotNull String molecule, @NotNull String
4646
if (Constants.MIME_TYPE_SMILES.equals(mimeType)) {
4747
mol = RWMol.MolFromSmiles(molecule);
4848
} else if (Constants.MIME_TYPE_MOLFILE.equals(mimeType)) {
49-
LOG.info("MOL: |" + molecule + "|");
49+
LOG.fine("MOL: |" + molecule + "|");
5050
mol = RWMol.MolFromMolBlock(molecule, true);
5151
} else {
5252
throw new IllegalArgumentException("Unexpected molecule format: " + mimeType);

fragnet-search/src/main/java/org/squonk/fragnet/search/model/v2/FragmentGraph.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019 Informatics Matters Ltd.
2+
* Copyright (c) 2021 Informatics Matters Ltd.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -289,7 +289,7 @@ public void add(Path path) {
289289
});
290290
}
291291

292-
private MoleculeNode generateMoleculeNode(Node node) {
292+
public static MoleculeNode generateMoleculeNode(Node node) {
293293
List<String> labels = new ArrayList<>();
294294
node.labels().forEach((l) -> labels.add(l));
295295
MoleculeNode.MoleculeType type = null;

fragnet-search/src/main/java/org/squonk/fragnet/search/queries/v2/ExpansionQuery.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020 Informatics Matters Ltd.
2+
* Copyright (c) 2021 Informatics Matters Ltd.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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.types.Node;
22+
import org.squonk.fragnet.chem.MolStandardize;
23+
24+
import org.squonk.fragnet.search.model.v2.FragmentGraph;
25+
import org.squonk.fragnet.search.model.v2.MoleculeNode;
26+
import org.squonk.fragnet.search.queries.AbstractQuery;
27+
28+
import javax.validation.constraints.NotNull;
29+
import java.util.Map;
30+
import java.util.logging.Logger;
31+
32+
import static org.neo4j.driver.v1.Values.parameters;
33+
34+
public class MoleculeQuery extends AbstractQuery {
35+
36+
private static final Logger LOG = Logger.getLogger(MoleculeQuery.class.getName());
37+
38+
public MoleculeQuery(Session session) {
39+
super(session);
40+
41+
}
42+
43+
private final String MOLECULE_QUERY = "MATCH p=(m:F2) WHERE m.smiles=$smiles RETURN m";
44+
45+
@Override
46+
protected String getQueryTemplate() {
47+
return MOLECULE_QUERY;
48+
}
49+
50+
public MoleculeNode 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+
MoleculeNode value = getSession().writeTransaction((tx) -> {
56+
LOG.fine("Executing MoleculeQuery: " + MOLECULE_QUERY);
57+
StatementResult result = tx.run(MOLECULE_QUERY, parameters(new Object[]{"smiles", stdSmiles}));
58+
59+
MoleculeNode molNode = null;
60+
if (result.hasNext()) {
61+
Record rec = result.next();
62+
Map<String, Object> m = rec.asMap();
63+
Node n = (Node) m.get("m");
64+
molNode = FragmentGraph.generateMoleculeNode(n);
65+
}
66+
return molNode;
67+
});
68+
69+
return value;
70+
}
71+
}

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

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020 Informatics Matters Ltd.
2+
* Copyright (c) 2021 Informatics Matters Ltd.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -54,6 +54,11 @@ public class FragnetSearchRouteBuilder extends AbstractFragnetSearchRouteBuilder
5454
private List<Map<String, String>> suppliers;
5555
private Map<String, String> supplierMappings;
5656

57+
private final Counter moleculeSearchRequestsTotal = Counter.build()
58+
.name("requests_molecule_total")
59+
.help("Total number of molecule search requests")
60+
.register();
61+
5762
private final Counter neighbourhoodSearchRequestsTotal = Counter.build()
5863
.name("requests_neighbourhood_total")
5964
.help("Total number of neighbourhood search requests")
@@ -74,6 +79,11 @@ public class FragnetSearchRouteBuilder extends AbstractFragnetSearchRouteBuilder
7479
.help("Total duration of calculations")
7580
.register();
7681

82+
private final Counter moleculeSearchNeo4jSearchDuration = Counter.build()
83+
.name("duration_molecule_neo4j_ns")
84+
.help("Total duration of molecule Neo4j cypher query")
85+
.register();
86+
7787
private final Counter neighbourhoodSearchNeo4jSearchDuration = Counter.build()
7888
.name("duration_neighbourhood_neo4j_ns")
7989
.help("Total duration of neighbourhood Neo4j cypher query")
@@ -84,6 +94,16 @@ public class FragnetSearchRouteBuilder extends AbstractFragnetSearchRouteBuilder
8494
.help("Total duration of mcs determination")
8595
.register();
8696

97+
private final Counter moleculeSearchHitsTotal = Counter.build()
98+
.name("results_molecules_hits_molecules")
99+
.help("Total number of molecule search hits")
100+
.register();
101+
102+
private final Counter moleculeSearchMissesTotal = Counter.build()
103+
.name("results_molecules_misses_molecules")
104+
.help("Total number of molecule search misses")
105+
.register();
106+
87107
private final Counter neighbourhoodSearchHitsTotal = Counter.build()
88108
.name("results_neighbourhood_hits_molecules")
89109
.help("Total number of molecules found for neighbourhood search")
@@ -160,6 +180,27 @@ public void configure() throws Exception {
160180
getUserInfo(exch);
161181
})
162182
.endRest()
183+
// Is this molecule part of the fragment network
184+
// example:
185+
// curl "$FRAGNET_SERVER/fragnet-search/rest/v2/search/molecule/OC(Cn1ccnn1)C1CC1"
186+
.get("molecule/{smiles}").description("Molecule search")
187+
.param().name("smiles").type(RestParamType.path).description("SMILES query").endParam()
188+
.produces("application/json")
189+
.route()
190+
.process((Exchange exch) -> {
191+
executeMoleculeQuery(exch);
192+
})
193+
.endRest()
194+
.post("molecule/").description("Molecule search")
195+
.bindingMode(RestBindingMode.off)
196+
.param().name("molfile").type(RestParamType.body).description("Molfile query").endParam()
197+
.produces("application/json")
198+
.route()
199+
.process((Exchange exch) -> {
200+
executeMoleculeQuery(exch);
201+
})
202+
.marshal().json(JsonLibrary.Jackson)
203+
.endRest()
163204
// example:
164205
// curl "$FRAGNET_SERVER/fragnet-search/rest/v2/search/neighbourhood/c1ccc%28Nc2nc3ccccc3o2%29cc1?hac=3&rac=1&hops=2&calcs=LOGP,SIM_RDKIT_TANIMOTO"
165206
.get("neighbourhood/{smiles}").description("Neighbourhood search")
@@ -291,15 +332,15 @@ void executeAvailabilityQuery(Exchange exch) {
291332
try {
292333
Availability availability = getAvailability(smiles);
293334
if (availability == null || availability.getItems().size() == 0) {
294-
message.setBody("{\"error\": \"NeighbourhoodQuery Failed\",\"message\",\"SMILES not found\"}");
335+
message.setBody("{\"error\": \"AvailabilityQuery Failed\",\"message\": \"SMILES not found\"}");
295336
message.setHeader(Exchange.HTTP_RESPONSE_CODE, 404);
296337
} else {
297338
message.setBody(availability);
298339
message.setHeader(Exchange.HTTP_RESPONSE_CODE, 200);
299340
}
300341
} catch (Exception ex) {
301-
LOG.log(Level.SEVERE, "NeighbourhoodQuery Failed", ex);
302-
message.setBody("{\"error\": \"NeighbourhoodQuery Failed\",\"message\",\"" + ex.getLocalizedMessage() + "\"}");
342+
LOG.log(Level.SEVERE, "AvailabilityQuery Failed", ex);
343+
message.setBody("{\"error\": \"AvailabilityQuery Failed\",\"message\": \"" + ex.getLocalizedMessage() + "\"}");
303344
message.setHeader(Exchange.HTTP_RESPONSE_CODE, 500);
304345
}
305346
}
@@ -416,7 +457,8 @@ void executeExpansionQuery(Exchange exch, String conentType) {
416457

417458
if (result.getSize() == 0) { // no results found
418459
LOG.info("ExpansionQuery found no results");
419-
writeErrorResponse(message, 404, "{\"error\": \"No Results\",\"message\": \"ExpansionQuery molecule not found in the database\"}");
460+
writeErrorResponse(message, 404,
461+
"{\"error\": \"No Results\",\"message\": \"ExpansionQuery molecule not found in the database or could not be expanded\"}");
420462
} else {
421463
message.setBody(result);
422464
message.setHeader(Exchange.HTTP_RESPONSE_CODE, 200);
@@ -526,6 +568,67 @@ void executeExpansionMultiQuery(Exchange exch) {
526568
}
527569
}
528570

571+
void executeMoleculeQuery(Exchange exch) {
572+
573+
LOG.info("Executing executeMoleculeQuery");
574+
575+
moleculeSearchRequestsTotal.inc();
576+
577+
Message message = exch.getIn();
578+
579+
long t0 = System.nanoTime();
580+
String username = getUsername(exch);
581+
582+
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+
}
596+
597+
if (queryMol == null || queryMol.isEmpty()) {
598+
throw new IllegalArgumentException("Query molecule must be specified");
599+
}
600+
601+
MoleculeNode molNode;
602+
try (Session session = graphdb.getSession()) {
603+
// execute the query
604+
MoleculeQuery query = new MoleculeQuery(session);
605+
606+
long n0 = System.nanoTime();
607+
molNode = query.execute(queryMol, mimeType);
608+
long n1 = System.nanoTime();
609+
moleculeSearchNeo4jSearchDuration.inc((double) (n1 - n0));
610+
if (molNode == null) {
611+
moleculeSearchMissesTotal.inc(1.0d);
612+
// throw 404
613+
message.setBody("{\"error\": \"MoleculeQuery Failed\",\"message\": \"Molecule not found\"}");
614+
message.setHeader(Exchange.HTTP_RESPONSE_CODE, 404);
615+
} else {
616+
moleculeSearchHitsTotal.inc(1.0d);
617+
message.setBody(molNode);
618+
message.setHeader(Exchange.HTTP_RESPONSE_CODE, 200);
619+
}
620+
}
621+
622+
} catch (Exception ex) {
623+
LOG.log(Level.SEVERE, "MoleculeQuery Failed", ex);
624+
neighbourhoodSearchErrorsTotal.inc();
625+
message.setBody("{\"error\": \"MoleculeQuery Failed\",\"message\":\"" + ex.getLocalizedMessage() + "\"}");
626+
message.setHeader(Exchange.HTTP_RESPONSE_CODE, 500);
627+
628+
long t1 = System.nanoTime();
629+
writeErrorToQueryLog(username, "MoleculeQuery", t1 - t0, ex.getLocalizedMessage());
630+
}
631+
}
529632

530633
void executeNeighbourhoodQuery(Exchange exch) {
531634

0 commit comments

Comments
 (0)