Index: CHANGES.txt =================================================================== --- CHANGES.txt (revision 707958) +++ CHANGES.txt (working copy) @@ -33,9 +33,17 @@ then retrievable via IndexReader.getCommitUserData instance and static methods. (Shalin Shekhar Mangar via Mike McCandless) - 3. LUCENE-1406: Added Arabic analyzer. (Robert Muir via Grant Ingersoll) + 4. LUCENE-1420: Similarity now has a computeNorm method that allows + custom Similarity classes to override how norm is computed. It's + provided a FieldInvertState instance that contains details from + inverting the field. The default impl is boost * + lengthNorm(numTerms), to be backwards compatible. Also added + {set/get}DiscountOverlaps to DefaultSimilarity, to control whether + overlapping tokens (tokens with 0 position increment) should be + counted in lengthNorm. (Andrzej Bialecki via Mike McCandless) + Optimizations Documentation Index: src/java/org/apache/lucene/search/Similarity.java =================================================================== --- src/java/org/apache/lucene/search/Similarity.java (revision 707958) +++ src/java/org/apache/lucene/search/Similarity.java (working copy) @@ -17,6 +17,7 @@ * limitations under the License. */ +import org.apache.lucene.index.FieldInvertState; import org.apache.lucene.index.Term; import org.apache.lucene.util.SmallFloat; @@ -333,6 +334,29 @@ return NORM_TABLE; } + /** + * Compute the normalization value for a field, given the accumulated + * state of term processing for this field (see {@link FieldInvertState}). + * + *

Implementations should calculate a float value based on the field + * state and then return that value. + * + *

