Solr:使用EdgeNGramFilterFactory进行精确短语查询

7
在Solr(3.3)中,是否可以通过使用EdgeNGramFilterFactory使字段逐字搜索,并且对短语查询敏感?
例如,我正在寻找一个字段,如果包含“contrat informatique”,则用户输入以下内容时将找到该字段:
- contrat - informatique - contr - informa - “contrat informatique” - “contrat info”
目前,我做了类似于这样的事情:
<fieldtype name="terms" class="solr.TextField">
    <analyzer type="index">
        <charFilter class="solr.MappingCharFilterFactory" mapping="mapping-ISOLatin1Accent.txt"/>
        <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1"/>
        <tokenizer class="solr.LowerCaseTokenizerFactory"/>
        <filter class="solr.EdgeNGramFilterFactory" minGramSize="2" maxGramSize="15" side="front"/>
    </analyzer>
    <analyzer type="query">
        <charFilter class="solr.MappingCharFilterFactory" mapping="mapping-ISOLatin1Accent.txt"/>
        <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1"/>
        <tokenizer class="solr.LowerCaseTokenizerFactory"/>
    </analyzer>
</fieldtype>

...但是它在短语查询上失败了。

当我在solr管理页面的模式分析器中查看时,发现"contrat informatique"生成了以下令牌:

[...] contr contra contrat in inf info infor inform [...]

因此,“contrat in”(连续标记)可以与查询一起使用,但“contrat inf”不行(因为这两个标记是分开的)。

我相信任何类型的词干处理都可以与短语查询一起使用,但在使用EdgeNGramFilterFactory之前,我找不到正确的标记器或过滤器。

4个回答

6

由于默认情况下查询偏移参数为0,因此精确短语搜索无法正常工作。 搜索短语“'Hello World'”时,它将搜索具有连续位置的术语。 我希望EdgeNGramFilter有一个参数来控制输出位置,这看起来像是一个旧的问题

通过将qs参数设置为非常高的值(大于ngrams之间的最大距离),您可以获得短语。这部分解决了允许短语的问题,但不是精确匹配,也会找到排列。例如,搜索“contrat informatique”将匹配文本“...contract abandoned. Informatique...”

enter image description here

为了支持“精确短语查询”,我最终使用了为ngrams单独创建字段
所需步骤:
定义单独的字段类型以索引常规值和ngrams。
<fieldType name="text" class="solr.TextField" omitNorms="false">
  <analyzer>
    <tokenizer class="solr.StandardTokenizerFactory"/>
    <filter class="solr.LowerCaseFilterFactory"/>
  </analyzer>
</fieldType>

<fieldType name="ngrams" class="solr.TextField" omitNorms="false">
  <analyzer type="index">
    <tokenizer class="solr.StandardTokenizerFactory"/>
    <filter class="solr.LowerCaseFilterFactory"/>
    <filter class="solr.EdgeNGramFilterFactory" minGramSize="2" maxGramSize="15" side="front"/>
  </analyzer>
  <analyzer type="query">
    <tokenizer class="solr.StandardTokenizerFactory"/>
    <filter class="solr.LowerCaseFilterFactory"/>
  </analyzer>
</fieldType>

告诉solr在索引时复制字段
您可以为每个字段定义单独的ngrams反射:
<field name="contact_ngrams" type="ngrams" indexed="true" stored="false"/>
<field name="product_ngrams" type="ngrams" indexed="true" stored="false"/>
<copyField source="contact_text" dest="contact_ngrams"/>
<copyField source="product_text" dest="product_ngrams"/>

或者您可以将所有的ngram放入一个字段中:

<field name="heap_ngrams" type="ngrams" indexed="true" stored="false"/>
<copyField source="*_text" dest="heap_ngrams"/>

请注意,在这种情况下,您将无法分离增强器。
最后一件事是在查询中指定ngrams字段和增强器。 一种方法是配置您的应用程序。 另一种方法是在solrconfig.xml中指定“appends”参数。
   <lst name="appends">
     <str name="qf">heap_ngrams</str>
   </lst>

2
由于我无法像Jayendra Patil建议的那样正确使用PositionFilter(PositionFilter使任何查询都成为OR布尔查询),因此我采用了不同的方法。仍然使用EdgeNGramFilter,我添加了用户键入的每个关键字都是强制性的事实,并禁用了所有短语。因此,如果用户要求"cont info",它会转换为+cont +info。这比真正的短语更宽容,但它成功地做到了我想要的事情(并且不会返回只有两个术语中的一个的结果)。这种解决方法唯一的缺点是结果中的术语可以被排列(因此还会找到具有“informatique contrat”的文档),但这并不是大问题。

你好,Xavier。你能否解释一下你是如何将“cont info”转换为+cont+info的?是否有现成的工具类可以实现这个功能?还是只需要手动识别双引号并进行转换?我正在尝试解决这个问题:http://stackoverflow.com/questions/37033381/solr-search-field-best-practices - wattale
这是一个手动操作,需要查找双引号并添加加号。我没有找到任何可以自动化此过程的东西 :-/ - Xavier Portebois
感谢回复Xavier,对我来说,在爬取了这么多的内容后,也找不到一个现成的解决方案。我认为手动做这个任务就像是重复造轮子。但是我想手动操作可能是唯一可行的选择:| - wattale

