blob: 068273d9298498813a5243712981b18564abd807 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
package org.apache.solr.schema;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.SortedSetDocValuesField;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.util.TestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.util.RefCounted;
import org.junit.Before;
import org.junit.BeforeClass;
import static org.hamcrest.CoreMatchers.instanceOf;
public class TestSortableTextField extends SolrTestCaseJ4 {
protected static final String BIG_CONST
= StringUtils.repeat("x", SortableTextField.DEFAULT_MAX_CHARS_FOR_DOC_VALUES);
@BeforeClass
public static void create() throws Exception {
initCore("solrconfig-minimal.xml","schema-sorting-text.xml");
// sanity check our fields & types...
// these should all use docValues (either explicitly or implicitly)...
for (String n : Arrays.asList("keyword_stxt",
"whitespace_stxt", "whitespace_f_stxt", "whitespace_l_stxt")) {
FieldType ft = h.getCore().getLatestSchema().getFieldTypeByName(n);
assertEquals("type " + ft.getTypeName() + " should have docvalues - schema got changed?",
true, ft.getNamedPropertyValues(true).get("docValues")) ;
}
for (String n : Arrays.asList("keyword_stxt", "keyword_dv_stxt",
"whitespace_stxt", "whitespace_nois_stxt",
"whitespace_f_stxt", "whitespace_l_stxt")) {
SchemaField sf = h.getCore().getLatestSchema().getField(n);
assertTrue("field " + sf.getName() + " should have docvalues - schema got changed?",
sf.hasDocValues()) ;
}
{ // this field should *NOT* have docValues .. should behave like a plain old TextField
SchemaField sf = h.getCore().getLatestSchema().getField("whitespace_nodv_stxt");
assertFalse("field " + sf.getName() + " should not have docvalues - schema got changed?",
sf.hasDocValues()) ;
}
}
@Before
public void cleanup() throws Exception {
clearIndex();
}
public void testSimple() throws Exception {
assertU(adoc("id","1", "whitespace_stxt", "how now brown cow ?", "whitespace_f_stxt", "aaa bbb"));
assertU(adoc("id","2", "whitespace_stxt", "how now brown dog ?", "whitespace_f_stxt", "bbb aaa"));
assertU(adoc("id","3", "whitespace_stxt", "how now brown cat ?", "whitespace_f_stxt", "xxx yyy"));
assertU(adoc("id","4", "whitespace_stxt", "dog and cat" /* no val for whitespace_f_stxt */));
assertU(commit());
// search & sort
// NOTE: even if the field is indexed=false, should still be able to sort on it
for (String sortf : Arrays.asList("whitespace_stxt", "whitespace_nois_stxt", "whitespace_plain_str")) {
assertQ(req("q", "whitespace_stxt:cat", "sort", sortf + " asc")
, "//*[@numFound='2']"
, "//result/doc[1]/str[@name='id'][.=4]"
, "//result/doc[2]/str[@name='id'][.=3]"
);
assertQ(req("q", "whitespace_stxt:cat", "sort", sortf + " desc")
, "//*[@numFound='2']"
, "//result/doc[1]/str[@name='id'][.=3]"
, "//result/doc[2]/str[@name='id'][.=4]"
);
assertQ(req("q", "whitespace_stxt:brown", "sort", sortf + " asc")
, "//*[@numFound='3']"
, "//result/doc[1]/str[@name='id'][.=3]"
, "//result/doc[2]/str[@name='id'][.=1]"
, "//result/doc[3]/str[@name='id'][.=2]"
);
assertQ(req("q", "whitespace_stxt:brown", "sort", sortf + " desc")
, "//*[@numFound='3']"
, "//result/doc[1]/str[@name='id'][.=2]"
, "//result/doc[2]/str[@name='id'][.=1]"
, "//result/doc[3]/str[@name='id'][.=3]"
);
// we should still be able to search if docValues="false" (but sort on a diff field)
assertQ(req("q","whitespace_nodv_stxt:cat", "sort", sortf + " asc")
, "//*[@numFound='2']"
, "//result/doc[1]/str[@name='id'][.=4]"
, "//result/doc[2]/str[@name='id'][.=3]"
);
}
// attempting to sort on docValues="false" field should give an error...
assertQEx("attempting to sort on docValues=false field should give an error",
"when docValues=\"false\"",
req("q","*:*", "sort", "whitespace_nodv_stxt asc"),
ErrorCode.BAD_REQUEST);
// sortMissing - whitespace_f_stxt copyField to whitespace_l_stxt
assertQ(req("q","*:*", "sort", "whitespace_f_stxt asc")
, "//*[@numFound='4']"
, "//result/doc[1]/str[@name='id'][.=4]"
, "//result/doc[2]/str[@name='id'][.=1]"
, "//result/doc[3]/str[@name='id'][.=2]"
, "//result/doc[4]/str[@name='id'][.=3]"
);
assertQ(req("q","*:*", "sort", "whitespace_f_stxt desc")
, "//*[@numFound='4']"
, "//result/doc[1]/str[@name='id'][.=4]"
, "//result/doc[2]/str[@name='id'][.=3]"
, "//result/doc[3]/str[@name='id'][.=2]"
, "//result/doc[4]/str[@name='id'][.=1]"
);
assertQ(req("q","*:*", "sort", "whitespace_l_stxt asc")
, "//*[@numFound='4']"
, "//result/doc[1]/str[@name='id'][.=1]"
, "//result/doc[2]/str[@name='id'][.=2]"
, "//result/doc[3]/str[@name='id'][.=3]"
, "//result/doc[4]/str[@name='id'][.=4]"
);
assertQ(req("q","*:*", "sort", "whitespace_l_stxt desc")
, "//*[@numFound='4']"
, "//result/doc[1]/str[@name='id'][.=3]"
, "//result/doc[2]/str[@name='id'][.=2]"
, "//result/doc[3]/str[@name='id'][.=1]"
, "//result/doc[4]/str[@name='id'][.=4]"
);
}
public void testSimpleSearchAndFacets() throws Exception {
assertU(adoc("id","1", "whitespace_stxt", "how now brown cow ?"));
assertU(adoc("id","2", "whitespace_stxt", "how now brown cow ?"));
assertU(adoc("id","3", "whitespace_stxt", "holy cow !"));
assertU(adoc("id","4", "whitespace_stxt", "dog and cat"));
assertU(commit());
// NOTE: even if the field is indexed=false, should still be able to facet on it
for (String facet : Arrays.asList("whitespace_stxt", "whitespace_nois_stxt",
"whitespace_m_stxt", "whitespace_plain_str")) {
for (String search : Arrays.asList("whitespace_stxt", "whitespace_nodv_stxt",
"whitespace_m_stxt", "whitespace_plain_txt")) {
// facet.field
final String fpre = "//lst[@name='facet_fields']/lst[@name='"+facet+"']/";
assertQ(req("q", search + ":cow", "rows", "0",
"facet.field", facet, "facet", "true")
, "//*[@numFound='3']"
, fpre + "int[@name='how now brown cow ?'][.=2]"
, fpre + "int[@name='holy cow !'][.=1]"
, fpre + "int[@name='dog and cat'][.=0]"
);
// json facet
final String jpre = "//lst[@name='facets']/lst[@name='x']/arr[@name='buckets']/";
assertQ(req("q", search + ":cow", "rows", "0",
"json.facet", "{x:{ type: terms, field:'" + facet + "', mincount:0 }}")
, "//*[@numFound='3']"
, jpre + "lst[str[@name='val'][.='how now brown cow ?']][int[@name='count'][.=2]]"
, jpre + "lst[str[@name='val'][.='holy cow !']][int[@name='count'][.=1]]"
, jpre + "lst[str[@name='val'][.='dog and cat']][int[@name='count'][.=0]]"
);
}
}
}
public void testWhiteboxIndexReader() throws Exception {
assertU(adoc("id","1",
"whitespace_stxt", "how now brown cow ?",
"whitespace_m_stxt", "xxx",
"whitespace_m_stxt", "yyy",
"whitespace_f_stxt", "aaa bbb",
"keyword_stxt", "Blarggghhh!"));
assertU(commit());
final RefCounted<SolrIndexSearcher> searcher = h.getCore().getNewestSearcher(false);
try {
final LeafReader r = searcher.get().getSlowAtomicReader();
// common cases...
for (String field : Arrays.asList("keyword_stxt", "keyword_dv_stxt",
"whitespace_stxt", "whitespace_f_stxt", "whitespace_l_stxt")) {
assertNotNull("FieldInfos: " + field, r.getFieldInfos().fieldInfo(field));
assertEquals("DocValuesType: " + field,
DocValuesType.SORTED, r.getFieldInfos().fieldInfo(field).getDocValuesType());
assertNotNull("DocValues: " + field, r.getSortedDocValues(field));
assertNotNull("Terms: " + field, r.terms(field));
}
// special cases...
assertNotNull(r.getFieldInfos().fieldInfo("whitespace_nodv_stxt"));
assertEquals(DocValuesType.NONE,
r.getFieldInfos().fieldInfo("whitespace_nodv_stxt").getDocValuesType());
assertNull(r.getSortedDocValues("whitespace_nodv_stxt"));
assertNotNull(r.terms("whitespace_nodv_stxt"));
//
assertNotNull(r.getFieldInfos().fieldInfo("whitespace_nois_stxt"));
assertEquals(DocValuesType.SORTED,
r.getFieldInfos().fieldInfo("whitespace_nois_stxt").getDocValuesType());
assertNotNull(r.getSortedDocValues("whitespace_nois_stxt"));
assertNull(r.terms("whitespace_nois_stxt"));
//
assertNotNull(r.getFieldInfos().fieldInfo("whitespace_m_stxt"));
assertEquals(DocValuesType.SORTED_SET,
r.getFieldInfos().fieldInfo("whitespace_m_stxt").getDocValuesType());
assertNotNull(r.getSortedSetDocValues("whitespace_m_stxt"));
assertNotNull(r.terms("whitespace_m_stxt"));
} finally {
if (null != searcher) {
searcher.decref();
}
}
}
public void testWhiteboxCreateFields() throws Exception {
List<IndexableField> values = null;
// common case...
for (String field : Arrays.asList("keyword_stxt", "keyword_dv_stxt",
"whitespace_stxt", "whitespace_f_stxt", "whitespace_l_stxt")) {
values = createIndexableFields(field);
assertEquals(field, 2, values.size());
assertThat(field, values.get(0), instanceOf(Field.class));
assertThat(field, values.get(1), instanceOf(SortedDocValuesField.class));
}
// special cases...
values = createIndexableFields("whitespace_nois_stxt");
assertEquals(1, values.size());
assertThat(values.get(0), instanceOf(SortedDocValuesField.class));
//
values = createIndexableFields("whitespace_nodv_stxt");
assertEquals(1, values.size());
assertThat(values.get(0), instanceOf(Field.class));
//
values = createIndexableFields("whitespace_m_stxt");
assertEquals(2, values.size());
assertThat(values.get(0), instanceOf(Field.class));
assertThat(values.get(1), instanceOf(SortedSetDocValuesField.class));
}
private List<IndexableField> createIndexableFields(String fieldName) {
SchemaField sf = h.getCore().getLatestSchema().getField(fieldName);
return sf.getType().createFields(sf, "dummy value");
}
public void testMaxCharsSort() throws Exception {
assertU(adoc("id","1", "whitespace_stxt", "aaa bbb ccc ddd"));
assertU(adoc("id","2", "whitespace_stxt", "aaa bbb xxx yyy"));
assertU(adoc("id","3", "whitespace_stxt", "aaa bbb ccc xxx"));
assertU(adoc("id","4", "whitespace_stxt", "aaa"));
assertU(commit());
// all terms should be searchable in all fields, even if the docvalues are limited
for (String searchF : Arrays.asList("whitespace_stxt", "whitespace_plain_txt",
"whitespace_max3_stxt", "whitespace_max6_stxt",
"whitespace_max0_stxt", "whitespace_maxNeg_stxt")) {
// maxChars of 0 or neg should be equivalent to no max at all
for (String sortF : Arrays.asList("whitespace_stxt", "whitespace_plain_str",
"whitespace_max0_stxt", "whitespace_maxNeg_stxt")) {
assertQ(req("q", searchF + ":ccc", "sort", sortF + " desc, id asc")
, "//*[@numFound='2']"
, "//result/doc[1]/str[@name='id'][.=3]"
, "//result/doc[2]/str[@name='id'][.=1]"
);
assertQ(req("q", searchF + ":ccc", "sort", sortF + " asc, id desc")
, "//*[@numFound='2']"
, "//result/doc[1]/str[@name='id'][.=1]"
, "//result/doc[2]/str[@name='id'][.=3]"
);
}
}
// sorting on a maxChars limited fields should force tie breaker
for (String dir : Arrays.asList("asc", "desc")) {
// for max3, dir shouldn't matter - should always tie..
assertQ(req("q", "*:*", "sort", "whitespace_max3_stxt "+dir+", id desc") // max3, id desc
, "//*[@numFound='4']"
, "//result/doc[1]/str[@name='id'][.=4]"
, "//result/doc[2]/str[@name='id'][.=3]"
, "//result/doc[3]/str[@name='id'][.=2]"
, "//result/doc[4]/str[@name='id'][.=1]"
);
assertQ(req("q", "*:*", "sort", "whitespace_max3_stxt "+dir+", id asc") // max3, id desc
, "//*[@numFound='4']"
, "//result/doc[1]/str[@name='id'][.=1]"
, "//result/doc[2]/str[@name='id'][.=2]"
, "//result/doc[3]/str[@name='id'][.=3]"
, "//result/doc[4]/str[@name='id'][.=4]"
);
}
assertQ(req("q", "*:*", "sort", "whitespace_max6_stxt asc, id desc") // max6 asc, id desc
, "//*[@numFound='4']"
, "//result/doc[1]/str[@name='id'][.=4]" // no tiebreaker needed
, "//result/doc[2]/str[@name='id'][.=3]"
, "//result/doc[3]/str[@name='id'][.=2]"
, "//result/doc[4]/str[@name='id'][.=1]"
);
assertQ(req("q", "*:*", "sort", "whitespace_max6_stxt asc, id asc") // max6 asc, id desc
, "//*[@numFound='4']"
, "//result/doc[1]/str[@name='id'][.=4]" // no tiebreaker needed
, "//result/doc[2]/str[@name='id'][.=1]"
, "//result/doc[3]/str[@name='id'][.=2]"
, "//result/doc[4]/str[@name='id'][.=3]"
);
assertQ(req("q", "*:*", "sort", "whitespace_max6_stxt desc, id desc") // max6 desc, id desc
, "//*[@numFound='4']"
, "//result/doc[1]/str[@name='id'][.=3]"
, "//result/doc[2]/str[@name='id'][.=2]"
, "//result/doc[3]/str[@name='id'][.=1]"
, "//result/doc[4]/str[@name='id'][.=4]" // no tiebreaker needed
);
assertQ(req("q", "*:*", "sort", "whitespace_max6_stxt desc, id asc") // max6 desc, id desc
, "//*[@numFound='4']"
, "//result/doc[1]/str[@name='id'][.=1]"
, "//result/doc[2]/str[@name='id'][.=2]"
, "//result/doc[3]/str[@name='id'][.=3]"
, "//result/doc[4]/str[@name='id'][.=4]" // no tiebreaker needed
);
// sanity check that the default max is working....
assertU(adoc("id","5", "whitespace_stxt", BIG_CONST + " aaa zzz"));
assertU(adoc("id","6", "whitespace_stxt", BIG_CONST + " bbb zzz "));
assertU(commit());
// for these fields, the tie breaker should be the only thing that matters, regardless of direction...
for (String sortF : Arrays.asList("whitespace_stxt", "whitespace_nois_stxt")) {
for (String dir : Arrays.asList("asc", "desc")) {
assertQ(req("q", "whitespace_stxt:zzz", "sort", sortF + " " + dir + ", id asc")
, "//*[@numFound='2']"
, "//result/doc[1]/str[@name='id'][.=5]"
, "//result/doc[2]/str[@name='id'][.=6]"
);
assertQ(req("q", "whitespace_stxt:zzz", "sort", sortF + " " + dir + ", id desc")
, "//*[@numFound='2']"
, "//result/doc[1]/str[@name='id'][.=6]"
, "//result/doc[2]/str[@name='id'][.=5]"
);
}
}
}
/**
* test how various permutations of useDocValuesAsStored and maxCharsForDocValues interact
*/
public void testUseDocValuesAsStored() throws Exception {
ignoreException("when useDocValuesAsStored=true \\(length=");
// first things first...
// unlike most field types, SortableTextField should default to useDocValuesAsStored==false
// (check a handful that should have the default behavior)
for (String n : Arrays.asList("keyword_stxt", "whitespace_max0_stxt", "whitespace_max6_stxt")) {
{
FieldType ft = h.getCore().getLatestSchema().getFieldTypeByName(n);
assertEquals("type " + ft.getTypeName() + " should not default to useDocValuesAsStored",
false, ft.useDocValuesAsStored()) ;
}
{
SchemaField sf = h.getCore().getLatestSchema().getField(n);
assertEquals("field " + sf.getName() + " should not default to useDocValuesAsStored",
false, sf.useDocValuesAsStored()) ;
}
}
// but it should be possible to set useDocValuesAsStored=true explicitly on types...
int num_types_found = 0;
for (Map.Entry<String,FieldType> entry : h.getCore().getLatestSchema().getFieldTypes().entrySet()) {
if (entry.getKey().endsWith("_has_usedvs")) {
num_types_found++;
FieldType ft = entry.getValue();
assertEquals("type " + ft.getTypeName() + " has unexpected useDocValuesAsStored value",
true, ft.useDocValuesAsStored()) ;
}
}
assertEquals("sanity check: wrong number of *_has_usedvs types found -- schema changed?",
2, num_types_found);
// ...and it should be possible to set/override useDocValuesAsStored=true on fields...
int num_fields_found = 0;
List<String> xpaths = new ArrayList<>(42);
for (Map.Entry<String,SchemaField> entry : h.getCore().getLatestSchema().getFields().entrySet()) {
if (entry.getKey().endsWith("_usedvs")) {
num_fields_found++;
final SchemaField sf = entry.getValue();
final String name = sf.getName();
// some sanity check before we move on with the rest of our testing...
assertFalse("schema change? field should not be stored=true: " + name, sf.stored());
final boolean usedvs = name.endsWith("_has_usedvs");
assertTrue("schema change broke assumptions: field must be '*_has_usedvs' or '*_negates_usedvs': " +
name, usedvs ^ name.endsWith("_negates_usedvs"));
final boolean max6 = name.startsWith("max6_");
assertTrue("schema change broke assumptions: field must be 'max6_*' or 'max0_*': " +
name, max6 ^ name.startsWith("max0_"));
assertEquals("Unexpected useDocValuesAsStored value for field: " + name,
usedvs, sf.useDocValuesAsStored()) ;
final String docid = ""+num_fields_found;
if (usedvs && max6) {
// if useDocValuesAsStored==true and maxCharsForDocValues=N then longer values should fail
final String doc = adoc("id", docid, name, "apple pear orange");
SolrException ex = expectThrows(SolrException.class, () -> { assertU(doc); });
for (String expect : Arrays.asList("field " + name,
"length=17",
"useDocValuesAsStored=true",
"maxCharsForDocValues=6")) {
assertTrue("exception must mention " + expect + ": " + ex.getMessage(),
ex.getMessage().contains(expect));
}
} else {
// otherwise (useDocValuesAsStored==false *OR* maxCharsForDocValues=0) any value
// should be fine when adding a doc and we should be able to search for it later...
final String val = docid + " apple pear orange " + BIG_CONST;
assertU(adoc("id", docid, name, val));
String doc_xpath = "//result/doc[str[@name='id'][.='"+docid+"']]";
if (usedvs) {
// ...and if it *does* usedvs, then we should defnitely see our value when searching...
doc_xpath = doc_xpath + "[str[@name='"+name+"'][.='"+val+"']]";
} else {
// ...but if not, then we should definitely not see any value for our field...
doc_xpath = doc_xpath + "[not(str[@name='"+name+"'])]";
}
xpaths.add(doc_xpath);
}
}
}
assertEquals("sanity check: wrong number of *_usedvs fields found -- schema changed?",
6, num_fields_found);
// check all our expected docs can be found (with the expected values)
assertU(commit());
xpaths.add("//*[@numFound='"+xpaths.size()+"']");
assertQ(req("q", "*:*", "fl", "*"), xpaths.toArray(new String[xpaths.size()]));
}
/**
* tests that a SortableTextField using KeywordTokenzier (w/docValues) behaves exactly the same as
* StrFields that it's copied to for quering and sorting
*/
public void testRandomStrEquivalentBehavior() throws Exception {
final List<String> test_fields = Arrays.asList("keyword_stxt", "keyword_dv_stxt",
"keyword_s_dv", "keyword_s");
// we use embedded client instead of assertQ: we want to compare the responses from multiple requests
@SuppressWarnings("resource") final SolrClient client = new EmbeddedSolrServer(h.getCore());
final int numDocs = atLeast(100);
final int magicIdx = TestUtil.nextInt(random(), 1, numDocs);
String magic = null;
for (int i = 1; i <= numDocs; i++) {
// ideally we'd test all "realistic" unicode string, but EmbeddedSolrServer uses XML request writer
// and has no option to change this so ctrl-characters break the request
final String val = TestUtil.randomSimpleString(random(), 100);
if (i == magicIdx) {
magic = val;
}
assertEquals(0, client.add(sdoc("id", ""+i, "keyword_stxt", val)).getStatus());
}
assertNotNull(magic);
assertEquals(0, client.commit().getStatus());
// query for magic term should match same doc regardless of field (reminder: keyword tokenizer)
// (we need the filter in the unlikely event that magic value with randomly picked twice)
for (String f : test_fields) {
final SolrDocumentList results = client.query(params("q", "{!field f="+f+" v=$v}",
"v", magic,
"fq", "id:" + magicIdx )).getResults();
assertEquals(f + ": Query ("+magic+") filtered by id: " + magicIdx + " ==> " + results,
1L, results.getNumFound());
final SolrDocument doc = results.get(0);
assertEquals(f + ": Query ("+magic+") filtered by id: " + magicIdx + " ==> " + doc,
""+magicIdx, doc.getFieldValue("id"));
assertEquals(f + ": Query ("+magic+") filtered by id: " + magicIdx + " ==> " + doc,
magic, doc.getFieldValue(f));
}
// do some random id range queries using all 3 fields for sorting. results should be identical
final int numQ = atLeast(10);
for (int i = 0; i < numQ; i++) {
final int hi = TestUtil.nextInt(random(), 1, numDocs-1);
final int lo = TestUtil.nextInt(random(), 1, hi);
final boolean fwd = random().nextBoolean();
SolrDocumentList previous = null;
String prevField = null;
for (String f : test_fields) {
final SolrDocumentList results = client.query(params("q","id_i:["+lo+" TO "+hi+"]",
"sort", f + (fwd ? " asc" : " desc") +
// secondary on id for determinism
", id asc")
).getResults();
assertEquals(results.toString(), (1L + hi - lo), results.getNumFound());
if (null != previous) {
assertEquals(prevField + " vs " + f,
previous.getNumFound(), results.getNumFound());
for (int d = 0; d < results.size(); d++) {
assertEquals(prevField + " vs " + f + ": " + d,
previous.get(d).getFieldValue("id"),
results.get(d).getFieldValue("id"));
assertEquals(prevField + " vs " + f + ": " + d,
previous.get(d).getFieldValue(prevField),
results.get(d).getFieldValue(f));
}
}
previous = results;
prevField = f;
}
}
}
}