package lux.index.field; import static org.junit.Assert.*; import lux.Evaluator; import lux.IndexTestSupport; import lux.QueryStats; import lux.XdmResultSet; import lux.index.XmlIndexer; import lux.index.field.FieldDefinition.Type; import net.sf.saxon.s9api.XdmSequenceIterator; import org.apache.lucene.document.Field.Store; import org.apache.lucene.store.RAMDirectory; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; /** * Tests for features related to XPathFields. */ public class XPathFieldTest { private static IndexTestSupport indexTestSupport; private static Evaluator eval; @BeforeClass public static void init() throws Exception { XmlIndexer indexer = new XmlIndexer(); indexer.getConfiguration().addField(new XPathField("string-length", "string-length(.)", null, Store.YES, Type.INT)); indexer.getConfiguration().addField(new XPathField("string-length-string", "string-length(.)", null, Store.YES, Type.STRING)); indexer.getConfiguration().addField(new XPathField("string-length-long", "string-length(.)", null, Store.YES, Type.LONG)); indexer.getConfiguration().addField(new XPathField("name", "name(*)", null, Store.YES, Type.STRING)); // if the root element has a numeric id, index it as an int: indexer.getConfiguration().addField(new XPathField("id", "*[@id]/string-length()", null, Store.YES, Type.INT)); indexTestSupport = new IndexTestSupport(indexer, new RAMDirectory()); indexTestSupport.indexAllElements("lux/reader-test.xml"); indexTestSupport.reopen(); // commit, make the commit visible eval = indexTestSupport.makeEvaluator(); } @Before public void setup() { eval.setQueryStats(new QueryStats()); } @Test public void testSortByInt() throws Exception { XdmResultSet result = eval.evaluate("count(collection())"); assertEquals ("5", result.getXdmValue().itemAt(0).getStringValue()); // sort by XPathField value, as int String s1 = getStringResult("for $doc in collection() order by string-length($doc) return name($doc/*)"); assertEquals("entities entities title token test", s1); // retrieving stored field values String s2 = getStringResult("for $doc in collection() order by string-length($doc) return $doc/lux:key('string-length')"); assertEquals("2 3 4 16 95", s2); // using lux:search sort argument String s3 = getStringResult("for $doc in lux:search('*:*', 'string-length int') return name($doc/*)"); assertEquals("entities entities title token test", s3); // in descending order String s4 = getStringResult("for $doc in lux:search('*:*', 'string-length int descending') return name($doc/*)"); assertEquals("test token title entities entities", s4); } @Test public void testSortByString() throws Exception { String s = getStringResult("for $doc in lux:search('*:*', 'string-length-string string') return lux:key('string-length', $doc)"); assertEquals("16 2 3 4 95", s); } @Test public void testSortMixed() throws Exception { String s = getStringResult("for $doc in lux:search('*:*', 'name') return name($doc/*)"); assertEquals("entities entities test title token", s); String s2 = getStringResult("for $doc in lux:search('*:*', ('name', 'string-length int')) return lux:key('string-length',$doc)"); assertEquals("2 3 95 4 16", s2); String s3 = getStringResult("for $doc in lux:search('*:*', ('name', 'string-length descending')) return lux:key('string-length',$doc)"); assertEquals("3 2 95 4 16", s3); } @Test public void testSortMissingValues () throws Exception { // default is 'empty least' String s = getStringResult("for $doc in lux:search('*:*', ('id int', 'name ascending')) return (name($doc/*), lux:key('id', $doc))"); assertEquals("entities title token entities 2 test 95", s); // explicit 'empty least' s = getStringResult("for $doc in lux:search('*:*', ('id int', 'name ascending empty least')) return (name($doc/*), lux:key('id', $doc))"); assertEquals("entities title token entities 2 test 95", s); } @Test public void testSortEmptyGreatest () throws Exception { // 'empty greatest' String s1 = getStringResult("for $doc in lux:search('*:*', ('id int empty greatest', 'name ascending')) return (name($doc/*), lux:key('id', $doc))"); assertEquals("entities 2 test 95 entities title token", s1); // 'empty greatest' long s1 = getStringResult("for $doc in lux:search('*:*', ('string-length-long long empty greatest', 'name ascending')) return (name($doc/*), lux:key('string-length-long', $doc))"); assertEquals("entities 2 entities 3 title 4 token 16 test 95", s1); } @Test public void testSortOptimization () throws Exception { // order by an int field String s = getStringResult("(for $doc in collection() order by lux:key('string-length', $doc) return name($doc/*))[1]"); assertEquals ("entities", s); assertEquals (1, eval.getQueryStats().docCount); } @Test public void testSortContextOptimization () throws Exception { // order by an int field String s = getStringResult("(for $doc in collection() order by $doc/lux:key('string-length') return name($doc/*))[1]"); assertEquals ("entities", s); assertEquals (1, eval.getQueryStats().docCount); } @Test public void testSortByConstantFieldValue () throws Exception { // order by a constant value that might look the same - ordering by docid: // What if we produce two sequences, each ordered by a constant involving two different // key calls? They should both be in document order, but the orders will be different...so // we do in fact have to enforce *some* consistent ordering, and docid ordering is the only one that // makes any sense. String name = getStringResult("collection()[1]/ * /name()"); eval.getQueryStats().docCount = 0; String s = getStringResult("(for $doc in collection() order by lux:key('id', collection()[1]) return name($doc/*))[1]"); assertEquals (name, s); // each of 5 documents is retrieved once, so they can be sorted, and then the collection()[1] document is retrieved each time: // - the collection()[1] is not recognized as a constant by the optimizer assertEquals (10, eval.getQueryStats().docCount); } @Test @Ignore public void testSortByXPath() throws Exception { // order by the source path for the field String s = getStringResult("(for $doc in collection() order by string-length($doc) return name($doc/*))[1]"); assertEquals ("entities", s); assertEquals (1, eval.getQueryStats().docCount); } @Test public void testInvalidSortBy() throws Exception { // unknown ordering keyword xxx XdmResultSet result = eval.evaluate("for $doc in lux:search('*:*', 'string-length-string xxx') return lux:key('string-length', $doc)"); assertEquals (1, result.getErrors().size()); assertEquals ("lux.exception.LuxException: invalid keyword 'xxx' in: string-length-string xxx", result.getErrors().get(0).getMessage()); // conflicting ordering keywords "ascending descending" result = eval.evaluate("for $doc in lux:search('*:*', 'string-length-string ascending descending') return lux:key('string-length', $doc)"); assertEquals (1, result.getErrors().size()); assertEquals ("lux.exception.LuxException: invalid ordering keyword in: string-length-string ascending descending", result.getErrors().get(0).getMessage()); // missing keyword after empty result = eval.evaluate("for $doc in lux:search('*:*', 'string-length-string empty') return lux:key('string-length', $doc)"); assertEquals (1, result.getErrors().size()); assertEquals ("lux.exception.LuxException: missing keyword after 'empty' in: string-length-string empty", result.getErrors().get(0).getMessage()); // bad keyword after empty result = eval.evaluate("for $doc in lux:search('*:*', 'string-length-string empty blah') return lux:key('string-length', $doc)"); assertEquals (1, result.getErrors().size()); assertEquals ("lux.exception.LuxException: missing or invalid keyword after 'empty' in: string-length-string empty blah", result.getErrors().get(0).getMessage()); // reverse relevance result = eval.evaluate("for $doc in lux:search('*:*', 'lux:score ascending') return lux:key('string-length', $doc)"); assertEquals (1, result.getErrors().size()); assertEquals ("lux.exception.LuxException: not countenanced: attempt to sort by irrelevance", result.getErrors().get(0).getMessage()); } @Test public void testSortByLong() throws Exception { // treat int field as long - generates an error XdmResultSet result = eval.evaluate("for $doc in lux:search('*:*', 'string-length long') return name($doc/*)"); assertEquals (1, result.getErrors().size()); String s = getStringResult("for $doc in lux:search('*:*', 'string-length-long long') return name($doc/*)"); assertEquals("entities entities title token test", s); } // return null if no results, else concatenate the string values of all the results, // separated by a single space private String getStringResult (String query) { XdmResultSet result = eval.evaluate(query); if (! result.getErrors().isEmpty()) { fail(result.getErrors().get(0).getMessage()); } XdmSequenceIterator iter = result.getXdmValue().iterator(); if (!iter.hasNext()) { return null; } StringBuilder buf = new StringBuilder (); buf.append (iter.next().getStringValue()); while (iter.hasNext()) { buf.append (' '); buf.append (iter.next().getStringValue()); } return buf.toString(); } }