1

这是我的想法 -
为了使ngrams成为短语匹配,每个单词生成的标记位置应该相同。
我检查了边缘gram过滤器,它会增加标记,并且没有找到任何防止它的参数。
有一个位置过滤器可用,它将标记位置保持为与开头相同的标记。
因此,如果使用以下配置,则所有标记都位于相同的位置,并且与短语查询匹配(匹配作为短语的相同标记位置)。
我通过分析工具进行了检查,查询匹配了。

所以你可能想尝试一下这个提示:-

<analyzer type="index">
    <tokenizer class="solr.WhitespaceTokenizerFactory" />
    <charFilter class="solr.MappingCharFilterFactory" 
            mapping="mapping-ISOLatin1Accent.txt" />
    <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" 
            generateNumberParts="1" catenateWords="1" catenateNumbers="1" 
            catenateAll="0" splitOnCaseChange="1"/>
    <filter class="solr.LowerCaseFilterFactory" />
    <filter class="solr.EdgeNGramFilterFactory" minGramSize="2" 
            maxGramSize="15" side="front"/>
    <filter class="solr.PositionFilterFactory" />
</analyzer>

这个想法很好,但似乎无论如何都不起作用 :-/ 即使我通过管理分析工具得到匹配项,真正的查询也返回空结果(可能是因为在分析工具中,突出显示令牌的方式不涉及短语)。此外,如维基所述,PositionFilter 使查询变成了布尔查询,因此“contrat informatique”甚至“+contrat +informatique”都会返回包含“contrat”但没有“informatique”的文档,因为默认运算符是 OR。一个可能的替代方案是将查询转换为 +contrat +informatique,我想。 - Xavier Portebois

1

我已经对EdgeNGramFilter进行了修复,以便不再增加标记内的位置:

    public class CustomEdgeNGramTokenFilterFactory extends TokenFilterFactory {
    private int maxGramSize = 0;

    private int minGramSize = 0;

    @Override
    public void init(Map<String, String> args) {
        super.init(args);
        String maxArg = args.get("maxGramSize");
        maxGramSize = (maxArg != null ? Integer.parseInt(maxArg)
                : EdgeNGramTokenFilter.DEFAULT_MAX_GRAM_SIZE);

        String minArg = args.get("minGramSize");
        minGramSize = (minArg != null ? Integer.parseInt(minArg)
                : EdgeNGramTokenFilter.DEFAULT_MIN_GRAM_SIZE);

    }

    @Override
    public CustomEdgeNGramTokenFilter create(TokenStream input) {
        return new CustomEdgeNGramTokenFilter(input, minGramSize, maxGramSize);
    }
}

public class CustomEdgeNGramTokenFilter extends TokenFilter {
    private final int minGram;
    private final int maxGram;
    private char[] curTermBuffer;
    private int curTermLength;
    private int curGramSize;

    private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
    private final OffsetAttribute offsetAtt = addAttribute(OffsetAttribute.class);
    private final PositionIncrementAttribute positionIncrementAttribute = addAttribute(PositionIncrementAttribute.class);

    /**
     * Creates EdgeNGramTokenFilter that can generate n-grams in the sizes of the given range
     *
     * @param input   {@link org.apache.lucene.analysis.TokenStream} holding the input to be tokenized
     * @param minGram the smallest n-gram to generate
     * @param maxGram the largest n-gram to generate
     */
    public CustomEdgeNGramTokenFilter(TokenStream input, int minGram, int maxGram) {
        super(input);

        if (minGram < 1) {
            throw new IllegalArgumentException("minGram must be greater than zero");
        }

        if (minGram > maxGram) {
            throw new IllegalArgumentException("minGram must not be greater than maxGram");
        }

        this.minGram = minGram;
        this.maxGram = maxGram;
    }

@Override
public final boolean incrementToken() throws IOException {
    while (true) {
        int positionIncrement = 0;
        if (curTermBuffer == null) {
            if (!input.incrementToken()) {
                return false;
            } else {
                positionIncrement = positionIncrementAttribute.getPositionIncrement();
                curTermBuffer = termAtt.buffer().clone();
                curTermLength = termAtt.length();
                curGramSize = minGram;
            }
        }
        if (curGramSize <= maxGram) {
            if (!(curGramSize > curTermLength         // if the remaining input is too short, we can't generate any n-grams
                    || curGramSize > maxGram)) {       // if we have hit the end of our n-gram size range, quit
                // grab gramSize chars from front
                int start = 0;
                int end = start + curGramSize;
                offsetAtt.setOffset(start, end);
                positionIncrementAttribute.setPositionIncrement(positionIncrement);
                termAtt.copyBuffer(curTermBuffer, start, curGramSize);
                curGramSize++;

                return true;
            }
        }
        curTermBuffer = null;
    }
}

    @Override
    public void reset() throws IOException {
        super.reset();
        curTermBuffer = null;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接