Запрещенная оговорка Lucene размыта там, где не должна

Я пишу фильтр на основе Lucene: у меня есть некоторые результаты от API, и я хотел бы принудительно обеспечить соответствие результатов определенному запросу (иногда API не работает). Поскольку результаты получаются из API, я в основном сохраняю их в оперативной памяти, индексирую и фильтрую. Если Lucene найдет документ в моем индексе, я считаю, что этот документ в порядке, если нет, он будет отфильтрован.

Иногда я хочу, чтобы это было нечетко, иногда нет. Есть переключатель приближения. Поэтому я использую StandardAnalyzer для приближения = false и BrazilianAnalyzer для приближения = true. Ok?

Проблема в том, что BrazilianAnalyzer аппроксимирует термины отрицания, что, на мой взгляд, не лучший подход. Например, если мне нужно "greve -trabalhadores", документ с "greve do trabalho" соответствует запросу, но не должен. Если я использую StandardAnalyzer, он работает нормально, если я использую BrazilianAnalyzer, он будет игнорировать все, что содержит «trabalh», из-за основы.

Мое решение состояло в том, чтобы переписать запрещенные предложения с помощью StandardAnalyzer, которые не делают определение основы/нечеткости. Итак, часть запроса, которая запрещена, я буду использовать StandardAnalyzer, другая часть будет использовать либо BrazilianAnalyzer, либо Standard (в зависимости от переключателя аппроксимации).

Проблема в том, что он не работает (иногда).

Небольшой тест моего кода выглядит следующим образом:

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.br.BrazilianAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.analysis.util.CharArraySet;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.IntField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;

public class Lucene {

    private static Logger log = Logger.getLogger(Lucene.class.getName());

    private String[] fields = new String[] { "title" };

    private BrazilianAnalyzer analyzerBrazil = new BrazilianAnalyzer(Version.LUCENE_41, new CharArraySet(Version.LUCENE_41, Collections.emptyList(), true));
    private StandardAnalyzer analyzerStandard = new StandardAnalyzer(Version.LUCENE_41, new CharArraySet(Version.LUCENE_41, Collections.emptyList(), true));

    private MultiFieldQueryParser parserBrazil = new MultiFieldQueryParser(Version.LUCENE_41, fields , analyzerBrazil);
    private MultiFieldQueryParser parserStandard = new MultiFieldQueryParser(Version.LUCENE_41, fields , analyzerStandard);

