/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Dragos Mihalache
* Florent Guillaume
* Benoit Delbosc
* Benjamin Jalon
*/
package org.nuxeo.ecm.core;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.Blobs;
import org.nuxeo.ecm.core.api.CoreInstance;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.DocumentRef;
import org.nuxeo.ecm.core.api.IterableQueryResult;
import org.nuxeo.ecm.core.api.PathRef;
import org.nuxeo.ecm.core.api.VersioningOption;
import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
import org.nuxeo.ecm.core.event.Event;
import org.nuxeo.ecm.core.event.EventContext;
import org.nuxeo.ecm.core.event.EventService;
import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
import org.nuxeo.ecm.core.query.QueryParseException;
import org.nuxeo.ecm.core.query.sql.NXQL;
import org.nuxeo.ecm.core.storage.sql.listeners.DummyTestListener;
import org.nuxeo.ecm.core.test.CoreFeature;
import org.nuxeo.ecm.core.test.StorageConfiguration;
import org.nuxeo.ecm.core.test.annotations.Granularity;
import org.nuxeo.ecm.core.test.annotations.RepositoryConfig;
import org.nuxeo.runtime.test.runner.Deploy;
import org.nuxeo.runtime.test.runner.Features;
import org.nuxeo.runtime.test.runner.FeaturesRunner;
import org.nuxeo.runtime.test.runner.LocalDeploy;
import org.nuxeo.runtime.transaction.TransactionHelper;
@RunWith(FeaturesRunner.class)
@Features(CoreFeature.class)
@RepositoryConfig(cleanup = Granularity.METHOD)
@Deploy({ "org.nuxeo.ecm.core.convert.api", //
"org.nuxeo.ecm.core.convert", //
"org.nuxeo.ecm.core.convert.plugins", //
})
@LocalDeploy({ "org.nuxeo.ecm.core.test.tests:OSGI-INF/testquery-core-types-contrib.xml",
"org.nuxeo.ecm.core.test.tests:OSGI-INF/test-repo-core-types-contrib.xml",
"org.nuxeo.ecm.core.test.tests:OSGI-INF/test-repo-core-types-contrib-2.xml",
"org.nuxeo.ecm.core.test.tests:OSGI-INF/disable-schedulers.xml" })
public class TestSQLRepositoryFulltextQuery {
@Inject
protected CoreFeature coreFeature;
@Inject
protected EventService eventService;
@Inject
protected CoreSession session;
protected boolean isDBS() {
return coreFeature.getStorageConfiguration().isDBS();
}
protected boolean isDBSMongoDB() {
return coreFeature.getStorageConfiguration().isDBSMongoDB();
}
protected void reopenSession() {
session = coreFeature.reopenCoreSession();
}
protected void waitForFulltextIndexing() {
nextTransaction();
coreFeature.getStorageConfiguration().waitForFulltextIndexing();
}
protected void waitForAsyncCompletion() {
nextTransaction();
eventService.waitForAsyncCompletion();
}
protected void nextTransaction() {
if (TransactionHelper.isTransactionActiveOrMarkedRollback()) {
TransactionHelper.commitOrRollbackTransaction();
TransactionHelper.startTransaction();
}
}
protected static void assertIdSet(DocumentModelList dml, String... ids) {
Collection<String> expected = new HashSet<>(Arrays.asList(ids));
Collection<String> actual = dml.stream().map(DocumentModel::getId).collect(Collectors.toSet());
assertEquals(expected, actual);
}
protected static void assertEventSet(String... expectedEventNames) {
List<String> list = getDummyListenerEvents();
Map<String, AtomicInteger> map = new HashMap<>();
for (String name : list) {
AtomicInteger i = map.get(name);
if (i == null) {
map.put(name, i = new AtomicInteger(0));
}
i.incrementAndGet();
}
Set<String> set = new HashSet<>();
for (Entry<String, AtomicInteger> es : map.entrySet()) {
set.add(es.getKey() + '=' + es.getValue());
}
assertEquals(new HashSet<>(Arrays.asList(expectedEventNames)), set);
}
protected static List<String> getDummyListenerEvents() {
List<String> actual = new ArrayList<>();
for (Event event : DummyTestListener.EVENTS_RECEIVED) {
String eventName = event.getName();
EventContext context = event.getContext();
if (context instanceof DocumentEventContext) {
DocumentModel doc = ((DocumentEventContext) context).getSourceDocument();
if (doc != null) {
if (doc.isProxy()) {
eventName += "/p";
} else if (doc.isVersion()) {
eventName += "/v";
} else if (doc.isFolder()) {
eventName += "/f";
}
}
}
actual.add(eventName);
}
return actual;
}
protected Calendar getCalendar(int year, int month, int day, int hours, int minutes, int seconds) {
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("Europe/Paris"));
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, month - 1); // 0-based
cal.set(Calendar.DAY_OF_MONTH, day);
cal.set(Calendar.HOUR_OF_DAY, hours);
cal.set(Calendar.MINUTE, minutes);
cal.set(Calendar.SECOND, seconds);
return cal;
}
/**
* Creates the following structure of documents:
*
* <pre>
* root (UUID_1)
* |- testfolder1 (UUID_2)
* | |- testfile1 (UUID_3) (content UUID_4)
* | |- testfile2 (UUID_5) (content UUID_6)
* | \- testfile3 (UUID_7) (Note)
* \- tesfolder2 (UUID_8)
* \- testfolder3 (UUID_9)
* \- testfile4 (UUID_10) (content UUID_11)
* </pre>
*/
protected void createDocs() throws Exception {
DocumentModel folder1 = new DocumentModelImpl("/", "testfolder1", "Folder");
folder1.setPropertyValue("dc:title", "testfolder1_Title");
folder1 = session.createDocument(folder1);
DocumentModel file1 = new DocumentModelImpl("/testfolder1", "testfile1", "File");
file1.setPropertyValue("dc:title", "testfile1_Title");
file1.setPropertyValue("dc:description", "testfile1_description");
String content = "Some caf\u00e9 in a restaurant.\nDrink!.\n";
String filename = "testfile.txt";
Blob blob1 = Blobs.createBlob(content);
blob1.setFilename(filename);
file1.setPropertyValue("content", (Serializable) blob1);
Calendar cal1 = getCalendar(2007, 3, 1, 12, 0, 0);
file1.setPropertyValue("dc:created", cal1);
file1.setPropertyValue("dc:coverage", "football");
file1.setPropertyValue("dc:subjects", new String[] { "foo", "gee/moo" });
file1.setPropertyValue("uid", "uid123");
file1 = session.createDocument(file1);
DocumentModel file2 = new DocumentModelImpl("/testfolder1", "testfile2", "File");
file2.setPropertyValue("dc:title", "testfile2_Title");
file2.setPropertyValue("dc:description", "testfile2_DESCRIPTION2");
Calendar cal2 = getCalendar(2007, 4, 1, 12, 0, 0);
file2.setPropertyValue("dc:created", cal2);
file2.setPropertyValue("dc:contributors", new String[] { "bob", "pete" });
file2.setPropertyValue("dc:coverage", "foo/bar");
file2 = session.createDocument(file2);
DocumentModel file3 = new DocumentModelImpl("/testfolder1", "testfile3", "Note");
file3.setPropertyValue("dc:title", "testfile3_Title");
file3.setPropertyValue("dc:description", "testfile3_desc1 testfile3_desc2, testfile3_desc3");
file3.setPropertyValue("dc:contributors", new String[] { "bob", "john" });
file3 = session.createDocument(file3);
DocumentModel folder2 = new DocumentModelImpl("/", "testfolder2", "Folder");
folder2 = session.createDocument(folder2);
DocumentModel folder3 = new DocumentModelImpl("/testfolder2", "testfolder3", "Folder");
folder3 = session.createDocument(folder3);
// create file 4
DocumentModel file4 = new DocumentModelImpl("/testfolder2/testfolder3", "testfile4", "File");
// title without space or _ for Oracle fulltext searchability
// (testFulltextProxy)
file4.setPropertyValue("dc:title", "testfile4Title");
file4.setPropertyValue("dc:description", "testfile4_DESCRIPTION4");
file4 = session.createDocument(file4);
session.save();
}
/**
* Publishes testfile4 to testfolder1:
* <p>
* version (UUID_12, content UUID_13)
* <p>
* proxy (UUID_14)
*/
protected DocumentModel publishDoc() throws Exception {
DocumentModel doc = session.getDocument(new PathRef("/testfolder2/testfolder3/testfile4"));
DocumentModel sec = session.getDocument(new PathRef("/testfolder1"));
DocumentModel proxy = session.publishDocument(doc, sec);
session.save();
DocumentModelList proxies = session.getProxies(doc.getRef(), sec.getRef());
assertEquals(1, proxies.size());
return proxy;
}
@Test
public void testFulltext() throws Exception {
createDocs();
waitForFulltextIndexing();
String query;
String nquery = null;
DocumentModelList dml;
DocumentModel file1 = session.getDocument(new PathRef("/testfolder1/testfile1"));
DocumentModel file2 = session.getDocument(new PathRef("/testfolder1/testfile2"));
DocumentModel file3 = session.getDocument(new PathRef("/testfolder1/testfile3"));
DocumentModel file4 = session.getDocument(new PathRef("/testfolder2/testfolder3/testfile4"));
// query
query = "SELECT * FROM File WHERE ecm:fulltext = 'world'";
dml = session.query(query);
assertEquals(0, dml.size());
// negative query (not possible on DBS)
if (!isDBS()) {
nquery = "SELECT * FROM File WHERE NOT(ecm:fulltext = 'world')";
dml = session.query(nquery);
assertIdSet(dml, file1.getId(), file2.getId(), file4.getId());
}
file1.setProperty("dublincore", "title", "hello world");
session.saveDocument(file1);
session.save();
waitForFulltextIndexing();
// query
dml = session.query(query);
assertIdSet(dml, file1.getId());
// negative query (not possible on DBS)
if (!isDBS()) {
dml = session.query(nquery);
assertIdSet(dml, file2.getId(), file4.getId());
}
file2.setProperty("dublincore", "description", "the world is my oyster");
session.saveDocument(file2);
session.save();
waitForFulltextIndexing();
// query
dml = session.query(query);
assertIdSet(dml, file1.getId(), file2.getId());
// negative query (not possible on DBS)
if (!isDBS()) {
dml = session.query(nquery);
assertIdSet(dml, file4.getId());
}
file3.setProperty("dublincore", "title", "brave new world");
session.saveDocument(file3);
session.save();
waitForFulltextIndexing();
// query
dml = session.query(query);
assertIdSet(dml, file1.getId(), file2.getId()); // file3 is a Note
// negative query (not possible on DBS)
if (!isDBS()) {
dml = session.query(nquery);
assertIdSet(dml, file4.getId());
}
query = "SELECT * FROM Note WHERE ecm:fulltext = 'world'";
dml = session.query(query);
assertIdSet(dml, file3.getId());
query = "SELECT * FROM Document WHERE ecm:fulltext = 'world' " + "AND dc:contributors = 'pete'";
waitForFulltextIndexing();
dml = session.query(query);
assertIdSet(dml, file2.getId());
// multi-valued field
query = "SELECT * FROM Document WHERE ecm:fulltext = 'bzzt'";
waitForFulltextIndexing();
dml = session.query(query);
assertEquals(0, dml.size());
file1.setProperty("dublincore", "subjects", new String[] { "bzzt" });
session.saveDocument(file1);
session.save();
waitForFulltextIndexing();
query = "SELECT * FROM Document WHERE ecm:fulltext = 'bzzt'";
dml = session.query(query);
assertIdSet(dml, file1.getId());
}
@Test
public void testFulltext2() throws Exception {
createDocs();
waitForFulltextIndexing();
String query;
query = "SELECT * FROM File WHERE ecm:fulltext = 'restaurant'";
assertEquals(1, session.query(query).size());
// negative query (not possible on DBS)
if (!isDBS()) {
query = "SELECT * FROM File WHERE NOT (ecm:fulltext = 'restaurant')";
assertEquals(2, session.query(query).size());
}
// Test multiple fulltext (not possible on DBS)
if (!isDBS()) {
query = "SELECT * FROM File WHERE ecm:fulltext = 'restaurant' OR ecm:fulltext = 'pete'";
assertEquals(2, session.query(query).size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'restaurant' AND ecm:fulltext = 'pete'";
assertEquals(0, session.query(query).size());
}
// other query generation cases
// no union and implicit score sort
query = "SELECT * FROM File WHERE ecm:fulltext = 'restaurant' AND ecm:isProxy = 0";
assertEquals(1, session.query(query).size());
// order by so no implicit score sort
query = "SELECT * FROM File WHERE ecm:fulltext = 'restaurant' ORDER BY dc:title";
assertEquals(1, session.query(query).size());
// order by and no union so no implicit score sort
query = "SELECT * FROM File WHERE ecm:fulltext = 'restaurant' AND ecm:isProxy = 0 ORDER BY dc:title";
assertEquals(1, session.query(query).size());
// no union but distinct so no implicit score sort
query = "SELECT DISTINCT * FROM File WHERE ecm:fulltext = 'restaurant' AND ecm:isProxy = 0";
assertEquals(1, session.query(query).size());
}
@Test
public void testFulltextScore() throws Exception {
String query;
IterableQueryResult res;
Map<String, Serializable> map;
createDocs();
waitForFulltextIndexing();
query = "SELECT ecm:uuid, ecm:fulltextScore FROM File WHERE ecm:fulltext = 'restaurant'";
res = session.queryAndFetch(query, "NXQL");
assertEquals(1, res.size());
map = res.iterator().next();
assertTrue(map.containsKey(NXQL.ECM_UUID));
assertTrue(map.containsKey(NXQL.ECM_FULLTEXT_SCORE));
res.close();
// ORDER BY ecm:fulltextScore DESC added implicitly
query = "SELECT ecm:uuid FROM File WHERE ecm:fulltext = 'restaurant'";
res = session.queryAndFetch(query, "NXQL");
assertEquals(1, res.size());
res.close();
// but not here
query = "SELECT ecm:uuid FROM File WHERE ecm:fulltext = 'restaurant' ORDER BY ecm:uuid";
res = session.queryAndFetch(query, "NXQL");
assertEquals(1, res.size());
res.close();
// ORDER BY ecm:fulltextScore must always be DESC for DBS
// without proxies
query = "SELECT ecm:uuid FROM File WHERE ecm:fulltext = 'restaurant' AND ecm:isProxy = 0 ORDER BY ecm:fulltextScore DESC";
res = session.queryAndFetch(query, "NXQL");
assertEquals(1, res.size());
res.close();
// same with proxies
query = "SELECT ecm:uuid FROM File WHERE ecm:fulltext = 'restaurant' ORDER BY ecm:fulltextScore DESC";
res = session.queryAndFetch(query, "NXQL");
assertEquals(1, res.size());
res.close();
query = "SELECT ecm:uuid, ecm:fulltextScore FROM File WHERE ecm:fulltext = 'restaurant' ORDER BY ecm:fulltextScore DESC";
res = session.queryAndFetch(query, "NXQL");
assertEquals(1, res.size());
res.close();
// cannot select score if there's no search
try {
query = "SELECT ecm:fulltextScore FROM File";
res = session.queryAndFetch(query, "NXQL");
fail("query should fail");
} catch (QueryParseException e) {
assertTrue(e.toString(), e.getMessage().contains("ecm:fulltextScore cannot be used without ecm:fulltext"));
}
// cannot order by score if there's no search
try {
query = "SELECT ecm:uuid FROM File ORDER BY ecm:fulltextScore DESC";
res = session.queryAndFetch(query, "NXQL");
fail("query should fail");
} catch (QueryParseException e) {
assertTrue(e.toString(), e.getMessage().contains("ecm:fulltextScore cannot be used without ecm:fulltext"));
}
}
/*
* This used to crash SQL Server 2008 R2 (NXP-6143). It works on SQL Server 2005.
*/
@Test
public void testFulltextCrashingSQLServer2008() throws Exception {
createDocs();
waitForFulltextIndexing();
String query = "SELECT * FROM File WHERE ecm:fulltext = 'restaurant' AND dc:title = 'testfile1_Title'";
assertEquals(1, session.query(query).size());
}
@Test
public void testFulltextPrefix() throws Exception {
assumeTrue("DBS cannot do prefix fulltext search", !isDBS());
createDocs();
DocumentModel file1 = session.getDocument(new PathRef("/testfolder1/testfile1"));
file1.setPropertyValue("dc:title", "hello world citizens");
session.saveDocument(file1);
session.save();
waitForFulltextIndexing();
String query;
query = "SELECT * FROM File WHERE ecm:fulltext = 'wor*'";
assertEquals(1, session.query(query).size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'wor%'";
assertEquals(1, session.query(query).size());
// BBB for direct PostgreSQL syntax
StorageConfiguration storageConfiguration = coreFeature.getStorageConfiguration();
if (storageConfiguration.isVCSPostgreSQL()) {
query = "SELECT * FROM File WHERE ecm:fulltext = 'wor:*'";
assertEquals(1, session.query(query).size());
}
// prefix in phrase search
// not in H2 (with Lucene default parser)
// not in MySQL
// not in Derby
if (storageConfiguration.isVCSPostgreSQL() //
|| storageConfiguration.isVCSOracle() //
|| storageConfiguration.isVCSSQLServer()) {
query = "SELECT * FROM File WHERE ecm:fulltext = '\"hello wor*\"'";
assertEquals(1, session.query(query).size());
}
// prefix wildcard in the middle of a phrase
// really only in Oracle, and approximation in PostgreSQL
if (storageConfiguration.isVCSPostgreSQL() || storageConfiguration.isVCSOracle()) {
query = "SELECT * FROM File WHERE ecm:fulltext = '\"hel* world\"'";
assertEquals(1, session.query(query).size());
query = "SELECT * FROM File WHERE ecm:fulltext = '\"hel* wor*\"'";
assertEquals(1, session.query(query).size());
// PostgreSQL mid-phrase wildcards are too greedy
if (storageConfiguration.isVCSOracle()) {
// no match wanted here
query = "SELECT * FROM File WHERE ecm:fulltext = '\"hel* citizens\"'";
assertEquals(0, session.query(query).size());
}
}
}
@Test
public void testFulltextSpuriousCharacters() throws Exception {
assumeTrue("DBS cannot remove spurious characters in fulltext search", !isDBS());
createDocs();
waitForFulltextIndexing();
String query = "SELECT * FROM File WHERE ecm:fulltext = 'restaurant :'";
assertEquals(1, session.query(query).size());
}
@Test
public void testFulltextMixin() throws Exception {
createDocs();
DocumentModel file1 = session.getDocument(new PathRef("/testfolder1/testfile1"));
file1.addFacet("Aged");
file1.setPropertyValue("age:age", "barbar");
session.saveDocument(file1);
session.save();
waitForFulltextIndexing();
String query = "SELECT * FROM File WHERE ecm:fulltext = 'barbar'";
assertEquals(1, session.query(query).size());
}
@Test
public void testFulltextProxy() throws Exception {
createDocs();
waitForFulltextIndexing();
String query;
DocumentModelList dml;
DocumentModel doc = session.getDocument(new PathRef("/testfolder2/testfolder3/testfile4"));
String docId = doc.getId();
query = "SELECT * FROM Document WHERE ecm:fulltext = 'testfile4Title'";
dml = session.query(query);
assertIdSet(dml, docId);
// publish doc
DocumentModel proxy = publishDoc();
String proxyId = proxy.getId();
String versionId = proxy.getSourceId();
waitForFulltextIndexing();
// query must return also proxies and versions
dml = session.query(query);
assertIdSet(dml, docId, proxyId, versionId);
// remove proxy
session.removeDocument(proxy.getRef());
session.save();
waitForAsyncCompletion();
session.save(); // process invalidations
// leaves live doc and version
dml = session.query(query);
assertIdSet(dml, docId, versionId);
// remove live doc
session.removeDocument(doc.getRef());
session.save();
// wait for async version removal
waitForAsyncCompletion();
// version gone as well
dml = session.query(query);
assertTrue(dml.isEmpty());
}
@Test
public void testFulltextExpressionSyntax() throws Exception {
assumeTrue(!coreFeature.getStorageConfiguration().isVCSDerby());
createDocs();
waitForFulltextIndexing();
String query;
DocumentModelList dml;
DocumentModel file1 = session.getDocument(new PathRef("/testfolder1/testfile1"));
file1.setProperty("dublincore", "title", "the world is my oyster");
session.saveDocument(file1);
session.save();
waitForFulltextIndexing();
query = "SELECT * FROM File WHERE ecm:fulltext = 'pete'";
dml = session.query(query);
assertEquals(1, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'world'";
dml = session.query(query);
assertEquals(1, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = '+world'";
dml = session.query(query);
assertEquals(1, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'oyster'";
dml = session.query(query);
assertEquals(1, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'kangaroo'"; // absent
dml = session.query(query);
assertEquals(0, dml.size());
// implicit AND
query = "SELECT * FROM File WHERE ecm:fulltext = 'world oyster'";
dml = session.query(query);
assertEquals(1, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'world +oyster'";
dml = session.query(query);
assertEquals(1, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'world kangaroo'";
dml = session.query(query);
assertEquals(0, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'kangaroo oyster'";
dml = session.query(query);
assertEquals(0, dml.size());
// NOT
query = "SELECT * FROM File WHERE ecm:fulltext = 'world -oyster'";
dml = session.query(query);
assertEquals(0, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = '-world oyster'";
dml = session.query(query);
assertEquals(0, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'world -kangaroo'";
dml = session.query(query);
assertEquals(1, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'world -kangaroo -smurf'";
dml = session.query(query);
assertEquals(1, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = '-world kangaroo'";
dml = session.query(query);
assertEquals(0, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'kangaroo -oyster'";
dml = session.query(query);
assertEquals(0, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = '-kangaroo oyster'";
dml = session.query(query);
assertEquals(1, dml.size());
// OR
query = "SELECT * FROM File WHERE ecm:fulltext = 'pete OR world'";
dml = session.query(query);
assertEquals(2, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'pete OR world smurf'";
dml = session.query(query);
assertEquals(isDBSMongoDB() ? 0 : 1, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'pete OR world -smurf'";
dml = session.query(query);
assertEquals(isDBSMongoDB() ? 1 : 2, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = '-smurf world OR pete'";
dml = session.query(query);
assertEquals(isDBSMongoDB() ? 1 : 2, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'pete OR world oyster'";
dml = session.query(query);
assertEquals(isDBSMongoDB() ? 1 : 2, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'pete OR world -oyster'";
dml = session.query(query);
assertEquals(isDBSMongoDB() ? 0 : 1, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = '-oyster world OR pete'";
dml = session.query(query);
assertEquals(isDBSMongoDB() ? 0 : 1, dml.size());
}
// don't use small words, they are eliminated by some fulltext engines
@Test
public void testFulltextExpressionPhrase() throws Exception {
assumeTrue(!coreFeature.getStorageConfiguration().isVCSDerby());
String query;
DocumentModel file1 = new DocumentModelImpl("/", "testfile1", "File");
file1.setPropertyValue("dc:title", "bobby can learn international commerce easily");
file1 = session.createDocument(file1);
// other files with data to avoid words being present in
// too high a percentage of the indexes
for (int i = 0; i < 10; i++) {
DocumentModel f = new DocumentModelImpl("/", "otherfile" + i, "File");
f.setPropertyValue("dc:title", "some other text never matched");
f.setPropertyValue("dc:description", "desc" + i);
f = session.createDocument(f);
}
session.save();
waitForFulltextIndexing();
query = "SELECT * FROM File WHERE ecm:fulltext = '\"international commerce\"'";
assertEquals(1, session.query(query).size());
query = "SELECT * FROM File WHERE ecm:fulltext = '\"learn commerce\"'";
assertEquals(0, session.query(query).size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'bobby \"can learn\"'";
assertEquals(1, session.query(query).size());
// negative phrase search
query = "SELECT * FROM File WHERE ecm:fulltext = 'bobby -\"commerce easily\"'";
assertEquals(0, session.query(query).size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'bobby -\"hello world\"'";
assertEquals(1, session.query(query).size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'bobby -\"hello world\" commerce'";
assertEquals(1, session.query(query).size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'bobby -\"hello world\" \"commerce easily\"'";
assertEquals(1, session.query(query).size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'bobby \"commerce easily\" -\"hello world\"'";
assertEquals(1, session.query(query).size());
}
@Test
public void testFulltextSecondary() throws Exception {
assumeTrue("Skipping multi-fulltext test for unsupported database",
coreFeature.getStorageConfiguration().supportsMultipleFulltextIndexes());
createDocs();
String query;
DocumentModelList dml;
DocumentModel file1 = session.getDocument(new PathRef("/testfolder1/testfile1"));
DocumentModel file2 = session.getDocument(new PathRef("/testfolder1/testfile2"));
DocumentModel file3 = session.getDocument(new PathRef("/testfolder1/testfile3"));
file1.setProperty("dublincore", "title", "hello world");
session.saveDocument(file1);
file2.setProperty("dublincore", "description", "the world is my oyster");
session.saveDocument(file2);
file3.setProperty("dublincore", "title", "brave new world");
session.saveDocument(file3);
session.save();
waitForFulltextIndexing();
// check main fulltext index
query = "SELECT * FROM Document WHERE ecm:fulltext = 'world'";
dml = session.query(query);
assertIdSet(dml, file1.getId(), file2.getId(), file3.getId());
// check secondary fulltext index, just for title field (no secondary indexes on MongoDB)
if (!isDBS()) {
query = "SELECT * FROM Document WHERE ecm:fulltext_title = 'world'";
dml = session.query(query);
assertIdSet(dml, file1.getId(), file3.getId()); // file2 has it in descr
}
// field-based fulltext
// index exists
query = "SELECT * FROM Document WHERE ecm:fulltext.dc:title = 'brave'";
dml = session.query(query);
assertIdSet(dml, file3.getId());
// no index exists
query = "SELECT * FROM Document WHERE ecm:fulltext.dc:description = 'oyster'";
dml = session.query(query);
assertIdSet(dml, file2.getId());
query = "SELECT * FROM Document WHERE ecm:fulltext.dc:description = 'world OYSTER'";
dml = session.query(query);
assertIdSet(dml, file2.getId());
}
@Test
public void testFulltextBlob() throws Exception {
createDocs();
waitForFulltextIndexing();
String query;
DocumentModelList dml;
DocumentModel file1 = session.getDocument(new PathRef("/testfolder1/testfile1"));
query = "SELECT * FROM File WHERE ecm:isProxy = 0 AND ecm:fulltext = 'restaurant'";
dml = session.query(query);
assertIdSet(dml, file1.getId());
query = "SELECT * FROM File WHERE ecm:isProxy = 1 AND ecm:fulltext = 'restaurant'";
dml = session.query(query);
assertEquals(0, dml.size());
query = "SELECT * FROM File WHERE ecm:fulltext = 'restaurant'";
dml = session.query(query);
assertIdSet(dml, file1.getId());
// check text extraction with '\0' in it
String content = "Text with a \0 in it";
Blob blob1 = Blobs.createBlob(content);
file1.setPropertyValue("content", (Serializable) blob1);
session.saveDocument(file1);
session.save();
waitForFulltextIndexing();
}
@Test
public void testFulltextCopy() throws Exception {
createDocs();
String query;
DocumentModelList dml;
DocumentModel folder1 = session.getDocument(new PathRef("/testfolder1"));
DocumentModel file1 = session.getDocument(new PathRef("/testfolder1/testfile1"));
file1.setProperty("dublincore", "title", "hello world");
session.saveDocument(file1);
session.save();
waitForFulltextIndexing();
query = "SELECT * FROM File WHERE ecm:fulltext = 'world'";
dml = session.query(query);
assertIdSet(dml, file1.getId());
// copy
DocumentModel copy = session.copy(file1.getRef(), folder1.getRef(), "file1Copy");
// the save is needed to update the read acls
session.save();
waitForFulltextIndexing();
dml = session.query(query);
assertIdSet(dml, file1.getId(), copy.getId());
}
@Test
public void testFulltextComplexType() throws Exception {
DocumentModel doc = new DocumentModelImpl("/", "complex-doc", "ComplexDoc");
doc = session.createDocument(doc);
DocumentRef docRef = doc.getRef();
session.save();
reopenSession();
waitForAsyncCompletion();
// test setting and reading a map with an empty list
doc = session.getDocument(docRef);
Map<String, Object> attachedFile = new HashMap<>();
List<Map<String, Object>> vignettes = new ArrayList<>();
attachedFile.put("name", "somename");
attachedFile.put("vignettes", vignettes);
doc.setPropertyValue("cmpf:attachedFile", (Serializable) attachedFile);
session.saveDocument(doc);
session.save();
reopenSession();
waitForFulltextIndexing();
// test fulltext indexing of complex property at level one
DocumentModelList results = session.query("SELECT * FROM Document WHERE ecm:fulltext = 'somename'", 1);
assertNotNull(results);
assertEquals(1, results.size());
assertEquals("complex-doc", results.get(0).getTitle());
// test setting and reading a list of maps without a complex type in the
// maps
Map<String, Object> vignette = new HashMap<>();
vignette.put("content", Blobs.createBlob("textblob content"));
vignette.put("label", "vignettelabel");
vignettes.add(vignette);
doc.setPropertyValue("cmpf:attachedFile", (Serializable) attachedFile);
session.saveDocument(doc);
session.save();
reopenSession();
waitForFulltextIndexing();
// test fulltext indexing of complex property at level 3
results = session.query("SELECT * FROM Document" + " WHERE ecm:fulltext = 'vignettelabel'", 2);
assertNotNull(results);
assertEquals(1, results.size());
assertEquals("complex-doc", results.get(0).getTitle());
// test fulltext indexing of complex property at level 3 in blob
results = session.query("SELECT * FROM Document" + " WHERE ecm:fulltext = 'textblob content'", 2);
assertNotNull(results);
assertEquals(1, results.size());
assertEquals("complex-doc", results.get(0).getTitle());
// test deleting the list of vignette and ensure that the fulltext index
// has been properly updated (regression test for NXP-6315)
doc.setPropertyValue("cmpf:attachedFile/vignettes", new ArrayList<Map<String, Object>>());
session.saveDocument(doc);
session.save();
reopenSession();
waitForFulltextIndexing();
results = session.query("SELECT * FROM Document" + " WHERE ecm:fulltext = 'vignettelabel'", 2);
assertNotNull(results);
assertEquals(0, results.size());
results = session.query("SELECT * FROM Document" + " WHERE ecm:fulltext = 'textblob content'", 2);
assertNotNull(results);
assertEquals(0, results.size());
}
@Test
public void testFulltextSecurity() throws Exception {
createDocs();
try (CoreSession bobSession = CoreInstance.openCoreSession(session.getRepositoryName(), "bob")) {
bobSession.query("SELECT * FROM Document WHERE ecm:isProxy = 0 AND ecm:fulltext = 'world'");
// this failed with ORA-00918 on Oracle (NXP-5410)
bobSession.query("SELECT * FROM Document WHERE ecm:fulltext = 'world'");
// we don't care about the answer, just that the query executes
}
}
@Test
public void testFulltextFacet() throws Exception {
DocumentModel doc = new DocumentModelImpl("/", "foo", "File");
doc.addFacet("Aged");
doc.setPropertyValue("age:age", "barbar");
doc = session.createDocument(doc);
session.save();
waitForFulltextIndexing();
DocumentModelList list = session.query("SELECT * FROM File WHERE ecm:fulltext = 'barbar'");
assertEquals(1, list.size());
}
@Test
@LocalDeploy("org.nuxeo.ecm.core.test.tests:OSGI-INF/test-listeners-all-contrib.xml")
public void testFulltextReindexOnCreateDelete() throws Exception {
waitForFulltextIndexing();
// create
DocumentModel doc = new DocumentModelImpl("/", "doc", "File");
doc = session.createDocument(doc);
DummyTestListener.clear();
session.save();
waitForFulltextIndexing();
assertEventSet("sessionSaved=1");
// modify regular
doc.setPropertyValue("dc:title", "The title");
doc = session.saveDocument(doc);
DummyTestListener.clear();
session.save();
waitForFulltextIndexing();
// 2 = 1 main save + 1 index
assertEventSet("sessionSaved=2");
// modify binary
Blob blob = Blobs.createBlob("hello world");
doc.setPropertyValue("file:content", (Serializable) blob);
doc = session.saveDocument(doc);
DummyTestListener.clear();
session.save();
waitForFulltextIndexing();
// 3 = 1 main save + 1 simple index + 1 binary index
assertEventSet("sessionSaved=3", "binaryTextUpdated=1");
// delete
session.removeDocument(doc.getRef());
DummyTestListener.clear();
session.save();
waitForFulltextIndexing();
assertEventSet("sessionSaved=1");
}
@Test
public void testGetBinaryFulltext() throws Exception {
createDocs();
waitForFulltextIndexing();
DocumentModelList list = session.query("SELECT * FROM File WHERE ecm:fulltext = 'Drink'");
assertTrue(!list.isEmpty());
Map<String, String> map = session.getBinaryFulltext(list.get(0).getRef());
assertTrue(map.containsKey("binarytext"));
assertTrue(map.get("binarytext").contains("drink"));
}
@Test
public void testGetBinaryFulltextFromProxy() throws Exception {
createDocs();
waitForFulltextIndexing();
// Publish testfile1 into testfolder2
session.publishDocument(session.getDocument(new PathRef("/testfolder1/testfile1")),
session.getDocument(new PathRef("/testfolder2")));
waitForFulltextIndexing();
DocumentModelList list = session.query("SELECT * FROM File WHERE ecm:fulltext = 'Drink' and ecm:isProxy = 1");
assertTrue(!list.isEmpty());
Map<String, String> map = session.getBinaryFulltext(list.get(0).getRef());
assertTrue(map.containsKey("binarytext"));
assertTrue(map.get("binarytext").contains("drink"));
}
@Test
public void testFulltextAfterAutoVersioning() throws Exception {
String query;
DocumentModelList dml;
DocumentModel doc = session.createDocumentModel("/", "doc", "File");
doc.setPropertyValue("dc:title", "world");
doc = session.createDocument(doc);
session.saveDocument(doc);
session.save();
waitForFulltextIndexing();
query = "SELECT * FROM File WHERE ecm:fulltext = 'world'";
dml = session.query(query);
assertEquals(1, dml.size());
assertIdSet(dml, doc.getId());
// create a version, modify and save
doc.checkIn(VersioningOption.MAJOR, "No comment");
doc.setPropertyValue("dc:title", "universe");
session.saveDocument(doc);
session.save();
waitForFulltextIndexing();
List<DocumentModel> versions = session.getVersions(doc.getRef());
assertEquals(1, versions.size());
DocumentModel ver = versions.get(0);
query = "SELECT * FROM File WHERE ecm:fulltext = 'world'";
dml = session.query(query);
assertIdSet(dml, ver.getId());
query = "SELECT * FROM File WHERE ecm:fulltext = 'universe'";
dml = session.query(query);
assertIdSet(dml, doc.getId());
}
}