For backward compatibility this method by default calls + * {@link #lengthNorm(String, int)} passing + * {@link FieldInvertState#getLength()} as the second argument, and + * then multiplies this value by {@link FieldInvertState#getBoost()}.

+ * + *

WARNING: This API is new and experimental and may + * suddenly change.

+ * + * @param field field name + * @param state current processing state for this field + * @return the calculated float norm + */ + public float computeNorm(String field, FieldInvertState state) { + return (float) (state.getBoost() * lengthNorm(field, state.getLength())); + } + /** Computes the normalization value for a field given the total number of * terms contained in a field. These values, together with field boosts, are * stored in an index and multipled into scores for hits on each field by the @@ -341,10 +365,10 @@ *

Matches in longer fields are less precise, so implementations of this * method usually return smaller values when numTokens is large, * and larger values when numTokens is small. - * - *

That these values are computed under + * + *

Note that the return values are computed under * {@link org.apache.lucene.index.IndexWriter#addDocument(org.apache.lucene.document.Document)} - * and stored then using + * and then stored using * {@link #encodeNorm(float)}. * Thus they have limited precision, and documents * must be re-indexed if this method is altered. Index: src/java/org/apache/lucene/search/SimilarityDelegator.java =================================================================== --- src/java/org/apache/lucene/search/SimilarityDelegator.java (revision 707958) +++ src/java/org/apache/lucene/search/SimilarityDelegator.java (working copy) @@ -1,5 +1,7 @@ package org.apache.lucene.search; +import org.apache.lucene.index.FieldInvertState; + /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -32,6 +34,10 @@ this.delegee = delegee; } + public float computeNorm(String fieldName, FieldInvertState state) { + return delegee.computeNorm(fieldName, state); + } + public float lengthNorm(String fieldName, int numTerms) { return delegee.lengthNorm(fieldName, numTerms); } Index: src/java/org/apache/lucene/search/DefaultSimilarity.java =================================================================== --- src/java/org/apache/lucene/search/DefaultSimilarity.java (revision 707958) +++ src/java/org/apache/lucene/search/DefaultSimilarity.java (working copy) @@ -1,5 +1,7 @@ package org.apache.lucene.search; +import org.apache.lucene.index.FieldInvertState; + /** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -19,6 +21,25 @@ /** Expert: Default scoring implementation. */ public class DefaultSimilarity extends Similarity { + + /** Implemented as + * state.getBoost()*lengthNorm(numTerms), where + * numTerms is {@link FieldInvertState#getLength()} if {@link + * #setDiscountOverlaps} is false, else it's {@link + * FieldInvertState#getLength()} - {@link + * FieldInvertState#getNumOverlap()}. + * + *

WARNING: This API is new and experimental, and may suddenly + * change.

*/ + public float computeNorm(String field, FieldInvertState state) { + final int numTerms; + if (discountOverlaps) + numTerms = state.getLength() - state.getNumOverlap(); + else + numTerms = state.getLength(); + return (float) (state.getBoost() * lengthNorm(field, numTerms)); + } + /** Implemented as 1/sqrt(numTerms). */ public float lengthNorm(String fieldName, int numTerms) { return (float)(1.0 / Math.sqrt(numTerms)); @@ -48,4 +69,26 @@ public float coord(int overlap, int maxOverlap) { return overlap / (float)maxOverlap; } + + // Default false + protected boolean discountOverlaps; + + /** Determines whether overlap tokens (Tokens with + * 0 position increment) are ignored when computing + * norm. By default this is false, meaning overlap + * tokens are counted just like non-overlap tokens. + * + *

WARNING: This API is new and experimental, and may suddenly + * change.

+ * + * @see #computeNorm + */ + public void setDiscountOverlaps(boolean v) { + discountOverlaps = v; + } + + /** @see #setDiscountOverlaps */ + public boolean getDiscountOverlaps() { + return discountOverlaps; + } } Index: src/java/org/apache/lucene/index/DocInverterPerField.java =================================================================== --- src/java/org/apache/lucene/index/DocInverterPerField.java (revision 707958) +++ src/java/org/apache/lucene/index/DocInverterPerField.java (working copy) @@ -39,7 +39,7 @@ final InvertedDocConsumerPerField consumer; final InvertedDocEndConsumerPerField endConsumer; final DocumentsWriter.DocState docState; - final DocInverter.FieldInvertState fieldState; + final FieldInvertState fieldState; public DocInverterPerField(DocInverterPerThread perThread, FieldInfo fieldInfo) { this.perThread = perThread; @@ -134,7 +134,11 @@ Token token = stream.next(localToken); if (token == null) break; - fieldState.position += (token.getPositionIncrement() - 1); + final int posIncr = token.getPositionIncrement(); + fieldState.position += posIncr - 1; + if (posIncr == 0) + fieldState.numOverlap++; + boolean success = false; try { // If we hit an exception in here, we abort Index: src/java/org/apache/lucene/index/NormsWriterPerField.java =================================================================== --- src/java/org/apache/lucene/index/NormsWriterPerField.java (revision 707958) +++ src/java/org/apache/lucene/index/NormsWriterPerField.java (working copy) @@ -36,7 +36,7 @@ byte[] norms = new byte[1]; int upto; - final DocInverter.FieldInvertState fieldState; + final FieldInvertState fieldState; public void reset() { // Shrink back if we are overallocated now: @@ -68,7 +68,7 @@ docIDs = ArrayUtil.grow(docIDs, 1+upto); norms = ArrayUtil.grow(norms, 1+upto); } - final float norm = fieldState.boost * docState.similarity.lengthNorm(fieldInfo.name, fieldState.length); + final float norm = docState.similarity.computeNorm(fieldInfo.name, fieldState); norms[upto] = Similarity.encodeNorm(norm); docIDs[upto] = docState.docID; upto++; Index: src/java/org/apache/lucene/index/TermsHashPerField.java =================================================================== --- src/java/org/apache/lucene/index/TermsHashPerField.java (revision 707958) +++ src/java/org/apache/lucene/index/TermsHashPerField.java (working copy) @@ -30,7 +30,7 @@ final TermsHashPerField nextPerField; final TermsHashPerThread perThread; final DocumentsWriter.DocState docState; - final DocInverter.FieldInvertState fieldState; + final FieldInvertState fieldState; // Copied from our perThread final CharBlockPool charPool; Index: src/java/org/apache/lucene/index/FreqProxTermsWriterPerField.java =================================================================== --- src/java/org/apache/lucene/index/FreqProxTermsWriterPerField.java (revision 707958) +++ src/java/org/apache/lucene/index/FreqProxTermsWriterPerField.java (working copy) @@ -30,7 +30,7 @@ final TermsHashPerField termsHashPerField; final FieldInfo fieldInfo; final DocumentsWriter.DocState docState; - final DocInverter.FieldInvertState fieldState; + final FieldInvertState fieldState; boolean omitTf; public FreqProxTermsWriterPerField(TermsHashPerField termsHashPerField, FreqProxTermsWriterPerThread perThread, FieldInfo fieldInfo) { Index: src/java/org/apache/lucene/index/FieldInvertState.java =================================================================== --- src/java/org/apache/lucene/index/FieldInvertState.java (revision 0) +++ src/java/org/apache/lucene/index/FieldInvertState.java (revision 0) @@ -0,0 +1,100 @@ +/** + * 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.lucene.index; + +import org.apache.lucene.search.Similarity; + +/** + * This class tracks the number and position / offset parameters of terms + * being added to the index. The information collected in this class is + * also used to calculate the normalization factor for a field. + * + *

WARNING: This API is new and experimental, and may suddenly + * change.

+ */ +public final class FieldInvertState { + int position; + int length; + int numOverlap; + int offset; + float boost; + + public FieldInvertState() { + } + + public FieldInvertState(int position, int length, int numOverlap, int offset, float boost) { + this.position = position; + this.length = length; + this.numOverlap = numOverlap; + this.offset = offset; + this.boost = boost; + } + + /** + * Re-initialize the state, using this boost value. + * @param docBoost boost value to use. + */ + void reset(float docBoost) { + position = 0; + length = 0; + numOverlap = 0; + offset = 0; + boost = docBoost; + } + + /** + * Get the last processed term position. + * @return the position + */ + public int getPosition() { + return position; + } + + /** + * Get total number of terms in this field. + * @return the length + */ + public int getLength() { + return length; + } + + /** + * Get the number of terms with positionIncrement == 0. + * @return the numOverlap + */ + public int getNumOverlap() { + return numOverlap; + } + + /** + * Get end offset of the last processed term. + * @return the offset + */ + public int getOffset() { + return offset; + } + + /** + * Get boost value. This is the cumulative product of + * document boost and field boost for all field instances + * sharing the same field name. + * @return the boost + */ + public float getBoost() { + return boost; + } +} Property changes on: src/java/org/apache/lucene/index/FieldInvertState.java ___________________________________________________________________ Name: svn:eol-style + native Index: src/java/org/apache/lucene/index/TermVectorsTermsWriterPerField.java =================================================================== --- src/java/org/apache/lucene/index/TermVectorsTermsWriterPerField.java (revision 707958) +++ src/java/org/apache/lucene/index/TermVectorsTermsWriterPerField.java (working copy) @@ -30,7 +30,7 @@ final TermVectorsTermsWriter termsWriter; final FieldInfo fieldInfo; final DocumentsWriter.DocState docState; - final DocInverter.FieldInvertState fieldState; + final FieldInvertState fieldState; boolean doVectors; boolean doVectorPositions; Index: src/java/org/apache/lucene/index/DocInverterPerThread.java =================================================================== --- src/java/org/apache/lucene/index/DocInverterPerThread.java (revision 707958) +++ src/java/org/apache/lucene/index/DocInverterPerThread.java (working copy) @@ -32,7 +32,7 @@ final Token localToken = new Token(); final DocumentsWriter.DocState docState; - final DocInverter.FieldInvertState fieldState = new DocInverter.FieldInvertState(); + final FieldInvertState fieldState = new FieldInvertState(); // Used to read a string value for a field final ReusableStringReader stringReader = new ReusableStringReader(); Index: src/java/org/apache/lucene/index/DocInverter.java =================================================================== --- src/java/org/apache/lucene/index/DocInverter.java (revision 707958) +++ src/java/org/apache/lucene/index/DocInverter.java (working copy) @@ -92,18 +92,4 @@ public DocFieldConsumerPerThread addThread(DocFieldProcessorPerThread docFieldProcessorPerThread) { return new DocInverterPerThread(docFieldProcessorPerThread, this); } - - final static class FieldInvertState { - int position; - int length; - int offset; - float boost; - - void reset(float docBoost) { - position = 0; - length = 0; - offset = 0; - boost = docBoost; - } - } } Index: src/java/org/apache/lucene/document/AbstractField.java =================================================================== --- src/java/org/apache/lucene/document/AbstractField.java (revision 707958) +++ src/java/org/apache/lucene/document/AbstractField.java (working copy) @@ -98,13 +98,19 @@ *

The boost is multiplied by {@link org.apache.lucene.document.Document#getBoost()} of the document * containing this field. If a document has multiple fields with the same * name, all such values are multiplied together. This product is then - * multipled by the value {@link org.apache.lucene.search.Similarity#lengthNorm(String,int)}, and + * used to compute the norm factor for the field. By + * default, in the {@link + * org.apache.lucene.search.Similarity#computeNorm(String, + * FieldInvertState)} method, the boost value is multipled + * by the {@link + * org.apache.lucene.search.Similarity#lengthNorm(String, + * int)} and then * rounded by {@link org.apache.lucene.search.Similarity#encodeNorm(float)} before it is stored in the * index. One should attempt to ensure that this product does not overflow * the range of that encoding. * * @see org.apache.lucene.document.Document#setBoost(float) - * @see org.apache.lucene.search.Similarity#lengthNorm(String, int) + * @see org.apache.lucene.search.Similarity#computeNorm(String, org.apache.lucene.index.FieldInvertState) * @see org.apache.lucene.search.Similarity#encodeNorm(float) */ public void setBoost(float boost) { Index: src/java/org/apache/lucene/document/Fieldable.java =================================================================== --- src/java/org/apache/lucene/document/Fieldable.java (revision 707958) +++ src/java/org/apache/lucene/document/Fieldable.java (working copy) @@ -17,6 +17,7 @@ */ import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.index.FieldInvertState; import java.io.Reader; import java.io.Serializable; @@ -39,13 +40,18 @@ *

The boost is multiplied by {@link org.apache.lucene.document.Document#getBoost()} of the document * containing this field. If a document has multiple fields with the same * name, all such values are multiplied together. This product is then - * multipled by the value {@link org.apache.lucene.search.Similarity#lengthNorm(String,int)}, and - * rounded by {@link org.apache.lucene.search.Similarity#encodeNorm(float)} before it is stored in the + * used to compute the norm factor for the field. By + * default, in the {@link + * org.apache.lucene.search.Similarity#computeNorm(String, + * FieldInvertState)} method, the boost value is multipled + * by the {@link + * org.apache.lucene.search.Similarity#lengthNorm(String, + * int)} and then rounded by {@link org.apache.lucene.search.Similarity#encodeNorm(float)} before it is stored in the * index. One should attempt to ensure that this product does not overflow * the range of that encoding. * * @see org.apache.lucene.document.Document#setBoost(float) - * @see org.apache.lucene.search.Similarity#lengthNorm(String, int) + * @see org.apache.lucene.search.Similarity#computeNorm(String, FieldInvertState) * @see org.apache.lucene.search.Similarity#encodeNorm(float) */ void setBoost(float boost); Index: contrib/miscellaneous/src/java/org/apache/lucene/misc/SweetSpotSimilarity.java =================================================================== --- contrib/miscellaneous/src/java/org/apache/lucene/misc/SweetSpotSimilarity.java (revision 707958) +++ contrib/miscellaneous/src/java/org/apache/lucene/misc/SweetSpotSimilarity.java (working copy) @@ -19,6 +19,7 @@ import org.apache.lucene.search.Similarity; import org.apache.lucene.search.DefaultSimilarity; +import org.apache.lucene.index.FieldInvertState; import java.util.Map; import java.util.HashMap; @@ -53,6 +54,7 @@ private Map ln_mins = new HashMap(7); private Map ln_maxs = new HashMap(7); private Map ln_steeps = new HashMap(7); + private Map ln_overlaps = new HashMap(7); private float tf_base = 0.0f; private float tf_min = 0.0f; @@ -106,18 +108,67 @@ } /** - * Sets the function variables used by lengthNorm for a specific named field + * Sets the function variables used by lengthNorm for a + * specific named field. + * + * @deprecated Please call {@link #setLengthNormFactors(String, + * int, int, float, boolean)} instead. + * + * @param field field name + * @param min minimum value + * @param max maximum value + * @param steepness steepness of the curve * * @see #lengthNorm */ public void setLengthNormFactors(String field, int min, int max, float steepness) { + setLengthNormFactors(field, min, max, steepness, false); + } + + /** + * Sets the function variables used by lengthNorm for a specific named field. + * + * @param field field name + * @param min minimum value + * @param max maximum value + * @param steepness steepness of the curve + * @param discountOverlaps if true, numOverlapTokens will be + * subtracted from numTokens; if false then + * numOverlapTokens will be assumed to be 0 (see + * {@link DefaultSimilarity#computeNorm(String, FieldInvertState)} for details). + * + * @see #lengthNorm + */ + public void setLengthNormFactors(String field, int min, int max, + float steepness, boolean discountOverlaps) { ln_mins.put(field, new Integer(min)); ln_maxs.put(field, new Integer(max)); ln_steeps.put(field, new Float(steepness)); + ln_overlaps.put(field, new Boolean(discountOverlaps)); } /** + * Implemented as state.getBoost() * + * lengthNorm(fieldName, numTokens) where + * numTokens does not count overlap tokens if + * discountOverlaps is true by default or true for this + * specific field. */ + public float computeNorm(String fieldName, FieldInvertState state) { + final int numTokens; + boolean overlaps = discountOverlaps; + if (ln_overlaps.containsKey(fieldName)) { + overlaps = ((Boolean)ln_overlaps.get(fieldName)).booleanValue(); + } + if (overlaps) + numTokens = state.getLength() - state.getNumOverlap(); + else + numTokens = state.getLength(); + + return state.getBoost() * lengthNorm(fieldName, numTokens); + } + + /** * Implemented as: * * 1/sqrt( steepness * (abs(x-min) + abs(x-max) - (max-min)) + 1 ) Index: contrib/memory/src/java/org/apache/lucene/index/memory/MemoryIndex.java =================================================================== --- contrib/memory/src/java/org/apache/lucene/index/memory/MemoryIndex.java (revision 707958) +++ contrib/memory/src/java/org/apache/lucene/index/memory/MemoryIndex.java (working copy) @@ -41,6 +41,7 @@ import org.apache.lucene.index.TermPositionVector; import org.apache.lucene.index.TermPositions; import org.apache.lucene.index.TermVectorMapper; +import org.apache.lucene.index.FieldInvertState; import org.apache.lucene.search.HitCollector; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; @@ -348,6 +349,7 @@ HashMap terms = new HashMap(); int numTokens = 0; + int numOverlapTokens = 0; int pos = -1; final Token reusableToken = new Token(); for (Token nextToken = stream.next(reusableToken); nextToken != null; nextToken = stream.next(reusableToken)) { @@ -355,7 +357,10 @@ if (term.length() == 0) continue; // nothing to do // if (DEBUG) System.err.println("token='" + term + "'"); numTokens++; - pos += nextToken.getPositionIncrement(); + final int posIncr = nextToken.getPositionIncrement(); + if (posIncr == 0) + numOverlapTokens++; + pos += posIncr; ArrayIntList positions = (ArrayIntList) terms.get(term); if (positions == null) { // term not seen before @@ -372,7 +377,7 @@ // ensure infos.numTokens > 0 invariant; needed for correct operation of terms() if (numTokens > 0) { boost = boost * docBoost; // see DocumentWriter.addDocument(...) - fields.put(fieldName, new Info(terms, numTokens, boost)); + fields.put(fieldName, new Info(terms, numTokens, numOverlapTokens, boost)); sortedFields = null; // invalidate sorted view, if any } } catch (IOException e) { // can never happen @@ -574,6 +579,9 @@ /** Number of added tokens for this field */ private final int numTokens; + /** Number of overlapping tokens for this field */ + private final int numOverlapTokens; + /** Boost factor for hits for this field */ private final float boost; @@ -582,9 +590,10 @@ private static final long serialVersionUID = 2882195016849084649L; - public Info(HashMap terms, int numTokens, float boost) { + public Info(HashMap terms, int numTokens, int numOverlapTokens, float boost) { this.terms = terms; this.numTokens = numTokens; + this.numOverlapTokens = numOverlapTokens; this.boost = boost; } @@ -1067,9 +1076,10 @@ if (fieldName != cachedFieldName || sim != cachedSimilarity) { // not cached? Info info = getInfo(fieldName); int numTokens = info != null ? info.numTokens : 0; - float n = sim.lengthNorm(fieldName, numTokens); + int numOverlapTokens = info != null ? info.numOverlapTokens : 0; float boost = info != null ? info.getBoost() : 1.0f; - n = n * boost; // see DocumentWriter.writeNorms(String segment) + FieldInvertState invertState = new FieldInvertState(0, numTokens, numOverlapTokens, 0, boost); + float n = sim.computeNorm(fieldName, invertState); byte norm = Similarity.encodeNorm(n); norms = new byte[] {norm};