    public void filter(String query, boolean fuzzy, List<Result> results) {
        Directory index = null;

        if (results == null || results.size() == 0) {
            return;
        }

        try {
            Analyzer analyzer = fuzzy ? analyzerBrazil : analyzerStandard;
            Query q = fuzzy ? parserBrazil.parse(query) : parserStandard.parse(query);

            // terms to ignore/prohibited shoudn't be fuzzyfied...
            if (fuzzy) {
                Query queryNoFuzzy = parserStandard.parse(query);

                if (q instanceof BooleanQuery) {
                    BooleanClause[] clauses = ((BooleanQuery)queryNoFuzzy).getClauses();
                    if (clauses != null && clauses.length > 0) {
                        BooleanClause clause = null;
                        for (int i = 0; i < clauses.length; i++) {
                            clause = clauses[i];
                            if (clause.isProhibited()) {
                                ((BooleanQuery)q).clauses().set(i, clause);
                            }
                        }
                    }
                }
            }

            log.info(q.toString());

            index = index(results, analyzer);
            IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(index));
            TopDocs resultsFoundDocs = searcher.search(q, results.size());

            List<Result> resultsFound = new ArrayList<Result>();
            for (ScoreDoc resultadoFiltro : resultsFoundDocs.scoreDocs) {
                log.info("Score " + resultadoFiltro.score);
                resultsFound.add(results.get(Integer.parseInt(searcher.doc(resultadoFiltro.doc).get("index"))));
            }

            for (Result result : results) {
                if (!resultsFound.contains(result)) {
                    result.setFiltered(true);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                index.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private Directory index(List<Result> resultados, Analyzer analyzer) {
        try {
            Directory index = new RAMDirectory();
            IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_41, analyzer);

            IndexWriter writer = new IndexWriter(index, config);
            indexResults(writer, analyzer, resultados);

            return index;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private void indexResults(IndexWriter w, Analyzer analyzer, List<Result> resultados) throws IOException {
        try {
            Document resultado = null;

            for (int i = 0; i < resultados.size(); i++) {
                resultado = new Document();

                resultado.add(new TextField(fields[0], resultados.get(i).getTitle(), Field.Store.YES));
                resultado.add(new IntField("index", i, Field.Store.YES));

                w.addDocument(resultado, analyzer);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            w.close();
        }
    }   

    public static void main(String[] args) {
        List<Result> ocs = new ArrayList<Result>();

        Result rb = new Result("Vivo Celular - não instalação do produto");
        ocs.add(rb);

        System.out.println("ITEMS ____________________________");
        for (Result oc : ocs) {
            System.out.println(oc.getTitle());
        }
        System.out.println("ITEMS ____________________________");

        String query = "vivo -celular";

        System.out.println("\n >> QUERY " + query);

        new Lucene().filter(query, true, ocs);

        System.out.println("\nFOUND ____________________________");
        for (Result oc : ocs) {
            if (!oc.getFiltered()) {
                System.out.println(oc.getTitle());
            }
        }
        System.out.println("FOUND ____________________________");
    }

}

class Result {

    private String title;
    private Boolean filtered = false;

    public Result(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Boolean getFiltered() {
        return filtered;
    }

    public void setFiltered(Boolean filtered) {
        this.filtered = filtered;
    }

}

Существует простой документ со следующим заголовком «Vivo Celular - não instalação do produto». Я запрошу "vivo -celular", поэтому, поскольку документ содержит celular, он не должен возвращаться вызовом searcher.search(q, results.size());. Это происходит только при включенном приближении, даже если он печатает запрос, чтобы связать только "vivo" с "viv" (запрос "(title:viv) -(title:celular)").

Это правильно???

Я использую версию 4.2.2. Это происходит и на 4.1.0.

Может ли кто-нибудь просветить меня по этому поводу?

Спасибо заранее.


person Ivan    schedule 18.03.2013    source источник


Ответы (3)


Я считаю, что проблема заключается в том, что вы путаете анализаторы.

Если для вашего флага fuzzy установлено значение true, вы индексируете документы, используя BrazilianAnalyzer (который определяет основу), но вы пытаетесь переписать часть запроса с некоторыми неосновными терминами, используя StandardAnalyzer.

Другими словами, даже если у вас есть правильный запрос "(title:viv) -(title:celular)", термин celular, скорее всего, был найден в каталоге (это потому, что вы проиндексировали с StandardAnalyzer), и поэтому предложение -celular никогда не будет работать.

Возможный обходной путь для этого, хотя и добавляет некоторые накладные расходы, состоит в том, чтобы поддерживать два разных индекса: базовый и неосновной. Чтобы сделать это легко, вы можете создать два разных поля, скажем, titleStandardAnalyzer) и stemmedtitleBrazilianAnalyzer). Используйте PerFieldAnalyzerWrapper. создать анализатор, работающий на двух разных полях. Затем вы можете переписать свой запрос как stemmedtitle:viv -title:celular, и это должно помочь.

person Emanuele Bezzi    schedule 19.03.2013
comment
Эмануэле, я нашел лучший способ сделать это, как вы можете видеть в моем собственном ответе! В любом случае, ваш совет был очень полезен! Спасибо. - person Ivan; 20.03.2013

Для тех, кто ищет ответ:

Я нашел лучший (и правильный) способ сделать это: бразильский анализатор (и большинство анализаторов) имеют перегруженный конструктор, который принимает стоп-слова и слова, которые не должны быть выделены (или нечетки). Итак, что вам нужно сделать, это:

Создайте свой Анализатор следующим образом:

new BrazilianAnalyzer(Version.LUCENE_41, stops, getNoStemmingSet(query));

Тогда getNoStemmingSet будет:

private CharArraySet getNoStemmingSet(String query) {
    if (query != null && !query.contains(" -")) {
        return new CharArraySet(Version.LUCENE_41, Collections.emptyList(), true);
    }

    List<String> proihibitedClauses = new ArrayList<String>();

    for (String clause : query.split("\\s")) {
        if (clause.startsWith("-")) {
            proihibitedClauses.add(clause.replace("-", ""));
        }
    }

    return new CharArraySet(Version.LUCENE_41, proihibitedClauses, true);
}

Поэтому, если запрос содержит запрещенные предложения (знак минус), мы берем каждое из них и игнорируем создание нового CharArraySet.

Stops — это еще один CharArraySet, который вы хотели бы использовать в качестве стоп-слов. Если вам не нужны собственные стоп-слова, вы можете использовать их по умолчанию:

BrazilianAnalyzer.getDefaultStopSet()

Вот и все.

person Ivan    schedule 20.03.2013
comment
Интересное решение. Две заботы. Не похоже, чтобы это сработало для чего-то более сложного, чем простой один термин в поле по умолчанию (т.е. что происходит для query -fieldname:prohibition или фразового запроса query -"long prohibition"). Кроме того, как указал @EmanueleBezzi, это должно удалить указанные вами ложные срабатывания, но ложные отрицания могут быть проблемой, поскольку поля, которые вы ищете, предположительно, были ограничены. Если при индексации все экземпляры vivo будут заменены на viv, то запрос с -vivo не будет иметь никакого эффекта. - person femtoRgon; 20.03.2013
comment
Вы правы, но мои запросы должны быть обычными поисковыми запросами. Например, он не будет содержать fieldname:something. Запросы будут содержать только искомые слова с И, ИЛИ и -. В любом случае, я буду работать над своим REGEX, чтобы рассмотреть ваш пример с кавычками! - person Ivan; 20.03.2013

Для тех, кто хочет использовать -"какую-то фразу", этот код должен помочь (не проверял его так хорошо, но вы можете попробовать):

private CharArraySet getNoStemmingSet(String query) {

if (query != null && !query.contains(" -")) {
    return new CharArraySet(Version.LUCENE_41, Collections.emptyList(), true);
}

List<String> proihibitedClauses = new ArrayList<String>();
String[] quotedWords = null;

for (int i = 0; i < query.length(); i++) {
    if (query.charAt(i) == '-' && query.charAt(i+1) == '\"') {
        quotedWords = query.substring(i+2, query.indexOf('\"', i+2)).split("\\s");
        for (String quotedWord : quotedWords) {
            proihibitedClauses.add(quotedWord);
        }
    } else if (query.charAt(i) == '-') {
        if (query.indexOf(' ', i+1) > 0) {
            proihibitedClauses.add(query.substring(i+1, query.indexOf(' ', i+1)));
        } else {
            proihibitedClauses.add(query.substring(i+1, query.length()));
        }
    } else {
        continue;
    }
}

return new CharArraySet(Version.LUCENE_41, proihibitedClauses, true);
}
person Ivan    schedule 20.03.2013