/* Copyright 2013 Red Hat, Inc. and/or its affiliates. This file is part of lightblue. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.redhat.lightblue.mindex; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.HashSet; import java.util.Iterator; import java.util.Comparator; import java.util.List; import com.redhat.lightblue.util.JsonDoc; import com.redhat.lightblue.util.Tuples; /** * This is an in-memory document index implementation using a map. * * Before using the index, the caller should decide what type of * queries will be run on the index, and build a key spec to be used * for index keys. The index is declare dusing a KeySpec. A KeySpec * can be a SimpleKeySpec for a top-level value field: * * <pre> * { field: f1, op:=, rvalue:str } -> * new SimpleKeySpec(md.resolve("f1")) * </pre> * * Or a simple value field under an array: * * <pre> * { field: arr.*.f1, op:=, rvalue:str} -> * new SimpleKeySpec(md.resolve("arr.*.f1")) * </pre> * * If the field is accessed using an elem-match search, and there are * multiple predicates involved under the same array, then an * ArrayKeySpec must be used: * * <pre> * { array:arr, elemMatch: { $and: [ {field:f1,op:=,rvalue:str1}, {field:f2,op:=,rvalue:str2}]}} -> * arrMd=md.resolve("arr") * new ArrayKeySpec(arrMd,new KeySpec[] { * new SimpleKeySpec(arrMd.getElement().resolve("f1")), * new SimpleKeySpec(arrMd.getElement().resolve("f2"))}); * </pre> * * For clauses combined with AND, use CompositeKeySpec. * * Once the index is constructed with a key spec, add docs using add() * method. This will create index entries for each doc using the * keyspec. * * For lookups, the caller must create a lookup spec in the same * structure as the key spec. A lookup spec composed ot only Value * lookups and multi-value lookups is a simple lookup. If a range * lookup spec or prefix lookup spec is used, the lookup becomes an * index scan. * */ public class MemDocIndex { /** * The index keys are ordered based on the keyFields array */ private final HashMap<Key,Set<JsonDoc>> documents; public final KeySpec keySpec; /** * Constructs a document index using the given key spec */ public MemDocIndex(KeySpec keys) { this.keySpec=keys; this.documents=new HashMap<Key,Set<JsonDoc>>(); } /** * Clear the indexed documents */ public void clear() { documents.clear(); } /** * Add the document to the index */ public void add(JsonDoc doc) { Set<Key> keys=keySpec.extract(doc,null); for(Key k:keys) { Set<JsonDoc> docSet=documents.get(k); if(docSet==null) documents.put(k,docSet=new HashSet<>()); docSet.add(doc); } } public Set<JsonDoc> find(LookupSpec spec) { Set<JsonDoc> results=new HashSet<>(); if(spec.multiValued()) { Tuples<Object> tuples=new Tuples<>(); spec.iterate(tuples); for(Iterator<List<Object>> itr=tuples.tuples();itr.hasNext();) { List<Object> tuple=itr.next(); LookupSpec singleValueSpec=spec.next(tuple.iterator()); findSingleValue(singleValueSpec,results); } } else { findSingleValue(spec,results); } return results; } private void findSingleValue(LookupSpec spec,Set<JsonDoc> results) { if(spec.needsScan()) { indexScan(spec,results); } else { indexLookup(spec,results); } } private void indexScan(LookupSpec spec,Set<JsonDoc> results) { for(Map.Entry<Key,Set<JsonDoc>> entry:documents.entrySet()) { Key indexKey=entry.getKey(); if(spec.matches(indexKey)) results.addAll(entry.getValue()); } } private void indexLookup(LookupSpec spec,Set<JsonDoc> results) { Set<JsonDoc> docs=documents.get(spec.buildKey()); if(docs!=null) results.addAll(docs); } @Override public String toString() { StringBuilder bld=new StringBuilder(); for(Map.Entry<Key,Set<JsonDoc>> entry:documents.entrySet()) { bld.append(entry.getKey()).append(":").append("["); for(JsonDoc d:entry.getValue()) { bld.append(' '); bld.append(Integer.toHexString(d.hashCode())); } bld.append("]\n"); } return bld.toString(); } }